blob: 77ba050b1ba348022c8077c862c4f9d16669d94f [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.encryption;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.IOContext;
import org.apache.lucene.store.IndexInput;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.cloud.MiniSolrCloudCluster;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.apache.solr.encryption.crypto.AesCtrEncrypterFactory;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Tests {@link EncryptionDirectory}.
* <p>
* This test class ignores the DirectoryFactory defined in solrconfig.xml to use
* {@link EncryptionDirectoryFactory}.
*/
public class EncryptionDirectoryTest extends SolrCloudTestCase {
private static final String COLLECTION_PREFIX = EncryptionDirectoryTest.class.getSimpleName() + "-collection-";
private static MockEncryptionDirectory mockDir;
private String collectionName;
private CloudSolrClient solrClient;
private TestUtil testUtil;
@BeforeClass
public static void beforeClass() throws Exception {
System.setProperty(EncryptionDirectoryFactory.PROPERTY_INNER_ENCRYPTION_DIRECTORY_FACTORY, MockFactory.class.getName());
TestUtil.setInstallDirProperty();
cluster = new MiniSolrCloudCluster.Builder(1, createTempDir())
.addConfig("config", TestUtil.getConfigPath("collection1"))
.configure();
}
@AfterClass
public static void afterClass() throws Exception {
System.clearProperty(EncryptionDirectoryFactory.PROPERTY_INNER_ENCRYPTION_DIRECTORY_FACTORY);
cluster.shutdown();
}
@Override
public void setUp() throws Exception {
super.setUp();
collectionName = COLLECTION_PREFIX + UUID.randomUUID();
solrClient = cluster.getSolrClient();
solrClient.setDefaultCollection(collectionName);
CollectionAdminRequest.createCollection(collectionName, 1, 1).process(solrClient);
cluster.waitForActiveCollection(collectionName, 1, 1);
testUtil = new TestUtil(solrClient, collectionName);
}
@Override
public void tearDown() throws Exception {
mockDir.clearMockValues();
CollectionAdminRequest.deleteCollection(collectionName).process(solrClient);
super.tearDown();
}
/**
* Verifies the encryption of an index, moving from no keys to one key.
*/
@Test
public void testEncryptionFromNoKeysToOneKey() throws Exception {
indexAndEncryptOneSegment();
}
/**
* Starts from an empty index, indexes two documents in two segments, then encrypt the index
* with {@link TestingKeyManager#KEY_ID_1}. The resulting encrypted index is composed of one segment.
*/
private void indexAndEncryptOneSegment() throws Exception {
// Start with no key ids defined in the latest commit metadata.
mockDir.clearMockValues();
// Create 2 index segments without encryption.
testUtil.indexDocsAndCommit("weather broadcast");
testUtil.indexDocsAndCommit("sunny weather");
testUtil.assertQueryReturns("weather", 2);
// Verify that without key id, we can reload the index because it is not encrypted.
testUtil.reloadCore();
testUtil.assertQueryReturns("weather", 2);
// Set the encryption key id in the commit user data,
// and run an optimized commit to rewrite the index, now encrypted.
mockDir.setKeysInCommitUserData(TestingKeyManager.KEY_ID_1);
solrClient.optimize();
// Verify that without key id, we cannot decrypt the index anymore.
mockDir.forceClearText = true;
testUtil.assertCannotReloadCore();
// Verify that with a wrong key id, we cannot decrypt the index.
mockDir.forceClearText = false;
mockDir.forceKeySecret = TestingKeyManager.KEY_SECRET_2;
testUtil.assertCannotReloadCore();
// Verify that with the right key id, we can decrypt the index and search it.
mockDir.forceKeySecret = null;
mockDir.expectedKeySecret = TestingKeyManager.KEY_SECRET_1;
testUtil.reloadCore();
testUtil.assertQueryReturns("weather", 2);
testUtil.assertQueryReturns("sunny", 1);
mockDir.clearMockValues();
}
/**
* Verifies an encrypted index cannot be loaded without the right encryption key,
* and that we can search the index if we have the right encryption key.
*/
@Test
public void testIndexingAndQueryingWithEncryption() throws Exception {
indexAndEncryptTwoSegments();
}
/**
* Creates an index encrypted with {@link TestingKeyManager#KEY_ID_1} and containing two segments.
*/
private void indexAndEncryptTwoSegments() throws Exception {
// Prepare an encrypted index with one segment.
indexAndEncryptOneSegment();
// Create 1 new segment with the same encryption key id.
mockDir.setKeysInCommitUserData(TestingKeyManager.KEY_ID_1);
testUtil.indexDocsAndCommit("foggy weather");
// Verify that without key id, we cannot decrypt the index.
mockDir.forceClearText = true;
testUtil.assertCannotReloadCore();
// Verify that with a wrong key id, we cannot decrypt the index.
mockDir.forceClearText = false;
mockDir.forceKeySecret = TestingKeyManager.KEY_SECRET_2;
testUtil.assertCannotReloadCore();
// Verify that with the right key id, we can decrypt the index and search it.
mockDir.forceKeySecret = null;
mockDir.expectedKeySecret = TestingKeyManager.KEY_SECRET_1;
testUtil.reloadCore();
testUtil.assertQueryReturns("weather", 3);
testUtil.assertQueryReturns("sunny", 1);
mockDir.clearMockValues();
}
/**
* Verifies the re-encryption of an index, moving from one key to another key.
*/
@Test
public void testReEncryptionFromOneKeyToAnotherKey() throws Exception {
// Prepare an encrypted index with two segments.
indexAndEncryptTwoSegments();
// Set the new encryption key id in the commit user data,
// and run an optimized commit to rewrite the index, now encrypted with the new key.
mockDir.setKeysInCommitUserData(TestingKeyManager.KEY_ID_1, TestingKeyManager.KEY_ID_2);
solrClient.optimize();
// Verify that without key id, we cannot decrypt the index.
mockDir.forceClearText = true;
testUtil.assertCannotReloadCore();
// Verify that with a wrong key id, we cannot decrypt the index.
mockDir.forceClearText = false;
mockDir.forceKeySecret = TestingKeyManager.KEY_SECRET_1;
testUtil.assertCannotReloadCore();
// Verify that with the right key id, we can decrypt the index and search it.
mockDir.forceKeySecret = null;
mockDir.expectedKeySecret = TestingKeyManager.KEY_SECRET_2;
testUtil.reloadCore();
testUtil.assertQueryReturns("weather", 3);
testUtil.assertQueryReturns("sunny", 1);
}
/**
* Verifies the decryption of an index, moving from one key to no keys.
*/
@Test
public void testDecryptionFromOneKeyToNoKeys() throws Exception {
// Prepare an encrypted index with two segments.
indexAndEncryptTwoSegments();
// Remove the active key parameter from the commit user data,
// and run an optimized commit to rewrite the index, now cleartext with no keys.
mockDir.setKeysInCommitUserData(TestingKeyManager.KEY_ID_1, null);
solrClient.optimize();
// Verify that without key id, we can reload the index because it is not encrypted.
mockDir.forceClearText = true;
testUtil.reloadCore();
testUtil.assertQueryReturns("weather", 3);
testUtil.assertQueryReturns("sunny", 1);
}
public static class MockFactory implements EncryptionDirectoryFactory.InnerFactory {
@Override
public EncryptionDirectory create(Directory delegate,
AesCtrEncrypterFactory encrypterFactory,
KeyManager keyManager) throws IOException {
return mockDir = new MockEncryptionDirectory(delegate, encrypterFactory, keyManager);
}
}
private static class MockEncryptionDirectory extends EncryptionDirectory {
final KeyManager keyManager;
boolean forceClearText;
byte[] forceKeySecret;
byte[] expectedKeySecret;
MockEncryptionDirectory(Directory delegate, AesCtrEncrypterFactory encrypterFactory, KeyManager keyManager)
throws IOException {
super(delegate, encrypterFactory, keyManager);
this.keyManager = keyManager;
}
void clearMockValues() {
commitUserData.data.clear();
forceClearText = false;
forceKeySecret = null;
expectedKeySecret = null;
}
/**
* Clears the commit user data, then sets the provided key ids. The last key id is the active one.
*/
void setKeysInCommitUserData(String... keyIds) throws IOException {
commitUserData.data.clear();
for (String keyId : keyIds) {
if (keyId == null) {
EncryptionUtil.removeActiveKeyRefFromCommit(commitUserData.data);
} else {
EncryptionUtil.setNewActiveKeyIdInCommit(keyId, keyManager.getKeyCookie(keyId), commitUserData.data);
}
}
}
@Override
public IndexInput openInput(String fileName, IOContext context) throws IOException {
return forceClearText ? in.openInput(fileName, context) : super.openInput(fileName, context);
}
@Override
protected CommitUserData readLatestCommitUserData() {
// Keep the same data map because it contains the mock values for the test,
// so the test is easier to write and clearer to read.
Map<String, String> data = commitUserData == null ? new HashMap<>() : commitUserData.data;
return new CommitUserData("test", data);
}
@Override
protected byte[] getKeySecret(String keyRef) throws IOException {
if (forceKeySecret != null) {
return forceKeySecret;
}
byte[] keySecret = super.getKeySecret(keyRef);
if (expectedKeySecret != null) {
assertArrayEquals(expectedKeySecret, keySecret);
}
return keySecret;
}
}
}