diff --git a/encryption/src/main/java/org/apache/solr/encryption/EncryptionDirectory.java b/encryption/src/main/java/org/apache/solr/encryption/EncryptionDirectory.java index fe4cd26..cb364b2 100644 --- a/encryption/src/main/java/org/apache/solr/encryption/EncryptionDirectory.java +++ b/encryption/src/main/java/org/apache/solr/encryption/EncryptionDirectory.java @@ -90,6 +90,8 @@ public class EncryptionDirectory extends FilterDirectory { protected final KeySupplier keySupplier; + protected final EncryptionListener encryptionListener; + /** Cache of the latest commit user data. */ protected volatile CommitUserData commitUserData; @@ -100,14 +102,20 @@ public class EncryptionDirectory extends FilterDirectory { * Creates an {@link EncryptionDirectory} which wraps a delegate {@link Directory} to encrypt/decrypt * files on the fly. * - * @param encrypterFactory creates {@link AesCtrEncrypter}. - * @param keySupplier provides the key secrets and determines which files should be encrypted. + * @param encrypterFactory creates {@link AesCtrEncrypter}. + * @param keySupplier provides the key secrets and determines which files should be encrypted. + * @param encryptionListener notified when the index is encrypted. */ - public EncryptionDirectory(Directory delegate, AesCtrEncrypterFactory encrypterFactory, KeySupplier keySupplier) + public EncryptionDirectory( + Directory delegate, + AesCtrEncrypterFactory encrypterFactory, + KeySupplier keySupplier, + EncryptionListener encryptionListener) throws IOException { super(delegate); this.encrypterFactory = encrypterFactory; this.keySupplier = keySupplier; + this.encryptionListener = encryptionListener; commitUserData = readLatestCommitUserData(); } @@ -148,6 +156,7 @@ protected IndexOutput maybeWrapOutput(IndexOutput indexOutput) throws IOExceptio try { String keyRef = getActiveKeyRefFromCommit(getLatestCommitData().data); if (keyRef != null) { + encryptionListener.onEncryption(); // Get the key secret first. If it fails, we do not write anything. byte[] keySecret = getKeySecret(keyRef); // The IndexOutput has to be wrapped to be encrypted with the key. @@ -173,10 +182,17 @@ private static void writeEncryptionHeader(String keyRef, DataOutput out) throws /** * Forces this {@link EncryptionDirectory} to read the user data of the latest commit, to refresh its cache. */ - public void forceReadCommitUserData() { + public void clearCachedCommitUserData() { shouldReadCommitUserData = true; } + /** + * Clears the cached encryption status for logs. + */ + public void clearCachedEncryptionStatus() { + encryptionListener.clearEncryptionStatus(); + } + /** * Gets the user data from the latest commit, potentially reading the latest commit if the cache is stale. */ @@ -255,6 +271,7 @@ public IndexInput openInput(String fileName, IOContext context) throws IOExcepti try { String keyRef = getKeyRefForReading(indexInput); if (keyRef != null) { + encryptionListener.onEncryption(); // The IndexInput has to be wrapped to be decrypted with the key. indexInput = new DecryptingIndexInput(indexInput, getKeySecret(keyRef), encrypterFactory); } @@ -371,4 +388,25 @@ protected CommitUserData(String segmentFileName, Map data) { keyCookies = getKeyCookiesFromCommit(data); } } + + /** + * Notified when the index is encrypted. + */ + public interface EncryptionListener { + + EncryptionListener NO_LISTENER = new EncryptionListener() { + + @Override + public void onEncryption() { + } + + @Override + public void clearEncryptionStatus() { + } + }; + + void onEncryption(); + + void clearEncryptionStatus(); + } } diff --git a/encryption/src/main/java/org/apache/solr/encryption/EncryptionDirectoryFactory.java b/encryption/src/main/java/org/apache/solr/encryption/EncryptionDirectoryFactory.java index 4232723..5d12933 100644 --- a/encryption/src/main/java/org/apache/solr/encryption/EncryptionDirectoryFactory.java +++ b/encryption/src/main/java/org/apache/solr/encryption/EncryptionDirectoryFactory.java @@ -24,6 +24,7 @@ import org.apache.solr.core.DirectoryFactory; import org.apache.solr.core.MMapDirectoryFactory; import org.apache.solr.core.SolrCore; +import org.apache.solr.encryption.EncryptionDirectory.EncryptionListener; import org.apache.solr.encryption.crypto.AesCtrEncrypterFactory; import org.apache.solr.encryption.crypto.CipherAesCtrEncrypter; @@ -74,9 +75,20 @@ public class EncryptionDirectoryFactory extends MMapDirectoryFactory { */ static final String PROPERTY_INNER_ENCRYPTION_DIRECTORY_FACTORY = "innerEncryptionDirectoryFactory"; + private final EncryptionListener encryptionListener = new EncryptionListener() { + @Override + public void onEncryption() { + indexEncrypted = true; + } + @Override + public void clearEncryptionStatus() { + indexEncrypted = false; + } + }; private KeySupplier keySupplier; private AesCtrEncrypterFactory encrypterFactory; private InnerFactory innerFactory; + private volatile boolean indexEncrypted; public EncryptionDirectoryFactory() {} @@ -145,6 +157,15 @@ public KeySupplier getKeySupplier() { return keySupplier; } + /** + * Returns whether the index is encrypted. + * This flag is set when the {@link EncryptionDirectory} opens an output/input stream that + * requires encryption. + */ + public boolean isIndexEncrypted() { + return indexEncrypted; + } + public static EncryptionDirectoryFactory getFactory(SolrCore core) { if (!(core.getDirectoryFactory() instanceof EncryptionDirectoryFactory)) { throw new SolrException(SolrException.ErrorCode.SERVICE_UNAVAILABLE, @@ -157,7 +178,7 @@ public static EncryptionDirectoryFactory getFactory(SolrCore core) { @Override protected Directory create(String path, LockFactory lockFactory, DirContext dirContext) throws IOException { - return innerFactory.create(super.create(path, lockFactory, dirContext), getEncrypterFactory(), getKeySupplier()); + return innerFactory.create(super.create(path, lockFactory, dirContext), getEncrypterFactory(), getKeySupplier(), encryptionListener); } @Override @@ -178,7 +199,11 @@ public void close() throws IOException { * Visible for tests only - Inner factory that creates {@link EncryptionDirectory} instances. */ interface InnerFactory { - EncryptionDirectory create(Directory delegate, AesCtrEncrypterFactory encrypterFactory, KeySupplier keySupplier) + EncryptionDirectory create( + Directory delegate, + AesCtrEncrypterFactory encrypterFactory, + KeySupplier keySupplier, + EncryptionListener encryptionListener) throws IOException; } } diff --git a/encryption/src/main/java/org/apache/solr/encryption/EncryptionMergePolicy.java b/encryption/src/main/java/org/apache/solr/encryption/EncryptionMergePolicy.java index e915c31..75287c3 100644 --- a/encryption/src/main/java/org/apache/solr/encryption/EncryptionMergePolicy.java +++ b/encryption/src/main/java/org/apache/solr/encryption/EncryptionMergePolicy.java @@ -74,7 +74,12 @@ public MergeSpecification findForcedMerges(SegmentInfos segmentInfos, // Make sure the EncryptionDirectory does not keep its cache for the commit user data. // It must read the latest commit user data to get the latest active key, so below the // segments with old key (to re-encrypt) are always accurate. - encryptionDir.forceReadCommitUserData(); + encryptionDir.clearCachedCommitUserData(); + // Also clear the encryption status for logs if the index becomes cleartext after rewriting + // the segments. + if (activeKeyId == null) { + encryptionDir.clearCachedEncryptionStatus(); + } List segmentsWithOldKeyId = encryptionDir.getSegmentsWithOldKeyId(segmentInfos, activeKeyId); if (segmentsWithOldKeyId.isEmpty()) { return null; diff --git a/encryption/src/main/java/org/apache/solr/encryption/EncryptionUpdateLog.java b/encryption/src/main/java/org/apache/solr/encryption/EncryptionUpdateLog.java index db3a706..b67b0a4 100644 --- a/encryption/src/main/java/org/apache/solr/encryption/EncryptionUpdateLog.java +++ b/encryption/src/main/java/org/apache/solr/encryption/EncryptionUpdateLog.java @@ -98,7 +98,7 @@ public synchronized boolean encryptLogs() throws IOException { String activeKeyRef; EncryptionDirectory directory = directorySupplier.get(); try { - directory.forceReadCommitUserData(); + directory.clearCachedCommitUserData(); latestCommitData = directory.getLatestCommitData().data; activeKeyRef = getActiveKeyRefFromCommit(latestCommitData); for (TransactionLog log : logs) { diff --git a/encryption/src/test/java/org/apache/solr/encryption/EncryptionBackupRepositoryTest.java b/encryption/src/test/java/org/apache/solr/encryption/EncryptionBackupRepositoryTest.java index 9f36a19..42ac87f 100644 --- a/encryption/src/test/java/org/apache/solr/encryption/EncryptionBackupRepositoryTest.java +++ b/encryption/src/test/java/org/apache/solr/encryption/EncryptionBackupRepositoryTest.java @@ -182,7 +182,11 @@ public void testCanDisableChecksumVerification() throws Exception { AesCtrEncrypterFactory encrypterFactory = random().nextBoolean() ? CipherAesCtrEncrypter.FACTORY : LightAesCtrEncrypter.FACTORY; KeySupplier keySupplier = new TestingKeySupplier.Factory().create(); try (Directory fsSourceDir = FSDirectory.open(sourcePath); - Directory encSourceDir = new TestEncryptionDirectory(fsSourceDir, encrypterFactory, keySupplier); + Directory encSourceDir = new TestEncryptionDirectory( + fsSourceDir, + encrypterFactory, + keySupplier, + EncryptionDirectory.EncryptionListener.NO_LISTENER); Directory destinationDir = FSDirectory.open(createTempDir().toAbsolutePath())) { String fileName = "source-file"; String content = "content"; @@ -331,9 +335,13 @@ public void setDelegate(BackupRepository delegate) { */ private static class TestEncryptionDirectory extends EncryptionDirectory { - TestEncryptionDirectory(Directory delegate, AesCtrEncrypterFactory encrypterFactory, KeySupplier keySupplier) - throws IOException { - super(delegate, encrypterFactory, keySupplier); + TestEncryptionDirectory( + Directory delegate, + AesCtrEncrypterFactory encrypterFactory, + KeySupplier keySupplier, + EncryptionListener encryptionListener) + throws IOException { + super(delegate, encrypterFactory, keySupplier, encryptionListener); } @Override diff --git a/encryption/src/test/java/org/apache/solr/encryption/EncryptionDirectoryTest.java b/encryption/src/test/java/org/apache/solr/encryption/EncryptionDirectoryTest.java index f278701..709eb0c 100644 --- a/encryption/src/test/java/org/apache/solr/encryption/EncryptionDirectoryTest.java +++ b/encryption/src/test/java/org/apache/solr/encryption/EncryptionDirectoryTest.java @@ -262,8 +262,10 @@ static void setKeysInCommitUserData(String... keyIds) throws IOException { @Override public EncryptionDirectory create(Directory delegate, AesCtrEncrypterFactory encrypterFactory, - KeySupplier keySupplier) throws IOException { - MockEncryptionDirectory mockDir = new MockEncryptionDirectory(delegate, encrypterFactory, keySupplier); + KeySupplier keySupplier, + EncryptionDirectory.EncryptionListener encryptionListener) + throws IOException { + MockEncryptionDirectory mockDir = new MockEncryptionDirectory(delegate, encrypterFactory, keySupplier, encryptionListener); mockDirs.add(mockDir); return mockDir; } @@ -273,9 +275,13 @@ private static class MockEncryptionDirectory extends EncryptionDirectory { final KeySupplier keySupplier; - MockEncryptionDirectory(Directory delegate, AesCtrEncrypterFactory encrypterFactory, KeySupplier keySupplier) + MockEncryptionDirectory( + Directory delegate, + AesCtrEncrypterFactory encrypterFactory, + KeySupplier keySupplier, + EncryptionListener encryptionListener) throws IOException { - super(delegate, encrypterFactory, keySupplier); + super(delegate, encrypterFactory, keySupplier, encryptionListener); this.keySupplier = keySupplier; } diff --git a/encryption/src/test/java/org/apache/solr/encryption/EncryptionMergePolicyFactoryTest.java b/encryption/src/test/java/org/apache/solr/encryption/EncryptionMergePolicyFactoryTest.java index 78d2ff2..7d517a2 100644 --- a/encryption/src/test/java/org/apache/solr/encryption/EncryptionMergePolicyFactoryTest.java +++ b/encryption/src/test/java/org/apache/solr/encryption/EncryptionMergePolicyFactoryTest.java @@ -30,6 +30,7 @@ import org.apache.lucene.tests.util.LuceneTestCase; import org.apache.solr.core.SolrResourceLoader; import org.apache.solr.encryption.crypto.LightAesCtrEncrypter; +import org.apache.solr.encryption.EncryptionDirectory.EncryptionListener; import org.apache.solr.index.MergePolicyFactoryArgs; import org.apache.solr.index.TieredMergePolicyFactory; import org.junit.Test; @@ -38,6 +39,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import static org.apache.solr.encryption.TestingEncryptionRequestHandler.MOCK_COOKIE_PARAMS; @@ -80,9 +82,22 @@ private MergePolicy createMergePolicy() { @Test public void testSegmentReencryption() throws Exception { KeySupplier keySupplier = new TestingKeySupplier.Factory().create(); - try (Directory dir = new EncryptionDirectory(new MMapDirectory(createTempDir(), FSLockFactory.getDefault()), - LightAesCtrEncrypter.FACTORY, - keySupplier)) { + AtomicBoolean indexEncrypted = new AtomicBoolean(false); + EncryptionListener encryptionListener = new EncryptionListener() { + @Override + public void onEncryption() { + indexEncrypted.set(true); + } + @Override + public void clearEncryptionStatus() { + indexEncrypted.set(false); + } + }; + try (Directory dir = new EncryptionDirectory( + new MMapDirectory(createTempDir(), FSLockFactory.getDefault()), + LightAesCtrEncrypter.FACTORY, + keySupplier, + encryptionListener)) { IndexWriterConfig iwc = new IndexWriterConfig(new WhitespaceAnalyzer()); iwc.setMergeScheduler(new ConcurrentMergeScheduler()); iwc.setMergePolicy(createMergePolicy()); @@ -125,6 +140,7 @@ public void testSegmentReencryption() throws Exception { assertTrue(segmentNames.isEmpty()); } } + assertTrue(indexEncrypted.get()); } private void commit(IndexWriter writer, KeySupplier keySupplier, String... keyIds) throws IOException { diff --git a/encryption/src/test/java/org/apache/solr/encryption/EncryptionMergePolicyTest.java b/encryption/src/test/java/org/apache/solr/encryption/EncryptionMergePolicyTest.java index 88cd3b2..0e61184 100644 --- a/encryption/src/test/java/org/apache/solr/encryption/EncryptionMergePolicyTest.java +++ b/encryption/src/test/java/org/apache/solr/encryption/EncryptionMergePolicyTest.java @@ -45,213 +45,219 @@ */ public class EncryptionMergePolicyTest extends LuceneTestCase { - private final Path tempDir = createTempDir(); - private final KeySupplier keySupplier = new TestingKeySupplier.Factory().create(); - private final AesCtrEncrypterFactory encrypterFactory = LightAesCtrEncrypter.FACTORY; - - @Test - public void testNoReencryptionWhenNoKeyChange() throws Exception { - try (Directory dir = new EncryptionDirectory( - new MMapDirectory(tempDir, FSLockFactory.getDefault()), - encrypterFactory, - keySupplier)) { - - IndexWriterConfig iwc = new IndexWriterConfig(new WhitespaceAnalyzer()); - iwc.setMergePolicy(createMergePolicy()); - - try (IndexWriter writer = new IndexWriter(dir, iwc)) { - // Create initial segments with KEY_ID_1. - commit(writer, keySupplier, KEY_ID_1); - int numSegments = 3; - for (int i = 0; i < numSegments; ++i) { - writer.addDocument(new Document()); - commit(writer, keySupplier, KEY_ID_1); - } - - Set initialSegmentNames = readSegmentNames(dir); - assertEquals(numSegments, initialSegmentNames.size()); - - // Force merge with MAX_VALUE should not trigger reencryption. - writer.forceMerge(Integer.MAX_VALUE); - commit(writer, keySupplier, KEY_ID_1); - - // Verify segments remain unchanged. - assertEquals(initialSegmentNames, readSegmentNames(dir)); - } + private final Path tempDir = createTempDir(); + private final KeySupplier keySupplier = new TestingKeySupplier.Factory().create(); + private final AesCtrEncrypterFactory encrypterFactory = LightAesCtrEncrypter.FACTORY; + private final EncryptionDirectory.EncryptionListener encryptionListener = EncryptionDirectory.EncryptionListener.NO_LISTENER; + + @Test + public void testNoReencryptionWhenNoKeyChange() throws Exception { + try (Directory dir = new EncryptionDirectory( + new MMapDirectory(tempDir, FSLockFactory.getDefault()), + encrypterFactory, + keySupplier, + encryptionListener)) { + + IndexWriterConfig iwc = new IndexWriterConfig(new WhitespaceAnalyzer()); + iwc.setMergePolicy(createMergePolicy()); + + try (IndexWriter writer = new IndexWriter(dir, iwc)) { + // Create initial segments with KEY_ID_1. + commit(writer, keySupplier, KEY_ID_1); + int numSegments = 3; + for (int i = 0; i < numSegments; ++i) { + writer.addDocument(new Document()); + commit(writer, keySupplier, KEY_ID_1); } + + Set initialSegmentNames = readSegmentNames(dir); + assertEquals(numSegments, initialSegmentNames.size()); + + // Force merge with MAX_VALUE should not trigger reencryption. + writer.forceMerge(Integer.MAX_VALUE); + commit(writer, keySupplier, KEY_ID_1); + + // Verify segments remain unchanged. + assertEquals(initialSegmentNames, readSegmentNames(dir)); + } } + } + + @Test + public void testReencryptionWithKeyChange() throws Exception { + try (Directory dir = new EncryptionDirectory( + new MMapDirectory(tempDir, FSLockFactory.getDefault()), + encrypterFactory, + keySupplier, + encryptionListener)) { - @Test - public void testReencryptionWithKeyChange() throws Exception { - try (Directory dir = new EncryptionDirectory( - new MMapDirectory(tempDir, FSLockFactory.getDefault()), - encrypterFactory, - keySupplier)) { - - IndexWriterConfig iwc = new IndexWriterConfig(new WhitespaceAnalyzer()); - iwc.setMergePolicy(createMergePolicy()); - - try (IndexWriter writer = new IndexWriter(dir, iwc)) { - // Create initial segments with KEY_ID_1. - commit(writer, keySupplier, KEY_ID_1); - int numSegments = 3; - for (int i = 0; i < numSegments; ++i) { - writer.addDocument(new Document()); - commit(writer, keySupplier, KEY_ID_1); - } - - Set initialSegmentNames = readSegmentNames(dir); - assertEquals(numSegments, initialSegmentNames.size()); - - // Change active key to KEY_ID_2. - commit(writer, keySupplier, KEY_ID_1, KEY_ID_2); - - // Force merge with MAX_VALUE should trigger reencryption. - writer.forceMerge(Integer.MAX_VALUE); - commit(writer, keySupplier, KEY_ID_1, KEY_ID_2); - - // Verify all segments have been rewritten. - Set newSegmentNames = readSegmentNames(dir); - assertEquals(initialSegmentNames.size(), newSegmentNames.size()); - assertNotEquals(initialSegmentNames, newSegmentNames); - newSegmentNames.retainAll(initialSegmentNames); - assertTrue(newSegmentNames.isEmpty()); - } + IndexWriterConfig iwc = new IndexWriterConfig(new WhitespaceAnalyzer()); + iwc.setMergePolicy(createMergePolicy()); + + try (IndexWriter writer = new IndexWriter(dir, iwc)) { + // Create initial segments with KEY_ID_1. + commit(writer, keySupplier, KEY_ID_1); + int numSegments = 3; + for (int i = 0; i < numSegments; ++i) { + writer.addDocument(new Document()); + commit(writer, keySupplier, KEY_ID_1); } + + Set initialSegmentNames = readSegmentNames(dir); + assertEquals(numSegments, initialSegmentNames.size()); + + // Change active key to KEY_ID_2. + commit(writer, keySupplier, KEY_ID_1, KEY_ID_2); + + // Force merge with MAX_VALUE should trigger reencryption. + writer.forceMerge(Integer.MAX_VALUE); + commit(writer, keySupplier, KEY_ID_1, KEY_ID_2); + + // Verify all segments have been rewritten. + Set newSegmentNames = readSegmentNames(dir); + assertEquals(initialSegmentNames.size(), newSegmentNames.size()); + assertNotEquals(initialSegmentNames, newSegmentNames); + newSegmentNames.retainAll(initialSegmentNames); + assertTrue(newSegmentNames.isEmpty()); + } } + } + + @Test + public void testNoReencryptionWithNonMaxValueForceMerge() throws Exception { + try (Directory dir = new EncryptionDirectory( + new MMapDirectory(tempDir, FSLockFactory.getDefault()), + encrypterFactory, + keySupplier, + encryptionListener)) { + + IndexWriterConfig iwc = new IndexWriterConfig(new WhitespaceAnalyzer()); + iwc.setMergePolicy(createMergePolicy()); - @Test - public void testNoReencryptionWithNonMaxValueForceMerge() throws Exception { - try (Directory dir = new EncryptionDirectory( - new MMapDirectory(tempDir, FSLockFactory.getDefault()), - encrypterFactory, - keySupplier)) { - - IndexWriterConfig iwc = new IndexWriterConfig(new WhitespaceAnalyzer()); - iwc.setMergePolicy(createMergePolicy()); - - try (IndexWriter writer = new IndexWriter(dir, iwc)) { - // Create initial segments with KEY_ID_1. - commit(writer, keySupplier, KEY_ID_1); - int numSegments = 3; - for (int i = 0; i < numSegments; ++i) { - writer.addDocument(new Document()); - commit(writer, keySupplier, KEY_ID_1); - } - - Set initialSegmentNames = readSegmentNames(dir); - assertEquals(numSegments, initialSegmentNames.size()); - - // Change active key to KEY_ID_2. - commit(writer, keySupplier, KEY_ID_1, KEY_ID_2); - - // Force merge with non-MAX_VALUE should not trigger reencryption. - writer.forceMerge(10); - commit(writer, keySupplier, KEY_ID_1, KEY_ID_2); - - // Verify segments remain unchanged. - assertEquals(initialSegmentNames, readSegmentNames(dir)); - } + try (IndexWriter writer = new IndexWriter(dir, iwc)) { + // Create initial segments with KEY_ID_1. + commit(writer, keySupplier, KEY_ID_1); + int numSegments = 3; + for (int i = 0; i < numSegments; ++i) { + writer.addDocument(new Document()); + commit(writer, keySupplier, KEY_ID_1); } + + Set initialSegmentNames = readSegmentNames(dir); + assertEquals(numSegments, initialSegmentNames.size()); + + // Change active key to KEY_ID_2. + commit(writer, keySupplier, KEY_ID_1, KEY_ID_2); + + // Force merge with non-MAX_VALUE should not trigger reencryption. + writer.forceMerge(10); + commit(writer, keySupplier, KEY_ID_1, KEY_ID_2); + + // Verify segments remain unchanged. + assertEquals(initialSegmentNames, readSegmentNames(dir)); + } } + } - @Test - public void testEmptyIndex() throws Exception { - try (Directory dir = new EncryptionDirectory( - new MMapDirectory(tempDir, FSLockFactory.getDefault()), - encrypterFactory, - keySupplier)) { - - IndexWriterConfig iwc = new IndexWriterConfig(new WhitespaceAnalyzer()); - iwc.setMergePolicy(createMergePolicy()); - - try (IndexWriter writer = new IndexWriter(dir, iwc)) { - // Create empty index with KEY_ID_1. - commit(writer, keySupplier, KEY_ID_1); - - // Change active key to KEY_ID_2. - commit(writer, keySupplier, KEY_ID_1, KEY_ID_2); - - // Force merge with MAX_VALUE should not trigger any merges. - writer.forceMerge(Integer.MAX_VALUE); - commit(writer, keySupplier, KEY_ID_1, KEY_ID_2); - - // Verify no segments exist. - assertEquals(0, readSegmentNames(dir).size()); - } - } + @Test + public void testEmptyIndex() throws Exception { + try (Directory dir = new EncryptionDirectory( + new MMapDirectory(tempDir, FSLockFactory.getDefault()), + encrypterFactory, + keySupplier, + encryptionListener)) { + + IndexWriterConfig iwc = new IndexWriterConfig(new WhitespaceAnalyzer()); + iwc.setMergePolicy(createMergePolicy()); + + try (IndexWriter writer = new IndexWriter(dir, iwc)) { + // Create empty index with KEY_ID_1. + commit(writer, keySupplier, KEY_ID_1); + + // Change active key to KEY_ID_2. + commit(writer, keySupplier, KEY_ID_1, KEY_ID_2); + + // Force merge with MAX_VALUE should not trigger any merges. + writer.forceMerge(Integer.MAX_VALUE); + commit(writer, keySupplier, KEY_ID_1, KEY_ID_2); + + // Verify no segments exist. + assertEquals(0, readSegmentNames(dir).size()); + } } + } + + @Test + public void testPartiallyEncryptedSegments() throws Exception { + try (Directory dir = new EncryptionDirectory( + new MMapDirectory(tempDir, FSLockFactory.getDefault()), + encrypterFactory, + keySupplier, + encryptionListener)) { + + IndexWriterConfig iwc = new IndexWriterConfig(new WhitespaceAnalyzer()); + iwc.setMergePolicy(createMergePolicy()); + + try (IndexWriter writer = new IndexWriter(dir, iwc)) { + // Create segments with mixed encryption states. + commit(writer, keySupplier, KEY_ID_1); - @Test - public void testPartiallyEncryptedSegments() throws Exception { - try (Directory dir = new EncryptionDirectory( - new MMapDirectory(tempDir, FSLockFactory.getDefault()), - encrypterFactory, - keySupplier)) { - - IndexWriterConfig iwc = new IndexWriterConfig(new WhitespaceAnalyzer()); - iwc.setMergePolicy(createMergePolicy()); - - try (IndexWriter writer = new IndexWriter(dir, iwc)) { - // Create segments with mixed encryption states. - commit(writer, keySupplier, KEY_ID_1); - - // Add some documents with KEY_ID_1. - for (int i = 0; i < 3; i++) { - Document doc = new Document(); - doc.add(new StringField("id", String.valueOf(i), Field.Store.YES)); - writer.addDocument(doc); - commit(writer, keySupplier, KEY_ID_1); - } - - Set key1SegmentNames = readSegmentNames(dir); - assertEquals(3, key1SegmentNames.size()); - - // Change to KEY_ID_2. - commit(writer, keySupplier, KEY_ID_1, KEY_ID_2); - - // Add more documents with KEY_ID_2. - for (int i = 3; i < 6; i++) { - Document doc = new Document(); - doc.add(new StringField("id", String.valueOf(i), Field.Store.YES)); - writer.addDocument(doc); - commit(writer, keySupplier, KEY_ID_1, KEY_ID_2); - } - - Set key2NewSegmentNames = readSegmentNames(dir); - key2NewSegmentNames.removeAll(key1SegmentNames); - assertEquals(3, key2NewSegmentNames.size()); - - // Force merge with MAX_VALUE should trigger reencryption of old segments. - writer.forceMerge(Integer.MAX_VALUE); - commit(writer, keySupplier, KEY_ID_1, KEY_ID_2); - - // Verify only and all key1 segments have been rewritten. - Set finalSegmentNames = readSegmentNames(dir); - assertEquals(6, finalSegmentNames.size()); - assertTrue(finalSegmentNames.containsAll(key2NewSegmentNames)); - assertTrue(finalSegmentNames.stream().noneMatch(key1SegmentNames::contains)); - } + // Add some documents with KEY_ID_1. + for (int i = 0; i < 3; i++) { + Document doc = new Document(); + doc.add(new StringField("id", String.valueOf(i), Field.Store.YES)); + writer.addDocument(doc); + commit(writer, keySupplier, KEY_ID_1); } - } - private MergePolicy createMergePolicy() { - return new EncryptionMergePolicy(new TieredMergePolicy()); - } + Set key1SegmentNames = readSegmentNames(dir); + assertEquals(3, key1SegmentNames.size()); - private void commit(IndexWriter writer, KeySupplier keySupplier, String... keyIds) throws IOException { - Map commitData = new HashMap<>(); - for (String keyId : keyIds) { - EncryptionUtil.setNewActiveKeyIdInCommit(keyId, keySupplier.getKeyCookie(keyId, MOCK_COOKIE_PARAMS), commitData); + // Change to KEY_ID_2. + commit(writer, keySupplier, KEY_ID_1, KEY_ID_2); + + // Add more documents with KEY_ID_2. + for (int i = 3; i < 6; i++) { + Document doc = new Document(); + doc.add(new StringField("id", String.valueOf(i), Field.Store.YES)); + writer.addDocument(doc); + commit(writer, keySupplier, KEY_ID_1, KEY_ID_2); } - writer.setLiveCommitData(commitData.entrySet()); - writer.commit(); + + Set key2NewSegmentNames = readSegmentNames(dir); + key2NewSegmentNames.removeAll(key1SegmentNames); + assertEquals(3, key2NewSegmentNames.size()); + + // Force merge with MAX_VALUE should trigger reencryption of old segments. + writer.forceMerge(Integer.MAX_VALUE); + commit(writer, keySupplier, KEY_ID_1, KEY_ID_2); + + // Verify only and all key1 segments have been rewritten. + Set finalSegmentNames = readSegmentNames(dir); + assertEquals(6, finalSegmentNames.size()); + assertTrue(finalSegmentNames.containsAll(key2NewSegmentNames)); + assertTrue(finalSegmentNames.stream().noneMatch(key1SegmentNames::contains)); + } } + } - private Set readSegmentNames(Directory dir) throws IOException { - SegmentInfos segmentInfos = SegmentInfos.readLatestCommit(dir); - return segmentInfos.asList().stream() - .map(sci -> sci.info.name) - .collect(Collectors.toSet()); + private MergePolicy createMergePolicy() { + return new EncryptionMergePolicy(new TieredMergePolicy()); + } + + private void commit(IndexWriter writer, KeySupplier keySupplier, String... keyIds) throws IOException { + Map commitData = new HashMap<>(); + for (String keyId : keyIds) { + EncryptionUtil.setNewActiveKeyIdInCommit(keyId, keySupplier.getKeyCookie(keyId, MOCK_COOKIE_PARAMS), commitData); } + writer.setLiveCommitData(commitData.entrySet()); + writer.commit(); + } + + private Set readSegmentNames(Directory dir) throws IOException { + SegmentInfos segmentInfos = SegmentInfos.readLatestCommit(dir); + return segmentInfos.asList().stream() + .map(sci -> sci.info.name) + .collect(Collectors.toSet()); + } } \ No newline at end of file diff --git a/encryption/src/test/java/org/apache/solr/encryption/EncryptionRequestHandlerTest.java b/encryption/src/test/java/org/apache/solr/encryption/EncryptionRequestHandlerTest.java index 0dcf9bc..bcabe01 100644 --- a/encryption/src/test/java/org/apache/solr/encryption/EncryptionRequestHandlerTest.java +++ b/encryption/src/test/java/org/apache/solr/encryption/EncryptionRequestHandlerTest.java @@ -27,6 +27,7 @@ import org.apache.solr.common.params.SolrParams; import org.apache.solr.embedded.JettySolrRunner; import org.apache.solr.encryption.crypto.AesCtrEncrypterFactory; +import org.apache.solr.encryption.EncryptionDirectory.EncryptionListener; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.request.SolrQueryRequestBase; import org.apache.solr.response.SolrQueryResponse; @@ -393,16 +394,22 @@ public static class MockFactory implements EncryptionDirectoryFactory.InnerFacto @Override public EncryptionDirectory create(Directory delegate, AesCtrEncrypterFactory encrypterFactory, - KeySupplier keySupplier) throws IOException { - return new MockEncryptionDirectory(delegate, encrypterFactory, keySupplier); + KeySupplier keySupplier, + EncryptionListener encryptionListener) + throws IOException { + return new MockEncryptionDirectory(delegate, encrypterFactory, keySupplier, encryptionListener); } } private static class MockEncryptionDirectory extends EncryptionDirectory { - MockEncryptionDirectory(Directory delegate, AesCtrEncrypterFactory encrypterFactory, KeySupplier keySupplier) + MockEncryptionDirectory( + Directory delegate, + AesCtrEncrypterFactory encrypterFactory, + KeySupplier keySupplier, + EncryptionListener encryptionListener) throws IOException { - super(delegate, encrypterFactory, keySupplier); + super(delegate, encrypterFactory, keySupplier, encryptionListener); } @Override