Compare commits

..

4 Commits

Author SHA1 Message Date
Julia
9439e7feef Polishing for formatting
Original Pull Request: #4455
2023-08-07 11:28:40 -04:00
Julia
f80e2e7f1d Add integration test to ensure schema validation fails when domain type property values are not encrypted as expected.
Closes #4454
Original Pull Request: #4455
2023-08-03 10:17:06 -04:00
Christoph Strobl
d1ed973fa0 Fix schema generation for encrypted fields that are considered domain entities.
This commit makes sure to consider the encrypted annotation on fields that are considered domain type property values, encrypting the entire object if necessary.
2023-07-18 06:42:23 +02:00
Christoph Strobl
24e1ae0a2b Prepare issue branch. 2023-07-18 06:25:06 +02:00
12 changed files with 142 additions and 148 deletions

View File

@@ -5,7 +5,7 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.2.x-4429-SNAPSHOT</version>
<version>4.2.x-4454-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Spring Data MongoDB</name>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.2.x-4429-SNAPSHOT</version>
<version>4.2.x-4454-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.2.x-4429-SNAPSHOT</version>
<version>4.2.x-4454-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.2.x-4429-SNAPSHOT</version>
<version>4.2.x-4454-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -203,8 +203,9 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator {
target.properties(nestedProperties.toArray(new JsonSchemaProperty[0])), required));
}
}
return targetProperties.size() == 1 ? targetProperties.iterator().next()
JsonSchemaProperty schemaProperty = targetProperties.size() == 1 ? targetProperties.iterator().next()
: JsonSchemaProperty.merged(targetProperties);
return applyEncryptionDataIfNecessary(property, schemaProperty);
}
}

View File

@@ -18,12 +18,8 @@ package org.springframework.data.mongodb.core;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;
import org.bson.Document;
@@ -191,19 +187,16 @@ public interface MongoOperations extends FluentMongoOperations {
return new SessionScoped() {
private final Lock lock = new ReentrantLock();
private @Nullable ClientSession session;
private final Object lock = new Object();
private @Nullable ClientSession session = null;
@Override
public <T> T execute(SessionCallback<T> action, Consumer<ClientSession> onComplete) {
lock.lock();
try {
synchronized (lock) {
if (session == null) {
session = sessionProvider.get();
}
} finally {
lock.unlock();
}
try {

View File

@@ -22,9 +22,6 @@ import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Supplier;
import org.aopalliance.intercept.MethodInterceptor;
@@ -137,8 +134,7 @@ public final class LazyLoadingProxyFactory {
}
return prepareProxyFactory(propertyType,
() -> new LazyLoadingInterceptor(property, callback, source, exceptionTranslator))
.getProxy(LazyLoadingProxy.class.getClassLoader());
() -> new LazyLoadingInterceptor(property, callback, source, exceptionTranslator)).getProxy(LazyLoadingProxy.class.getClassLoader());
}
/**
@@ -175,8 +171,6 @@ public final class LazyLoadingProxyFactory {
}
}
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final MongoPersistentProperty property;
private final DbRefResolverCallback callback;
private final Object source;
@@ -345,29 +339,25 @@ public final class LazyLoadingProxyFactory {
}
@Nullable
private Object resolve() {
private synchronized Object resolve() {
lock.readLock().lock();
try {
if (resolved) {
if (resolved) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace(String.format("Accessing already resolved lazy loading property %s.%s",
property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName()));
}
return result;
if (LOGGER.isTraceEnabled()) {
LOGGER.trace(String.format("Accessing already resolved lazy loading property %s.%s",
property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName()));
}
} finally {
lock.readLock().unlock();
}
if (LOGGER.isTraceEnabled()) {
LOGGER.trace(String.format("Resolving lazy loading property %s.%s",
property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName()));
return result;
}
try {
return executeWhileLocked(lock.writeLock(), () -> callback.resolve(property));
if (LOGGER.isTraceEnabled()) {
LOGGER.trace(String.format("Resolving lazy loading property %s.%s",
property.getOwner() != null ? property.getOwner().getName() : "unknown", property.getName()));
}
return callback.resolve(property);
} catch (RuntimeException ex) {
DataAccessException translatedException = exceptionTranslator.translateExceptionIfPossible(ex);
@@ -380,16 +370,6 @@ public final class LazyLoadingProxyFactory {
translatedException != null ? translatedException : ex);
}
}
private static <T> T executeWhileLocked(Lock lock, Supplier<T> stuff) {
lock.lock();
try {
return stuff.get();
} finally {
lock.unlock();
}
}
}
}

View File

@@ -18,8 +18,6 @@ package org.springframework.data.mongodb.core.messaging;
import java.time.Duration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
import org.springframework.dao.DataAccessResourceFailureException;
@@ -41,7 +39,7 @@ import com.mongodb.client.MongoCursor;
*/
abstract class CursorReadingTask<T, R> implements Task {
private final Lock lock = new ReentrantLock();
private final Object lifecycleMonitor = new Object();
private final MongoTemplate template;
private final SubscriptionRequest<T, R, RequestOptions> request;
@@ -88,14 +86,19 @@ abstract class CursorReadingTask<T, R> implements Task {
}
} catch (InterruptedException e) {
doWhileLocked(lock, () -> state = State.CANCELLED);
synchronized (lifecycleMonitor) {
state = State.CANCELLED;
}
Thread.currentThread().interrupt();
break;
}
}
} catch (RuntimeException e) {
doWhileLocked(lock, () -> state = State.CANCELLED);
synchronized (lifecycleMonitor) {
state = State.CANCELLED;
}
errorHandler.handleError(e);
}
}
@@ -111,32 +114,30 @@ abstract class CursorReadingTask<T, R> implements Task {
*/
private void start() {
doWhileLocked(lock, () -> {
synchronized (lifecycleMonitor) {
if (!State.RUNNING.equals(state)) {
state = State.STARTING;
}
});
}
do {
// boolean valid = false;
boolean valid = false;
boolean valid = executeWhileLocked(lock, () -> {
synchronized (lifecycleMonitor) {
if (!State.STARTING.equals(state)) {
return false;
if (State.STARTING.equals(state)) {
MongoCursor<T> cursor = execute(() -> initCursor(template, request.getRequestOptions(), targetType));
valid = isValidCursor(cursor);
if (valid) {
this.cursor = cursor;
state = State.RUNNING;
} else if (cursor != null) {
cursor.close();
}
}
MongoCursor<T> cursor = execute(() -> initCursor(template, request.getRequestOptions(), targetType));
boolean isValid = isValidCursor(cursor);
if (isValid) {
this.cursor = cursor;
state = State.RUNNING;
} else if (cursor != null) {
cursor.close();
}
return isValid;
});
}
if (!valid) {
@@ -144,7 +145,9 @@ abstract class CursorReadingTask<T, R> implements Task {
Thread.sleep(100);
} catch (InterruptedException e) {
doWhileLocked(lock, () -> state = State.CANCELLED);
synchronized (lifecycleMonitor) {
state = State.CANCELLED;
}
Thread.currentThread().interrupt();
}
}
@@ -160,7 +163,7 @@ abstract class CursorReadingTask<T, R> implements Task {
@Override
public void cancel() throws DataAccessResourceFailureException {
doWhileLocked(lock, () -> {
synchronized (lifecycleMonitor) {
if (State.RUNNING.equals(state) || State.STARTING.equals(state)) {
this.state = State.CANCELLED;
@@ -168,7 +171,7 @@ abstract class CursorReadingTask<T, R> implements Task {
cursor.close();
}
}
});
}
}
@Override
@@ -178,7 +181,10 @@ abstract class CursorReadingTask<T, R> implements Task {
@Override
public State getState() {
return executeWhileLocked(lock, () -> state);
synchronized (lifecycleMonitor) {
return state;
}
}
@Override
@@ -214,12 +220,13 @@ abstract class CursorReadingTask<T, R> implements Task {
@Nullable
private T getNext() {
return executeWhileLocked(lock, () -> {
synchronized (lifecycleMonitor) {
if (State.RUNNING.equals(state)) {
return cursor.tryNext();
}
throw new IllegalStateException(String.format("Cursor %s is not longer open", cursor));
});
}
throw new IllegalStateException(String.format("Cursor %s is not longer open", cursor));
}
private static boolean isValidCursor(@Nullable MongoCursor<?> cursor) {
@@ -256,23 +263,4 @@ abstract class CursorReadingTask<T, R> implements Task {
throw translated != null ? translated : e;
}
}
private static void doWhileLocked(Lock lock, Runnable action) {
executeWhileLocked(lock, () -> {
action.run();
return null;
});
}
@Nullable
private static <T> T executeWhileLocked(Lock lock, Supplier<T> stuff) {
lock.lock();
try {
return stuff.get();
} finally {
lock.unlock();
}
}
}

View File

@@ -20,10 +20,6 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -39,7 +35,8 @@ import org.springframework.util.ObjectUtils;
/**
* Simple {@link Executor} based {@link MessageListenerContainer} implementation for running {@link Task tasks} like
* listening to MongoDB <a href="https://docs.mongodb.com/manual/changeStreams/">Change Streams</a> and tailable
* cursors. <br />
* cursors.
* <br />
* This message container creates long-running tasks that are executed on {@link Executor}.
*
* @author Christoph Strobl
@@ -52,11 +49,9 @@ public class DefaultMessageListenerContainer implements MessageListenerContainer
private final TaskFactory taskFactory;
private final Optional<ErrorHandler> errorHandler;
private final Object lifecycleMonitor = new Object();
private final Map<SubscriptionRequest, Subscription> subscriptions = new LinkedHashMap<>();
ReadWriteLock lifecycleMonitor = new ReentrantReadWriteLock();
ReadWriteLock subscriptionMonitor = new ReentrantReadWriteLock();
private boolean running = false;
/**
@@ -114,34 +109,43 @@ public class DefaultMessageListenerContainer implements MessageListenerContainer
@Override
public void start() {
doWhileLocked(lifecycleMonitor.writeLock(), () -> {
if (!this.running) {
subscriptions.values().stream() //
.filter(it -> !it.isActive()) //
.filter(TaskSubscription.class::isInstance) //
.map(TaskSubscription.class::cast) //
.map(TaskSubscription::getTask) //
.forEach(taskExecutor::execute);
synchronized (lifecycleMonitor) {
running = true;
if (this.running) {
return;
}
});
subscriptions.values().stream() //
.filter(it -> !it.isActive()) //
.filter(TaskSubscription.class::isInstance) //
.map(TaskSubscription.class::cast) //
.map(TaskSubscription::getTask) //
.forEach(taskExecutor::execute);
running = true;
}
}
@Override
public void stop() {
doWhileLocked(lifecycleMonitor.writeLock(), () -> {
synchronized (lifecycleMonitor) {
if (this.running) {
subscriptions.values().forEach(Cancelable::cancel);
running = false;
}
});
}
}
@Override
public boolean isRunning() {
return executeWhileLocked(lifecycleMonitor.readLock(), () -> running);
synchronized (this.lifecycleMonitor) {
return running;
}
}
@Override
@@ -166,32 +170,36 @@ public class DefaultMessageListenerContainer implements MessageListenerContainer
@Override
public Optional<Subscription> lookup(SubscriptionRequest<?, ?, ?> request) {
return executeWhileLocked(subscriptionMonitor.readLock(), () -> Optional.ofNullable(subscriptions.get(request)));
synchronized (lifecycleMonitor) {
return Optional.ofNullable(subscriptions.get(request));
}
}
public Subscription register(SubscriptionRequest request, Task task) {
return executeWhileLocked(this.subscriptionMonitor.writeLock(), () ->
{
Subscription subscription = new TaskSubscription(task);
synchronized (lifecycleMonitor) {
if (subscriptions.containsKey(request)) {
return subscriptions.get(request);
}
Subscription subscription = new TaskSubscription(task);
this.subscriptions.put(request, subscription);
if (this.isRunning()) {
if (this.running) {
taskExecutor.execute(task);
}
return subscription;
});
}
return subscription;
}
@Override
public void remove(Subscription subscription) {
doWhileLocked(this.subscriptionMonitor.writeLock(), () -> {
synchronized (lifecycleMonitor) {
if (subscriptions.containsValue(subscription)) {
@@ -201,25 +209,6 @@ public class DefaultMessageListenerContainer implements MessageListenerContainer
subscriptions.values().remove(subscription);
}
});
}
private static void doWhileLocked(Lock lock, Runnable action) {
executeWhileLocked(lock, () -> {
action.run();
return null;
});
}
@Nullable
private static <T> T executeWhileLocked(Lock lock, Supplier<T> stuff) {
lock.lock();
try {
return stuff.get();
} finally {
lock.unlock();
}
}

View File

@@ -36,6 +36,7 @@ import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.Timest
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* {@link JsonSchemaProperty} implementation.
@@ -1139,7 +1140,9 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
enc.append("bsonType", type.toBsonType().value()); // TODO: no samples with type -> is it bson type all the way?
}
enc.append("algorithm", algorithm);
if (StringUtils.hasText(algorithm)) {
enc.append("algorithm", algorithm);
}
propertySpecification.append("encrypt", enc);

View File

@@ -271,6 +271,17 @@ class MappingMongoJsonSchemaCreatorUnitTests {
.containsEntry("properties.value", new Document("type", "string"));
}
@Test // GH-4454
void wrapEncryptedEntityTypeLikeProperty() {
MongoJsonSchema schema = MongoJsonSchemaCreator.create() //
.filter(MongoJsonSchemaCreator.encryptedOnly()) // filter non encrypted fields
.createSchemaFor(WithEncryptedEntityLikeProperty.class);
assertThat(schema.schemaDocument()) //
.containsEntry("properties.domainTypeValue", Document.parse("{'encrypt': {'bsonType': 'object' } }"));
}
// --> TYPES AND JSON
// --> ENUM
@@ -676,4 +687,9 @@ class MappingMongoJsonSchemaCreatorUnitTests {
static class PropertyClashWithA {
Integer aNonEncrypted;
}
@Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic")
static class WithEncryptedEntityLikeProperty {
@Encrypted SomeDomainType domainTypeValue;
}
}

View File

@@ -33,8 +33,10 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
import org.springframework.data.mongodb.core.CollectionOptions.ValidationOptions;
import org.springframework.data.mongodb.core.mapping.Encrypted;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.data.mongodb.test.util.Client;
import org.springframework.data.mongodb.test.util.MongoClientExtension;
import org.springframework.lang.Nullable;
@@ -46,11 +48,13 @@ import com.mongodb.client.model.ValidationLevel;
/**
* Integration tests for {@link CollectionOptions#validation(ValidationOptions)} using
* {@link org.springframework.data.mongodb.core.validation.CriteriaValidator} and
* {@link org.springframework.data.mongodb.core.validation.DocumentValidator}.
* {@link org.springframework.data.mongodb.core.validation.CriteriaValidator},
* {@link org.springframework.data.mongodb.core.validation.DocumentValidator} and
* {@link org.springframework.data.mongodb.core.validation.JsonSchemaValidator}.
*
* @author Andreas Zink
* @author Christoph Strobl
* @author Julia Lee
*/
@ExtendWith({ MongoClientExtension.class, SpringExtension.class })
public class MongoTemplateValidationTests {
@@ -186,6 +190,20 @@ public class MongoTemplateValidationTests {
assertThat(getValidatorInfo(COLLECTION_NAME)).isEqualTo(new Document("customName", new Document("$type", "bool")));
}
@Test // GH-4454
public void failsJsonSchemaValidationForEncryptedDomainEntityProperty() {
MongoJsonSchema schema = MongoJsonSchemaCreator.create().createSchemaFor(BeanWithEncryptedDomainEntity.class);
template.createCollection(COLLECTION_NAME, CollectionOptions.empty().schema(schema));
BeanWithEncryptedDomainEntity person = new BeanWithEncryptedDomainEntity();
person.encryptedDomainEntity = new SimpleBean("some string", 100, null);
assertThatExceptionOfType(DataIntegrityViolationException.class)
.isThrownBy(() -> template.save(person))
.withMessageContaining("Document failed validation");
}
private Document getCollectionOptions(String collectionName) {
return getCollectionInfo(collectionName).get("options", Document.class);
}
@@ -271,4 +289,10 @@ public class MongoTemplateValidationTests {
return "MongoTemplateValidationTests.SimpleBean(nonNullString=" + this.getNonNullString() + ", rangedInteger=" + this.getRangedInteger() + ", customFieldName=" + this.getCustomFieldName() + ")";
}
}
@org.springframework.data.mongodb.core.mapping.Document(collection = COLLECTION_NAME)
@Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic")
static class BeanWithEncryptedDomainEntity {
@Encrypted SimpleBean encryptedDomainEntity;
}
}