allow usage of evaluation context extension to compute keyIds
This commit is contained in:
@@ -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";
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
// }
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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>() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user