Compare commits

...

15 Commits

Author SHA1 Message Date
Christoph Strobl
d46f4bed74 Remove EvaluationContextProvider from MPE & MPP 2021-09-06 14:32:12 +02:00
Christoph Strobl
e8d0492520 Update documentation 2021-09-06 14:15:29 +02:00
Christoph Strobl
bd06bb422a Remove the explicit wrapper name and replace it with schemaDocument() method 2021-09-06 14:15:29 +02:00
Christoph Strobl
16f2cf61e6 Switch filter from using SchemaProperty to actual persistentProperties. 2021-09-06 14:15:29 +02:00
Christoph Strobl
d769e212d3 Revert "Hacking - retrieve environment properties and expose them via systemProperties."
This reverts commit ebe3bf8b1e836035a2d905b98c0de18ee1481cdb.
2021-09-06 14:15:29 +02:00
Christoph Strobl
b580840529 Hacking - retrieve environment properties and expose them via systemProperties. 2021-09-06 14:15:29 +02:00
Christoph Strobl
bb2d0f5376 Remove EncryptedField annotation in favor of Encrypted 2021-09-06 14:15:29 +02:00
Christoph Strobl
3448b46739 allow usage of evaluation context extension to compute keyIds 2021-09-06 14:15:29 +02:00
Christoph Strobl
c9aeccd0f5 use an extension to allow users providing the encryption 2021-09-06 14:15:28 +02:00
Christoph Strobl
1bbfd5b6de Move to explicit filter for encrypted fields. 2021-09-06 14:15:28 +02:00
Christoph Strobl
0df74b0b61 moar hacking 2021-09-06 14:15:28 +02:00
Christoph Strobl
ae27af3d16 allow algorithm on top level - maybe we should just have the encrypted annotation 2021-09-06 14:15:28 +02:00
Christoph Strobl
e7308cd806 hacking part II 2021-09-06 14:15:28 +02:00
Christoph Strobl
5c581fc450 hacking 2021-09-06 14:15:28 +02:00
Christoph Strobl
f16a6c6d34 Prepare issue branch. 2021-09-06 14:15:28 +02:00
24 changed files with 1089 additions and 27 deletions

View File

@@ -5,7 +5,7 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.3.0-SNAPSHOT</version>
<version>3.3.0-FLE-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Spring Data MongoDB</name>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.3.0-SNAPSHOT</version>
<version>3.3.0-FLE-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.3.0-SNAPSHOT</version>
<version>3.3.0-FLE-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -11,7 +11,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>3.3.0-SNAPSHOT</version>
<version>3.3.0-FLE-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2021 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;
/**
* Encryption algorithms supported by MongoDB Client Side Field Level Encryption.
*
* @author Christoph Strobl
* @since 3.3
*/
public final class EncryptionAlgorithms {
public static final String AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic";
public static final String AEAD_AES_256_CBC_HMAC_SHA_512_Random = "AEAD_AES_256_CBC_HMAC_SHA_512-Random";
}

View File

@@ -20,13 +20,19 @@ import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.bson.Document;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.Encrypted;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.JsonSchemaObject;
import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type;
@@ -34,10 +40,12 @@ import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema.MongoJsonSchemaBuilder;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* {@link MongoJsonSchemaCreator} implementation using both {@link MongoConverter} and {@link MappingContext} to obtain
@@ -52,6 +60,7 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
private final MongoConverter converter;
private final MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
private final Predicate<JsonSchemaPropertyContext> filter;
/**
* Create a new instance of {@link MappingMongoJsonSchemaCreator}.
@@ -61,10 +70,24 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
@SuppressWarnings("unchecked")
MappingMongoJsonSchemaCreator(MongoConverter converter) {
this(converter, (MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty>) converter.getMappingContext(),
(property) -> true);
}
@SuppressWarnings("unchecked")
MappingMongoJsonSchemaCreator(MongoConverter converter,
MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
Predicate<JsonSchemaPropertyContext> filter) {
Assert.notNull(converter, "Converter must not be null!");
this.converter = converter;
this.mappingContext = (MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty>) converter
.getMappingContext();
this.mappingContext = mappingContext;
this.filter = filter;
}
@Override
public MongoJsonSchemaCreator filter(Predicate<JsonSchemaPropertyContext> filter) {
return new MappingMongoJsonSchemaCreator(converter, mappingContext, filter);
}
/*
@@ -75,13 +98,35 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
public MongoJsonSchema createSchemaFor(Class<?> type) {
MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(type);
if (entity instanceof BasicMongoPersistentEntity) {
((BasicMongoPersistentEntity<?>) entity).getEvaluationContext(null);
}
MongoJsonSchemaBuilder schemaBuilder = MongoJsonSchema.builder();
{
Encrypted encrypted = entity.findAnnotation(Encrypted.class);
if (encrypted != null) {
Document encryptionMetadata = new Document();
Collection<Object> encryptionKeyIds = entity.getEncryptionKeyIds();
if (!CollectionUtils.isEmpty(encryptionKeyIds)) {
encryptionMetadata.append("keyId", encryptionKeyIds);
}
if (StringUtils.hasText(encrypted.algorithm())) {
encryptionMetadata.append("algorithm", encrypted.algorithm());
}
schemaBuilder.encryptionMetadata(encryptionMetadata);
}
}
List<JsonSchemaProperty> schemaProperties = computePropertiesForEntity(Collections.emptyList(), entity);
schemaBuilder.properties(schemaProperties.toArray(new JsonSchemaProperty[0]));
return schemaBuilder.build();
}
private List<JsonSchemaProperty> computePropertiesForEntity(List<MongoPersistentProperty> path,
@@ -93,6 +138,11 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
List<MongoPersistentProperty> currentPath = new ArrayList<>(path);
if (!filter.test(new PropertyContext(
currentPath.stream().map(PersistentProperty::getName).collect(Collectors.joining(".")), nested))) {
continue;
}
if (path.contains(nested)) { // cycle guard
schemaProperties.add(createSchemaProperty(computePropertyFieldName(CollectionUtils.lastElement(currentPath)),
Object.class, false));
@@ -120,15 +170,38 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
String fieldName = computePropertyFieldName(property);
JsonSchemaProperty schemaProperty;
if (property.isCollectionLike()) {
return createSchemaProperty(fieldName, targetType, required);
schemaProperty = createSchemaProperty(fieldName, targetType, required);
} else if (property.isMap()) {
return createSchemaProperty(fieldName, Type.objectType(), required);
schemaProperty = createSchemaProperty(fieldName, Type.objectType(), required);
} else if (ClassUtils.isAssignable(Enum.class, targetType)) {
return createEnumSchemaProperty(fieldName, targetType, required);
schemaProperty = createEnumSchemaProperty(fieldName, targetType, required);
} else {
schemaProperty = createSchemaProperty(fieldName, targetType, required);
}
return createSchemaProperty(fieldName, targetType, required);
return applyEncryptionDataIfNecessary(property, schemaProperty);
}
@Nullable
private JsonSchemaProperty applyEncryptionDataIfNecessary(MongoPersistentProperty property,
JsonSchemaProperty schemaProperty) {
Encrypted encrypted = property.findAnnotation(Encrypted.class);
if (encrypted == null) {
return schemaProperty;
}
EncryptedJsonSchemaProperty enc = new EncryptedJsonSchemaProperty(schemaProperty);
if (StringUtils.hasText(encrypted.algorithm())) {
enc = enc.algorithm(encrypted.algorithm());
}
if (!ObjectUtils.isEmpty(encrypted.keyId())) {
enc = enc.keys(property.getEncryptionKeyIds());
}
return enc;
}
private JsonSchemaProperty createObjectSchemaPropertyForEntity(List<MongoPersistentProperty> path,
@@ -207,4 +280,30 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
return JsonSchemaProperty.required(property);
}
class PropertyContext implements JsonSchemaPropertyContext {
private String path;
private MongoPersistentProperty property;
public PropertyContext(String path, MongoPersistentProperty property) {
this.path = path;
this.property = property;
}
@Override
public String getPath() {
return path;
}
@Override
public MongoPersistentProperty getProperty() {
return property;
}
@Override
public <T> MongoPersistentEntity<T> resolveEntity(MongoPersistentProperty property) {
return (MongoPersistentEntity<T>) mappingContext.getPersistentEntity(property);
}
}
}

View File

@@ -15,7 +15,23 @@
*/
package org.springframework.data.mongodb.core;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.mapping.Encrypted;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
import org.springframework.data.mongodb.core.mapping.Unwrapped.Nullable;
import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.util.Assert;
@@ -46,6 +62,7 @@ import org.springframework.util.Assert;
* {@link org.bson.types.ObjectId} like {@link String} will be mapped to {@code type : 'object'} unless there is more
* specific information available via the {@link org.springframework.data.mongodb.core.mapping.MongoId} annotation.
* </p>
* {@link Encrypted} properties will contain {@literal encrypt} information.
*
* @author Christoph Strobl
* @since 2.2
@@ -60,6 +77,88 @@ public interface MongoJsonSchemaCreator {
*/
MongoJsonSchema createSchemaFor(Class<?> type);
/**
* Filter matching {@link JsonSchemaProperty properties}.
*
* @param filter the {@link Predicate} to evaluate for inclusion. Must not be {@literal null}.
* @return new instance of {@link MongoJsonSchemaCreator}.
* @since 3.3
*/
MongoJsonSchemaCreator filter(Predicate<JsonSchemaPropertyContext> filter);
/**
* The context in which a specific {@link #getProperty()} is encountered during schema creation.
*
* @since 3.3
*/
interface JsonSchemaPropertyContext {
/**
* The path to a given field/property in dot notation.
*
* @return never {@literal null}.
*/
String getPath();
/**
* The current property.
*
* @return never {@literal null}.
*/
MongoPersistentProperty getProperty();
/**
* Obtain the {@link MongoPersistentEntity} for a given property.
*
* @param property must not be {@literal null}.
* @param <T>
* @return {@literal null} if the property is not an entity. It is nevertheless recommend to check
* {@link PersistentProperty#isEntity()} first.
*/
@Nullable
<T> MongoPersistentEntity<T> resolveEntity(MongoPersistentProperty property);
}
/**
* A filter {@link Predicate} that matches {@link Encrypted encrypted properties} and those having nested ones.
*
* @return new instance of {@link Predicate}.
* @since 3.3
*/
static Predicate<JsonSchemaPropertyContext> encryptedOnly() {
return new Predicate<JsonSchemaPropertyContext>() {
// cycle guard
private final Set<MongoPersistentProperty> seen = new HashSet<>();
@Override
public boolean test(JsonSchemaPropertyContext context) {
return extracted(context.getProperty(), context);
}
private boolean extracted(MongoPersistentProperty property, JsonSchemaPropertyContext context) {
if (property.isAnnotationPresent(Encrypted.class)) {
return true;
}
if (!property.isEntity() || seen.contains(property)) {
return false;
}
seen.add(property);
for (MongoPersistentProperty nested : context.resolveEntity(property)) {
if (extracted(nested, context)) {
return true;
}
}
return false;
}
};
}
/**
* Creates a new {@link MongoJsonSchemaCreator} that is aware of conversions applied by the given
* {@link MongoConverter}.
@@ -72,4 +171,41 @@ public interface MongoJsonSchemaCreator {
Assert.notNull(mongoConverter, "MongoConverter must not be null!");
return new MappingMongoJsonSchemaCreator(mongoConverter);
}
/**
* Creates a new {@link MongoJsonSchemaCreator} that is aware of type mappings and potential
* {@link org.springframework.data.spel.spi.EvaluationContextExtension extensions}.
*
* @param mappingContext must not be {@literal null}.
* @return new instance of {@link MongoJsonSchemaCreator}.
* @since 3.3
*/
static MongoJsonSchemaCreator create(MappingContext mappingContext) {
MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext);
converter.setCustomConversions(MongoCustomConversions.create(config -> {}));
converter.afterPropertiesSet();
return create(converter);
}
/**
* Creates a new {@link MongoJsonSchemaCreator} that does not consider potential extensions - suitable for testing. We
* recommend to use {@link #create(MappingContext)}.
*
* @return new instance of {@link MongoJsonSchemaCreator}.
* @since 3.3
*/
static MongoJsonSchemaCreator create() {
MongoMappingContext mappingContext = new MongoMappingContext();
mappingContext.setSimpleTypeHolder(MongoSimpleTypes.HOLDER);
mappingContext.afterPropertiesSet();
MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext);
converter.setCustomConversions(MongoCustomConversions.create(config -> {}));
converter.afterPropertiesSet();
return create(converter);
}
}

View File

@@ -17,8 +17,12 @@ package org.springframework.data.mongodb.core.mapping;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.data.annotation.Id;
@@ -28,6 +32,9 @@ import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.mongodb.MongoCollectionUtils;
import org.springframework.data.mongodb.util.encryption.EncryptionUtils;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
@@ -212,6 +219,11 @@ public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, Mong
return super.getEvaluationContext(rootObject);
}
@Override
public EvaluationContext getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) {
return super.getEvaluationContext(rootObject, dependencies);
}
private void verifyFieldUniqueness() {
AssertFieldNameUniquenessHandler handler = new AssertFieldNameUniquenessHandler();
@@ -360,6 +372,32 @@ public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, Mong
}
}
@Override
public Collection<Object> getEncryptionKeyIds() {
Encrypted encrypted = findAnnotation(Encrypted.class);
if (encrypted == null) {
return null;
}
if (ObjectUtils.isEmpty(encrypted.keyId())) {
return Collections.emptySet();
}
Lazy<EvaluationContext> evaluationContext = Lazy.of(() -> {
EvaluationContext ctx = getEvaluationContext(null);
ctx.setVariable("target", getType().getSimpleName());
return ctx;
});
List<Object> target = new ArrayList<>();
for (String keyId : encrypted.keyId()) {
target.add(EncryptionUtils.resolveKeyId(keyId, evaluationContext));
}
return target;
}
/**
* @author Christoph Strobl
* @since 1.6

View File

@@ -16,7 +16,11 @@
package org.springframework.data.mongodb.core.mapping;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.bson.types.ObjectId;
@@ -30,7 +34,12 @@ import org.springframework.data.mapping.model.FieldNamingStrategy;
import org.springframework.data.mapping.model.Property;
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.mongodb.util.encryption.EncryptionUtils;
import org.springframework.data.util.Lazy;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
@@ -300,4 +309,43 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope
return isAnnotationPresent(TextScore.class);
}
/**
* Obtain the {@link EvaluationContext} for a specific root object.
*
* @param rootObject can be {@literal null}.
* @return never {@literal null}.
* @since 3.3
*/
public EvaluationContext getEvaluationContext(@Nullable Object rootObject) {
if (getOwner() instanceof BasicMongoPersistentEntity) {
return ((BasicMongoPersistentEntity) getOwner()).getEvaluationContext(rootObject);
}
return rootObject != null ? new StandardEvaluationContext(rootObject) : new StandardEvaluationContext();
}
@Override
public Collection<Object> getEncryptionKeyIds() {
Encrypted encrypted = findAnnotation(Encrypted.class);
if (encrypted == null) {
return null;
}
if (ObjectUtils.isEmpty(encrypted.keyId())) {
return Collections.emptySet();
}
Lazy<EvaluationContext> evaluationContext = Lazy.of(() -> {
EvaluationContext ctx = getEvaluationContext(null);
ctx.setVariable("target", getOwner().getType().getSimpleName() + "." + getName());
return ctx;
});
List<Object> target = new ArrayList<>();
for (String keyId : encrypted.keyId()) {
target.add(EncryptionUtils.resolveKeyId(keyId, evaluationContext));
}
return target;
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2021. 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.mapping;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* {@link Encrypted} provides data required for MongoDB Client Side Field Level Encryption that is applied during schema
* resolution. It can be applied on top level (typically those types annotated with {@link Document} to provide the
* {@literal encryptMetadata}.
*
* <pre class="code">
* &#64;Document
* &#64;Encrypted(keyId = "4fPYFM9qSgyRAjgQ2u+IMQ==")
* public class Patient {
* private ObjectId id;
* private String name;
*
* &#64;Field("publisher_ac")
* &#64;DocumentReference(lookup = "{ 'acronym' : ?#{#target} }") private Publisher publisher;
* }
*
* "encryptMetadata": {
* "keyId": [
* {
* "$binary": {
* "base64": "4fPYFM9qSgyRAjgQ2u+IMQ==",
* "subType": "04"
* }
* }
* ]
* }
* </pre>
*
* <br />
* On property level it is used for deriving field specific {@literal encrypt} settings.
*
* <pre class="code">
* public class Patient {
* private ObjectId id;
* private String name;
*
* &#64;Encrypted(keyId = "4fPYFM9qSgyRAjgQ2u+IMQ==", algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic")
* private String ssn;
* }
*
* "ssn" : {
* "encrypt": {
* "keyId": [
* {
* "$binary": {
* "base64": "4fPYFM9qSgyRAjgQ2u+IMQ==",
* "subType": "04"
* }
* }
* ],
* "algorithm" : "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic",
* "bsonType" : "string"
* }
* }
* </pre>
*
* @author Christoph Strobl
* @since 3.3
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.FIELD })
public @interface Encrypted {
/**
* Get the {@code keyId} to use. The value must resolve to either the UUID representation of the key or a base64
* encoded value representing the UUID value.
* <p />
* On {@link ElementType#TYPE} level the {@link #keyId()} can be left empty if explicitly set for fields. <br />
* On {@link ElementType#FIELD} level the {@link #keyId()} can be left empty if inherited from
* {@literal encryptMetadata}.
*
* @return the key id to use. May contain a parsable {@link org.springframework.expression.Expression expression}. In
* this case the {@code #target} variable will hold the target element name.
*/
String[] keyId() default {};
/**
* Set the algorithm to use.
* <p />
* On {@link ElementType#TYPE} level the {@link #algorithm()} can be left empty if explicitly set for fields. <br />
* On {@link ElementType#FIELD} level the {@link #algorithm()} can be left empty if inherited from
* {@literal encryptMetadata}.
*
* @return the encryption algorithm.
* @see org.springframework.data.mongodb.core.EncryptionAlgorithms
*/
String algorithm() default "";
}

View File

@@ -46,6 +46,9 @@ public class MongoMappingContext extends AbstractMappingContext<MongoPersistentE
private FieldNamingStrategy fieldNamingStrategy = DEFAULT_NAMING_STRATEGY;
private boolean autoIndexCreation = false;
@Nullable
private ApplicationContext applicationContext;
/**
* Creates a new {@link MongoMappingContext}.
*/
@@ -103,6 +106,8 @@ public class MongoMappingContext extends AbstractMappingContext<MongoPersistentE
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
super.setApplicationContext(applicationContext);
}
@@ -145,4 +150,10 @@ public class MongoMappingContext extends AbstractMappingContext<MongoPersistentE
return new UnwrappedMongoPersistentEntity<>(entity, new UnwrapEntityContext(persistentProperty));
}
@Nullable
public ApplicationContext getApplicationContext() {
return this.applicationContext;
}
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.mongodb.core.mapping;
import java.util.Collection;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.model.MutablePersistentEntity;
import org.springframework.lang.Nullable;
@@ -102,4 +104,11 @@ public interface MongoPersistentEntity<T> extends MutablePersistentEntity<T, Mon
return false;
}
/**
* @return the resolved encryption keyIds if applicable. An empty {@link Collection} if no keyIds specified.
* {@literal null} no {@link Encrypted} annotation found.
* @since 3.3
*/
@Nullable
Collection<Object> getEncryptionKeyIds();
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.mongodb.core.mapping;
import java.util.Collection;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.mapping.PersistentEntity;
@@ -160,6 +162,13 @@ public interface MongoPersistentProperty extends PersistentProperty<MongoPersist
return isEntity() && isAnnotationPresent(Unwrapped.class);
}
/**
* @return the resolved encryption keyIds if applicable. An empty {@link Collection} if no keyIds specified.
* {@literal null} no {@link Encrypted} annotation found.
* @since 3.3
*/
Collection<Object> getEncryptionKeyIds();
/**
* Simple {@link Converter} implementation to transform a {@link MongoPersistentProperty} into its field name.
*

View File

@@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core.mapping;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
@@ -323,4 +324,9 @@ class UnwrappedMongoPersistentEntity<T> implements MongoPersistentEntity<T> {
public boolean isUnwrapped() {
return context.getProperty().isUnwrapped();
}
@Override
public Collection<Object> getEncryptionKeyIds() {
return delegate.getEncryptionKeyIds();
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.data.mongodb.core.mapping;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.PersistentEntity;
@@ -268,6 +269,11 @@ class UnwrappedMongoPersistentProperty implements MongoPersistentProperty {
return delegate.isUnwrapped();
}
@Override
public Collection<Object> getEncryptionKeyIds() {
return delegate.getEncryptionKeyIds();
}
@Override
@Nullable
public Class<?> getComponentType() {

View File

@@ -16,7 +16,9 @@
package org.springframework.data.mongodb.core.schema;
import org.bson.Document;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* Value object representing a MongoDB-specific JSON schema which is the default {@link MongoJsonSchema} implementation.
@@ -29,18 +31,44 @@ class DefaultMongoJsonSchema implements MongoJsonSchema {
private final JsonSchemaObject root;
DefaultMongoJsonSchema(JsonSchemaObject root) {
@Nullable //
private final Document encryptionMetadata;
DefaultMongoJsonSchema(JsonSchemaObject root) {
this(root, null);
}
/**
* Create new instance of {@link DefaultMongoJsonSchema}.
*
* @param root the schema root element.
* @param encryptionMetadata can be {@literal null}.
* @since 3.3
*/
DefaultMongoJsonSchema(JsonSchemaObject root, @Nullable Document encryptionMetadata) {
Assert.notNull(root, "Root schema object must not be null!");
Assert.notNull(root, "Root must not be null!");
this.root = root;
this.encryptionMetadata = encryptionMetadata;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.MongoJsonSchema#toDocument()
* @see org.springframework.data.mongodb.core.schema.MongoJsonSchema#schema()
*/
@Override
public Document toDocument() {
return new Document("$jsonSchema", root.toDocument());
public Document schemaDocument() {
Document schemaDocument = new Document();
// we want this to be the first element rendered, so it reads nice when printed to json
if (!CollectionUtils.isEmpty(encryptionMetadata)) {
schemaDocument.append("encryptMetadata", encryptionMetadata);
}
schemaDocument.putAll(root.toDocument());
return schemaDocument;
}
}

View File

@@ -36,10 +36,10 @@ class DocumentJsonSchema implements MongoJsonSchema {
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.MongoJsonSchema#toDocument()
* @see org.springframework.data.mongodb.core.schema.MongoJsonSchema#schema()
*/
@Override
public Document toDocument() {
return new Document("$jsonSchema", new Document(document));
public Document schemaDocument() {
return new Document(document);
}
}

View File

@@ -523,6 +523,10 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
public ObjectJsonSchemaProperty generatedDescription() {
return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
}
public List<JsonSchemaProperty> getProperties() {
return jsonSchemaObjectDelegate.getProperties();
}
}
/**
@@ -1060,7 +1064,7 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
private final JsonSchemaProperty targetProperty;
private final @Nullable String algorithm;
private final @Nullable String keyId;
private final @Nullable List<UUID> keyIds;
private final @Nullable List<?> keyIds;
/**
* Create new instance of {@link EncryptedJsonSchemaProperty} wrapping the given {@link JsonSchemaProperty target}.
@@ -1072,7 +1076,7 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
}
private EncryptedJsonSchemaProperty(JsonSchemaProperty target, @Nullable String algorithm, @Nullable String keyId,
@Nullable List<UUID> keyIds) {
@Nullable List<?> keyIds) {
Assert.notNull(target, "Target must not be null!");
this.targetProperty = target;
@@ -1134,6 +1138,14 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
return new EncryptedJsonSchemaProperty(targetProperty, algorithm, null, Arrays.asList(keyId));
}
/**
* @param keyId must not be {@literal null}.
* @return new instance of {@link EncryptedJsonSchemaProperty}.
*/
public EncryptedJsonSchemaProperty keys(Object... keyId) {
return new EncryptedJsonSchemaProperty(targetProperty, algorithm, null, Arrays.asList(keyId));
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#toDocument()

View File

@@ -20,6 +20,7 @@ import java.util.Set;
import org.bson.Document;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject;
import org.springframework.lang.Nullable;
/**
* Interface defining MongoDB-specific JSON schema object. New objects can be built with {@link #builder()}, for
@@ -62,13 +63,25 @@ import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.Object
public interface MongoJsonSchema {
/**
* Create the {@link Document} containing the specified {@code $jsonSchema}. <br />
* Create the {@code $jsonSchema} {@link Document} containing the specified {@link #schemaDocument()}. <br />
* Property and field names need to be mapped to the domain type ones by running the {@link Document} through a
* {@link org.springframework.data.mongodb.core.convert.JsonSchemaMapper} to apply field name customization.
*
* @return never {@literal null}.
*/
Document toDocument();
default Document toDocument() {
return new Document("$jsonSchema", schemaDocument());
}
/**
* Create the {@link Document} defining the schema. <br />
* Property and field names need to be mapped to the domain type ones by running the {@link Document} through a
* {@link org.springframework.data.mongodb.core.convert.JsonSchemaMapper} to apply field name customization.
*
* @return never {@literal null}.
* @since 3.3
*/
Document schemaDocument();
/**
* Create a new {@link MongoJsonSchema} for a given root object.
@@ -108,6 +121,9 @@ public interface MongoJsonSchema {
private ObjectJsonSchemaObject root;
@Nullable //
private Document encryptionMetadata;
MongoJsonSchemaBuilder() {
root = new ObjectJsonSchemaObject();
}
@@ -266,13 +282,23 @@ public interface MongoJsonSchema {
return this;
}
/**
* Define the {@literal encryptMetadata} element of the schema.
*
* @param encryptionMetadata can be {@literal null}.
* @since 3.3
*/
public void encryptionMetadata(@Nullable Document encryptionMetadata) {
this.encryptionMetadata = encryptionMetadata;
}
/**
* Obtain the {@link MongoJsonSchema}.
*
* @return new instance of {@link MongoJsonSchema}.
*/
public MongoJsonSchema build() {
return MongoJsonSchema.of(root);
return new DefaultMongoJsonSchema(root, encryptionMetadata);
}
}
}

View File

@@ -437,6 +437,10 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
return newInstance(description, true, restrictions);
}
public List<JsonSchemaProperty> getProperties() {
return properties;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#toDocument()

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2021 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.util.encryption;
import java.util.UUID;
import java.util.function.Supplier;
import org.springframework.data.mongodb.util.spel.ExpressionUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Internal utility class for dealing with encryption related matters.
*
* @author Christoph Strobl
* @since 3.3
*/
public final class EncryptionUtils {
/**
* Resolve a given plain {@link String} value into the store native {@literal keyId} format, considering potential
* {@link Expression expressions}. <br />
* The potential keyId is probed against an {@link UUID#fromString(String) UUID value} and the {@literal base64}
* encoded {@code $binary} representation.
*
* @param value the source value to resolve the keyId for. Must not be {@literal null}.
* @param evaluationContext a {@link Supplier} used to provide the {@link EvaluationContext} in case an
* {@link Expression} is {@link ExpressionUtils#detectExpression(String) detected}.
* @return can be {@literal null}.
* @throws IllegalArgumentException if one of the required arguments is {@literal null}.
*/
@Nullable
public static Object resolveKeyId(String value, Supplier<EvaluationContext> evaluationContext) {
Assert.notNull(value, "Value must not be null!");
Object potentialKeyId = value;
Expression expression = ExpressionUtils.detectExpression(value);
if (expression != null) {
potentialKeyId = expression.getValue(evaluationContext.get());
if (!(potentialKeyId instanceof String)) {
return potentialKeyId;
}
}
try {
return UUID.fromString(potentialKeyId.toString());
} catch (IllegalArgumentException e) {
return org.bson.Document.parse("{ val : { $binary : { base64 : '" + potentialKeyId + "', subType : '04'} } }")
.get("val");
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2021 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.util.spel;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
* Internal utility class for dealing with {@link Expression} and potential ones.
*
* @author Christoph Strobl
* @since 3.3
*/
public final class ExpressionUtils {
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
/**
* Returns a SpEL {@link Expression} if the given {@link String} is actually an expression that does not evaluate to a
* {@link LiteralExpression} (indicating that no subsequent evaluation is necessary).
*
* @param potentialExpression can be {@literal null}
* @return can be {@literal null}.
*/
@Nullable
public static Expression detectExpression(@Nullable String potentialExpression) {
if (!StringUtils.hasText(potentialExpression)) {
return null;
}
Expression expression = PARSER.parseExpression(potentialExpression, ParserContext.TEMPLATE_EXPRESSION);
return expression instanceof LiteralExpression ? null : expression;
}
}

View File

@@ -19,23 +19,27 @@ import static org.springframework.data.mongodb.test.util.Assertions.*;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.bson.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.data.annotation.Transient;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.mapping.Encrypted;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.FieldType;
import org.springframework.data.mongodb.core.mapping.MongoId;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.data.spel.spi.Function;
/**
* Unit tests for {@link MappingMongoJsonSchemaCreator}.
@@ -95,6 +99,64 @@ public class MappingMongoJsonSchemaCreatorUnitTests {
"{ 'type' : 'object', 'properties' : { '_id' : { 'type' : 'object' }, 'nested' : { 'type' : 'object' } } }");
}
@Test // GH-???
public void csfle/*encryptedFieldsOnly*/() {
MongoJsonSchema schema = MongoJsonSchemaCreator.create() //
.filter(MongoJsonSchemaCreator.encryptedOnly()) // filter non encrypted fields
.createSchemaFor(Patient.class);
Document targetSchema = schema.schemaDocument();
assertThat(targetSchema).isEqualTo(Document.parse(PATIENT));
}
@Test // GH-???
public void csfleCyclic/*encryptedFieldsOnly*/() {
MongoJsonSchema schema = MongoJsonSchemaCreator.create() //
.filter(MongoJsonSchemaCreator.encryptedOnly()) // filter non encrypted fields
.createSchemaFor(Cyclic.class);
Document targetSchema = schema.schemaDocument();
assertThat(targetSchema).isNotNull();
}
@Test // GH-???
public void csfleWithKeyFromProperties() {
GenericApplicationContext applicationContext = new GenericApplicationContext();
applicationContext.registerBean("encryptionExtension", EncryptionExtension.class, () -> new EncryptionExtension());
applicationContext.refresh();
MongoMappingContext mappingContext = new MongoMappingContext();
mappingContext.setApplicationContext(applicationContext);
mappingContext.afterPropertiesSet();
MongoJsonSchema schema = MongoJsonSchemaCreator.create(mappingContext) //
.filter(MongoJsonSchemaCreator.encryptedOnly()) //
.createSchemaFor(EncryptionMetadataFromProperty.class);
assertThat(schema.schemaDocument()).isEqualTo(Document.parse(ENC_FROM_PROPERTY_SCHEMA));
}
@Test // GH-???
public void csfleWithKeyFromMethod() {
GenericApplicationContext applicationContext = new GenericApplicationContext();
applicationContext.registerBean("encryptionExtension", EncryptionExtension.class, () -> new EncryptionExtension());
applicationContext.refresh();
MongoMappingContext mappingContext = new MongoMappingContext();
mappingContext.setApplicationContext(applicationContext);
mappingContext.afterPropertiesSet();
MongoJsonSchema schema = MongoJsonSchemaCreator.create(mappingContext) //
.filter(MongoJsonSchemaCreator.encryptedOnly()) //
.createSchemaFor(EncryptionMetadataFromMethod.class);
assertThat(schema.schemaDocument()).isEqualTo(Document.parse(ENC_FROM_METHOD_SCHEMA));
}
// --> TYPES AND JSON
// --> ENUM
@@ -125,8 +187,7 @@ public class MappingMongoJsonSchemaCreatorUnitTests {
" 'collectionProperty' : { 'type' : 'array' }," + //
" 'mapProperty' : { 'type' : 'object' }," + //
" 'objectProperty' : { 'type' : 'object' }," + //
" 'enumProperty' : " + JUST_SOME_ENUM + //
" }" + //
" 'enumProperty' : " + JUST_SOME_ENUM + " }" + //
"}";
static class VariousFieldTypes {
@@ -249,4 +310,209 @@ public class MappingMongoJsonSchemaCreatorUnitTests {
}
}
static final String PATIENT = "{" + //
" 'type': 'object'," + //
" 'encryptMetadata': {" + //
" 'keyId': [" + //
" {" + //
" '$binary': {" + //
" 'base64': 'xKVup8B1Q+CkHaVRx+qa+g=='," + //
" 'subType': '04'" + //
" }" + //
" }" + //
" ]" + //
" }," + //
" 'properties': {" + //
" 'ssn': {" + //
" 'encrypt': {" + //
" 'bsonType': 'int'," + //
" 'algorithm': 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'" + //
" }" + //
" }," + //
" 'bloodType': {" + //
" 'encrypt': {" + //
" 'bsonType': 'string'," + //
" 'algorithm': 'AEAD_AES_256_CBC_HMAC_SHA_512-Random'" + //
" }" + //
" }," + //
" 'medicalRecords': {" + //
" 'encrypt': {" + //
" 'bsonType': 'array'," + //
" 'algorithm': 'AEAD_AES_256_CBC_HMAC_SHA_512-Random'" + //
" }" + //
" }," + //
" 'insurance': {" + //
" 'type': 'object'," + //
" 'properties': {" + //
" 'policyNumber': {" + //
" 'encrypt': {" + //
" 'bsonType': 'int'," + //
" 'algorithm': 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'" + //
" }" + //
" }" + //
" }" + //
" }" + //
" }" + //
"}";
@Encrypted(keyId = "xKVup8B1Q+CkHaVRx+qa+g==")
static class Patient {
String name;
@Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic") //
Integer ssn;
@Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Random") //
String bloodType;
String keyAltNameField;
@Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Random") //
List<Map<String, String>> medicalRecords;
Insurance insurance;
}
static class Insurance {
@Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic") //
Integer policyNumber;
String provider;
}
static final String ENC_FROM_PROPERTY_ENTITY_KEY = "C5a5aMB7Ttq4wSJTFeRn8g==";
static final String ENC_FROM_PROPERTY_PROPOERTY_KEY = "Mw6mdTVPQfm4quqSCLVB3g=";
static final String ENC_FROM_PROPERTY_SCHEMA = "{" + //
" 'encryptMetadata': {" + //
" 'keyId': [" + //
" {" + //
" '$binary': {" + //
" 'base64': '" + ENC_FROM_PROPERTY_ENTITY_KEY + "'," + //
" 'subType': '04'" + //
" }" + //
" }" + //
" ]" + //
" }," + //
" 'type': 'object'," + //
" 'properties': {" + //
" 'policyNumber': {" + //
" 'encrypt': {" + //
" 'keyId': [" + //
" [" + //
" {" + //
" '$binary': {" + //
" 'base64': '" + ENC_FROM_PROPERTY_PROPOERTY_KEY + "'," + //
" 'subType': '04'" + //
" }" + //
" }" + //
" ]" + //
" ]," + //
" 'bsonType': 'int'," + //
" 'algorithm': 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'" + //
" }" + //
" }" + //
" }" + //
"}";
@Encrypted(keyId = "#{entityKey}")
static class EncryptionMetadataFromProperty {
@Encrypted(keyId = "#{propertyKey}", algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic") //
Integer policyNumber;
String provider;
}
static final String ENC_FROM_METHOD_ENTITY_KEY = "4fPYFM9qSgyRAjgQ2u+IMQ==";
static final String ENC_FROM_METHOD_PROPOERTY_KEY = "+idiseKwTVCJfSKC3iUeYQ==";
static final String ENC_FROM_METHOD_SCHEMA = "{" + //
" 'encryptMetadata': {" + //
" 'keyId': [" + //
" {" + //
" '$binary': {" + //
" 'base64': '" + ENC_FROM_METHOD_ENTITY_KEY + "'," + //
" 'subType': '04'" + //
" }" + //
" }" + //
" ]" + //
" }," + //
" 'type': 'object'," + //
" 'properties': {" + //
" 'policyNumber': {" + //
" 'encrypt': {" + //
" 'keyId': [" + //
" [" + //
" {" + //
" '$binary': {" + //
" 'base64': '" + ENC_FROM_METHOD_PROPOERTY_KEY + "'," + //
" 'subType': '04'" + //
" }" + //
" }" + //
" ]" + //
" ]," + //
" 'bsonType': 'int'," + //
" 'algorithm': 'AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic'" + //
" }" + //
" }" + //
" }" + //
"}";
@Encrypted(keyId = "#{mongocrypt.keyId(#target)}")
static class EncryptionMetadataFromMethod {
@Encrypted(keyId = "#{mongocrypt.keyId(#target)}", algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic") //
Integer policyNumber;
String provider;
}
public static class EncryptionExtension implements EvaluationContextExtension {
/*
* (non-Javadoc)
* @see org.springframework.data.spel.spi.EvaluationContextExtension#getExtensionId()
*/
@Override
public String getExtensionId() {
return "mongocrypt";
}
/*
* (non-Javadoc)
* @see org.springframework.data.spel.spi.EvaluationContextExtension#getProperties()
*/
@Override
public Map<String, Object> getProperties() {
Map<String, Object> properties = new LinkedHashMap<>();
properties.put("entityKey", ENC_FROM_PROPERTY_ENTITY_KEY);
properties.put("propertyKey", ENC_FROM_PROPERTY_PROPOERTY_KEY);
return properties;
}
@Override
public Map<String, Function> getFunctions() {
try {
return Collections.singletonMap("keyId",
new Function(EncryptionExtension.class.getMethod("keyId", String.class), this));
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return Collections.emptyMap();
}
public String keyId(String target) {
if (target.equals("EncryptionMetadataFromMethod")) {
return ENC_FROM_METHOD_ENTITY_KEY;
}
if (target.equals("EncryptionMetadataFromMethod.policyNumber")) {
return ENC_FROM_METHOD_PROPOERTY_KEY;
}
return "xKVup8B1Q+CkHaVRx+qa+g==";
}
}
}

View File

@@ -225,6 +225,110 @@ MongoJsonSchema schema = MongoJsonSchema.builder()
----
====
Instead of defining encrypted fields manually it is possible leverage the `@Encrypted` annotation as shown in the snippet below.
.Client-Side Field Level Encryption via Json Schema
====
[source,java]
----
@Document
@Encrypted(keyId = "xKVup8B1Q+CkHaVRx+qa+g==", algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Random") <1>
static class Patient {
@Id String id;
String name;
@Encrypted <2>
String bloodType;
@Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic") <3>
Integer ssn;
}
----
<1> Default encryption settings that will be set for `encryptMetadata`.
<2> Encrypted field using default encryption settings.
<3> Encrypted field overriding the default encryption algorithm.
====
[TIP]
====
The `@EncryptedAnnoation` supports resolving keyIds via SpEL Expressions.
To do so additional environment metadata (via the `MappingContext`) is required and must be provided.
[source,java]
----
@Document
@Encrypted(keyId = "#{mongocrypt.keyId(#target)}")
static class Patient {
@Id String id;
String name;
@Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Random")
String bloodType;
@Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic")
Integer ssn;
}
MongoJsonSchemaCreator schemaCreator = MongoJsonSchemaCreator.create(mappingContext);
MongoJsonSchema personSchema = schemaCreator
.filter(MongoJsonSchemaCreator.encryptedOnly())
.createSchemaFor(Patient.class);
----
The `mongocrypt.keyId` function is defined via an `EvaluationContextExtension` as shown in the snippet below.
Providing a custom extension provides the most flexible way of computing keyIds.
[source,java]
----
public class EncryptionExtension implements EvaluationContextExtension {
@Override
public String getExtensionId() {
return "mongocrypt";
}
@Override
public Map<String, Function> getFunctions() {
return Collections.singletonMap("keyId", new Function(getMethod("computeKeyId", String.class), this));
}
public String computeKeyId(String target) {
// ... lookup via target element name
}
}
----
To combine derived encryption settings with `AutoEncryptionSettings` in a Spring Boot application use the `MongoClientSettingsBuilderCustomizer`.
[source,java]
----
@Bean
MongoClientSettingsBuilderCustomizer customizer(MappingContext mappingContext) {
return (builder) -> {
// ... keyVaultCollection, kmsProvider, ...
MongoJsonSchemaCreator schemaCreator = MongoJsonSchemaCreator.create(mappingContext);
MongoJsonSchema patientSchema = schemaCreator
.filter(MongoJsonSchemaCreator.encryptedOnly())
.createSchemaFor(Patient.class);
AutoEncryptionSettings autoEncryptionSettings = AutoEncryptionSettings.builder()
.keyVaultNamespace(keyVaultCollection)
.kmsProviders(kmsProviders)
.extraOptions(extraOpts)
.schemaMap(Collections.singletonMap("db.patient", patientSchema.schemaDocument().toBsonDocument()))
.build();
builder.autoEncryptionSettings(autoEncryptionSettings);
};
}
----
====
NOTE: Make sure to set the drivers `com.mongodb.AutoEncryptionSettings` to use client-side encryption. MongoDB does not support encryption for all field types. Specific data types require deterministic encryption to preserve equality comparison functionality.
[[mongo.jsonSchema.types]]