Skip to content

Add support for non-queryable encrypted fields in CollectionOptions #4992

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>5.0.0-SNAPSHOT</version>
<version>5.0.x-GH-4988-SNAPSHOT</version>
<packaging>pom</packaging>

<name>Spring Data MongoDB</name>
Expand Down
2 changes: 1 addition & 1 deletion spring-data-mongodb-distribution/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>5.0.0-SNAPSHOT</version>
<version>5.0.x-GH-4988-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 1 addition & 1 deletion spring-data-mongodb/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>5.0.0-SNAPSHOT</version>
<version>5.0.x-GH-4988-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -681,17 +682,16 @@ public static class EncryptedFieldsOptions {
private static final EncryptedFieldsOptions NONE = new EncryptedFieldsOptions();

private final @Nullable MongoJsonSchema schema;
private final List<QueryableJsonSchemaProperty> queryableProperties;
private final List<JsonSchemaProperty> properties;

EncryptedFieldsOptions() {
this(null, List.of());
}

private EncryptedFieldsOptions(@Nullable MongoJsonSchema schema,
List<QueryableJsonSchemaProperty> queryableProperties) {
private EncryptedFieldsOptions(@Nullable MongoJsonSchema schema, List<JsonSchemaProperty> queryableProperties) {

this.schema = schema;
this.queryableProperties = queryableProperties;
this.properties = queryableProperties;
}

/**
Expand All @@ -711,7 +711,7 @@ public static EncryptedFieldsOptions fromSchema(MongoJsonSchema schema) {
/**
* @return new instance of {@link EncryptedFieldsOptions}.
*/
public static EncryptedFieldsOptions fromProperties(List<QueryableJsonSchemaProperty> properties) {
public static EncryptedFieldsOptions fromProperties(List<JsonSchemaProperty> properties) {
return new EncryptedFieldsOptions(null, List.copyOf(properties));
}

Expand All @@ -731,13 +731,50 @@ public static EncryptedFieldsOptions fromProperties(List<QueryableJsonSchemaProp
@CheckReturnValue
public EncryptedFieldsOptions queryable(JsonSchemaProperty property, QueryCharacteristic... characteristics) {

List<QueryableJsonSchemaProperty> targetPropertyList = new ArrayList<>(queryableProperties.size() + 1);
targetPropertyList.addAll(queryableProperties);
List<JsonSchemaProperty> 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<JsonSchemaProperty> 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());
}
Expand All @@ -756,20 +793,20 @@ private List<Document> selectPaths() {

private List<Document> fromProperties() {

if (queryableProperties.isEmpty()) {
if (properties.isEmpty()) {
return List.of();
}

List<Document> converted = new ArrayList<>(queryableProperties.size());
for (QueryableJsonSchemaProperty property : queryableProperties) {
List<Document> converted = new ArrayList<>(properties.size());
for (JsonSchemaProperty property : properties) {

Document field = new Document("path", property.getIdentifier());

if (!property.getTypes().isEmpty()) {
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) {
Expand All @@ -779,11 +816,21 @@ private List<Document> 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);
}
Expand Down Expand Up @@ -812,7 +859,9 @@ private List<Document> 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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -94,12 +96,16 @@ public void createsCollectionWithEncryptedFieldsCorrectly(CollectionOptions coll
assertThat(encryptedFields).containsKey("fields");

List<Document> 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' }}")));
Expand All @@ -109,16 +115,19 @@ private static Stream<Arguments> 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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -125,18 +123,19 @@ 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");

source.put("name",
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));
Expand All @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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) {
Expand All @@ -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 + '}';
}
}

Expand Down
Loading