Skip to content

Field Level Encryption Support. #1546

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@ The generated documentation is available from `target/site/reference/html/index.
popd
----

=== Intellij Issue with Importing pom.xml

There is an issue in Intellij that prevents it from importing modules when one of the module
directories has the same name as the project directory. The work-around is to create a new module (any name, any type will suffice).
When Intellij creates the new module, it will also recognize the existing modules. Once the new module is
created, it can be deleted and Intellij will now recognize the existing modules.

The generated documentation is available from `target/site/reference/html/index.html`.

== Examples
Expand Down
14 changes: 12 additions & 2 deletions spring-data-couchbase/pom.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

Expand Down Expand Up @@ -217,6 +218,13 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.couchbase.client</groupId>
<artifactId>couchbase-encryption</artifactId>
<version>3.1.0</version>
<scope>test</scope>
</dependency>

</dependencies>

<repositories>
Expand Down Expand Up @@ -303,7 +311,9 @@
</goals>
<configuration>
<outputDirectory>target/generated-test-sources</outputDirectory>
<processor>org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor</processor>
<processor>
org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor
</processor>
</configuration>
</execution>
</executions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@

import static com.couchbase.client.java.ClusterOptions.clusterOptions;

import java.util.Collections;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.couchbase.CouchbaseClientFactory;
Expand All @@ -35,6 +37,7 @@
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions;
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
import org.springframework.data.couchbase.core.convert.OtherConverters;
import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService;
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext;
Expand Down Expand Up @@ -149,7 +152,9 @@ public ClusterEnvironment couchbaseClusterEnvironment() {
if (!nonShadowedJacksonPresent()) {
throw new CouchbaseException("non-shadowed Jackson not present");
}
builder.jsonSerializer(JacksonJsonSerializer.create(couchbaseObjectMapper()));
CryptoManager cryptoManager = cryptoManager();
builder.jsonSerializer(JacksonJsonSerializer.create(couchbaseObjectMapper(cryptoManager)));
builder.cryptoManager(cryptoManager);
configureEnvironment(builder);
return builder.build();
}
Expand Down Expand Up @@ -280,8 +285,8 @@ public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingConte
@Bean
public TranslationService couchbaseTranslationService() {
final JacksonTranslationService jacksonTranslationService = new JacksonTranslationService();
jacksonTranslationService.setObjectMapper(couchbaseObjectMapper(cryptoManager()));
jacksonTranslationService.afterPropertiesSet();

// for sdk3, we need to ask the mapper _it_ uses to ignore extra fields...
JacksonTransformers.MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return jacksonTranslationService;
Expand All @@ -308,10 +313,25 @@ public CouchbaseMappingContext couchbaseMappingContext(CustomConversions customC
*/

public ObjectMapper couchbaseObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
return couchbaseObjectMapper(cryptoManager());
}

/**
* Creates a {@link ObjectMapper} for the jsonSerializer of the ClusterEnvironment
*
* @param cryptoManager
* @return ObjectMapper
*/

ObjectMapper mapper;

public ObjectMapper couchbaseObjectMapper(CryptoManager cryptoManager) {
if (mapper != null) {
return mapper;
}
mapper = new ObjectMapper(); // or use the one from the Java SDK (?) JacksonTransformers.MAPPER
mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.registerModule(new JsonValueModule());
CryptoManager cryptoManager = null;
if (cryptoManager != null) {
mapper.registerModule(new EncryptionModule(cryptoManager));
}
Expand All @@ -320,7 +340,7 @@ public ObjectMapper couchbaseObjectMapper() {

/**
* The default blocking transaction manager. It is an implementation of CallbackPreferringTransactionManager
* CallbackPreferrringTransactionmanagers do not play well with test-cases that rely
* CallbackPreferringTransactionManagers do not play well with test-cases that rely
* on @TestTransaction/@BeforeTransaction/@AfterTransaction
*
* @param clientFactory
Expand All @@ -341,6 +361,7 @@ CouchbaseCallbackTransactionManager couchbaseTransactionManager(CouchbaseClientF
TransactionTemplate couchbaseTransactionTemplate(CouchbaseCallbackTransactionManager couchbaseTransactionManager) {
return new TransactionTemplate(couchbaseTransactionManager);
}

/**
* The default TransactionalOperator.
*
Expand Down Expand Up @@ -379,11 +400,28 @@ protected boolean autoIndexCreation() {
* and {@link #couchbaseMappingContext(CustomConversions)}. Returns an empty {@link CustomConversions} instance by
* default.
*
* @param cryptoManagerOptional optional cryptoManager. Make varargs for backwards compatibility.
* @return must not be {@literal null}.
*/
@Bean(name = BeanNames.COUCHBASE_CUSTOM_CONVERSIONS)
public CustomConversions customConversions() {
return new CouchbaseCustomConversions(Collections.emptyList());
public CustomConversions customConversions(CryptoManager... cryptoManagerOptional) {
assert (cryptoManagerOptional == null || cryptoManagerOptional.length <= 1);
Copy link
Contributor

@dnault dnault Aug 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of these alternatives suggested by StackOverflow users?

public CustomConversions customConversions(
    Optional<CryptoManager> cryptoManagerOptional
)

Or this:

public CustomConversions customConversions(
    @Autowired(required = false) CryptoManager cryptoManager
)

Reference: https://stackoverflow.com/questions/46704950/how-to-define-optional-parameters-dependencies-in-the-bean-method-in-spring

Copy link
Collaborator Author

@mikereiche mikereiche Aug 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first one changes a public API so is not possible.
I have doubts about using @Autowired in the configuration class.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing foo() to foo(T...) is a source-compatible change, but I suspect it's not binary-compatible. (Maybe binary compatibility is not a concern?)

CryptoManager cryptoManager = cryptoManagerOptional != null && cryptoManagerOptional.length == 1
? cryptoManagerOptional[0]
: null;
List<GenericConverter> newConverters = new ArrayList();
// the cryptoConverters take an argument, so they cannot be created in the
// static block of CouchbaseCustomConversions. And they must be set before the super() constructor
// in CouchbaseCustomerConversions
if (cryptoManager != null) {
newConverters.addAll(OtherConverters.getCryptoConverters(cryptoManager));
}
return new CouchbaseCustomConversions(newConverters);
}

@Bean
protected CryptoManager cryptoManager() {
return null;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@

import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mapping.model.EntityInstantiators;

/**
* An abstract {@link CouchbaseConverter} that provides the basics for the {@link MappingCouchbaseConverter}.
*
* @author Michael Nitschinger
* @author Mark Paluch
* @author Michael Reiche
*/
public abstract class AbstractCouchbaseConverter implements CouchbaseConverter, InitializingBean {

Expand Down Expand Up @@ -93,6 +97,36 @@ public void afterPropertiesSet() {
conversions.registerConvertersIn(conversionService);
}

/**
* This convertForWriteIfNeeded takes a property and accessor so that the annotations can be accessed (ie. @Encrypted)
*
* @param prop the property to be converted to the class that would actually be stored.
* @param accessor the property accessor
* @return
*/
@Override
public Object convertForWriteIfNeeded(CouchbasePersistentProperty prop, ConvertingPropertyAccessor<Object> accessor) {
Object value = accessor.getProperty(prop, prop.getType());
if (value == null) {
return null;
}

Object result = this.conversions.getCustomWriteTarget(prop.getType()) //
.map(it -> this.conversionService.convert(value, new TypeDescriptor(prop.getField()),
TypeDescriptor.valueOf(it))) //
.orElseGet(() -> Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value);

return result;

}

/**
* This convertForWriteIfNeed takes only the value to convert. It cannot access the annotations of the Field being
* converted.
*
* @param value the value to be converted to the class that would actually be stored.
* @return
*/
@Override
public Object convertForWriteIfNeeded(Object value) {
if (value == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.mapping.Alias;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.util.TypeInformation;

/**
Expand All @@ -37,13 +38,24 @@ public interface CouchbaseConverter

/**
* Convert the value if necessary to the class that would actually be stored, or leave it as is if no conversion
* needed.
* needed. This method cannot access the annotations of the field.
*
* @param value the value to be converted to the class that would actually be stored.
* @return the converted value (or the same value if no conversion necessary).
*/
Object convertForWriteIfNeeded(Object value);

/**
* Convert the value if necessary to the class that would actually be stored, or leave it as is if no conversion
* needed. This method can access the annotations of the field.
*
* @param source the property to be converted to the class that would actually be stored.
* @param accessor the property accessor
* @return the converted value (or the same value if no conversion necessary).
*/
Object convertForWriteIfNeeded(final CouchbasePersistentProperty source,
final ConvertingPropertyAccessor<Object> accessor);

/**
* Return the Class that would actually be stored for a given Class.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

import org.springframework.data.mapping.model.SimpleTypeHolder;

import com.couchbase.client.core.encryption.CryptoManager;

/**
* Value object to capture custom conversion.
* <p>
Expand All @@ -32,6 +34,7 @@
* @author Oliver Gierke
* @author Mark Paluch
* @author Subhashni Balakrishnan
* @Michael Reiche
* @see org.springframework.data.convert.CustomConversions
* @see SimpleTypeHolder
* @since 2.0
Expand All @@ -42,6 +45,18 @@ public class CouchbaseCustomConversions extends org.springframework.data.convert

private static final List<Object> STORE_CONVERTERS;

private CryptoManager cryptoManager;

/**
* Expose the CryptoManager used by a DecryptingReadingConverter or EncryptingWritingConverter, if any. There can only
* be one. MappingCouchbaseConverter needs it.
*
* @return cryptoManager
*/
public CryptoManager getCryptoManager() {
return cryptoManager;
}

static {

List<Object> converters = new ArrayList<>();
Expand All @@ -61,5 +76,31 @@ public class CouchbaseCustomConversions extends org.springframework.data.convert
*/
public CouchbaseCustomConversions(final List<?> converters) {
super(STORE_CONVERSIONS, converters);
for (Object c : converters) {
if (c instanceof DecryptingReadingConverter) {
CryptoManager foundCryptoManager = ((DecryptingReadingConverter) c).cryptoManager;
if (foundCryptoManager == null) {
throw new RuntimeException(("DecryptingReadingConverter must have a cryptoManager"));
} else {
if (cryptoManager != null && this.cryptoManager != cryptoManager) {
throw new RuntimeException(
"all DecryptingReadingConverters and EncryptingWringConverters must use " + " a single CryptoManager");
}
}
cryptoManager = foundCryptoManager;
}
if (c instanceof EncryptingWritingConverter) {
CryptoManager foundCryptoManager = ((EncryptingWritingConverter) c).cryptoManager;
if (foundCryptoManager == null) {
throw new RuntimeException(("EncryptingWritingConverter must have a cryptoManager"));
} else {
if (cryptoManager != null && this.cryptoManager != cryptoManager) {
throw new RuntimeException(
"all DecryptingReadingConverters and EncryptingWringConverters must use " + " a single CryptoManager");
}
}
cryptoManager = foundCryptoManager;
}
}
}
}
Loading