-
Notifications
You must be signed in to change notification settings - Fork 194
Fix serialization for field level encryption. #1622
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -160,15 +160,16 @@ public Object convertForWriteIfNeeded(Object inValue) { | |
| 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), | ||
| new CouchbaseList(conversions.getSimpleTypeHolder()), type, null, null); | ||
| } 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; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. writeInternalRoot needs a flag of whether or not to process PropertyValueConverters (specifically the CryptoConverter) so it doesn't recursively encrypt such an annotated property. |
||
| } | ||
| return value; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,7 +31,7 @@ | |
| import com.couchbase.client.core.encryption.CryptoManager; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Addition of ObjectMapper. |
||
| 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 @@ | |
| */ | ||
| 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 <DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV, | |
|
|
||
| // 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); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,15 +20,12 @@ | |
| 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.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 @@ private Object coerceToValueRead(byte[] decrypted, CouchbaseConversionContext co | |
| 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); | ||
| */ | ||
|
|
||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The deleted comment is a bit misleading. The conversions do take place in context.getConverter.read() and getPotentiallyConvertedSimpleRead() below. |
||
| if (!cnvs.isSimpleType(type) && !type.isArray()) { | ||
| JsonObject jo = JsonObject.fromJson(decryptedString); | ||
|
|
@@ -133,16 +124,20 @@ private byte[] coerceToBytesWrite(CouchbasePersistentProperty property, Converti | |
| } | ||
| 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(); | ||
| } | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The old code relied on the entity (Airport, Address etc) produced by context.read(value) have a nice toString() method that gives the json representation of the object. It's a bad assumption. |
||
| return plainText; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -443,7 +443,7 @@ public void write(final Object source, final CouchbaseDocument target) { | |
| 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 void write(final Object source, final CouchbaseDocument target) { | |
| */ | ||
| @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 void writeInternalRoot(final Object source, CouchbaseDocument target, Typ | |
| } | ||
|
|
||
| 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 @@ private String convertToString(Object propertyObj) { | |
| * @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 void doWithAssociation(final Association<CouchbasePersistentProperty> ass | |
| } | ||
| }); | ||
|
|
||
| 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() | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will prevent recursively encrypting an annotated property. |
||
| .getValueConverter(prop).write(source, new CouchbaseConversionContext(prop, this, accessor)); | ||
| target.setContent(JsonObject.from(propertyConverted)); | ||
|
|
@@ -677,7 +678,7 @@ protected void writePropertyInternal(final Object source, final CouchbaseDocumen | |
| 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 @@ private CouchbaseDocument writeMapInternal(final Map<? extends Object, Object> s | |
| 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 CouchbaseList writeCollectionInternal(final Collection<?> source, final C | |
| } 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 Object getPotentiallyConvertedSimpleWrite(final CouchbasePersistentProper | |
| * @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 | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,6 @@ | |
| */ | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add one of our @jsonvalue Enums to Address which gets used in the FLE tests and elsewhere. |
||
| package org.springframework.data.couchbase.domain; | ||
|
|
||
| import com.couchbase.client.java.encryption.annotation.Encrypted; | ||
| import org.springframework.data.couchbase.core.mapping.Document; | ||
|
|
||
| @Document | ||
|
|
@@ -26,6 +25,7 @@ public class Address extends ComparableEntity { | |
| // for N1qlJoin | ||
| private String id; | ||
| private String parentId; | ||
| private ETurbulenceCategory turbulence; | ||
|
|
||
| public Address() {} | ||
|
|
||
|
|
@@ -45,6 +45,13 @@ public void setCity(String city) { | |
| this.city = city; | ||
| } | ||
|
|
||
| public ETurbulenceCategory getTurbulence() { | ||
| return turbulence; | ||
| } | ||
|
|
||
| public void setTurbulence(ETurbulenceCategory turbulence) { | ||
| this.turbulence = turbulence; | ||
| } | ||
| public String getParentId() { | ||
| return parentId; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,7 @@ | |
| * @author Michael Reiche | ||
| */ | ||
| public enum ETurbulenceCategory { | ||
| T10("10%"), T20("20%"), T30("30%"); | ||
| T10("\"10%"), T20("\"20%"), T30("\"30%"); | ||
|
|
||
| private final String code; | ||
|
|
||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a quote to the content of the Enum serialization. |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CryptoConverter now needs the ObjectMapper.