Compare commits
15 Commits
issue/4491
...
feature/fl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d46f4bed74 | ||
|
|
e8d0492520 | ||
|
|
bd06bb422a | ||
|
|
16f2cf61e6 | ||
|
|
d769e212d3 | ||
|
|
b580840529 | ||
|
|
bb2d0f5376 | ||
|
|
3448b46739 | ||
|
|
c9aeccd0f5 | ||
|
|
1bbfd5b6de | ||
|
|
0df74b0b61 | ||
|
|
ae27af3d16 | ||
|
|
e7308cd806 | ||
|
|
5c581fc450 | ||
|
|
f16a6c6d34 |
2
pom.xml
2
pom.xml
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<groupId>org.springframework.data</groupId>
|
<groupId>org.springframework.data</groupId>
|
||||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||||
<version>3.3.0-SNAPSHOT</version>
|
<version>3.3.0-FLE-SNAPSHOT</version>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
<name>Spring Data MongoDB</name>
|
<name>Spring Data MongoDB</name>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.data</groupId>
|
<groupId>org.springframework.data</groupId>
|
||||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||||
<version>3.3.0-SNAPSHOT</version>
|
<version>3.3.0-FLE-SNAPSHOT</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.data</groupId>
|
<groupId>org.springframework.data</groupId>
|
||||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||||
<version>3.3.0-SNAPSHOT</version>
|
<version>3.3.0-FLE-SNAPSHOT</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.data</groupId>
|
<groupId>org.springframework.data</groupId>
|
||||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||||
<version>3.3.0-SNAPSHOT</version>
|
<version>3.3.0-FLE-SNAPSHOT</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|||||||
@@ -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,13 +20,19 @@ import java.util.Collection;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
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.PersistentProperty;
|
||||||
import org.springframework.data.mapping.context.MappingContext;
|
import org.springframework.data.mapping.context.MappingContext;
|
||||||
import org.springframework.data.mongodb.core.convert.MongoConverter;
|
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.Field;
|
||||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
|
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
|
||||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
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.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty;
|
||||||
import org.springframework.data.mongodb.core.schema.JsonSchemaObject;
|
import org.springframework.data.mongodb.core.schema.JsonSchemaObject;
|
||||||
import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type;
|
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;
|
||||||
import org.springframework.data.mongodb.core.schema.MongoJsonSchema.MongoJsonSchemaBuilder;
|
import org.springframework.data.mongodb.core.schema.MongoJsonSchema.MongoJsonSchemaBuilder;
|
||||||
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject;
|
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link MongoJsonSchemaCreator} implementation using both {@link MongoConverter} and {@link MappingContext} to obtain
|
* {@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 MongoConverter converter;
|
||||||
private final MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
|
private final MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
|
||||||
|
private final Predicate<JsonSchemaPropertyContext> filter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new instance of {@link MappingMongoJsonSchemaCreator}.
|
* Create a new instance of {@link MappingMongoJsonSchemaCreator}.
|
||||||
@@ -61,10 +70,24 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
|
|||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
MappingMongoJsonSchemaCreator(MongoConverter converter) {
|
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!");
|
Assert.notNull(converter, "Converter must not be null!");
|
||||||
this.converter = converter;
|
this.converter = converter;
|
||||||
this.mappingContext = (MappingContext<MongoPersistentEntity<?>, MongoPersistentProperty>) converter
|
this.mappingContext = mappingContext;
|
||||||
.getMappingContext();
|
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) {
|
public MongoJsonSchema createSchemaFor(Class<?> type) {
|
||||||
|
|
||||||
MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(type);
|
MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(type);
|
||||||
|
if (entity instanceof BasicMongoPersistentEntity) {
|
||||||
|
((BasicMongoPersistentEntity<?>) entity).getEvaluationContext(null);
|
||||||
|
}
|
||||||
|
|
||||||
MongoJsonSchemaBuilder schemaBuilder = MongoJsonSchema.builder();
|
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);
|
List<JsonSchemaProperty> schemaProperties = computePropertiesForEntity(Collections.emptyList(), entity);
|
||||||
schemaBuilder.properties(schemaProperties.toArray(new JsonSchemaProperty[0]));
|
schemaBuilder.properties(schemaProperties.toArray(new JsonSchemaProperty[0]));
|
||||||
|
|
||||||
return schemaBuilder.build();
|
return schemaBuilder.build();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<JsonSchemaProperty> computePropertiesForEntity(List<MongoPersistentProperty> path,
|
private List<JsonSchemaProperty> computePropertiesForEntity(List<MongoPersistentProperty> path,
|
||||||
@@ -93,6 +138,11 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
|
|||||||
|
|
||||||
List<MongoPersistentProperty> currentPath = new ArrayList<>(path);
|
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
|
if (path.contains(nested)) { // cycle guard
|
||||||
schemaProperties.add(createSchemaProperty(computePropertyFieldName(CollectionUtils.lastElement(currentPath)),
|
schemaProperties.add(createSchemaProperty(computePropertyFieldName(CollectionUtils.lastElement(currentPath)),
|
||||||
Object.class, false));
|
Object.class, false));
|
||||||
@@ -120,15 +170,38 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
|
|||||||
|
|
||||||
String fieldName = computePropertyFieldName(property);
|
String fieldName = computePropertyFieldName(property);
|
||||||
|
|
||||||
|
JsonSchemaProperty schemaProperty;
|
||||||
if (property.isCollectionLike()) {
|
if (property.isCollectionLike()) {
|
||||||
return createSchemaProperty(fieldName, targetType, required);
|
schemaProperty = createSchemaProperty(fieldName, targetType, required);
|
||||||
} else if (property.isMap()) {
|
} else if (property.isMap()) {
|
||||||
return createSchemaProperty(fieldName, Type.objectType(), required);
|
schemaProperty = createSchemaProperty(fieldName, Type.objectType(), required);
|
||||||
} else if (ClassUtils.isAssignable(Enum.class, targetType)) {
|
} 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,
|
private JsonSchemaProperty createObjectSchemaPropertyForEntity(List<MongoPersistentProperty> path,
|
||||||
@@ -207,4 +280,30 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
|
|||||||
|
|
||||||
return JsonSchemaProperty.required(property);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,23 @@
|
|||||||
*/
|
*/
|
||||||
package org.springframework.data.mongodb.core;
|
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.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.data.mongodb.core.schema.MongoJsonSchema;
|
||||||
import org.springframework.util.Assert;
|
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
|
* {@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.
|
* specific information available via the {@link org.springframework.data.mongodb.core.mapping.MongoId} annotation.
|
||||||
* </p>
|
* </p>
|
||||||
|
* {@link Encrypted} properties will contain {@literal encrypt} information.
|
||||||
*
|
*
|
||||||
* @author Christoph Strobl
|
* @author Christoph Strobl
|
||||||
* @since 2.2
|
* @since 2.2
|
||||||
@@ -60,6 +77,88 @@ public interface MongoJsonSchemaCreator {
|
|||||||
*/
|
*/
|
||||||
MongoJsonSchema createSchemaFor(Class<?> type);
|
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
|
* Creates a new {@link MongoJsonSchemaCreator} that is aware of conversions applied by the given
|
||||||
* {@link MongoConverter}.
|
* {@link MongoConverter}.
|
||||||
@@ -72,4 +171,41 @@ public interface MongoJsonSchemaCreator {
|
|||||||
Assert.notNull(mongoConverter, "MongoConverter must not be null!");
|
Assert.notNull(mongoConverter, "MongoConverter must not be null!");
|
||||||
return new MappingMongoJsonSchemaCreator(mongoConverter);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,12 @@ package org.springframework.data.mongodb.core.mapping;
|
|||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Modifier;
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.data.annotation.Id;
|
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.PropertyHandler;
|
||||||
import org.springframework.data.mapping.model.BasicPersistentEntity;
|
import org.springframework.data.mapping.model.BasicPersistentEntity;
|
||||||
import org.springframework.data.mongodb.MongoCollectionUtils;
|
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.data.util.TypeInformation;
|
||||||
import org.springframework.expression.EvaluationContext;
|
import org.springframework.expression.EvaluationContext;
|
||||||
import org.springframework.expression.Expression;
|
import org.springframework.expression.Expression;
|
||||||
@@ -212,6 +219,11 @@ public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, Mong
|
|||||||
return super.getEvaluationContext(rootObject);
|
return super.getEvaluationContext(rootObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EvaluationContext getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) {
|
||||||
|
return super.getEvaluationContext(rootObject, dependencies);
|
||||||
|
}
|
||||||
|
|
||||||
private void verifyFieldUniqueness() {
|
private void verifyFieldUniqueness() {
|
||||||
|
|
||||||
AssertFieldNameUniquenessHandler handler = new AssertFieldNameUniquenessHandler();
|
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
|
* @author Christoph Strobl
|
||||||
* @since 1.6
|
* @since 1.6
|
||||||
|
|||||||
@@ -16,7 +16,11 @@
|
|||||||
package org.springframework.data.mongodb.core.mapping;
|
package org.springframework.data.mongodb.core.mapping;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.bson.types.ObjectId;
|
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.Property;
|
||||||
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
|
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
|
||||||
import org.springframework.data.mapping.model.SimpleTypeHolder;
|
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.lang.Nullable;
|
||||||
|
import org.springframework.util.ObjectUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -300,4 +309,43 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope
|
|||||||
return isAnnotationPresent(TextScore.class);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
* @Document
|
||||||
|
* @Encrypted(keyId = "4fPYFM9qSgyRAjgQ2u+IMQ==")
|
||||||
|
* public class Patient {
|
||||||
|
* private ObjectId id;
|
||||||
|
* private String name;
|
||||||
|
*
|
||||||
|
* @Field("publisher_ac")
|
||||||
|
* @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;
|
||||||
|
*
|
||||||
|
* @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 "";
|
||||||
|
}
|
||||||
@@ -46,6 +46,9 @@ public class MongoMappingContext extends AbstractMappingContext<MongoPersistentE
|
|||||||
private FieldNamingStrategy fieldNamingStrategy = DEFAULT_NAMING_STRATEGY;
|
private FieldNamingStrategy fieldNamingStrategy = DEFAULT_NAMING_STRATEGY;
|
||||||
private boolean autoIndexCreation = false;
|
private boolean autoIndexCreation = false;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private ApplicationContext applicationContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new {@link MongoMappingContext}.
|
* Creates a new {@link MongoMappingContext}.
|
||||||
*/
|
*/
|
||||||
@@ -103,6 +106,8 @@ public class MongoMappingContext extends AbstractMappingContext<MongoPersistentE
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||||
|
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
super.setApplicationContext(applicationContext);
|
super.setApplicationContext(applicationContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,4 +150,10 @@ public class MongoMappingContext extends AbstractMappingContext<MongoPersistentE
|
|||||||
|
|
||||||
return new UnwrappedMongoPersistentEntity<>(entity, new UnwrapEntityContext(persistentProperty));
|
return new UnwrappedMongoPersistentEntity<>(entity, new UnwrapEntityContext(persistentProperty));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public ApplicationContext getApplicationContext() {
|
||||||
|
return this.applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.springframework.data.mongodb.core.mapping;
|
package org.springframework.data.mongodb.core.mapping;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
import org.springframework.data.mapping.PersistentEntity;
|
import org.springframework.data.mapping.PersistentEntity;
|
||||||
import org.springframework.data.mapping.model.MutablePersistentEntity;
|
import org.springframework.data.mapping.model.MutablePersistentEntity;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
@@ -102,4 +104,11 @@ public interface MongoPersistentEntity<T> extends MutablePersistentEntity<T, Mon
|
|||||||
return false;
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
package org.springframework.data.mongodb.core.mapping;
|
package org.springframework.data.mongodb.core.mapping;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
import org.springframework.core.convert.converter.Converter;
|
import org.springframework.core.convert.converter.Converter;
|
||||||
import org.springframework.data.annotation.Id;
|
import org.springframework.data.annotation.Id;
|
||||||
import org.springframework.data.mapping.PersistentEntity;
|
import org.springframework.data.mapping.PersistentEntity;
|
||||||
@@ -160,6 +162,13 @@ public interface MongoPersistentProperty extends PersistentProperty<MongoPersist
|
|||||||
return isEntity() && isAnnotationPresent(Unwrapped.class);
|
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.
|
* 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.lang.annotation.Annotation;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Spliterator;
|
import java.util.Spliterator;
|
||||||
@@ -323,4 +324,9 @@ class UnwrappedMongoPersistentEntity<T> implements MongoPersistentEntity<T> {
|
|||||||
public boolean isUnwrapped() {
|
public boolean isUnwrapped() {
|
||||||
return context.getProperty().isUnwrapped();
|
return context.getProperty().isUnwrapped();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
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.annotation.Annotation;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
import org.springframework.data.mapping.Association;
|
import org.springframework.data.mapping.Association;
|
||||||
import org.springframework.data.mapping.PersistentEntity;
|
import org.springframework.data.mapping.PersistentEntity;
|
||||||
@@ -268,6 +269,11 @@ class UnwrappedMongoPersistentProperty implements MongoPersistentProperty {
|
|||||||
return delegate.isUnwrapped();
|
return delegate.isUnwrapped();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<Object> getEncryptionKeyIds() {
|
||||||
|
return delegate.getEncryptionKeyIds();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public Class<?> getComponentType() {
|
public Class<?> getComponentType() {
|
||||||
|
|||||||
@@ -16,7 +16,9 @@
|
|||||||
package org.springframework.data.mongodb.core.schema;
|
package org.springframework.data.mongodb.core.schema;
|
||||||
|
|
||||||
import org.bson.Document;
|
import org.bson.Document;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.util.Assert;
|
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.
|
* 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;
|
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.root = root;
|
||||||
|
this.encryptionMetadata = encryptionMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* (non-Javadoc)
|
* (non-Javadoc)
|
||||||
* @see org.springframework.data.mongodb.core.schema.MongoJsonSchema#toDocument()
|
* @see org.springframework.data.mongodb.core.schema.MongoJsonSchema#schema()
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Document toDocument() {
|
public Document schemaDocument() {
|
||||||
return new Document("$jsonSchema", root.toDocument());
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ class DocumentJsonSchema implements MongoJsonSchema {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* (non-Javadoc)
|
* (non-Javadoc)
|
||||||
* @see org.springframework.data.mongodb.core.schema.MongoJsonSchema#toDocument()
|
* @see org.springframework.data.mongodb.core.schema.MongoJsonSchema#schema()
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Document toDocument() {
|
public Document schemaDocument() {
|
||||||
return new Document("$jsonSchema", new Document(document));
|
return new Document(document);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -523,6 +523,10 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
|
|||||||
public ObjectJsonSchemaProperty generatedDescription() {
|
public ObjectJsonSchemaProperty generatedDescription() {
|
||||||
return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.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 JsonSchemaProperty targetProperty;
|
||||||
private final @Nullable String algorithm;
|
private final @Nullable String algorithm;
|
||||||
private final @Nullable String keyId;
|
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}.
|
* 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,
|
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!");
|
Assert.notNull(target, "Target must not be null!");
|
||||||
this.targetProperty = target;
|
this.targetProperty = target;
|
||||||
@@ -1134,6 +1138,14 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
|
|||||||
return new EncryptedJsonSchemaProperty(targetProperty, algorithm, null, Arrays.asList(keyId));
|
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)
|
* (non-Javadoc)
|
||||||
* @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#toDocument()
|
* @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#toDocument()
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import java.util.Set;
|
|||||||
|
|
||||||
import org.bson.Document;
|
import org.bson.Document;
|
||||||
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject;
|
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
|
* 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 {
|
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
|
* 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.
|
* {@link org.springframework.data.mongodb.core.convert.JsonSchemaMapper} to apply field name customization.
|
||||||
*
|
*
|
||||||
* @return never {@literal null}.
|
* @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.
|
* Create a new {@link MongoJsonSchema} for a given root object.
|
||||||
@@ -108,6 +121,9 @@ public interface MongoJsonSchema {
|
|||||||
|
|
||||||
private ObjectJsonSchemaObject root;
|
private ObjectJsonSchemaObject root;
|
||||||
|
|
||||||
|
@Nullable //
|
||||||
|
private Document encryptionMetadata;
|
||||||
|
|
||||||
MongoJsonSchemaBuilder() {
|
MongoJsonSchemaBuilder() {
|
||||||
root = new ObjectJsonSchemaObject();
|
root = new ObjectJsonSchemaObject();
|
||||||
}
|
}
|
||||||
@@ -266,13 +282,23 @@ public interface MongoJsonSchema {
|
|||||||
return this;
|
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}.
|
* Obtain the {@link MongoJsonSchema}.
|
||||||
*
|
*
|
||||||
* @return new instance of {@link MongoJsonSchema}.
|
* @return new instance of {@link MongoJsonSchema}.
|
||||||
*/
|
*/
|
||||||
public MongoJsonSchema build() {
|
public MongoJsonSchema build() {
|
||||||
return MongoJsonSchema.of(root);
|
return new DefaultMongoJsonSchema(root, encryptionMetadata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -437,6 +437,10 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
|
|||||||
return newInstance(description, true, restrictions);
|
return newInstance(description, true, restrictions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<JsonSchemaProperty> getProperties() {
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* (non-Javadoc)
|
* (non-Javadoc)
|
||||||
* @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#toDocument()
|
* @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#toDocument()
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,23 +19,27 @@ import static org.springframework.data.mongodb.test.util.Assertions.*;
|
|||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.bson.Document;
|
import org.bson.Document;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.context.support.GenericApplicationContext;
|
||||||
import org.springframework.data.annotation.Transient;
|
import org.springframework.data.annotation.Transient;
|
||||||
import org.springframework.data.convert.WritingConverter;
|
import org.springframework.data.convert.WritingConverter;
|
||||||
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
|
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
|
||||||
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
|
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
|
||||||
import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
|
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.Field;
|
||||||
import org.springframework.data.mongodb.core.mapping.FieldType;
|
import org.springframework.data.mongodb.core.mapping.FieldType;
|
||||||
import org.springframework.data.mongodb.core.mapping.MongoId;
|
import org.springframework.data.mongodb.core.mapping.MongoId;
|
||||||
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
|
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
|
||||||
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
|
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}.
|
* Unit tests for {@link MappingMongoJsonSchemaCreator}.
|
||||||
@@ -95,6 +99,64 @@ public class MappingMongoJsonSchemaCreatorUnitTests {
|
|||||||
"{ 'type' : 'object', 'properties' : { '_id' : { 'type' : 'object' }, 'nested' : { 'type' : 'object' } } }");
|
"{ '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
|
// --> TYPES AND JSON
|
||||||
|
|
||||||
// --> ENUM
|
// --> ENUM
|
||||||
@@ -125,8 +187,7 @@ public class MappingMongoJsonSchemaCreatorUnitTests {
|
|||||||
" 'collectionProperty' : { 'type' : 'array' }," + //
|
" 'collectionProperty' : { 'type' : 'array' }," + //
|
||||||
" 'mapProperty' : { 'type' : 'object' }," + //
|
" 'mapProperty' : { 'type' : 'object' }," + //
|
||||||
" 'objectProperty' : { 'type' : 'object' }," + //
|
" 'objectProperty' : { 'type' : 'object' }," + //
|
||||||
" 'enumProperty' : " + JUST_SOME_ENUM + //
|
" 'enumProperty' : " + JUST_SOME_ENUM + " }" + //
|
||||||
" }" + //
|
|
||||||
"}";
|
"}";
|
||||||
|
|
||||||
static class VariousFieldTypes {
|
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==";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
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]]
|
[[mongo.jsonSchema.types]]
|
||||||
|
|||||||
Reference in New Issue
Block a user