Skip to content

Commit 3b33f90

Browse files
christophstroblmp911de
authored andcommitted
Add support for explicit field encryption.
We now support explicit field encryption using mapped entities through the `@ExplicitEncrypted` annotation. class Person { ObjectId id; @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic, altKeyName = "my-secret-key") String socialSecurityNumber; } Encryption is applied transparently to all mapped entities leveraging the existing converter infrastructure. Original pull request: #4302 Closes: #4284
1 parent 3b7b1ac commit 3b33f90

27 files changed

+2414
-48
lines changed

spring-data-mongodb/pom.xml

+7
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@
112112
<optional>true</optional>
113113
</dependency>
114114

115+
<dependency>
116+
<groupId>org.mongodb</groupId>
117+
<artifactId>mongodb-crypt</artifactId>
118+
<version>1.6.1</version>
119+
<optional>true</optional>
120+
</dependency>
121+
115122
<dependency>
116123
<groupId>io.projectreactor</groupId>
117124
<artifactId>reactor-core</artifactId>

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java

+4
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
6868
private static final Set<String> DATA_INTEGRITY_EXCEPTIONS = new HashSet<>(
6969
Arrays.asList("WriteConcernException", "MongoWriteException", "MongoBulkWriteException"));
7070

71+
private static final Set<String> SECURITY_EXCEPTIONS = Set.of("MongoCryptException");
72+
7173
@Nullable
7274
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
7375

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

136140
return new UncategorizedMongoDbException(ex.getMessage(), ex);

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java

+29-15
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ private <R> R doReadProjection(ConversionContext context, Bson bson, EntityProje
331331
PersistentPropertyAccessor<?> convertingAccessor = PropertyTranslatingPropertyAccessor
332332
.create(new ConvertingPropertyAccessor<>(accessor, conversionService), propertyTranslator);
333333
MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(context, documentAccessor,
334-
evaluator);
334+
evaluator, spELContext);
335335

336336
readProperties(context, entity, convertingAccessor, documentAccessor, valueProvider, evaluator,
337337
Predicates.isTrue());
@@ -367,7 +367,7 @@ String getFieldName(MongoPersistentProperty prop) {
367367
populateProperties(context, mappedEntity, documentAccessor, evaluator, instance);
368368

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

372372
readProperties(context, mappedEntity, convertingAccessor, documentAccessor, valueProvider, evaluator,
373373
Predicates.isTrue());
@@ -529,7 +529,7 @@ private <S> S populateProperties(ConversionContext context, MongoPersistentEntit
529529
ConversionContext contextToUse = context.withPath(currentPath);
530530

531531
MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(contextToUse, documentAccessor,
532-
evaluator);
532+
evaluator, spELContext);
533533

534534
Predicate<MongoPersistentProperty> propertyFilter = isIdentifier(entity).or(isConstructorArgument(entity)).negate();
535535
readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, evaluator, propertyFilter);
@@ -868,9 +868,9 @@ private void writeProperties(Bson bson, MongoPersistentEntity<?> entity, Persist
868868
dbObjectAccessor.put(prop, null);
869869
}
870870
} else if (!conversions.isSimpleType(value.getClass())) {
871-
writePropertyInternal(value, dbObjectAccessor, prop);
871+
writePropertyInternal(value, dbObjectAccessor, prop, accessor);
872872
} else {
873-
writeSimpleInternal(value, bson, prop);
873+
writeSimpleInternal(value, bson, prop, accessor);
874874
}
875875
}
876876
}
@@ -887,11 +887,11 @@ private void writeAssociation(Association<MongoPersistentProperty> association,
887887
return;
888888
}
889889

890-
writePropertyInternal(value, dbObjectAccessor, inverseProp);
890+
writePropertyInternal(value, dbObjectAccessor, inverseProp, accessor);
891891
}
892892

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

896896
if (obj == null) {
897897
return;
@@ -902,7 +902,13 @@ protected void writePropertyInternal(@Nullable Object obj, DocumentAccessor acce
902902

903903
if (conversions.hasValueConverter(prop)) {
904904
accessor.put(prop, conversions.getPropertyValueConversions().getValueConverter(prop).write(obj,
905-
new MongoConversionContext(prop, this)));
905+
new MongoConversionContext(new PropertyValueProvider<MongoPersistentProperty>() {
906+
@Nullable
907+
@Override
908+
public <T> T getPropertyValue(MongoPersistentProperty property) {
909+
return (T) persistentPropertyAccessor.getProperty(property);
910+
}
911+
}, prop, this, spELContext)));
906912
return;
907913
}
908914

@@ -1234,12 +1240,18 @@ private void writeSimpleInternal(@Nullable Object value, Bson bson, String key)
12341240
BsonUtils.addToMap(bson, key, getPotentiallyConvertedSimpleWrite(value, Object.class));
12351241
}
12361242

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

12401246
if (conversions.hasValueConverter(property)) {
12411247
accessor.put(property, conversions.getPropertyValueConversions().getValueConverter(property).write(value,
1242-
new MongoConversionContext(property, this)));
1248+
new MongoConversionContext(new PropertyValueProvider<MongoPersistentProperty>() {
1249+
@Nullable
1250+
@Override
1251+
public <T> T getPropertyValue(MongoPersistentProperty property) {
1252+
return (T) persistentPropertyAccessor.getProperty(property);
1253+
}
1254+
}, property, this, spELContext)));
12431255
return;
12441256
}
12451257

@@ -1845,6 +1857,7 @@ static class MongoDbPropertyValueProvider implements PropertyValueProvider<Mongo
18451857
final ConversionContext context;
18461858
final DocumentAccessor accessor;
18471859
final SpELExpressionEvaluator evaluator;
1860+
final SpELContext spELContext;
18481861

18491862
/**
18501863
* Creates a new {@link MongoDbPropertyValueProvider} for the given source, {@link SpELExpressionEvaluator} and
@@ -1855,7 +1868,7 @@ static class MongoDbPropertyValueProvider implements PropertyValueProvider<Mongo
18551868
* @param evaluator must not be {@literal null}.
18561869
*/
18571870
MongoDbPropertyValueProvider(ConversionContext context, Bson source, SpELExpressionEvaluator evaluator) {
1858-
this(context, new DocumentAccessor(source), evaluator);
1871+
this(context, new DocumentAccessor(source), evaluator, null);
18591872
}
18601873

18611874
/**
@@ -1867,7 +1880,7 @@ static class MongoDbPropertyValueProvider implements PropertyValueProvider<Mongo
18671880
* @param evaluator must not be {@literal null}.
18681881
*/
18691882
MongoDbPropertyValueProvider(ConversionContext context, DocumentAccessor accessor,
1870-
SpELExpressionEvaluator evaluator) {
1883+
SpELExpressionEvaluator evaluator, SpELContext spELContext) {
18711884

18721885
Assert.notNull(context, "ConversionContext must no be null");
18731886
Assert.notNull(accessor, "DocumentAccessor must no be null");
@@ -1876,6 +1889,7 @@ static class MongoDbPropertyValueProvider implements PropertyValueProvider<Mongo
18761889
this.context = context;
18771890
this.accessor = accessor;
18781891
this.evaluator = evaluator;
1892+
this.spELContext = spELContext;
18791893
}
18801894

18811895
@Nullable
@@ -1892,7 +1906,7 @@ public <T> T getPropertyValue(MongoPersistentProperty property) {
18921906
CustomConversions conversions = context.getCustomConversions();
18931907
if (conversions.hasValueConverter(property)) {
18941908
return (T) conversions.getPropertyValueConversions().getValueConverter(property).read(value,
1895-
new MongoConversionContext(property, context.getSourceConverter()));
1909+
new MongoConversionContext(this, property, context.getSourceConverter(), spELContext));
18961910
}
18971911

18981912
ConversionContext contextToUse = context.forProperty(property);
@@ -1902,7 +1916,7 @@ public <T> T getPropertyValue(MongoPersistentProperty property) {
19021916

19031917
public MongoDbPropertyValueProvider withContext(ConversionContext context) {
19041918

1905-
return context == this.context ? this : new MongoDbPropertyValueProvider(context, accessor, evaluator);
1919+
return context == this.context ? this : new MongoDbPropertyValueProvider(context, accessor, evaluator, spELContext);
19061920
}
19071921
}
19081922

@@ -1925,7 +1939,7 @@ class AssociationAwareMongoDbPropertyValueProvider extends MongoDbPropertyValueP
19251939
*/
19261940
AssociationAwareMongoDbPropertyValueProvider(ConversionContext context, DocumentAccessor source,
19271941
SpELExpressionEvaluator evaluator) {
1928-
super(context, source, evaluator);
1942+
super(context, source, evaluator, MappingMongoConverter.this.spELContext);
19291943
}
19301944

19311945
@Override

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java

+27-1
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,17 @@
1515
*/
1616
package org.springframework.data.mongodb.core.convert;
1717

18+
import java.util.function.Supplier;
19+
1820
import org.bson.conversions.Bson;
1921
import org.springframework.data.convert.ValueConversionContext;
22+
import org.springframework.data.mapping.PersistentPropertyAccessor;
23+
import org.springframework.data.mapping.model.PropertyValueProvider;
24+
import org.springframework.data.mapping.model.SpELContext;
25+
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
2026
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
2127
import org.springframework.data.util.TypeInformation;
28+
import org.springframework.expression.EvaluationContext;
2229
import org.springframework.lang.Nullable;
2330

2431
/**
@@ -29,20 +36,34 @@
2936
*/
3037
public class MongoConversionContext implements ValueConversionContext<MongoPersistentProperty> {
3138

39+
private final PropertyValueProvider accessor; // TODO: generics
3240
private final MongoPersistentProperty persistentProperty;
3341
private final MongoConverter mongoConverter;
3442

35-
public MongoConversionContext(MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) {
43+
@Nullable
44+
private final SpELContext spELContext;
45+
46+
public MongoConversionContext(PropertyValueProvider<?> accessor, MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) {
47+
this(accessor, persistentProperty, mongoConverter, null);
48+
}
49+
50+
public MongoConversionContext(PropertyValueProvider<?> accessor, MongoPersistentProperty persistentProperty, MongoConverter mongoConverter, SpELContext spELContext) {
3651

52+
this.accessor = accessor;
3753
this.persistentProperty = persistentProperty;
3854
this.mongoConverter = mongoConverter;
55+
this.spELContext = spELContext;
3956
}
4057

4158
@Override
4259
public MongoPersistentProperty getProperty() {
4360
return persistentProperty;
4461
}
4562

63+
public Object getValue(String propertyPath) {
64+
return accessor.getPropertyValue(persistentProperty.getOwner().getRequiredPersistentProperty(propertyPath));
65+
}
66+
4667
@Override
4768
public <T> T write(@Nullable Object value, TypeInformation<T> target) {
4869
return (T) mongoConverter.convertToMongoType(value, target);
@@ -53,4 +74,9 @@ public <T> T read(@Nullable Object value, TypeInformation<T> target) {
5374
return value instanceof Bson ? mongoConverter.read(target.getType(), (Bson) value)
5475
: ValueConversionContext.super.read(value, target);
5576
}
77+
78+
@Nullable
79+
public SpELContext getSpELContext() {
80+
return spELContext;
81+
}
5682
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ protected Object getMappedValue(Field documentField, Object sourceValue) {
439439
&& converter.getCustomConversions().hasValueConverter(documentField.getProperty())) {
440440
return converter.getCustomConversions().getPropertyValueConversions()
441441
.getValueConverter(documentField.getProperty())
442-
.write(value, new MongoConversionContext(documentField.getProperty(), converter));
442+
.write(value, new MongoConversionContext(null, documentField.getProperty(), converter));
443443
}
444444

445445
if (documentField.isIdField() && !documentField.isAssociation()) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.convert.encryption;
17+
18+
import org.springframework.data.mongodb.core.convert.MongoConversionContext;
19+
import org.springframework.data.mongodb.core.convert.MongoValueConverter;
20+
import org.springframework.data.mongodb.core.encryption.EncryptionContext;
21+
22+
/**
23+
* A specialized {@link MongoValueConverter} for {@literal en-/decrypting} properties.
24+
*
25+
* @author Christoph Strobl
26+
* @since 4.1
27+
*/
28+
public interface EncryptingConverter<S, T> extends MongoValueConverter<S, T> {
29+
30+
@Override
31+
default S read(Object value, MongoConversionContext context) {
32+
return decrypt(value, buildEncryptionContext(context));
33+
}
34+
35+
@Override
36+
default T write(Object value, MongoConversionContext context) {
37+
return encrypt(value, buildEncryptionContext(context));
38+
}
39+
40+
/**
41+
* Decrypt the given encrypted source value within the given {@link EncryptionContext context}.
42+
*
43+
* @param encryptedValue the encrypted source.
44+
* @param context the context to operate in.
45+
* @return never {@literal null}.
46+
*/
47+
S decrypt(Object encryptedValue, EncryptionContext context);
48+
49+
/**
50+
* Encrypt the given raw source value within the given {@link EncryptionContext context}.
51+
*
52+
* @param value the encrypted source.
53+
* @param context the context to operate in.
54+
* @return never {@literal null}.
55+
*/
56+
T encrypt(Object value, EncryptionContext context);
57+
58+
/**
59+
* Obtain the {@link EncryptionContext} for a given {@link MongoConversionContext value conversion context}.
60+
*
61+
* @param context the current MongoDB specific {@link org.springframework.data.convert.ValueConversionContext}.
62+
* @return the {@link EncryptionContext} to operate in.
63+
* @see org.springframework.data.convert.ValueConversionContext
64+
*/
65+
EncryptionContext buildEncryptionContext(MongoConversionContext context);
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.convert.encryption;
17+
18+
import org.springframework.data.mongodb.core.convert.MongoConversionContext;
19+
import org.springframework.data.mongodb.core.encryption.EncryptionContext;
20+
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
21+
import org.springframework.data.util.TypeInformation;
22+
import org.springframework.expression.EvaluationContext;
23+
import org.springframework.lang.NonNull;
24+
import org.springframework.lang.Nullable;
25+
26+
/**
27+
* Default {@link EncryptionContext} implementation.
28+
*
29+
* @author Christoph Strobl
30+
* @since 4.1
31+
*/
32+
class ExplicitEncryptionContext implements EncryptionContext {
33+
34+
private final MongoConversionContext conversionContext;
35+
36+
public ExplicitEncryptionContext(MongoConversionContext conversionContext) {
37+
this.conversionContext = conversionContext;
38+
}
39+
40+
@Override
41+
public MongoPersistentProperty getProperty() {
42+
return conversionContext.getProperty();
43+
}
44+
45+
@Nullable
46+
@Override
47+
public Object lookupValue(String path) {
48+
return conversionContext.getValue(path);
49+
}
50+
51+
@Override
52+
public Object convertToMongoType(Object value) {
53+
return conversionContext.write(value);
54+
}
55+
56+
@Override
57+
public EvaluationContext getEvaluationContext(Object source) {
58+
return conversionContext.getSpELContext().getEvaluationContext(source);
59+
}
60+
61+
@Override
62+
public <T> T read(@Nullable Object value, TypeInformation<T> target) {
63+
return conversionContext.read(value, target);
64+
}
65+
66+
@Override
67+
public <T> T write(@Nullable Object value, TypeInformation<T> target) {
68+
return conversionContext.write(value, target);
69+
}
70+
}

0 commit comments

Comments
 (0)