This is an automated email from the ASF dual-hosted git repository.
broustant pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-sandbox.git
The following commit(s) were added to refs/heads/main by this push:
new 1951920 Encrypt handler calls EncryptionUpdateLog to reencrypt logs.
Rewrite the test. (#96)
1951920 is described below
commit 195192017369a8c4468f34af215f717b7617dbf8
Author: Bruno Roustant <[email protected]>
AuthorDate: Mon Dec 4 17:57:55 2023 +0100
Encrypt handler calls EncryptionUpdateLog to reencrypt logs. Rewrite the
test. (#96)
---
.../encryption/EncryptionDirectoryFactory.java | 2 +-
.../solr/encryption/EncryptionRequestHandler.java | 42 ++-
.../solr/encryption/EncryptionTransactionLog.java | 90 ++++--
.../solr/encryption/EncryptionUpdateLog.java | 94 ++++--
.../org/apache/solr/encryption/KeySupplier.java | 12 +-
.../encryption/crypto/CharStreamEncrypter.java | 35 +++
.../encryption/crypto/EncryptingIndexOutput.java | 1 -
.../org/apache/solr/update/TransactionLog.java | 2 +-
.../solr/encryption/EncryptionHeavyLoadTest.java | 28 +-
.../encryption/EncryptionRequestHandlerTest.java | 94 ++----
.../apache/solr/encryption/EncryptionTestUtil.java | 61 ++++
.../solr/encryption/EncryptionUpdateLogTest.java | 325 ++++++++++-----------
.../apache/solr/encryption/TestingKeySupplier.java | 3 +-
.../encryption/crypto/AesCtrEncrypterTest.java | 8 +-
.../encryption/crypto/CharStreamEncrypterTest.java | 8 +-
.../CryptoTestUtil.java} | 30 +-
.../crypto/DecryptingChannelInputStreamTest.java | 5 +-
.../crypto/DecryptingIndexInputTest.java | 5 +-
.../crypto/EncryptingIndexOutputTest.java | 5 +-
.../crypto/EncryptingOutputStreamTest.java | 5 +-
.../resources/configs/collection1/solrconfig.xml | 2 +-
21 files changed, 490 insertions(+), 367 deletions(-)
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 ad51886..102005f 100644
---
a/encryption/src/main/java/org/apache/solr/encryption/EncryptionDirectoryFactory.java
+++
b/encryption/src/main/java/org/apache/solr/encryption/EncryptionDirectoryFactory.java
@@ -78,7 +78,7 @@ public class EncryptionDirectoryFactory extends
MMapDirectoryFactory {
}
KeySupplier.Factory keySupplierFactory =
coreContainer.getResourceLoader().newInstance(keySupplierFactoryClass,
KeySupplier.Factory.class);
- keySupplierFactory.init(args);
+ keySupplierFactory.init(args, coreContainer);
try {
keySupplier = keySupplierFactory.create();
} catch (IOException e) {
diff --git
a/encryption/src/main/java/org/apache/solr/encryption/EncryptionRequestHandler.java
b/encryption/src/main/java/org/apache/solr/encryption/EncryptionRequestHandler.java
index 9c3b099..77e6c4d 100644
---
a/encryption/src/main/java/org/apache/solr/encryption/EncryptionRequestHandler.java
+++
b/encryption/src/main/java/org/apache/solr/encryption/EncryptionRequestHandler.java
@@ -29,6 +29,8 @@ import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.security.AuthorizationContext;
import org.apache.solr.security.PermissionNameProvider;
import org.apache.solr.update.CommitUpdateCommand;
+import org.apache.solr.update.UpdateHandler;
+import org.apache.solr.update.UpdateLog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -209,7 +211,8 @@ public class EncryptionRequestHandler extends
RequestHandlerBase {
if (isCommitActiveKeyId(keyId, segmentInfos)) {
log.debug("{} provided keyId={} is the current active key id",
ENCRYPTION_LOG_PREFIX, keyId);
if
(Boolean.parseBoolean(segmentInfos.getUserData().get(COMMIT_ENCRYPTION_PENDING)))
{
- encryptionComplete = areAllSegmentsEncryptedWithKeyId(keyId,
req.getCore(), segmentInfos);
+ encryptionComplete = areAllSegmentsEncryptedWithKeyId(keyId,
req.getCore(), segmentInfos)
+ && areAllLogsEncryptedWithKeyId(keyId, req.getCore(),
segmentInfos);
if (encryptionComplete) {
commitEncryptionComplete(keyId, segmentInfos, req);
}
@@ -328,13 +331,22 @@ public class EncryptionRequestHandler extends
RequestHandlerBase {
log.debug("{} submitting async encryption", ENCRYPTION_LOG_PREFIX);
executor.submit(() -> {
try {
- log.debug("{} running async encryption", ENCRYPTION_LOG_PREFIX);
+ EncryptionUpdateLog updateLog = getEncryptionUpdateLog(req.getCore());
+ if (updateLog != null) {
+ log.debug("{} running async update log encryption",
ENCRYPTION_LOG_PREFIX);
+ boolean logEncryptionComplete = updateLog.encryptLogs();
+ log.info("{} {} encrypted the update log in {}",
+ ENCRYPTION_LOG_PREFIX, logEncryptionComplete ?
"successfully" : "partially", elapsedTime(startTimeMs));
+ }
+
+ log.debug("{} running async index encryption", ENCRYPTION_LOG_PREFIX);
CommitUpdateCommand commitCmd = new CommitUpdateCommand(req, true);
// Trigger EncryptionMergePolicy.findForcedMerges() to re-encrypt
// each segment which is not encrypted with the latest active key id.
commitCmd.maxOptimizeSegments = Integer.MAX_VALUE;
req.getCore().getUpdateHandler().commit(commitCmd);
log.info("{} successfully encrypted the index in {}",
ENCRYPTION_LOG_PREFIX, elapsedTime(startTimeMs));
+
} catch (IOException e) {
log.error("{} exception while encrypting the index after {}",
ENCRYPTION_LOG_PREFIX, elapsedTime(startTimeMs), e);
} finally {
@@ -346,9 +358,9 @@ public class EncryptionRequestHandler extends
RequestHandlerBase {
});
}
- public static boolean areAllSegmentsEncryptedWithKeyId(@Nullable String
keyId,
- SolrCore core,
- SegmentInfos
segmentInfos) throws IOException {
+ private boolean areAllSegmentsEncryptedWithKeyId(@Nullable String keyId,
+ SolrCore core,
+ SegmentInfos segmentInfos)
throws IOException {
DirectoryFactory directoryFactory = core.getDirectoryFactory();
Directory indexDir = directoryFactory.get(core.getIndexDir(),
DirectoryFactory.DirContext.DEFAULT,
@@ -364,6 +376,26 @@ public class EncryptionRequestHandler extends
RequestHandlerBase {
}
}
+ private boolean areAllLogsEncryptedWithKeyId(String keyId, SolrCore core,
SegmentInfos segmentInfos)
+ throws IOException {
+ EncryptionUpdateLog updateLog = getEncryptionUpdateLog(core);
+ return updateLog == null || updateLog.areAllLogsEncryptedWithKeyId(keyId,
segmentInfos);
+ }
+
+ private EncryptionUpdateLog getEncryptionUpdateLog(SolrCore core) {
+ UpdateHandler updateHandler = core.getUpdateHandler();
+ if (updateHandler == null) {
+ return null;
+ }
+ if (!(updateHandler.getUpdateLog() instanceof EncryptionUpdateLog)) {
+ throw new SolrException(SolrException.ErrorCode.SERVICE_UNAVAILABLE,
+ UpdateLog.class.getSimpleName()
+ + " must be configured with an "
+ + EncryptionUpdateLog.class.getSimpleName());
+ }
+ return (EncryptionUpdateLog) updateHandler.getUpdateLog();
+ }
+
private boolean isCommitActiveKeyId(String keyId, SegmentInfos segmentInfos)
{
String keyRef = getActiveKeyRefFromCommit(segmentInfos.getUserData());
String activeKeyId = keyRef == null ? null : getKeyIdFromCommit(keyRef,
segmentInfos.getUserData());
diff --git
a/encryption/src/main/java/org/apache/solr/encryption/EncryptionTransactionLog.java
b/encryption/src/main/java/org/apache/solr/encryption/EncryptionTransactionLog.java
index b9d842a..6b62329 100644
---
a/encryption/src/main/java/org/apache/solr/encryption/EncryptionTransactionLog.java
+++
b/encryption/src/main/java/org/apache/solr/encryption/EncryptionTransactionLog.java
@@ -16,6 +16,7 @@
*/
package org.apache.solr.encryption;
+import org.apache.solr.encryption.crypto.AesCtrEncrypterFactory;
import org.apache.solr.encryption.crypto.DecryptingChannelInputStream;
import org.apache.solr.encryption.crypto.EncryptingOutputStream;
import org.apache.solr.update.TransactionLog;
@@ -44,8 +45,8 @@ import static
org.apache.solr.encryption.crypto.AesCtrUtil.IV_LENGTH;
*/
public class EncryptionTransactionLog extends TransactionLog {
- static final int ENCRYPTION_KEY_HEADER_LENGTH = 2 * Integer.BYTES;
- private static final int ENCRYPTION_FULL_HEADER_LENGTH =
ENCRYPTION_KEY_HEADER_LENGTH + IV_LENGTH;
+ protected static final int ENCRYPTION_KEY_HEADER_LENGTH = 2 * Integer.BYTES;
+ protected static final int ENCRYPTION_FULL_HEADER_LENGTH =
ENCRYPTION_KEY_HEADER_LENGTH + IV_LENGTH;
/** Creates an {@link EncryptionTransactionLog}. */
public EncryptionTransactionLog(Path tlogFile,
@@ -55,18 +56,38 @@ public class EncryptionTransactionLog extends
TransactionLog {
this(tlogFile, globalStrings, openExisting, directorySupplier, new
IvHolder());
}
- private EncryptionTransactionLog(Path tlogFile,
+ protected EncryptionTransactionLog(Path tlogFile,
Collection<String> globalStrings,
boolean openExisting,
EncryptionDirectorySupplier
directorySupplier,
IvHolder ivHolder) {
- super(tlogFile,
+ this(tlogFile,
globalStrings,
openExisting,
new EncryptionOutputStreamOpener(directorySupplier, ivHolder),
new EncryptionChannelInputStreamOpener(directorySupplier, ivHolder));
}
+ protected EncryptionTransactionLog(Path tlogFile,
+ Collection<String> globalStrings,
+ boolean openExisting,
+ EncryptionOutputStreamOpener
outputStreamOpener,
+ EncryptionChannelInputStreamOpener
channelInputStreamOpener) {
+ super(tlogFile,
+ globalStrings,
+ openExisting,
+ outputStreamOpener,
+ channelInputStreamOpener);
+ }
+
+ public Path path() {
+ return tlog;
+ }
+
+ public int refCount() {
+ return refcount.get();
+ }
+
@Override
protected void setWrittenCount(long fileStartOffset) throws IOException {
if (os instanceof EncryptingOutputStream) {
@@ -135,23 +156,24 @@ public class EncryptionTransactionLog extends
TransactionLog {
}
/** Supplies and releases {@link EncryptionDirectory}. */
- interface EncryptionDirectorySupplier {
+ protected interface EncryptionDirectorySupplier {
EncryptionDirectory get();
void release(EncryptionDirectory directory) throws IOException;
}
- private static class IvHolder {
+ /** Holds the IV only during the constructor call. */
+ protected static class IvHolder {
private byte[] iv;
}
- private static class EncryptionOutputStreamOpener implements
OutputStreamOpener {
+ protected static class EncryptionOutputStreamOpener implements
OutputStreamOpener {
- private final EncryptionDirectorySupplier directorySupplier;
- private final IvHolder ivHolder;
+ protected final EncryptionDirectorySupplier directorySupplier;
+ protected final IvHolder ivHolder;
- EncryptionOutputStreamOpener(EncryptionDirectorySupplier
directorySupplier, IvHolder ivHolder) {
+ protected EncryptionOutputStreamOpener(EncryptionDirectorySupplier
directorySupplier, IvHolder ivHolder) {
this.directorySupplier = directorySupplier;
this.ivHolder = ivHolder;
}
@@ -169,11 +191,11 @@ public class EncryptionTransactionLog extends
TransactionLog {
// The output stream has to be wrapped to be encrypted with the key.
directory.shouldCheckEncryptionWhenReading = true;
writeEncryptionHeader(keyRef, outputStream);
- EncryptingOutputStream eos = new EncryptingOutputStream(outputStream,
- position,
- ivHolder.iv,
-
directory.getKeySecret(keyRef),
-
directory.getEncrypterFactory());
+ EncryptingOutputStream eos =
createEncryptingOutputStream(outputStream,
+ position,
+
ivHolder.iv,
+
directory.getKeySecret(keyRef),
+
directory.getEncrypterFactory());
ivHolder.iv = eos.getIv();
return eos;
}
@@ -183,15 +205,24 @@ public class EncryptionTransactionLog extends
TransactionLog {
directorySupplier.release(directory);
}
}
+
+ protected EncryptingOutputStream createEncryptingOutputStream(OutputStream
outputStream,
+ long
position,
+ byte[] iv,
+ byte[] key,
+
AesCtrEncrypterFactory factory)
+ throws IOException {
+ return new EncryptingOutputStream(outputStream, position, iv, key,
factory);
+ }
}
- private static class EncryptionChannelInputStreamOpener implements
ChannelInputStreamOpener {
+ protected static class EncryptionChannelInputStreamOpener implements
ChannelInputStreamOpener {
- private final EncryptionDirectorySupplier directorySupplier;
- private final IvHolder ivHolder;
- private final ByteBuffer readBuffer;
+ protected final EncryptionDirectorySupplier directorySupplier;
+ protected final IvHolder ivHolder;
+ protected final ByteBuffer readBuffer;
- EncryptionChannelInputStreamOpener(
+ protected EncryptionChannelInputStreamOpener(
EncryptionDirectorySupplier directorySupplier, IvHolder ivHolder) {
this.directorySupplier = directorySupplier;
this.ivHolder = ivHolder;
@@ -206,11 +237,11 @@ public class EncryptionTransactionLog extends
TransactionLog {
String keyRef = readEncryptionHeader(channel, readBuffer);
if (keyRef != null) {
// The IndexInput has to be wrapped to be decrypted with the key.
- DecryptingChannelInputStream dcis = new
DecryptingChannelInputStream(channel,
-
ENCRYPTION_KEY_HEADER_LENGTH,
-
position,
-
directory.getKeySecret(keyRef),
-
directory.getEncrypterFactory());
+ DecryptingChannelInputStream dcis =
createDecryptingChannelInputStream(channel,
+
ENCRYPTION_KEY_HEADER_LENGTH,
+
position,
+
directory.getKeySecret(keyRef),
+
directory.getEncrypterFactory());
ivHolder.iv = dcis.getIv();
return dcis;
}
@@ -221,5 +252,14 @@ public class EncryptionTransactionLog extends
TransactionLog {
ivHolder.iv = null;
return CHANNEL_INPUT_STREAM_OPENER.open(channel, position);
}
+
+ protected DecryptingChannelInputStream
createDecryptingChannelInputStream(FileChannel channel,
+
long offset,
+
long position,
+
byte[] key,
+
AesCtrEncrypterFactory factory)
+ throws IOException {
+ return new DecryptingChannelInputStream(channel, offset, position, key,
factory);
+ }
}
}
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 de35a55..ab60c8a 100644
---
a/encryption/src/main/java/org/apache/solr/encryption/EncryptionUpdateLog.java
+++
b/encryption/src/main/java/org/apache/solr/encryption/EncryptionUpdateLog.java
@@ -16,6 +16,7 @@
*/
package org.apache.solr.encryption;
+import org.apache.lucene.index.SegmentInfos;
import org.apache.solr.common.SolrException;
import org.apache.solr.core.DirectoryFactory;
import org.apache.solr.core.SolrCore;
@@ -44,62 +45,115 @@ import java.util.Objects;
import static
org.apache.solr.encryption.EncryptionTransactionLog.ENCRYPTION_KEY_HEADER_LENGTH;
import static
org.apache.solr.encryption.EncryptionTransactionLog.readEncryptionHeader;
import static
org.apache.solr.encryption.EncryptionTransactionLog.writeEncryptionHeader;
+import static org.apache.solr.encryption.EncryptionUtil.ENCRYPTION_LOG_PREFIX;
import static
org.apache.solr.encryption.EncryptionUtil.getActiveKeyRefFromCommit;
+import static org.apache.solr.encryption.EncryptionUtil.getKeyIdFromCommit;
/**
* {@link UpdateLog} that creates {@link EncryptionUpdateLog} to encrypts logs
if the
* core index is marked for encryption.
* <p>
* The encryption is triggered when the {@link EncryptionRequestHandler} is
called. It
- * commits with some metadata that mark the index for encryption. From that
point, this
- * {@link EncryptionUpdateLog} will first re-encrypt old log files with the
active
- * encryption key id (nearly as fast as a file copy), and it will encrypt new
log files
- * with the same active key.
+ * commits with some metadata that mark the index for encryption. It also calls
+ * {@link #encryptLogs} to (re)encrypt old log files with the active
encryption key id
+ * (nearly as fast as a file copy). New log files created with {@link
#newTransactionLog}
+ * will be encrypted using the active key stored in the commit metadata.
*/
public class EncryptionUpdateLog extends UpdateLog {
private static final Logger log =
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
- private static final int REENCRYPTION_BUFFER_SIZE = 1024;
+ protected static final int REENCRYPTION_BUFFER_SIZE = 1024;
- private final DirectorySupplier directorySupplier = new DirectorySupplier();
+ protected final DirectorySupplier directorySupplier = new
DirectorySupplier();
+
+ protected boolean shouldEncryptOldLogs = true;
@Override
public void init(UpdateHandler updateHandler, SolrCore core) {
directorySupplier.init(core);
super.init(updateHandler, core);
+ try {
+ encryptLogs();
+ } catch (IOException e) {
+ log.error("{} exception while encrypting old transaction logs",
ENCRYPTION_LOG_PREFIX, e);
+ }
}
@Override
public TransactionLog newTransactionLog(Path tlogFile, Collection<String>
globalStrings, boolean openExisting) {
- if (openExisting) {
- assert Files.exists(tlogFile) : tlogFile + " does not exist";
+ return new EncryptionTransactionLog(tlogFile, globalStrings, openExisting,
directorySupplier);
+ }
+
+ /**
+ * (Re)encrypts all existing transaction logs if an encryption key is
defined in the {@link SolrCore}
+ * index commit metadata.
+ *
+ * @return {@code true} if all logs were successfully encrypted; {@code
false} if some logs were not
+ * encrypted because they are still in use, in this case the encryption will
be retried at the next commit.
+ */
+ public synchronized boolean encryptLogs() throws IOException {
+ boolean allLogsEncrypted = true;
+ for (TransactionLog log : logs) {
+ EncryptionTransactionLog encLog = (EncryptionTransactionLog) log;
+ if (encLog.refCount() <= 1) {
+ // The log is only owned by this update log. We can encrypt it.
+ encryptLog(encLog);
+ } else {
+ allLogsEncrypted = false;
+ }
+ }
+ shouldEncryptOldLogs = !allLogsEncrypted;
+ return allLogsEncrypted;
+ }
+
+ @Override
+ protected synchronized void addOldLog(TransactionLog oldLog, boolean
removeOld) {
+ super.addOldLog(oldLog, removeOld);
+ if (shouldEncryptOldLogs) {
try {
- reencryptOldLogFileIfRequired(tlogFile);
- } catch (Exception e) {
- // Absorb the exception and continue opening the transaction log.
- log.error("Failure to re-encrypt log file (non fatal) {}", tlogFile,
e);
+ encryptLogs();
+ } catch (IOException e) {
+ log.error("{} exception while encrypting old transaction logs",
ENCRYPTION_LOG_PREFIX, e);
}
}
- return new EncryptionTransactionLog(tlogFile, globalStrings, openExisting,
directorySupplier);
}
- protected void reencryptOldLogFileIfRequired(Path tlog) throws IOException {
- if (Files.size(tlog) > 0) {
+ /**
+ * Returns whether all logs are encrypted with the provided key id.
+ */
+ public synchronized boolean areAllLogsEncryptedWithKeyId(String keyId,
SegmentInfos segmentInfos) throws IOException {
+ if (!logs.isEmpty()) {
+ ByteBuffer readBuffer = ByteBuffer.allocate(4);
+ for (TransactionLog log : logs) {
+ try (FileChannel logChannel =
FileChannel.open(((EncryptionTransactionLog) log).path(),
StandardOpenOption.READ)) {
+ String logKeyRef = readEncryptionHeader(logChannel, readBuffer);
+ String logKeyId = logKeyRef == null ? null :
getKeyIdFromCommit(logKeyRef, segmentInfos.getUserData());
+ if (!Objects.equals(logKeyId, keyId)) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ protected void encryptLog(EncryptionTransactionLog log) throws IOException {
+ if (Files.size(log.path()) > 0) {
EncryptionDirectory directory = directorySupplier.get();
try {
directory.forceReadCommitUserData();
- try (FileChannel inputChannel = FileChannel.open(tlog,
StandardOpenOption.READ)) {
+ try (FileChannel inputChannel = FileChannel.open(log.path(),
StandardOpenOption.READ)) {
String inputKeyRef = readEncryptionHeader(inputChannel,
ByteBuffer.allocate(4));
String activeKeyRef =
getActiveKeyRefFromCommit(directory.getLatestCommitData().data);
if (!Objects.equals(inputKeyRef, activeKeyRef)) {
- Path newLog = tlog.resolveSibling(tlog.getFileName() + ".enc");
+ Path newLog = log.path().resolveSibling(log.path().getFileName() +
".enc");
try (OutputStream outputStream = new
FileOutputStream(newLog.toFile())) {
reencrypt(inputChannel, inputKeyRef, outputStream, activeKeyRef,
directory);
}
- Path backupLog = tlog.resolveSibling(tlog.getFileName() + ".bak");
- Files.move(tlog, backupLog);
- Files.move(newLog, tlog);
+ Path backupLog =
log.path().resolveSibling(log.path().getFileName() + ".bak");
+ Files.move(log.path(), backupLog);
+ Files.move(newLog, log.path());
Files.delete(backupLog);
}
}
diff --git
a/encryption/src/main/java/org/apache/solr/encryption/KeySupplier.java
b/encryption/src/main/java/org/apache/solr/encryption/KeySupplier.java
index 6a533df..2bab61c 100644
--- a/encryption/src/main/java/org/apache/solr/encryption/KeySupplier.java
+++ b/encryption/src/main/java/org/apache/solr/encryption/KeySupplier.java
@@ -18,7 +18,7 @@ package org.apache.solr.encryption;
import org.apache.lucene.index.IndexFileNames;
import org.apache.solr.common.util.NamedList;
-import org.apache.solr.util.plugin.NamedListInitializedPlugin;
+import org.apache.solr.core.CoreContainer;
import javax.annotation.Nullable;
import java.io.Closeable;
@@ -70,10 +70,14 @@ public interface KeySupplier extends Closeable {
/**
* Creates {@link KeySupplier}.
*/
- interface Factory extends NamedListInitializedPlugin {
+ interface Factory {
- /** This factory may be configured with parameters defined in
solrconfig.xml. */
- void init(NamedList<?> args);
+ /**
+ * Initializes this factory.
+ *
+ * @param args non-null list of initialization parameters (may be empty).
+ */
+ void init(NamedList<?> args, CoreContainer coreContainer);
/** Creates a {@link KeySupplier}. */
KeySupplier create() throws IOException;
diff --git
a/encryption/src/main/java/org/apache/solr/encryption/crypto/CharStreamEncrypter.java
b/encryption/src/main/java/org/apache/solr/encryption/crypto/CharStreamEncrypter.java
index d370a1e..15a8b7b 100644
---
a/encryption/src/main/java/org/apache/solr/encryption/crypto/CharStreamEncrypter.java
+++
b/encryption/src/main/java/org/apache/solr/encryption/crypto/CharStreamEncrypter.java
@@ -27,6 +27,7 @@ import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringReader;
+import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
@@ -50,6 +51,23 @@ public class CharStreamEncrypter {
this.factory = factory;
}
+ /**
+ * Encrypts an input string to base 64 characters compatible with JSON.
+ *
+ * @param key AES key, can either 16, 24 or 32 bytes.
+ * @return the encrypted base 64 chars.
+ */
+ public String encrypt(String input, byte[] key) {
+ StringBuilder output = new StringBuilder(input.length() * 2);
+ try {
+ encrypt(input, key, output);
+ } catch (IOException e) {
+ // Never happens when appending to a StringBuilder.
+ throw new UncheckedIOException(e);
+ }
+ return output.toString();
+ }
+
/**
* Encrypts an input string to base 64 characters compatible with JSON.
*
@@ -97,6 +115,23 @@ public class CharStreamEncrypter {
}
}
+ /**
+ * Decrypts an input string previously encrypted with {@link #encrypt}.
+ *
+ * @param key AES key, can either 16, 24 or 32 bytes.
+ * @return the decrypted chars.
+ */
+ public String decrypt(String input, byte[] key) {
+ StringBuilder output = new StringBuilder((int) (input.length() * 0.6f));
+ try {
+ decrypt(input, key, output);
+ } catch (IOException e) {
+ // Never happens when appending to a StringBuilder.
+ throw new UncheckedIOException(e);
+ }
+ return output.toString();
+ }
+
/**
* Decrypts an input string previously encrypted with {@link #encrypt}.
*
diff --git
a/encryption/src/main/java/org/apache/solr/encryption/crypto/EncryptingIndexOutput.java
b/encryption/src/main/java/org/apache/solr/encryption/crypto/EncryptingIndexOutput.java
index 7e18770..04a56f2 100644
---
a/encryption/src/main/java/org/apache/solr/encryption/crypto/EncryptingIndexOutput.java
+++
b/encryption/src/main/java/org/apache/solr/encryption/crypto/EncryptingIndexOutput.java
@@ -83,7 +83,6 @@ public class EncryptingIndexOutput extends IndexOutput {
throws IOException {
super("Encrypting " + indexOutput.toString(), indexOutput.getName());
this.indexOutput = indexOutput;
-
byte[] iv = generateRandomIv();
encrypter = factory.create(key, iv);
encrypter.init(0);
diff --git
a/encryption/src/main/java/org/apache/solr/update/TransactionLog.java
b/encryption/src/main/java/org/apache/solr/update/TransactionLog.java
index 309f783..5ced26c 100644
--- a/encryption/src/main/java/org/apache/solr/update/TransactionLog.java
+++ b/encryption/src/main/java/org/apache/solr/update/TransactionLog.java
@@ -74,7 +74,7 @@ public class TransactionLog implements Closeable {
public static final String END_MESSAGE = "SOLR_TLOG_END";
long id;
- Path tlog;
+ protected Path tlog;
protected FileChannel channel;
protected OutputStream os;
// all accesses to this stream should be synchronized on "this" (The
TransactionLog)
diff --git
a/encryption/src/test/java/org/apache/solr/encryption/EncryptionHeavyLoadTest.java
b/encryption/src/test/java/org/apache/solr/encryption/EncryptionHeavyLoadTest.java
index aa6be0c..3459cf6 100644
---
a/encryption/src/test/java/org/apache/solr/encryption/EncryptionHeavyLoadTest.java
+++
b/encryption/src/test/java/org/apache/solr/encryption/EncryptionHeavyLoadTest.java
@@ -18,16 +18,12 @@ package org.apache.solr.encryption;
import com.carrotsearch.randomizedtesting.generators.RandomStrings;
import org.apache.solr.client.solrj.SolrQuery;
-import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
-import org.apache.solr.client.solrj.request.GenericSolrRequest;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.cloud.MiniSolrCloudCluster;
import org.apache.solr.cloud.SolrCloudTestCase;
import org.apache.solr.common.SolrInputDocument;
-import org.apache.solr.common.params.ModifiableSolrParams;
-import org.apache.solr.common.util.NamedList;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.BeforeClass;
@@ -44,9 +40,11 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
-import static org.apache.solr.encryption.EncryptionRequestHandler.*;
-import static
org.apache.solr.encryption.EncryptionRequestHandlerTest.EncryptionStatus;
-import static org.apache.solr.encryption.TestingKeySupplier.*;
+import static org.apache.solr.encryption.EncryptionRequestHandler.NO_KEY_ID;
+import static org.apache.solr.encryption.EncryptionTestUtil.EncryptionStatus;
+import static org.apache.solr.encryption.TestingKeySupplier.KEY_ID_1;
+import static org.apache.solr.encryption.TestingKeySupplier.KEY_ID_2;
+import static org.apache.solr.encryption.TestingKeySupplier.KEY_ID_3;
/**
* Tests the encryption handler under heavy concurrent load test.
@@ -192,12 +190,12 @@ public class EncryptionHeavyLoadTest extends
SolrCloudTestCase {
private boolean encrypt(String keyId, boolean waitForCompletion) throws
Exception {
EncryptionStatus encryptionStatus = sendEncryptionRequests(keyId);
- if (!encryptionStatus.complete) {
+ if (!encryptionStatus.isComplete()) {
if (!waitForCompletion) {
return false;
}
print("waiting for encryption completion for keyId=" + keyId);
- while (!encryptionStatus.complete) {
+ while (!encryptionStatus.isComplete()) {
if (isTimeElapsed()) {
return false;
}
@@ -214,16 +212,8 @@ public class EncryptionHeavyLoadTest extends
SolrCloudTestCase {
}
private EncryptionStatus sendEncryptionRequests(String keyId) {
- ModifiableSolrParams params = new ModifiableSolrParams();
- params.set(PARAM_KEY_ID, keyId);
- GenericSolrRequest encryptRequest = new
GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/encrypt", params);
- EncryptionStatus encryptionStatus = new EncryptionStatus();
- testUtil.forAllReplicas(replica -> {
- NamedList<Object> response = testUtil.requestCore(encryptRequest,
replica);
- encryptionStatus.success &= response.get(STATUS).equals(STATUS_SUCCESS);
- encryptionStatus.complete &=
response.get(ENCRYPTION_STATE).equals(STATE_COMPLETE);
- });
- print("encrypt keyId=" + keyId + " => response success=" +
encryptionStatus.success + " complete=" + encryptionStatus.complete);
+ EncryptionStatus encryptionStatus = testUtil.encrypt(keyId);
+ print("encrypt keyId=" + keyId + " => response success=" +
encryptionStatus.isSuccess() + " complete=" + encryptionStatus.isComplete());
return encryptionStatus;
}
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 05e3da1..5b70650 100644
---
a/encryption/src/test/java/org/apache/solr/encryption/EncryptionRequestHandlerTest.java
+++
b/encryption/src/test/java/org/apache/solr/encryption/EncryptionRequestHandlerTest.java
@@ -19,15 +19,10 @@ 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.SolrRequest;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
-import org.apache.solr.client.solrj.request.GenericSolrRequest;
import org.apache.solr.cloud.MiniSolrCloudCluster;
import org.apache.solr.cloud.SolrCloudTestCase;
-import org.apache.solr.common.params.ModifiableSolrParams;
-import org.apache.solr.common.util.NamedList;
-import org.apache.solr.common.util.RetryUtil;
import org.apache.solr.encryption.crypto.AesCtrEncrypterFactory;
import org.junit.AfterClass;
import org.junit.BeforeClass;
@@ -36,10 +31,10 @@ import org.junit.Test;
import java.io.IOException;
import java.util.Map;
import java.util.UUID;
-import java.util.concurrent.TimeUnit;
import static
org.apache.solr.encryption.EncryptionDirectoryFactory.PROPERTY_INNER_ENCRYPTION_DIRECTORY_FACTORY;
-import static org.apache.solr.encryption.EncryptionRequestHandler.*;
+import static org.apache.solr.encryption.EncryptionRequestHandler.NO_KEY_ID;
+import static org.apache.solr.encryption.EncryptionTestUtil.EncryptionStatus;
import static org.apache.solr.encryption.EncryptionUtil.getKeyIdFromCommit;
import static org.apache.solr.encryption.TestingKeySupplier.KEY_ID_1;
import static org.apache.solr.encryption.TestingKeySupplier.KEY_ID_2;
@@ -93,9 +88,9 @@ public class EncryptionRequestHandlerTest extends
SolrCloudTestCase {
@Test
public void testEncryptionFromNoKeysToOneKey_NoIndex() throws Exception {
// Send an encrypt request with a key id on an empty index.
- EncryptionStatus encryptionStatus = encrypt(KEY_ID_1);
- assertTrue(encryptionStatus.success);
- assertTrue(encryptionStatus.complete);
+ EncryptionStatus encryptionStatus = testUtil.encrypt(KEY_ID_1);
+ assertTrue(encryptionStatus.isSuccess());
+ assertTrue(encryptionStatus.isComplete());
// Index some documents to create a first segment.
testUtil.indexDocsAndCommit("weather broadcast");
@@ -111,14 +106,14 @@ public class EncryptionRequestHandlerTest extends
SolrCloudTestCase {
@Test
public void testEncryptionFromNoKeysToOneKeyToNoKeys_NoIndex() throws
Exception {
// Send an encrypt request with a key id on an empty index.
- EncryptionStatus encryptionStatus = encrypt(KEY_ID_1);
- assertTrue(encryptionStatus.success);
- assertTrue(encryptionStatus.complete);
+ EncryptionStatus encryptionStatus = testUtil.encrypt(KEY_ID_1);
+ assertTrue(encryptionStatus.isSuccess());
+ assertTrue(encryptionStatus.isComplete());
// Send another encrypt request with no key id, still on the empty index.
- encryptionStatus = encrypt(NO_KEY_ID);
- assertTrue(encryptionStatus.success);
- assertTrue(encryptionStatus.complete);
+ encryptionStatus = testUtil.encrypt(NO_KEY_ID);
+ assertTrue(encryptionStatus.isSuccess());
+ assertTrue(encryptionStatus.isComplete());
// Index some documents to create a first segment.
testUtil.indexDocsAndCommit("weather broadcast");
@@ -145,11 +140,11 @@ public class EncryptionRequestHandlerTest extends
SolrCloudTestCase {
forceClearText = false;
// Send an encrypt request with a key id.
- EncryptionStatus encryptionStatus = encrypt(KEY_ID_1);
- assertTrue(encryptionStatus.success);
- assertFalse(encryptionStatus.complete);
+ EncryptionStatus encryptionStatus = testUtil.encrypt(KEY_ID_1);
+ assertTrue(encryptionStatus.isSuccess());
+ assertFalse(encryptionStatus.isComplete());
- waitUntilEncryptionIsComplete(KEY_ID_1);
+ testUtil.waitUntilEncryptionIsComplete(KEY_ID_1);
// Verify that the segment is encrypted.
forceClearText = true;
@@ -169,11 +164,11 @@ public class EncryptionRequestHandlerTest extends
SolrCloudTestCase {
testUtil.indexDocsAndCommit("foggy weather");
// Send an encrypt request with another key id.
- EncryptionStatus encryptionStatus = encrypt(KEY_ID_2);
- assertTrue(encryptionStatus.success);
- assertFalse(encryptionStatus.complete);
+ EncryptionStatus encryptionStatus = testUtil.encrypt(KEY_ID_2);
+ assertTrue(encryptionStatus.isSuccess());
+ assertFalse(encryptionStatus.isComplete());
- waitUntilEncryptionIsComplete(KEY_ID_2);
+ testUtil.waitUntilEncryptionIsComplete(KEY_ID_2);
// Verify that the segment is encrypted.
forceClearText = true;
@@ -192,11 +187,11 @@ public class EncryptionRequestHandlerTest extends
SolrCloudTestCase {
testUtil.indexDocsAndCommit("foggy weather");
// Send an encrypt request with no key id.
- EncryptionStatus encryptionStatus = encrypt(NO_KEY_ID);
- assertTrue(encryptionStatus.success);
- assertFalse(encryptionStatus.complete);
+ EncryptionStatus encryptionStatus = testUtil.encrypt(NO_KEY_ID);
+ assertTrue(encryptionStatus.isSuccess());
+ assertFalse(encryptionStatus.isComplete());
- waitUntilEncryptionIsComplete(NO_KEY_ID);
+ testUtil.waitUntilEncryptionIsComplete(NO_KEY_ID);
// Verify that the segment is cleartext.
forceClearText = true;
@@ -208,11 +203,11 @@ public class EncryptionRequestHandlerTest extends
SolrCloudTestCase {
testUtil.indexDocsAndCommit("cloudy weather");
// Send an encrypt request with another key id.
- encryptionStatus = encrypt(KEY_ID_2);
- assertTrue(encryptionStatus.success);
- assertFalse(encryptionStatus.complete);
+ encryptionStatus = testUtil.encrypt(KEY_ID_2);
+ assertTrue(encryptionStatus.isSuccess());
+ assertFalse(encryptionStatus.isComplete());
- waitUntilEncryptionIsComplete(KEY_ID_2);
+ testUtil.waitUntilEncryptionIsComplete(KEY_ID_2);
// Verify that the segment is encrypted.
forceClearText = true;
@@ -223,36 +218,6 @@ public class EncryptionRequestHandlerTest extends
SolrCloudTestCase {
testUtil.assertQueryReturns("weather", 4);
}
- private EncryptionStatus encrypt(String keyId) {
- ModifiableSolrParams params = new ModifiableSolrParams();
- params.set(PARAM_KEY_ID, keyId);
- GenericSolrRequest encryptRequest = new
GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/encrypt", params);
- EncryptionStatus encryptionStatus = new EncryptionStatus();
- testUtil.forAllReplicas(replica -> {
- NamedList<Object> response = testUtil.requestCore(encryptRequest,
replica);
- encryptionStatus.success &= response.get(STATUS).equals(STATUS_SUCCESS);
- encryptionStatus.complete &=
response.get(ENCRYPTION_STATE).equals(STATE_COMPLETE);
- });
- return encryptionStatus;
- }
-
- private void waitUntilEncryptionIsComplete(String keyId) throws
InterruptedException {
- RetryUtil.retryUntil("Timeout waiting for encryption completion",
- 50,
- 100,
- TimeUnit.MILLISECONDS,
- () -> {
- EncryptionStatus encryptionStatus;
- try {
- encryptionStatus = encrypt(keyId);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- assertTrue(encryptionStatus.success);
- return encryptionStatus.complete;
- });
- }
-
private static void clearMockValues() {
forceClearText = false;
soleKeyIdAllowed = null;
@@ -304,9 +269,4 @@ public class EncryptionRequestHandlerTest extends
SolrCloudTestCase {
}
}
}
-
- public static class EncryptionStatus {
- public boolean success = true;
- public boolean complete = true;
- }
}
diff --git
a/encryption/src/test/java/org/apache/solr/encryption/EncryptionTestUtil.java
b/encryption/src/test/java/org/apache/solr/encryption/EncryptionTestUtil.java
index d5c9128..61d36c4 100644
---
a/encryption/src/test/java/org/apache/solr/encryption/EncryptionTestUtil.java
+++
b/encryption/src/test/java/org/apache/solr/encryption/EncryptionTestUtil.java
@@ -23,19 +23,30 @@ import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.impl.Http2SolrClient;
import org.apache.solr.client.solrj.request.CoreAdminRequest;
+import org.apache.solr.client.solrj.request.GenericSolrRequest;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.params.CoreAdminParams;
+import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.NamedList;
+import org.apache.solr.common.util.RetryUtil;
import org.junit.Assert;
import java.io.IOException;
import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
+import static
org.apache.solr.encryption.EncryptionRequestHandler.ENCRYPTION_STATE;
+import static org.apache.solr.encryption.EncryptionRequestHandler.PARAM_KEY_ID;
+import static
org.apache.solr.encryption.EncryptionRequestHandler.STATE_COMPLETE;
+import static org.apache.solr.encryption.EncryptionRequestHandler.STATUS;
+import static
org.apache.solr.encryption.EncryptionRequestHandler.STATUS_SUCCESS;
+import static org.junit.Assert.assertTrue;
+
/**
* Utility methods for encryption tests.
*/
@@ -92,6 +103,36 @@ public class EncryptionTestUtil {
Assert.assertEquals(expectedNumResults, response.getResults().size());
}
+ public EncryptionStatus encrypt(String keyId) {
+ ModifiableSolrParams params = new ModifiableSolrParams();
+ params.set(PARAM_KEY_ID, keyId);
+ GenericSolrRequest encryptRequest = new
GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/encrypt", params);
+ EncryptionStatus encryptionStatus = new EncryptionStatus();
+ forAllReplicas(replica -> {
+ NamedList<Object> response = requestCore(encryptRequest, replica);
+ encryptionStatus.success &= response.get(STATUS).equals(STATUS_SUCCESS);
+ encryptionStatus.complete &=
response.get(ENCRYPTION_STATE).equals(STATE_COMPLETE);
+ });
+ return encryptionStatus;
+ }
+
+ public void waitUntilEncryptionIsComplete(String keyId) throws
InterruptedException {
+ RetryUtil.retryUntil("Timeout waiting for encryption completion",
+ 50,
+ 100,
+ TimeUnit.MILLISECONDS,
+ () -> {
+ EncryptionStatus encryptionStatus;
+ try {
+ encryptionStatus = encrypt(keyId);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ assertTrue(encryptionStatus.success);
+ return encryptionStatus.complete;
+ });
+ }
+
/**
* Reloads the leader replica core of the first shard of the collection.
*/
@@ -155,4 +196,24 @@ public class EncryptionTestUtil {
super(msg, cause);
}
}
+
+ /** Status of the encryption of potentially multiple cores. */
+ public static class EncryptionStatus {
+
+ private boolean success;
+ private boolean complete;
+
+ private EncryptionStatus() {
+ this.success = true;
+ this.complete = true;
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public boolean isComplete() {
+ return complete;
+ }
+ }
}
diff --git
a/encryption/src/test/java/org/apache/solr/encryption/EncryptionUpdateLogTest.java
b/encryption/src/test/java/org/apache/solr/encryption/EncryptionUpdateLogTest.java
index 1dec37c..f174336 100644
---
a/encryption/src/test/java/org/apache/solr/encryption/EncryptionUpdateLogTest.java
+++
b/encryption/src/test/java/org/apache/solr/encryption/EncryptionUpdateLogTest.java
@@ -16,89 +16,78 @@
*/
package org.apache.solr.encryption;
-import org.apache.solr.client.solrj.SolrClient;
-import org.apache.solr.client.solrj.SolrRequest;
+import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
-import org.apache.solr.client.solrj.request.GenericSolrRequest;
-import org.apache.solr.client.solrj.request.QueryRequest;
-import org.apache.solr.client.solrj.request.UpdateRequest;
-import org.apache.solr.cloud.AbstractDistribZkTestBase;
+import org.apache.solr.cloud.MiniSolrCloudCluster;
import org.apache.solr.cloud.SolrCloudTestCase;
-import org.apache.solr.common.params.ModifiableSolrParams;
-import org.apache.solr.common.util.NamedList;
-import org.apache.solr.common.util.RetryUtil;
-import org.apache.solr.embedded.JettySolrRunner;
-import org.hamcrest.MatcherAssert;
+import org.apache.solr.encryption.crypto.AesCtrEncrypterFactory;
+import org.apache.solr.encryption.crypto.DecryptingChannelInputStream;
+import org.apache.solr.encryption.crypto.EncryptingOutputStream;
+import org.apache.solr.update.TransactionLog;
import org.junit.After;
+import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Deque;
-import java.util.List;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.channels.FileChannel;
+import java.nio.file.Path;
+import java.util.Collection;
import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
+import java.util.concurrent.atomic.AtomicInteger;
-import static org.apache.solr.encryption.EncryptionRequestHandler.*;
-import static
org.apache.solr.encryption.TestingEncryptionUpdateLog.reencryptionCallCount;
-import static org.apache.solr.encryption.TestingKeySupplier.*;
-import static org.hamcrest.Matchers.*;
+import static org.apache.solr.encryption.EncryptionRequestHandler.NO_KEY_ID;
+import static org.apache.solr.encryption.TestingKeySupplier.KEY_ID_1;
+import static org.apache.solr.encryption.TestingKeySupplier.KEY_ID_2;
+import static org.apache.solr.encryption.EncryptionTestUtil.EncryptionStatus;
+import static
org.apache.solr.encryption.EncryptionUpdateLogTest.TestingEncryptionUpdateLog.*;
/**
* Tests {@link EncryptionUpdateLog} and {@link EncryptionTransactionLog}.
*/
public class EncryptionUpdateLogTest extends SolrCloudTestCase {
- private static final int NUM_SHARDS = 1;
- private static final int NUM_REPLICAS = 4;
- private static final long TIMEOUT = DEFAULT_TIMEOUT;
+ private static final String COLLECTION_PREFIX =
EncryptionRequestHandlerTest.class.getSimpleName() + "-collection-";
+
+ private static final int NUM_SHARDS = 2;
+ private static final int NUM_REPLICAS = 2;
private String collectionName;
- private List<SolrClient> solrClients;
- private int nonLeaderIndex;
+ private CloudSolrClient solrClient;
+ private EncryptionTestUtil testUtil;
@BeforeClass
- public static void setupClass() throws Exception {
- configureCluster(NUM_SHARDS * NUM_REPLICAS)
+ public static void beforeClass() throws Exception {
+ EncryptionTestUtil.setInstallDirProperty();
+ System.setProperty("solr.updateLog",
TestingEncryptionUpdateLog.class.getName());
+ cluster = new MiniSolrCloudCluster.Builder(NUM_SHARDS, createTempDir())
.addConfig("config", EncryptionTestUtil.getConfigPath("collection1"))
.configure();
}
+ @AfterClass
+ public static void afterClass() throws Exception {
+ cluster.shutdown();
+ }
+
+ @Override
@Before
- public void setupTest() throws Exception {
- collectionName = "collection" + UUID.randomUUID();
- CollectionAdminRequest.createCollection(collectionName, "config",
NUM_SHARDS, NUM_REPLICAS)
- .processAndWait(cluster.getSolrClient(), TIMEOUT);
- AbstractDistribZkTestBase.waitForRecoveriesToFinish(
- collectionName, cluster.getZkStateReader(), false, true, TIMEOUT);
-
- solrClients = new ArrayList<>();
- nonLeaderIndex = 0;
- for (JettySolrRunner jettySolrRunner : cluster.getJettySolrRunners()) {
- if (!jettySolrRunner
- .getBaseUrl()
- .toString()
-
.equals(getCollectionState(collectionName).getLeader("shard1").getBaseUrl())) {
- nonLeaderIndex = solrClients.size();
- }
- solrClients.add(jettySolrRunner.newClient());
- }
+ public void setUp() throws Exception {
+ super.setUp();
+ collectionName = COLLECTION_PREFIX + UUID.randomUUID();
+ solrClient = cluster.getSolrClient();
+ CollectionAdminRequest.createCollection(collectionName, NUM_SHARDS,
NUM_REPLICAS).process(solrClient);
+ cluster.waitForActiveCollection(collectionName, NUM_SHARDS, NUM_SHARDS *
NUM_REPLICAS);
+ testUtil = new EncryptionTestUtil(solrClient, collectionName);
}
+ @Override
@After
- public void tearDownTest() throws Exception {
- try {
- if (solrClients != null) {
- for (SolrClient solrClient : solrClients) {
- solrClient.close();
- }
- }
- } finally {
-
CollectionAdminRequest.deleteCollection(collectionName).process(cluster.getSolrClient());
- }
+ public void tearDown() throws Exception {
+
CollectionAdminRequest.deleteCollection(collectionName).process(solrClient);
+ super.tearDown();
}
@Test
@@ -117,149 +106,133 @@ public class EncryptionUpdateLogTest extends
SolrCloudTestCase {
}
private void checkEncryptionFromNoKeysToOneKey(String keyId) throws
Exception {
+ EncryptionStatus encryptionStatus = testUtil.encrypt(keyId);
+ assertTrue(encryptionStatus.isSuccess());
+ assertTrue(encryptionStatus.isComplete());
+
+ resetCounters();
+ testUtil.indexDocsAndCommit("doc0");
+ assertEquals(NUM_REPLICAS, encryptedLogWriteCount.get());
+ assertEquals(0, encryptedLogReadCount.get());
+ assertEquals(0, reencryptionCallCount.get());
- reencryptionCallCount.set(0);
- EncryptionStatus encryptionStatus = encrypt(keyId);
- assertTrue(encryptionStatus.statusSuccess);
- assertTrue(encryptionStatus.stateComplete);
-
- new UpdateRequest()
- .add(sdoc("id", "0", "text", "zero"))
- .commit(cluster.getSolrClient(), collectionName);
-
- for (SolrClient solrClient : solrClients) {
- checkNumUpdates(solrClient, 1);
- }
-
- cluster.getJettySolrRunner(nonLeaderIndex).stop();
- AbstractDistribZkTestBase.waitForRecoveriesToFinish(
- collectionName, cluster.getZkStateReader(), false, true, TIMEOUT);
-
- new UpdateRequest()
- .add(sdoc("id", "1", "text", "one"))
- .deleteById("2")
- .deleteByQuery("text:three")
- .commit(cluster.getSolrClient(), collectionName);
-
- cluster.getJettySolrRunner(nonLeaderIndex).start();
- AbstractDistribZkTestBase.waitForRecoveriesToFinish(
- collectionName, cluster.getZkStateReader(), false, true, TIMEOUT);
-
- for (SolrClient solrClient : solrClients) {
- checkNumUpdates(solrClient, 4);
- }
-
+ resetCounters();
+ testUtil.reloadCores();
+ assertEquals(0, encryptedLogWriteCount.get());
+ assertEquals(NUM_REPLICAS, encryptedLogReadCount.get());
assertEquals(0, reencryptionCallCount.get());
}
private void checkEncryptionFromOneKeyToAnotherKey(String fromKeyId, String
toKeyId) throws Exception {
checkEncryptionFromNoKeysToOneKey(fromKeyId);
- reencryptionCallCount.set(0);
- encrypt(fromKeyId);
- waitUntilEncryptionIsComplete(toKeyId);
-
- new UpdateRequest()
- .add(sdoc("id", "2", "text", "four"))
- .commit(cluster.getSolrClient(), collectionName);
-
- for (SolrClient solrClient : solrClients) {
- checkNumUpdates(solrClient, 5);
+ for (int i = 1 ; i <= 3; i++) {
+ resetCounters();
+ testUtil.indexDocsAndCommit("doc" + i);
+ assertEquals(NUM_REPLICAS, encryptedLogWriteCount.get());
+ assertEquals(0, encryptedLogReadCount.get());
+ assertEquals(0, reencryptionCallCount.get());
}
- cluster.getJettySolrRunner(nonLeaderIndex).stop();
- AbstractDistribZkTestBase.waitForRecoveriesToFinish(
- collectionName, cluster.getZkStateReader(), false, true, TIMEOUT);
+ resetCounters();
+ EncryptionStatus encryptionStatus = testUtil.encrypt(toKeyId);
+ assertTrue(encryptionStatus.isSuccess());
+ testUtil.waitUntilEncryptionIsComplete(toKeyId);
+ assertEquals(0, encryptedLogWriteCount.get());
+ assertEquals(0, encryptedLogReadCount.get());
+ assertEquals(4 * NUM_REPLICAS, reencryptionCallCount.get());
+
+ resetCounters();
+ testUtil.reloadCores();
+ assertEquals(0, encryptedLogWriteCount.get());
+ assertEquals(4 * NUM_REPLICAS, encryptedLogReadCount.get());
+ assertEquals(0, reencryptionCallCount.get());
+ }
- new UpdateRequest()
- .add(sdoc("id", "3", "text", "five"))
- .deleteById("6")
- .deleteByQuery("text:seven")
- .commit(cluster.getSolrClient(), collectionName);
+ public static class TestingEncryptionUpdateLog extends EncryptionUpdateLog {
- cluster.getJettySolrRunner(nonLeaderIndex).start();
- AbstractDistribZkTestBase.waitForRecoveriesToFinish(
- collectionName, cluster.getZkStateReader(), false, true, TIMEOUT);
+ static final AtomicInteger reencryptionCallCount = new AtomicInteger();
+ static final AtomicInteger encryptedLogWriteCount = new AtomicInteger();
+ static final AtomicInteger encryptedLogReadCount = new AtomicInteger();
- for (SolrClient solrClient : solrClients) {
- checkNumUpdates(solrClient, 8);
+ static void resetCounters() {
+ reencryptionCallCount.set(0);
+ encryptedLogWriteCount.set(0);
+ encryptedLogReadCount.set(0);
}
- MatcherAssert.assertThat(reencryptionCallCount.get(),
greaterThanOrEqualTo(1));
- }
+ @Override
+ public TransactionLog newTransactionLog(Path tlogFile, Collection<String>
globalStrings, boolean openExisting) {
+ return new TestingTransactionLog(tlogFile, globalStrings, openExisting,
directorySupplier);
+ }
- private EncryptionStatus encrypt(String keyId) throws Exception {
- ModifiableSolrParams params = new ModifiableSolrParams();
- params.set(PARAM_KEY_ID, keyId);
-
- boolean statusSuccess = true;
- boolean stateComplete = true;
- for (SolrClient solrClient : solrClients) {
- NamedList<Object> response = solrClient.request(
- new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/encrypt",
params), collectionName);
- statusSuccess &= STATUS_SUCCESS.equals(response.get(STATUS));
- stateComplete &= STATE_COMPLETE.equals(response.get(ENCRYPTION_STATE));
+ protected void reencrypt(FileChannel inputChannel,
+ String inputKeyRef,
+ OutputStream outputStream,
+ String activeKeyRef,
+ EncryptionDirectory directory)
+ throws IOException {
+ reencryptionCallCount.incrementAndGet();
+ super.reencrypt(inputChannel, inputKeyRef, outputStream, activeKeyRef,
directory);
}
- return new EncryptionStatus(statusSuccess, stateComplete);
- }
- private void waitUntilEncryptionIsComplete(String keyId) throws
InterruptedException {
- RetryUtil.retryUntil("Timeout waiting for encryption completion",
- 50,
- 100,
- TimeUnit.MILLISECONDS,
- () -> {
- EncryptionStatus encryptionStatus;
- try {
- encryptionStatus = encrypt(keyId);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- assertTrue(encryptionStatus.statusSuccess);
- return encryptionStatus.stateComplete;
- });
- }
+ private static class TestingTransactionLog extends
EncryptionTransactionLog {
- @SuppressWarnings("unchecked")
- private void checkNumUpdates(SolrClient solrClient, int numExpected) throws
Exception {
+ TestingTransactionLog(Path tlogFile,
+ Collection<String> globalStrings,
+ boolean openExisting,
+ EncryptionDirectorySupplier directorySupplier) {
+ this(tlogFile, globalStrings, openExisting, directorySupplier, new
IvHolder());
+ }
- final QueryRequest reqV = new QueryRequest(params("qt", "/get",
"getVersions", "12345"));
- final NamedList<?> rspV = solrClient.request(reqV, collectionName);
- final List<Long> versions = (List<Long>) rspV.get("versions");
- assertEquals(versions.toString(), numExpected, versions.size());
- if (numExpected == 0) {
- return;
- }
+ TestingTransactionLog(Path tlogFile,
+ Collection<String> globalStrings,
+ boolean openExisting,
+ EncryptionDirectorySupplier directorySupplier,
+ IvHolder ivHolder) {
+ super(tlogFile,
+ globalStrings,
+ openExisting,
+ new TestingEncryptionOutputStreamOpener(directorySupplier,
ivHolder),
+ new TestingEncryptionChannelInputStreamOpener(directorySupplier,
ivHolder));
+ }
- final Deque<Long> absVersions =
-
versions.stream().map(Math::abs).sorted().collect(Collectors.toCollection(ArrayDeque::new));
- final Long minVersion = absVersions.getFirst();
- final Long maxVersion = absVersions.getLast();
-
- for (boolean skipDbq : new boolean[]{false, true}) {
- final QueryRequest reqU =
- new QueryRequest(
- params(
- "qt",
- "/get",
- "getUpdates",
- minVersion + "..." + maxVersion,
- "skipDbq",
- Boolean.toString(skipDbq)));
- final NamedList<?> rspU = solrClient.request(reqU, collectionName);
- final List<?> updatesList = (List<?>) rspU.get("updates");
- assertEquals(updatesList.toString(), numExpected, updatesList.size());
- }
- }
+ static class TestingEncryptionOutputStreamOpener extends
EncryptionOutputStreamOpener {
+
+ TestingEncryptionOutputStreamOpener(EncryptionDirectorySupplier
directorySupplier, IvHolder ivHolder) {
+ super(directorySupplier, ivHolder);
+ }
- private static class EncryptionStatus {
+ @Override
+ protected EncryptingOutputStream
createEncryptingOutputStream(OutputStream outputStream,
+ long
position,
+ byte[]
iv,
+ byte[]
key,
+
AesCtrEncrypterFactory factory)
+ throws IOException {
+ encryptedLogWriteCount.incrementAndGet();
+ return super.createEncryptingOutputStream(outputStream, position,
iv, key, factory);
+ }
+ }
- final boolean statusSuccess;
- final boolean stateComplete;
+ static class TestingEncryptionChannelInputStreamOpener extends
EncryptionChannelInputStreamOpener {
- EncryptionStatus(boolean statusSuccess, boolean stateComplete) {
- this.statusSuccess = statusSuccess;
- this.stateComplete = stateComplete;
+ TestingEncryptionChannelInputStreamOpener(EncryptionDirectorySupplier
directorySupplier,
+ IvHolder ivHolder)
{
+ super(directorySupplier, ivHolder);
+ }
+
+ @Override
+ protected DecryptingChannelInputStream
createDecryptingChannelInputStream(FileChannel channel,
+
long offset,
+
long position,
+
byte[] key,
+
AesCtrEncrypterFactory factory)
+ throws IOException {
+ encryptedLogReadCount.incrementAndGet();
+ return super.createDecryptingChannelInputStream(channel, offset,
position, key, factory);
+ }
+ }
}
}
}
diff --git
a/encryption/src/test/java/org/apache/solr/encryption/TestingKeySupplier.java
b/encryption/src/test/java/org/apache/solr/encryption/TestingKeySupplier.java
index 6c4556e..3dafea0 100644
---
a/encryption/src/test/java/org/apache/solr/encryption/TestingKeySupplier.java
+++
b/encryption/src/test/java/org/apache/solr/encryption/TestingKeySupplier.java
@@ -18,6 +18,7 @@ package org.apache.solr.encryption;
import org.apache.lucene.index.IndexFileNames;
import org.apache.solr.common.util.NamedList;
+import org.apache.solr.core.CoreContainer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
@@ -177,7 +178,7 @@ public class TestingKeySupplier implements KeySupplier {
private static final KeySupplier SINGLETON = new TestingKeySupplier();
@Override
- public void init(NamedList<?> args) {
+ public void init(NamedList<?> args, CoreContainer coreContainer) {
// Do nothing.
}
diff --git
a/encryption/src/test/java/org/apache/solr/encryption/crypto/AesCtrEncrypterTest.java
b/encryption/src/test/java/org/apache/solr/encryption/crypto/AesCtrEncrypterTest.java
index c8f2809..8ec4eff 100644
---
a/encryption/src/test/java/org/apache/solr/encryption/crypto/AesCtrEncrypterTest.java
+++
b/encryption/src/test/java/org/apache/solr/encryption/crypto/AesCtrEncrypterTest.java
@@ -20,6 +20,7 @@ import com.carrotsearch.randomizedtesting.RandomizedTest;
import org.junit.Test;
import static org.apache.solr.encryption.crypto.AesCtrUtil.*;
+import static
org.apache.solr.encryption.crypto.CryptoTestUtil.encrypterFactory;
import static org.junit.Assert.assertArrayEquals;
/**
@@ -55,13 +56,6 @@ public class AesCtrEncrypterTest extends RandomizedTest {
}
}
- private AesCtrEncrypterFactory encrypterFactory() {
- if (LightAesCtrEncrypter.isSupported()) {
- return randomBoolean() ? CipherAesCtrEncrypter.FACTORY :
LightAesCtrEncrypter.FACTORY;
- }
- return CipherAesCtrEncrypter.FACTORY;
- }
-
private static byte[] generateRandomBytes(int numBytes) {
byte[] b = new byte[numBytes];
// Random.nextBytes(byte[]) does not produce good enough randomness here,
diff --git
a/encryption/src/test/java/org/apache/solr/encryption/crypto/CharStreamEncrypterTest.java
b/encryption/src/test/java/org/apache/solr/encryption/crypto/CharStreamEncrypterTest.java
index d6aa764..ed9f098 100644
---
a/encryption/src/test/java/org/apache/solr/encryption/crypto/CharStreamEncrypterTest.java
+++
b/encryption/src/test/java/org/apache/solr/encryption/crypto/CharStreamEncrypterTest.java
@@ -21,6 +21,7 @@ import org.junit.Test;
import java.io.IOException;
+import static
org.apache.solr.encryption.crypto.CryptoTestUtil.encrypterFactory;
import static org.junit.Assert.assertEquals;
/** Tests {@link CharStreamEncrypter}. */
@@ -54,11 +55,4 @@ public class CharStreamEncrypterTest extends RandomizedTest {
encrypter.decrypt(encryptedBuilder.toString(), key, decryptedBuilder);
assertEquals(inputString, decryptedBuilder.toString());
}
-
- private AesCtrEncrypterFactory encrypterFactory() {
- if (LightAesCtrEncrypter.isSupported()) {
- return randomBoolean() ? CipherAesCtrEncrypter.FACTORY :
LightAesCtrEncrypter.FACTORY;
- }
- return CipherAesCtrEncrypter.FACTORY;
- }
}
diff --git
a/encryption/src/test/java/org/apache/solr/encryption/TestingEncryptionUpdateLog.java
b/encryption/src/test/java/org/apache/solr/encryption/crypto/CryptoTestUtil.java
similarity index 52%
rename from
encryption/src/test/java/org/apache/solr/encryption/TestingEncryptionUpdateLog.java
rename to
encryption/src/test/java/org/apache/solr/encryption/crypto/CryptoTestUtil.java
index 497c368..7a4044f 100644
---
a/encryption/src/test/java/org/apache/solr/encryption/TestingEncryptionUpdateLog.java
+++
b/encryption/src/test/java/org/apache/solr/encryption/crypto/CryptoTestUtil.java
@@ -14,24 +14,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.solr.encryption;
+package org.apache.solr.encryption.crypto;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.channels.FileChannel;
-import java.util.concurrent.atomic.AtomicInteger;
+import com.carrotsearch.randomizedtesting.RandomizedTest;
-public class TestingEncryptionUpdateLog extends EncryptionUpdateLog {
-
- public static AtomicInteger reencryptionCallCount = new AtomicInteger();
+/**
+ * Tools for encryption tests.
+ */
+public class CryptoTestUtil {
- protected void reencrypt(FileChannel inputChannel,
- String inputKeyRef,
- OutputStream outputStream,
- String activeKeyRef,
- EncryptionDirectory directory)
- throws IOException {
- super.reencrypt(inputChannel, inputKeyRef, outputStream, activeKeyRef,
directory);
- reencryptionCallCount.incrementAndGet();
+ /**
+ * Provides an {@link AesCtrEncrypterFactory} selected randomly.
+ */
+ public static AesCtrEncrypterFactory encrypterFactory() {
+ if (LightAesCtrEncrypter.isSupported()) {
+ return RandomizedTest.randomBoolean() ? CipherAesCtrEncrypter.FACTORY :
LightAesCtrEncrypter.FACTORY;
+ }
+ return CipherAesCtrEncrypter.FACTORY;
}
}
diff --git
a/encryption/src/test/java/org/apache/solr/encryption/crypto/DecryptingChannelInputStreamTest.java
b/encryption/src/test/java/org/apache/solr/encryption/crypto/DecryptingChannelInputStreamTest.java
index 592037a..621e193 100644
---
a/encryption/src/test/java/org/apache/solr/encryption/crypto/DecryptingChannelInputStreamTest.java
+++
b/encryption/src/test/java/org/apache/solr/encryption/crypto/DecryptingChannelInputStreamTest.java
@@ -30,6 +30,7 @@ import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import static org.apache.solr.encryption.crypto.AesCtrUtil.IV_LENGTH;
+import static
org.apache.solr.encryption.crypto.CryptoTestUtil.encrypterFactory;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -80,10 +81,6 @@ public class DecryptingChannelInputStreamTest extends
RandomizedTest {
assertArrayEquals(source, decryptedBytes);
}
- private static AesCtrEncrypterFactory encrypterFactory() {
- return randomBoolean() ? CipherAesCtrEncrypter.FACTORY :
LightAesCtrEncrypter.FACTORY;
- }
-
private static void write(byte[] source, int offset, int length,
OutputStream os) throws IOException {
if (length > 0) {
byte[] buffer = new byte[randomIntBetween(2, Math.max(length, 2))];
diff --git
a/encryption/src/test/java/org/apache/solr/encryption/crypto/DecryptingIndexInputTest.java
b/encryption/src/test/java/org/apache/solr/encryption/crypto/DecryptingIndexInputTest.java
index 7caf30b..0e6337d 100644
---
a/encryption/src/test/java/org/apache/solr/encryption/crypto/DecryptingIndexInputTest.java
+++
b/encryption/src/test/java/org/apache/solr/encryption/crypto/DecryptingIndexInputTest.java
@@ -35,6 +35,7 @@ import org.apache.lucene.tests.util.LuceneTestCase;
import org.junit.Before;
import org.junit.Test;
+import static
org.apache.solr.encryption.crypto.CryptoTestUtil.encrypterFactory;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -262,8 +263,4 @@ public class DecryptingIndexInputTest extends
RandomizedTest {
indexInput.seek(offset);
return new DecryptingIndexInput(indexInput, key, encrypterFactory());
}
-
- private AesCtrEncrypterFactory encrypterFactory() {
- return randomBoolean() ? CipherAesCtrEncrypter.FACTORY :
LightAesCtrEncrypter.FACTORY;
- }
}
\ No newline at end of file
diff --git
a/encryption/src/test/java/org/apache/solr/encryption/crypto/EncryptingIndexOutputTest.java
b/encryption/src/test/java/org/apache/solr/encryption/crypto/EncryptingIndexOutputTest.java
index edb6849..8f69d6d 100644
---
a/encryption/src/test/java/org/apache/solr/encryption/crypto/EncryptingIndexOutputTest.java
+++
b/encryption/src/test/java/org/apache/solr/encryption/crypto/EncryptingIndexOutputTest.java
@@ -35,6 +35,7 @@ import org.junit.Test;
import static com.carrotsearch.randomizedtesting.RandomizedTest.*;
import static org.apache.solr.encryption.crypto.AesCtrUtil.*;
+import static
org.apache.solr.encryption.crypto.CryptoTestUtil.encrypterFactory;
/**
* Tests {@link EncryptingIndexOutput}.
@@ -117,10 +118,6 @@ public class EncryptingIndexOutputTest extends
BaseDataOutputTestCase<Encrypting
}
}
- private AesCtrEncrypterFactory encrypterFactory() {
- return randomBoolean() ? CipherAesCtrEncrypter.FACTORY :
LightAesCtrEncrypter.FACTORY;
- }
-
/**
* Replaces the {@link java.security.SecureRandom} by a repeatable {@link
Random} for tests.
* This is used to generate a repeatable random IV.
diff --git
a/encryption/src/test/java/org/apache/solr/encryption/crypto/EncryptingOutputStreamTest.java
b/encryption/src/test/java/org/apache/solr/encryption/crypto/EncryptingOutputStreamTest.java
index ad97069..801ffbd 100644
---
a/encryption/src/test/java/org/apache/solr/encryption/crypto/EncryptingOutputStreamTest.java
+++
b/encryption/src/test/java/org/apache/solr/encryption/crypto/EncryptingOutputStreamTest.java
@@ -26,6 +26,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import static org.apache.solr.encryption.crypto.AesCtrUtil.IV_LENGTH;
+import static
org.apache.solr.encryption.crypto.CryptoTestUtil.encrypterFactory;
import static org.junit.Assert.*;
/**
@@ -117,10 +118,6 @@ public class EncryptingOutputStreamTest extends
RandomizedTest {
assertArrayEquals(source, decryptedBytes);
}
- private static AesCtrEncrypterFactory encrypterFactory() {
- return randomBoolean() ? CipherAesCtrEncrypter.FACTORY :
LightAesCtrEncrypter.FACTORY;
- }
-
private static void write(byte[] source, int offset, int length,
OutputStream os) throws IOException {
if (length > 0) {
byte[] buffer = new byte[randomIntBetween(2, Math.max(length, 2))];
diff --git a/encryption/src/test/resources/configs/collection1/solrconfig.xml
b/encryption/src/test/resources/configs/collection1/solrconfig.xml
index 2680a13..d9a7bb8 100644
--- a/encryption/src/test/resources/configs/collection1/solrconfig.xml
+++ b/encryption/src/test/resources/configs/collection1/solrconfig.xml
@@ -47,7 +47,7 @@
<!-- EncryptionUpdateHandler transfers the encryption key ids from a commit
to the next. -->
<updateHandler class="org.apache.solr.encryption.EncryptionUpdateHandler">
<!-- EncryptionUpdateLog encrypts transaction logs if the index is
encrypted. -->
- <updateLog class="org.apache.solr.encryption.TestingEncryptionUpdateLog"/>
+ <updateLog
class="${solr.updateLog:org.apache.solr.encryption.EncryptionUpdateLog}"/>
</updateHandler>
<!-- Encryption handler can be called to trigger the encryption of the index
and update log. -->