Skip to content

Add support for explicit field encryption (sync client) #4302

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
26397c4
Prepare issue branch.
christophstrobl Feb 11, 2023
01428d5
Hacking
christophstrobl Nov 11, 2022
8295ef7
Enable full encryption of nested documents.
christophstrobl Nov 14, 2022
8c64194
Encrypt collection of simple values
christophstrobl Nov 14, 2022
e571bc2
Encrypt collection of complex types.
christophstrobl Nov 14, 2022
52905be
Some changes that allow reading the alt key from a field
christophstrobl Nov 15, 2022
0dae0ff
Test encryption during update
christophstrobl Nov 16, 2022
a89745a
move inner classes to dedicated package
christophstrobl Feb 11, 2023
a49e1c7
Hacking
christophstrobl Feb 11, 2023
cc7d47d
Move stuff from manual encryption context to clientencryptionconverter
christophstrobl Feb 14, 2023
14ad6c3
move decrypt to converter
christophstrobl Feb 14, 2023
e01365d
so this works, maybe we can move the keyid detection as well
christophstrobl Feb 14, 2023
bab9e66
introduce key provider api
christophstrobl Feb 14, 2023
53d5029
add test for typed aggregation
christophstrobl Feb 14, 2023
1d83fa3
Moooar abstractions
christophstrobl Feb 14, 2023
6b88854
Some polishing
christophstrobl Feb 15, 2023
6008824
Add tests for MongoClientEncryption implementation
christophstrobl Feb 15, 2023
8e1f657
Add mooar tests
christophstrobl Feb 15, 2023
11f03f0
pass on spel context to resolve expressions for key ids
christophstrobl Feb 15, 2023
bb6865d
Additional tests
christophstrobl Feb 22, 2023
fd8ea42
Untangle EncryptionContext from ValueConversionContext
christophstrobl Mar 7, 2023
df061f8
Update documentation
christophstrobl Mar 8, 2023
7c536be
Use property TypeInformation instead of Class for conversion to avoid…
christophstrobl Mar 10, 2023
3d2712a
Polishing.
mp911de Mar 14, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-4284-SNAPSHOT</version>
<packaging>pom</packaging>

<name>Spring Data MongoDB</name>
Expand Down
2 changes: 1 addition & 1 deletion spring-data-mongodb-benchmarks/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-4284-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 1 addition & 1 deletion spring-data-mongodb-distribution/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-4284-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
9 changes: 8 additions & 1 deletion spring-data-mongodb/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.1.0-SNAPSHOT</version>
<version>4.1.x-GH-4284-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down Expand Up @@ -112,6 +112,13 @@
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-crypt</artifactId>
<version>1.6.1</version>
<optional>true</optional>
</dependency>

<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
private static final Set<String> DATA_INTEGRITY_EXCEPTIONS = new HashSet<>(
Arrays.asList("WriteConcernException", "MongoWriteException", "MongoBulkWriteException"));

private static final Set<String> SECURITY_EXCEPTIONS = Set.of("MongoCryptException");

@Nullable
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {

Expand Down Expand Up @@ -131,6 +133,8 @@ public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
return new ClientSessionException(ex.getMessage(), ex);
} else if (MongoDbErrorCodes.isTransactionFailureCode(code)) {
return new MongoTransactionException(ex.getMessage(), ex);
} else if(ex.getCause() != null && SECURITY_EXCEPTIONS.contains(ClassUtils.getShortName(ex.getCause().getClass()))) {
return new PermissionDeniedDataAccessException(ex.getMessage(), ex);
}

return new UncategorizedMongoDbException(ex.getMessage(), ex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,7 @@

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

Expand All @@ -39,7 +30,6 @@
import org.bson.conversions.Bson;
import org.bson.json.JsonReader;
import org.bson.types.ObjectId;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.context.ApplicationContext;
Expand All @@ -51,16 +41,7 @@
import org.springframework.data.annotation.Reference;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.TypeMapper;
import org.springframework.data.mapping.AccessOptions;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.InstanceCreatorMetadata;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PersistentPropertyPathAccessor;
import org.springframework.data.mapping.*;
import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
Expand Down Expand Up @@ -331,7 +312,7 @@ private <R> R doReadProjection(ConversionContext context, Bson bson, EntityProje
PersistentPropertyAccessor<?> convertingAccessor = PropertyTranslatingPropertyAccessor
.create(new ConvertingPropertyAccessor<>(accessor, conversionService), propertyTranslator);
MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(context, documentAccessor,
evaluator);
evaluator, spELContext);

readProperties(context, entity, convertingAccessor, documentAccessor, valueProvider, evaluator,
Predicates.isTrue());
Expand Down Expand Up @@ -367,7 +348,7 @@ String getFieldName(MongoPersistentProperty prop) {
populateProperties(context, mappedEntity, documentAccessor, evaluator, instance);

PersistentPropertyAccessor<?> convertingAccessor = new ConvertingPropertyAccessor<>(accessor, conversionService);
MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(context, documentAccessor, evaluator);
MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(context, documentAccessor, evaluator, spELContext);

readProperties(context, mappedEntity, convertingAccessor, documentAccessor, valueProvider, evaluator,
Predicates.isTrue());
Expand Down Expand Up @@ -529,7 +510,7 @@ private <S> S populateProperties(ConversionContext context, MongoPersistentEntit
ConversionContext contextToUse = context.withPath(currentPath);

MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(contextToUse, documentAccessor,
evaluator);
evaluator, spELContext);

Predicate<MongoPersistentProperty> propertyFilter = isIdentifier(entity).or(isConstructorArgument(entity)).negate();
readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, evaluator, propertyFilter);
Expand Down Expand Up @@ -868,9 +849,9 @@ private void writeProperties(Bson bson, MongoPersistentEntity<?> entity, Persist
dbObjectAccessor.put(prop, null);
}
} else if (!conversions.isSimpleType(value.getClass())) {
writePropertyInternal(value, dbObjectAccessor, prop);
writePropertyInternal(value, dbObjectAccessor, prop, accessor);
} else {
writeSimpleInternal(value, bson, prop);
writeSimpleInternal(value, bson, prop, accessor);
}
}
}
Expand All @@ -887,11 +868,11 @@ private void writeAssociation(Association<MongoPersistentProperty> association,
return;
}

writePropertyInternal(value, dbObjectAccessor, inverseProp);
writePropertyInternal(value, dbObjectAccessor, inverseProp, accessor);
}

@SuppressWarnings({ "unchecked" })
protected void writePropertyInternal(@Nullable Object obj, DocumentAccessor accessor, MongoPersistentProperty prop) {
protected void writePropertyInternal(@Nullable Object obj, DocumentAccessor accessor, MongoPersistentProperty prop, PersistentPropertyAccessor<?> persistentPropertyAccessor) {

if (obj == null) {
return;
Expand All @@ -902,7 +883,13 @@ protected void writePropertyInternal(@Nullable Object obj, DocumentAccessor acce

if (conversions.hasValueConverter(prop)) {
accessor.put(prop, conversions.getPropertyValueConversions().getValueConverter(prop).write(obj,
new MongoConversionContext(prop, this)));
new MongoConversionContext(new PropertyValueProvider<>() {
@Nullable
@Override
public <T> T getPropertyValue(MongoPersistentProperty property) {
return (T) persistentPropertyAccessor.getProperty(property);
}
}, prop, this, spELContext)));
return;
}

Expand Down Expand Up @@ -1234,12 +1221,18 @@ private void writeSimpleInternal(@Nullable Object value, Bson bson, String key)
BsonUtils.addToMap(bson, key, getPotentiallyConvertedSimpleWrite(value, Object.class));
}

private void writeSimpleInternal(@Nullable Object value, Bson bson, MongoPersistentProperty property) {
private void writeSimpleInternal(@Nullable Object value, Bson bson, MongoPersistentProperty property, PersistentPropertyAccessor<?> persistentPropertyAccessor) {
DocumentAccessor accessor = new DocumentAccessor(bson);

if (conversions.hasValueConverter(property)) {
accessor.put(property, conversions.getPropertyValueConversions().getValueConverter(property).write(value,
new MongoConversionContext(property, this)));
new MongoConversionContext(new PropertyValueProvider<>() {
@Nullable
@Override
public <T> T getPropertyValue(MongoPersistentProperty property) {
return (T) persistentPropertyAccessor.getProperty(property);
}
}, property, this, spELContext)));
return;
}

Expand Down Expand Up @@ -1845,6 +1838,7 @@ static class MongoDbPropertyValueProvider implements PropertyValueProvider<Mongo
final ConversionContext context;
final DocumentAccessor accessor;
final SpELExpressionEvaluator evaluator;
final SpELContext spELContext;

/**
* Creates a new {@link MongoDbPropertyValueProvider} for the given source, {@link SpELExpressionEvaluator} and
Expand All @@ -1855,7 +1849,7 @@ static class MongoDbPropertyValueProvider implements PropertyValueProvider<Mongo
* @param evaluator must not be {@literal null}.
*/
MongoDbPropertyValueProvider(ConversionContext context, Bson source, SpELExpressionEvaluator evaluator) {
this(context, new DocumentAccessor(source), evaluator);
this(context, new DocumentAccessor(source), evaluator, null);
}

/**
Expand All @@ -1867,7 +1861,7 @@ static class MongoDbPropertyValueProvider implements PropertyValueProvider<Mongo
* @param evaluator must not be {@literal null}.
*/
MongoDbPropertyValueProvider(ConversionContext context, DocumentAccessor accessor,
SpELExpressionEvaluator evaluator) {
SpELExpressionEvaluator evaluator, SpELContext spELContext) {

Assert.notNull(context, "ConversionContext must no be null");
Assert.notNull(accessor, "DocumentAccessor must no be null");
Expand All @@ -1876,6 +1870,7 @@ static class MongoDbPropertyValueProvider implements PropertyValueProvider<Mongo
this.context = context;
this.accessor = accessor;
this.evaluator = evaluator;
this.spELContext = spELContext;
}

@Nullable
Expand All @@ -1892,7 +1887,7 @@ public <T> T getPropertyValue(MongoPersistentProperty property) {
CustomConversions conversions = context.getCustomConversions();
if (conversions.hasValueConverter(property)) {
return (T) conversions.getPropertyValueConversions().getValueConverter(property).read(value,
new MongoConversionContext(property, context.getSourceConverter()));
new MongoConversionContext(this, property, context.getSourceConverter(), spELContext));
}

ConversionContext contextToUse = context.forProperty(property);
Expand All @@ -1902,7 +1897,7 @@ public <T> T getPropertyValue(MongoPersistentProperty property) {

public MongoDbPropertyValueProvider withContext(ConversionContext context) {

return context == this.context ? this : new MongoDbPropertyValueProvider(context, accessor, evaluator);
return context == this.context ? this : new MongoDbPropertyValueProvider(context, accessor, evaluator, spELContext);
}
}

Expand All @@ -1925,7 +1920,7 @@ class AssociationAwareMongoDbPropertyValueProvider extends MongoDbPropertyValueP
*/
AssociationAwareMongoDbPropertyValueProvider(ConversionContext context, DocumentAccessor source,
SpELExpressionEvaluator evaluator) {
super(context, source, evaluator);
super(context, source, evaluator, MappingMongoConverter.this.spELContext);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

import org.bson.conversions.Bson;
import org.springframework.data.convert.ValueConversionContext;
import org.springframework.data.mapping.model.PropertyValueProvider;
import org.springframework.data.mapping.model.SpELContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
Expand All @@ -29,21 +31,39 @@
*/
public class MongoConversionContext implements ValueConversionContext<MongoPersistentProperty> {

private final PropertyValueProvider<MongoPersistentProperty> accessor; // TODO: generics
private final MongoPersistentProperty persistentProperty;
private final MongoConverter mongoConverter;

public MongoConversionContext(MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) {
@Nullable
private final SpELContext spELContext;

public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) {
this(accessor, persistentProperty, mongoConverter, null);
}

public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
MongoPersistentProperty persistentProperty, MongoConverter mongoConverter, @Nullable SpELContext spELContext) {

this.accessor = accessor;
this.persistentProperty = persistentProperty;
this.mongoConverter = mongoConverter;
this.spELContext = spELContext;
}

@Override
public MongoPersistentProperty getProperty() {
return persistentProperty;
}

@Nullable
public Object getValue(String propertyPath) {
return accessor.getPropertyValue(persistentProperty.getOwner().getRequiredPersistentProperty(propertyPath));
}

@Override
@SuppressWarnings("unchecked")
public <T> T write(@Nullable Object value, TypeInformation<T> target) {
return (T) mongoConverter.convertToMongoType(value, target);
}
Expand All @@ -53,4 +73,9 @@ public <T> T read(@Nullable Object value, TypeInformation<T> target) {
return value instanceof Bson ? mongoConverter.read(target.getType(), (Bson) value)
: ValueConversionContext.super.read(value, target);
}

@Nullable
public SpELContext getSpELContext() {
return spELContext;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ protected Object getMappedValue(Field documentField, Object sourceValue) {
&& converter.getCustomConversions().hasValueConverter(documentField.getProperty())) {
return converter.getCustomConversions().getPropertyValueConversions()
.getValueConverter(documentField.getProperty())
.write(value, new MongoConversionContext(documentField.getProperty(), converter));
.write(value, new MongoConversionContext(null, documentField.getProperty(), converter));
}

if (documentField.isIdField() && !documentField.isAssociation()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core.convert.encryption;

import org.springframework.data.mongodb.core.convert.MongoConversionContext;
import org.springframework.data.mongodb.core.convert.MongoValueConverter;
import org.springframework.data.mongodb.core.encryption.EncryptionContext;

/**
* A specialized {@link MongoValueConverter} for {@literal encryptiong} and {@literal decrypting} properties.
*
* @author Christoph Strobl
* @since 4.1
*/
public interface EncryptingConverter<S, T> extends MongoValueConverter<S, T> {

@Override
default S read(Object value, MongoConversionContext context) {
return decrypt(value, buildEncryptionContext(context));
}

/**
* Decrypt the given encrypted source value within the given {@link EncryptionContext context}.
*
* @param encryptedValue the encrypted source.
* @param context the context to operate in.
* @return never {@literal null}.
*/
S decrypt(Object encryptedValue, EncryptionContext context);

@Override
default T write(Object value, MongoConversionContext context) {
return encrypt(value, buildEncryptionContext(context));
}

/**
* Encrypt the given raw source value within the given {@link EncryptionContext context}.
*
* @param value the encrypted source.
* @param context the context to operate in.
* @return never {@literal null}.
*/
T encrypt(Object value, EncryptionContext context);

/**
* Obtain the {@link EncryptionContext} for a given {@link MongoConversionContext value conversion context}.
*
* @param context the current MongoDB specific {@link org.springframework.data.convert.ValueConversionContext}.
* @return the {@link EncryptionContext} to operate in.
* @see org.springframework.data.convert.ValueConversionContext
*/
EncryptionContext buildEncryptionContext(MongoConversionContext context);
}
Loading