From 7472a3abd8cd38159618fb4c8110a70f7c58b6cc Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 27 Mar 2025 14:43:09 +0100 Subject: [PATCH] Introduce Queryable annotation --- .../core/MappingMongoJsonSchemaCreator.java | 15 +++--- .../encryption/MongoEncryptionConverter.java | 25 +++++----- .../data/mongodb/core/mapping/Encrypted.java | 12 ----- .../data/mongodb/core/mapping/Queryable.java | 49 +++++++++++++++++++ .../mongodb/core/mapping/RangeEncrypted.java | 6 ++- .../core/encryption/RangeEncryptionTests.java | 41 +++++++++++++--- 6 files changed, 109 insertions(+), 39 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java index 9e6d91b33..790fa9429 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java @@ -31,6 +31,7 @@ import org.springframework.data.mongodb.core.mapping.Encrypted; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.mapping.Queryable; import org.springframework.data.mongodb.core.mapping.RangeEncrypted; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ArrayJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty; @@ -296,7 +297,8 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator { enc = enc.keys(property.getEncryptionKeyIds()); } - if (!StringUtils.hasText(encrypted.queryType())) { + Queryable queryable = property.findAnnotation(Queryable.class); + if (queryable == null || !StringUtils.hasText(queryable.queryType())) { return enc; } @@ -304,7 +306,7 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator { @Override public String queryType() { - return encrypted.queryType(); + return queryable.queryType(); } @Override @@ -312,12 +314,11 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator { Document options = QueryCharacteristic.super.toDocument(); - RangeEncrypted rangeEncrypted = property.findAnnotation(RangeEncrypted.class); - if (rangeEncrypted != null) { - options.put("contention", rangeEncrypted.contentionFactor()); + if (queryable.contentionFactor() >= 0) { + options.put("contention", queryable.contentionFactor()); } - if (!encrypted.queryAttributes().isEmpty()) { - options.putAll(Document.parse(encrypted.queryAttributes())); + if (!queryable.queryAttributes().isEmpty()) { + options.putAll(Document.parse(queryable.queryAttributes())); } return options; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java index 7edb49963..e56621964 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java @@ -41,7 +41,7 @@ import org.springframework.data.mongodb.core.encryption.EncryptionKeyResolver; import org.springframework.data.mongodb.core.encryption.EncryptionOptions; import org.springframework.data.mongodb.core.mapping.Encrypted; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.data.mongodb.core.mapping.RangeEncrypted; +import org.springframework.data.mongodb.core.mapping.Queryable; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; @@ -179,7 +179,8 @@ public class MongoEncryptionConverter implements EncryptingConverter= 0) { - queryableEncryptionOptions = queryableEncryptionOptions - .contentionFactor(rangeEncryptedAnnotation.contentionFactor()); + if (queryableAnnotation.contentionFactor() >= 0) { + queryableEncryptionOptions = queryableEncryptionOptions.contentionFactor(queryableAnnotation.contentionFactor()); } boolean isPartOfARangeQuery = fieldNameAndQueryOperator != null; - if (isPartOfARangeQuery || !encryptedAnnotation.queryType().isEmpty()) { - queryableEncryptionOptions = queryableEncryptionOptions.queryType(encryptedAnnotation.queryType()); // should the type move to an extra annotation? - queryableEncryptionOptions = queryableEncryptionOptions.contentionFactor(1l); + if (isPartOfARangeQuery) { + queryableEncryptionOptions = queryableEncryptionOptions.queryType(queryableAnnotation.queryType()); // should the + // type move + // to an extra + // annotation? } return queryableEncryptionOptions; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Encrypted.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Encrypted.java index 4915f7b35..3e169026a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Encrypted.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Encrypted.java @@ -109,16 +109,4 @@ public @interface Encrypted { * @see org.springframework.data.mongodb.core.EncryptionAlgorithms */ String algorithm() default ""; - - /** - * @return empty {@link String} if not set. - * @since 4.5 - */ - String queryType() default ""; - - /** - * @return empty {@link String} if not set. - * @since 4.5 - */ - String queryAttributes() default ""; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java new file mode 100644 index 000000000..cc4b71ef1 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java @@ -0,0 +1,49 @@ +/* + * Copyright 2025. 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 + * + * http://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.mapping; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Christoph Strobl + * @since 4.5 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) +public @interface Queryable { + + /** + * @return empty {@link String} if not set. + * @since 4.5 + */ + String queryType() default ""; + + /** + * @return empty {@link String} if not set. + * @since 4.5 + */ + String queryAttributes() default ""; + + /** + * Set the contention factor + * + * @return the contention factor + */ + long contentionFactor() default -1; +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java index 224fcbc99..5710c081f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java @@ -29,7 +29,8 @@ import org.springframework.core.annotation.AliasFor; */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) -@Encrypted(algorithm = "Range", queryType = "range") +@Encrypted(algorithm = "Range") +@Queryable(queryType = "range") public @interface RangeEncrypted { /** @@ -37,6 +38,7 @@ public @interface RangeEncrypted { * * @return the contention factor */ + @AliasFor(annotation = Queryable.class, value = "contentionFactor") long contentionFactor() default -1; /** @@ -49,6 +51,6 @@ public @interface RangeEncrypted { * * @return the json representation of range options */ - @AliasFor(annotation = Encrypted.class, value = "queryAttributes") + @AliasFor(annotation = Queryable.class, value = "queryAttributes") String rangeOptions() default ""; } 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 d041a359b..d9e6a4f94 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 @@ -51,6 +51,7 @@ import org.springframework.data.mongodb.core.convert.MongoCustomConversions.Mong import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter; import org.springframework.data.mongodb.core.mapping.Encrypted; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.Queryable; import org.springframework.data.mongodb.core.mapping.RangeEncrypted; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.schema.MongoJsonSchema; @@ -110,12 +111,22 @@ class RangeEncryptionTests { .rangeOptions(new RangeOptions().min(new BsonInt32(0)).max(new BsonInt32(200))) .keyId(keyHolder.getEncryptionKey("encryptedInt")).queryType("range"); - EncryptOptions stringEncOptions = new EncryptOptions("Range") - .contentionFactor(8L).rangeOptions(new RangeOptions()) - .keyId(keyHolder.getEncryptionKey("name")).queryType("equality"); + /* + @Encrypted(algorithm = "Indexed") + @Queryable(queryType = "equality", contentionFactor = 0) + */ + EncryptOptions equalityEncOptions = new EncryptOptions("Indexed") + .contentionFactor(0L) + .keyId(keyHolder.getEncryptionKey("age"));; + + EncryptOptions equalityEncOptionsString = new EncryptOptions("Indexed") + .contentionFactor(0L) + .keyId(keyHolder.getEncryptionKey("name"));; Document source = new Document("_id", "id-1"); - source.put("name", clientEncryption.getClientEncryption().encrypt(new BsonString("It's a Me, Mario!"), stringEncOptions)); + + 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("_class", Person.class.getName()); @@ -143,6 +154,16 @@ class RangeEncryptionTests { assertThat(loaded).isEqualTo(source); } + @Test + void eqQueryWorksOnEncryptedField() { + + Person source = createPerson(); + template.insert(source); + + Person loaded = template.query(Person.class).matching(where("age").is(source.age)).firstValue(); + assertThat(loaded).isEqualTo(source); + } + @Test void canRangeMatchRangeEncryptedField() { @@ -200,7 +221,8 @@ class RangeEncryptionTests { private Person createPerson() { Person source = new Person(); source.id = "id-1"; - source.name = "it'se me mario!"; + source.name = "it's a me mario!"; + source.age = 42; source.encryptedInt = 101; source.encryptedLong = 1001L; return source; @@ -358,10 +380,17 @@ class RangeEncryptionTests { static class Person { String id; + @ValueConverter(MongoEncryptionConverter.class) - @Encrypted(algorithm = "Range", queryType = "equality") + @Encrypted(algorithm = "Indexed") + @Queryable(queryType = "equality", contentionFactor = 0) String name; + @ValueConverter(MongoEncryptionConverter.class) + @Encrypted(algorithm = "Indexed") + @Queryable(queryType = "equality", contentionFactor = 0) + Integer age; + @ValueConverter(MongoEncryptionConverter.class) @RangeEncrypted(contentionFactor = 0L, rangeOptions = "{\"min\": 0, \"max\": 200, \"trimFactor\": 1, \"sparsity\": 1}") //