You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by ab...@apache.org on 2019/06/10 09:58:20 UTC

[cayenne] 01/02: CAY-2584 Crypto: can't use ColumnSelect with encrypted columns

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 <st...@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