From 99203b397a27f9cd595eebb028161b9c054dfd68 Mon Sep 17 00:00:00 2001
From: Christoph Strobl
Date: Tue, 24 Aug 2021 07:06:17 +0200
Subject: [PATCH] Add support for deriving json schema for encrypted
properties.
This commit introduces support for creating a MongoJsonSchema containing encrypted fields for a given type based on mapping metadata.
Using the Encrypted annotation allows to derive required encryptMetadata and encrypt properties within a given (mapping)context.
@Document
@Encrypted(keyId = "...")
static class Patient {
// ...
@Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic")
private Integer ssn;
}
MongoJsonSchemaCreator schemaCreator = MongoJsonSchemaCreator.create(mappingContext);
MongoJsonSchema patientSchema = schemaCreator
.filter(MongoJsonSchemaCreator.encryptedOnly())
.createSchemaFor(Patient.class);
Closes: #3800
Original pull request: #3801.
---
.../mongodb/core/EncryptionAlgorithms.java | 29 ++
.../core/MappingMongoJsonSchemaCreator.java | 109 ++++++-
.../mongodb/core/MongoJsonSchemaCreator.java | 136 +++++++++
.../mapping/BasicMongoPersistentEntity.java | 38 +++
.../mapping/BasicMongoPersistentProperty.java | 48 ++++
.../data/mongodb/core/mapping/Encrypted.java | 112 ++++++++
.../core/mapping/MongoMappingContext.java | 6 +
.../core/mapping/MongoPersistentEntity.java | 9 +
.../core/mapping/MongoPersistentProperty.java | 9 +
.../UnwrappedMongoPersistentEntity.java | 6 +
.../UnwrappedMongoPersistentProperty.java | 6 +
.../core/schema/DefaultMongoJsonSchema.java | 38 ++-
.../core/schema/DocumentJsonSchema.java | 6 +-
.../IdentifiableJsonSchemaProperty.java | 16 +-
.../mongodb/core/schema/MongoJsonSchema.java | 32 ++-
.../core/schema/TypedJsonSchemaObject.java | 4 +
.../util/encryption/EncryptionUtils.java | 67 +++++
.../mongodb/util/spel/ExpressionUtils.java | 52 ++++
...appingMongoJsonSchemaCreatorUnitTests.java | 272 +++++++++++++++++-
.../asciidoc/reference/mongo-json-schema.adoc | 103 +++++++
20 files changed, 1075 insertions(+), 23 deletions(-)
create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java
create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Encrypted.java
create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/encryption/EncryptionUtils.java
create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java
new file mode 100644
index 000000000..0ed7340aa
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java
@@ -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";
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java
index ecbf8a4f0..a53ff8f5a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java
@@ -20,13 +20,19 @@ import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import org.bson.Document;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.convert.MongoConverter;
+import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
+import org.springframework.data.mongodb.core.mapping.Encrypted;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.JsonSchemaObject;
import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type;
@@ -34,10 +40,12 @@ import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema.MongoJsonSchemaBuilder;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject;
+import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
/**
* {@link MongoJsonSchemaCreator} implementation using both {@link MongoConverter} and {@link MappingContext} to obtain
@@ -52,6 +60,7 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
private final MongoConverter converter;
private final MappingContext, MongoPersistentProperty> mappingContext;
+ private final Predicate filter;
/**
* Create a new instance of {@link MappingMongoJsonSchemaCreator}.
@@ -61,10 +70,24 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
@SuppressWarnings("unchecked")
MappingMongoJsonSchemaCreator(MongoConverter converter) {
+ this(converter, (MappingContext, MongoPersistentProperty>) converter.getMappingContext(),
+ (property) -> true);
+ }
+
+ @SuppressWarnings("unchecked")
+ MappingMongoJsonSchemaCreator(MongoConverter converter,
+ MappingContext, MongoPersistentProperty> mappingContext,
+ Predicate filter) {
+
Assert.notNull(converter, "Converter must not be null!");
this.converter = converter;
- this.mappingContext = (MappingContext, MongoPersistentProperty>) converter
- .getMappingContext();
+ this.mappingContext = mappingContext;
+ this.filter = filter;
+ }
+
+ @Override
+ public MongoJsonSchemaCreator filter(Predicate filter) {
+ return new MappingMongoJsonSchemaCreator(converter, mappingContext, filter);
}
/*
@@ -77,11 +100,29 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
MongoPersistentEntity> entity = mappingContext.getRequiredPersistentEntity(type);
MongoJsonSchemaBuilder schemaBuilder = MongoJsonSchema.builder();
+ {
+ Encrypted encrypted = entity.findAnnotation(Encrypted.class);
+ if (encrypted != null) {
+
+ Document encryptionMetadata = new Document();
+
+ Collection
+ * {@link Encrypted} properties will contain {@literal encrypt} information.
*
* @author Christoph Strobl
* @since 2.2
@@ -60,6 +77,88 @@ public interface MongoJsonSchemaCreator {
*/
MongoJsonSchema createSchemaFor(Class> type);
+ /**
+ * Filter matching {@link JsonSchemaProperty properties}.
+ *
+ * @param filter the {@link Predicate} to evaluate for inclusion. Must not be {@literal null}.
+ * @return new instance of {@link MongoJsonSchemaCreator}.
+ * @since 3.3
+ */
+ MongoJsonSchemaCreator filter(Predicate 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
+ * @return {@literal null} if the property is not an entity. It is nevertheless recommend to check
+ * {@link PersistentProperty#isEntity()} first.
+ */
+ @Nullable
+ MongoPersistentEntity 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 encryptedOnly() {
+
+ return new Predicate() {
+
+ // cycle guard
+ private final Set seen = new HashSet<>();
+
+ @Override
+ public boolean test(JsonSchemaPropertyContext context) {
+ return extracted(context.getProperty(), context);
+ }
+
+ private boolean extracted(MongoPersistentProperty property, JsonSchemaPropertyContext context) {
+ if (property.isAnnotationPresent(Encrypted.class)) {
+ return true;
+ }
+
+ if (!property.isEntity() || seen.contains(property)) {
+ return false;
+ }
+
+ seen.add(property);
+
+ for (MongoPersistentProperty nested : context.resolveEntity(property)) {
+ if (extracted(nested, context)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+ }
+
/**
* Creates a new {@link MongoJsonSchemaCreator} that is aware of conversions applied by the given
* {@link MongoConverter}.
@@ -72,4 +171,41 @@ public interface MongoJsonSchemaCreator {
Assert.notNull(mongoConverter, "MongoConverter must not be null!");
return new MappingMongoJsonSchemaCreator(mongoConverter);
}
+
+ /**
+ * Creates a new {@link MongoJsonSchemaCreator} that is aware of type mappings and potential
+ * {@link org.springframework.data.spel.spi.EvaluationContextExtension extensions}.
+ *
+ * @param mappingContext must not be {@literal null}.
+ * @return new instance of {@link MongoJsonSchemaCreator}.
+ * @since 3.3
+ */
+ static MongoJsonSchemaCreator create(MappingContext mappingContext) {
+
+ MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext);
+ converter.setCustomConversions(MongoCustomConversions.create(config -> {}));
+ converter.afterPropertiesSet();
+
+ return create(converter);
+ }
+
+ /**
+ * Creates a new {@link MongoJsonSchemaCreator} that does not consider potential extensions - suitable for testing. We
+ * recommend to use {@link #create(MappingContext)}.
+ *
+ * @return new instance of {@link MongoJsonSchemaCreator}.
+ * @since 3.3
+ */
+ static MongoJsonSchemaCreator create() {
+
+ MongoMappingContext mappingContext = new MongoMappingContext();
+ mappingContext.setSimpleTypeHolder(MongoSimpleTypes.HOLDER);
+ mappingContext.afterPropertiesSet();
+
+ MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext);
+ converter.setCustomConversions(MongoCustomConversions.create(config -> {}));
+ converter.afterPropertiesSet();
+
+ return create(converter);
+ }
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java
index 7bf8214ae..6840fce5b 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java
@@ -17,8 +17,12 @@ package org.springframework.data.mongodb.core.mapping;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import org.springframework.data.annotation.Id;
@@ -28,6 +32,9 @@ import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.mongodb.MongoCollectionUtils;
+import org.springframework.data.mongodb.util.encryption.EncryptionUtils;
+import org.springframework.data.spel.ExpressionDependencies;
+import org.springframework.data.util.Lazy;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
@@ -212,6 +219,11 @@ public class BasicMongoPersistentEntity extends BasicPersistentEntity extends BasicPersistentEntity getEncryptionKeyIds() {
+
+ Encrypted encrypted = findAnnotation(Encrypted.class);
+ if (encrypted == null) {
+ return null;
+ }
+
+ if (ObjectUtils.isEmpty(encrypted.keyId())) {
+ return Collections.emptySet();
+ }
+
+ Lazy evaluationContext = Lazy.of(() -> {
+
+ EvaluationContext ctx = getEvaluationContext(null);
+ ctx.setVariable("target", getType().getSimpleName());
+ return ctx;
+ });
+
+ List