Browse Source

needs some caching to avoid too many lookups

issue/4185-light
Christoph Strobl 9 months ago
parent
commit
975555b1cf
No known key found for this signature in database
GPG Key ID: E6054036D0C37A4B
  1. 15
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java
  2. 92
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java
  3. 59
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java
  4. 81
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java

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

@ -15,10 +15,9 @@ @@ -15,10 +15,9 @@
*/
package org.springframework.data.mongodb.core.convert.encryption;
import static java.util.Arrays.*;
import static java.util.Collections.*;
import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*;
import static org.springframework.data.mongodb.core.encryption.EncryptionOptions.*;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.springframework.data.mongodb.core.encryption.EncryptionOptions.QueryableEncryptionOptions;
import java.util.Collection;
import java.util.LinkedHashMap;
@ -188,7 +187,7 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj @@ -188,7 +187,7 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
QueryableEncryptionOptions queryableEncryptionOptions = QueryableEncryptionOptions.none();
String rangeOptions = rangeEncryptedAnnotation.rangeOptions();
if (!rangeOptions.isEmpty()) {
queryableEncryptionOptions = queryableEncryptionOptions.rangeOptions(Document.parse(rangeOptions));
queryableEncryptionOptions = queryableEncryptionOptions.attributes(Document.parse(rangeOptions));
}
if (rangeEncryptedAnnotation.contentionFactor() >= 0) {
@ -240,9 +239,9 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj @@ -240,9 +239,9 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
/**
* Encrypts a range query expression.
*
* <p>The mongodb-crypt {@code encryptExpression} has strict formatting requirements so this method
* ensures these requirements are met and then picks out and returns just the value for use with a range query.
* <p>
* The mongodb-crypt {@code encryptExpression} has strict formatting requirements so this method ensures these
* requirements are met and then picks out and returns just the value for use with a range query.
*
* @param fieldNameAndQueryOperator field name and query operator
* @param value the value of the expression to be encrypted

92
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java

@ -15,20 +15,20 @@ @@ -15,20 +15,20 @@
*/
package org.springframework.data.mongodb.core.encryption;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import com.mongodb.client.model.vault.RangeOptions;
import org.bson.Document;
import org.springframework.data.mongodb.core.FindAndReplaceOptions;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.data.util.Optionals;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import com.mongodb.client.model.vault.RangeOptions;
/**
* Options, like the {@link #algorithm()}, to apply when encrypting values.
* Options used to provide additional information when {@link Encryption encrypting} values. like the
* {@link #algorithm()} to be used.
*
* @author Christoph Strobl
* @author Ross Lawley
@ -45,6 +45,7 @@ public class EncryptionOptions { @@ -45,6 +45,7 @@ public class EncryptionOptions {
}
public EncryptionOptions(String algorithm, EncryptionKey key, QueryableEncryptionOptions queryableEncryptionOptions) {
Assert.hasText(algorithm, "Algorithm must not be empty");
Assert.notNull(key, "EncryptionKey must not be empty");
Assert.notNull(key, "QueryableEncryptionOptions must not be empty");
@ -107,20 +108,23 @@ public class EncryptionOptions { @@ -107,20 +108,23 @@ public class EncryptionOptions {
* Options, like the {@link #getQueryType()}, to apply when encrypting queryable values.
*
* @author Ross Lawley
* @author Christoph Strobl
* @since 4.5
*/
public static class QueryableEncryptionOptions {
private static final QueryableEncryptionOptions NONE = new QueryableEncryptionOptions(null, null, null);
public static final QueryableEncryptionOptions NONE = new QueryableEncryptionOptions(null, null, Map.of());
private final @Nullable String queryType;
private final @Nullable Long contentionFactor;
private final @Nullable Document rangeOptions;
private final Map<String, Object> attributes;
private QueryableEncryptionOptions(@Nullable String queryType, @Nullable Long contentionFactor,
@Nullable Document rangeOptions) {
Map<String, Object> attributes) {
this.queryType = queryType;
this.contentionFactor = contentionFactor;
this.rangeOptions = rangeOptions;
this.attributes = attributes;
}
/**
@ -139,7 +143,7 @@ public class EncryptionOptions { @@ -139,7 +143,7 @@ public class EncryptionOptions {
* @return new instance of {@link QueryableEncryptionOptions}.
*/
public QueryableEncryptionOptions queryType(@Nullable String queryType) {
return new QueryableEncryptionOptions(queryType, contentionFactor, rangeOptions);
return new QueryableEncryptionOptions(queryType, contentionFactor, attributes);
}
/**
@ -149,89 +153,57 @@ public class EncryptionOptions { @@ -149,89 +153,57 @@ public class EncryptionOptions {
* @return new instance of {@link QueryableEncryptionOptions}.
*/
public QueryableEncryptionOptions contentionFactor(@Nullable Long contentionFactor) {
return new QueryableEncryptionOptions(queryType, contentionFactor, rangeOptions);
return new QueryableEncryptionOptions(queryType, contentionFactor, attributes);
}
/**
* Define the {@code rangeOptions} to be used for queryable document encryption.
*
* @param rangeOptions can be {@literal null}.
* @param attributes can be {@literal null}.
* @return new instance of {@link QueryableEncryptionOptions}.
*/
public QueryableEncryptionOptions rangeOptions(@Nullable Document rangeOptions) {
return new QueryableEncryptionOptions(queryType, contentionFactor, rangeOptions);
public QueryableEncryptionOptions attributes(Map<String, Object> attributes) {
return new QueryableEncryptionOptions(queryType, contentionFactor, attributes);
}
/**
* Get the {@code queryType} to apply.
*
* @return {@link Optional#empty()} if not set.
* @return {@literal null} if not set.
*/
public Optional<String> getQueryType() {
return Optional.ofNullable(queryType);
public @Nullable String getQueryType() {
return queryType;
}
/**
* Get the {@code contentionFactor} to apply.
*
* @return {@link Optional#empty()} if not set.
* @return {@literal null} if not set.
*/
public Optional<Long> getContentionFactor() {
return Optional.ofNullable(contentionFactor);
public @Nullable Long getContentionFactor() {
return contentionFactor;
}
/**
* Get the {@code rangeOptions} to apply.
*
* @return {@link Optional#empty()} if not set.
* @return never {@literal null}.
*/
public Optional<RangeOptions> getRangeOptions() {
if (rangeOptions == null) {
return Optional.empty();
}
RangeOptions encryptionRangeOptions = new RangeOptions();
if (rangeOptions.containsKey("min")) {
encryptionRangeOptions.min(BsonUtils.simpleToBsonValue(rangeOptions.get("min")));
}
if (rangeOptions.containsKey("max")) {
encryptionRangeOptions.max(BsonUtils.simpleToBsonValue(rangeOptions.get("max")));
}
if (rangeOptions.containsKey("trimFactor")) {
Object trimFactor = rangeOptions.get("trimFactor");
Assert.isInstanceOf(Integer.class, trimFactor, () -> String
.format("Expected to find a %s but it turned out to be %s.", Integer.class, trimFactor.getClass()));
encryptionRangeOptions.trimFactor((Integer) trimFactor);
}
if (rangeOptions.containsKey("sparsity")) {
Object sparsity = rangeOptions.get("sparsity");
Assert.isInstanceOf(Number.class, sparsity,
() -> String.format("Expected to find a %s but it turned out to be %s.", Long.class, sparsity.getClass()));
encryptionRangeOptions.sparsity(((Number) sparsity).longValue());
}
if (rangeOptions.containsKey("precision")) {
Object precision = rangeOptions.get("precision");
Assert.isInstanceOf(Number.class, precision, () -> String
.format("Expected to find a %s but it turned out to be %s.", Integer.class, precision.getClass()));
encryptionRangeOptions.precision(((Number) precision).intValue());
}
return Optional.of(encryptionRangeOptions);
public Map<String, Object> getAttributes() {
return Map.copyOf(attributes);
}
/**
* @return {@literal true} if no arguments set.
*/
boolean isEmpty() {
return !Optionals.isAnyPresent(getQueryType(), getContentionFactor(), getRangeOptions());
return getQueryType() == null && getContentionFactor() == null && getAttributes().isEmpty();
}
@Override
public String toString() {
return "QueryableEncryptionOptions{" + "queryType='" + queryType + '\'' + ", contentionFactor=" + contentionFactor
+ ", rangeOptions=" + rangeOptions + '}';
+ ", attributes=" + attributes + '}';
}
@Override
@ -251,12 +223,12 @@ public class EncryptionOptions { @@ -251,12 +223,12 @@ public class EncryptionOptions {
if (!ObjectUtils.nullSafeEquals(contentionFactor, that.contentionFactor)) {
return false;
}
return ObjectUtils.nullSafeEquals(rangeOptions, that.rangeOptions);
return ObjectUtils.nullSafeEquals(attributes, that.attributes);
}
@Override
public int hashCode() {
return Objects.hash(queryType, contentionFactor, rangeOptions);
return Objects.hash(queryType, contentionFactor, attributes);
}
}
}

59
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java

@ -15,15 +15,19 @@ @@ -15,15 +15,19 @@
*/
package org.springframework.data.mongodb.core.encryption;
import java.util.Map;
import java.util.function.Supplier;
import org.bson.BsonBinary;
import org.bson.BsonDocument;
import org.bson.BsonValue;
import org.springframework.data.mongodb.core.encryption.EncryptionKey.Type;
import org.springframework.data.mongodb.core.encryption.EncryptionOptions.QueryableEncryptionOptions;
import org.springframework.data.mongodb.util.BsonUtils;
import org.springframework.util.Assert;
import com.mongodb.client.model.vault.EncryptOptions;
import com.mongodb.client.model.vault.RangeOptions;
import com.mongodb.client.vault.ClientEncryption;
/**
@ -74,6 +78,7 @@ public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary> @@ -74,6 +78,7 @@ public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary>
}
private EncryptOptions createEncryptOptions(EncryptionOptions options) {
EncryptOptions encryptOptions = new EncryptOptions(options.algorithm());
if (Type.ALT.equals(options.key().type())) {
@ -82,10 +87,58 @@ public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary> @@ -82,10 +87,58 @@ public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary>
encryptOptions = encryptOptions.keyId((BsonBinary) options.key().value());
}
options.queryableEncryptionOptions().getQueryType().map(encryptOptions::queryType);
options.queryableEncryptionOptions().getContentionFactor().map(encryptOptions::contentionFactor);
options.queryableEncryptionOptions().getRangeOptions().map(encryptOptions::rangeOptions);
if (options.queryableEncryptionOptions().isEmpty()) {
return encryptOptions;
}
QueryableEncryptionOptions qeOptions = options.queryableEncryptionOptions();
if (qeOptions.getQueryType() != null) {
encryptOptions.queryType(qeOptions.getQueryType());
}
if (qeOptions.getContentionFactor() != null) {
encryptOptions.contentionFactor(qeOptions.getContentionFactor());
}
if (!qeOptions.getAttributes().isEmpty()) {
encryptOptions.rangeOptions(rangeOptions(qeOptions.getAttributes()));
}
return encryptOptions;
}
protected RangeOptions rangeOptions(Map<String, Object> attributes) {
RangeOptions encryptionRangeOptions = new RangeOptions();
if (attributes.isEmpty()) {
return encryptionRangeOptions;
}
if (attributes.containsKey("min")) {
encryptionRangeOptions.min(BsonUtils.simpleToBsonValue(attributes.get("min")));
}
if (attributes.containsKey("max")) {
encryptionRangeOptions.max(BsonUtils.simpleToBsonValue(attributes.get("max")));
}
if (attributes.containsKey("trimFactor")) {
Object trimFactor = attributes.get("trimFactor");
Assert.isInstanceOf(Integer.class, trimFactor, () -> String
.format("Expected to find a %s but it turned out to be %s.", Integer.class, trimFactor.getClass()));
encryptionRangeOptions.trimFactor((Integer) trimFactor);
}
if (attributes.containsKey("sparsity")) {
Object sparsity = attributes.get("sparsity");
Assert.isInstanceOf(Number.class, sparsity,
() -> String.format("Expected to find a %s but it turned out to be %s.", Long.class, sparsity.getClass()));
encryptionRangeOptions.sparsity(((Number) sparsity).longValue());
}
if (attributes.containsKey("precision")) {
Object precision = attributes.get("precision");
Assert.isInstanceOf(Number.class, precision, () -> String
.format("Expected to find a %s but it turned out to be %s.", Integer.class, precision.getClass()));
encryptionRangeOptions.precision(((Number) precision).intValue());
}
return encryptionRangeOptions;
}
}

81
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java

@ -16,6 +16,8 @@ @@ -16,6 +16,8 @@
package org.springframework.data.mongodb.core.encryption;
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.List;
@ -38,6 +40,7 @@ import org.springframework.beans.factory.annotation.Autowired; @@ -38,6 +40,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.data.convert.PropertyValueConverterFactory;
import org.springframework.data.convert.ValueConverter;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.core.CollectionOptions;
import org.springframework.data.mongodb.core.CollectionOptions.EncryptedFieldsOptions;
@ -47,6 +50,7 @@ import org.springframework.data.mongodb.core.convert.MongoCustomConversions.Mong @@ -47,6 +50,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.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.RangeEncrypted;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion;
import org.springframework.data.mongodb.test.util.EnableIfReplicaSetAvailable;
@ -123,6 +127,74 @@ class RangeEncryptionTests { @@ -123,6 +127,74 @@ class RangeEncryptionTests {
assertThat(result).containsEntry("encryptedInt", 101);
}
@Test
void canLesserThanEqualMatchRangeEncryptedField() {
Person source = createPerson();
template.insert(source);
Person loaded = template.query(Person.class).matching(where("encryptedInt").lte(source.encryptedInt)).firstValue();
assertThat(loaded).isEqualTo(source);
}
@Test
void canRangeMatchRangeEncryptedField() {
Person source = createPerson();
template.insert(source);
Query q = Query.query(where("encryptedLong").lte(1001L).gte(1001L));
q.fields().exclude("__safeContent__");
Person loaded = template.query(Person.class).matching(q).firstValue();
assertThat(loaded).isEqualTo(source);
}
@Test
void canUpdateRangeEncryptedField() {
Person source = createPerson();
template.insert(source);
source.encryptedInt = 123;
source.encryptedLong = 9999L;
template.save(source);
Person loaded = template.query(Person.class).matching(where("id").is(source.id)).firstValue();
assertThat(loaded).isEqualTo(source);
}
@Test
void errorsWhenUsingNonRangeOperatorEqOnRangeEncryptedField() {
Person source = createPerson();
template.insert(source);
assertThatThrownBy(
() -> template.query(Person.class).matching(where("encryptedInt").is(source.encryptedInt)).firstValue())
.isInstanceOf(AssertionError.class)
.hasMessageStartingWith("Not a valid range query. Querying a range encrypted field but "
+ "the query operator '$eq' for field path 'encryptedInt' is not a range query.");
}
@Test
void errorsWhenUsingNonRangeOperatorInOnRangeEncryptedField() {
Person source = createPerson();
template.insert(source);
assertThatThrownBy(
() -> template.query(Person.class).matching(where("encryptedLong").in(1001L, 9999L)).firstValue())
.isInstanceOf(AssertionError.class)
.hasMessageStartingWith("Not a valid range query. Querying a range encrypted field but "
+ "the query operator '$in' for field path 'encryptedLong' is not a range query.");
}
private Person createPerson() {
Person source = new Person();
source.id = "id-1";
source.name = "it'se me mario!";
source.encryptedInt = 101;
source.encryptedLong = 1001L;
return source;
}
protected static class EncryptionConfig extends AbstractMongoClientConfiguration {
private static final String LOCAL_KMS_PROVIDER = "local";
@ -277,10 +349,15 @@ class RangeEncryptionTests { @@ -277,10 +349,15 @@ class RangeEncryptionTests {
String id;
String name;
@ValueConverter(MongoEncryptionConverter.class)
@RangeEncrypted(contentionFactor = 0L,
rangeOptions = "{\"min\": 0, \"max\": 200, \"trimFactor\": 1, \"sparsity\": 1}") Integer encryptedInt;
rangeOptions = "{\"min\": 0, \"max\": 200, \"trimFactor\": 1, \"sparsity\": 1}") //
Integer encryptedInt;
@ValueConverter(MongoEncryptionConverter.class)
@RangeEncrypted(contentionFactor = 0L,
rangeOptions = "{\"min\": {\"$numberLong\": \"1000\"}, \"max\": {\"$numberLong\": \"9999\"}, \"trimFactor\": 1, \"sparsity\": 1}") Long encryptedLong;
rangeOptions = "{\"min\": {\"$numberLong\": \"1000\"}, \"max\": {\"$numberLong\": \"9999\"}, \"trimFactor\": 1, \"sparsity\": 1}") //
Long encryptedLong;
public String getId() {
return this.id;

Loading…
Cancel
Save