This is an automated email from the ASF dual-hosted git repository. abulatski pushed a commit to branch STABLE-4.1 in repository https://gitbox.apache.org/repos/asf/cayenne.git
commit 5c9b194d0ca7c11b56fe5f387947cfc74efdb835 Author: Nikita Timofeev <stari...@gmail.com> AuthorDate: Thu Jun 6 18:09:47 2019 +0300 CAY-2584 Crypto: can't use ColumnSelect with encrypted columns --- RELEASE-NOTES.txt | 1 + .../reader/CryptoRowReaderFactoryDecorator.java | 155 ++++++++++++++++----- .../apache/cayenne/crypto/Runtime_AES128_IT.java | 100 +++++++++++++ .../cayenne/access/jdbc/ColumnDescriptor.java | 4 + .../jdbc/reader/DefaultRowReaderFactory.java | 10 +- .../translator/select/DefaultSelectTranslator.java | 26 ++-- 6 files changed, 246 insertions(+), 50 deletions(-) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index d0ac1c6..144aa21 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -16,6 +16,7 @@ Bug Fixes: CAY-2573 DI field injection is triggered when creating sql Driver CAY-2582 Double insert of manyToMany relationship mapped to Set +CAY-2584 Crypto: can't use ColumnSelect with encrypted columns ---------------------------------- Release: 4.1.B2 diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/reader/CryptoRowReaderFactoryDecorator.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/reader/CryptoRowReaderFactoryDecorator.java index ab2c08d..e60600a 100644 --- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/reader/CryptoRowReaderFactoryDecorator.java +++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/reader/CryptoRowReaderFactoryDecorator.java @@ -20,6 +20,7 @@ package org.apache.cayenne.crypto.reader; import org.apache.cayenne.access.jdbc.ColumnDescriptor; import org.apache.cayenne.access.jdbc.RowDescriptor; +import org.apache.cayenne.access.jdbc.reader.DefaultRowReaderFactory; import org.apache.cayenne.access.jdbc.reader.RowReader; import org.apache.cayenne.access.jdbc.reader.RowReaderFactory; import org.apache.cayenne.access.types.ExtendedType; @@ -27,68 +28,70 @@ import org.apache.cayenne.access.types.ExtendedTypeMap; import org.apache.cayenne.crypto.map.ColumnMapper; import org.apache.cayenne.crypto.transformer.MapTransformer; import org.apache.cayenne.crypto.transformer.TransformerFactory; +import org.apache.cayenne.crypto.transformer.bytes.BytesDecryptor; +import org.apache.cayenne.crypto.transformer.bytes.BytesTransformerFactory; +import org.apache.cayenne.crypto.transformer.value.ValueDecryptor; +import org.apache.cayenne.crypto.transformer.value.ValueTransformerFactory; import org.apache.cayenne.dba.DbAdapter; import org.apache.cayenne.dba.TypesMapping; import org.apache.cayenne.di.Inject; import org.apache.cayenne.map.DbAttribute; import org.apache.cayenne.map.ObjAttribute; +import org.apache.cayenne.query.EntityResultSegment; import org.apache.cayenne.query.QueryMetadata; +import org.apache.cayenne.query.ScalarResultSegment; import java.sql.ResultSet; import java.util.Map; -public class CryptoRowReaderFactoryDecorator implements RowReaderFactory { +public class CryptoRowReaderFactoryDecorator extends DefaultRowReaderFactory { - private RowReaderFactory delegate; private TransformerFactory transformerFactory; private ColumnMapper columnMapper; + private BytesTransformerFactory bytesTransformerFactory; + private ValueTransformerFactory valueTransformerFactory; public CryptoRowReaderFactoryDecorator(@Inject RowReaderFactory delegate, @Inject TransformerFactory transformerFactory, - @Inject ColumnMapper columnMapper) { - this.delegate = delegate; + @Inject ColumnMapper columnMapper, + @Inject BytesTransformerFactory bytesTransformerFactory, + @Inject ValueTransformerFactory valueTransformerFactory) { this.transformerFactory = transformerFactory; this.columnMapper = columnMapper; + this.bytesTransformerFactory = bytesTransformerFactory; + this.valueTransformerFactory = valueTransformerFactory; } @Override - public RowReader<?> rowReader(final RowDescriptor descriptor, QueryMetadata queryMetadata, DbAdapter adapter, + public RowReader<?> rowReader(RowDescriptor descriptor, QueryMetadata queryMetadata, DbAdapter adapter, Map<ObjAttribute, ColumnDescriptor> attributeOverrides) { + RowDescriptor encryptedRowDescriptor = encryptedRowDescriptor(descriptor, adapter.getExtendedTypes()); + return super.rowReader(encryptedRowDescriptor, queryMetadata, adapter, attributeOverrides); + } - final RowReader<?> delegateReader = delegate.rowReader(encryptedRowDescriptor(descriptor, adapter.getExtendedTypes()), - queryMetadata, - adapter, - attributeOverrides); - - return new RowReader<Object>() { - - private boolean decryptorCompiled; - private MapTransformer decryptor; - - private void ensureDecryptorCompiled(Object row) { - if (!decryptorCompiled) { - decryptor = transformerFactory.decryptor(descriptor.getColumns(), row); - decryptorCompiled = true; - } - } - - @Override - public Object readRow(ResultSet resultSet) { - Object row = delegateReader.readRow(resultSet); - - ensureDecryptorCompiled(row); - - if (decryptor != null) { - - @SuppressWarnings({"unchecked", "rawtypes"}) - Map<String, Object> map = (Map) row; + @Override + protected RowReader<?> createScalarRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata, + ScalarResultSegment segment) { + RowReader<?> scalarRowReader = super + .createScalarRowReader(descriptor, queryMetadata, segment); + return new DecoratedScalarRowReader(descriptor.getColumns()[segment.getColumnOffset()], scalarRowReader); + } - decryptor.transform(map); - } + @Override + protected RowReader<?> createEntityRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata, + EntityResultSegment resultMetadata, + PostprocessorFactory postProcessorFactory) { + RowReader<?> entityRowReader = super + .createEntityRowReader(descriptor, queryMetadata, resultMetadata, postProcessorFactory); + return new DecoratedFullRowReader(descriptor, entityRowReader); + } - return row; - } - }; + @Override + protected RowReader<?> createFullRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata, + PostprocessorFactory postProcessorFactory) { + RowReader<?> fullRowReader = super + .createFullRowReader(descriptor, queryMetadata, postProcessorFactory); + return new DecoratedFullRowReader(descriptor, fullRowReader); } protected RowDescriptor encryptedRowDescriptor(RowDescriptor descriptor, ExtendedTypeMap typeMap) { @@ -121,6 +124,82 @@ public class CryptoRowReaderFactoryDecorator implements RowReaderFactory { encryptedConverters[i] = t; } - return new RowDescriptor(originalColumns, encryptedConverters); + return new DecoratedRowDescriptor(descriptor, originalColumns, encryptedConverters); + } + + private static class DecoratedRowDescriptor extends RowDescriptor { + + private final RowDescriptor original; + + DecoratedRowDescriptor(RowDescriptor rowDescriptor, ColumnDescriptor[] columns, ExtendedType[] converters) { + this.original = rowDescriptor; + this.columns = columns; + this.converters = converters; + } + + public RowDescriptor unwrap() { + return original; + } + } + + private class DecoratedScalarRowReader implements RowReader<Object> { + private final RowReader<?> delegateReader; + private final ValueDecryptor valueDecryptor; + private final BytesDecryptor bytesDecryptor; + + DecoratedScalarRowReader(ColumnDescriptor descriptor, RowReader<?> delegateReader) { + this.delegateReader = delegateReader; + if(descriptor.getAttribute() != null && columnMapper.isEncrypted(descriptor.getAttribute())) { + this.valueDecryptor = valueTransformerFactory.decryptor(descriptor.getAttribute()); + this.bytesDecryptor = bytesTransformerFactory.decryptor(); + } else { + this.valueDecryptor = null; + this.bytesDecryptor = null; + } + } + + @Override + public Object readRow(ResultSet resultSet) { + Object value = delegateReader.readRow(resultSet); + if(valueDecryptor == null) { + return value; + } + return valueDecryptor.decrypt(bytesDecryptor, value); + } + } + + private class DecoratedFullRowReader implements RowReader<Object> { + + private final RowDescriptor descriptor; + private final RowReader<?> delegateReader; + private boolean decryptorCompiled; + private MapTransformer decryptor; + + DecoratedFullRowReader(RowDescriptor descriptor, RowReader<?> delegateReader) { + this.descriptor = descriptor; + this.delegateReader = delegateReader; + } + + private void ensureDecryptorCompiled(Object row) { + if (!decryptorCompiled) { + decryptor = transformerFactory.decryptor(descriptor.getColumns(), row); + decryptorCompiled = true; + } + } + + @Override + public Object readRow(ResultSet resultSet) { + Object row = delegateReader.readRow(resultSet); + + ensureDecryptorCompiled(row); + + if (decryptor != null) { + @SuppressWarnings("unchecked") + Map<String, Object> map = (Map<String, Object>) row; + decryptor.transform(map); + } + + return row; + } } } diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_IT.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_IT.java index 1e4b4bc..b6f75d3 100644 --- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_IT.java +++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_IT.java @@ -23,6 +23,8 @@ import org.apache.cayenne.crypto.db.Table1; import org.apache.cayenne.crypto.db.Table2; import org.apache.cayenne.crypto.transformer.value.IntegerConverter; import org.apache.cayenne.crypto.unit.CryptoUnitUtils; +import org.apache.cayenne.exp.Property; +import org.apache.cayenne.query.ObjectSelect; import org.apache.cayenne.query.SelectQuery; import org.junit.Before; import org.junit.Test; @@ -156,4 +158,102 @@ public class Runtime_AES128_IT extends Runtime_AES128_Base { assertEquals(61, result.get(0).getCryptoInt()); } + @Test + public void test_ColumnQueryObject() { + + ObjectContext context = runtime.newContext(); + + Table1 t1 = context.newObject(Table1.class); + t1.setCryptoInt(1); + t1.setCryptoString("test"); + context.commitChanges(); + + List<Table1> result = ObjectSelect + .columnQuery(Table1.class, Property.createSelf(Table1.class)) + .select(context); + + assertEquals(1, result.size()); + assertEquals(1, result.get(0).getCryptoInt()); + assertEquals("test", result.get(0).getCryptoString()); + } + + @Test + public void test_ColumnQueryObjectWithPlainScalar() { + + ObjectContext context = runtime.newContext(); + + Table1 t1 = context.newObject(Table1.class); + t1.setCryptoInt(1); + t1.setPlainInt(2); + t1.setCryptoString("test"); + context.commitChanges(); + + List<Object[]> result = ObjectSelect + .columnQuery(Table1.class, Property.createSelf(Table1.class), Table1.PLAIN_INT) + .select(context); + + assertEquals(1, result.size()); + assertEquals(1, ((Table1)result.get(0)[0]).getCryptoInt()); + assertEquals("test", ((Table1)result.get(0)[0]).getCryptoString()); + assertEquals(2, result.get(0)[1]); + } + + @Test + public void test_ColumnQueryObjectWithEncryptedScalar() { + + ObjectContext context = runtime.newContext(); + + Table1 t1 = context.newObject(Table1.class); + t1.setCryptoInt(1); + t1.setPlainInt(2); + t1.setCryptoString("test"); + context.commitChanges(); + + List<Object[]> result = ObjectSelect + .columnQuery(Table1.class, Property.createSelf(Table1.class), Table1.CRYPTO_INT) + .select(context); + + assertEquals(1, result.size()); + assertEquals(1, ((Table1)result.get(0)[0]).getCryptoInt()); + assertEquals("test", ((Table1)result.get(0)[0]).getCryptoString()); + assertEquals(1, result.get(0)[1]); + } + + @Test + public void test_ColumnQuerySingleScalar() { + ObjectContext context = runtime.newContext(); + + Table1 t1 = context.newObject(Table1.class); + t1.setCryptoInt(1); + t1.setCryptoString("test"); + context.commitChanges(); + + List<String> result = ObjectSelect + .columnQuery(Table1.class, Table1.CRYPTO_STRING) + .select(context); + + assertEquals(1, result.size()); + assertEquals("test", result.get(0)); + } + + @Test + public void test_ColumnQueryMultipleScalars() { + ObjectContext context = runtime.newContext(); + + Table1 t1 = context.newObject(Table1.class); + t1.setCryptoInt(1); + t1.setCryptoString("test"); + t1.setPlainInt(2); + context.commitChanges(); + + List<Object[]> result = ObjectSelect + .columnQuery(Table1.class, Table1.CRYPTO_STRING, Table1.CRYPTO_INT, Table1.PLAIN_INT) + .select(context); + + assertEquals(1, result.size()); + assertEquals("test", result.get(0)[0]); + assertEquals(1, result.get(0)[1]); + assertEquals(2, result.get(0)[2]); + } + } diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ColumnDescriptor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ColumnDescriptor.java index 632f5e3..cd8d1fc 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ColumnDescriptor.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ColumnDescriptor.java @@ -149,6 +149,10 @@ public class ColumnDescriptor { return name; } + public void setAttribute(DbAttribute attribute) { + this.attribute = attribute; + } + /** * Returns a DbAttribute for this column. Since columns descriptors can be * initialized in a context where a DbAttribite is unknown, this method may diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java index 36c7027..507cbbb 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java @@ -70,7 +70,7 @@ public class DefaultRowReaderFactory implements RowReaderFactory { return createEntityRowReader(descriptor, queryMetadata, (EntityResultSegment) segment, postProcessorFactory); } else { - return new ScalarRowReader<>(descriptor, (ScalarResultSegment) segment); + return createScalarRowReader(descriptor, queryMetadata, (ScalarResultSegment) segment); } } else { CompoundRowReader reader = new CompoundRowReader(resultWidth); @@ -84,7 +84,7 @@ public class DefaultRowReaderFactory implements RowReaderFactory { createEntityRowReader(descriptor, queryMetadata, (EntityResultSegment) segment, postProcessorFactory)); } else { - reader.addRowReader(i, new ScalarRowReader<>(descriptor, (ScalarResultSegment) segment)); + reader.addRowReader(i, createScalarRowReader(descriptor, queryMetadata, (ScalarResultSegment) segment)); } } @@ -92,7 +92,11 @@ public class DefaultRowReaderFactory implements RowReaderFactory { } } - private RowReader<?> createEntityRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata, + protected RowReader<?> createScalarRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata, ScalarResultSegment segment) { + return new ScalarRowReader<Object>(descriptor, segment); + } + + protected RowReader<?> createEntityRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata, EntityResultSegment resultMetadata, PostprocessorFactory postProcessorFactory) { if (queryMetadata.getPageSize() > 0) { diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java index 4a2c591..2eefd81 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java @@ -437,7 +437,8 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra for(Property<?> property : query.getColumns()) { joinTableAliasForProperty[0] = null; - int expressionType = property.getExpression().getType(); + Expression propertyExpression = property.getExpression(); + int expressionType = propertyExpression.getType(); // forbid direct selection of toMany relationships columns if(property.getType() != null && (expressionType == Expression.OBJ_PATH || expressionType == Expression.DB_PATH) @@ -456,7 +457,7 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra // Qualifier Translator in case of Object Columns have side effect - // it will create required joins, that we catch with listener above. // And we force created join alias for all columns of Object we select. - qualifierTranslator.setQualifier(property.getExpression()); + qualifierTranslator.setQualifier(propertyExpression); qualifierTranslator.setForceJoinForRelations(objectProperty); StringBuilder builder = qualifierTranslator.appendPart(new StringBuilder()); @@ -481,13 +482,15 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra builder.append(" AS ").append(alias); } - int type = getJdbcTypeForProperty(property); + DbAttribute attribute = getAttributeForProperty(propertyExpression); + int type = attribute == null ? getJdbcTypeForProperty(property) : attribute.getType(); ColumnDescriptor descriptor; if(property.getType() != null) { descriptor = new ColumnDescriptor(builder.toString(), type, property.getType().getCanonicalName()); } else { descriptor = new ColumnDescriptor(builder.toString(), type); } + descriptor.setAttribute(attribute); descriptor.setDataRowKey(alias); descriptor.setIsExpression(true); columns.add(descriptor); @@ -508,18 +511,18 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra return columns; } - private int getJdbcTypeForProperty(Property<?> property) { - int expressionType = property.getExpression().getType(); + private DbAttribute getAttributeForProperty(Expression propertyExpression) { + int expressionType = propertyExpression.getType(); if(expressionType == Expression.OBJ_PATH) { // Scan obj path, stop as soon as DbAttribute found for (PathComponent<ObjAttribute, ObjRelationship> component : - getQueryMetadata().getObjEntity().resolvePath(property.getExpression(), getPathAliases())) { + getQueryMetadata().getObjEntity().resolvePath(propertyExpression, getPathAliases())) { if(component.getAttribute() != null) { Iterator<CayenneMapEntry> dbPathIterator = component.getAttribute().getDbPathIterator(); while (dbPathIterator.hasNext()) { Object pathPart = dbPathIterator.next(); if (pathPart instanceof DbAttribute) { - return ((DbAttribute) pathPart).getType(); + return ((DbAttribute) pathPart); } } } @@ -527,12 +530,17 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra } else if(expressionType == Expression.DB_PATH) { // Scan db path, stop as soon as DbAttribute found for (PathComponent<DbAttribute, DbRelationship> component : - getQueryMetadata().getDbEntity().resolvePath(property.getExpression(), getPathAliases())) { + getQueryMetadata().getDbEntity().resolvePath(propertyExpression, getPathAliases())) { if(component.getAttribute() != null) { - return component.getAttribute().getType(); + return component.getAttribute(); } } } + + return null; + } + + private int getJdbcTypeForProperty(Property<?> property) { // NOTE: If no attribute found or expression have some other type // return JDBC type based on Java type of the property. // This can lead to incorrect behavior in case we deal with some custom type