From 35a4e8a46adf60988cc29fd82c0115126c1660a3 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Tue, 22 Sep 2026 09:07:01 +0000
Subject: [PATCH] [#992] Apply the confidentiality of a backend index to the running backend (#1000)
---
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ReplayedConfigChangeTest.java | 268 ++++++++++++++
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/DefaultIndex.java | 47 ++
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java | 14
opendj-server-legacy/src/test/java/org/opends/server/backends/pdb/PDBIndexConfidentialityChangeTest.java | 49 ++
opendj-server-legacy/src/test/java/org/opends/server/backends/jeb/JEIndexConfidentialityChangeTest.java | 54 +++
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/IndexConfidentialityChangeTestCase.java | 449 ++++++++++++++++++++++++
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/Index.java | 2
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/AttributeIndex.java | 178 ++++++++-
8 files changed, 1,026 insertions(+), 35 deletions(-)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/AttributeIndex.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/AttributeIndex.java
index fa7e27e..0dd3608 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/AttributeIndex.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/AttributeIndex.java
@@ -1039,7 +1039,7 @@
newIndexIdToIndexes.putAll(updatedIndexes);
// What the new configuration asks of the indexes which stay is decided here, before any of
- // the three writes below, and reported here as well. Decided before the write which applies
+ // the writes below, and reported here as well. Decided before the write which applies
// it: neither the entry limit an index holds nor its in-memory trusted flag is rolled back
// with the transaction, while the removal of the persisted TRUSTED flag is, so an attempt
// which rolls back would leave the raised limit in place, and a replay of it would compare
@@ -1048,10 +1048,18 @@
// rather than once they have committed, because the instruction holds whichever way they go:
// the configuration entry already holds the raised limit when this listener runs, and the
// next open of the index applies it to a tree whose keys were given up under the lower one.
- // Only the limit itself waits for the write which untrusts the index to commit.
+ // Only the limit itself waits for the write which untrusts the index to commit. The
+ // confidentiality is asked of the indexes rather than of the configuration this attribute
+ // index holds, or of the suite they share: the suite is switched outside the writes below and
+ // is not rolled back with them, so from the moment the change is asked for it reads as
+ // applied, while a tree stays in the encoding it was opened under until those writes have
+ // given it up and opened it again. What an index was opened under is what the index alone
+ // carries - and what a give-up of the reopen puts back - so it is the index which answers
+ // whether the change asked for is still to be made.
final List<Index> indexesToUntrust = new ArrayList<>();
+ final List<MatchingRuleIndex> treesToGiveUp = new ArrayList<>();
final List<LocalizableMessage> rebuildMessages = new ArrayList<>();
- planIndexUpdates(updatedIndexes.values(), newConfiguration, indexesToUntrust, rebuildMessages);
+ planIndexUpdates(updatedIndexes.values(), newConfiguration, indexesToUntrust, treesToGiveUp, rebuildMessages);
for (LocalizableMessage rebuildMessage : rebuildMessages)
{
ccr.setAdminActionRequired(true);
@@ -1079,6 +1087,16 @@
});
}
+ // The confidentiality of an index is carried by the CryptoSuite the indexes of its attribute
+ // share, and read from that suite by every index which opens a tree, so the new setting has to
+ // be in force before the indexes added below bind their codecs to it. Applied outside the
+ // writes, which the storage may replay: it changes a live object rather than the storage, and
+ // is idempotent - so it is compared with what the suite carries, not with the configuration.
+ if (cryptoSuite.isEncrypted() != newConfiguration.isConfidentialityEnabled())
+ {
+ entryContainer.setIndexConfidentiality(cryptoSuite, newConfiguration.isConfidentialityEnabled());
+ }
+
// Open added indexes *before* adding them to indexIdToIndexes
final List<TreeName> addedIndexesToRebuild = new ArrayList<>();
entryContainer.getRootContainer().getStorage().write(new WriteOperation()
@@ -1139,23 +1157,66 @@
entryContainer.unlock();
}
- // The only part of what the indexes which stay are asked for that is written down. A change
- // which untrusts none of them - a lowered limit - opens no transaction, rather than one a
- // bounded storage could give up on with nothing to give up; VLVIndex guards its write the
- // same way.
- if (!indexesToUntrust.isEmpty())
+ if (!treesToGiveUp.isEmpty())
{
- entryContainer.getRootContainer().getStorage().write(new WriteOperation()
+ // No query may be reading a tree which is being given up and opened again below - the same
+ // exclusive access the removal of an index takes.
+ entryContainer.lock();
+ try
{
- @Override
- public void run(WriteableTransaction txn) throws Exception
+ writeUntrust(indexesToUntrust, treesToGiveUp);
+ // Held so that a give-up of the reopen below can be put back to what it answered before:
+ // what afterOpen binds is memory, and outlives a write the storage rolled back, while the
+ // tree it opened is not - so a re-apply which trusted that binding would agree with the
+ // new setting and never open the tree the write above just deleted.
+ final List<IndexBinding> bindings = new ArrayList<>(treesToGiveUp.size());
+ for (final MatchingRuleIndex givenUp : treesToGiveUp)
{
- for (final Index updatedIndex : indexesToUntrust)
- {
- updatedIndex.setTrusted(txn, false);
- }
+ bindings.add(new IndexBinding(givenUp));
}
- });
+ try
+ {
+ // In a write of its own, since the storage engines delete and create the tree of an index
+ // as operations of their own - as removing and adding an index does - rather than as a
+ // deletion and a creation one transaction carries together. The write above is the one
+ // which untrusts and deletes, so a change interrupted in between leaves an index the next
+ // open of this container creates empty and keeps degraded, rather than a trusted one
+ // holding nothing.
+ entryContainer.getRootContainer().getStorage().write(new WriteOperation()
+ {
+ @Override
+ public void run(WriteableTransaction txn) throws Exception
+ {
+ for (final MatchingRuleIndex givenUp : treesToGiveUp)
+ {
+ // Which is what binds the codec of the index to the confidentiality now in force.
+ givenUp.open(txn, true);
+ }
+ }
+ });
+ }
+ catch (Exception e)
+ {
+ for (IndexBinding binding : bindings)
+ {
+ binding.revert();
+ }
+ throw e;
+ }
+ }
+ finally
+ {
+ entryContainer.unlock();
+ }
+ }
+ else if (!indexesToUntrust.isEmpty())
+ {
+ // The only part of what the indexes which stay are asked for that is written down. A change
+ // which untrusts none of them - a lowered limit - opens no transaction, rather than one a
+ // bounded storage could give up on with nothing to give up; VLVIndex guards its write the
+ // same way. A change of the entry limit alone leaves the trees where they are, and needs no
+ // exclusive access of its own.
+ writeUntrust(indexesToUntrust, treesToGiveUp);
}
for (final Index updatedIndex : updatedIndexes.values())
{
@@ -1190,6 +1251,77 @@
}
/**
+ * Untrusts the indexes which are kept and may no longer be trusted, in a write of its own, and
+ * gives up the trees of those whose confidentiality changes in that same write.
+ */
+ private void writeUntrust(final List<Index> indexesToUntrust, final List<MatchingRuleIndex> treesToGiveUp)
+ throws Exception
+ {
+ entryContainer.getRootContainer().getStorage().write(new WriteOperation()
+ {
+ @Override
+ public void run(WriteableTransaction txn) throws Exception
+ {
+ for (final Index updatedIndex : indexesToUntrust)
+ {
+ updatedIndex.setTrusted(txn, false);
+ }
+ for (final MatchingRuleIndex givenUp : treesToGiveUp)
+ {
+ /*
+ * What this tree holds was written in the encoding of the setting which has just been given
+ * up, and the codec of the new one does not read it back as what it is: an encrypted record
+ * read as clear text decodes to an empty set of entry IDs rather than failing, which is a
+ * search answered with no entries at all. So the tree is given up here rather than left to
+ * the rebuild this change asks for, leaving an index which answers "undefined" - and is
+ * therefore not used - until that rebuild has run.
+ *
+ * Deleted rather than emptied record by record, which for an index of any size would be a
+ * transaction of its own making. The state record of the index is kept, so the tree the
+ * caller opens again is written back in the serialization this one was created with.
+ *
+ * Guarded by whether the tree is still there: a change asked for again after a give-up of
+ * the write which reopens it finds this index still to give up, since the setting it was
+ * opened under was put back, but the write which deleted it already committed the first
+ * time - deleting an already given-up tree a second time is what the storage answers this
+ * with otherwise.
+ */
+ if (txn.treeExists(givenUp.getName()))
+ {
+ givenUp.delete(txn);
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * What an index answered before its tree was given up, kept so it can be put back if the reopen
+ * which follows gives its commit up. Taken after the write which untrusts, so the trust it holds
+ * is the one that write committed.
+ */
+ private static final class IndexBinding
+ {
+ private final MatchingRuleIndex index;
+ private final boolean encrypted;
+ private final EntryIDSetCodec codec;
+ private final boolean trusted;
+
+ IndexBinding(MatchingRuleIndex index)
+ {
+ this.index = index;
+ this.encrypted = index.isEncrypted();
+ this.codec = index.codec();
+ this.trusted = index.isTrusted();
+ }
+
+ void revert()
+ {
+ index.revertFailedReopen(encrypted, codec, trusted);
+ }
+ }
+
+ /**
* Opens an index this change adds, and answers whether it has to be rebuilt before it is used.
* Answered to the caller rather than reported from here: this runs inside a {@link WriteOperation}
* the storage may replay, and the report belongs to the attempt which commits.
@@ -1207,9 +1339,9 @@
* the same answer on every attempt.
*/
private static void planIndexUpdates(Collection<MatchingRuleIndex> updatedIndexes, BackendIndexCfg newConfig,
- List<Index> indexesToUntrust, List<LocalizableMessage> rebuildMessages)
+ List<Index> indexesToUntrust, List<MatchingRuleIndex> treesToGiveUp, List<LocalizableMessage> rebuildMessages)
{
- for (Index updatedIndex : updatedIndexes)
+ for (MatchingRuleIndex updatedIndex : updatedIndexes)
{
// This index could still be used since a new smaller index size limit doesn't impact validity of the results.
boolean newLimitRequiresRebuild = updatedIndex.getIndexEntryLimit() < newConfig.getIndexEntryLimit();
@@ -1217,12 +1349,16 @@
{
rebuildMessages.add(NOTE_CONFIG_INDEX_ENTRY_LIMIT_REQUIRES_REBUILD.get(updatedIndex.getName()));
}
- // This index could still be used when disabling confidentiality. Asked rather than told: for an
- // index this only compares the configuration with the parameters its crypto suite holds.
- boolean newConfidentialityRequiresRebuild = updatedIndex.setConfidential(newConfig.isConfidentialityEnabled());
+ // A change of the confidentiality gives up the tree of this index, whichever way it goes. The
+ // index answers whether it writes under the setting asked for: the tree it holds is in the
+ // encoding it was opened under, until the change gives it up and opens it again. An index whose
+ // every tree is replaced, as enabling the confidentiality of an equality index does, is not
+ // among those asked, and so has nothing to give up or to open again.
+ boolean newConfidentialityRequiresRebuild = updatedIndex.isEncrypted() != newConfig.isConfidentialityEnabled();
if (newConfidentialityRequiresRebuild)
{
rebuildMessages.add(NOTE_CONFIG_INDEX_CONFIDENTIALITY_REQUIRES_REBUILD.get(updatedIndex.getName()));
+ treesToGiveUp.add(updatedIndex);
}
if (newLimitRequiresRebuild || newConfidentialityRequiresRebuild)
{
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/DefaultIndex.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/DefaultIndex.java
index 88edf18..f1799b5 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/DefaultIndex.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/DefaultIndex.java
@@ -53,7 +53,18 @@
/** The limit on the number of entry IDs that may be indexed by one key. */
private int indexEntryLimit;
- private EntryIDSetCodec codec;
+ /**
+ * Volatile because {@link #afterOpen} binds it to the confidentiality then in force, and an index
+ * whose configuration gives that setting up - or asks for it - is opened again while operations of
+ * other threads are holding this instance.
+ */
+ private volatile EntryIDSetCodec codec;
+ /**
+ * Whether that codec encrypts: the confidentiality this index was opened under. Held here rather
+ * than read from the suite, since the suite carries the setting now in force for the attribute,
+ * which this index writes under only once its tree has been given up and opened again.
+ */
+ private volatile boolean encrypted;
private CryptoSuite cryptoSuite;
/**
@@ -98,7 +109,8 @@
{
final EnumSet<IndexFlag> flags = state.getIndexFlags(txn, getName());
codec = flags.contains(COMPACTED) ? CODEC_V2 : CODEC_V1;
- if (cryptoSuite.isEncrypted())
+ encrypted = cryptoSuite.isEncrypted();
+ if (encrypted)
{
codec = new EntryIDSet.EntryIDSetCodecV3(codec, cryptoSuite);
}
@@ -307,12 +319,6 @@
}
@Override
- public boolean setConfidential(boolean indexConfidential)
- {
- return cryptoSuite.isEncrypted() != indexConfidential;
- }
-
- @Override
public final int getIndexEntryLimit()
{
return indexEntryLimit;
@@ -338,8 +344,31 @@
return trusted;
}
+ /** Whether this index encrypts what it writes: the confidentiality its tree was opened under. */
final boolean isEncrypted()
{
- return cryptoSuite.isEncrypted();
+ return encrypted;
+ }
+
+ /** The codec this index currently reads and writes under. */
+ final EntryIDSetCodec codec()
+ {
+ return codec;
+ }
+
+ /**
+ * Puts this index back to the confidentiality, codec and trust it answered before its tree was
+ * given up and opened again, for a reopen whose commit the storage gave up: what
+ * {@link #afterOpen} binds is memory, and outlives a write the storage rolled back, so a change
+ * asked for again would otherwise find this index already agreeing with the setting that write
+ * never durably reached. The trust is bound the same way - an index opened over an empty entry
+ * container is trusted on the spot - and, left behind, is what an operation which then fails
+ * writes down for every index in its buffer, for a tree the give-up took with it.
+ */
+ final void revertFailedReopen(boolean encrypted, EntryIDSetCodec codec, boolean trusted)
+ {
+ this.encrypted = encrypted;
+ this.codec = codec;
+ this.trusted = trusted;
}
}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java
index 4c6d71b..5825abe 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java
@@ -565,6 +565,20 @@
config.getCipherKeyLength(), confidentiality);
}
+ /**
+ * Puts the confidentiality the configuration of a backend index asks for in force on the
+ * {@link CryptoSuite} the indexes of that attribute share, keeping the cipher of this backend.
+ *
+ * @param indexCrypto
+ * the crypto suite the indexes of one attribute were opened with
+ * @param confidentiality
+ * whether what those indexes store has to be encrypted from now on
+ */
+ void setIndexConfidentiality(CryptoSuite indexCrypto, boolean confidentiality)
+ {
+ indexCrypto.newParameters(config.getCipherTransformation(), config.getCipherKeyLength(), confidentiality);
+ }
+
private AttributeIndex newAttributeIndex(BackendIndexCfg cfg, CryptoSuite cryptoSuite) throws ConfigException
{
return new AttributeIndex(cfg, state, this, cryptoSuite);
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/Index.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/Index.java
index 078348d..a2cd969 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/Index.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/Index.java
@@ -57,8 +57,6 @@
boolean setIndexEntryLimit(int indexEntryLimit);
- boolean setConfidential(boolean indexConfidential);
-
void setTrusted(WriteableTransaction txn, boolean trusted);
void update(WriteableTransaction txn, ByteString key, EntryIDSet deletedIDs, EntryIDSet addedIDs);
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/jeb/JEIndexConfidentialityChangeTest.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/jeb/JEIndexConfidentialityChangeTest.java
new file mode 100644
index 0000000..f343da0
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/jeb/JEIndexConfidentialityChangeTest.java
@@ -0,0 +1,54 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.server.backends.jeb;
+
+import static org.forgerock.opendj.config.ConfigurationMock.mockCfg;
+import static org.mockito.Mockito.when;
+
+import org.forgerock.opendj.config.server.ConfigException;
+import org.forgerock.opendj.server.config.server.JEBackendCfg;
+import org.opends.server.backends.pluggable.IndexConfidentialityChangeTestCase;
+import org.opends.server.backends.pluggable.spi.Storage;
+import org.opends.server.core.ServerContext;
+import org.testng.annotations.Test;
+
+/**
+ * A confidentiality change of a backend index of a {@link JEBackend}, which deletes and creates the
+ * tree of an index through a storage engine of its own.
+ */
+@Test
+public class JEIndexConfidentialityChangeTest extends IndexConfidentialityChangeTestCase<JEBackendCfg>
+{
+ @Override
+ protected JEBackendCfg createBackendCfg()
+ {
+ final JEBackendCfg backendCfg = mockCfg(JEBackendCfg.class);
+ when(backendCfg.getBackendId()).thenReturn("JEIndexConfidentialityChangeTest");
+ when(backendCfg.getDBDirectory()).thenReturn("JEIndexConfidentialityChangeTest");
+ when(backendCfg.getDBDirectoryPermissions()).thenReturn("755");
+ when(backendCfg.getDBCacheSize()).thenReturn(0L);
+ when(backendCfg.getDBCachePercent()).thenReturn(20);
+ when(backendCfg.getDBNumCleanerThreads()).thenReturn(2);
+ when(backendCfg.getDBNumLockTables()).thenReturn(63);
+ return backendCfg;
+ }
+
+ @Override
+ protected Storage createStorage(JEBackendCfg cfg, ServerContext serverContext) throws ConfigException
+ {
+ return new JEStorage(cfg, serverContext);
+ }
+}
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/pdb/PDBIndexConfidentialityChangeTest.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/pdb/PDBIndexConfidentialityChangeTest.java
new file mode 100644
index 0000000..de0ffa1
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/pdb/PDBIndexConfidentialityChangeTest.java
@@ -0,0 +1,49 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.server.backends.pdb;
+
+import static org.forgerock.opendj.config.ConfigurationMock.mockCfg;
+import static org.mockito.Mockito.when;
+
+import org.forgerock.opendj.config.server.ConfigException;
+import org.forgerock.opendj.server.config.server.PDBBackendCfg;
+import org.opends.server.backends.pluggable.IndexConfidentialityChangeTestCase;
+import org.opends.server.backends.pluggable.spi.Storage;
+import org.opends.server.core.ServerContext;
+import org.testng.annotations.Test;
+
+/** A confidentiality change of a backend index of a {@link PDBBackend}. */
+@Test
+public class PDBIndexConfidentialityChangeTest extends IndexConfidentialityChangeTestCase<PDBBackendCfg>
+{
+ @Override
+ protected PDBBackendCfg createBackendCfg()
+ {
+ final PDBBackendCfg backendCfg = mockCfg(PDBBackendCfg.class);
+ when(backendCfg.getBackendId()).thenReturn("PDBIndexConfidentialityChangeTest");
+ when(backendCfg.getDBDirectory()).thenReturn("PDBIndexConfidentialityChangeTest");
+ when(backendCfg.getDBDirectoryPermissions()).thenReturn("755");
+ when(backendCfg.getDBCacheSize()).thenReturn(0L);
+ when(backendCfg.getDBCachePercent()).thenReturn(20);
+ return backendCfg;
+ }
+
+ @Override
+ protected Storage createStorage(PDBBackendCfg cfg, ServerContext serverContext) throws ConfigException
+ {
+ return new PDBStorage(cfg, serverContext);
+ }
+}
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/IndexConfidentialityChangeTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/IndexConfidentialityChangeTestCase.java
new file mode 100644
index 0000000..756b59b
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/IndexConfidentialityChangeTestCase.java
@@ -0,0 +1,449 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.server.backends.pluggable;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.opends.messages.BackendMessages.NOTE_CONFIG_INDEX_CONFIDENTIALITY_REQUIRES_REBUILD;
+import static org.opends.server.util.CollectionUtils.newTreeSet;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.config.server.ConfigChangeResult;
+import org.forgerock.opendj.config.server.ConfigException;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.opendj.server.config.meta.BackendIndexCfgDefn.IndexType;
+import org.forgerock.opendj.server.config.server.BackendIndexCfg;
+import org.forgerock.opendj.server.config.server.PluggableBackendCfg;
+import org.opends.server.DirectoryServerTestCase;
+import org.opends.server.TestCaseUtils;
+import org.opends.server.backends.pluggable.AttributeIndex.MatchingRuleIndex;
+import org.opends.server.backends.pluggable.spi.Storage;
+import org.opends.server.core.AddOperation;
+import org.opends.server.core.ServerContext;
+import org.opends.server.types.Entry;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+/**
+ * Tests that a change of {@code confidentiality-enabled} on a backend index is applied to the
+ * running backend, rather than only reported as requiring a rebuild which cannot help - see OpenDJ
+ * issue #992.
+ * <p>
+ * The confidentiality of an index is carried by the {@link org.opends.server.crypto.CryptoSuite} the
+ * indexes of one attribute share, and read from it when an index binds its codec at open time. A
+ * change which does not put the new setting in force on that suite, and does not bind the codecs
+ * again, leaves the stored records in the encoding of the previous setting for the life of the
+ * container.
+ */
+@SuppressWarnings("javadoc")
+@Test(groups = { "precommit", "pluggablebackend" }, sequential = true)
+public abstract class IndexConfidentialityChangeTestCase<C extends PluggableBackendCfg> extends DirectoryServerTestCase
+{
+ private static final DN BASE_DN = DN.valueOf("dc=b992,dc=com");
+ /** Indexed for presence, whose tree a confidentiality change keeps, and for equality, whose it does not. */
+ private static final String INDEXED_ATTRIBUTE = "sn";
+ /** The first byte {@code EntryIDSet.EntryIDSetCodecV3} prepends to a record it encrypted. */
+ private static final byte ENCRYPTED_RECORD_TAG = 0x00;
+ private static final int ENTRY_LIMIT = 4000;
+
+ private ServerContext serverContext;
+ private AttributeType attributeType;
+
+ /**
+ * Factory method for the configuration of the backend under test, with the settings specific to its
+ * storage engine stubbed out.
+ *
+ * @return the new backend configuration
+ */
+ protected abstract C createBackendCfg();
+
+ /**
+ * Factory method for the storage of the backend under test, which the test reads the stored records
+ * back from.
+ *
+ * @param cfg
+ * the configuration the backend was configured with
+ * @param serverContext
+ * the server context of the running test server
+ * @return the storage of the backend under test
+ * @throws ConfigException
+ * if the configuration is not one the storage can be opened with
+ */
+ protected abstract Storage createStorage(C cfg, ServerContext serverContext) throws ConfigException;
+
+ @BeforeClass
+ public void startServer() throws Exception
+ {
+ TestCaseUtils.startServer();
+ serverContext = TestCaseUtils.getServerContext();
+ attributeType = serverContext.getSchema().getAttributeType(INDEXED_ATTRIBUTE);
+ }
+
+ /**
+ * A test which fails before it closes its backend leaves the base DN behind in the server wide
+ * registry, where it would outlive the test and break the next one to use that DN.
+ */
+ @AfterMethod
+ public void deregisterLeftoverBaseDN()
+ {
+ try
+ {
+ serverContext.getBackendConfigManager().deregisterBaseDN(BASE_DN);
+ }
+ catch (Exception alreadyGone)
+ {
+ // Which is what a test that closed its backend has left behind.
+ }
+ }
+
+ /**
+ * The records an index holds are in the encoding of the setting in force when they were written,
+ * and the codec of the new setting cannot read them back. They are given up here rather than left
+ * for the searches which run before the rebuild this change asks for.
+ */
+ @Test
+ public void enablingConfidentialityEmptiesTheIndexItAsksToRebuild() throws Exception
+ {
+ final TestBackend backend = openBackend(false);
+ try
+ {
+ addEntry(backend, "user.0");
+ final AttributeIndex attributeIndex = attributeIndex(backend);
+ assertThat(recordCount(backend, presenceIndex(attributeIndex))).isEqualTo(1);
+
+ final ConfigChangeResult ccr = attributeIndex.applyConfigurationChange(indexCfg(true, ENTRY_LIMIT));
+
+ assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
+ assertThat(ccr.adminActionRequired()).isTrue();
+ assertThat(ordinalsOf(ccr)).contains(NOTE_CONFIG_INDEX_CONFIDENTIALITY_REQUIRES_REBUILD.ordinal());
+ final MatchingRuleIndex presence = presenceIndex(attributeIndex);
+ assertThat(recordCount(backend, presence)).isEqualTo(0);
+ assertThat(presence.isTrusted()).isFalse();
+ // Undefined rather than empty, so that a search of it is not answered with no candidates.
+ final ByteString key = keyOf(presence, entryOf("user.0"));
+ final boolean defined = backend.storage.read(txn -> presence.get(txn, key).isDefined());
+ assertThat(defined).isFalse();
+ }
+ finally
+ {
+ backend.finalizeBackend();
+ }
+ }
+
+ /** The setting the operator asked for has to reach what the index writes from then on. */
+ @Test
+ public void enablingConfidentialityEncryptsWhatTheIndexWritesNext() throws Exception
+ {
+ final TestBackend backend = openBackend(false);
+ try
+ {
+ final AttributeIndex attributeIndex = attributeIndex(backend);
+ addEntry(backend, "user.0");
+ assertThat(rawRecord(backend, presenceIndex(attributeIndex), "user.0").byteAt(0))
+ .isNotEqualTo(ENCRYPTED_RECORD_TAG);
+
+ attributeIndex.applyConfigurationChange(indexCfg(true, ENTRY_LIMIT));
+ trust(backend, attributeIndex);
+ addEntry(backend, "user.1");
+
+ final MatchingRuleIndex presence = presenceIndex(attributeIndex);
+ assertThat(rawRecord(backend, presence, "user.1").byteAt(0)).isEqualTo(ENCRYPTED_RECORD_TAG);
+ assertThat(idsOf(backend, presence, "user.1")).hasSize(1);
+ }
+ finally
+ {
+ backend.finalizeBackend();
+ }
+ }
+
+ /**
+ * Enabling confidentiality of an equality index replaces its tree with one whose keys are hashed,
+ * which the change creates and opens itself. That one has to be opened with the new setting in
+ * force, or its keys are protected while its records are not.
+ */
+ @Test
+ public void enablingConfidentialityEncryptsTheKeyHashedIndexItCreates() throws Exception
+ {
+ final TestBackend backend = openBackend(false);
+ try
+ {
+ final AttributeIndex attributeIndex = attributeIndex(backend);
+ addEntry(backend, "user.0");
+ assertThat(keyHashedIndex(attributeIndex)).isNull();
+
+ attributeIndex.applyConfigurationChange(indexCfg(true, ENTRY_LIMIT));
+ trust(backend, attributeIndex);
+ addEntry(backend, "user.1");
+
+ final MatchingRuleIndex hashed = keyHashedIndex(attributeIndex);
+ assertThat(hashed).isNotNull();
+ assertThat(rawRecord(backend, hashed, "user.1").byteAt(0)).isEqualTo(ENCRYPTED_RECORD_TAG);
+ assertThat(idsOf(backend, hashed, "user.1")).hasSize(1);
+ }
+ finally
+ {
+ backend.finalizeBackend();
+ }
+ }
+
+ /** And the same the other way around: giving the setting up has to stop the encryption. */
+ @Test
+ public void disablingConfidentialityStopsEncryptingWhatTheIndexWritesNext() throws Exception
+ {
+ final TestBackend backend = openBackend(true);
+ try
+ {
+ final AttributeIndex attributeIndex = attributeIndex(backend);
+ addEntry(backend, "user.0");
+ assertThat(rawRecord(backend, presenceIndex(attributeIndex), "user.0").byteAt(0))
+ .isEqualTo(ENCRYPTED_RECORD_TAG);
+
+ attributeIndex.applyConfigurationChange(indexCfg(false, ENTRY_LIMIT));
+ // The tree given up carries the record written under the setting just given up, which the
+ // codec of the new one does not read back as what it is - so it is given up here regardless of
+ // which way the setting goes, not only where enabling replaces it with a key hashed twin.
+ assertThat(recordCount(backend, presenceIndex(attributeIndex))).as("the encrypted tree, given up").isEqualTo(0);
+ trust(backend, attributeIndex);
+ addEntry(backend, "user.1");
+
+ final MatchingRuleIndex presence = presenceIndex(attributeIndex);
+ assertThat(rawRecord(backend, presence, "user.1").byteAt(0)).isNotEqualTo(ENCRYPTED_RECORD_TAG);
+ assertThat(idsOf(backend, presence, "user.1")).hasSize(1);
+ }
+ finally
+ {
+ backend.finalizeBackend();
+ }
+ }
+
+ /**
+ * Once the change is applied, the comparison it is reported by converges: an unrelated change of
+ * the same index must not untrust it again, and must not repeat the rebuild message.
+ */
+ @Test
+ public void aLaterUnrelatedChangeLeavesTheIndexTrusted() throws Exception
+ {
+ final TestBackend backend = openBackend(false);
+ try
+ {
+ final AttributeIndex attributeIndex = attributeIndex(backend);
+ addEntry(backend, "user.0");
+ attributeIndex.applyConfigurationChange(indexCfg(true, ENTRY_LIMIT));
+ trust(backend, attributeIndex);
+
+ // A smaller index entry limit does not invalidate what the index holds, so this change has no
+ // rebuild of its own to ask for.
+ final ConfigChangeResult ccr = attributeIndex.applyConfigurationChange(indexCfg(true, ENTRY_LIMIT - 1));
+
+ assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
+ assertThat(ccr.getMessages()).isEmpty();
+ assertThat(ccr.adminActionRequired()).isFalse();
+ for (MatchingRuleIndex index : attributeIndex.getNameToIndexes().values())
+ {
+ assertThat(index.isTrusted()).as(index.getName().toString()).isTrue();
+ }
+ }
+ finally
+ {
+ backend.finalizeBackend();
+ }
+ }
+
+ private TestBackend openBackend(boolean indexConfidentiality) throws Exception
+ {
+ final C cfg = backendCfg(indexConfidentiality);
+ final TestBackend backend = new TestBackend();
+ backend.setBackendID(cfg.getBackendId());
+ backend.configureBackend(cfg, serverContext);
+ // Start from a pristine on-disk state, so that a previous run cannot mask the defect.
+ backend.storage.removeStorageFiles();
+ try
+ {
+ backend.openBackend();
+ backend.addEntry(TestCaseUtils.makeEntry(
+ "dn: " + BASE_DN,
+ "objectClass: top",
+ "objectClass: domain",
+ "dc: b992"), mock(AddOperation.class));
+ }
+ catch (Exception e)
+ {
+ // openBackend() registers the base DN and the monitor before it returns, so a failure after
+ // that would leave both behind and break every following test rather than only this one.
+ try
+ {
+ backend.finalizeBackend();
+ }
+ catch (Exception cleanupFailure)
+ {
+ e.addSuppressed(cleanupFailure);
+ }
+ throw e;
+ }
+ return backend;
+ }
+
+ private Entry addEntry(TestBackend backend, String uid) throws Exception
+ {
+ final Entry entry = entryOf(uid);
+ backend.addEntry(entry, mock(AddOperation.class));
+ return entry;
+ }
+
+ /** Trusts every index of the attribute, which is what the rebuild the change asks for leaves behind. */
+ private void trust(TestBackend backend, final AttributeIndex attributeIndex) throws Exception
+ {
+ backend.storage.write(txn -> {
+ for (MatchingRuleIndex index : attributeIndex.getNameToIndexes().values())
+ {
+ index.setTrusted(txn, true);
+ }
+ });
+ }
+
+ private AttributeIndex attributeIndex(TestBackend backend)
+ {
+ return backend.getRootContainer().getEntryContainer(BASE_DN).getAttributeIndex(attributeType);
+ }
+
+ private static MatchingRuleIndex presenceIndex(AttributeIndex attributeIndex)
+ {
+ return attributeIndex.getNameToIndexes().get(IndexType.PRESENCE.toString());
+ }
+
+ private static MatchingRuleIndex keyHashedIndex(AttributeIndex attributeIndex)
+ {
+ for (Map.Entry<String, MatchingRuleIndex> index : attributeIndex.getNameToIndexes().entrySet())
+ {
+ if (index.getKey().endsWith(AttributeIndex.PROTECTED_INDEX_ID))
+ {
+ return index.getValue();
+ }
+ }
+ return null;
+ }
+
+ private static ByteString keyOf(MatchingRuleIndex index, Entry entry)
+ {
+ return index.indexEntry(entry).iterator().next();
+ }
+
+ /** The record as it is stored, which is what says whether it was encrypted. */
+ private ByteString rawRecord(TestBackend backend, final MatchingRuleIndex index, String uid) throws Exception
+ {
+ final ByteString key = keyOf(index, entryOf(uid));
+ final ByteString record = backend.storage.read(txn -> txn.read(index.getName(), key));
+ assertThat(record).as("the record of " + index.getName() + " at key " + key).isNotNull();
+ return record;
+ }
+
+ private List<Long> idsOf(TestBackend backend, final MatchingRuleIndex index, String uid) throws Exception
+ {
+ final ByteString key = keyOf(index, entryOf(uid));
+ final EntryIDSet idSet = backend.storage.read(txn -> index.get(txn, key));
+ assertThat(idSet.isDefined()).as("the entry IDs of " + index.getName() + " at key " + key).isTrue();
+ final List<Long> ids = new ArrayList<>();
+ for (EntryID id : idSet)
+ {
+ ids.add(id.longValue());
+ }
+ return ids;
+ }
+
+ /** The entry as it was added, which the indexers generate the keys of a record from. */
+ private Entry entryOf(String uid) throws Exception
+ {
+ return TestCaseUtils.makeEntry(
+ "dn: uid=" + uid + "," + BASE_DN,
+ "objectClass: top",
+ "objectClass: person",
+ "objectClass: organizationalPerson",
+ "objectClass: inetOrgPerson",
+ "uid: " + uid,
+ "cn: " + uid,
+ "sn: " + uid);
+ }
+
+ private long recordCount(TestBackend backend, final MatchingRuleIndex index) throws Exception
+ {
+ return backend.storage.read(txn -> index.getRecordCount(txn));
+ }
+
+ private static Set<Integer> ordinalsOf(ConfigChangeResult ccr)
+ {
+ final Set<Integer> ordinals = new HashSet<>();
+ for (LocalizableMessage message : ccr.getMessages())
+ {
+ ordinals.add(message.ordinal());
+ }
+ return ordinals;
+ }
+
+ private C backendCfg(boolean indexConfidentiality) throws ConfigException
+ {
+ final C cfg = createBackendCfg();
+ // Read outside the when() below, which calling a mock inside would leave unfinished.
+ final String backendId = cfg.getBackendId();
+ when(cfg.dn()).thenReturn(DN.valueOf("ds-cfg-backend-id=" + backendId + ",cn=Backends,cn=config"));
+ when(cfg.getBaseDN()).thenReturn(newTreeSet(BASE_DN));
+ when(cfg.listBackendIndexes()).thenReturn(new String[] { INDEXED_ATTRIBUTE });
+ when(cfg.listBackendVLVIndexes()).thenReturn(new String[0]);
+ // An index can only be confidential in a backend which is, and the cipher of the backend is what
+ // the crypto suite of an index takes its parameters from.
+ when(cfg.isConfidentialityEnabled()).thenReturn(true);
+ when(cfg.getCipherTransformation()).thenReturn("AES/CBC/PKCS5Padding");
+ when(cfg.getCipherKeyLength()).thenReturn(128);
+ // Stubbed outside the when() below, which stubbing another mock inside would leave unfinished.
+ final BackendIndexCfg indexCfg = indexCfg(indexConfidentiality, ENTRY_LIMIT);
+ when(cfg.getBackendIndex(INDEXED_ATTRIBUTE)).thenReturn(indexCfg);
+ return cfg;
+ }
+
+ private BackendIndexCfg indexCfg(boolean confidentiality, int entryLimit)
+ {
+ final BackendIndexCfg cfg = mock(BackendIndexCfg.class);
+ when(cfg.getIndexType()).thenReturn(newTreeSet(IndexType.PRESENCE, IndexType.EQUALITY));
+ when(cfg.getAttribute()).thenReturn(attributeType);
+ when(cfg.getIndexEntryLimit()).thenReturn(entryLimit);
+ when(cfg.getSubstringLength()).thenReturn(6);
+ when(cfg.isConfidentialityEnabled()).thenReturn(confidentiality);
+ return cfg;
+ }
+
+ /** A backend whose storage the test reads the stored records back from. */
+ private final class TestBackend extends BackendImpl<C>
+ {
+ private Storage storage;
+
+ @Override
+ protected Storage configureStorage(C cfg, ServerContext serverContext) throws ConfigException
+ {
+ storage = createStorage(cfg, serverContext);
+ return storage;
+ }
+ }
+}
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ReplayedConfigChangeTest.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ReplayedConfigChangeTest.java
index cc030d8..2d37dea 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ReplayedConfigChangeTest.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ReplayedConfigChangeTest.java
@@ -16,9 +16,11 @@
package org.opends.server.backends.pluggable;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.opends.messages.BackendMessages.ERR_BACKEND_BASEDN_NO_LONGER_HELD;
import static org.opends.messages.BackendMessages.ERR_BACKEND_CANNOT_LIST_TREES_AFTER_BASEDN_CHANGE;
import static org.opends.messages.BackendMessages.ERR_BACKEND_CANNOT_REGISTER_BASEDN;
+import static org.opends.messages.BackendMessages.NOTE_CONFIG_INDEX_CONFIDENTIALITY_REQUIRES_REBUILD;
import static org.opends.messages.BackendMessages.NOTE_CONFIG_INDEX_ENTRY_LIMIT_REQUIRES_REBUILD;
import static org.opends.messages.BackendMessages.NOTE_INDEX_ADD_REQUIRES_REBUILD;
import static org.forgerock.opendj.config.ConfigurationMock.mockCfg;
@@ -58,6 +60,7 @@
import org.opends.server.TestCaseUtils;
import org.opends.server.backends.pdb.PDBStorage;
import org.opends.server.backends.pluggable.AttributeIndex.MatchingRuleIndex;
+import org.opends.server.backends.pluggable.EntryIDSet.EntryIDSetCodec;
import org.opends.server.backends.pluggable.State.IndexFlag;
import org.opends.server.backends.pluggable.spi.AccessMode;
import org.opends.server.backends.pluggable.spi.Cursor;
@@ -862,6 +865,226 @@
}
/**
+ * A change of the confidentiality gives up the tree of an index it keeps and opens it again under
+ * the new setting, in a write which untrusts and deletes and a write which opens. Whether an index
+ * needs that is asked of the index - what its tree was opened under - so that an attempt the
+ * storage replays reaches the same answer, and the instruction is given before any write, since
+ * the configuration entry holds the new setting whichever way the writes go.
+ */
+ @Test
+ public void aConfidentialityChangeUntrustsTheIndexWhenTheTransactionIsReplayed() throws Exception
+ {
+ final ReplayingBackend backend = openBackendWithPresenceIndex();
+ try
+ {
+ final RootContainer rootContainer = backend.getRootContainer();
+ final EntryContainer ec = rootContainer.getEntryContainer(KEPT);
+ final AttributeIndex index = ec.getAttributeIndex(cnType);
+ final MatchingRuleIndex cnIndex = index.getNameToIndexes().values().iterator().next();
+ assertThat(persistedFlags(rootContainer, ec, cnIndex.getName())).contains(TRUSTED);
+ // Held so that the index opened again below stays untrusted: an index of an empty backend is
+ // trusted when it is opened, whatever its tree holds.
+ addBaseEntry(backend, KEPT, "b907a");
+ assertThat(cnIndex.isEncrypted()).isFalse();
+
+ // The third write is the one which untrusts the index and gives up its tree; the fourth opens
+ // it again, which is what binds its codec to the setting now in force.
+ final int writesBefore = backend.storage.writes();
+ backend.storage.conflictAtCommitOnWrite(3, 1);
+ final ConfigChangeResult ccr = index.applyConfigurationChange(confidentialPresenceIndexCfg());
+
+ assertThat(backend.storage.writes()).as("the armed write was the third of four").isEqualTo(writesBefore + 4);
+ assertThat(backend.storage.attempts()).isEqualTo(2);
+ assertThat(ccr.getResultCode()).isEqualTo(ResultCode.SUCCESS);
+ assertThat(ccr.adminActionRequired()).isTrue();
+ assertThat(ccr.getMessages()).as("the rebuild the new setting needs, asked for once").hasSize(1);
+ assertThat(ordinalsOf(ccr)).containsOnly(NOTE_CONFIG_INDEX_CONFIDENTIALITY_REQUIRES_REBUILD.ordinal());
+ assertThat(cnIndex.isTrusted()).isFalse();
+ assertThat(persistedFlags(rootContainer, ec, cnIndex.getName()))
+ .as("the flag the attempt which committed had to remove").doesNotContain(TRUSTED);
+ assertThat(cnIndex.isEncrypted()).as("the setting the tree was opened again under").isTrue();
+
+ // The setting the change applied is the one the index writes under from now on, so asking for
+ // it a second time asks for nothing of the index, and untrusts nothing.
+ final int writesAfter = backend.storage.writes();
+ final ConfigChangeResult again = index.applyConfigurationChange(confidentialPresenceIndexCfg());
+ assertThat(again.getResultCode()).isEqualTo(ResultCode.SUCCESS);
+ assertThat(again.adminActionRequired()).as("a change which changes nothing").isFalse();
+ assertThat(again.getMessages()).isEmpty();
+ assertThat(backend.storage.writes()).as("the two writes which add and remove indexes, and no third")
+ .isEqualTo(writesAfter + 2);
+ }
+ finally
+ {
+ backend.finalizeBackend();
+ }
+ }
+
+ /**
+ * The same instruction survives a give-up on the write which untrusts the index and gives up its
+ * tree, and the change is still a change when asked for again: the suite the indexes of the
+ * attribute share was switched before that write and is not rolled back with it, so it already
+ * reads as applied, while the index still writes under the setting its tree was opened under -
+ * which is what it is asked.
+ */
+ @Test
+ public void aConfidentialityChangeIsReportedWhenTheWriteWhichGivesUpTheTreeGivesUp() throws Exception
+ {
+ final ReplayingBackend backend = openBackendWithPresenceIndex();
+ try
+ {
+ final RootContainer rootContainer = backend.getRootContainer();
+ final EntryContainer ec = rootContainer.getEntryContainer(KEPT);
+ final AttributeIndex index = ec.getAttributeIndex(cnType);
+ final MatchingRuleIndex cnIndex = index.getNameToIndexes().values().iterator().next();
+ assertThat(persistedFlags(rootContainer, ec, cnIndex.getName())).contains(TRUSTED);
+ // Held so that the index opened again below stays untrusted: an index of an empty backend is
+ // trusted when it is opened, whatever its tree holds.
+ addBaseEntry(backend, KEPT, "b907a");
+
+ final int writesBefore = backend.storage.writes();
+ backend.storage.failWithoutReplayOnWrite(3);
+ final ConfigChangeResult ccr = index.applyConfigurationChange(confidentialPresenceIndexCfg());
+
+ assertThat(backend.storage.writes()).as("the armed write was the third, and the last one made")
+ .isEqualTo(writesBefore + 3);
+ assertThat(ccr.getResultCode()).isEqualTo(serverErrorResultCode());
+ assertThat(ccr.adminActionRequired()).as("the rebuild the new setting needs, on the road which failed").isTrue();
+ assertThat(ordinalsOf(ccr)).contains(NOTE_CONFIG_INDEX_CONFIDENTIALITY_REQUIRES_REBUILD.ordinal());
+ assertThat(ccr.getMessages().toString()).as("the failure, next to the rebuild")
+ .contains(UnreplayableFailure.class.getSimpleName());
+ assertThat(persistedFlags(rootContainer, ec, cnIndex.getName()))
+ .as("what the restart will read").contains(TRUSTED);
+
+ final ConfigChangeResult again = index.applyConfigurationChange(confidentialPresenceIndexCfg());
+ assertThat(again.getResultCode()).isEqualTo(ResultCode.SUCCESS);
+ assertThat(again.adminActionRequired())
+ .as("the setting the failed write did not apply is still a change").isTrue();
+ assertThat(ordinalsOf(again)).containsOnly(NOTE_CONFIG_INDEX_CONFIDENTIALITY_REQUIRES_REBUILD.ordinal());
+ assertThat(cnIndex.isTrusted()).isFalse();
+ assertThat(persistedFlags(rootContainer, ec, cnIndex.getName())).doesNotContain(TRUSTED);
+ assertThat(cnIndex.isEncrypted()).as("the setting the tree was opened again under").isTrue();
+ }
+ finally
+ {
+ backend.finalizeBackend();
+ }
+ }
+
+ /**
+ * A give-up of the write which opens the tree again - the fourth, once the third has committed -
+ * must not leave the index believing it already writes under the new setting: what
+ * {@link DefaultIndex#afterOpen} binds is memory, and outlives a write the storage rolled back. A
+ * re-apply which agreed with that binding would answer SUCCESS with no instruction, and the tree
+ * the third write deleted would never be reopened.
+ */
+ @Test
+ public void aConfidentialityChangeIsReportedWhenTheWriteWhichReopensTheTreeGivesUp() throws Exception
+ {
+ final ReplayingBackend backend = openBackendWithPresenceIndex();
+ try
+ {
+ final RootContainer rootContainer = backend.getRootContainer();
+ final EntryContainer ec = rootContainer.getEntryContainer(KEPT);
+ final AttributeIndex index = ec.getAttributeIndex(cnType);
+ final MatchingRuleIndex cnIndex = index.getNameToIndexes().values().iterator().next();
+ assertThat(persistedFlags(rootContainer, ec, cnIndex.getName())).contains(TRUSTED);
+ // Held so that the index opened again below stays untrusted: an index of an empty backend is
+ // trusted when it is opened, whatever its tree holds.
+ addBaseEntry(backend, KEPT, "b907a");
+ assertThat(cnIndex.isEncrypted()).isFalse();
+ final EntryIDSetCodec codecBefore = cnIndex.codec();
+
+ final int writesBefore = backend.storage.writes();
+ backend.storage.failWithoutReplayOnWrite(4);
+ final ConfigChangeResult ccr = index.applyConfigurationChange(confidentialPresenceIndexCfg());
+
+ assertThat(backend.storage.writes()).as("the third write committed, the fourth was armed and given up")
+ .isEqualTo(writesBefore + 4);
+ assertThat(ccr.getResultCode()).isEqualTo(serverErrorResultCode());
+ assertThat(ccr.getMessages().toString()).as("the failure, next to the rebuild")
+ .contains(UnreplayableFailure.class.getSimpleName());
+ assertThat(persistedFlags(rootContainer, ec, cnIndex.getName()))
+ .as("the write which untrusts and deletes committed before the reopen was armed").doesNotContain(TRUSTED);
+ assertThat(cnIndex.isEncrypted())
+ .as("the reopen's commit gave up: the setting the tree is still to be opened under").isFalse();
+ // Nothing decodes through the codec between here and the reopen asked for again, which binds
+ // it afresh: pinned on the invariant DefaultIndex states rather than on a road which turns red.
+ assertThat(cnIndex.codec()).as("the codec the given-up reopen bound is put back").isSameAs(codecBefore);
+
+ // The re-apply must still find this index to give up and open again, not one which already
+ // agrees with the setting the failed reopen never durably reached.
+ final ConfigChangeResult again = index.applyConfigurationChange(confidentialPresenceIndexCfg());
+ assertThat(again.getResultCode()).isEqualTo(ResultCode.SUCCESS);
+ assertThat(again.adminActionRequired())
+ .as("the setting the failed reopen did not apply is still a change").isTrue();
+ assertThat(ordinalsOf(again)).containsOnly(NOTE_CONFIG_INDEX_CONFIDENTIALITY_REQUIRES_REBUILD.ordinal());
+ assertThat(cnIndex.isTrusted()).isFalse();
+ assertThat(persistedFlags(rootContainer, ec, cnIndex.getName())).doesNotContain(TRUSTED);
+ assertThat(cnIndex.isEncrypted()).as("the setting the tree was opened again under").isTrue();
+ }
+ finally
+ {
+ backend.finalizeBackend();
+ }
+ }
+
+ /**
+ * The same give-up over an empty backend, where the reopen trusts the index it opens: that trust
+ * is bound in memory before the write commits, as the codec is, and must be put back with it. Left
+ * behind, it is what a failed operation writes down for each index in its buffer - for a tree the
+ * give-up took with it - while the index skips the entries added meanwhile, which a search after
+ * a restart then never finds.
+ */
+ @Test
+ public void aGivenUpReopenOverAnEmptyBackendDoesNotLeaveTheIndexTrusted() throws Exception
+ {
+ final ReplayingBackend backend = openBackendWithPresenceIndex();
+ try
+ {
+ final RootContainer rootContainer = backend.getRootContainer();
+ final EntryContainer ec = rootContainer.getEntryContainer(KEPT);
+ final AttributeIndex index = ec.getAttributeIndex(cnType);
+ final MatchingRuleIndex cnIndex = index.getNameToIndexes().values().iterator().next();
+ assertThat(persistedFlags(rootContainer, ec, cnIndex.getName())).contains(TRUSTED);
+ assertThat(cnIndex.isTrusted()).isTrue();
+
+ backend.storage.failWithoutReplayOnWrite(4);
+ final ConfigChangeResult ccr = index.applyConfigurationChange(confidentialPresenceIndexCfg());
+
+ assertThat(ccr.getResultCode()).isEqualTo(serverErrorResultCode());
+ assertThat(persistedFlags(rootContainer, ec, cnIndex.getName())).doesNotContain(TRUSTED);
+ assertThat(cnIndex.isTrusted())
+ .as("the reopen which trusted the index over an empty backend gave up").isFalse();
+
+ // The road that memory takes to disk: an operation which fails writes down what each index in
+ // its buffer answers, so that one it found corrupt stays untrusted. An add the container
+ // refuses - no parent - has the cn index in its buffer all the same.
+ assertThatThrownBy(() -> backend.addEntry(
+ TestCaseUtils.makeEntry("dn: cn=user.1," + KEPT, "objectClass: top", "objectClass: person",
+ "cn: user.1", "sn: user.1"),
+ mock(AddOperation.class)))
+ .isInstanceOf(DirectoryException.class)
+ .hasFieldOrPropertyWithValue("resultCode", ResultCode.NO_SUCH_OBJECT);
+ assertThat(persistedFlags(rootContainer, ec, cnIndex.getName()))
+ .as("what the failed add wrote down is what the index answers, not what the give-up bound")
+ .doesNotContain(TRUSTED);
+
+ // Over a backend still empty, the reopen asked for again trusts the index it opens, and commits.
+ final ConfigChangeResult again = index.applyConfigurationChange(confidentialPresenceIndexCfg());
+ assertThat(again.getResultCode()).isEqualTo(ResultCode.SUCCESS);
+ assertThat(ordinalsOf(again)).containsOnly(NOTE_CONFIG_INDEX_CONFIDENTIALITY_REQUIRES_REBUILD.ordinal());
+ assertThat(cnIndex.isEncrypted()).isTrue();
+ assertThat(cnIndex.isTrusted()).as("opened again over an empty backend").isTrue();
+ assertThat(persistedFlags(rootContainer, ec, cnIndex.getName())).contains(TRUSTED);
+ }
+ finally
+ {
+ backend.finalizeBackend();
+ }
+ }
+
+ /**
* The same road for a vlvIndex: the write which untrusts it is the only one the change makes, and
* a failure of it is reported with the rebuild the change asked for, rather than thrown out of
* the listener with that result discarded. The change touches all four published fields at once,
@@ -935,14 +1158,30 @@
/** The vlvIndex is opened only where a test is about one, since every base DN gets a copy of it. */
private ReplayingBackend openBackendWithVlvIndex() throws Exception
{
- return openBackend(newTreeSet(KEPT), true);
+ return openBackend(newTreeSet(KEPT), true, newTreeSet(IndexType.EQUALITY));
+ }
+
+ /**
+ * A cn index of the presence type, whose tree a change of the confidentiality keeps: the equality
+ * type is replaced by a key hashed twin when it is made confidential, and so is added and removed
+ * rather than kept.
+ */
+ private ReplayingBackend openBackendWithPresenceIndex() throws Exception
+ {
+ return openBackend(newTreeSet(KEPT), false, newTreeSet(IndexType.PRESENCE));
}
private ReplayingBackend openBackend(SortedSet<DN> baseDNs, boolean withVlvIndex) throws Exception
{
+ return openBackend(baseDNs, withVlvIndex, newTreeSet(IndexType.EQUALITY));
+ }
+
+ private ReplayingBackend openBackend(SortedSet<DN> baseDNs, boolean withVlvIndex, SortedSet<IndexType> cnIndexTypes)
+ throws Exception
+ {
final ReplayingBackend backend = new ReplayingBackend();
backend.setBackendID(BACKEND_ID);
- backend.configuredWith = backendCfg(baseDNs, withVlvIndex);
+ backend.configuredWith = backendCfg(baseDNs, withVlvIndex, cnIndexTypes);
backend.configureBackend(backend.configuredWith, serverContext);
// Start from a pristine on-disk state so that a previous run cannot mask the defect.
backend.storage.removeStorageFiles();
@@ -987,6 +1226,12 @@
private PDBBackendCfg backendCfg(SortedSet<DN> baseDNs, boolean withVlvIndex) throws ConfigException
{
+ return backendCfg(baseDNs, withVlvIndex, newTreeSet(IndexType.EQUALITY));
+ }
+
+ private PDBBackendCfg backendCfg(SortedSet<DN> baseDNs, boolean withVlvIndex, SortedSet<IndexType> cnIndexTypes)
+ throws ConfigException
+ {
final PDBBackendCfg cfg = mockCfg(PDBBackendCfg.class);
when(cfg.dn()).thenReturn(DN.valueOf("ds-cfg-backend-id=" + BACKEND_ID + ",cn=Backends,cn=config"));
when(cfg.getBackendId()).thenReturn(BACKEND_ID);
@@ -996,9 +1241,14 @@
when(cfg.getDBCachePercent()).thenReturn(20);
when(cfg.getBaseDN()).thenReturn(baseDNs);
when(cfg.listBackendIndexes()).thenReturn(new String[] { "cn" });
+ // An index can only be confidential in a backend which is, and the cipher of the backend is what
+ // the crypto suite of an index takes its parameters from.
+ when(cfg.isConfidentialityEnabled()).thenReturn(true);
+ when(cfg.getCipherTransformation()).thenReturn("AES/CBC/PKCS5Padding");
+ when(cfg.getCipherKeyLength()).thenReturn(128);
// Built before it is handed over: stubbing a mock from inside a when() of another mock leaves
// that when() unfinished, and Mockito fails the next test to touch either of them.
- final BackendIndexCfg cnIndexCfg = indexCfg(newTreeSet(IndexType.EQUALITY), 4000);
+ final BackendIndexCfg cnIndexCfg = indexCfg(cnIndexTypes, 4000);
when(cfg.getBackendIndex("cn")).thenReturn(cnIndexCfg);
if (withVlvIndex)
{
@@ -1015,11 +1265,23 @@
private BackendIndexCfg indexCfg(SortedSet<IndexType> indexTypes, int indexEntryLimit)
{
+ return indexCfg(indexTypes, indexEntryLimit, false);
+ }
+
+ /** The configuration which makes the presence index of {@link #openBackendWithPresenceIndex()} confidential. */
+ private BackendIndexCfg confidentialPresenceIndexCfg()
+ {
+ return indexCfg(newTreeSet(IndexType.PRESENCE), 4000, true);
+ }
+
+ private BackendIndexCfg indexCfg(SortedSet<IndexType> indexTypes, int indexEntryLimit, boolean confidentiality)
+ {
final BackendIndexCfg cfg = mock(BackendIndexCfg.class);
when(cfg.getIndexType()).thenReturn(indexTypes);
when(cfg.getAttribute()).thenReturn(cnType);
when(cfg.getIndexEntryLimit()).thenReturn(indexEntryLimit);
when(cfg.getSubstringLength()).thenReturn(6);
+ when(cfg.isConfidentialityEnabled()).thenReturn(confidentiality);
return cfg;
}
--
Gitblit v1.10.0