Fix serialiation/deserialization for field level encryption. (#1622)
Closes #1621.
This commit is contained in:
committed by
mikereiche
parent
761ed0d8a1
commit
0030214aa1
@@ -26,8 +26,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import com.couchbase.client.java.encryption.annotation.Encrypted;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||
@@ -77,11 +75,13 @@ import com.couchbase.client.core.env.PasswordAuthenticator;
|
||||
import com.couchbase.client.core.error.CouchbaseException;
|
||||
import com.couchbase.client.java.Cluster;
|
||||
import com.couchbase.client.java.codec.JacksonJsonSerializer;
|
||||
import com.couchbase.client.java.encryption.annotation.Encrypted;
|
||||
import com.couchbase.client.java.encryption.databind.jackson.EncryptionModule;
|
||||
import com.couchbase.client.java.env.ClusterEnvironment;
|
||||
import com.couchbase.client.java.json.JacksonTransformers;
|
||||
import com.couchbase.client.java.json.JsonValueModule;
|
||||
import com.couchbase.client.java.query.QueryScanConsistency;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
@@ -312,7 +312,7 @@ public abstract class AbstractCouchbaseConfiguration {
|
||||
}
|
||||
|
||||
final public ObjectMapper getObjectMapper() {
|
||||
if(objectMapper == null) {
|
||||
if (objectMapper == null) {
|
||||
synchronized (this) {
|
||||
if (objectMapper == null) {
|
||||
objectMapper = couchbaseObjectMapper();
|
||||
@@ -403,7 +403,7 @@ public abstract class AbstractCouchbaseConfiguration {
|
||||
*/
|
||||
@Bean(name = BeanNames.COUCHBASE_CUSTOM_CONVERSIONS)
|
||||
public CustomConversions customConversions() {
|
||||
return customConversions(getCryptoManager());
|
||||
return customConversions(getCryptoManager(), getObjectMapper());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -414,11 +414,12 @@ public abstract class AbstractCouchbaseConfiguration {
|
||||
* @param cryptoManager
|
||||
* @return must not be {@literal null}.
|
||||
*/
|
||||
public CustomConversions customConversions(CryptoManager cryptoManager) {
|
||||
public CustomConversions customConversions(CryptoManager cryptoManager, ObjectMapper objectMapper) {
|
||||
List<GenericConverter> newConverters = new ArrayList();
|
||||
CustomConversions customConversions = CouchbaseCustomConversions.create(configurationAdapter -> {
|
||||
SimplePropertyValueConversions valueConversions = new SimplePropertyValueConversions();
|
||||
valueConversions.setConverterFactory(new CouchbasePropertyValueConverterFactory(cryptoManager, annotationToConverterMap()));
|
||||
valueConversions.setConverterFactory(
|
||||
new CouchbasePropertyValueConverterFactory(cryptoManager, annotationToConverterMap(), objectMapper));
|
||||
valueConversions.setValueConverterRegistry(new PropertyValueConverterRegistrar().buildRegistry());
|
||||
valueConversions.afterPropertiesSet(); // wraps the CouchbasePropertyValueConverterFactory with CachingPVCFactory
|
||||
configurationAdapter.setPropertyValueConversions(valueConversions);
|
||||
@@ -431,17 +432,18 @@ public abstract class AbstractCouchbaseConfiguration {
|
||||
return customConversions;
|
||||
}
|
||||
|
||||
Map<Class<? extends Annotation>,Class<?>> annotationToConverterMap(){
|
||||
Map<Class<? extends Annotation>,Class<?>> map= new HashMap();
|
||||
Map<Class<? extends Annotation>, Class<?>> annotationToConverterMap() {
|
||||
Map<Class<? extends Annotation>, Class<?>> map = new HashMap();
|
||||
map.put(Encrypted.class, CryptoConverter.class);
|
||||
map.put(JsonValue.class, JsonValueConverter.class);
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* cryptoManager can be null, so it cannot be a bean and then used as an arg for bean methods
|
||||
*/
|
||||
private CryptoManager getCryptoManager() {
|
||||
if(cryptoManager == null) {
|
||||
if (cryptoManager == null) {
|
||||
synchronized (this) {
|
||||
if (cryptoManager == null) {
|
||||
cryptoManager = cryptoManager();
|
||||
|
||||
@@ -160,7 +160,8 @@ public abstract class AbstractCouchbaseConverter implements CouchbaseConverter,
|
||||
Class<?> elementType = value.getClass();
|
||||
|
||||
if (elementType == null || conversions.isSimpleType(elementType)) {
|
||||
// superseded by EnumCvtrs value = Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value;
|
||||
// superseded by EnumCvtrs value = Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() :
|
||||
// value;
|
||||
} else if (value instanceof Collection || elementType.isArray()) {
|
||||
TypeInformation<?> type = ClassTypeInformation.from(value.getClass());
|
||||
value = ((MappingCouchbaseConverter) this).writeCollectionInternal(MappingCouchbaseConverter.asCollection(value),
|
||||
@@ -168,7 +169,7 @@ public abstract class AbstractCouchbaseConverter implements CouchbaseConverter,
|
||||
} else {
|
||||
CouchbaseDocument embeddedDoc = new CouchbaseDocument();
|
||||
TypeInformation<?> type = ClassTypeInformation.from(value.getClass());
|
||||
((MappingCouchbaseConverter) this).writeInternalRoot(value, embeddedDoc, type, false, null);
|
||||
((MappingCouchbaseConverter) this).writeInternalRoot(value, embeddedDoc, type, false, null, true);
|
||||
value = embeddedDoc;
|
||||
}
|
||||
return value;
|
||||
|
||||
@@ -31,7 +31,7 @@ import org.springframework.data.mapping.PersistentProperty;
|
||||
import com.couchbase.client.core.encryption.CryptoManager;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import org.springframework.util.Assert;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Accept the Couchbase @Encrypted and @JsonValue annotations in addition to @ValueConverter annotation.<br>
|
||||
@@ -48,14 +48,16 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class CouchbasePropertyValueConverterFactory implements PropertyValueConverterFactory {
|
||||
|
||||
final CryptoManager cryptoManager;
|
||||
final Map<Class<? extends Annotation>, Class<?>> annotationToConverterMap;
|
||||
private final CryptoManager cryptoManager;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final Map<Class<? extends Annotation>, Class<?>> annotationToConverterMap;
|
||||
static protected final Map<Class<?>, Optional<PropertyValueConverter<?, ?, ?>>> converterCacheForType = new ConcurrentHashMap<>();
|
||||
|
||||
public CouchbasePropertyValueConverterFactory(CryptoManager cryptoManager,
|
||||
Map<Class<? extends Annotation>, Class<?>> annotationToConverterMap) {
|
||||
Map<Class<? extends Annotation>, Class<?>> annotationToConverterMap, ObjectMapper objectMapper) {
|
||||
this.cryptoManager = cryptoManager;
|
||||
this.annotationToConverterMap = annotationToConverterMap;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,7 +157,7 @@ public class CouchbasePropertyValueConverterFactory implements PropertyValueConv
|
||||
|
||||
// CryptoConverter takes a cryptoManager argument
|
||||
if (CryptoConverter.class.isAssignableFrom(converterType)) {
|
||||
return (PropertyValueConverter<DV, SV, P>) new CryptoConverter(cryptoManager);
|
||||
return (PropertyValueConverter<DV, SV, P>) new CryptoConverter(cryptoManager, objectMapper);
|
||||
} else if (property != null) { // try constructor that takes PersistentProperty
|
||||
try {
|
||||
Constructor<?> constructor = converterType.getConstructor(PersistentProperty.class);
|
||||
|
||||
@@ -20,15 +20,12 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.core.convert.ConversionFailedException;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.ConverterNotFoundException;
|
||||
import org.springframework.data.convert.CustomConversions;
|
||||
import org.springframework.data.convert.PropertyValueConverter;
|
||||
import org.springframework.data.convert.ValueConversionContext;
|
||||
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.CouchbaseDocument;
|
||||
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
@@ -41,20 +38,24 @@ import com.couchbase.client.java.encryption.annotation.Encrypted;
|
||||
import com.couchbase.client.java.json.JsonArray;
|
||||
import com.couchbase.client.java.json.JsonObject;
|
||||
import com.couchbase.client.java.json.JsonValue;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Encrypt/Decrypted properties annotated. This is registered in
|
||||
* {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions(CryptoManager)}.
|
||||
* {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions(CryptoManager, ObjectMapper)}.
|
||||
*
|
||||
* @author Michael Reiche
|
||||
*/
|
||||
public class CryptoConverter implements
|
||||
PropertyValueConverter<Object, CouchbaseDocument, ValueConversionContext<? extends PersistentProperty<?>>> {
|
||||
|
||||
CryptoManager cryptoManager;
|
||||
private final CryptoManager cryptoManager;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public CryptoConverter(CryptoManager cryptoManager) {
|
||||
public CryptoConverter(CryptoManager cryptoManager, ObjectMapper objectMapper) {
|
||||
this.cryptoManager = cryptoManager;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,22 +82,12 @@ public class CryptoConverter implements
|
||||
CouchbasePersistentProperty property = context.getProperty();
|
||||
|
||||
CustomConversions cnvs = context.getConverter().getConversions();
|
||||
ConversionService svc = context.getConverter().getConversionService();
|
||||
Class<?> type = property.getType();
|
||||
|
||||
String decryptedString = new String(decrypted);
|
||||
if ("null".equals(decryptedString)) {
|
||||
return null;
|
||||
}
|
||||
// TODO - as-is, this never gets ran through ObjectMapper() -
|
||||
// TODO - i.e. @JsonValue etc will not be processed by ObjectMapper
|
||||
|
||||
/* this what we would do if we could use a JsonParser with a beanPropertyTypeRef
|
||||
final JsonParser plaintextParser = p.getCodec().getFactory().createParser(plaintext);
|
||||
plaintextParser.setCodec(p.getCodec());
|
||||
|
||||
return plaintextParser.readValueAs(beanPropertyTypeRef);
|
||||
*/
|
||||
|
||||
if (!cnvs.isSimpleType(type) && !type.isArray()) {
|
||||
JsonObject jo = JsonObject.fromJson(decryptedString);
|
||||
@@ -133,16 +124,20 @@ public class CryptoConverter implements
|
||||
}
|
||||
plainText = ja.toBytes();
|
||||
} else if (cnvs.isSimpleType(sourceType)) { // simpleType
|
||||
String plainString = value != null ? value.toString() : null; // TODO - this will ignore @JsonValue
|
||||
String plainString = value != null ? value.toString() : null;
|
||||
if ((sourceType == String.class || targetType == String.class) || sourceType == Character.class
|
||||
|| sourceType == char.class || Enum.class.isAssignableFrom(sourceType)
|
||||
|| Locale.class.isAssignableFrom(sourceType)) {
|
||||
// TODO use jackson serializer here
|
||||
plainString = "\"" + plainString.replaceAll("\"", "\\\"") + "\"";
|
||||
|| sourceType == char.class || sourceType.isEnum() || Locale.class.isAssignableFrom(sourceType)) {
|
||||
try {
|
||||
plainString = objectMapper.writeValueAsString(plainString);// put quotes around strings
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
plainText = plainString.getBytes(StandardCharsets.UTF_8);
|
||||
} else { // an entity
|
||||
plainText = JsonObject.fromJson(context.read(value).toString().getBytes(StandardCharsets.UTF_8)).toBytes();
|
||||
CouchbaseDocument doc = new CouchbaseDocument();
|
||||
context.getConverter().writeInternalRoot(value, doc, property.getTypeInformation(), false, property, false);
|
||||
plainText = JsonObject.from(doc.export()).toBytes();
|
||||
}
|
||||
return plainText;
|
||||
}
|
||||
|
||||
@@ -443,7 +443,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
|
||||
typeMapper.writeType(type, target);
|
||||
}
|
||||
|
||||
writeInternalRoot(source, target, type, true, null);
|
||||
writeInternalRoot(source, target, type, true, null, true);
|
||||
if (target.getId() == null) {
|
||||
throw new MappingException("An ID property is needed, but not found/could not be generated on this entity.");
|
||||
}
|
||||
@@ -459,7 +459,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void writeInternalRoot(final Object source, CouchbaseDocument target, TypeInformation<?> typeHint,
|
||||
boolean withId, CouchbasePersistentProperty property) {
|
||||
boolean withId, CouchbasePersistentProperty property, boolean processValueConverter) {
|
||||
if (source == null) {
|
||||
return;
|
||||
}
|
||||
@@ -480,7 +480,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
|
||||
}
|
||||
|
||||
CouchbasePersistentEntity<?> entity = mappingContext.getPersistentEntity(source.getClass());
|
||||
writeInternalEntity(source, target, entity, withId, property);
|
||||
writeInternalEntity(source, target, entity, withId, property, processValueConverter);
|
||||
addCustomTypeKeyIfNecessary(typeHint, source, target);
|
||||
}
|
||||
|
||||
@@ -517,7 +517,8 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
|
||||
* @param withId one of the top-level properties is the id for the document
|
||||
*/
|
||||
protected void writeInternalEntity(final Object source, final CouchbaseDocument target,
|
||||
final CouchbasePersistentEntity<?> entity, boolean withId, CouchbasePersistentProperty prop) {
|
||||
final CouchbasePersistentEntity<?> entity, boolean withId, CouchbasePersistentProperty prop,
|
||||
boolean processValueConverter) {
|
||||
if (source == null) {
|
||||
return;
|
||||
}
|
||||
@@ -566,7 +567,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
|
||||
}
|
||||
});
|
||||
|
||||
if (prop != null && conversions.hasValueConverter(prop)) { // whole entity is encrypted
|
||||
if (prop != null && processValueConverter && conversions.hasValueConverter(prop)) { // whole entity is encrypted
|
||||
Map<String, Object> propertyConverted = (Map<String, Object>) conversions.getPropertyValueConversions()
|
||||
.getValueConverter(prop).write(source, new CouchbaseConversionContext(prop, this, accessor));
|
||||
target.setContent(JsonObject.from(propertyConverted));
|
||||
@@ -677,7 +678,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
|
||||
CouchbasePersistentEntity<?> entity = isSubtype(prop.getType(), source.getClass())
|
||||
? mappingContext.getRequiredPersistentEntity(source.getClass())
|
||||
: mappingContext.getRequiredPersistentEntity(prop);
|
||||
writeInternalEntity(source, propertyDoc, entity, false, prop);
|
||||
writeInternalEntity(source, propertyDoc, entity, false, prop, true);
|
||||
target.put(maybeMangle(prop), propertyDoc);
|
||||
}
|
||||
|
||||
@@ -728,7 +729,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
|
||||
prop.getTypeInformation(), prop, getPropertyAccessor(val)));
|
||||
} else {
|
||||
CouchbaseDocument embeddedDoc = new CouchbaseDocument();
|
||||
writeInternalRoot(val, embeddedDoc, prop.getTypeInformation(), false, prop);
|
||||
writeInternalRoot(val, embeddedDoc, prop.getTypeInformation(), false, prop, true);
|
||||
target.put(simpleKey, embeddedDoc);
|
||||
}
|
||||
} else {
|
||||
@@ -772,7 +773,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
|
||||
} else {
|
||||
CouchbaseDocument embeddedDoc = new CouchbaseDocument();
|
||||
writeInternalRoot(element, embeddedDoc,
|
||||
prop != null ? prop.getTypeInformation() : TypeInformation.of(elementType), false, prop);
|
||||
prop != null ? prop.getTypeInformation() : TypeInformation.of(elementType), false, prop, true);
|
||||
target.put(embeddedDoc);
|
||||
}
|
||||
|
||||
@@ -873,9 +874,9 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
|
||||
* @param processValueConverter
|
||||
* @return
|
||||
*/
|
||||
public Object getPotentiallyConvertedSimpleWrite(final CouchbasePersistentProperty value,
|
||||
public Object getPotentiallyConvertedSimpleWrite(final CouchbasePersistentProperty property,
|
||||
ConvertingPropertyAccessor<Object> accessor, boolean processValueConverter) {
|
||||
return convertForWriteIfNeeded(value, accessor, processValueConverter); // can access annotations
|
||||
return convertForWriteIfNeeded(property, accessor, processValueConverter); // can access annotations
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,7 +27,6 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectWriter;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.convert.ReadingConverter;
|
||||
import org.springframework.data.convert.WritingConverter;
|
||||
@@ -200,7 +199,7 @@ public final class OtherConverters {
|
||||
|
||||
/**
|
||||
* Writing converter for Enums. This is registered in
|
||||
* {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions( CryptoManager)}.
|
||||
* {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions( CryptoManager, ObjectMapper)}.
|
||||
* The corresponding reading converters are in {@link IntegerToEnumConverterFactory} and
|
||||
* {@link StringToEnumConverterFactory}
|
||||
*/
|
||||
@@ -220,12 +219,12 @@ public final class OtherConverters {
|
||||
objectMapper.writeValue(generator, source);
|
||||
String s = writer.toString();
|
||||
if (s != null && s.startsWith("\"")) {
|
||||
return objectMapper.readValue(s,String.class);
|
||||
return objectMapper.readValue(s, String.class);
|
||||
}
|
||||
if ("true".equals(s) || "false".equals(s)) {
|
||||
return objectMapper.readValue(s,Boolean.class);
|
||||
return objectMapper.readValue(s, Boolean.class);
|
||||
}
|
||||
return objectMapper.readValue(s,Number.class);
|
||||
return objectMapper.readValue(s, Number.class);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user