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.
This commit is contained in:
Christoph Strobl
2019-06-19 14:16:39 +02:00
committed by Mark Paluch
parent 56ac8397aa
commit da0cb6d766
9 changed files with 441 additions and 11 deletions

View File

@@ -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<MongoClie
private boolean ssl;
private @Nullable SSLSocketFactory sslSocketFactory;
private @Nullable AutoEncryptionSettings autoEncryptionSettings;
/**
* Set the {@link MongoClient} description.
@@ -272,6 +274,16 @@ public class MongoClientOptionsFactoryBean extends AbstractFactoryBean<MongoClie
this.serverSelectionTimeout = serverSelectionTimeout;
}
/**
* Set the {@link AutoEncryptionSettings} to be used.
*
* @param autoEncryptionSettings can be {@literal null}.
* @since 2.2
*/
public void setAutoEncryptionSettings(@Nullable AutoEncryptionSettings autoEncryptionSettings) {
this.autoEncryptionSettings = autoEncryptionSettings;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.config.AbstractFactoryBean#createInstance()
@@ -304,7 +316,8 @@ public class MongoClientOptionsFactoryBean extends AbstractFactoryBean<MongoClie
.requiredReplicaSetName(requiredReplicaSetName) //
.serverSelectionTimeout(serverSelectionTimeout) //
.sslEnabled(ssl) //
.socketFactory(socketFactoryToUse) // TODO: Mongo Driver 4 - remove if not available
.autoEncryptionSettings(autoEncryptionSettings) //
.socketFactory(socketFactoryToUse) // TODO: Mongo Driver 4 -
.socketKeepAlive(socketKeepAlive) // TODO: Mongo Driver 4 - remove if not available
.socketTimeout(socketTimeout) //
.threadsAllowedToBlockForConnectionMultiplier(threadsAllowedToBlockForConnectionMultiplier) //

View File

@@ -0,0 +1,119 @@
/*
* 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 java.util.Collections;
import java.util.Map;
import org.bson.BsonDocument;
import org.springframework.beans.factory.FactoryBean;
import com.mongodb.AutoEncryptionSettings;
import com.mongodb.MongoClientSettings;
/**
* {@link FactoryBean} for creating {@link AutoEncryptionSettings} using the {@link AutoEncryptionSettings.Builder}.
*
* @author Christoph Strobl
* @since 2.2
*/
public class MongoEncryptionSettingsFactoryBean implements FactoryBean<AutoEncryptionSettings> {
private boolean bypassAutoEncryption;
private String keyVaultNamespace;
private Map<String, Object> extraOptions;
private MongoClientSettings keyVaultClientSettings;
private Map<String, Map<String, Object>> kmsProviders;
private Map<String, BsonDocument> 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<String, Object> 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<String, Map<String, Object>> kmsProviders) {
this.kmsProviders = kmsProviders;
}
/**
* @param schemaMap
* @see AutoEncryptionSettings.Builder#schemaMap(Map)
*/
public void setSchemaMap(Map<String, BsonDocument> 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 <K, V> Map<K, V> orEmpty(Map<K, V> source) {
return source != null ? source : Collections.emptyMap();
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.FactoryBean#getObjectType()
*/
@Override
public Class<?> getObjectType() {
return AutoEncryptionSettings.class;
}
}

View File

@@ -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<T extends JsonSchemaObject> 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<Type> 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;
}
}
}

View File

@@ -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;

View File

@@ -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'}.
*

View File

@@ -311,6 +311,20 @@ The name of the MongoClient object that determines what server to monitor. (by d
<xsd:union memberTypes="xsd:string"/>
</xsd:simpleType>
<xsd:simpleType name="encryptionSettingsRef">
<xsd:annotation>
<xsd:documentation><![CDATA[
Reference to FactoryBean for com.mongodb.AutoEncryptionSettings - @since 2.2
]]></xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:assignable-to type="org.springframework.data.mongodb.core.MongoEncryptionSettingsFactoryBean" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
<xsd:union memberTypes="xsd:string"/>
</xsd:simpleType>
<xsd:simpleType name="writeConcernEnumeration">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="NONE" />
@@ -546,6 +560,13 @@ The SSLSocketFactory to use for the SSL connection. If none is configured here,
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="encryption-settings-ref" type="encryptionSettingsRef" use="optional">
<xsd:annotation>
<xsd:documentation><![CDATA[
AutoEncryptionSettings for MongoDB 4.2+ - @since 2.2
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
</xsd:complexType>
<xsd:group name="beanElementGroup">

View File

@@ -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");
}
}

View File

@@ -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));

View File

@@ -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