Skip to content

Commit 194a3c2

Browse files
committed
Field Level Encryption Support.
Note that an @Encrypted property object that contains an @Encrypted property is not supported and will result in an exception. Closes #763.
1 parent a92ebd0 commit 194a3c2

18 files changed

+1058
-80
lines changed

README.adoc

+7
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,13 @@ The generated documentation is available from `target/site/reference/html/index.
200200
popd
201201
----
202202

203+
=== Intellij Issue with Importing pom.xml
204+
205+
There is an issue in Intellij that prevents it from importing modules when one of the module
206+
directories has the same name as the project directory. The work-around is to create a new module (any name, any type will suffice).
207+
When Intellij creates the new module, it will also recognize the existing modules. Once the new module is
208+
created, it can be deleted and Intellij will now recognize the existing modules.
209+
203210
The generated documentation is available from `target/site/reference/html/index.html`.
204211

205212
== Examples

spring-data-couchbase/pom.xml

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2-
<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">
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
34

45
<modelVersion>4.0.0</modelVersion>
56

@@ -217,6 +218,13 @@
217218
<scope>test</scope>
218219
</dependency>
219220

221+
<dependency>
222+
<groupId>com.couchbase.client</groupId>
223+
<artifactId>couchbase-encryption</artifactId>
224+
<version>3.1.0</version>
225+
<scope>test</scope>
226+
</dependency>
227+
220228
</dependencies>
221229

222230
<repositories>
@@ -303,7 +311,9 @@
303311
</goals>
304312
<configuration>
305313
<outputDirectory>target/generated-test-sources</outputDirectory>
306-
<processor>org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor</processor>
314+
<processor>
315+
org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor
316+
</processor>
307317
</configuration>
308318
</execution>
309319
</executions>

spring-data-couchbase/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java

+45-8
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,17 @@
1818

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

21-
import java.util.Collections;
21+
import java.util.ArrayList;
2222
import java.util.HashSet;
23+
import java.util.List;
2324
import java.util.Set;
2425

2526
import org.springframework.beans.factory.config.BeanDefinition;
2627
import org.springframework.context.annotation.Bean;
2728
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
2829
import org.springframework.context.annotation.Configuration;
2930
import org.springframework.context.annotation.Role;
31+
import org.springframework.core.convert.converter.GenericConverter;
3032
import org.springframework.core.type.filter.AnnotationTypeFilter;
3133
import org.springframework.data.convert.CustomConversions;
3234
import org.springframework.data.couchbase.CouchbaseClientFactory;
@@ -35,6 +37,7 @@
3537
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
3638
import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions;
3739
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
40+
import org.springframework.data.couchbase.core.convert.OtherConverters;
3841
import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService;
3942
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
4043
import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext;
@@ -149,7 +152,8 @@ public ClusterEnvironment couchbaseClusterEnvironment() {
149152
if (!nonShadowedJacksonPresent()) {
150153
throw new CouchbaseException("non-shadowed Jackson not present");
151154
}
152-
builder.jsonSerializer(JacksonJsonSerializer.create(couchbaseObjectMapper()));
155+
builder.jsonSerializer(JacksonJsonSerializer.create(couchbaseObjectMapper(cryptoManager())));
156+
builder.cryptoManager();
153157
configureEnvironment(builder);
154158
return builder.build();
155159
}
@@ -280,8 +284,8 @@ public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingConte
280284
@Bean
281285
public TranslationService couchbaseTranslationService() {
282286
final JacksonTranslationService jacksonTranslationService = new JacksonTranslationService();
287+
jacksonTranslationService.setObjectMapper(couchbaseObjectMapper(cryptoManager()));
283288
jacksonTranslationService.afterPropertiesSet();
284-
285289
// for sdk3, we need to ask the mapper _it_ uses to ignore extra fields...
286290
JacksonTransformers.MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
287291
return jacksonTranslationService;
@@ -308,10 +312,25 @@ public CouchbaseMappingContext couchbaseMappingContext(CustomConversions customC
308312
*/
309313

310314
public ObjectMapper couchbaseObjectMapper() {
311-
ObjectMapper mapper = new ObjectMapper();
315+
return couchbaseObjectMapper(cryptoManager());
316+
}
317+
318+
/**
319+
* Creates a {@link ObjectMapper} for the jsonSerializer of the ClusterEnvironment
320+
*
321+
* @param cryptoManager
322+
* @return ObjectMapper
323+
*/
324+
325+
ObjectMapper mapper;
326+
327+
public ObjectMapper couchbaseObjectMapper(CryptoManager cryptoManager) {
328+
if (mapper != null) {
329+
return mapper;
330+
}
331+
mapper = new ObjectMapper(); // or use the one from the Java SDK (?) JacksonTransformers.MAPPER
312332
mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
313333
mapper.registerModule(new JsonValueModule());
314-
CryptoManager cryptoManager = null;
315334
if (cryptoManager != null) {
316335
mapper.registerModule(new EncryptionModule(cryptoManager));
317336
}
@@ -320,7 +339,7 @@ public ObjectMapper couchbaseObjectMapper() {
320339

321340
/**
322341
* The default blocking transaction manager. It is an implementation of CallbackPreferringTransactionManager
323-
* CallbackPreferrringTransactionmanagers do not play well with test-cases that rely
342+
* CallbackPreferringTransactionManagers do not play well with test-cases that rely
324343
* on @TestTransaction/@BeforeTransaction/@AfterTransaction
325344
*
326345
* @param clientFactory
@@ -341,6 +360,7 @@ CouchbaseCallbackTransactionManager couchbaseTransactionManager(CouchbaseClientF
341360
TransactionTemplate couchbaseTransactionTemplate(CouchbaseCallbackTransactionManager couchbaseTransactionManager) {
342361
return new TransactionTemplate(couchbaseTransactionManager);
343362
}
363+
344364
/**
345365
* The default TransactionalOperator.
346366
*
@@ -379,11 +399,28 @@ protected boolean autoIndexCreation() {
379399
* and {@link #couchbaseMappingContext(CustomConversions)}. Returns an empty {@link CustomConversions} instance by
380400
* default.
381401
*
402+
* @param cryptoManagerOptional optional cryptoManager. Make varargs for backwards compatibility.
382403
* @return must not be {@literal null}.
383404
*/
384405
@Bean(name = BeanNames.COUCHBASE_CUSTOM_CONVERSIONS)
385-
public CustomConversions customConversions() {
386-
return new CouchbaseCustomConversions(Collections.emptyList());
406+
public CustomConversions customConversions(CryptoManager... cryptoManagerOptional) {
407+
assert (cryptoManagerOptional == null || cryptoManagerOptional.length <= 1);
408+
CryptoManager cryptoManager = cryptoManagerOptional != null && cryptoManagerOptional.length == 1
409+
? cryptoManagerOptional[0]
410+
: null;
411+
List<GenericConverter> newConverters = new ArrayList();
412+
// the cryptoConverters take an argument, so they cannot be created in the
413+
// static block of CouchbaseCustomConversions. And they must be set before the super() constructor
414+
// in CouchbaseCustomerConversions
415+
if (cryptoManager != null) {
416+
newConverters.addAll(OtherConverters.getCryptoConverters(cryptoManager));
417+
}
418+
return new CouchbaseCustomConversions(newConverters);
419+
}
420+
421+
@Bean
422+
protected CryptoManager cryptoManager() {
423+
return null;
387424
}
388425

389426
/**

spring-data-couchbase/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java

+55
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,19 @@
2020

2121
import org.springframework.beans.factory.InitializingBean;
2222
import org.springframework.core.convert.ConversionService;
23+
import org.springframework.core.convert.TypeDescriptor;
2324
import org.springframework.core.convert.support.GenericConversionService;
2425
import org.springframework.data.convert.CustomConversions;
26+
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
27+
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
2528
import org.springframework.data.mapping.model.EntityInstantiators;
2629

2730
/**
2831
* An abstract {@link CouchbaseConverter} that provides the basics for the {@link MappingCouchbaseConverter}.
2932
*
3033
* @author Michael Nitschinger
3134
* @author Mark Paluch
35+
* @author Michael Reiche
3236
*/
3337
public abstract class AbstractCouchbaseConverter implements CouchbaseConverter, InitializingBean {
3438

@@ -93,6 +97,57 @@ public void afterPropertiesSet() {
9397
conversions.registerConvertersIn(conversionService);
9498
}
9599

100+
/**
101+
* This convertForWriteIfNeeded takes a property and accessor so that the annotations can be accessed (ie. @Encrypted)
102+
*
103+
* @param prop the property to be converted to the class that would actually be stored.
104+
* @param accessor the property accessor
105+
* @return
106+
*/
107+
@Override
108+
public Object convertForWriteIfNeeded(CouchbasePersistentProperty prop, ConvertingPropertyAccessor<Object> accessor) {
109+
Object value = accessor.getProperty(prop, prop.getType());
110+
if (value == null) {
111+
return null;
112+
}
113+
114+
Object result = this.conversions.getCustomWriteTarget(prop.getType()) //
115+
.map(it -> this.conversionService.convert(value, new TypeDescriptor(prop.getField()),
116+
TypeDescriptor.valueOf(it))) //
117+
.orElseGet(() -> Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value);
118+
119+
return result;
120+
121+
}
122+
123+
/**
124+
* This convertForWriteIfNeeded takes a property and accessor so that the annotations can be accessed (ie. @Encrypted)
125+
*
126+
* @param prop the property to be converted to the class that would actually be stored.
127+
* @param accessor the property accessor
128+
* @return
129+
*/
130+
// @Override
131+
public Object convertElementForWriteIfNeeded(CouchbasePersistentProperty prop) {
132+
Object value = null;
133+
//return conversionService.convert(value, TypeDescriptor.forObject(value), new TypeDescriptor(target.getField()));
134+
135+
Object result = this.conversions.getCustomWriteTarget(prop.getType()) //
136+
.map(it -> this.conversionService.convert(value, new TypeDescriptor(prop.getField()),
137+
TypeDescriptor.valueOf(it))) //
138+
.orElseGet(() -> Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value);
139+
140+
return result;
141+
142+
}
143+
144+
/**
145+
* This convertForWriteIfNeed takes only the value to convert. It cannot access the annotations of the Field being
146+
* converted.
147+
*
148+
* @param value the value to be converted to the class that would actually be stored.
149+
* @return
150+
*/
96151
@Override
97152
public Object convertForWriteIfNeeded(Object value) {
98153
if (value == null) {

spring-data-couchbase/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseConverter.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
2323
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
2424
import org.springframework.data.mapping.Alias;
25+
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
2526
import org.springframework.data.util.TypeInformation;
2627

2728
/**
@@ -37,13 +38,24 @@ public interface CouchbaseConverter
3738

3839
/**
3940
* Convert the value if necessary to the class that would actually be stored, or leave it as is if no conversion
40-
* needed.
41+
* needed. This method cannot access the annotations of the field.
4142
*
4243
* @param value the value to be converted to the class that would actually be stored.
4344
* @return the converted value (or the same value if no conversion necessary).
4445
*/
4546
Object convertForWriteIfNeeded(Object value);
4647

48+
/**
49+
* Convert the value if necessary to the class that would actually be stored, or leave it as is if no conversion
50+
* needed. This method can access the annotations of the field.
51+
*
52+
* @param source the property to be converted to the class that would actually be stored.
53+
* @param accessor the property accessor
54+
* @return the converted value (or the same value if no conversion necessary).
55+
*/
56+
Object convertForWriteIfNeeded(final CouchbasePersistentProperty source,
57+
final ConvertingPropertyAccessor<Object> accessor);
58+
4759
/**
4860
* Return the Class that would actually be stored for a given Class.
4961
*

spring-data-couchbase/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseCustomConversions.java

+41
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

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

25+
import com.couchbase.client.core.encryption.CryptoManager;
26+
2527
/**
2628
* Value object to capture custom conversion.
2729
* <p>
@@ -32,6 +34,7 @@
3234
* @author Oliver Gierke
3335
* @author Mark Paluch
3436
* @author Subhashni Balakrishnan
37+
* @Michael Reiche
3538
* @see org.springframework.data.convert.CustomConversions
3639
* @see SimpleTypeHolder
3740
* @since 2.0
@@ -42,6 +45,18 @@ public class CouchbaseCustomConversions extends org.springframework.data.convert
4245

4346
private static final List<Object> STORE_CONVERTERS;
4447

48+
private CryptoManager cryptoManager;
49+
50+
/**
51+
* Expose the CryptoManager used by a DecryptingReadingConverter or EncryptingWritingConverter, if any. There can only
52+
* be one. MappingCouchbaseConverter needs it.
53+
*
54+
* @return cryptoManager
55+
*/
56+
public CryptoManager getCryptoManager() {
57+
return cryptoManager;
58+
}
59+
4560
static {
4661

4762
List<Object> converters = new ArrayList<>();
@@ -61,5 +76,31 @@ public class CouchbaseCustomConversions extends org.springframework.data.convert
6176
*/
6277
public CouchbaseCustomConversions(final List<?> converters) {
6378
super(STORE_CONVERSIONS, converters);
79+
for (Object c : converters) {
80+
if (c instanceof DecryptingReadingConverter) {
81+
CryptoManager foundCryptoManager = ((DecryptingReadingConverter) c).cryptoManager;
82+
if (foundCryptoManager == null) {
83+
throw new RuntimeException(("DecryptingReadingConverter must have a cryptoManager"));
84+
} else {
85+
if (cryptoManager != null && this.cryptoManager != cryptoManager) {
86+
throw new RuntimeException(
87+
"all DecryptingReadingConverters and EncryptingWringConverters must use " + " a single CryptoManager");
88+
}
89+
}
90+
cryptoManager = foundCryptoManager;
91+
}
92+
if (c instanceof EncryptingWritingConverter) {
93+
CryptoManager foundCryptoManager = ((EncryptingWritingConverter) c).cryptoManager;
94+
if (foundCryptoManager == null) {
95+
throw new RuntimeException(("EncryptingWritingConverter must have a cryptoManager"));
96+
} else {
97+
if (cryptoManager != null && this.cryptoManager != cryptoManager) {
98+
throw new RuntimeException(
99+
"all DecryptingReadingConverters and EncryptingWringConverters must use " + " a single CryptoManager");
100+
}
101+
}
102+
cryptoManager = foundCryptoManager;
103+
}
104+
}
64105
}
65106
}

0 commit comments

Comments
 (0)