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

Valery Kharseko
10 hours ago 3295ecee421bbfab950bf5e4bb32e9cc95746f5e
opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-groups.adoc
@@ -12,7 +12,7 @@
  information: "Portions copyright [year] [name of copyright owner]".
 
  Copyright 2017 ForgeRock AS.
  Portions Copyright 2024 3A Systems LLC.
  Portions Copyright 2024-2026 3A Systems, LLC.
////
:figure-caption!:
@@ -539,6 +539,6 @@
----
By default, the referential integrity plugin is configured to manage `member` and `uniqueMember` attributes. These attributes take values that are DNs, and are indexed for equality by default for the default backend. Before you add an additional attribute to manage, make sure that it has DN syntax and that it is indexed for equality. OpenDJ directory server requires that the attribute be indexed because an unindexed search for integrity would potentially consume too many of the server's resources. Attribute syntax is explained in xref:../admin-guide/chap-schema.adoc#chap-schema["Managing Schema"] in the __Administration Guide__. For instructions on indexing attributes, see xref:../admin-guide/chap-indexing.adoc#configure-indexes["Configuring and Rebuilding Indexes"] in the __Administration Guide__.
You can also configure the referential integrity plugin to check that new entries added to groups actually exist in the directory by setting the `check-references` property to `true`. You can specify additional criteria once you have activated the check. To ensure that entries added must match a filter, set the `check-references-filter-criteria` to identify the attribute and the filter. For example, you can specify that group members must be person entries by setting `check-references-filter-criteria` to `member:(objectclass=person)`. To ensure that entries must be located in the same naming context, set `check-references-scope-criteria` to `naming-context`.
You can also configure the referential integrity plugin to check that new entries added to groups actually exist in the directory by setting the `check-references` property to `true`. You can specify additional criteria once you have activated the check. To ensure that entries added must match a filter, set the `check-references-filter-criteria` to identify the attribute and the filter. For example, you can specify that group members must be person entries by setting `check-references-filter-criteria` to `member:(objectclass=person)`. To ensure that entries must be located in the same naming context, set `check-references-scope-criteria` to `naming-context`. The check runs when entries are added and modified, so the plugin must be registered for the `preOperationAdd` and `preOperationModify` plugin types, as the default configuration is. When the plugin is enabled, OpenDJ refuses to set `check-references` to `true` if `plugin-type` lacks either of them, and it refuses to enable a plugin configured that way. A plugin already configured that way when the server starts is loaded with a warning, and it does not check references until the types are added. Plugin types take effect when the plugin is enabled, so add them before enabling the plugin, or disable and re-enable it afterwards.
opendj-server-legacy/resource/config/config.ldif
@@ -1388,6 +1388,8 @@
ds-cfg-plugin-type: postOperationModifyDN
ds-cfg-plugin-type: subordinateModifyDN
ds-cfg-plugin-type: subordinateDelete
ds-cfg-plugin-type: preOperationAdd
ds-cfg-plugin-type: preOperationModify
ds-cfg-attribute-type: member
ds-cfg-attribute-type: uniqueMember
ds-cfg-invoke-for-internal-operations: true
opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java
@@ -33,6 +33,7 @@
import java.io.FileWriter;
import java.io.IOException;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
@@ -105,6 +106,9 @@
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
  /** The plugin types {@code check-references} needs: the references are checked in these hooks. */
  private static final Set<PluginCfgDefn.PluginType> CHECK_REFERENCES_PLUGIN_TYPES = Collections.unmodifiableSet(
      EnumSet.of(PluginCfgDefn.PluginType.PREOPERATIONADD, PluginCfgDefn.PluginType.PREOPERATIONMODIFY));
  /** Current plugin configuration. */
@@ -169,11 +173,19 @@
    pluginCfg.addReferentialIntegrityChangeListener(this);
    LinkedList<LocalizableMessage> unacceptableReasons = new LinkedList<>();
    if (!isConfigurationAcceptable(pluginCfg, unacceptableReasons))
    if (!isConfigurationAcceptableIgnoringCheckReferencesPluginTypes(pluginCfg, unacceptableReasons))
    {
      throw new ConfigException(unacceptableReasons.getFirst());
    }
    // Enabling the plugin or changing its configuration is refused without these types, but a configuration
    // already stored without them (the entry shipped before issue #1118) is loaded with a warning: refusing
    // it would also stop the delete and modify DN clean-up, which does not need them.
    for (PluginCfgDefn.PluginType t : getMissingCheckReferencesPluginTypes(pluginCfg))
    {
      logger.warn(WARN_PLUGIN_REFERENT_CHECK_REFERENCES_WITHOUT_PLUGIN_TYPE.get(pluginCfg.dn(), t, t));
    }
    applyConfigurationChange(pluginCfg);
    // Set up log file. Note: it is not allowed to change once the plugin is active.
@@ -255,9 +267,39 @@
  public boolean isConfigurationAcceptable(PluginCfg configuration,
                                           List<LocalizableMessage> unacceptableReasons)
  {
    boolean isAcceptable = true;
    ReferentialIntegrityPluginCfg pluginCfg =
         (ReferentialIntegrityPluginCfg) configuration;
    boolean isAcceptable = isConfigurationAcceptableIgnoringCheckReferencesPluginTypes(pluginCfg, unacceptableReasons);
    for (PluginCfgDefn.PluginType t : getMissingCheckReferencesPluginTypes(pluginCfg))
    {
      isAcceptable = false;
      unacceptableReasons.add(ERR_PLUGIN_REFERENT_CHECK_REFERENCES_WITHOUT_PLUGIN_TYPE.get(t, t));
    }
    return isAcceptable;
  }
  /**
   * Returns the plugin types {@code check-references} needs that the configuration does not list. The references
   * are checked in the pre-operation add and modify hooks, which the plugin manager only calls for the plugin types
   * listed in the configuration.
   */
  private static Set<PluginCfgDefn.PluginType> getMissingCheckReferencesPluginTypes(
      ReferentialIntegrityPluginCfg pluginCfg)
  {
    if (!pluginCfg.isCheckReferences())
    {
      return Collections.emptySet();
    }
    Set<PluginCfgDefn.PluginType> missing = EnumSet.copyOf(CHECK_REFERENCES_PLUGIN_TYPES);
    missing.removeAll(pluginCfg.getPluginType());
    return missing;
  }
  private boolean isConfigurationAcceptableIgnoringCheckReferencesPluginTypes(
      ReferentialIntegrityPluginCfg pluginCfg, List<LocalizableMessage> unacceptableReasons)
  {
    boolean isAcceptable = true;
    for (PluginCfgDefn.PluginType t : pluginCfg.getPluginType())
    {
opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/Upgrade.java
@@ -97,6 +97,12 @@
      "cn: End Transaction",
      "ds-cfg-java-class: org.opends.server.extensions.EndTransactionExtendedOperation",
      "ds-cfg-enabled: true" };
  static final String REFERENTIAL_INTEGRITY_PLUGIN_FILTER =
      "(objectClass=ds-cfg-referential-integrity-plugin)";
  static final String[] ADD_REFERENTIAL_INTEGRITY_PRE_OPERATION_PLUGIN_TYPES = {
      "add: ds-cfg-plugin-type",
      "ds-cfg-plugin-type: preOperationAdd",
      "ds-cfg-plugin-type: preOperationModify" };
  static
  {
@@ -638,6 +644,16 @@
            START_TRANSACTION_HANDLER_ENTRY),
        addConfigEntry(END_TRANSACTION_HANDLER_ENTRY));
    /* See issue #1118: the shipped Referential Integrity plugin entry lacked the pre-operation plugin
     * types that check-references runs in, so turning check-references on checked nothing. The plugin
     * now refuses check-references without them, so every referential integrity plugin entry gets them:
     * the add is permissive, and the upgrade schema matches plugin types ignoring case, so a type an
     * administrator already added is not added a second time. */
    register("5.2.0",
        modifyConfigEntry(INFO_UPGRADE_TASK_ADD_REFERENTIAL_INTEGRITY_PRE_OPERATION_PLUGIN_TYPES.get(),
            REFERENTIAL_INTEGRITY_PLUGIN_FILTER,
            ADD_REFERENTIAL_INTEGRITY_PRE_OPERATION_PLUGIN_TYPES));
    /*
     * See issue #746. Builds before #661 (fixed in 5.1.2) shipped a duplicate
     * org.openidentityplatform.opendj.opendj-server-legacy.jar alongside opendj.jar in lib/.
@@ -763,7 +779,7 @@
   * @return A list containing all the tasks which are required in order to upgrade
   *         from {@code fromVersion} to {@code toVersion}.
   */
  private static List<UpgradeTask> getUpgradeTasks(final BuildVersion fromVersion, final BuildVersion toVersion)
  static List<UpgradeTask> getUpgradeTasks(final BuildVersion fromVersion, final BuildVersion toVersion)
  {
    final List<UpgradeTask> tasks = new LinkedList<>();
    try {
opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/UpgradeUtils.java
@@ -728,6 +728,12 @@
        + " EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15"
        + " X-ORIGIN 'OpenDS Directory Server' )", false);
    // Adds ds-cfg-plugin-type / ignore match syntax: dsconfig writes plugin types in lower case,
    // while config.ldif spells them in camel case (issue #1118)
    sb.addAttributeType("( 1.3.6.1.4.1.26027.1.1.54 NAME 'ds-cfg-plugin-type'"
        + " EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15"
        + " X-ORIGIN 'OpenDS Directory Server' )", false);
    return sb.toSchema().asNonStrictSchema();
  }
opendj-server-legacy/src/messages/org/opends/messages/plugin.properties
@@ -354,3 +354,13 @@
 the configured naming contexts
ERR_PLUGIN_REFERENT_EXCEPTION_129=The opration could not be processed \
 due to an unexpected exception: '%s'
ERR_PLUGIN_REFERENT_CHECK_REFERENCES_WITHOUT_PLUGIN_TYPE_131=The property \
 'check-references' is set to true, but the property 'plugin-type' does not list \
 '%s', so the references added by that operation would not be checked. Add '%s' to \
 'plugin-type', then disable and re-enable the plugin, or set 'check-references' to false
WARN_PLUGIN_REFERENT_CHECK_REFERENCES_WITHOUT_PLUGIN_TYPE_132=The Referential \
 Integrity plugin %s has 'check-references' set to true, but its property \
 'plugin-type' does not list '%s', so the references added by that operation are \
 not checked. The plugin is loaded and still removes the references to deleted and \
 renamed entries. Add '%s' to 'plugin-type', then disable and re-enable the plugin, \
 or set 'check-references' to false
opendj-server-legacy/src/messages/org/opends/messages/tool.properties
@@ -2738,3 +2738,5 @@
 Global configuration
INFO_UPGRADE_TASK_ADD_TRANSACTION_EXTENDED_OPERATIONS=Adding configuration for LDAP transactions \
 extended operation handlers (RFC 5805)
INFO_UPGRADE_TASK_ADD_REFERENTIAL_INTEGRITY_PRE_OPERATION_PLUGIN_TYPES=Adding the pre-operation add \
 and modify plugin types to the referential integrity plugins
opendj-server-legacy/src/test/java/org/opends/server/plugins/ReferentialIntegrityPluginTestCase.java
@@ -18,8 +18,13 @@
 */
package org.opends.server.plugins;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DN;
@@ -30,6 +35,7 @@
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.schema.AttributeType;
import org.opends.server.TestCaseUtils;
import org.forgerock.opendj.server.config.meta.PluginCfgDefn.PluginType;
import org.forgerock.opendj.server.config.meta.ReferentialIntegrityPluginCfgDefn;
import org.opends.server.api.Group;
import org.opends.server.controls.SubtreeDeleteControl;
@@ -54,6 +60,7 @@
import org.testng.annotations.Test;
import static org.forgerock.opendj.ldap.ModificationType.*;
import static org.opends.messages.PluginMessages.*;
import static org.opends.server.core.DirectoryServer.*;
import static org.opends.server.protocols.internal.InternalClientConnection.*;
import static org.opends.server.protocols.internal.Requests.*;
@@ -781,6 +788,215 @@
    plugin.finalizePlugin();
  }
  /**
   * Issue #1118: configurations with {@code check-references} set to true that lack a pre-operation plugin type,
   * with the plugin types each one lacks.
   */
  @DataProvider(name = "checkReferencesWithoutPreOperationTypes")
  public Object[][] createCheckReferencesWithoutPreOperationTypes()
         throws Exception
  {
    List<Entry> entries = TestCaseUtils.makeEntries(
            // check-references true, plugin types of the entry shipped before issue #1118
            "dn: cn=Referential Integrity,cn=Plugins,cn=config",
            "objectClass: top",
            "objectClass: ds-cfg-plugin",
            "objectClass: ds-cfg-referential-integrity-plugin",
            "cn: Referential Integrity",
            "ds-cfg-java-class: org.opends.server.plugins.ReferentialIntegrityPlugin",
            "ds-cfg-enabled: true",
            "ds-cfg-plugin-type: postOperationDelete",
            "ds-cfg-plugin-type: postOperationModifyDN",
            "ds-cfg-plugin-type: subordinateModifyDN",
            "ds-cfg-plugin-type: subordinateDelete",
            "ds-cfg-attribute-type: member",
            "ds-cfg-check-references: true",
            "",
            // check-references true, preOperationModify missing
            "dn: cn=Referential Integrity,cn=Plugins,cn=config",
            "objectClass: top",
            "objectClass: ds-cfg-plugin",
            "objectClass: ds-cfg-referential-integrity-plugin",
            "cn: Referential Integrity",
            "ds-cfg-java-class: org.opends.server.plugins.ReferentialIntegrityPlugin",
            "ds-cfg-enabled: true",
            "ds-cfg-plugin-type: postOperationDelete",
            "ds-cfg-plugin-type: preOperationAdd",
            "ds-cfg-attribute-type: member",
            "ds-cfg-check-references: true",
            "",
            // check-references true, preOperationAdd missing
            "dn: cn=Referential Integrity,cn=Plugins,cn=config",
            "objectClass: top",
            "objectClass: ds-cfg-plugin",
            "objectClass: ds-cfg-referential-integrity-plugin",
            "cn: Referential Integrity",
            "ds-cfg-java-class: org.opends.server.plugins.ReferentialIntegrityPlugin",
            "ds-cfg-enabled: true",
            "ds-cfg-plugin-type: postOperationDelete",
            "ds-cfg-plugin-type: preOperationModify",
            "ds-cfg-attribute-type: member",
            "ds-cfg-check-references: true"
    );
    return new Object[][] {
      { entries.get(0), new PluginType[] { PluginType.PREOPERATIONADD, PluginType.PREOPERATIONMODIFY } },
      { entries.get(1), new PluginType[] { PluginType.PREOPERATIONMODIFY } },
      { entries.get(2), new PluginType[] { PluginType.PREOPERATIONADD } },
    };
  }
  /**
   * Issue #1118: enabling the plugin or changing its configuration is refused when
   * {@code check-references} lacks a pre-operation plugin type, with one reason for each missing type.
   */
  @Test(dataProvider = "checkReferencesWithoutPreOperationTypes")
  public void testCheckReferencesWithoutPreOperationTypesIsNotAcceptable(Entry e, PluginType[] missing)
          throws Exception
  {
    List<String> expected = new ArrayList<>();
    for (PluginType t : missing)
    {
      expected.add(ERR_PLUGIN_REFERENT_CHECK_REFERENCES_WITHOUT_PLUGIN_TYPE.get(t, t).toString());
    }
    List<LocalizableMessage> reasons = new ArrayList<>();
    boolean acceptable = new ReferentialIntegrityPlugin().isConfigurationAcceptable(
        InitializationUtils.getConfiguration(ReferentialIntegrityPluginCfgDefn.getInstance(), e), reasons);
    assertFalse(acceptable);
    List<String> actual = new ArrayList<>();
    for (LocalizableMessage reason : reasons)
    {
      actual.add(reason.toString());
    }
    assertEquals(actual, expected);
  }
  /**
   * Issue #1118: a configuration stored before the check existed is still loaded when the server starts, with a
   * warning for each missing type, so that the delete and modify DN clean-up keeps running.
   */
  @Test(dataProvider = "checkReferencesWithoutPreOperationTypes")
  public void testCheckReferencesWithoutPreOperationTypesIsLoadedWithWarning(Entry e, PluginType[] missing)
          throws Exception
  {
    TestCaseUtils.ERROR_TEXT_WRITER.clear();
    ReferentialIntegrityPlugin plugin = initializePlugin(e);
    plugin.finalizePlugin();
    List<String> logged = TestCaseUtils.ERROR_TEXT_WRITER.getMessages();
    for (PluginType t : missing)
    {
      LocalizableMessage warning = WARN_PLUGIN_REFERENT_CHECK_REFERENCES_WITHOUT_PLUGIN_TYPE.get(e.getName(), t, t);
      String record = "msgID=" + warning.ordinal() + " msg=" + warning;
      assertTrue(logged.stream().anyMatch(line -> line.contains(record)), logged.toString());
    }
  }
  /**
   * Issue #1118: on a disabled plugin {@code check-references} can be set without the pre-operation types, but the
   * plugin cannot then be enabled.
   */
  @Test
  public void testEnablingCheckReferencesWithoutPreOperationTypesIsRejected() throws Exception
  {
    try
    {
      assertEquals(replaceAttrEntry(configDN, "ds-cfg-enabled", "false").getResultCode(), ResultCode.SUCCESS);
      assertEquals(replaceAttrEntry(configDN, dsConfigPluginType,
                                 "postoperationdelete",
                                 "postoperationmodifydn",
                                 "subordinatemodifydn",
                                 "subordinatedelete").getResultCode(), ResultCode.SUCCESS);
      assertEquals(replaceAttrEntry(configDN, dsConfigEnforceIntegrity, "true").getResultCode(), ResultCode.SUCCESS);
      ModifyOperation op = replaceAttrEntry(configDN, "ds-cfg-enabled", "true");
      assertNotEquals(op.getResultCode(), ResultCode.SUCCESS);
      String reason = op.getErrorMessage().toString();
      assertTrue(reason.contains("preoperationadd") && reason.contains("preoperationmodify"), reason);
    }
    finally
    {
      deleteAttrsEntry(configDN, dsConfigEnforceIntegrity);
      replaceAttrEntry(configDN, dsConfigPluginType,
                               "postoperationdelete",
                               "postoperationmodifydn",
                               "subordinatemodifydn",
                               "subordinatedelete",
                               "preoperationadd",
                               "preoperationmodify");
      assertEquals(replaceAttrEntry(configDN, "ds-cfg-enabled", "true").getResultCode(), ResultCode.SUCCESS);
    }
  }
  /**
   * Issue #1118: the entry a fresh install ships must be able to check references once
   * {@code check-references} is turned on, so it has to list the pre-operation plugin types.
   */
  @Test
  public void testShippedEntryAcceptsCheckReferences() throws Exception
  {
    List<String> ldif = shippedEntryLines();
    ldif.add("ds-cfg-check-references: true");
    ReferentialIntegrityPlugin plugin = initializePlugin(TestCaseUtils.makeEntry(ldif.toArray(new String[0])));
    plugin.finalizePlugin();
  }
  /**
   * Issue #1118: turning {@code check-references} on for a running plugin whose plugin types
   * lack the pre-operation types must be refused rather than stored and silently ignored.
   */
  @Test
  public void testCheckReferencesWithoutPreOperationTypesIsRejected() throws Exception
  {
    assertEquals(replaceAttrEntry(configDN, dsConfigPluginType,
                               "postoperationdelete",
                               "postoperationmodifydn",
                               "subordinatemodifydn",
                               "subordinatedelete").getResultCode(), ResultCode.SUCCESS);
    assertEquals(replaceAttrEntry(configDN, "ds-cfg-enabled", "true").getResultCode(), ResultCode.SUCCESS);
    try
    {
      ModifyOperation op = replaceAttrEntry(configDN, dsConfigEnforceIntegrity, "true");
      assertNotEquals(op.getResultCode(), ResultCode.SUCCESS);
      String reason = op.getErrorMessage().toString();
      assertTrue(reason.contains("preoperationadd") && reason.contains("preoperationmodify"), reason);
    }
    finally
    {
      replaceAttrEntry(configDN, dsConfigPluginType,
                               "postoperationdelete",
                               "postoperationmodifydn",
                               "subordinatemodifydn",
                               "subordinatedelete",
                               "preoperationadd",
                               "preoperationmodify");
    }
  }
  /** The lines of the Referential Integrity plugin entry in the fresh-install config.ldif template. */
  private List<String> shippedEntryLines() throws Exception
  {
    File template = new File(TestCaseUtils.getBuildRoot(), "resource/config/config.ldif");
    List<String> entry = new ArrayList<>();
    for (String line : Files.readAllLines(template.toPath(), StandardCharsets.UTF_8))
    {
      if (entry.isEmpty() && !line.equalsIgnoreCase("dn: cn=Referential Integrity,cn=Plugins,cn=config"))
      {
        continue;
      }
      if (line.isEmpty())
      {
        break;
      }
      entry.add(line);
    }
    assertFalse(entry.isEmpty(), "config.ldif template no longer ships the Referential Integrity plugin");
    return entry;
  }
  private ReferentialIntegrityPlugin initializePlugin(Entry e) throws ConfigException, InitializationException {
    return InitializationUtils.initializePlugin(
        new ReferentialIntegrityPlugin(), e, ReferentialIntegrityPluginCfgDefn.getInstance());
opendj-server-legacy/src/test/java/org/opends/server/tools/upgrade/UpgradeUtilsTestCase.java
@@ -19,25 +19,33 @@
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.ldif.LDIFEntryReader;
import org.forgerock.opendj.ldif.LDIFEntryWriter;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.opends.server.util.BuildVersion;
import org.opends.server.util.ChangeOperationType;
import org.opends.server.util.StaticUtils;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.opends.messages.ToolMessages.INFO_UPGRADE_TASK_ADD_REFERENTIAL_INTEGRITY_PRE_OPERATION_PLUGIN_TYPES;
import static org.testng.Assert.*;
/**
 * Tests that the issue #851 upgrade task payloads, applied through
 * {@link UpgradeUtils#updateConfigFile}, add the RFC 5805 transaction extended operation handler
 * entries exactly once and match the fresh-install template.
 * entries exactly once and match the fresh-install template, and that the issue #1118 payload
 * gives the Referential Integrity plugin the plugin types of the fresh-install template.
 */
@SuppressWarnings("javadoc")
@Test(groups = { "precommit", "tools" }, sequential = true)
@@ -46,6 +54,10 @@
  /** Unknown config attributes must not fail parsing, as in the upgrade tool's own schema. */
  private final Schema schema = Schema.getCoreSchema().asNonStrictSchema();
  private static final String PLUGIN_TYPE = "ds-cfg-plugin-type";
  private final DN referentialIntegrityPluginDN =
      DN.valueOf("cn=Referential Integrity,cn=Plugins,cn=config", schema);
  /** The config.ldif template a fresh install starts from. */
  private File freshInstallTemplate()
  {
@@ -88,11 +100,111 @@
    }
  }
  @DataProvider
  public Object[][] referentialIntegrityPluginTypesBeforeUpgrade()
  {
    return new Object[][] {
      // The entry shipped before issue #1118.
      { new String[] { "postOperationDelete", "postOperationModifyDN", "subordinateModifyDN",
                       "subordinateDelete" } },
      // The same entry with one type already added by hand, in the lower case dsconfig writes.
      { new String[] { "postOperationDelete", "postOperationModifyDN", "subordinateModifyDN",
                       "subordinateDelete", "preoperationadd" } },
    };
  }
  /** Issue #1118: the task leaves the plugin with the plugin types a fresh install ships, each once. */
  @Test(dataProvider = "referentialIntegrityPluginTypesBeforeUpgrade")
  public void testAddReferentialIntegrityPluginTypesMirrorsFreshInstallTemplate(final String[] pluginTypes)
      throws Exception
  {
    final File tempDir = TestCaseUtils.createTemporaryDirectory("upgradeTask1118");
    try
    {
      final File config = new File(tempDir, "config.ldif");
      writeConfigWithReferentialIntegrityPluginTypes(config, pluginTypes);
      assertEquals(applyAddReferentialIntegrityPluginTypes(config), 1);
      assertEquals(applyAddReferentialIntegrityPluginTypes(config), 1);
      assertEquals(referentialIntegrityPluginTypes(config),
                   referentialIntegrityPluginTypes(freshInstallTemplate()));
    }
    finally
    {
      StaticUtils.recursiveDelete(tempDir);
    }
  }
  /** Issue #1118: an upgrade from 5.1.x to 5.2.0 runs the task that adds the plugin types. */
  @Test
  public void testAddReferentialIntegrityPluginTypesTaskRunsOnUpgradeTo520() throws Exception
  {
    final List<String> summaries = new ArrayList<>();
    for (final UpgradeTask task : Upgrade.getUpgradeTasks(BuildVersion.valueOf("5.1.2"), BuildVersion.valueOf("5.2.0")))
    {
      summaries.add(task.toString());
    }
    assertTrue(summaries.contains(INFO_UPGRADE_TASK_ADD_REFERENTIAL_INTEGRITY_PRE_OPERATION_PLUGIN_TYPES.get().toString()),
        summaries.toString());
  }
  private int applyAdd(final File config, final String... ldifLines) throws Exception
  {
    return UpgradeUtils.updateConfigFile(config, null, ChangeOperationType.ADD, ldifLines);
  }
  private int applyAddReferentialIntegrityPluginTypes(final File config) throws Exception
  {
    return UpgradeUtils.updateConfigFile(config, Filter.valueOf(Upgrade.REFERENTIAL_INTEGRITY_PLUGIN_FILTER),
        ChangeOperationType.MODIFY, Upgrade.ADD_REFERENTIAL_INTEGRITY_PRE_OPERATION_PLUGIN_TYPES);
  }
  private void writeConfigWithReferentialIntegrityPluginTypes(final File config, final String... pluginTypes)
      throws Exception
  {
    int replaced = 0;
    try (LDIFEntryReader reader =
            new LDIFEntryReader(new FileInputStream(freshInstallTemplate())).setSchema(schema);
        LDIFEntryWriter writer = new LDIFEntryWriter(new FileOutputStream(config)))
    {
      while (reader.hasNext())
      {
        final Entry entry = reader.readEntry();
        if (entry.getName().equals(referentialIntegrityPluginDN))
        {
          entry.replaceAttribute(PLUGIN_TYPE, (Object[]) pluginTypes);
          replaced++;
        }
        writer.writeEntry(entry);
      }
    }
    assertEquals(replaced, 1, "fresh-install template no longer ships the Referential Integrity plugin");
  }
  /**
   * The plugin types of the Referential Integrity plugin, lower-cased and sorted. Duplicates are
   * kept: the schema used to read matches the unknown config attribute case-exactly, so two
   * values differing only in case both show.
   */
  private List<String> referentialIntegrityPluginTypes(final File config) throws Exception
  {
    for (final Entry entry : readEntries(config))
    {
      if (entry.getName().equals(referentialIntegrityPluginDN))
      {
        final List<String> types = new ArrayList<>();
        for (final ByteString value : entry.getAttribute(PLUGIN_TYPE))
        {
          types.add(value.toString().toLowerCase(Locale.ROOT));
        }
        Collections.sort(types);
        return types;
      }
    }
    throw new AssertionError("no entry " + referentialIntegrityPluginDN + " in " + config);
  }
  private void writeConfigWithoutTransactionEntries(final File config) throws Exception
  {
    final DN startDN = dnOf(Upgrade.START_TRANSACTION_HANDLER_ENTRY);