Skip to content

Commit f8d6138

Browse files
committed
Adding queryable encryption range support
Supports range style queries for encrypted fields
1 parent 14985a9 commit f8d6138

15 files changed

+1148
-44
lines changed

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

+42-13
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Optional;
2020
import java.util.function.Function;
2121

22+
import org.bson.conversions.Bson;
2223
import org.springframework.data.mongodb.core.mapping.Field;
2324
import org.springframework.data.mongodb.core.query.Collation;
2425
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
@@ -51,10 +52,11 @@ public class CollectionOptions {
5152
private ValidationOptions validationOptions;
5253
private @Nullable TimeSeriesOptions timeSeriesOptions;
5354
private @Nullable CollectionChangeStreamOptions changeStreamOptions;
55+
private @Nullable Bson encryptedFields;
5456

5557
private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nullable Boolean capped,
5658
@Nullable Collation collation, ValidationOptions validationOptions, @Nullable TimeSeriesOptions timeSeriesOptions,
57-
@Nullable CollectionChangeStreamOptions changeStreamOptions) {
59+
@Nullable CollectionChangeStreamOptions changeStreamOptions, @Nullable Bson encryptedFields) {
5860

5961
this.maxDocuments = maxDocuments;
6062
this.size = size;
@@ -63,6 +65,7 @@ private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nul
6365
this.validationOptions = validationOptions;
6466
this.timeSeriesOptions = timeSeriesOptions;
6567
this.changeStreamOptions = changeStreamOptions;
68+
this.encryptedFields = encryptedFields;
6669
}
6770

6871
/**
@@ -76,7 +79,7 @@ public static CollectionOptions just(Collation collation) {
7679

7780
Assert.notNull(collation, "Collation must not be null");
7881

79-
return new CollectionOptions(null, null, null, collation, ValidationOptions.none(), null, null);
82+
return new CollectionOptions(null, null, null, collation, ValidationOptions.none(), null, null, null);
8083
}
8184

8285
/**
@@ -86,7 +89,7 @@ public static CollectionOptions just(Collation collation) {
8689
* @since 2.0
8790
*/
8891
public static CollectionOptions empty() {
89-
return new CollectionOptions(null, null, null, null, ValidationOptions.none(), null, null);
92+
return new CollectionOptions(null, null, null, null, ValidationOptions.none(), null, null, null);
9093
}
9194

9295
/**
@@ -136,7 +139,7 @@ public static CollectionOptions emitChangedRevisions() {
136139
*/
137140
public CollectionOptions capped() {
138141
return new CollectionOptions(size, maxDocuments, true, collation, validationOptions, timeSeriesOptions,
139-
changeStreamOptions);
142+
changeStreamOptions, encryptedFields);
140143
}
141144

142145
/**
@@ -148,7 +151,7 @@ public CollectionOptions capped() {
148151
*/
149152
public CollectionOptions maxDocuments(long maxDocuments) {
150153
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
151-
changeStreamOptions);
154+
changeStreamOptions, encryptedFields);
152155
}
153156

154157
/**
@@ -160,7 +163,7 @@ public CollectionOptions maxDocuments(long maxDocuments) {
160163
*/
161164
public CollectionOptions size(long size) {
162165
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
163-
changeStreamOptions);
166+
changeStreamOptions, encryptedFields);
164167
}
165168

166169
/**
@@ -172,7 +175,7 @@ public CollectionOptions size(long size) {
172175
*/
173176
public CollectionOptions collation(@Nullable Collation collation) {
174177
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
175-
changeStreamOptions);
178+
changeStreamOptions, encryptedFields);
176179
}
177180

178181
/**
@@ -293,7 +296,7 @@ public CollectionOptions validation(ValidationOptions validationOptions) {
293296

294297
Assert.notNull(validationOptions, "ValidationOptions must not be null");
295298
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
296-
changeStreamOptions);
299+
changeStreamOptions, encryptedFields);
297300
}
298301

299302
/**
@@ -307,7 +310,7 @@ public CollectionOptions timeSeries(TimeSeriesOptions timeSeriesOptions) {
307310

308311
Assert.notNull(timeSeriesOptions, "TimeSeriesOptions must not be null");
309312
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
310-
changeStreamOptions);
313+
changeStreamOptions, encryptedFields);
311314
}
312315

313316
/**
@@ -321,7 +324,19 @@ public CollectionOptions changeStream(CollectionChangeStreamOptions changeStream
321324

322325
Assert.notNull(changeStreamOptions, "ChangeStreamOptions must not be null");
323326
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
324-
changeStreamOptions);
327+
changeStreamOptions, encryptedFields);
328+
}
329+
330+
/**
331+
* Create new {@link CollectionOptions} with the given {@code encryptedFields}.
332+
*
333+
* @param encryptedFields can be null
334+
* @return new instance of {@link CollectionOptions}.
335+
* @since QERange
336+
*/
337+
public CollectionOptions encryptedFields(@Nullable Bson encryptedFields) {
338+
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
339+
changeStreamOptions, encryptedFields);
325340
}
326341

327342
/**
@@ -392,12 +407,22 @@ public Optional<CollectionChangeStreamOptions> getChangeStreamOptions() {
392407
return Optional.ofNullable(changeStreamOptions);
393408
}
394409

410+
/**
411+
* Get the {@code encryptedFields} if available.
412+
*
413+
* @return {@link Optional#empty()} if not specified.
414+
* @since QERange
415+
*/
416+
public Optional<Bson> getEncryptedFields() {
417+
return Optional.ofNullable(encryptedFields);
418+
}
419+
395420
@Override
396421
public String toString() {
397422
return "CollectionOptions{" + "maxDocuments=" + maxDocuments + ", size=" + size + ", capped=" + capped
398423
+ ", collation=" + collation + ", validationOptions=" + validationOptions + ", timeSeriesOptions="
399-
+ timeSeriesOptions + ", changeStreamOptions=" + changeStreamOptions + ", disableValidation="
400-
+ disableValidation() + ", strictValidation=" + strictValidation() + ", moderateValidation="
424+
+ timeSeriesOptions + ", changeStreamOptions=" + changeStreamOptions + ", encryptedFields=" + encryptedFields
425+
+ ", disableValidation=" + disableValidation() + ", strictValidation=" + strictValidation() + ", moderateValidation="
401426
+ moderateValidation() + ", warnOnValidationError=" + warnOnValidationError() + ", failOnValidationError="
402427
+ failOnValidationError() + '}';
403428
}
@@ -431,7 +456,10 @@ public boolean equals(@Nullable Object o) {
431456
if (!ObjectUtils.nullSafeEquals(timeSeriesOptions, that.timeSeriesOptions)) {
432457
return false;
433458
}
434-
return ObjectUtils.nullSafeEquals(changeStreamOptions, that.changeStreamOptions);
459+
if (!ObjectUtils.nullSafeEquals(changeStreamOptions, that.changeStreamOptions)) {
460+
return false;
461+
}
462+
return ObjectUtils.nullSafeEquals(encryptedFields, that.encryptedFields);
435463
}
436464

437465
@Override
@@ -443,6 +471,7 @@ public int hashCode() {
443471
result = 31 * result + ObjectUtils.nullSafeHashCode(validationOptions);
444472
result = 31 * result + ObjectUtils.nullSafeHashCode(timeSeriesOptions);
445473
result = 31 * result + ObjectUtils.nullSafeHashCode(changeStreamOptions);
474+
result = 31 * result + ObjectUtils.nullSafeHashCode(encryptedFields);
446475
return result;
447476
}
448477

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

+2
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,6 @@ public final class EncryptionAlgorithms {
2626
public static final String AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic";
2727
public static final String AEAD_AES_256_CBC_HMAC_SHA_512_Random = "AEAD_AES_256_CBC_HMAC_SHA_512-Random";
2828

29+
public static final String RANGE = "Range";
30+
2931
}

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

+1
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ public CreateCollectionOptions convertToCreateCollectionOptions(@Nullable Collec
378378
collectionOptions.getChangeStreamOptions().ifPresent(it -> result
379379
.changeStreamPreAndPostImagesOptions(new ChangeStreamPreAndPostImagesOptions(it.getPreAndPostImages())));
380380

381+
collectionOptions.getEncryptedFields().ifPresent(result::encryptedFields);
381382
return result;
382383
}
383384

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

+9-7
Original file line numberDiff line numberDiff line change
@@ -2172,8 +2172,9 @@ protected <O> AggregationResults<O> doAggregate(Aggregation aggregation, String
21722172

21732173
List<Document> pipeline = aggregationUtil.createPipeline(aggregation, context);
21742174

2175-
if (LOGGER.isDebugEnabled()) {
2176-
LOGGER.debug(
2175+
// TODO revert to DEBUG
2176+
if (LOGGER.isErrorEnabled()) {
2177+
LOGGER.error(
21772178
String.format("Executing aggregation: %s in collection %s", serializeToJsonSafely(pipeline), collectionName));
21782179
}
21792180

@@ -2594,10 +2595,10 @@ protected <S, T> List<T> doFind(String collectionName,
25942595
Document mappedFields = queryContext.getMappedFields(entity, EntityProjection.nonProjecting(entityClass));
25952596
Document mappedQuery = queryContext.getMappedQuery(entity);
25962597

2597-
if (LOGGER.isDebugEnabled()) {
2598-
2598+
// TODO revert to DEBUG
2599+
if (LOGGER.isErrorEnabled()) {
25992600
Document mappedSort = getMappedSortObject(query, entityClass);
2600-
LOGGER.debug(String.format("find using query: %s fields: %s sort: %s for class: %s in collection: %s",
2601+
LOGGER.error(String.format("find using query: %s fields: %s sort: %s for class: %s in collection: %s",
26012602
serializeToJsonSafely(mappedQuery), mappedFields, serializeToJsonSafely(mappedSort), entityClass,
26022603
collectionName));
26032604
}
@@ -2623,8 +2624,9 @@ <S, T> List<T> doFind(CollectionPreparer<MongoCollection<Document>> collectionPr
26232624
Document mappedQuery = queryContext.getMappedQuery(entity);
26242625
Document mappedSort = getMappedSortObject(query, sourceClass);
26252626

2626-
if (LOGGER.isDebugEnabled()) {
2627-
LOGGER.debug(String.format("find using query: %s fields: %s sort: %s for class: %s in collection: %s",
2627+
// TODO revert to DEBUG
2628+
if (LOGGER.isErrorEnabled()) {
2629+
LOGGER.error(String.format("find using query: %s fields: %s sort: %s for class: %s in collection: %s",
26282630
serializeToJsonSafely(mappedQuery), mappedFields, serializeToJsonSafely(mappedSort), sourceClass,
26292631
collectionName));
26302632
}

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

+20-2
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,37 @@
3333
public class MongoConversionContext implements ValueConversionContext<MongoPersistentProperty> {
3434

3535
private final PropertyValueProvider<MongoPersistentProperty> accessor; // TODO: generics
36-
private final @Nullable MongoPersistentProperty persistentProperty;
3736
private final MongoConverter mongoConverter;
3837

38+
@Nullable private final MongoPersistentProperty persistentProperty;
3939
@Nullable private final SpELContext spELContext;
40+
@Nullable private final String queryFieldPath;
4041

4142
public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
4243
@Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) {
43-
this(accessor, persistentProperty, mongoConverter, null);
44+
this(accessor, mongoConverter, persistentProperty, null);
4445
}
4546

4647
public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
4748
@Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter,
4849
@Nullable SpELContext spELContext) {
50+
this(accessor, mongoConverter, persistentProperty, spELContext, null);
51+
}
52+
53+
public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor, MongoConverter mongoConverter,
54+
@Nullable MongoPersistentProperty persistentProperty, @Nullable String queryFieldPath) {
55+
this(accessor, mongoConverter, persistentProperty, null, queryFieldPath);
56+
}
57+
58+
public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor, MongoConverter mongoConverter,
59+
@Nullable MongoPersistentProperty persistentProperty, @Nullable SpELContext spELContext,
60+
@Nullable String queryFieldPath) {
4961

5062
this.accessor = accessor;
5163
this.persistentProperty = persistentProperty;
5264
this.mongoConverter = mongoConverter;
5365
this.spELContext = spELContext;
66+
this.queryFieldPath = queryFieldPath;
5467
}
5568

5669
@Override
@@ -84,4 +97,9 @@ public <T> T read(@Nullable Object value, TypeInformation<T> target) {
8497
public SpELContext getSpELContext() {
8598
return spELContext;
8699
}
100+
101+
@Nullable
102+
public String getQueryFieldPath() {
103+
return queryFieldPath;
104+
}
87105
}

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

+27-6
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext;
6060
import org.springframework.data.mongodb.core.convert.MappingMongoConverter.NestedDocument;
6161
import org.springframework.data.mongodb.core.mapping.FieldName;
62+
import org.springframework.data.mongodb.core.mapping.MongoField;
6263
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
6364
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
6465
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty.PropertyToFieldNameConverter;
@@ -356,9 +357,10 @@ protected Entry<String, Object> getMappedObjectForField(Field field, Object rawV
356357
return createMapEntry(key, getMappedObject(mongoExpression.toDocument(), field.getEntity()));
357358
}
358359

359-
if (isNestedKeyword(rawValue) && !field.isIdField()) {
360+
if (isNestedKeyword(rawValue)) {
360361
Keyword keyword = new Keyword((Document) rawValue);
361-
value = getMappedKeyword(field, keyword);
362+
field = field.with(keyword.getKey());
363+
value = field.isIdField() ? getMappedValue(field, rawValue) : getMappedKeyword(field, keyword);
362364
} else {
363365
value = getMappedValue(field, rawValue);
364366
}
@@ -455,11 +457,20 @@ protected Document getMappedKeyword(Field property, Keyword keyword) {
455457
@Nullable
456458
@SuppressWarnings("unchecked")
457459
protected Object getMappedValue(Field documentField, Object sourceValue) {
458-
459460
Object value = applyFieldTargetTypeHintToValue(documentField, sourceValue);
460461

461-
if (documentField.getProperty() != null
462-
&& converter.getCustomConversions().hasValueConverter(documentField.getProperty())) {
462+
MongoPersistentProperty property = documentField.getProperty();
463+
464+
String queryPath = property != null && !property.getFieldName().equals(documentField.name)
465+
? property.getFieldName() + "." + documentField.name
466+
: documentField.name;
467+
468+
// TODO add flattened path to convert value and remove logging
469+
if (LOGGER.isErrorEnabled()) {
470+
LOGGER.error(" >-|-> " + queryPath);
471+
}
472+
473+
if (property != null && converter.getCustomConversions().hasValueConverter(documentField.getProperty())) {
463474

464475
PropertyValueConverter<Object, Object, ValueConversionContext<MongoPersistentProperty>> valueConverter = converter
465476
.getCustomConversions().getPropertyValueConversions().getValueConverter(documentField.getProperty());
@@ -668,8 +679,18 @@ private Object convertValue(Field documentField, Object sourceValue, Object valu
668679
PropertyValueConverter<Object, Object, ValueConversionContext<MongoPersistentProperty>> valueConverter) {
669680

670681
MongoPersistentProperty property = documentField.getProperty();
682+
683+
String queryPath = property != null && !property.getFieldName().equals(documentField.name)
684+
? property.getFieldName() + "." + documentField.name
685+
: documentField.name;
686+
687+
// TODO add flattened path to convert value and remove logging
688+
if (LOGGER.isErrorEnabled()) {
689+
LOGGER.error(" >--> " + queryPath);
690+
}
691+
671692
MongoConversionContext conversionContext = new MongoConversionContext(NoPropertyPropertyValueProvider.INSTANCE,
672-
property, converter);
693+
converter, property, queryPath);
673694

674695
/* might be an $in clause with multiple entries */
675696
if (property != null && !property.isCollectionLike() && sourceValue instanceof Collection<?> collection) {

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

+6
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,10 @@ public <T> T read(@Nullable Object value, TypeInformation<T> target) {
6666
public <T> T write(@Nullable Object value, TypeInformation<T> target) {
6767
return conversionContext.write(value, target);
6868
}
69+
70+
// TODO QE - add to interface
71+
@Nullable
72+
public String getQueryFieldPath() {
73+
return conversionContext.getQueryFieldPath();
74+
}
6975
}

0 commit comments

Comments
 (0)