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,24 +1157,67 @@ 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()) { // 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 { 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) { 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 Index updatedIndex : indexesToUntrust) for (final MatchingRuleIndex givenUp : treesToGiveUp) { updatedIndex.setTrusted(txn, false); // 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()) { updatedIndex.setIndexEntryLimit(newConfiguration.getIndexEntryLimit()); @@ -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) { 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; } } 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); 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); opendj-server-legacy/src/test/java/org/opends/server/backends/jeb/JEIndexConfidentialityChangeTest.java
New file @@ -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); } } opendj-server-legacy/src/test/java/org/opends/server/backends/pdb/PDBIndexConfidentialityChangeTest.java
New file @@ -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); } } opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/IndexConfidentialityChangeTestCase.java
New file @@ -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; } } } 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; }