allow usage of evaluation context extension to compute keyIds

This commit is contained in:
Christoph Strobl
2021-09-01 10:33:01 +02:00
parent c9aeccd0f5
commit 3448b46739
14 changed files with 372 additions and 185 deletions

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,23 +20,16 @@ import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.UUID;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.bson.Document;
import org.springframework.context.ApplicationContext;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.DefaultSpELExpressionEvaluator;
import org.springframework.data.mapping.model.SpELContext;
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
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.MongoMappingContext;
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;
@@ -47,13 +40,7 @@ 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.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
@@ -75,6 +62,9 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
private final MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
private final Predicate<JsonSchemaProperty> filter;
@Nullable
private final String wrapperElementName;
/**
* Create a new instance of {@link MappingMongoJsonSchemaCreator}.
*
@@ -83,16 +73,17 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
@SuppressWarnings("unchecked")
MappingMongoJsonSchemaCreator(MongoConverter converter) {
this(converter, (MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty>) converter.getMappingContext(),
this("$jsonSchema", converter, (MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty>) converter.getMappingContext(),
(property) -> true);
}
@SuppressWarnings("unchecked")
MappingMongoJsonSchemaCreator(MongoConverter converter,
MappingMongoJsonSchemaCreator(String wrapperElementName, MongoConverter converter,
MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
Predicate<JsonSchemaProperty> filter) {
Assert.notNull(converter, "Converter must not be null!");
this.wrapperElementName = wrapperElementName;
this.converter = converter;
this.mappingContext = mappingContext;
this.filter = filter;
@@ -100,7 +91,12 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
@Override
public MongoJsonSchemaCreator filter(Predicate<JsonSchemaProperty> filter) {
return new MappingMongoJsonSchemaCreator(converter, mappingContext, filter);
return new MappingMongoJsonSchemaCreator(wrapperElementName, converter, mappingContext, filter);
}
@Override
public MongoJsonSchemaCreator wrapperName(@Nullable String wrapperElementName) {
return new MappingMongoJsonSchemaCreator(wrapperElementName, converter, mappingContext, filter);
}
/*
@@ -111,18 +107,30 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
public MongoJsonSchema createSchemaFor(Class<?> type) {
MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(type);
if(entity instanceof BasicMongoPersistentEntity) {
if (entity instanceof BasicMongoPersistentEntity) {
((BasicMongoPersistentEntity<?>) entity).getEvaluationContext(null);
}
MongoJsonSchemaBuilder schemaBuilder = MongoJsonSchema.builder();
if (entity.isAnnotationPresent(Encrypted.class)) {
MongoJsonSchemaBuilder schemaBuilder = MongoJsonSchema.builder();
schemaBuilder.wrapperObject(wrapperElementName);
{
Encrypted encrypted = entity.findAnnotation(Encrypted.class);
Document encryptionMetadata = new Document("keyId", objectToKeyId(entity.getEncryptionKeyIds()));
if (StringUtils.hasText(encrypted.algorithm())) {
encryptionMetadata.append("algorithm", encrypted.algorithm());
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);
}
schemaBuilder.encryptionMetadata(encryptionMetadata);
}
List<JsonSchemaProperty> schemaProperties = computePropertiesForEntity(Collections.emptyList(), entity);
@@ -150,29 +158,9 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
schemaProperties.add(computeSchemaForProperty(currentPath));
}
// if(!encryptedFieldsOnly) {
// return schemaProperties;
// }
return schemaProperties.stream().filter(filter).collect(Collectors.toList());
}
// private boolean containsEncrypted(JsonSchemaProperty property) {
// if(property instanceof EncryptedJsonSchemaProperty) {
// return true;
// }
//
// if(property instanceof ObjectJsonSchemaProperty) {
// ObjectJsonSchemaProperty val = (ObjectJsonSchemaProperty) property;
// for(JsonSchemaProperty p : val.getProperties()) {
// if(containsEncrypted(p) ) {
// return true;
// }
// }
// }
//
// return false;
// }
private JsonSchemaProperty computeSchemaForProperty(List<MongoPersistentProperty> path) {
MongoPersistentProperty property = CollectionUtils.lastElement(path);
@@ -187,7 +175,7 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
String fieldName = computePropertyFieldName(property);
JsonSchemaProperty schemaProperty = null;
JsonSchemaProperty schemaProperty;
if (property.isCollectionLike()) {
schemaProperty = createSchemaProperty(fieldName, targetType, required);
} else if (property.isMap()) {
@@ -198,19 +186,27 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
schemaProperty = createSchemaProperty(fieldName, targetType, required);
}
if (property.findAnnotation(Encrypted.class) != null) {
EncryptedJsonSchemaProperty enc = new EncryptedJsonSchemaProperty(schemaProperty);
return applyEncryptionDataIfNecessary(property, schemaProperty);
}
Encrypted annotation = property.findAnnotation(Encrypted.class);
enc = enc.algorithm(annotation.algorithm());
@Nullable
private JsonSchemaProperty applyEncryptionDataIfNecessary(MongoPersistentProperty property,
JsonSchemaProperty schemaProperty) {
if (!ObjectUtils.isEmpty(annotation.keyId())) {
enc.keys(objectToKeyId(property.getEncryptionKeyIds()));
}
return enc;
Encrypted encrypted = property.findAnnotation(Encrypted.class);
if (encrypted == null) {
return schemaProperty;
}
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,
@@ -289,70 +285,4 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
return JsonSchemaProperty.required(property);
}
private List<Object> objectToKeyId(Object[] values) {
List<Object> target = new ArrayList<>();
for (Object key : values) {
if(key instanceof UUID) {
target.add(key);
continue;
}
if(key instanceof String) {
try {
target.add(UUID.fromString((String)key));
} catch (IllegalArgumentException e) {
target.add(Document.parse("{ val : { $binary : { base64 : '" + key + "', subType : '04'} } }").get("val"));
// target.add(UuidHelper.decodeBinaryToUuid(key.getBytes(StandardCharsets.UTF_8),
// BsonBinarySubType.UUID_STANDARD.getValue(), UuidRepresentation.STANDARD));
// BsonBinary
// Document d = Document.parse()
// target.add(UUID.nameUUIDFromBytes(Base64Utils.decodeFromString(key)));
// target.add(new Document().append("$binary", new Document().append("base64", key).append("subType", "04")));
}
continue;
}
target.add(key);
}
return target;
}
// boolean isSpelExpression(String value) {
//
// Expression expression = new SpelExpressionParser(new SpelParserConfiguration(null, this.getClass().getClassLoader())).parseExpression(value, ParserContext.TEMPLATE_EXPRESSION);
// return expression instanceof LiteralExpression ? false : true;
// }
//
// Object evaluateSpelExpression(String path, String value) {
//
// SpelExpression spelExpression = new SpelExpressionParser(new SpelParserConfiguration(null, this.getClass().getClassLoader())).parseRaw(value);
//
// StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
//
// if(mappingContext instanceof MongoMappingContext) {
//
// ApplicationContext applicationContext = ((MongoMappingContext) mappingContext).getApplicationContext();
// //evaluationContext.registerFunction();
// evaluationContext.setBeanResolver(new BeanFactoryResolver(applicationContext));
// // evaluationContext.setMethodResolvers();
// evaluationContext.setVariable("target", path);
// }
//
//// if (factory != null) {
//// evaluationContext.setBeanResolver(new BeanFactoryResolver(factory));
//// }
//
// spelExpression.setEvaluationContext(evaluationContext);
// return spelExpression.getValue();
// }
}

View File

@@ -28,6 +28,7 @@ import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProper
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@@ -71,8 +72,27 @@ 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<JsonSchemaProperty> filter);
/**
* Change the default wrapper element name from {@literal $jsonSchema} to the given on. Use {@literal null} to omit
* the wrapper.
*
* @param rootElementName can be {@literal null}.
* @return new instance of {@link MongoJsonSchemaCreator}.
*/
MongoJsonSchemaCreator wrapperName(@Nullable String rootElementName);
/**
* @return new instance of {@link Predicate}.
*/
static Predicate<JsonSchemaProperty> encryptedOnly() {
return new Predicate<JsonSchemaProperty>() {

View File

@@ -18,6 +18,8 @@ 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;
@@ -31,7 +33,10 @@ 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.EvaluationContextProvider;
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;
@@ -55,7 +60,7 @@ import org.springframework.util.StringUtils;
* @author Mark Paluch
*/
public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, MongoPersistentProperty>
implements MongoPersistentEntity<T> {
implements MongoPersistentEntity<T>, EvaluationContextProvider {
private static final String AMBIGUOUS_FIELD_MAPPING = "Ambiguous field mapping detected! Both %s and %s map to the same field name %s! Disambiguate using @Field annotation!";
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
@@ -216,6 +221,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();
@@ -365,28 +375,29 @@ public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, Mong
}
@Override
public Object[] getEncryptionKeyIds() {
public Collection<Object> getEncryptionKeyIds() {
Encrypted encrypted = findAnnotation(Encrypted.class);
if(encrypted == null) {
if (encrypted == null) {
return null;
}
List<Object> target = new ArrayList<>();
EvaluationContext evaluationContext = getEvaluationContext(null);
evaluationContext.setVariable("target", getName());
for(String keyId : encrypted.keyId()) {
Expression expression = detectExpression(keyId);
if(expression == null) {
try {
target.add(UUID.fromString(keyId));
} catch (IllegalArgumentException e) {
target.add(org.bson.Document.parse("{ val : { $binary : { base64 : '" + keyId + "', subType : '04'} } }").get("val"));
}
} else {
target.add(expression.getValue(evaluationContext));
}
if(ObjectUtils.isEmpty(encrypted.keyId())) {
return Collections.emptySet();
}
return target.toArray();
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;
}
/**

View File

@@ -17,10 +17,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 java.util.UUID;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
@@ -33,12 +34,14 @@ 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.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExpressionDependencies;
import org.springframework.data.util.Lazy;
import org.springframework.expression.EvaluationContext;
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.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
@@ -52,7 +55,7 @@ import org.springframework.util.StringUtils;
* @author Divya Srivastava
*/
public class BasicMongoPersistentProperty extends AnnotationBasedPersistentProperty<MongoPersistentProperty>
implements MongoPersistentProperty {
implements MongoPersistentProperty, EvaluationContextProvider {
private static final Logger LOG = LoggerFactory.getLogger(BasicMongoPersistentProperty.class);
@@ -309,32 +312,45 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope
}
@Override
public Object[] getEncryptionKeyIds() {
public EvaluationContext getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) {
if (getOwner() instanceof EvaluationContextProvider) {
return ((EvaluationContextProvider) getOwner()).getEvaluationContext(rootObject, dependencies);
}
return new StandardEvaluationContext();
}
@Override
public EvaluationContext getEvaluationContext(Object rootObject) {
if (getOwner() instanceof EvaluationContextProvider) {
return ((EvaluationContextProvider) getOwner()).getEvaluationContext(rootObject);
}
return new StandardEvaluationContext();
}
@Override
public Collection<Object> getEncryptionKeyIds() {
Encrypted encrypted = findAnnotation(Encrypted.class);
if(encrypted == null) {
if (encrypted == null) {
return null;
}
List<Object> target = new ArrayList<>();
EvaluationContext evaluationContext = ((BasicMongoPersistentEntity)getOwner()). getEvaluationContext(null);
evaluationContext.setVariable("target", getName());
for(String keyId : encrypted.keyId()) {
Expression expression = detectExpression(keyId);
if(expression == null) {
try {
target.add(UUID.fromString(keyId));
} catch (IllegalArgumentException e) {
target.add(org.bson.Document.parse("{ val : { $binary : { base64 : '" + keyId + "', subType : '04'} } }").get("val"));
}
} else {
target.add(expression.getValue(evaluationContext));
}
}
return target.toArray();
}
private Expression detectExpression(String keyId) {
Expression expression = new SpelExpressionParser().parseExpression(keyId, ParserContext.TEMPLATE_EXPRESSION);
return expression instanceof LiteralExpression ? null : expression;
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

@@ -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,5 +104,11 @@ public interface MongoPersistentEntity<T> extends MutablePersistentEntity<T, Mon
return false;
}
Object[] getEncryptionKeyIds();
/**
* @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,7 +162,12 @@ public interface MongoPersistentProperty extends PersistentProperty<MongoPersist
return isEntity() && isAnnotationPresent(Unwrapped.class);
}
Object[] getEncryptionKeyIds();
/**
* @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;
@@ -325,7 +326,7 @@ class UnwrappedMongoPersistentEntity<T> implements MongoPersistentEntity<T> {
}
@Override
public Object[] getEncryptionKeyIds() {
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;
@@ -269,7 +270,7 @@ class UnwrappedMongoPersistentProperty implements MongoPersistentProperty {
}
@Override
public Object[] getEncryptionKeyIds() {
public Collection<Object> getEncryptionKeyIds() {
return delegate.getEncryptionKeyIds();
}

View File

@@ -19,7 +19,7 @@ import org.bson.Document;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Value object representing a MongoDB-specific JSON schema which is the default {@link MongoJsonSchema} implementation.
@@ -30,17 +30,32 @@ import org.springframework.util.ObjectUtils;
*/
class DefaultMongoJsonSchema implements MongoJsonSchema {
@Nullable
private final String wrapperName;
private final JsonSchemaObject root;
private final @Nullable Document encryptionMetadata;
@Nullable //
private final Document encryptionMetadata;
DefaultMongoJsonSchema(JsonSchemaObject root) {
this(root, null);
this("$jsonSchema", root, null);
}
DefaultMongoJsonSchema(JsonSchemaObject root, @Nullable Document encryptionMetadata) {
/**
* Create new instance of {@link DefaultMongoJsonSchema}.
*
* @param root the schema root element.
* @param encryptionMetadata can be {@literal null}.
* @since 3.3
*/
DefaultMongoJsonSchema(@Nullable String wrapperName, JsonSchemaObject root, @Nullable Document encryptionMetadata) {
Assert.notNull(root, "Root must not be null!");
Assert.notNull(root, "Root schema object must not be null!");
this.wrapperName = wrapperName;
this.root = root;
this.encryptionMetadata = encryptionMetadata;
}
@@ -52,10 +67,17 @@ class DefaultMongoJsonSchema implements MongoJsonSchema {
@Override
public Document toDocument() {
Document schemaDocument = root.toDocument();
if(!CollectionUtils.isEmpty(encryptionMetadata)) {
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);
}
return new Document("$jsonSchema", schemaDocument);
schemaDocument.putAll(root.toDocument());
if(!StringUtils.hasText(wrapperName)) {
return schemaDocument;
}
return new Document(wrapperName, schemaDocument);
}
}

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
@@ -106,9 +107,13 @@ public interface MongoJsonSchema {
*/
class MongoJsonSchemaBuilder {
@Nullable
private String wrapperName = "$jsonSchema";
private ObjectJsonSchemaObject root;
@Nullable //
private Document encryptionMetadata;
private boolean encryptedFieldsOnly;
MongoJsonSchemaBuilder() {
root = new ObjectJsonSchemaObject();
@@ -268,17 +273,31 @@ public interface MongoJsonSchema {
return this;
}
public void encryptionMetadata(Document encryptionMetadata) {
/**
* 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;
}
/**
*
* @param name can be {@literal null}.
*/
public void wrapperObject(@Nullable String name) {
this.wrapperName = name;
}
/**
* Obtain the {@link MongoJsonSchema}.
*
* @return new instance of {@link MongoJsonSchema}.
*/
public MongoJsonSchema build() {
return new DefaultMongoJsonSchema(root, encryptionMetadata);
return new DefaultMongoJsonSchema(wrapperName, root, encryptionMetadata);
}
}
}

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

@@ -291,9 +291,9 @@ public class MappingMongoJsonSchemaCreatorUnitTests {
public void csfle/*encryptedFieldsOnly*/() {
MongoJsonSchema schema = MongoJsonSchemaCreator.create().filter(MongoJsonSchemaCreator.encryptedOnly())
.createSchemaFor(Patient.class);
.wrapperName("db.patient").createSchemaFor(Patient.class);
Document $jsonSchema = schema.toDocument().get("$jsonSchema", Document.class);
Document $jsonSchema = schema.toDocument().get("db.patient", Document.class);
System.out.println($jsonSchema.toJson(JsonWriterSettings.builder().indent(true).build()));
assertThat($jsonSchema).isEqualTo(Document.parse(patientSchema));
@@ -346,6 +346,10 @@ public class MappingMongoJsonSchemaCreatorUnitTests {
@Encrypted(keyId = "#{mongocrypt.computeKeyId(#target)}")
static class MethodSpELPatient {
@EncryptedField(keyId = "#{mongocrypt.computeKeyId(#target)}", algorithm = EncryptionAlgorithms.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic)
Integer policyNumber;
String provider;
}
public static class EncryptionExtension implements EvaluationContextExtension {
@@ -374,7 +378,7 @@ public class MappingMongoJsonSchemaCreatorUnitTests {
@Override
public Map<String, Function> getFunctions() {
try {
return Collections.<String, Function>singletonMap("computeKeyId", new Function(EncryptionExtension.class.getMethod("computeKeyId", String.class), this));
return Collections.singletonMap("computeKeyId", new Function(EncryptionExtension.class.getMethod("computeKeyId", String.class), this));
} catch (NoSuchMethodException e) {
e.printStackTrace();
}