mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
15 hours ago 35a4e8a46adf60988cc29fd82c0115126c1660a3
[#992] Apply the confidentiality of a backend index to the running backend (#1000)
5 files modified
3 files added
1049 ■■■■■ changed files
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/AttributeIndex.java 166 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/DefaultIndex.java 47 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java 14 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/Index.java 2 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/backends/jeb/JEIndexConfidentialityChangeTest.java 54 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/backends/pdb/PDBIndexConfidentialityChangeTest.java 49 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/IndexConfidentialityChangeTestCase.java 449 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/ReplayedConfigChangeTest.java 268 ●●●●● patch | view | raw | blame | history
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;
  }