Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd175af09c | ||
|
|
a6372e6629 | ||
|
|
0b6c9978e6 | ||
|
|
edeb423b0e | ||
|
|
6cee61d807 | ||
|
|
70530d6ee4 | ||
|
|
9cfc46e589 | ||
|
|
7cb27c7465 | ||
|
|
0cd082e7fa | ||
|
|
56e763c9c0 | ||
|
|
489f593395 |
4
.mvn/wrapper/maven-wrapper.properties
vendored
4
.mvn/wrapper/maven-wrapper.properties
vendored
@@ -1,2 +1,2 @@
|
||||
#Tue Jun 13 08:53:53 CEST 2023
|
||||
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.2/apache-maven-3.9.2-bin.zip
|
||||
#Mon Jul 03 09:48:21 CEST 2023
|
||||
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Java versions
|
||||
java.main.tag=17.0.6_10-jdk-focal
|
||||
java.main.tag=17.0.7_7-jdk-focal
|
||||
java.next.tag=20-jdk-jammy
|
||||
|
||||
# Docker container images - standard
|
||||
@@ -7,15 +7,15 @@ docker.java.main.image=harbor-repo.vmware.com/dockerhub-proxy-cache/library/ecli
|
||||
docker.java.next.image=harbor-repo.vmware.com/dockerhub-proxy-cache/library/eclipse-temurin:${java.next.tag}
|
||||
|
||||
# Supported versions of MongoDB
|
||||
docker.mongodb.4.4.version=4.4.18
|
||||
docker.mongodb.5.0.version=5.0.14
|
||||
docker.mongodb.6.0.version=6.0.4
|
||||
docker.mongodb.4.4.version=4.4.22
|
||||
docker.mongodb.5.0.version=5.0.18
|
||||
docker.mongodb.6.0.version=6.0.7
|
||||
|
||||
# Supported versions of Redis
|
||||
docker.redis.6.version=6.2.10
|
||||
docker.redis.6.version=6.2.12
|
||||
|
||||
# Supported versions of Cassandra
|
||||
docker.cassandra.3.version=3.11.14
|
||||
docker.cassandra.3.version=3.11.15
|
||||
|
||||
# Docker environment settings
|
||||
docker.java.inside.basic=-v $HOME:/tmp/jenkins-home
|
||||
|
||||
6
pom.xml
6
pom.xml
@@ -5,7 +5,7 @@
|
||||
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||
<version>4.1.1</version>
|
||||
<version>4.1.2</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>Spring Data MongoDB</name>
|
||||
@@ -15,7 +15,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.data.build</groupId>
|
||||
<artifactId>spring-data-parent</artifactId>
|
||||
<version>3.1.1</version>
|
||||
<version>3.1.2</version>
|
||||
</parent>
|
||||
|
||||
<modules>
|
||||
@@ -26,7 +26,7 @@
|
||||
<properties>
|
||||
<project.type>multi</project.type>
|
||||
<dist.id>spring-data-mongodb</dist.id>
|
||||
<springdata.commons>3.1.1</springdata.commons>
|
||||
<springdata.commons>3.1.2</springdata.commons>
|
||||
<mongo>4.9.1</mongo>
|
||||
<mongo.reactivestreams>${mongo}</mongo.reactivestreams>
|
||||
<jmh.version>1.19</jmh.version>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||
<version>4.1.1</version>
|
||||
<version>4.1.2</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||
<version>4.1.1</version>
|
||||
<version>4.1.2</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-mongodb-parent</artifactId>
|
||||
<version>4.1.1</version>
|
||||
<version>4.1.2</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
@@ -1089,7 +1089,7 @@ public class QueryMapper {
|
||||
protected static class MetadataBackedField extends Field {
|
||||
|
||||
private static final Pattern POSITIONAL_PARAMETER_PATTERN = Pattern.compile("\\.\\$(\\[.*?\\])?");
|
||||
private static final Pattern DOT_POSITIONAL_PATTERN = Pattern.compile("\\.\\d+(?!$)");
|
||||
private static final Pattern NUMERIC_SEGMENT = Pattern.compile("\\d+");
|
||||
private static final String INVALID_ASSOCIATION_REFERENCE = "Invalid path reference %s; Associations can only be pointed to directly or via their id property";
|
||||
|
||||
private final MongoPersistentEntity<?> entity;
|
||||
@@ -1231,14 +1231,13 @@ public class QueryMapper {
|
||||
private PersistentPropertyPath<MongoPersistentProperty> getPath(String pathExpression,
|
||||
@Nullable MongoPersistentProperty sourceProperty) {
|
||||
|
||||
String rawPath = removePlaceholders(POSITIONAL_OPERATOR,
|
||||
removePlaceholders(DOT_POSITIONAL_PATTERN, pathExpression));
|
||||
|
||||
if (sourceProperty != null && sourceProperty.getOwner().equals(entity)) {
|
||||
return mappingContext.getPersistentPropertyPath(
|
||||
PropertyPath.from(Pattern.quote(sourceProperty.getName()), entity.getTypeInformation()));
|
||||
}
|
||||
|
||||
String rawPath = resolvePath(pathExpression);
|
||||
|
||||
PropertyPath path = forName(rawPath);
|
||||
if (path == null || isPathToJavaLangClassProperty(path)) {
|
||||
return null;
|
||||
@@ -1333,6 +1332,38 @@ public class QueryMapper {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String resolvePath(String source) {
|
||||
|
||||
String[] segments = source.split("\\.");
|
||||
if (segments.length == 1) {
|
||||
return source;
|
||||
}
|
||||
|
||||
List<String> path = new ArrayList<>(segments.length);
|
||||
|
||||
/* always start from a property, so we can skip the first segment.
|
||||
from there remove any position placeholder */
|
||||
for(int i=1; i < segments.length; i++) {
|
||||
String segment = segments[i];
|
||||
if (segment.startsWith("[") && segment.endsWith("]")) {
|
||||
continue;
|
||||
}
|
||||
if (NUMERIC_SEGMENT.matcher(segment).matches()) {
|
||||
continue;
|
||||
}
|
||||
path.add(segment);
|
||||
}
|
||||
|
||||
// when property is followed only by placeholders eg. 'values.0.3.90'
|
||||
// or when there is no difference in the number of segments
|
||||
if (path.isEmpty() || segments.length == path.size() + 1) {
|
||||
return source;
|
||||
}
|
||||
|
||||
path.add(0, segments[0]);
|
||||
return StringUtils.collectionToDelimitedString(path, ".");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link Converter} to be used to created the mapped key. Default implementation will use
|
||||
* {@link PropertyToFieldNameConverter}.
|
||||
|
||||
@@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core.convert.encryption;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -63,7 +64,7 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
|
||||
public Object read(Object value, MongoConversionContext context) {
|
||||
|
||||
Object decrypted = EncryptingConverter.super.read(value, context);
|
||||
return decrypted instanceof BsonValue ? BsonUtils.toJavaType((BsonValue) decrypted) : decrypted;
|
||||
return decrypted instanceof BsonValue bsonValue ? BsonUtils.toJavaType(bsonValue) : decrypted;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -87,36 +88,56 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
|
||||
}
|
||||
|
||||
MongoPersistentProperty persistentProperty = getProperty(context);
|
||||
|
||||
if (getProperty(context).isCollectionLike() && decryptedValue instanceof Iterable<?> iterable) {
|
||||
|
||||
int size = iterable instanceof Collection<?> c ? c.size() : 10;
|
||||
|
||||
if (!persistentProperty.isEntity()) {
|
||||
Collection<Object> collection = CollectionFactory.createCollection(persistentProperty.getType(), size);
|
||||
iterable.forEach(it -> collection.add(BsonUtils.toJavaType((BsonValue) it)));
|
||||
iterable.forEach(it -> {
|
||||
if (it instanceof BsonValue bsonValue) {
|
||||
collection.add(BsonUtils.toJavaType(bsonValue));
|
||||
} else {
|
||||
collection.add(context.read(it, persistentProperty.getActualType()));
|
||||
}
|
||||
});
|
||||
|
||||
return collection;
|
||||
} else {
|
||||
Collection<Object> collection = CollectionFactory.createCollection(persistentProperty.getType(), size);
|
||||
iterable.forEach(it -> {
|
||||
collection.add(context.read(BsonUtils.toJavaType((BsonValue) it), persistentProperty.getActualType()));
|
||||
if (it instanceof BsonValue bsonValue) {
|
||||
collection.add(context.read(BsonUtils.toJavaType(bsonValue), persistentProperty.getActualType()));
|
||||
} else {
|
||||
collection.add(context.read(it, persistentProperty.getActualType()));
|
||||
}
|
||||
});
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
|
||||
if (!persistentProperty.isEntity() && decryptedValue instanceof BsonValue bsonValue) {
|
||||
if (persistentProperty.isMap() && persistentProperty.getType() != Document.class) {
|
||||
return new LinkedHashMap<>((Document) BsonUtils.toJavaType(bsonValue));
|
||||
|
||||
if (!persistentProperty.isEntity() && persistentProperty.isMap()) {
|
||||
if (persistentProperty.getType() != Document.class) {
|
||||
if (decryptedValue instanceof BsonValue bsonValue) {
|
||||
return new LinkedHashMap<>((Document) BsonUtils.toJavaType(bsonValue));
|
||||
}
|
||||
if (decryptedValue instanceof Document document) {
|
||||
return new LinkedHashMap<>(document);
|
||||
}
|
||||
if (decryptedValue instanceof Map map) {
|
||||
return map;
|
||||
}
|
||||
}
|
||||
return BsonUtils.toJavaType(bsonValue);
|
||||
}
|
||||
|
||||
if (persistentProperty.isEntity() && decryptedValue instanceof BsonDocument bsonDocument) {
|
||||
return context.read(BsonUtils.toJavaType(bsonDocument), persistentProperty.getTypeInformation().getType());
|
||||
}
|
||||
|
||||
if (persistentProperty.isEntity() && decryptedValue instanceof Document document) {
|
||||
return context.read(document, persistentProperty.getTypeInformation().getType());
|
||||
}
|
||||
|
||||
return decryptedValue;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,12 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.util;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.Temporal;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -280,36 +286,22 @@ public class BsonUtils {
|
||||
*/
|
||||
public static Object toJavaType(BsonValue value) {
|
||||
|
||||
switch (value.getBsonType()) {
|
||||
case INT32:
|
||||
return value.asInt32().getValue();
|
||||
case INT64:
|
||||
return value.asInt64().getValue();
|
||||
case STRING:
|
||||
return value.asString().getValue();
|
||||
case DECIMAL128:
|
||||
return value.asDecimal128().doubleValue();
|
||||
case DOUBLE:
|
||||
return value.asDouble().getValue();
|
||||
case BOOLEAN:
|
||||
return value.asBoolean().getValue();
|
||||
case OBJECT_ID:
|
||||
return value.asObjectId().getValue();
|
||||
case DB_POINTER:
|
||||
return new DBRef(value.asDBPointer().getNamespace(), value.asDBPointer().getId());
|
||||
case BINARY:
|
||||
return value.asBinary().getData();
|
||||
case DATE_TIME:
|
||||
return new Date(value.asDateTime().getValue());
|
||||
case SYMBOL:
|
||||
return value.asSymbol().getSymbol();
|
||||
case ARRAY:
|
||||
return value.asArray().toArray();
|
||||
case DOCUMENT:
|
||||
return Document.parse(value.asDocument().toJson());
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
return switch (value.getBsonType()) {
|
||||
case INT32 -> value.asInt32().getValue();
|
||||
case INT64 -> value.asInt64().getValue();
|
||||
case STRING -> value.asString().getValue();
|
||||
case DECIMAL128 -> value.asDecimal128().doubleValue();
|
||||
case DOUBLE -> value.asDouble().getValue();
|
||||
case BOOLEAN -> value.asBoolean().getValue();
|
||||
case OBJECT_ID -> value.asObjectId().getValue();
|
||||
case DB_POINTER -> new DBRef(value.asDBPointer().getNamespace(), value.asDBPointer().getId());
|
||||
case BINARY -> value.asBinary().getData();
|
||||
case DATE_TIME -> new Date(value.asDateTime().getValue());
|
||||
case SYMBOL -> value.asSymbol().getSymbol();
|
||||
case ARRAY -> value.asArray().toArray();
|
||||
case DOCUMENT -> Document.parse(value.asDocument().toJson());
|
||||
default -> value;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -358,17 +350,36 @@ public class BsonUtils {
|
||||
return new BsonDouble(floatValue);
|
||||
}
|
||||
|
||||
if(source instanceof Binary binary) {
|
||||
if (source instanceof Binary binary) {
|
||||
return new BsonBinary(binary.getType(), binary.getData());
|
||||
}
|
||||
|
||||
if (source instanceof Temporal) {
|
||||
if (source instanceof Instant value) {
|
||||
return new BsonDateTime(value.toEpochMilli());
|
||||
}
|
||||
if (source instanceof LocalDateTime value) {
|
||||
return new BsonDateTime(value.toInstant(ZoneOffset.UTC).toEpochMilli());
|
||||
}
|
||||
if (source instanceof LocalDate value) {
|
||||
return new BsonDateTime(value.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli());
|
||||
}
|
||||
if (source instanceof LocalTime value) {
|
||||
return new BsonDateTime(value.atDate(LocalDate.ofEpochDay(0L)).toInstant(ZoneOffset.UTC).toEpochMilli());
|
||||
}
|
||||
}
|
||||
|
||||
if (source instanceof Date date) {
|
||||
new BsonDateTime(date.getTime());
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(String.format("Unable to convert %s (%s) to BsonValue.", source,
|
||||
source != null ? source.getClass().getName() : "null"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the given {@link Document documents} into on in the given order. Keys contained within multiple documents are
|
||||
* overwritten by their follow ups.
|
||||
* overwritten by their follow-ups.
|
||||
*
|
||||
* @param documents must not be {@literal null}. Can be empty.
|
||||
* @return the document containing all key value pairs.
|
||||
@@ -669,7 +680,7 @@ public class BsonUtils {
|
||||
|
||||
if (value instanceof Collection<?> collection) {
|
||||
return toString(collection);
|
||||
} else if (value instanceof Map<?,?> map) {
|
||||
} else if (value instanceof Map<?, ?> map) {
|
||||
return toString(map);
|
||||
} else if (ObjectUtils.isArray(value)) {
|
||||
return toString(Arrays.asList(ObjectUtils.toObjectArray(value)));
|
||||
@@ -691,8 +702,9 @@ public class BsonUtils {
|
||||
|
||||
private static String toString(Map<?, ?> source) {
|
||||
|
||||
// Avoid String.format for performance
|
||||
return iterableToDelimitedString(source.entrySet(), "{ ", " }",
|
||||
entry -> String.format("\"%s\" : %s", entry.getKey(), toJson(entry.getValue())));
|
||||
entry -> "\"" + entry.getKey() + "\" : " + toJson(entry.getValue()));
|
||||
}
|
||||
|
||||
private static String toString(Collection<?> source) {
|
||||
|
||||
@@ -35,6 +35,8 @@ import org.bson.types.ObjectId;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
@@ -1213,24 +1215,26 @@ class UpdateMapperUnitTests {
|
||||
assertThat(mappedUpdate).isEqualTo(new org.bson.Document("$set", new org.bson.Document("levelOne.a.b.d", "e")));
|
||||
}
|
||||
|
||||
@Test // GH-3775
|
||||
void mapNestedIntegerFieldCorrectly() {
|
||||
@ParameterizedTest // GH-3775, GH-4426
|
||||
@ValueSource(strings = {"levelOne.0.1.3", "levelOne.0.1.32", "levelOne2.0.1.32", "levelOne2.0.1.320"})
|
||||
void mapNestedIntegerFieldCorrectly(String path) {
|
||||
|
||||
Update update = new Update().set("levelOne.0.1.3", "4");
|
||||
Update update = new Update().set(path, "4");
|
||||
Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
|
||||
context.getPersistentEntity(EntityWithNestedMap.class));
|
||||
|
||||
assertThat(mappedUpdate).isEqualTo(new org.bson.Document("$set", new org.bson.Document("levelOne.0.1.3", "4")));
|
||||
assertThat(mappedUpdate).isEqualTo(new org.bson.Document("$set", new org.bson.Document(path, "4")));
|
||||
}
|
||||
|
||||
@Test // GH-3775
|
||||
void mapNestedMixedStringIntegerFieldCorrectly() {
|
||||
@ParameterizedTest // GH-3775, GH-4426
|
||||
@ValueSource(strings = {"levelOne.0.1.c", "levelOne.0.1.c.32", "levelOne2.0.1.32.c", "levelOne2.0.1.c.320"})
|
||||
void mapNestedMixedStringIntegerFieldCorrectly(String path) {
|
||||
|
||||
Update update = new Update().set("levelOne.0.1.c", "4");
|
||||
Update update = new Update().set(path, "4");
|
||||
Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(),
|
||||
context.getPersistentEntity(EntityWithNestedMap.class));
|
||||
|
||||
assertThat(mappedUpdate).isEqualTo(new org.bson.Document("$set", new org.bson.Document("levelOne.0.1.c", "4")));
|
||||
assertThat(mappedUpdate).isEqualTo(new org.bson.Document("$set", new org.bson.Document(path, "4")));
|
||||
}
|
||||
|
||||
@Test // GH-3775
|
||||
@@ -1720,6 +1724,7 @@ class UpdateMapperUnitTests {
|
||||
|
||||
static class EntityWithNestedMap {
|
||||
Map<String, Map<String, Map<String, Object>>> levelOne;
|
||||
Map<String, Map<String, Map<String, Object>>> levelOne2;
|
||||
}
|
||||
|
||||
static class Customer {
|
||||
|
||||
@@ -0,0 +1,754 @@
|
||||
/*
|
||||
* Copyright 2023 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 org.assertj.core.api.Assertions.*;
|
||||
import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*;
|
||||
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
|
||||
import static org.springframework.data.mongodb.core.query.Criteria.*;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Month;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.bson.BsonBinary;
|
||||
import org.bson.Document;
|
||||
import org.bson.types.Binary;
|
||||
import org.junit.jupiter.api.Test;
|
||||
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.dao.PermissionDeniedDataAccessException;
|
||||
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.aggregation.Aggregation;
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
|
||||
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.core.query.Update;
|
||||
import org.springframework.data.util.Lazy;
|
||||
|
||||
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.model.Filters;
|
||||
import com.mongodb.client.model.IndexOptions;
|
||||
import com.mongodb.client.model.Indexes;
|
||||
import com.mongodb.client.model.vault.DataKeyOptions;
|
||||
import com.mongodb.client.vault.ClientEncryption;
|
||||
import com.mongodb.client.vault.ClientEncryptions;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public abstract class AbstractEncryptionTestBase {
|
||||
|
||||
@Autowired MongoTemplate template;
|
||||
|
||||
@Test // GH-4284
|
||||
void encryptAndDecryptSimpleValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.ssn = "mySecretSSN";
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("ssn")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4432
|
||||
void encryptAndDecryptJavaTime() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.today = LocalDate.of(1979, Month.SEPTEMBER, 18);
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("today")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void encryptAndDecryptComplexValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.address = new Address();
|
||||
source.address.city = "NYC";
|
||||
source.address.street = "4th Ave.";
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("address")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void encryptAndDecryptValueWithinComplexOne() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.encryptedZip = new AddressWithEncryptedZip();
|
||||
source.encryptedZip.city = "Boston";
|
||||
source.encryptedZip.street = "central square";
|
||||
source.encryptedZip.zip = "1234567890";
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> {
|
||||
assertThat(it.get("encryptedZip")).isInstanceOf(Document.class);
|
||||
assertThat(it.get("encryptedZip", Document.class).get("city")).isInstanceOf(String.class);
|
||||
assertThat(it.get("encryptedZip", Document.class).get("street")).isInstanceOf(String.class);
|
||||
assertThat(it.get("encryptedZip", Document.class).get("zip")).isInstanceOf(Binary.class);
|
||||
}) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void encryptAndDecryptListOfSimpleValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.listOfString = Arrays.asList("spring", "data", "mongodb");
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("listOfString")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void encryptAndDecryptListOfComplexValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
|
||||
Address address = new Address();
|
||||
address.city = "SFO";
|
||||
address.street = "---";
|
||||
|
||||
source.listOfComplex = Collections.singletonList(address);
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("listOfComplex")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void encryptAndDecryptMapOfSimpleValues() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.mapOfString = Map.of("k1", "v1", "k2", "v2");
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("mapOfString")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void encryptAndDecryptMapOfComplexValues() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
|
||||
Address address1 = new Address();
|
||||
address1.city = "SFO";
|
||||
address1.street = "---";
|
||||
|
||||
Address address2 = new Address();
|
||||
address2.city = "NYC";
|
||||
address2.street = "---";
|
||||
|
||||
source.mapOfComplex = Map.of("a1", address1, "a2", address2);
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("mapOfComplex")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void canQueryDeterministicallyEncrypted() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.ssn = "mySecretSSN";
|
||||
|
||||
template.save(source);
|
||||
|
||||
Person loaded = template.query(Person.class).matching(where("ssn").is(source.ssn)).firstValue();
|
||||
assertThat(loaded).isEqualTo(source);
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void cannotQueryRandomlyEncrypted() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.wallet = "secret-wallet-id";
|
||||
|
||||
template.save(source);
|
||||
|
||||
Person loaded = template.query(Person.class).matching(where("wallet").is(source.wallet)).firstValue();
|
||||
assertThat(loaded).isNull();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void updateSimpleTypeEncryptedFieldWithNewValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
|
||||
template.save(source);
|
||||
|
||||
template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("ssn", "secret-value"))
|
||||
.first();
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("ssn")).isInstanceOf(Binary.class)) //
|
||||
.loadedMatches(it -> assertThat(it.getSsn()).isEqualTo("secret-value"));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void updateComplexTypeEncryptedFieldWithNewValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
|
||||
template.save(source);
|
||||
|
||||
Address address = new Address();
|
||||
address.city = "SFO";
|
||||
address.street = "---";
|
||||
|
||||
template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("address", address)).first();
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("address")).isInstanceOf(Binary.class)) //
|
||||
.loadedMatches(it -> assertThat(it.getAddress()).isEqualTo(address));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void updateEncryptedFieldInNestedElementWithNewValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.encryptedZip = new AddressWithEncryptedZip();
|
||||
source.encryptedZip.city = "Boston";
|
||||
source.encryptedZip.street = "central square";
|
||||
|
||||
template.save(source);
|
||||
|
||||
template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("encryptedZip.zip", "179"))
|
||||
.first();
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> {
|
||||
assertThat(it.get("encryptedZip")).isInstanceOf(Document.class);
|
||||
assertThat(it.get("encryptedZip", Document.class).get("city")).isInstanceOf(String.class);
|
||||
assertThat(it.get("encryptedZip", Document.class).get("street")).isInstanceOf(String.class);
|
||||
assertThat(it.get("encryptedZip", Document.class).get("zip")).isInstanceOf(Binary.class);
|
||||
}) //
|
||||
.loadedMatches(it -> assertThat(it.getEncryptedZip().getZip()).isEqualTo("179"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aggregationWithMatch() {
|
||||
|
||||
Person person = new Person();
|
||||
person.id = "id-1";
|
||||
person.name = "p1-name";
|
||||
person.ssn = "mySecretSSN";
|
||||
|
||||
template.save(person);
|
||||
|
||||
AggregationResults<Person> aggregationResults = template.aggregateAndReturn(Person.class)
|
||||
.by(newAggregation(Person.class, Aggregation.match(where("ssn").is(person.ssn)))).all();
|
||||
assertThat(aggregationResults.getMappedResults()).containsExactly(person);
|
||||
}
|
||||
|
||||
@Test
|
||||
void altKeyDetection(@Autowired CachingMongoClientEncryption mongoClientEncryption) throws InterruptedException {
|
||||
|
||||
BsonBinary user1key = mongoClientEncryption.getClientEncryption().createDataKey("local",
|
||||
new DataKeyOptions().keyAltNames(Collections.singletonList("user-1")));
|
||||
|
||||
BsonBinary user2key = mongoClientEncryption.getClientEncryption().createDataKey("local",
|
||||
new DataKeyOptions().keyAltNames(Collections.singletonList("user-2")));
|
||||
|
||||
Person p1 = new Person();
|
||||
p1.id = "id-1";
|
||||
p1.name = "user-1";
|
||||
p1.ssn = "ssn";
|
||||
p1.viaAltKeyNameField = "value-1";
|
||||
|
||||
Person p2 = new Person();
|
||||
p2.id = "id-2";
|
||||
p2.name = "user-2";
|
||||
p2.viaAltKeyNameField = "value-1";
|
||||
|
||||
Person p3 = new Person();
|
||||
p3.id = "id-3";
|
||||
p3.name = "user-1";
|
||||
p3.viaAltKeyNameField = "value-1";
|
||||
|
||||
template.save(p1);
|
||||
template.save(p2);
|
||||
template.save(p3);
|
||||
|
||||
template.execute(Person.class, collection -> {
|
||||
collection.find(new Document()).forEach(it -> System.out.println(it.toJson()));
|
||||
return null;
|
||||
});
|
||||
|
||||
// remove the key and invalidate encrypted data
|
||||
mongoClientEncryption.getClientEncryption().deleteKey(user2key);
|
||||
|
||||
// clear the 60 second key cache within the mongo client
|
||||
mongoClientEncryption.destroy();
|
||||
|
||||
assertThat(template.query(Person.class).matching(where("id").is(p1.id)).firstValue()).isEqualTo(p1);
|
||||
|
||||
assertThatExceptionOfType(PermissionDeniedDataAccessException.class)
|
||||
.isThrownBy(() -> template.query(Person.class).matching(where("id").is(p2.id)).firstValue());
|
||||
}
|
||||
|
||||
<T> SaveAndLoadAssert<T> verifyThat(T source) {
|
||||
return new SaveAndLoadAssert<>(source);
|
||||
}
|
||||
|
||||
class SaveAndLoadAssert<T> {
|
||||
|
||||
T source;
|
||||
Function<T, ?> idProvider;
|
||||
|
||||
SaveAndLoadAssert(T source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
SaveAndLoadAssert<T> identifiedBy(Function<T, ?> idProvider) {
|
||||
this.idProvider = idProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
SaveAndLoadAssert<T> wasSavedAs(Document expected) {
|
||||
return wasSavedMatching(it -> Assertions.assertThat(it).isEqualTo(expected));
|
||||
}
|
||||
|
||||
SaveAndLoadAssert<T> wasSavedMatching(Consumer<Document> saved) {
|
||||
AbstractEncryptionTestBase.this.assertSaved(source, idProvider, saved);
|
||||
return this;
|
||||
}
|
||||
|
||||
SaveAndLoadAssert<T> loadedMatches(Consumer<T> expected) {
|
||||
AbstractEncryptionTestBase.this.assertLoaded(source, idProvider, expected);
|
||||
return this;
|
||||
}
|
||||
|
||||
SaveAndLoadAssert<T> loadedIsEqualToSource() {
|
||||
return loadedIsEqualTo(source);
|
||||
}
|
||||
|
||||
SaveAndLoadAssert<T> loadedIsEqualTo(T expected) {
|
||||
return loadedMatches(it -> Assertions.assertThat(it).isEqualTo(expected));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
<T> void assertSaved(T source, Function<T, ?> idProvider, Consumer<Document> dbValue) {
|
||||
|
||||
Document savedDocument = template.execute(Person.class, collection -> {
|
||||
|
||||
MongoNamespace namespace = collection.getNamespace();
|
||||
|
||||
try (MongoClient rawClient = MongoClients.create()) {
|
||||
return rawClient.getDatabase(namespace.getDatabaseName()).getCollection(namespace.getCollectionName())
|
||||
.find(new Document("_id", idProvider.apply(source))).first();
|
||||
}
|
||||
});
|
||||
dbValue.accept(savedDocument);
|
||||
}
|
||||
|
||||
<T> void assertLoaded(T source, Function<T, ?> idProvider, Consumer<T> loadedValue) {
|
||||
|
||||
T loaded = template.query((Class<T>) source.getClass()).matching(where("id").is(idProvider.apply(source)))
|
||||
.firstValue();
|
||||
|
||||
loadedValue.accept(loaded);
|
||||
}
|
||||
|
||||
protected static class EncryptionConfig extends AbstractMongoClientConfiguration {
|
||||
|
||||
@Autowired ApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
protected String getDatabaseName() {
|
||||
return "fle-test";
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MongoClient mongoClient() {
|
||||
return super.mongoClient();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configureConverters(MongoConverterConfigurationAdapter converterConfigurationAdapter) {
|
||||
|
||||
converterConfigurationAdapter
|
||||
.registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(applicationContext));
|
||||
}
|
||||
|
||||
@Bean
|
||||
MongoEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEncryption) {
|
||||
|
||||
Lazy<BsonBinary> dataKey = Lazy.of(() -> mongoClientEncryption.getClientEncryption().createDataKey("local",
|
||||
new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey"))));
|
||||
|
||||
return new MongoEncryptionConverter(mongoClientEncryption,
|
||||
EncryptionKeyResolver.annotated((ctx) -> EncryptionKey.keyId(dataKey.get())));
|
||||
}
|
||||
|
||||
@Bean
|
||||
CachingMongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) {
|
||||
return new CachingMongoClientEncryption(() -> ClientEncryptions.create(encryptionSettings));
|
||||
}
|
||||
|
||||
@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")));
|
||||
|
||||
MongoCollection<Document> collection = mongoClient.getDatabase(getDatabaseName()).getCollection("test");
|
||||
collection.drop(); // Clear old data
|
||||
|
||||
byte[] localMasterKey = new byte[96];
|
||||
new SecureRandom().nextBytes(localMasterKey);
|
||||
Map<String, Map<String, Object>> kmsProviders = Map.of("local", Map.of("key", localMasterKey));
|
||||
|
||||
// Create the ClientEncryption instance
|
||||
return ClientEncryptionSettings.builder() //
|
||||
.keyVaultMongoClientSettings(
|
||||
MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb://localhost")).build()) //
|
||||
.keyVaultNamespace(keyVaultNamespace.getFullName()) //
|
||||
.kmsProviders(kmsProviders) //
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
static class CachingMongoClientEncryption extends MongoClientEncryption implements DisposableBean {
|
||||
|
||||
static final AtomicReference<ClientEncryption> cache = new AtomicReference<>();
|
||||
|
||||
CachingMongoClientEncryption(Supplier<ClientEncryption> source) {
|
||||
super(() -> {
|
||||
|
||||
if (cache.get() != null) {
|
||||
return cache.get();
|
||||
}
|
||||
|
||||
ClientEncryption 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 = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //
|
||||
String ssn;
|
||||
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "mySuperSecretKey") //
|
||||
String wallet;
|
||||
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // full document must be random
|
||||
Address address;
|
||||
|
||||
AddressWithEncryptedZip encryptedZip;
|
||||
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random
|
||||
List<String> listOfString;
|
||||
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random
|
||||
List<Address> listOfComplex;
|
||||
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "/name") //
|
||||
String viaAltKeyNameField;
|
||||
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
|
||||
Map<String, String> mapOfString;
|
||||
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
|
||||
Map<String, Address> mapOfComplex;
|
||||
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
|
||||
LocalDate today;
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public String getSsn() {
|
||||
return this.ssn;
|
||||
}
|
||||
|
||||
public String getWallet() {
|
||||
return this.wallet;
|
||||
}
|
||||
|
||||
public Address getAddress() {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
public AddressWithEncryptedZip getEncryptedZip() {
|
||||
return this.encryptedZip;
|
||||
}
|
||||
|
||||
public List<String> getListOfString() {
|
||||
return this.listOfString;
|
||||
}
|
||||
|
||||
public List<Address> getListOfComplex() {
|
||||
return this.listOfComplex;
|
||||
}
|
||||
|
||||
public String getViaAltKeyNameField() {
|
||||
return this.viaAltKeyNameField;
|
||||
}
|
||||
|
||||
public Map<String, String> getMapOfString() {
|
||||
return this.mapOfString;
|
||||
}
|
||||
|
||||
public Map<String, Address> getMapOfComplex() {
|
||||
return this.mapOfComplex;
|
||||
}
|
||||
|
||||
public LocalDate getToday() {
|
||||
return today;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public void setSsn(String ssn) {
|
||||
this.ssn = ssn;
|
||||
}
|
||||
|
||||
public void setWallet(String wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public void setAddress(Address address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public void setEncryptedZip(AddressWithEncryptedZip encryptedZip) {
|
||||
this.encryptedZip = encryptedZip;
|
||||
}
|
||||
|
||||
public void setListOfString(List<String> listOfString) {
|
||||
this.listOfString = listOfString;
|
||||
}
|
||||
|
||||
public void setListOfComplex(List<Address> listOfComplex) {
|
||||
this.listOfComplex = listOfComplex;
|
||||
}
|
||||
|
||||
public void setViaAltKeyNameField(String viaAltKeyNameField) {
|
||||
this.viaAltKeyNameField = viaAltKeyNameField;
|
||||
}
|
||||
|
||||
public void setMapOfString(Map<String, String> mapOfString) {
|
||||
this.mapOfString = mapOfString;
|
||||
}
|
||||
|
||||
public void setMapOfComplex(Map<String, Address> mapOfComplex) {
|
||||
this.mapOfComplex = mapOfComplex;
|
||||
}
|
||||
|
||||
public void setToday(LocalDate today) {
|
||||
this.today = today;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == this) {
|
||||
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(ssn, person.ssn)
|
||||
&& Objects.equals(wallet, person.wallet) && Objects.equals(address, person.address)
|
||||
&& Objects.equals(encryptedZip, person.encryptedZip) && Objects.equals(listOfString, person.listOfString)
|
||||
&& Objects.equals(listOfComplex, person.listOfComplex)
|
||||
&& Objects.equals(viaAltKeyNameField, person.viaAltKeyNameField)
|
||||
&& Objects.equals(mapOfString, person.mapOfString) && Objects.equals(mapOfComplex, person.mapOfComplex)
|
||||
&& Objects.equals(today, person.today);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, name, ssn, wallet, address, encryptedZip, listOfString, listOfComplex, viaAltKeyNameField,
|
||||
mapOfString, mapOfComplex, today);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "EncryptionTests.Person(id=" + this.getId() + ", name=" + this.getName() + ", ssn=" + this.getSsn()
|
||||
+ ", wallet=" + this.getWallet() + ", address=" + this.getAddress() + ", encryptedZip="
|
||||
+ this.getEncryptedZip() + ", listOfString=" + this.getListOfString() + ", listOfComplex="
|
||||
+ this.getListOfComplex() + ", viaAltKeyNameField=" + this.getViaAltKeyNameField() + ", mapOfString="
|
||||
+ this.getMapOfString() + ", mapOfComplex=" + this.getMapOfComplex() + ", today=" + this.getToday() + ")";
|
||||
}
|
||||
}
|
||||
|
||||
static class Address {
|
||||
String city;
|
||||
String street;
|
||||
|
||||
public Address() {}
|
||||
|
||||
public String getCity() {
|
||||
return this.city;
|
||||
}
|
||||
|
||||
public String getStreet() {
|
||||
return this.street;
|
||||
}
|
||||
|
||||
public void setCity(String city) {
|
||||
this.city = city;
|
||||
}
|
||||
|
||||
public void setStreet(String street) {
|
||||
this.street = street;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == this) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Address address = (Address) o;
|
||||
return Objects.equals(city, address.city) && Objects.equals(street, address.street);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(city, street);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "EncryptionTests.Address(city=" + this.getCity() + ", street=" + this.getStreet() + ")";
|
||||
}
|
||||
}
|
||||
|
||||
static class AddressWithEncryptedZip extends Address {
|
||||
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) String zip;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AddressWithEncryptedZip{" + "zip='" + zip + '\'' + ", city='" + getCity() + '\'' + ", street='"
|
||||
+ getStreet() + '\'' + '}';
|
||||
}
|
||||
|
||||
public String getZip() {
|
||||
return this.zip;
|
||||
}
|
||||
|
||||
public void setZip(String zip) {
|
||||
this.zip = zip;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2023 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 java.util.Collections;
|
||||
|
||||
import org.bson.BsonBinary;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.convert.PropertyValueConverterFactory;
|
||||
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
|
||||
import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter;
|
||||
import org.springframework.data.mongodb.core.encryption.BypassAutoEncryptionTest.Config;
|
||||
import org.springframework.data.util.Lazy;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import com.mongodb.AutoEncryptionSettings;
|
||||
import com.mongodb.ClientEncryptionSettings;
|
||||
import com.mongodb.MongoClientSettings.Builder;
|
||||
import com.mongodb.client.MongoClient;
|
||||
import com.mongodb.client.MongoClients;
|
||||
import com.mongodb.client.model.vault.DataKeyOptions;
|
||||
import com.mongodb.client.vault.ClientEncryptions;
|
||||
|
||||
/**
|
||||
* Encryption tests for client having {@link AutoEncryptionSettings#isBypassAutoEncryption()}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = Config.class)
|
||||
public class BypassAutoEncryptionTest extends AbstractEncryptionTestBase {
|
||||
|
||||
@Disabled
|
||||
@Override
|
||||
void altKeyDetection(@Autowired CachingMongoClientEncryption mongoClientEncryption) throws InterruptedException {
|
||||
super.altKeyDetection(mongoClientEncryption);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config extends EncryptionConfig {
|
||||
|
||||
@Autowired ApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
protected void configureClientSettings(Builder builder) {
|
||||
|
||||
MongoClient mongoClient = MongoClients.create();
|
||||
ClientEncryptionSettings clientEncryptionSettings = encryptionSettings(mongoClient);
|
||||
mongoClient.close();
|
||||
|
||||
builder.autoEncryptionSettings(AutoEncryptionSettings.builder() //
|
||||
.kmsProviders(clientEncryptionSettings.getKmsProviders()) //
|
||||
.keyVaultNamespace(clientEncryptionSettings.getKeyVaultNamespace()) //
|
||||
.bypassAutoEncryption(true).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configureConverters(MongoConverterConfigurationAdapter converterConfigurationAdapter) {
|
||||
|
||||
converterConfigurationAdapter
|
||||
.registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(applicationContext));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Override
|
||||
MongoEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEncryption) {
|
||||
|
||||
Lazy<BsonBinary> dataKey = Lazy.of(() -> mongoClientEncryption.getClientEncryption().createDataKey("local",
|
||||
new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey"))));
|
||||
|
||||
return new MongoEncryptionConverter(mongoClientEncryption,
|
||||
EncryptionKeyResolver.annotated((ctx) -> EncryptionKey.keyId(dataKey.get())));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Override
|
||||
CachingMongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) {
|
||||
return new CachingMongoClientEncryption(() -> ClientEncryptions.create(encryptionSettings));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,48 +15,30 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.encryption;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*;
|
||||
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
|
||||
import static org.springframework.data.mongodb.core.query.Criteria.*;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.bson.BsonBinary;
|
||||
import org.bson.Document;
|
||||
import org.bson.types.Binary;
|
||||
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.context.annotation.Configuration;
|
||||
import org.springframework.dao.PermissionDeniedDataAccessException;
|
||||
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.aggregation.Aggregation;
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
|
||||
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
|
||||
import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter;
|
||||
import org.springframework.data.mongodb.core.encryption.EncryptionTests.Config;
|
||||
import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted;
|
||||
import org.springframework.data.mongodb.core.query.Update;
|
||||
import org.springframework.data.util.Lazy;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
@@ -71,7 +53,6 @@ import com.mongodb.client.model.Filters;
|
||||
import com.mongodb.client.model.IndexOptions;
|
||||
import com.mongodb.client.model.Indexes;
|
||||
import com.mongodb.client.model.vault.DataKeyOptions;
|
||||
import com.mongodb.client.vault.ClientEncryption;
|
||||
import com.mongodb.client.vault.ClientEncryptions;
|
||||
|
||||
/**
|
||||
@@ -79,345 +60,7 @@ import com.mongodb.client.vault.ClientEncryptions;
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = Config.class)
|
||||
public class EncryptionTests {
|
||||
|
||||
@Autowired MongoTemplate template;
|
||||
|
||||
@Test // GH-4284
|
||||
void encryptAndDecryptSimpleValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.ssn = "mySecretSSN";
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("ssn")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void encryptAndDecryptComplexValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.address = new Address();
|
||||
source.address.city = "NYC";
|
||||
source.address.street = "4th Ave.";
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("address")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void encryptAndDecryptValueWithinComplexOne() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.encryptedZip = new AddressWithEncryptedZip();
|
||||
source.encryptedZip.city = "Boston";
|
||||
source.encryptedZip.street = "central square";
|
||||
source.encryptedZip.zip = "1234567890";
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> {
|
||||
assertThat(it.get("encryptedZip")).isInstanceOf(Document.class);
|
||||
assertThat(it.get("encryptedZip", Document.class).get("city")).isInstanceOf(String.class);
|
||||
assertThat(it.get("encryptedZip", Document.class).get("street")).isInstanceOf(String.class);
|
||||
assertThat(it.get("encryptedZip", Document.class).get("zip")).isInstanceOf(Binary.class);
|
||||
}) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void encryptAndDecryptListOfSimpleValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.listOfString = Arrays.asList("spring", "data", "mongodb");
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("listOfString")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void encryptAndDecryptListOfComplexValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
|
||||
Address address = new Address();
|
||||
address.city = "SFO";
|
||||
address.street = "---";
|
||||
|
||||
source.listOfComplex = Collections.singletonList(address);
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("listOfComplex")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void encryptAndDecryptMapOfSimpleValues() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.mapOfString = Map.of("k1", "v1", "k2", "v2");
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("mapOfString")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void encryptAndDecryptMapOfComplexValues() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
|
||||
Address address1 = new Address();
|
||||
address1.city = "SFO";
|
||||
address1.street = "---";
|
||||
|
||||
Address address2 = new Address();
|
||||
address2.city = "NYC";
|
||||
address2.street = "---";
|
||||
|
||||
source.mapOfComplex = Map.of("a1", address1, "a2", address2);
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("mapOfComplex")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void canQueryDeterministicallyEncrypted() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.ssn = "mySecretSSN";
|
||||
|
||||
template.save(source);
|
||||
|
||||
Person loaded = template.query(Person.class).matching(where("ssn").is(source.ssn)).firstValue();
|
||||
assertThat(loaded).isEqualTo(source);
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void cannotQueryRandomlyEncrypted() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.wallet = "secret-wallet-id";
|
||||
|
||||
template.save(source);
|
||||
|
||||
Person loaded = template.query(Person.class).matching(where("wallet").is(source.wallet)).firstValue();
|
||||
assertThat(loaded).isNull();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void updateSimpleTypeEncryptedFieldWithNewValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
|
||||
template.save(source);
|
||||
|
||||
template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("ssn", "secret-value"))
|
||||
.first();
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("ssn")).isInstanceOf(Binary.class)) //
|
||||
.loadedMatches(it -> assertThat(it.getSsn()).isEqualTo("secret-value"));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void updateComplexTypeEncryptedFieldWithNewValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
|
||||
template.save(source);
|
||||
|
||||
Address address = new Address();
|
||||
address.city = "SFO";
|
||||
address.street = "---";
|
||||
|
||||
template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("address", address)).first();
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("address")).isInstanceOf(Binary.class)) //
|
||||
.loadedMatches(it -> assertThat(it.getAddress()).isEqualTo(address));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void updateEncryptedFieldInNestedElementWithNewValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.encryptedZip = new AddressWithEncryptedZip();
|
||||
source.encryptedZip.city = "Boston";
|
||||
source.encryptedZip.street = "central square";
|
||||
|
||||
template.save(source);
|
||||
|
||||
template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("encryptedZip.zip", "179"))
|
||||
.first();
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> {
|
||||
assertThat(it.get("encryptedZip")).isInstanceOf(Document.class);
|
||||
assertThat(it.get("encryptedZip", Document.class).get("city")).isInstanceOf(String.class);
|
||||
assertThat(it.get("encryptedZip", Document.class).get("street")).isInstanceOf(String.class);
|
||||
assertThat(it.get("encryptedZip", Document.class).get("zip")).isInstanceOf(Binary.class);
|
||||
}) //
|
||||
.loadedMatches(it -> assertThat(it.getEncryptedZip().getZip()).isEqualTo("179"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aggregationWithMatch() {
|
||||
|
||||
Person person = new Person();
|
||||
person.id = "id-1";
|
||||
person.name = "p1-name";
|
||||
person.ssn = "mySecretSSN";
|
||||
|
||||
template.save(person);
|
||||
|
||||
AggregationResults<Person> aggregationResults = template.aggregateAndReturn(Person.class)
|
||||
.by(newAggregation(Person.class, Aggregation.match(where("ssn").is(person.ssn)))).all();
|
||||
assertThat(aggregationResults.getMappedResults()).containsExactly(person);
|
||||
}
|
||||
|
||||
@Test
|
||||
void altKeyDetection(@Autowired CachingMongoClientEncryption mongoClientEncryption) throws InterruptedException {
|
||||
|
||||
BsonBinary user1key = mongoClientEncryption.getClientEncryption().createDataKey("local",
|
||||
new DataKeyOptions().keyAltNames(Collections.singletonList("user-1")));
|
||||
|
||||
BsonBinary user2key = mongoClientEncryption.getClientEncryption().createDataKey("local",
|
||||
new DataKeyOptions().keyAltNames(Collections.singletonList("user-2")));
|
||||
|
||||
Person p1 = new Person();
|
||||
p1.id = "id-1";
|
||||
p1.name = "user-1";
|
||||
p1.ssn = "ssn";
|
||||
p1.viaAltKeyNameField = "value-1";
|
||||
|
||||
Person p2 = new Person();
|
||||
p2.id = "id-2";
|
||||
p2.name = "user-2";
|
||||
p2.viaAltKeyNameField = "value-1";
|
||||
|
||||
Person p3 = new Person();
|
||||
p3.id = "id-3";
|
||||
p3.name = "user-1";
|
||||
p3.viaAltKeyNameField = "value-1";
|
||||
|
||||
template.save(p1);
|
||||
template.save(p2);
|
||||
template.save(p3);
|
||||
|
||||
template.execute(Person.class, collection -> {
|
||||
collection.find(new Document()).forEach(it -> System.out.println(it.toJson()));
|
||||
return null;
|
||||
});
|
||||
|
||||
// remove the key and invalidate encrypted data
|
||||
mongoClientEncryption.getClientEncryption().deleteKey(user2key);
|
||||
|
||||
// clear the 60 second key cache within the mongo client
|
||||
mongoClientEncryption.destroy();
|
||||
|
||||
assertThat(template.query(Person.class).matching(where("id").is(p1.id)).firstValue()).isEqualTo(p1);
|
||||
|
||||
assertThatExceptionOfType(PermissionDeniedDataAccessException.class)
|
||||
.isThrownBy(() -> template.query(Person.class).matching(where("id").is(p2.id)).firstValue());
|
||||
}
|
||||
|
||||
<T> SaveAndLoadAssert<T> verifyThat(T source) {
|
||||
return new SaveAndLoadAssert<>(source);
|
||||
}
|
||||
|
||||
class SaveAndLoadAssert<T> {
|
||||
|
||||
T source;
|
||||
Function<T, ?> idProvider;
|
||||
|
||||
SaveAndLoadAssert(T source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
SaveAndLoadAssert<T> identifiedBy(Function<T, ?> idProvider) {
|
||||
this.idProvider = idProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
SaveAndLoadAssert<T> wasSavedAs(Document expected) {
|
||||
return wasSavedMatching(it -> Assertions.assertThat(it).isEqualTo(expected));
|
||||
}
|
||||
|
||||
SaveAndLoadAssert<T> wasSavedMatching(Consumer<Document> saved) {
|
||||
EncryptionTests.this.assertSaved(source, idProvider, saved);
|
||||
return this;
|
||||
}
|
||||
|
||||
SaveAndLoadAssert<T> loadedMatches(Consumer<T> expected) {
|
||||
EncryptionTests.this.assertLoaded(source, idProvider, expected);
|
||||
return this;
|
||||
}
|
||||
|
||||
SaveAndLoadAssert<T> loadedIsEqualToSource() {
|
||||
return loadedIsEqualTo(source);
|
||||
}
|
||||
|
||||
SaveAndLoadAssert<T> loadedIsEqualTo(T expected) {
|
||||
return loadedMatches(it -> Assertions.assertThat(it).isEqualTo(expected));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
<T> void assertSaved(T source, Function<T, ?> idProvider, Consumer<Document> dbValue) {
|
||||
|
||||
Document savedDocument = template.execute(Person.class, collection -> {
|
||||
return collection.find(new Document("_id", idProvider.apply(source))).first();
|
||||
});
|
||||
dbValue.accept(savedDocument);
|
||||
}
|
||||
|
||||
<T> void assertLoaded(T source, Function<T, ?> idProvider, Consumer<T> loadedValue) {
|
||||
|
||||
T loaded = template.query((Class<T>) source.getClass()).matching(where("id").is(idProvider.apply(source)))
|
||||
.firstValue();
|
||||
|
||||
loadedValue.accept(loaded);
|
||||
}
|
||||
public class EncryptionTests extends AbstractEncryptionTestBase {
|
||||
|
||||
@Configuration
|
||||
static class Config extends AbstractMongoClientConfiguration {
|
||||
@@ -430,6 +73,7 @@ public class EncryptionTests {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Override
|
||||
public MongoClient mongoClient() {
|
||||
return super.mongoClient();
|
||||
}
|
||||
@@ -470,57 +114,21 @@ public class EncryptionTests {
|
||||
MongoCollection<Document> collection = mongoClient.getDatabase(getDatabaseName()).getCollection("test");
|
||||
collection.drop(); // Clear old data
|
||||
|
||||
final byte[] localMasterKey = new byte[96];
|
||||
byte[] localMasterKey = new byte[96];
|
||||
new SecureRandom().nextBytes(localMasterKey);
|
||||
Map<String, Map<String, Object>> kmsProviders = new HashMap<>() {
|
||||
{
|
||||
put("local", new HashMap<>() {
|
||||
{
|
||||
put("key", localMasterKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
Map<String, Map<String, Object>> kmsProviders = Map.of("local", Map.of("key", localMasterKey));
|
||||
|
||||
// Create the ClientEncryption instance
|
||||
ClientEncryptionSettings clientEncryptionSettings = ClientEncryptionSettings.builder()
|
||||
return ClientEncryptionSettings.builder()
|
||||
.keyVaultMongoClientSettings(
|
||||
MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb://localhost")).build())
|
||||
.keyVaultNamespace(keyVaultNamespace.getFullName()).kmsProviders(kmsProviders).build();
|
||||
return clientEncryptionSettings;
|
||||
MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb://localhost")).build()) //
|
||||
.keyVaultNamespace(keyVaultNamespace.getFullName()) //
|
||||
.kmsProviders(kmsProviders) //
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class CachingMongoClientEncryption extends MongoClientEncryption implements DisposableBean {
|
||||
|
||||
static final AtomicReference<ClientEncryption> cache = new AtomicReference<>();
|
||||
|
||||
CachingMongoClientEncryption(Supplier<ClientEncryption> source) {
|
||||
super(() -> {
|
||||
|
||||
if (cache.get() != null) {
|
||||
return cache.get();
|
||||
}
|
||||
|
||||
ClientEncryption clientEncryption = source.get();
|
||||
cache.set(clientEncryption);
|
||||
|
||||
return clientEncryption;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
|
||||
ClientEncryption clientEncryption = cache.get();
|
||||
if (clientEncryption != null) {
|
||||
clientEncryption.close();
|
||||
cache.set(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@org.springframework.data.mongodb.core.mapping.Document("test")
|
||||
static class Person {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Spring Data MongoDB 4.1.1 (2023.0.1)
|
||||
Spring Data MongoDB 4.1.2 (2023.0.2)
|
||||
Copyright (c) [2010-2019] Pivotal Software, Inc.
|
||||
|
||||
This product is licensed to you under the Apache License, Version 2.0 (the "License").
|
||||
@@ -46,5 +46,6 @@ conditions of the subcomponent's license, as noted in the LICENSE file.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user