Browse Source

Adding queryable encryption range support

Supports range style queries for encrypted fields
issue/4185-light
Ross Lawley 11 months ago committed by Christoph Strobl
parent
commit
720aff2649
No known key found for this signature in database
GPG Key ID: E6054036D0C37A4B
  1. 56
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
  2. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java
  3. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java
  4. 24
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java
  5. 22
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
  6. 7
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java
  7. 83
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java
  8. 15
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java
  9. 10
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java
  10. 188
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java
  11. 23
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java
  12. 23
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java
  13. 27
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java
  14. 373
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java

56
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java

@ -19,6 +19,7 @@ import java.time.Duration; @@ -19,6 +19,7 @@ import java.time.Duration;
import java.util.Optional;
import java.util.function.Function;
import org.bson.conversions.Bson;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
@ -41,6 +42,7 @@ import com.mongodb.client.model.ValidationLevel; @@ -41,6 +42,7 @@ import com.mongodb.client.model.ValidationLevel;
* @author Mark Paluch
* @author Andreas Zink
* @author Ben Foster
* @author Ross Lawley
*/
public class CollectionOptions {
@ -51,10 +53,11 @@ public class CollectionOptions { @@ -51,10 +53,11 @@ public class CollectionOptions {
private ValidationOptions validationOptions;
private @Nullable TimeSeriesOptions timeSeriesOptions;
private @Nullable CollectionChangeStreamOptions changeStreamOptions;
private @Nullable Bson encryptedFields;
private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nullable Boolean capped,
@Nullable Collation collation, ValidationOptions validationOptions, @Nullable TimeSeriesOptions timeSeriesOptions,
@Nullable CollectionChangeStreamOptions changeStreamOptions) {
@Nullable CollectionChangeStreamOptions changeStreamOptions, @Nullable Bson encryptedFields) {
this.maxDocuments = maxDocuments;
this.size = size;
@ -63,6 +66,7 @@ public class CollectionOptions { @@ -63,6 +66,7 @@ public class CollectionOptions {
this.validationOptions = validationOptions;
this.timeSeriesOptions = timeSeriesOptions;
this.changeStreamOptions = changeStreamOptions;
this.encryptedFields = encryptedFields;
}
/**
@ -76,7 +80,7 @@ public class CollectionOptions { @@ -76,7 +80,7 @@ public class CollectionOptions {
Assert.notNull(collation, "Collation must not be null");
return new CollectionOptions(null, null, null, collation, ValidationOptions.none(), null, null);
return new CollectionOptions(null, null, null, collation, ValidationOptions.none(), null, null, null);
}
/**
@ -86,7 +90,7 @@ public class CollectionOptions { @@ -86,7 +90,7 @@ public class CollectionOptions {
* @since 2.0
*/
public static CollectionOptions empty() {
return new CollectionOptions(null, null, null, null, ValidationOptions.none(), null, null);
return new CollectionOptions(null, null, null, null, ValidationOptions.none(), null, null, null);
}
/**
@ -136,7 +140,7 @@ public class CollectionOptions { @@ -136,7 +140,7 @@ public class CollectionOptions {
*/
public CollectionOptions capped() {
return new CollectionOptions(size, maxDocuments, true, collation, validationOptions, timeSeriesOptions,
changeStreamOptions);
changeStreamOptions, encryptedFields);
}
/**
@ -148,7 +152,7 @@ public class CollectionOptions { @@ -148,7 +152,7 @@ public class CollectionOptions {
*/
public CollectionOptions maxDocuments(long maxDocuments) {
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
changeStreamOptions);
changeStreamOptions, encryptedFields);
}
/**
@ -160,7 +164,7 @@ public class CollectionOptions { @@ -160,7 +164,7 @@ public class CollectionOptions {
*/
public CollectionOptions size(long size) {
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
changeStreamOptions);
changeStreamOptions, encryptedFields);
}
/**
@ -172,7 +176,7 @@ public class CollectionOptions { @@ -172,7 +176,7 @@ public class CollectionOptions {
*/
public CollectionOptions collation(@Nullable Collation collation) {
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
changeStreamOptions);
changeStreamOptions, encryptedFields);
}
/**
@ -293,7 +297,7 @@ public class CollectionOptions { @@ -293,7 +297,7 @@ public class CollectionOptions {
Assert.notNull(validationOptions, "ValidationOptions must not be null");
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
changeStreamOptions);
changeStreamOptions, encryptedFields);
}
/**
@ -307,7 +311,7 @@ public class CollectionOptions { @@ -307,7 +311,7 @@ public class CollectionOptions {
Assert.notNull(timeSeriesOptions, "TimeSeriesOptions must not be null");
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
changeStreamOptions);
changeStreamOptions, encryptedFields);
}
/**
@ -321,7 +325,19 @@ public class CollectionOptions { @@ -321,7 +325,19 @@ public class CollectionOptions {
Assert.notNull(changeStreamOptions, "ChangeStreamOptions must not be null");
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
changeStreamOptions);
changeStreamOptions, encryptedFields);
}
/**
* Create new {@link CollectionOptions} with the given {@code encryptedFields}.
*
* @param encryptedFields can be null
* @return new instance of {@link CollectionOptions}.
* @since 4.5.0
*/
public CollectionOptions encryptedFields(@Nullable Bson encryptedFields) {
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
changeStreamOptions, encryptedFields);
}
/**
@ -392,12 +408,22 @@ public class CollectionOptions { @@ -392,12 +408,22 @@ public class CollectionOptions {
return Optional.ofNullable(changeStreamOptions);
}
/**
* Get the {@code encryptedFields} if available.
*
* @return {@link Optional#empty()} if not specified.
* @since 4.5.0
*/
public Optional<Bson> getEncryptedFields() {
return Optional.ofNullable(encryptedFields);
}
@Override
public String toString() {
return "CollectionOptions{" + "maxDocuments=" + maxDocuments + ", size=" + size + ", capped=" + capped
+ ", collation=" + collation + ", validationOptions=" + validationOptions + ", timeSeriesOptions="
+ timeSeriesOptions + ", changeStreamOptions=" + changeStreamOptions + ", disableValidation="
+ disableValidation() + ", strictValidation=" + strictValidation() + ", moderateValidation="
+ timeSeriesOptions + ", changeStreamOptions=" + changeStreamOptions + ", encryptedFields=" + encryptedFields
+ ", disableValidation=" + disableValidation() + ", strictValidation=" + strictValidation() + ", moderateValidation="
+ moderateValidation() + ", warnOnValidationError=" + warnOnValidationError() + ", failOnValidationError="
+ failOnValidationError() + '}';
}
@ -431,7 +457,10 @@ public class CollectionOptions { @@ -431,7 +457,10 @@ public class CollectionOptions {
if (!ObjectUtils.nullSafeEquals(timeSeriesOptions, that.timeSeriesOptions)) {
return false;
}
return ObjectUtils.nullSafeEquals(changeStreamOptions, that.changeStreamOptions);
if (!ObjectUtils.nullSafeEquals(changeStreamOptions, that.changeStreamOptions)) {
return false;
}
return ObjectUtils.nullSafeEquals(encryptedFields, that.encryptedFields);
}
@Override
@ -443,6 +472,7 @@ public class CollectionOptions { @@ -443,6 +472,7 @@ public class CollectionOptions {
result = 31 * result + ObjectUtils.nullSafeHashCode(validationOptions);
result = 31 * result + ObjectUtils.nullSafeHashCode(timeSeriesOptions);
result = 31 * result + ObjectUtils.nullSafeHashCode(changeStreamOptions);
result = 31 * result + ObjectUtils.nullSafeHashCode(encryptedFields);
return result;
}

2
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java

@ -19,11 +19,13 @@ package org.springframework.data.mongodb.core; @@ -19,11 +19,13 @@ package org.springframework.data.mongodb.core;
* Encryption algorithms supported by MongoDB Client Side Field Level Encryption.
*
* @author Christoph Strobl
* @author Ross Lawley
* @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";
public static final String RANGE = "Range";
}

2
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java

@ -83,6 +83,7 @@ import com.mongodb.client.model.ValidationOptions; @@ -83,6 +83,7 @@ import com.mongodb.client.model.ValidationOptions;
* @author Mark Paluch
* @author Christoph Strobl
* @author Ben Foster
* @author Ross Lawley
* @since 2.1
* @see MongoTemplate
* @see ReactiveMongoTemplate
@ -378,6 +379,7 @@ class EntityOperations { @@ -378,6 +379,7 @@ class EntityOperations {
collectionOptions.getChangeStreamOptions().ifPresent(it -> result
.changeStreamPreAndPostImagesOptions(new ChangeStreamPreAndPostImagesOptions(it.getPreAndPostImages())));
collectionOptions.getEncryptedFields().ifPresent(result::encryptedFields);
return result;
}

24
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java

@ -28,29 +28,44 @@ import org.springframework.lang.Nullable; @@ -28,29 +28,44 @@ import org.springframework.lang.Nullable;
* {@link ValueConversionContext} that allows to delegate read/write to an underlying {@link MongoConverter}.
*
* @author Christoph Strobl
* @author Ross Lawley
* @since 3.4
*/
public class MongoConversionContext implements ValueConversionContext<MongoPersistentProperty> {
private final PropertyValueProvider<MongoPersistentProperty> accessor; // TODO: generics
private final @Nullable MongoPersistentProperty persistentProperty;
private final MongoConverter mongoConverter;
@Nullable private final MongoPersistentProperty persistentProperty;
@Nullable private final SpELContext spELContext;
@Nullable private final String fieldNameAndQueryOperator;
public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
@Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) {
this(accessor, persistentProperty, mongoConverter, null);
this(accessor, persistentProperty, mongoConverter, null, null);
}
public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
@Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter,
@Nullable SpELContext spELContext) {
this(accessor, persistentProperty, mongoConverter, spELContext, null);
}
public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
@Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter,
@Nullable String fieldNameAndQueryOperator) {
this(accessor, persistentProperty, mongoConverter, null, fieldNameAndQueryOperator);
}
public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
@Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter,
@Nullable SpELContext spELContext, @Nullable String fieldNameAndQueryOperator) {
this.accessor = accessor;
this.persistentProperty = persistentProperty;
this.mongoConverter = mongoConverter;
this.spELContext = spELContext;
this.fieldNameAndQueryOperator = fieldNameAndQueryOperator;
}
@Override
@ -84,4 +99,9 @@ public class MongoConversionContext implements ValueConversionContext<MongoPersi @@ -84,4 +99,9 @@ public class MongoConversionContext implements ValueConversionContext<MongoPersi
public SpELContext getSpELContext() {
return spELContext;
}
@Nullable
public String getFieldNameAndQueryOperator() {
return fieldNameAndQueryOperator;
}
}

22
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java

@ -88,6 +88,7 @@ import com.mongodb.DBRef; @@ -88,6 +88,7 @@ import com.mongodb.DBRef;
* @author David Julia
* @author Divya Srivastava
* @author Gyungrai Wang
* @author Ross Lawley
*/
public class QueryMapper {
@ -670,9 +671,23 @@ public class QueryMapper { @@ -670,9 +671,23 @@ public class QueryMapper {
PropertyValueConverter<Object, Object, ValueConversionContext<MongoPersistentProperty>> valueConverter) {
MongoPersistentProperty property = documentField.getProperty();
String fieldNameAndQueryOperator = property != null && !property.getFieldName().equals(documentField.name)
? property.getFieldName() + "." + documentField.name
: documentField.name;
MongoConversionContext conversionContext = new MongoConversionContext(NoPropertyPropertyValueProvider.INSTANCE,
property, converter);
property, converter, fieldNameAndQueryOperator);
return convertValueWithConversionContext(documentField, sourceValue, value, valueConverter, conversionContext);
}
@Nullable
private Object convertValueWithConversionContext(Field documentField, Object sourceValue, Object value,
PropertyValueConverter<Object, Object, ValueConversionContext<MongoPersistentProperty>> valueConverter,
MongoConversionContext conversionContext) {
MongoPersistentProperty property = documentField.getProperty();
/* might be an $in clause with multiple entries */
if (property != null && !property.isCollectionLike() && sourceValue instanceof Collection<?> collection) {
@ -692,7 +707,10 @@ public class QueryMapper { @@ -692,7 +707,10 @@ public class QueryMapper {
return BsonUtils.mapValues(document, (key, val) -> {
if (isKeyword(key)) {
return getMappedValue(documentField, val);
MongoConversionContext fieldConversionContext = new MongoConversionContext(
NoPropertyPropertyValueProvider.INSTANCE, property, converter,
conversionContext.getFieldNameAndQueryOperator() + "." + key);
return convertValueWithConversionContext(documentField, val, val, valueConverter, fieldConversionContext);
}
return val;
});

7
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java

@ -26,6 +26,7 @@ import org.springframework.lang.Nullable; @@ -26,6 +26,7 @@ import org.springframework.lang.Nullable;
* Default {@link EncryptionContext} implementation.
*
* @author Christoph Strobl
* @author Ross Lawley
* @since 4.1
*/
class ExplicitEncryptionContext implements EncryptionContext {
@ -66,4 +67,10 @@ class ExplicitEncryptionContext implements EncryptionContext { @@ -66,4 +67,10 @@ class ExplicitEncryptionContext implements EncryptionContext {
public <T> T write(@Nullable Object value, TypeInformation<T> target) {
return conversionContext.write(value, target);
}
@Override
@Nullable
public String getFieldNameAndQueryOperator() {
return conversionContext.getFieldNameAndQueryOperator();
}
}

83
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java

@ -15,8 +15,14 @@ @@ -15,8 +15,14 @@
*/
package org.springframework.data.mongodb.core.convert.encryption;
import static java.util.Arrays.*;
import static java.util.Collections.*;
import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*;
import static org.springframework.data.mongodb.core.encryption.EncryptionOptions.*;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
@ -31,9 +37,11 @@ import org.springframework.core.CollectionFactory; @@ -31,9 +37,11 @@ import org.springframework.core.CollectionFactory;
import org.springframework.data.mongodb.core.convert.MongoConversionContext;
import org.springframework.data.mongodb.core.encryption.Encryption;
import org.springframework.data.mongodb.core.encryption.EncryptionContext;
import org.springframework.data.mongodb.core.encryption.EncryptionKey;
import org.springframework.data.mongodb.core.encryption.EncryptionKeyResolver;
import org.springframework.data.mongodb.core.encryption.EncryptionOptions;
import org.springframework.data.mongodb.core.mapping.Encrypted;
import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.lang.Nullable;
@ -44,11 +52,14 @@ import org.springframework.util.ObjectUtils; @@ -44,11 +52,14 @@ import org.springframework.util.ObjectUtils;
* {@link Encrypted @Encrypted} to provide key and algorithm metadata.
*
* @author Christoph Strobl
* @author Ross Lawley
* @since 4.1
*/
public class MongoEncryptionConverter implements EncryptingConverter<Object, Object> {
private static final Log LOGGER = LogFactory.getLog(MongoEncryptionConverter.class);
private static final String EQUALITY_OPERATOR = "$eq";
private static final List<String> RANGE_OPERATORS = asList("$gt", "$gte", "$lt", "$lte");
private final Encryption<BsonValue, BsonBinary> encryption;
private final EncryptionKeyResolver keyResolver;
@ -161,8 +172,42 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj @@ -161,8 +172,42 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
getProperty(context).getOwner().getName(), getProperty(context).getName()));
}
EncryptionOptions encryptionOptions = new EncryptionOptions(annotation.algorithm(), keyResolver.getKey(context));
boolean encryptExpression = false;
String algorithm = annotation.algorithm();
EncryptionKey key = keyResolver.getKey(context);
EncryptionOptions encryptionOptions = new EncryptionOptions(algorithm, key);
String fieldNameAndQueryOperator = context.getFieldNameAndQueryOperator();
ExplicitEncrypted explicitEncryptedAnnotation = persistentProperty.findAnnotation(ExplicitEncrypted.class);
if (explicitEncryptedAnnotation != null) {
QueryableEncryptionOptions queryableEncryptionOptions = QueryableEncryptionOptions.none();
String rangeOptions = explicitEncryptedAnnotation.rangeOptions();
if (!rangeOptions.isEmpty()) {
queryableEncryptionOptions = queryableEncryptionOptions.rangeOptions(Document.parse(rangeOptions));
}
if (explicitEncryptedAnnotation.contentionFactor() >= 0) {
queryableEncryptionOptions = queryableEncryptionOptions
.contentionFactor(explicitEncryptedAnnotation.contentionFactor());
}
boolean isPartOfARangeQuery = algorithm.equalsIgnoreCase(RANGE) && fieldNameAndQueryOperator != null;
if (isPartOfARangeQuery) {
encryptExpression = true;
queryableEncryptionOptions = queryableEncryptionOptions.queryType("range");
}
encryptionOptions = new EncryptionOptions(algorithm, key, queryableEncryptionOptions);
}
if (encryptExpression) {
return encryptExpression(fieldNameAndQueryOperator, value, encryptionOptions);
} else {
return encryptValue(value, context, persistentProperty, encryptionOptions);
}
}
private BsonBinary encryptValue(Object value, EncryptionContext context, MongoPersistentProperty persistentProperty,
EncryptionOptions encryptionOptions) {
if (!persistentProperty.isEntity()) {
if (persistentProperty.isCollectionLike()) {
@ -187,6 +232,42 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj @@ -187,6 +232,42 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
return encryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptionOptions);
}
/**
* Encrypts a range query expression.
*
* <p>The mongodb-crypt {@code encryptExpression} has strict formatting requirements so this method
* ensures these requirements are met and then picks out and returns just the value for use with a range query.
*
* @param fieldNameAndQueryOperator field name and query operator
* @param value the value of the expression to be encrypted
* @param encryptionOptions the options
* @return the encrypted range value for use in a range query
*/
private BsonValue encryptExpression(String fieldNameAndQueryOperator, Object value,
EncryptionOptions encryptionOptions) {
BsonValue doc = BsonUtils.simpleToBsonValue(value);
String fieldName = fieldNameAndQueryOperator;
String queryOperator = EQUALITY_OPERATOR;
int pos = fieldNameAndQueryOperator.lastIndexOf(".$");
if (pos > -1) {
fieldName = fieldNameAndQueryOperator.substring(0, pos);
queryOperator = fieldNameAndQueryOperator.substring(pos + 1);
}
if (!RANGE_OPERATORS.contains(queryOperator)) {
throw new AssertionError(String.format("Not a valid range query. Querying a range encrypted field but the "
+ "query operator '%s' for field path '%s' is not a range query.", queryOperator, fieldName));
}
BsonDocument encryptExpression = new BsonDocument("$and",
new BsonArray(singletonList(new BsonDocument(fieldName, new BsonDocument(queryOperator, doc)))));
BsonDocument result = encryption.encryptExpression(encryptExpression, encryptionOptions);
return result.getArray("$and").get(0).asDocument().getDocument(fieldName).getBinary(queryOperator);
}
private BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property,
EncryptionContext context) {

15
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java

@ -15,10 +15,13 @@ @@ -15,10 +15,13 @@
*/
package org.springframework.data.mongodb.core.encryption;
import org.bson.BsonDocument;
/**
* Component responsible for encrypting and decrypting values.
*
* @author Christoph Strobl
* @author Ross Lawley
* @since 4.1
*/
public interface Encryption<S, T> {
@ -40,4 +43,16 @@ public interface Encryption<S, T> { @@ -40,4 +43,16 @@ public interface Encryption<S, T> {
*/
S decrypt(T value);
/**
* Encrypt the given expression.
*
* @param value must not be {@literal null}.
* @param options must not be {@literal null}.
* @return the encrypted expression.
* @since 4.5.0
*/
default BsonDocument encryptExpression(BsonDocument value, EncryptionOptions options) {
throw new UnsupportedOperationException("Unsupported encryption method");
}
}

10
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java

@ -25,6 +25,7 @@ import org.springframework.lang.Nullable; @@ -25,6 +25,7 @@ import org.springframework.lang.Nullable;
* Context to encapsulate encryption for a specific {@link MongoPersistentProperty}.
*
* @author Christoph Strobl
* @author Ross Lawley
* @since 4.1
*/
public interface EncryptionContext {
@ -128,4 +129,13 @@ public interface EncryptionContext { @@ -128,4 +129,13 @@ public interface EncryptionContext {
EvaluationContext getEvaluationContext(Object source);
/**
* The field name and field query operator
*
* @return can be {@literal null}.
*/
@Nullable
default String getFieldNameAndQueryOperator() {
return null;
}
}

188
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java

@ -15,6 +15,16 @@ @@ -15,6 +15,16 @@
*/
package org.springframework.data.mongodb.core.encryption;
import static org.springframework.data.mongodb.util.MongoCompatibilityAdapter.*;
import java.util.Objects;
import java.util.Optional;
import com.mongodb.client.model.vault.RangeOptions;
import org.bson.Document;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.data.util.Optionals;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@ -22,20 +32,27 @@ import org.springframework.util.ObjectUtils; @@ -22,20 +32,27 @@ import org.springframework.util.ObjectUtils;
* Options, like the {@link #algorithm()}, to apply when encrypting values.
*
* @author Christoph Strobl
* @author Ross Lawley
* @since 4.1
*/
public class EncryptionOptions {
private final String algorithm;
private final EncryptionKey key;
private final QueryableEncryptionOptions queryableEncryptionOptions;
public EncryptionOptions(String algorithm, EncryptionKey key) {
this(algorithm, key, QueryableEncryptionOptions.NONE);
}
public EncryptionOptions(String algorithm, EncryptionKey key, QueryableEncryptionOptions queryableEncryptionOptions) {
Assert.hasText(algorithm, "Algorithm must not be empty");
Assert.notNull(key, "EncryptionKey must not be empty");
Assert.notNull(key, "QueryableEncryptionOptions must not be empty");
this.key = key;
this.algorithm = algorithm;
this.queryableEncryptionOptions = queryableEncryptionOptions;
}
public EncryptionKey key() {
@ -46,6 +63,10 @@ public class EncryptionOptions { @@ -46,6 +63,10 @@ public class EncryptionOptions {
return algorithm;
}
public QueryableEncryptionOptions queryableEncryptionOptions() {
return queryableEncryptionOptions;
}
@Override
public boolean equals(Object o) {
@ -61,7 +82,11 @@ public class EncryptionOptions { @@ -61,7 +82,11 @@ public class EncryptionOptions {
if (!ObjectUtils.nullSafeEquals(algorithm, that.algorithm)) {
return false;
}
return ObjectUtils.nullSafeEquals(key, that.key);
if (!ObjectUtils.nullSafeEquals(key, that.key)) {
return false;
}
return ObjectUtils.nullSafeEquals(queryableEncryptionOptions, that.queryableEncryptionOptions);
}
@Override
@ -69,11 +94,170 @@ public class EncryptionOptions { @@ -69,11 +94,170 @@ public class EncryptionOptions {
int result = ObjectUtils.nullSafeHashCode(algorithm);
result = 31 * result + ObjectUtils.nullSafeHashCode(key);
result = 31 * result + ObjectUtils.nullSafeHashCode(queryableEncryptionOptions);
return result;
}
@Override
public String toString() {
return "EncryptionOptions{" + "algorithm='" + algorithm + '\'' + ", key=" + key + '}';
return "EncryptionOptions{" + "algorithm='" + algorithm + '\'' + ", key=" + key + ", queryableEncryptionOptions='"
+ queryableEncryptionOptions + "'}";
}
/**
* Options, like the {@link #getQueryType()}, to apply when encrypting queryable values.
*
* @author Ross Lawley
*/
public static class QueryableEncryptionOptions {
private static final QueryableEncryptionOptions NONE = new QueryableEncryptionOptions(null, null, null);
private final @Nullable String queryType;
private final @Nullable Long contentionFactor;
private final @Nullable Document rangeOptions;
private QueryableEncryptionOptions(@Nullable String queryType, @Nullable Long contentionFactor,
@Nullable Document rangeOptions) {
this.queryType = queryType;
this.contentionFactor = contentionFactor;
this.rangeOptions = rangeOptions;
}
/**
* Create an empty {@link QueryableEncryptionOptions}.
*
* @return none {@literal null}.
*/
public static QueryableEncryptionOptions none() {
return NONE;
}
/**
* Define the {@code queryType} to be used for queryable document encryption.
*
* @param queryType can be {@literal null}.
* @return new instance of {@link QueryableEncryptionOptions}.
*/
public QueryableEncryptionOptions queryType(@Nullable String queryType) {
return new QueryableEncryptionOptions(queryType, contentionFactor, rangeOptions);
}
/**
* Define the {@code contentionFactor} to be used for queryable document encryption.
*
* @param contentionFactor can be {@literal null}.
* @return new instance of {@link QueryableEncryptionOptions}.
*/
public QueryableEncryptionOptions contentionFactor(@Nullable Long contentionFactor) {
return new QueryableEncryptionOptions(queryType, contentionFactor, rangeOptions);
}
/**
* Define the {@code rangeOptions} to be used for queryable document encryption.
*
* @param rangeOptions can be {@literal null}.
* @return new instance of {@link QueryableEncryptionOptions}.
*/
public QueryableEncryptionOptions rangeOptions(@Nullable Document rangeOptions) {
return new QueryableEncryptionOptions(queryType, contentionFactor, rangeOptions);
}
/**
* Get the {@code queryType} to apply.
*
* @return {@link Optional#empty()} if not set.
*/
public Optional<String> getQueryType() {
return Optional.ofNullable(queryType);
}
/**
* Get the {@code contentionFactor} to apply.
*
* @return {@link Optional#empty()} if not set.
*/
public Optional<Long> getContentionFactor() {
return Optional.ofNullable(contentionFactor);
}
/**
* Get the {@code rangeOptions} to apply.
*
* @return {@link Optional#empty()} if not set.
*/
public Optional<RangeOptions> getRangeOptions() {
if (rangeOptions == null) {
return Optional.empty();
}
RangeOptions encryptionRangeOptions = new RangeOptions();
if (rangeOptions.containsKey("min")) {
encryptionRangeOptions.min(BsonUtils.simpleToBsonValue(rangeOptions.get("min")));
}
if (rangeOptions.containsKey("max")) {
encryptionRangeOptions.max(BsonUtils.simpleToBsonValue(rangeOptions.get("max")));
}
if (rangeOptions.containsKey("trimFactor")) {
Object trimFactor = rangeOptions.get("trimFactor");
Assert.isInstanceOf(Integer.class, trimFactor, () -> String
.format("Expected to find a %s but it turned out to be %s.", Integer.class, trimFactor.getClass()));
rangeOptionsAdapter(encryptionRangeOptions).trimFactor((Integer) trimFactor);
}
if (rangeOptions.containsKey("sparsity")) {
Object sparsity = rangeOptions.get("sparsity");
Assert.isInstanceOf(Number.class, sparsity,
() -> String.format("Expected to find a %s but it turned out to be %s.", Long.class, sparsity.getClass()));
encryptionRangeOptions.sparsity(((Number) sparsity).longValue());
}
if (rangeOptions.containsKey("precision")) {
Object precision = rangeOptions.get("precision");
Assert.isInstanceOf(Number.class, precision, () -> String
.format("Expected to find a %s but it turned out to be %s.", Integer.class, precision.getClass()));
encryptionRangeOptions.precision(((Number) precision).intValue());
}
return Optional.of(encryptionRangeOptions);
}
/**
* @return {@literal true} if no arguments set.
*/
boolean isEmpty() {
return !Optionals.isAnyPresent(getQueryType(), getContentionFactor(), getRangeOptions());
}
@Override
public String toString() {
return "QueryableEncryptionOptions{" + "queryType='" + queryType + '\'' + ", contentionFactor=" + contentionFactor
+ ", rangeOptions=" + rangeOptions + '}';
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
QueryableEncryptionOptions that = (QueryableEncryptionOptions) o;
if (!ObjectUtils.nullSafeEquals(queryType, that.queryType)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(contentionFactor, that.contentionFactor)) {
return false;
}
return ObjectUtils.nullSafeEquals(rangeOptions, that.rangeOptions);
}
@Override
public int hashCode() {
return Objects.hash(queryType, contentionFactor, rangeOptions);
}
}
}

23
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java

@ -18,6 +18,7 @@ package org.springframework.data.mongodb.core.encryption; @@ -18,6 +18,7 @@ package org.springframework.data.mongodb.core.encryption;
import java.util.function.Supplier;
import org.bson.BsonBinary;
import org.bson.BsonDocument;
import org.bson.BsonValue;
import org.springframework.data.mongodb.core.encryption.EncryptionKey.Type;
import org.springframework.util.Assert;
@ -29,6 +30,7 @@ import com.mongodb.client.vault.ClientEncryption; @@ -29,6 +30,7 @@ import com.mongodb.client.vault.ClientEncryption;
* {@link ClientEncryption} based {@link Encryption} implementation.
*
* @author Christoph Strobl
* @author Ross Lawley
* @since 4.1
*/
public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary> {
@ -59,7 +61,19 @@ public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary> @@ -59,7 +61,19 @@ public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary>
@Override
public BsonBinary encrypt(BsonValue value, EncryptionOptions options) {
return getClientEncryption().encrypt(value, createEncryptOptions(options));
}
@Override
public BsonDocument encryptExpression(BsonDocument value, EncryptionOptions options) {
return getClientEncryption().encryptExpression(value, createEncryptOptions(options));
}
public ClientEncryption getClientEncryption() {
return source.get();
}
private EncryptOptions createEncryptOptions(EncryptionOptions options) {
EncryptOptions encryptOptions = new EncryptOptions(options.algorithm());
if (Type.ALT.equals(options.key().type())) {
@ -68,11 +82,10 @@ public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary> @@ -68,11 +82,10 @@ public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary>
encryptOptions = encryptOptions.keyId((BsonBinary) options.key().value());
}
return getClientEncryption().encrypt(value, encryptOptions);
}
public ClientEncryption getClientEncryption() {
return source.get();
options.queryableEncryptionOptions().getQueryType().map(encryptOptions::queryType);
options.queryableEncryptionOptions().getContentionFactor().map(encryptOptions::contentionFactor);
options.queryableEncryptionOptions().getRangeOptions().map(encryptOptions::rangeOptions);
return encryptOptions;
}
}

23
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java

@ -47,6 +47,7 @@ import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionC @@ -47,6 +47,7 @@ import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionC
* </pre>
*
* @author Christoph Strobl
* @author Ross Lawley
* @since 4.1
* @see ValueConverter
*/
@ -60,7 +61,8 @@ public @interface ExplicitEncrypted { @@ -60,7 +61,8 @@ public @interface ExplicitEncrypted {
* Define the algorithm to use.
* <p>
* A {@literal Deterministic} algorithm ensures that a given input value always encrypts to the same output while a
* {@literal randomized} one will produce different results every time.
* {@literal randomized} one will produce different results every time. A {@literal range} algorithm allows for
* the value to be queried whilst encrypted.
* <p>
* Please make sure to use an algorithm that is in line with MongoDB's encryption rules for simple types, complex
* objects and arrays as well as the query limitations that come with each of them.
@ -84,6 +86,24 @@ public @interface ExplicitEncrypted { @@ -84,6 +86,24 @@ public @interface ExplicitEncrypted {
*/
String keyAltName() default "";
/**
* Set the contention factor
* <p>
* Only required when using {@literal range} encryption.
* @return the contention factor
*/
long contentionFactor() default -1;
/**
* Set the {@literal range} options
* <p>
* Should be valid extended json representing the range options and including the following values:
* {@code min}, {@code max}, {@code trimFactor} and {@code sparsity}.
*
* @return the json representation of range options
*/
String rangeOptions() default "";
/**
* The {@link EncryptingConverter} type handling the {@literal en-/decryption} of the annotated property.
*
@ -91,4 +111,5 @@ public @interface ExplicitEncrypted { @@ -91,4 +111,5 @@ public @interface ExplicitEncrypted {
*/
@AliasFor(annotation = ValueConverter.class, value = "value")
Class<? extends PropertyValueConverter> value() default MongoEncryptionConverter.class;
}

27
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java

@ -34,6 +34,7 @@ import com.mongodb.client.MapReduceIterable; @@ -34,6 +34,7 @@ import com.mongodb.client.MapReduceIterable;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.MongoIterable;
import com.mongodb.client.model.IndexOptions;
import com.mongodb.client.model.vault.RangeOptions;
import com.mongodb.reactivestreams.client.MapReducePublisher;
/**
@ -42,11 +43,13 @@ import com.mongodb.reactivestreams.client.MapReducePublisher; @@ -42,11 +43,13 @@ import com.mongodb.reactivestreams.client.MapReducePublisher;
* This class is for internal use within the framework and should not be used by applications.
*
* @author Christoph Strobl
* @author Ross Lawley
* @since 4.3
*/
public class MongoCompatibilityAdapter {
private static final String NO_LONGER_SUPPORTED = "%s is no longer supported on Mongo Client 5 or newer";
private static final String NOT_SUPPORTED_ON_4 = "%s is not supported on Mongo Client 4";
private static final @Nullable Method getStreamFactoryFactory = ReflectionUtils.findMethod(MongoClientSettings.class,
"getStreamFactoryFactory");
@ -54,6 +57,9 @@ public class MongoCompatibilityAdapter { @@ -54,6 +57,9 @@ public class MongoCompatibilityAdapter {
private static final @Nullable Method setBucketSize = ReflectionUtils.findMethod(IndexOptions.class, "bucketSize",
Double.class);
private static final @Nullable Method setTrimFactor = ReflectionUtils.findMethod(RangeOptions.class, "setTrimFactor",
Integer.class);
/**
* Return a compatibility adapter for {@link MongoClientSettings.Builder}.
*
@ -122,6 +128,23 @@ public class MongoCompatibilityAdapter { @@ -122,6 +128,23 @@ public class MongoCompatibilityAdapter {
};
}
/**
* Return a compatibility adapter for {@link RangeOptions}.
*
* @param options
* @return
*/
public static RangeOptionsAdapter rangeOptionsAdapter(RangeOptions options) {
return trimFactor -> {
if (!MongoClientVersion.isVersion5orNewer() || setTrimFactor == null) {
throw new UnsupportedOperationException(NOT_SUPPORTED_ON_4.formatted("RangeOptions.trimFactor"));
}
ReflectionUtils.invokeMethod(setTrimFactor, options, trimFactor);
};
}
/**
* Return a compatibility adapter for {@code MapReducePublisher}.
*
@ -199,6 +222,10 @@ public class MongoCompatibilityAdapter { @@ -199,6 +222,10 @@ public class MongoCompatibilityAdapter {
MongoDatabaseAdapter forDb(com.mongodb.client.MongoDatabase db);
}
public interface RangeOptionsAdapter {
void trimFactor(Integer trimFactor);
}
@SuppressWarnings({ "unchecked", "DataFlowIssue" })
public static class MongoDatabaseAdapter {

373
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java

@ -0,0 +1,373 @@ @@ -0,0 +1,373 @@
/*
* Copyright 2023-2024 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;
import static java.util.Arrays.*;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import java.security.SecureRandom;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import com.mongodb.AutoEncryptionSettings;
import com.mongodb.ClientEncryptionSettings;
import com.mongodb.ConnectionString;
import com.mongodb.MongoClientSettings;
import com.mongodb.MongoNamespace;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.CreateCollectionOptions;
import com.mongodb.client.model.CreateEncryptedCollectionParams;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.IndexOptions;
import com.mongodb.client.model.Indexes;
import com.mongodb.client.vault.ClientEncryption;
import com.mongodb.client.vault.ClientEncryptions;
import org.assertj.core.api.Assumptions;
import org.bson.BsonArray;
import org.bson.BsonBinary;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonInt64;
import org.bson.BsonNull;
import org.bson.BsonString;
import org.bson.BsonValue;
import org.bson.Document;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.data.convert.PropertyValueConverterFactory;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter;
import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted;
import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion;
import org.springframework.data.mongodb.test.util.EnableIfReplicaSetAvailable;
import org.springframework.data.mongodb.test.util.MongoClientExtension;
import org.springframework.data.mongodb.util.MongoClientVersion;
import org.springframework.data.util.Lazy;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* @author Ross Lawley
*/
@ExtendWith({ MongoClientExtension.class, SpringExtension.class })
@EnableIfMongoServerVersion(isGreaterThanEqual = "8.0")
@EnableIfReplicaSetAvailable
@ContextConfiguration(classes = RangeEncryptionTests.EncryptionConfig.class)
class RangeEncryptionTests {
@Autowired MongoTemplate template;
@BeforeEach
void setUp() {
Assumptions.assumeThat(MongoClientVersion.isVersion5orNewer()).isTrue();
}
@AfterEach
void tearDown() {
template.getDb().getCollection("test").deleteMany(new BsonDocument());
}
@Test
void canGreaterThanEqualMatchRangeEncryptedField() {
Person source = createPerson();
template.insert(source);
Person loaded = template.query(Person.class).matching(where("encryptedInt").gte(source.encryptedInt)).firstValue();
assertThat(loaded).isEqualTo(source);
}
@Test
void canLesserThanEqualMatchRangeEncryptedField() {
Person source = createPerson();
template.insert(source);
Person loaded = template.query(Person.class).matching(where("encryptedInt").lte(source.encryptedInt)).firstValue();
assertThat(loaded).isEqualTo(source);
}
@Test
void canRangeMatchRangeEncryptedField() {
Person source = createPerson();
template.insert(source);
Person loaded = template.query(Person.class).matching(where("encryptedLong").lte(1001L).gte(1001L)).firstValue();
assertThat(loaded).isEqualTo(source);
}
@Test
void canUpdateRangeEncryptedField() {
Person source = createPerson();
template.insert(source);
source.encryptedInt = 123;
source.encryptedLong = 9999L;
template.save(source);
Person loaded = template.query(Person.class).matching(where("id").is(source.id)).firstValue();
assertThat(loaded).isEqualTo(source);
}
@Test
void errorsWhenUsingNonRangeOperatorEqOnRangeEncryptedField() {
Person source = createPerson();
template.insert(source);
assertThatThrownBy(
() -> template.query(Person.class).matching(where("encryptedInt").is(source.encryptedInt)).firstValue())
.isInstanceOf(AssertionError.class)
.hasMessageStartingWith("Not a valid range query. Querying a range encrypted field but "
+ "the query operator '$eq' for field path 'encryptedInt' is not a range query.");
}
@Test
void errorsWhenUsingNonRangeOperatorInOnRangeEncryptedField() {
Person source = createPerson();
template.insert(source);
assertThatThrownBy(
() -> template.query(Person.class).matching(where("encryptedLong").in(1001L, 9999L)).firstValue())
.isInstanceOf(AssertionError.class)
.hasMessageStartingWith("Not a valid range query. Querying a range encrypted field but "
+ "the query operator '$in' for field path 'encryptedLong' is not a range query.");
}
private Person createPerson() {
Person source = new Person();
source.id = "id-1";
source.encryptedInt = 101;
source.encryptedLong = 1001L;
return source;
}
protected static class EncryptionConfig extends AbstractMongoClientConfiguration {
private static final String LOCAL_KMS_PROVIDER = "local";
private static final Lazy<Map<String, Map<String, Object>>> LAZY_KMS_PROVIDERS = Lazy.of(() -> {
byte[] localMasterKey = new byte[96];
new SecureRandom().nextBytes(localMasterKey);
return Map.of(LOCAL_KMS_PROVIDER, Map.of("key", localMasterKey));
});
@Autowired ApplicationContext applicationContext;
@Override
protected String getDatabaseName() {
return "qe-test";
}
@Bean
public MongoClient mongoClient() {
return super.mongoClient();
}
@Override
protected void configureConverters(MongoConverterConfigurationAdapter converterConfigurationAdapter) {
converterConfigurationAdapter
.registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(applicationContext))
.useNativeDriverJavaTimeCodecs();
}
@Bean
MongoEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEncryption) {
Lazy<Map<String, BsonBinary>> lazyDataKeyMap = Lazy.of(() -> {
try (MongoClient client = mongoClient()) {
MongoDatabase database = client.getDatabase(getDatabaseName());
database.getCollection("test").drop();
ClientEncryption clientEncryption = mongoClientEncryption.getClientEncryption();
BsonDocument encryptedFields = new BsonDocument().append("fields",
new BsonArray(asList(
new BsonDocument("keyId", BsonNull.VALUE).append("path", new BsonString("encryptedInt"))
.append("bsonType", new BsonString("int"))
.append("queries",
new BsonDocument("queryType", new BsonString("range")).append("contention", new BsonInt64(0L))
.append("trimFactor", new BsonInt32(1)).append("sparsity", new BsonInt64(1))
.append("min", new BsonInt32(0)).append("max", new BsonInt32(200))),
new BsonDocument("keyId", BsonNull.VALUE).append("path", new BsonString("encryptedLong"))
.append("bsonType", new BsonString("long")).append("queries",
new BsonDocument("queryType", new BsonString("range")).append("contention", new BsonInt64(0L))
.append("trimFactor", new BsonInt32(1)).append("sparsity", new BsonInt64(1))
.append("min", new BsonInt64(1000)).append("max", new BsonInt64(9999))))));
BsonDocument local = clientEncryption.createEncryptedCollection(database, "test",
new CreateCollectionOptions().encryptedFields(encryptedFields),
new CreateEncryptedCollectionParams(LOCAL_KMS_PROVIDER));
return local.getArray("fields").stream().map(BsonValue::asDocument).collect(
Collectors.toMap(field -> field.getString("path").getValue(), field -> field.getBinary("keyId")));
}
});
return new MongoEncryptionConverter(mongoClientEncryption, EncryptionKeyResolver
.annotated((ctx) -> EncryptionKey.keyId(lazyDataKeyMap.get().get(ctx.getProperty().getFieldName()))));
}
@Bean
CachingMongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) {
return new CachingMongoClientEncryption(() -> ClientEncryptions.create(encryptionSettings));
}
@Override
protected void configureClientSettings(MongoClientSettings.Builder builder) {
try (MongoClient client = MongoClients.create()) {
ClientEncryptionSettings clientEncryptionSettings = encryptionSettings(client);
builder.autoEncryptionSettings(AutoEncryptionSettings.builder() //
.kmsProviders(clientEncryptionSettings.getKmsProviders()) //
.keyVaultNamespace(clientEncryptionSettings.getKeyVaultNamespace()) //
.bypassQueryAnalysis(true).build());
}
}
@Bean
ClientEncryptionSettings encryptionSettings(MongoClient mongoClient) {
MongoNamespace keyVaultNamespace = new MongoNamespace("encryption.testKeyVault");
MongoCollection<Document> keyVaultCollection = mongoClient.getDatabase(keyVaultNamespace.getDatabaseName())
.getCollection(keyVaultNamespace.getCollectionName());
keyVaultCollection.drop();
// Ensure that two data keys cannot share the same keyAltName.
keyVaultCollection.createIndex(Indexes.ascending("keyAltNames"),
new IndexOptions().unique(true).partialFilterExpression(Filters.exists("keyAltNames")));
mongoClient.getDatabase(getDatabaseName()).getCollection("test").drop(); // Clear old data
// Create the ClientEncryption instance
return ClientEncryptionSettings.builder() //
.keyVaultMongoClientSettings(
MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb://localhost")).build()) //
.keyVaultNamespace(keyVaultNamespace.getFullName()) //
.kmsProviders(LAZY_KMS_PROVIDERS.get()) //
.build();
}
}
static class CachingMongoClientEncryption extends MongoClientEncryption implements DisposableBean {
static final AtomicReference<ClientEncryption> cache = new AtomicReference<>();
CachingMongoClientEncryption(Supplier<ClientEncryption> source) {
super(() -> {
ClientEncryption clientEncryption = cache.get();
if (clientEncryption == null) {
clientEncryption = source.get();
cache.set(clientEncryption);
}
return clientEncryption;
});
}
@Override
public void destroy() {
ClientEncryption clientEncryption = cache.get();
if (clientEncryption != null) {
clientEncryption.close();
cache.set(null);
}
}
}
@org.springframework.data.mongodb.core.mapping.Document("test")
static class Person {
String id;
String name;
@ExplicitEncrypted(algorithm = RANGE, contentionFactor = 0L,
rangeOptions = "{\"min\": 0, \"max\": 200, \"trimFactor\": 1, \"sparsity\": 1}") Integer encryptedInt;
@ExplicitEncrypted(algorithm = RANGE, contentionFactor = 0L,
rangeOptions = "{\"min\": {\"$numberLong\": \"1000\"}, \"max\": {\"$numberLong\": \"9999\"}, \"trimFactor\": 1, \"sparsity\": 1}") Long encryptedLong;
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Integer getEncryptedInt() {
return this.encryptedInt;
}
public void setEncryptedInt(Integer encryptedInt) {
this.encryptedInt = encryptedInt;
}
public Long getEncryptedLong() {
return this.encryptedLong;
}
public void setEncryptedLong(Long encryptedLong) {
this.encryptedLong = encryptedLong;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Person person = (Person) o;
return Objects.equals(id, person.id) && Objects.equals(name, person.name)
&& Objects.equals(encryptedInt, person.encryptedInt) && Objects.equals(encryptedLong, person.encryptedLong);
}
@Override
public int hashCode() {
int result = Objects.hashCode(id);
result = 31 * result + Objects.hashCode(name);
result = 31 * result + Objects.hashCode(encryptedInt);
result = 31 * result + Objects.hashCode(encryptedLong);
return result;
}
@Override
public String toString() {
return "Person{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", encryptedInt=" + encryptedInt
+ ", encryptedLong=" + encryptedLong + '}';
}
}
}
Loading…
Cancel
Save