From da0cb6d766e92722291d3ce67ed67d28413c7629 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 19 Jun 2019 14:16:39 +0200 Subject: [PATCH] DATAMONGO-2306 - Add field level encryption to JSON Schema. We now support encrypted fields via MongoJsonSchema and allow XML configuration of com.mongodb.AutoEncryptionSettings via a dedicated factory bean. Original pull request: #766. --- .../core/MongoClientOptionsFactoryBean.java | 15 +- .../MongoEncryptionSettingsFactoryBean.java | 119 ++++++++++++++ .../IdentifiableJsonSchemaProperty.java | 145 ++++++++++++++++++ .../mongodb/core/schema/JsonSchemaObject.java | 42 +++++ .../core/schema/JsonSchemaProperty.java | 22 +-- .../data/mongodb/config/spring-mongo-2.2.xsd | 21 +++ ...ngoEncryptionSettingsFactoryBeanTests.java | 50 ++++++ .../core/schema/MongoJsonSchemaUnitTests.java | 16 ++ .../asciidoc/reference/mongo-json-schema.adoc | 22 +++ 9 files changed, 441 insertions(+), 11 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoEncryptionSettingsFactoryBean.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoEncryptionSettingsFactoryBeanTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientOptionsFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientOptionsFactoryBean.java index d3e169a18..249815ccf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientOptionsFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientOptionsFactoryBean.java @@ -22,6 +22,7 @@ import org.springframework.beans.factory.config.AbstractFactoryBean; import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.lang.Nullable; +import com.mongodb.AutoEncryptionSettings; import com.mongodb.DBDecoderFactory; import com.mongodb.DBEncoderFactory; import com.mongodb.MongoClient; @@ -73,6 +74,7 @@ public class MongoClientOptionsFactoryBean extends AbstractFactoryBean { + + private boolean bypassAutoEncryption; + private String keyVaultNamespace; + private Map extraOptions; + private MongoClientSettings keyVaultClientSettings; + private Map> kmsProviders; + private Map schemaMap; + + /** + * @param bypassAutoEncryption + * @see AutoEncryptionSettings.Builder#bypassAutoEncryption(boolean) + */ + public void setBypassAutoEncryption(boolean bypassAutoEncryption) { + this.bypassAutoEncryption = bypassAutoEncryption; + } + + /** + * @param extraOptions + * @see AutoEncryptionSettings.Builder#extraOptions(Map) + */ + public void setExtraOptions(Map extraOptions) { + this.extraOptions = extraOptions; + } + + /** + * @param keyVaultNamespace + * @see AutoEncryptionSettings.Builder#keyVaultNamespace(String) + */ + public void setKeyVaultNamespace(String keyVaultNamespace) { + this.keyVaultNamespace = keyVaultNamespace; + } + + /** + * @param keyVaultClientSettings + * @see AutoEncryptionSettings.Builder#keyVaultMongoClientSettings(MongoClientSettings) + */ + public void setKeyVaultClientSettings(MongoClientSettings keyVaultClientSettings) { + this.keyVaultClientSettings = keyVaultClientSettings; + } + + /** + * @param kmsProviders + * @see AutoEncryptionSettings.Builder#kmsProviders(Map) + */ + public void setKmsProviders(Map> kmsProviders) { + this.kmsProviders = kmsProviders; + } + + /** + * @param schemaMap + * @see AutoEncryptionSettings.Builder#schemaMap(Map) + */ + public void setSchemaMap(Map schemaMap) { + this.schemaMap = schemaMap; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.FactoryBean#getObject() + */ + @Override + public AutoEncryptionSettings getObject() { + + return AutoEncryptionSettings.builder() // + .bypassAutoEncryption(bypassAutoEncryption) // + .keyVaultNamespace(keyVaultNamespace) // + .keyVaultMongoClientSettings(keyVaultClientSettings) // + .kmsProviders(orEmpty(kmsProviders)) // + .extraOptions(orEmpty(extraOptions)) // + .schemaMap(orEmpty(schemaMap)) // + .build(); + } + + private Map orEmpty(Map source) { + return source != null ? source : Collections.emptyMap(); + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.FactoryBean#getObjectType() + */ + @Override + public Class getObjectType() { + return AutoEncryptionSettings.class; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java index bf2e80186..1a097f4ab 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java @@ -30,7 +30,9 @@ import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.Numeri import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.StringJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.TimestampJsonSchemaObject; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; /** * {@link JsonSchemaProperty} implementation. @@ -1044,4 +1046,147 @@ public class IdentifiableJsonSchemaProperty implemen return required; } } + + /** + * {@link JsonSchemaProperty} implementation for encrypted fields. + * + * @author Christoph Strobl + * @since 2.2 + */ + public static class EncryptedJsonSchemaProperty implements JsonSchemaProperty { + + private final JsonSchemaProperty targetProperty; + private final @Nullable String algorithm; + private final @Nullable char[] keyId; + private final @Nullable char[] iv; + + /** + * Create new instance of {@link EncryptedJsonSchemaProperty} wrapping the given {@link JsonSchemaProperty target}. + * + * @param target must not be {@literal null}. + */ + public EncryptedJsonSchemaProperty(JsonSchemaProperty target) { + this(target, null, null, null); + } + + private EncryptedJsonSchemaProperty(JsonSchemaProperty target, @Nullable String algorithm, @Nullable char[] keyId, + @Nullable char[] iv) { + + Assert.notNull(target, "Target must not be null!"); + this.targetProperty = target; + this.algorithm = algorithm; + this.keyId = keyId; + this.iv = iv; + } + + /** + * Create new instance of {@link EncryptedJsonSchemaProperty} wrapping the given {@link JsonSchemaProperty target}. + * + * @param target must not be {@literal null}. + * @return new instance of {@link EncryptedJsonSchemaProperty}. + */ + public static EncryptedJsonSchemaProperty encrypted(JsonSchemaProperty target) { + return new EncryptedJsonSchemaProperty(target); + } + + /** + * Use {@literal AEAD_AES_256_CBC_HMAC_SHA_512-Random} algorithm. + * + * @return new instance of {@link EncryptedJsonSchemaProperty}. + */ + public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_random() { + return algorithm("AEAD_AES_256_CBC_HMAC_SHA_512-Random"); + } + + /** + * Use {@literal AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic} algorithm. + * + * @return new instance of {@link EncryptedJsonSchemaProperty}. + */ + public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_deterministic() { + return algorithm("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"); + } + + /** + * Use the given algorithm identified via its name. + * + * @return new instance of {@link EncryptedJsonSchemaProperty}. + */ + public EncryptedJsonSchemaProperty algorithm(String algorithm) { + return new EncryptedJsonSchemaProperty(targetProperty, algorithm, keyId, iv); + } + + /** + * @param key + * @return + */ + public EncryptedJsonSchemaProperty keyId(char[] key) { + return new EncryptedJsonSchemaProperty(targetProperty, algorithm, key, iv); + } + + public EncryptedJsonSchemaProperty keyId(String key) { + return keyId(key.toCharArray()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#toDocument() + */ + @Override + public Document toDocument() { + + Document doc = targetProperty.toDocument(); + Document propertySpecification = doc.get(targetProperty.getIdentifier(), Document.class); + + Document enc = new Document(); + + if (!ObjectUtils.isEmpty(keyId)) { + enc.append("keyId", new String(keyId)); + } + + Type type = extractPropertyType(propertySpecification); + if (type != null) { + + propertySpecification.remove(type.representation()); + enc.append("bsonType", type.toBsonType().value()); // TODO: no samples with type -> is it bson type all the way? + } + + enc.append("algorithm", algorithm); + + propertySpecification.append("encrypt", enc); + + return doc; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.JsonSchemaProperty#getIdentifier() + */ + @Override + public String getIdentifier() { + return targetProperty.getIdentifier(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#getTypes() + */ + @Override + public Set getTypes() { + return targetProperty.getTypes(); + } + + @Nullable + private Type extractPropertyType(Document source) { + + if (source.containsKey("type")) { + return Type.of(source.get("type", String.class)); + } + if (source.containsKey("bsonType")) { + return Type.of(source.get("bsonType", String.class)); + } + + return null; + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java index 375845ada..5f0421272 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core.schema; +import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; import java.math.BigDecimal; @@ -428,6 +429,23 @@ public interface JsonSchemaObject { return new JsonType(name); } + /** + * Create a {@link Type} with its default {@link Type#representation() representation} via the name. + * + * @param name must not be {@literal null}. + * @return the matching type instance. + * @since 2.2 + */ + static Type of(String name) { + + Type type = jsonTypeOf(name); + if (jsonTypes().contains(type)) { + return type; + } + + return bsonTypeOf(name); + } + /** * @return all known JSON types. */ @@ -456,11 +474,34 @@ public interface JsonSchemaObject { */ Object value(); + /** + * Get the {@literal bsonType} representation of the given type. + * + * @return never {@literal null}. + * @since 2.2 + */ + default Type toBsonType() { + + if (representation().equals("bsonType")) { + return this; + } + + if (value().equals(Type.booleanType().value())) { + return bsonTypeOf("bool"); + } + if (value().equals(Type.numberType().value())) { + return bsonTypeOf("long"); + } + + return bsonTypeOf((String) value()); + } + /** * @author Christpoh Strobl * @since 2.1 */ @RequiredArgsConstructor + @EqualsAndHashCode class JsonType implements Type { private final String name; @@ -489,6 +530,7 @@ public interface JsonSchemaObject { * @since 2.1 */ @RequiredArgsConstructor + @EqualsAndHashCode class BsonType implements Type { private final String name; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java index 36a44eb3f..3bd149c53 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java @@ -18,18 +18,9 @@ package org.springframework.data.mongodb.core.schema; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; -import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ArrayJsonSchemaProperty; -import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.BooleanJsonSchemaProperty; -import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.DateJsonSchemaProperty; -import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.NullJsonSchemaProperty; -import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.NumericJsonSchemaProperty; -import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty; -import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.RequiredJsonSchemaProperty; -import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.StringJsonSchemaProperty; -import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.TimestampJsonSchemaProperty; -import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.UntypedJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.NumericJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.*; import org.springframework.lang.Nullable; /** @@ -68,6 +59,17 @@ public interface JsonSchemaProperty extends JsonSchemaObject { return new UntypedJsonSchemaProperty(identifier, JsonSchemaObject.untyped()); } + /** + * Turns the given target property into an {@link EncryptedJsonSchemaProperty ecrypted} one. + * + * @param property must not be {@literal null}. + * @return new instance of {@link EncryptedJsonSchemaProperty}. + * @since 2.2 + */ + static EncryptedJsonSchemaProperty encrypted(JsonSchemaProperty property) { + return EncryptedJsonSchemaProperty.encrypted(property); + } + /** * Creates a new {@link StringJsonSchemaProperty} with given {@literal identifier} of {@code type : 'string'}. * diff --git a/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-2.2.xsd b/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-2.2.xsd index 3b27d2a2f..bc1c52091 100644 --- a/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-2.2.xsd +++ b/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-2.2.xsd @@ -311,6 +311,20 @@ The name of the MongoClient object that determines what server to monitor. (by d + + + + + + + + + + + + @@ -546,6 +560,13 @@ The SSLSocketFactory to use for the SSL connection. If none is configured here, ]]> + + + + + diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoEncryptionSettingsFactoryBeanTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoEncryptionSettingsFactoryBeanTests.java new file mode 100644 index 000000000..c701e7e05 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoEncryptionSettingsFactoryBeanTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2019 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; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.test.util.ReflectionTestUtils; + +import com.mongodb.AutoEncryptionSettings; + +/** + * Integration tests for {@link MongoEncryptionSettingsFactoryBean}. + * + * @author Christoph Strobl + */ +public class MongoEncryptionSettingsFactoryBeanTests { + + @Test // DATAMONGO-2306 + public void createsAutoEncryptionSettings() { + + RootBeanDefinition definition = new RootBeanDefinition(MongoEncryptionSettingsFactoryBean.class); + definition.getPropertyValues().addPropertyValue("bypassAutoEncryption", true); + definition.getPropertyValues().addPropertyValue("keyVaultNamespace", "ns"); + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + factory.registerBeanDefinition("factory", definition); + + MongoEncryptionSettingsFactoryBean bean = factory.getBean("&factory", MongoEncryptionSettingsFactoryBean.class); + assertThat(ReflectionTestUtils.getField(bean, "bypassAutoEncryption")).isEqualTo(true); + + AutoEncryptionSettings target = factory.getBean(AutoEncryptionSettings.class); + assertThat(target.getKeyVaultNamespace()).isEqualTo("ns"); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java index 9a2fa794c..7ffbbce36 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core.schema; +import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.*; import static org.springframework.data.mongodb.test.util.Assertions.*; import java.util.Arrays; @@ -71,6 +72,21 @@ public class MongoJsonSchemaUnitTests { new Document("lastname", new Document("type", "string"))))); } + @Test // DATAMONGO-2306 + public void rendersEncryptedPropertyCorrectly() { + + MongoJsonSchema schema = MongoJsonSchema.builder().properties( // + encrypted(string("ssn")) // + .aead_aes_256_cbc_hmac_sha_512_deterministic() // + .keyId("*key0_id") // + ).build(); + + assertThat(schema.toDocument()).isEqualTo(new Document("$jsonSchema", + new Document("type", "object").append("properties", + new Document("ssn", new Document("encrypt", new Document("keyId", "*key0_id") + .append("algorithm", "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic").append("bsonType", "string")))))); + } + @Test // DATAMONGO-1835 public void throwsExceptionOnNullRoot() { assertThatIllegalArgumentException().isThrownBy(() -> MongoJsonSchema.of((JsonSchemaObject) null)); diff --git a/src/main/asciidoc/reference/mongo-json-schema.adoc b/src/main/asciidoc/reference/mongo-json-schema.adoc index 2f1bcf840..e4e7f05c5 100644 --- a/src/main/asciidoc/reference/mongo-json-schema.adoc +++ b/src/main/asciidoc/reference/mongo-json-schema.adoc @@ -205,6 +205,28 @@ template.find(query(matchingDocumentStructure(schema)), Person.class); ---- ==== +[[mongo.jsonSchema.encrypted-fields]] +==== Encrypted Fields + +MongoDB 4.2 https://docs.mongodb.com/master/core/security-client-side-encryption/[Field Level Encryption] allows to directly secure certain properties. + +Properties can be wrapped within an encrypted property when setting up the JSON Schema as shown in the example below. + +.Client-Side Field Level Encryption via Json Schema +==== +[source,java] +---- +MongoJsonSchema schema = MongoJsonSchema.builder() + .properties( + encrypted(string("ssn")) + .algorithm("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic") + .keyId("*key0_id") + ).build(); +---- +==== + +NOTE: Make sure to set the drivers `com.mongodb.AutoEncryptionSettings` to use client side encryption. + [[mongo.jsonSchema.types]] ==== JSON Schema Types