diff --git a/pom.xml b/pom.xml
index 95fc8379d9..5e2ced9221 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-mongodb-parent
- 5.0.0-SNAPSHOT
+ 5.0.x-GH-4988-SNAPSHOT
pom
Spring Data MongoDB
diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml
index fc88571622..00f356a093 100644
--- a/spring-data-mongodb-distribution/pom.xml
+++ b/spring-data-mongodb-distribution/pom.xml
@@ -15,7 +15,7 @@
org.springframework.data
spring-data-mongodb-parent
- 5.0.0-SNAPSHOT
+ 5.0.x-GH-4988-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml
index 6f34da5660..ec008323d3 100644
--- a/spring-data-mongodb/pom.xml
+++ b/spring-data-mongodb/pom.xml
@@ -13,7 +13,7 @@
org.springframework.data
spring-data-mongodb-parent
- 5.0.0-SNAPSHOT
+ 5.0.x-GH-4988-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
index f4d1891703..287c4bd206 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
@@ -34,6 +34,7 @@
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.QueryableJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
@@ -681,17 +682,16 @@ public static class EncryptedFieldsOptions {
private static final EncryptedFieldsOptions NONE = new EncryptedFieldsOptions();
private final @Nullable MongoJsonSchema schema;
- private final List queryableProperties;
+ private final List properties;
EncryptedFieldsOptions() {
this(null, List.of());
}
- private EncryptedFieldsOptions(@Nullable MongoJsonSchema schema,
- List queryableProperties) {
+ private EncryptedFieldsOptions(@Nullable MongoJsonSchema schema, List queryableProperties) {
this.schema = schema;
- this.queryableProperties = queryableProperties;
+ this.properties = queryableProperties;
}
/**
@@ -711,7 +711,7 @@ public static EncryptedFieldsOptions fromSchema(MongoJsonSchema schema) {
/**
* @return new instance of {@link EncryptedFieldsOptions}.
*/
- public static EncryptedFieldsOptions fromProperties(List properties) {
+ public static EncryptedFieldsOptions fromProperties(List properties) {
return new EncryptedFieldsOptions(null, List.copyOf(properties));
}
@@ -731,13 +731,50 @@ public static EncryptedFieldsOptions fromProperties(List targetPropertyList = new ArrayList<>(queryableProperties.size() + 1);
- targetPropertyList.addAll(queryableProperties);
+ List targetPropertyList = new ArrayList<>(properties.size() + 1);
+ targetPropertyList.addAll(properties);
targetPropertyList.add(JsonSchemaProperty.queryable(property, List.of(characteristics)));
return new EncryptedFieldsOptions(schema, targetPropertyList);
}
+ /**
+ * Add an {@link EncryptedJsonSchemaProperty encrypted property} that should not be queryable.
+ *
+ * @param property must not be {@literal null}.
+ * @return new instance of {@link EncryptedFieldsOptions}.
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ public EncryptedFieldsOptions with(EncryptedJsonSchemaProperty property) {
+ return encrypted(property, null);
+ }
+
+ /**
+ * Add a {@link JsonSchemaProperty property} that should not be encrypted but not queryable.
+ *
+ * @param property must not be {@literal null}.
+ * @param key can be {@literal null}.
+ * @return new instance of {@link EncryptedFieldsOptions}.
+ */
+ @Contract("_, _ -> new")
+ @CheckReturnValue
+ public EncryptedFieldsOptions encrypted(JsonSchemaProperty property, @Nullable Object key) {
+
+ List targetPropertyList = new ArrayList<>(properties.size() + 1);
+ targetPropertyList.addAll(properties);
+ if (property instanceof IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty) {
+ targetPropertyList.add(property);
+ } else {
+ EncryptedJsonSchemaProperty encryptedJsonSchemaProperty = new EncryptedJsonSchemaProperty(property);
+ if (key != null) {
+ targetPropertyList.add(encryptedJsonSchemaProperty.keyId(key));
+ }
+ }
+
+ return new EncryptedFieldsOptions(schema, targetPropertyList);
+ }
+
public Document toDocument() {
return new Document("fields", selectPaths());
}
@@ -756,12 +793,12 @@ private List selectPaths() {
private List fromProperties() {
- if (queryableProperties.isEmpty()) {
+ if (properties.isEmpty()) {
return List.of();
}
- List converted = new ArrayList<>(queryableProperties.size());
- for (QueryableJsonSchemaProperty property : queryableProperties) {
+ List converted = new ArrayList<>(properties.size());
+ for (JsonSchemaProperty property : properties) {
Document field = new Document("path", property.getIdentifier());
@@ -769,7 +806,7 @@ private List fromProperties() {
field.append("bsonType", property.getTypes().iterator().next().toBsonType().value());
}
- if (property
+ if (property instanceof QueryableJsonSchemaProperty qproperty && qproperty
.getTargetProperty() instanceof IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty encrypted) {
if (encrypted.getKeyId() != null) {
if (encrypted.getKeyId() instanceof String stringKey) {
@@ -779,11 +816,21 @@ private List fromProperties() {
field.append("keyId", encrypted.getKeyId());
}
}
+ } else if (property instanceof IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty encrypted) {
+ if (encrypted.getKeyId() != null) {
+ if (encrypted.getKeyId() instanceof String stringKey) {
+ field.append("keyId",
+ new BsonBinary(BsonBinarySubType.UUID_STANDARD, stringKey.getBytes(StandardCharsets.UTF_8)));
+ } else {
+ field.append("keyId", encrypted.getKeyId());
+ }
+ }
}
- field.append("queries", StreamSupport.stream(property.getCharacteristics().spliterator(), false)
- .map(QueryCharacteristic::toDocument).toList());
-
+ if (property instanceof QueryableJsonSchemaProperty qproperty) {
+ field.append("queries", StreamSupport.stream(qproperty.getCharacteristics().spliterator(), false)
+ .map(QueryCharacteristic::toDocument).toList());
+ }
if (!field.containsKey("keyId")) {
field.append("keyId", BsonNull.VALUE);
}
@@ -812,7 +859,9 @@ private List fromSchema() {
if (entry.getValue().containsKey("bsonType")) {
field.append("bsonType", entry.getValue().get("bsonType"));
}
- field.put("queries", entry.getValue().get("queries"));
+ if (entry.getValue().containsKey("queries")) {
+ field.put("queries", entry.getValue().get("queries"));
+ }
fields.add(field);
}
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java
index 9de0863cd2..fd8c9fb972 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java
@@ -89,22 +89,25 @@ void validatorEquals() {
.isNotEqualTo(empty().validator(Validator.document(new Document("one", "two"))).moderateValidation());
}
- @Test // GH-4185
+ @Test // GH-4185, GH-4988
@SuppressWarnings("unchecked")
void queryableEncryptionOptionsFromSchemaRenderCorrectly() {
MongoJsonSchema schema = MongoJsonSchema.builder()
.property(JsonSchemaProperty.object("spring")
.properties(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int32("data")), List.of())))
- .property(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("mongodb")), List.of())).build();
+ .property(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("mongodb")), List.of()))
+ .property(JsonSchemaProperty.encrypted(JsonSchemaProperty.string("rocks"))).build();
EncryptedFieldsOptions encryptionOptions = EncryptedFieldsOptions.fromSchema(schema);
- assertThat(encryptionOptions.toDocument().get("fields", List.class)).hasSize(2)
+ assertThat(encryptionOptions.toDocument().get("fields", List.class)).hasSize(3)
.contains(new Document("path", "mongodb").append("bsonType", "long").append("queries", List.of())
.append("keyId", BsonNull.VALUE))
.contains(new Document("path", "spring.data").append("bsonType", "int").append("queries", List.of())
- .append("keyId", BsonNull.VALUE));
+ .append("keyId", BsonNull.VALUE))
+ .contains(new Document("path", "rocks").append("bsonType", "string").append("keyId", BsonNull.VALUE));
+
}
@Test // GH-4185
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoQueryableEncryptionCollectionCreationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoQueryableEncryptionCollectionCreationTests.java
index b801d1770b..fc2b5426b8 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoQueryableEncryptionCollectionCreationTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoQueryableEncryptionCollectionCreationTests.java
@@ -15,9 +15,12 @@
*/
package org.springframework.data.mongodb.core.encryption;
-import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.*;
-import static org.springframework.data.mongodb.core.schema.QueryCharacteristics.*;
-import static org.springframework.data.mongodb.test.util.Assertions.*;
+import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.encrypted;
+import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.int32;
+import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.int64;
+import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.queryable;
+import static org.springframework.data.mongodb.core.schema.QueryCharacteristics.range;
+import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
import java.util.List;
import java.util.UUID;
@@ -31,7 +34,6 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
-
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
@@ -94,12 +96,16 @@ public void createsCollectionWithEncryptedFieldsCorrectly(CollectionOptions coll
assertThat(encryptedFields).containsKey("fields");
List fields = encryptedFields.get("fields", List.of());
- assertThat(fields.get(0)).containsEntry("path", "encryptedInt") //
+ assertThat(fields.get(0)).containsEntry("path", "encrypted-but-not-queryable") //
+ .containsEntry("bsonType", "int") //
+ .doesNotContainKey("queries");
+
+ assertThat(fields.get(1)).containsEntry("path", "encryptedInt") //
.containsEntry("bsonType", "int") //
.containsEntry("queries", List
.of(Document.parse("{'queryType': 'range', 'contention': { '$numberLong' : '1' }, 'min': 5, 'max': 100}")));
- assertThat(fields.get(1)).containsEntry("path", "nested.encryptedLong") //
+ assertThat(fields.get(2)).containsEntry("path", "nested.encryptedLong") //
.containsEntry("bsonType", "long") //
.containsEntry("queries", List.of(Document.parse(
"{'queryType': 'range', 'contention': { '$numberLong' : '0' }, 'min': { '$numberLong' : '-1' }, 'max': { '$numberLong' : '1' }}")));
@@ -109,16 +115,19 @@ private static Stream collectionOptions() {
BsonBinary key1 = new BsonBinary(UUID.randomUUID(), UuidRepresentation.STANDARD);
BsonBinary key2 = new BsonBinary(UUID.randomUUID(), UuidRepresentation.STANDARD);
+ BsonBinary key3 = new BsonBinary(UUID.randomUUID(), UuidRepresentation.STANDARD);
CollectionOptions manualOptions = CollectionOptions.encryptedCollection(options -> options //
- .queryable(encrypted(int32("encryptedInt")).keys(key1), range().min(5).max(100).contention(1)) //
- .queryable(encrypted(JsonSchemaProperty.int64("nested.encryptedLong")).keys(key2),
+ .encrypted(int32("encrypted-but-not-queryable"), key1) //
+ .queryable(encrypted(int32("encryptedInt")).keyId(key2), range().min(5).max(100).contention(1)) //
+ .queryable(encrypted(JsonSchemaProperty.int64("nested.encryptedLong")).keyId(key3),
range().min(-1L).max(1L).contention(0)));
- CollectionOptions schemaOptions = CollectionOptions.encryptedCollection(MongoJsonSchema.builder()
+ CollectionOptions schemaOptions = CollectionOptions.encryptedCollection(MongoJsonSchema.builder() //
+ .property(encrypted(int32("encrypted-but-not-queryable")).keyId(key1)) //
.property(
- queryable(encrypted(int32("encryptedInt")).keyId(key1), List.of(range().min(5).max(100).contention(1))))
- .property(queryable(encrypted(int64("nested.encryptedLong")).keyId(key2),
+ queryable(encrypted(int32("encryptedInt")).keyId(key2), List.of(range().min(5).max(100).contention(1))))
+ .property(queryable(encrypted(int64("nested.encryptedLong")).keyId(key3),
List.of(range().min(-1L).max(1L).contention(0))))
.build());
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java
index e4e760cc91..01cbdec9d5 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java
@@ -15,8 +15,9 @@
*/
package org.springframework.data.mongodb.core.encryption;
-import static org.assertj.core.api.Assertions.*;
-import static org.springframework.data.mongodb.core.query.Criteria.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.springframework.data.mongodb.core.query.Criteria.where;
import java.security.SecureRandom;
import java.util.LinkedHashMap;
@@ -32,13 +33,10 @@
import org.bson.BsonInt32;
import org.bson.BsonString;
import org.bson.Document;
-import org.junit.Before;
import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeAll;
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;
@@ -125,11 +123,11 @@ void manuallyEncryptedValuesCanBeSavedAndRetrievedCorrectly() {
EncryptOptions equalityEncOptions = new EncryptOptions("Indexed").contentionFactor(0L)
.keyId(keyHolder.getEncryptionKey("age"));
- ;
EncryptOptions equalityEncOptionsString = new EncryptOptions("Indexed").contentionFactor(0L)
.keyId(keyHolder.getEncryptionKey("name"));
- ;
+
+ EncryptOptions justEncryptOptions = new EncryptOptions("Unindexed").keyId(keyHolder.getEncryptionKey("ssn"));
Document source = new Document("_id", "id-1");
@@ -137,6 +135,7 @@ void manuallyEncryptedValuesCanBeSavedAndRetrievedCorrectly() {
clientEncryption.getClientEncryption().encrypt(new BsonString("It's a Me, Mario!"), equalityEncOptionsString));
source.put("age", clientEncryption.getClientEncryption().encrypt(new BsonInt32(101), equalityEncOptions));
source.put("encryptedInt", clientEncryption.getClientEncryption().encrypt(new BsonInt32(101), encryptOptions));
+ source.put("ssn", clientEncryption.getClientEncryption().encrypt(new BsonString("6-4-20"), justEncryptOptions));
source.put("_class", Person.class.getName());
template.execute(Person.class, col -> col.insertOne(source));
@@ -151,6 +150,8 @@ void manuallyEncryptedValuesCanBeSavedAndRetrievedCorrectly() {
});
assertThat(result).containsEntry("encryptedInt", 101);
+ assertThat(result).containsEntry("age", 101);
+ assertThat(result).containsEntry("ssn", "6-4-20");
}
@Test // GH-4185
@@ -283,6 +284,7 @@ private Person createPerson() {
source.encryptedLong = 1001L;
source.nested = new NestedWithQEFields();
source.nested.value = "Luigi time!";
+ source.ssn = "6-4-20";
return source;
}
@@ -480,6 +482,10 @@ static class Person {
rangeOptions = "{\"min\": {\"$numberLong\": \"1000\"}, \"max\": {\"$numberLong\": \"9999\"}, \"trimFactor\": 1, \"sparsity\": 1}") //
Long encryptedLong;
+ @ValueConverter(MongoEncryptionConverter.class)
+ @Encrypted(algorithm = "Unindexed") // encrypted, nothing else!
+ String ssn;
+
NestedWithQEFields nested;
public String getId() {
@@ -514,6 +520,14 @@ public void setEncryptedLong(Long encryptedLong) {
this.encryptedLong = encryptedLong;
}
+ public String getSsn() {
+ return ssn;
+ }
+
+ public void setSsn(String ssn) {
+ this.ssn = ssn;
+ }
+
@Override
public boolean equals(Object o) {
if (o == this) {
@@ -525,18 +539,20 @@ public boolean equals(Object o) {
Person person = (Person) o;
return Objects.equals(id, person.id) && Objects.equals(unencryptedValue, person.unencryptedValue)
&& Objects.equals(name, person.name) && Objects.equals(age, person.age)
- && Objects.equals(encryptedInt, person.encryptedInt) && Objects.equals(encryptedLong, person.encryptedLong);
+ && Objects.equals(encryptedInt, person.encryptedInt) && Objects.equals(encryptedLong, person.encryptedLong)
+ && Objects.equals(ssn, person.ssn);
}
@Override
public int hashCode() {
- return Objects.hash(id, unencryptedValue, name, age, encryptedInt, encryptedLong);
+ return Objects.hash(id, unencryptedValue, name, age, encryptedInt, encryptedLong, ssn);
}
@Override
public String toString() {
return "Person{" + "id='" + id + '\'' + ", unencryptedValue='" + unencryptedValue + '\'' + ", name='" + name
- + '\'' + ", age=" + age + ", encryptedInt=" + encryptedInt + ", encryptedLong=" + encryptedLong + '}';
+ + '\'' + ", age=" + age + ", encryptedInt=" + encryptedInt + ", encryptedLong=" + encryptedLong + ", ssn="
+ + ssn + '}';
}
}
diff --git a/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc b/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc
index 4c34c3831a..5a92168c2c 100644
--- a/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc
+++ b/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc
@@ -141,9 +141,10 @@ Manual Collection Setup::
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
CollectionOptions collectionOptions = CollectionOptions.encryptedCollection(options -> options
- .queryable(encrypted(string("ssn")).algorithm("Indexed"), equality().contention(0))
- .queryable(encrypted(int32("age")).algorithm("Range"), range().contention(8).min(0).max(150))
- .queryable(encrypted(int64("address.sign")).algorithm("Range"), range().contention(2).min(-10L).max(10L))
+ .queryable(encrypted(string("ssn")).algorithm("Indexed"), equality().contention(0))
+ .queryable(encrypted(int32("age")).algorithm("Range"), range().contention(8).min(0).max(150))
+ .encrypted(string("pin"))
+ .queryable(encrypted(int64("address.sign")).algorithm("Range"), range().contention(2).min(-10L).max(10L))
);
mongoTemplate.createCollection(Patient.class, collectionOptions); <1>
@@ -160,13 +161,16 @@ class Patient {
@Id String id;
- @Encrypted(algorithm = "Indexed") //
+ @Encrypted(algorithm = "Indexed")
@Queryable(queryType = "equality", contentionFactor = 0)
String ssn;
@RangeEncrypted(contentionFactor = 8, rangeOptions = "{ 'min' : 0, 'max' : 150 }")
Integer age;
+ @Encrypted(algorithm = "Unindexed")
+ String pin;
+
Address address;
}
@@ -210,6 +214,11 @@ MongoDB Collection Info::
bsonType: 'int',
queries: [ { queryType: 'range', contention: Long('8'), min: 0, max: 150 } ]
},
+ {
+ keyId: ...,
+ path: 'pin',
+ bsonType: 'string'
+ },
{
keyId: ...,
path: 'address.sign',