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. 17
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java
  2. 94
      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

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

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

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

@ -15,21 +15,21 @@
*/ */
package org.springframework.data.mongodb.core.encryption; package org.springframework.data.mongodb.core.encryption;
import java.util.Map;
import java.util.Objects; 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.mongodb.util.BsonUtils;
import org.springframework.data.util.Optionals;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils; 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 Christoph Strobl
* @author Ross Lawley * @author Ross Lawley
* @since 4.1 * @since 4.1
@ -45,6 +45,7 @@ public class EncryptionOptions {
} }
public EncryptionOptions(String algorithm, EncryptionKey key, QueryableEncryptionOptions queryableEncryptionOptions) { public EncryptionOptions(String algorithm, EncryptionKey key, QueryableEncryptionOptions queryableEncryptionOptions) {
Assert.hasText(algorithm, "Algorithm must not be empty"); Assert.hasText(algorithm, "Algorithm must not be empty");
Assert.notNull(key, "EncryptionKey must not be empty"); Assert.notNull(key, "EncryptionKey must not be empty");
Assert.notNull(key, "QueryableEncryptionOptions must not be empty"); Assert.notNull(key, "QueryableEncryptionOptions must not be empty");
@ -107,20 +108,23 @@ public class EncryptionOptions {
* Options, like the {@link #getQueryType()}, to apply when encrypting queryable values. * Options, like the {@link #getQueryType()}, to apply when encrypting queryable values.
* *
* @author Ross Lawley * @author Ross Lawley
* @author Christoph Strobl
* @since 4.5
*/ */
public static class QueryableEncryptionOptions { 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 String queryType;
private final @Nullable Long contentionFactor; private final @Nullable Long contentionFactor;
private final @Nullable Document rangeOptions; private final Map<String, Object> attributes;
private QueryableEncryptionOptions(@Nullable String queryType, @Nullable Long contentionFactor, private QueryableEncryptionOptions(@Nullable String queryType, @Nullable Long contentionFactor,
@Nullable Document rangeOptions) { Map<String, Object> attributes) {
this.queryType = queryType; this.queryType = queryType;
this.contentionFactor = contentionFactor; this.contentionFactor = contentionFactor;
this.rangeOptions = rangeOptions; this.attributes = attributes;
} }
/** /**
@ -139,7 +143,7 @@ public class EncryptionOptions {
* @return new instance of {@link QueryableEncryptionOptions}. * @return new instance of {@link QueryableEncryptionOptions}.
*/ */
public QueryableEncryptionOptions queryType(@Nullable String queryType) { 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 {
* @return new instance of {@link QueryableEncryptionOptions}. * @return new instance of {@link QueryableEncryptionOptions}.
*/ */
public QueryableEncryptionOptions contentionFactor(@Nullable Long contentionFactor) { 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. * 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}. * @return new instance of {@link QueryableEncryptionOptions}.
*/ */
public QueryableEncryptionOptions rangeOptions(@Nullable Document rangeOptions) { public QueryableEncryptionOptions attributes(Map<String, Object> attributes) {
return new QueryableEncryptionOptions(queryType, contentionFactor, rangeOptions); return new QueryableEncryptionOptions(queryType, contentionFactor, attributes);
} }
/** /**
* Get the {@code queryType} to apply. * Get the {@code queryType} to apply.
* *
* @return {@link Optional#empty()} if not set. * @return {@literal null} if not set.
*/ */
public Optional<String> getQueryType() { public @Nullable String getQueryType() {
return Optional.ofNullable(queryType); return queryType;
} }
/** /**
* Get the {@code contentionFactor} to apply. * Get the {@code contentionFactor} to apply.
* *
* @return {@link Optional#empty()} if not set. * @return {@literal null} if not set.
*/ */
public Optional<Long> getContentionFactor() { public @Nullable Long getContentionFactor() {
return Optional.ofNullable(contentionFactor); return contentionFactor;
} }
/** /**
* Get the {@code rangeOptions} to apply. * Get the {@code rangeOptions} to apply.
* *
* @return {@link Optional#empty()} if not set. * @return never {@literal null}.
*/ */
public Optional<RangeOptions> getRangeOptions() { public Map<String, Object> getAttributes() {
if (rangeOptions == null) { return Map.copyOf(attributes);
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);
} }
/** /**
* @return {@literal true} if no arguments set. * @return {@literal true} if no arguments set.
*/ */
boolean isEmpty() { boolean isEmpty() {
return !Optionals.isAnyPresent(getQueryType(), getContentionFactor(), getRangeOptions()); return getQueryType() == null && getContentionFactor() == null && getAttributes().isEmpty();
} }
@Override @Override
public String toString() { public String toString() {
return "QueryableEncryptionOptions{" + "queryType='" + queryType + '\'' + ", contentionFactor=" + contentionFactor return "QueryableEncryptionOptions{" + "queryType='" + queryType + '\'' + ", contentionFactor=" + contentionFactor
+ ", rangeOptions=" + rangeOptions + '}'; + ", attributes=" + attributes + '}';
} }
@Override @Override
@ -251,12 +223,12 @@ public class EncryptionOptions {
if (!ObjectUtils.nullSafeEquals(contentionFactor, that.contentionFactor)) { if (!ObjectUtils.nullSafeEquals(contentionFactor, that.contentionFactor)) {
return false; return false;
} }
return ObjectUtils.nullSafeEquals(rangeOptions, that.rangeOptions); return ObjectUtils.nullSafeEquals(attributes, that.attributes);
} }
@Override @Override
public int hashCode() { 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 @@
*/ */
package org.springframework.data.mongodb.core.encryption; package org.springframework.data.mongodb.core.encryption;
import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.bson.BsonBinary; import org.bson.BsonBinary;
import org.bson.BsonDocument; import org.bson.BsonDocument;
import org.bson.BsonValue; import org.bson.BsonValue;
import org.springframework.data.mongodb.core.encryption.EncryptionKey.Type; 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 org.springframework.util.Assert;
import com.mongodb.client.model.vault.EncryptOptions; import com.mongodb.client.model.vault.EncryptOptions;
import com.mongodb.client.model.vault.RangeOptions;
import com.mongodb.client.vault.ClientEncryption; import com.mongodb.client.vault.ClientEncryption;
/** /**
@ -74,6 +78,7 @@ public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary>
} }
private EncryptOptions createEncryptOptions(EncryptionOptions options) { private EncryptOptions createEncryptOptions(EncryptionOptions options) {
EncryptOptions encryptOptions = new EncryptOptions(options.algorithm()); EncryptOptions encryptOptions = new EncryptOptions(options.algorithm());
if (Type.ALT.equals(options.key().type())) { if (Type.ALT.equals(options.key().type())) {
@ -82,10 +87,58 @@ public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary>
encryptOptions = encryptOptions.keyId((BsonBinary) options.key().value()); encryptOptions = encryptOptions.keyId((BsonBinary) options.key().value());
} }
options.queryableEncryptionOptions().getQueryType().map(encryptOptions::queryType); if (options.queryableEncryptionOptions().isEmpty()) {
options.queryableEncryptionOptions().getContentionFactor().map(encryptOptions::contentionFactor); return encryptOptions;
options.queryableEncryptionOptions().getRangeOptions().map(encryptOptions::rangeOptions); }
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; 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 @@
package org.springframework.data.mongodb.core.encryption; package org.springframework.data.mongodb.core.encryption;
import static org.assertj.core.api.Assertions.assertThat; 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.security.SecureRandom;
import java.util.List; import java.util.List;
@ -38,6 +40,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.data.convert.PropertyValueConverterFactory; import org.springframework.data.convert.PropertyValueConverterFactory;
import org.springframework.data.convert.ValueConverter;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.core.CollectionOptions; import org.springframework.data.mongodb.core.CollectionOptions;
import org.springframework.data.mongodb.core.CollectionOptions.EncryptedFieldsOptions; import org.springframework.data.mongodb.core.CollectionOptions.EncryptedFieldsOptions;
@ -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.convert.encryption.MongoEncryptionConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.RangeEncrypted; 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.core.schema.MongoJsonSchema;
import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion; import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion;
import org.springframework.data.mongodb.test.util.EnableIfReplicaSetAvailable; import org.springframework.data.mongodb.test.util.EnableIfReplicaSetAvailable;
@ -123,6 +127,74 @@ class RangeEncryptionTests {
assertThat(result).containsEntry("encryptedInt", 101); 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 { protected static class EncryptionConfig extends AbstractMongoClientConfiguration {
private static final String LOCAL_KMS_PROVIDER = "local"; private static final String LOCAL_KMS_PROVIDER = "local";
@ -277,10 +349,15 @@ class RangeEncryptionTests {
String id; String id;
String name; String name;
@ValueConverter(MongoEncryptionConverter.class)
@RangeEncrypted(contentionFactor = 0L, @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, @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() { public String getId() {
return this.id; return this.id;

Loading…
Cancel
Save