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

Valery Kharseko
23 hours ago 60be91d8768178e3c4bbd8f01b713be382b9e9cf
[#1172] Refuse a second check-references-filter-criteria value for an attribute type, and enforce all the filters of a configuration that already has one (#1174)

Fixes #1172

## Problem

`ReferentialIntegrityPlugin.applyConfigurationChange` stored each
`check-references-filter-criteria` value with
`newAttrFiltMap.put(attrType, filter)`, so a later value for the same
attribute type overwrote an earlier one, while the validation checked
each value on its own and accepted the configuration. Which filter was
enforced depended on the sort order of the normalized values: the one
that sorts last won. Affected pairs: the same spelling (`manager:(a)` +
`manager:(b)`), a name and an OID or alias of the same type (`manager` +
`0.9.2342.19200300.100.1.10`), and, since #1158, a space before the
colon (`manager:(a)` + `manager :(b)`).

## Fix

The approach follows #1118 in the same plugin: refuse what is being
configured now, load what is already stored.

- **Enabling the plugin or changing its configuration** is refused when
two values name the same attribute type (compared as `AttributeType`, so
by OID): new `ERR_PLUGIN_REFERENT_DUPLICATE_FILTER_CRITERIA_133`, one
reason per attribute type, naming the attribute and all of its values.
- **A configuration already stored with such values** is still loaded
when the server starts, with
`WARN_PLUGIN_REFERENT_DUPLICATE_FILTER_CRITERIA_134`, and the filters of
one attribute type are combined in an AND filter: a reference has to
match all of them. Refusing it would stop the plugin from loading,
including the delete and modify DN clean-up; keeping "last one wins"
would keep an arbitrary choice.
- **Disabling a plugin loaded with a warning** is accepted, for this
case and for the #1118 one: a disabled configuration is checked only for
what loading it needs, as `PluginConfigManager` does, and enabling it
again is still refused.
- `isConfigurationAcceptableIgnoringCheckReferencesPluginTypes` is
renamed `isConfigurationLoadable`, since it now lets through both the
#1118 and the #1172 cases.
- The property description (configuration XML) and the Developer's Guide
(`chap-groups.adoc`) say that an attribute has one filter, that several
criteria go into one AND filter, and that a second value is refused
while the plugin is enabled. The XML has no `(&…)` example: the
description goes into the generated Javadoc of `opendj-config`, where
`&` fails doclint (`bad HTML entity`).

Message ordinals 133–134 in `plugin.properties`: no other open PR
changes that file.

## Tests

`ReferentialIntegrityPluginTestCase`, each for the three pairs above
(`manager:(employeeType=manager)` with `(description=approved)` as
`manager:`, as the OID, and as `manager :`):
- `testDuplicateFilterCriteriaIsNotAcceptable`:
`isConfigurationAcceptable` is false, with one reason naming `manager`
and both values.
- `testDuplicateFilterCriteriaIsLoadedWithWarning`: `initializePlugin`
succeeds and logs message 134 with the DN, the attribute and both
values.
- `testDuplicateFilterCriteriaAreAllEnforced`: a plugin loaded with both
values refuses an employee whose manager matches only
`(employeeType=manager)`, refuses one whose manager matches only
`(description=approved)`, and accepts one whose manager matches both.
The two "only one" steps make sure that neither filter alone is the one
enforced.
- `testDuplicateFilterCriteriaIsRejected`: on the running plugin, adding
`manager :(description=approved)` next to
`manager:(employeeType=manager)` is refused, and the reason names both
values.
- `testDuplicateFilterCriteriaCanBeDisabled`, and
`testCheckReferencesWithoutPreOperationTypesCanBeDisabled` for the #1118
configurations: a plugin loaded with the warning accepts its
configuration with `enabled: false`, and still refuses it with `enabled:
true`.
- `testFilterCriteriaForTwoAttributeTypesAreNotDuplicates`:
`manager:(…)` with `member:(…)` is acceptable, and a manager that
matches only the `manager` filter is accepted.

The #1158 case of `validConfigs` with a colon inside the filter now has
one value, `member:(&(o=urn:example)(cn:dn:=x))`, so that it stays a
clean load.

Without the fix (on #1167) the 10 cases of the first round fail and the
other 65 pass. With it the class passes 82/82. The 6 disable cases fail
with the first round's `isConfigurationChangeAcceptable`, and
`testFilterCriteriaForTwoAttributeTypesAreNotDuplicates` fails when
every value is grouped under the first attribute type, whether in the
duplicate check or in the filters that are enforced.

## Related

While writing the enforcement test I found that both `doPreOperation`
hooks stop after their first check, because a passing check's result
code is `null` rather than `SUCCESS`; that is why the test checks
`continueProcessing()`. Not changed here: tracked in #1173.
5 files modified
327 ■■■■■ changed files
opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-groups.adoc 2 ●●●●● patch | view | raw | blame | history
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/ReferentialIntegrityPluginConfiguration.xml 3 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java 69 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/plugin.properties 9 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/plugins/ReferentialIntegrityPluginTestCase.java 244 ●●●●● patch | view | raw | blame | history
opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-groups.adoc
@@ -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`. 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. Added plugin types take effect at once, and the plugin does not need to be disabled and enabled again.
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)`. An attribute can have only one filter: to require several criteria, combine them in an AND filter, such as `member:(&(objectclass=person)(description=approved))`. When the plugin is enabled, OpenDJ refuses a second value for the same attribute, and it refuses to enable a plugin configured that way. A configuration that already has one when the server starts is loaded with a warning and requires all of its filters. 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. Added plugin types take effect at once, and the plugin does not need to be disabled and enabled again.
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/ReferentialIntegrityPluginConfiguration.xml
@@ -15,6 +15,7 @@
  Copyright 2007-2010 Sun Microsystems, Inc.
  Portions copyright 2011 profiq s.r.o.
  Portions Copyright 2016 ForgeRock AS.
  Portions Copyright 2026 3A Systems, LLC.
  ! -->
<adm:managed-object name="referential-integrity-plugin"
  plural-name="referential-integrity-plugins"
@@ -195,6 +196,8 @@
      If a reference attribute has filter criteria defined then this plugin
      will ensure that any new references added as part of an add or modify
      operation refer to an existing entry which matches the specified filter.
      Each attribute can have only one filter: to require several criteria,
      combine them in a single AND filter.
    </adm:description>
    <adm:default-behavior>
      <adm:undefined />
opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java
@@ -32,6 +32,7 @@
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
@@ -172,7 +173,7 @@
  {
    LinkedList<LocalizableMessage> unacceptableReasons = new LinkedList<>();
    if (!isConfigurationAcceptableIgnoringCheckReferencesPluginTypes(pluginCfg, unacceptableReasons))
    if (!isConfigurationLoadable(pluginCfg, unacceptableReasons))
    {
      throw new ConfigException(unacceptableReasons.getFirst());
    }
@@ -188,6 +189,13 @@
    {
      logger.warn(WARN_PLUGIN_REFERENT_CHECK_REFERENCES_WITHOUT_PLUGIN_TYPE.get(pluginCfg.dn(), t, t));
    }
    // Likewise for two filter criteria of one attribute type (a configuration stored before issue #1172): the
    // plugin is loaded, and enforces all of their filters.
    for (Map.Entry<AttributeType, List<String>> e : getDuplicateFilterCriteria(pluginCfg).entrySet())
    {
      logger.warn(WARN_PLUGIN_REFERENT_DUPLICATE_FILTER_CRITERIA.get(
          pluginCfg.dn(), e.getKey().getNameOrOID(), String.join(", ", e.getValue())));
    }
    applyConfigurationChange(pluginCfg);
@@ -216,8 +224,9 @@
    LinkedHashSet<AttributeType> newAttributeTypes =
            new LinkedHashSet<>(newConfiguration.getAttributeType());
    // Load the attribute-filter mapping
    LinkedHashMap<AttributeType, SearchFilter> newAttrFiltMap = new LinkedHashMap<>();
    // Load the attribute-filter mapping. An attribute type has more than one filter only in a configuration stored
    // before issue #1172, and a reference held by it then has to match all of them.
    LinkedHashMap<AttributeType, List<SearchFilter>> newAttrFilters = new LinkedHashMap<>();
    for (String attrFilt : newConfiguration.getCheckReferencesFilterCriteria())
    {
@@ -228,7 +237,8 @@
      AttributeType attrType = DirectoryServer.getInstance().getServerContext().getSchema().getAttributeType(attr);
      try
      {
        newAttrFiltMap.put(attrType, SearchFilter.createFilterFromString(filtStr));
        SearchFilter filter = SearchFilter.createFilterFromString(filtStr);
        newAttrFilters.computeIfAbsent(attrType, k -> new ArrayList<>()).add(filter);
      }
      catch (DirectoryException unexpected)
      {
@@ -237,6 +247,13 @@
      }
    }
    LinkedHashMap<AttributeType, SearchFilter> newAttrFiltMap = new LinkedHashMap<>();
    for (Map.Entry<AttributeType, List<SearchFilter>> e : newAttrFilters.entrySet())
    {
      List<SearchFilter> filters = e.getValue();
      newAttrFiltMap.put(e.getKey(), filters.size() == 1 ? filters.get(0) : SearchFilter.createANDFilter(filters));
    }
    //User is not allowed to change the logfile name, append a message that the
    //server needs restarting for change to take effect.
    // The first time the plugin is initialised the 'logFileName' is
@@ -272,17 +289,45 @@
  {
    ReferentialIntegrityPluginCfg pluginCfg =
         (ReferentialIntegrityPluginCfg) configuration;
    boolean isAcceptable = isConfigurationAcceptableIgnoringCheckReferencesPluginTypes(pluginCfg, unacceptableReasons);
    boolean isAcceptable = isConfigurationLoadable(pluginCfg, unacceptableReasons);
    for (PluginCfgDefn.PluginType t : getMissingCheckReferencesPluginTypes(pluginCfg))
    {
      isAcceptable = false;
      unacceptableReasons.add(ERR_PLUGIN_REFERENT_CHECK_REFERENCES_WITHOUT_PLUGIN_TYPE.get(t, t));
    }
    for (Map.Entry<AttributeType, List<String>> e : getDuplicateFilterCriteria(pluginCfg).entrySet())
    {
      isAcceptable = false;
      unacceptableReasons.add(ERR_PLUGIN_REFERENT_DUPLICATE_FILTER_CRITERIA.get(
          e.getKey().getNameOrOID(), String.join(", ", e.getValue())));
    }
    return isAcceptable;
  }
  /**
   * Returns the check-references-filter-criteria values of each attribute type that more than one of them names,
   * whether with the same spelling, with another name or the OID of the type, or with a space before the colon.
   * Before issue #1172 only one of them was enforced, the one that sorts last.
   */
  private static Map<AttributeType, List<String>> getDuplicateFilterCriteria(ReferentialIntegrityPluginCfg pluginCfg)
  {
    Map<AttributeType, List<String>> valuesByType = new LinkedHashMap<>();
    for (String attrFilt : pluginCfg.getCheckReferencesFilterCriteria())
    {
      AttributeType attrType = DirectoryServer.getInstance().getServerContext().getSchema()
          .getAttributeType(splitFilterCriteria(attrFilt)[0]);
      // An attribute missing from the schema is refused as not listed in attribute-type.
      if (!attrType.isPlaceHolder())
      {
        valuesByType.computeIfAbsent(attrType, k -> new ArrayList<>()).add(attrFilt);
      }
    }
    valuesByType.values().removeIf(values -> values.size() < 2);
    return valuesByType;
  }
  /**
   * 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.
@@ -299,7 +344,13 @@
    return missing;
  }
  private boolean isConfigurationAcceptableIgnoringCheckReferencesPluginTypes(
  /**
   * Checks what a configuration must satisfy for the plugin to load it. Unlike
   * {@link #isConfigurationAcceptable(PluginCfg, List)}, it lets through a configuration that lacks the plugin types
   * {@code check-references} needs (issue #1118) or has two filter criteria for one attribute type (issue #1172):
   * one stored before those checks existed is loaded with a warning instead.
   */
  private boolean isConfigurationLoadable(
      ReferentialIntegrityPluginCfg pluginCfg, List<LocalizableMessage> unacceptableReasons)
  {
    boolean isAcceptable = true;
@@ -418,7 +469,11 @@
          ReferentialIntegrityPluginCfg configuration,
          List<LocalizableMessage> unacceptableReasons)
  {
    return isConfigurationAcceptable(configuration, unacceptableReasons);
    // A disabled plugin checks no references: let a configuration loaded with a warning be disabled, as
    // PluginConfigManager does, and refuse it again when it is enabled.
    return configuration.isEnabled()
        ? isConfigurationAcceptable(configuration, unacceptableReasons)
        : isConfigurationLoadable(configuration, unacceptableReasons);
  }
  @SuppressWarnings("unchecked")
opendj-server-legacy/src/messages/org/opends/messages/plugin.properties
@@ -363,3 +363,12 @@
 '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', or set 'check-references' to false
ERR_PLUGIN_REFERENT_DUPLICATE_FILTER_CRITERIA_133=The property \
 'check-references-filter-criteria' has more than one value for attribute '%s': \
 %s. Keep a single value for the attribute, and combine the filters in an AND \
 filter if a referenced entry has to match all of them
WARN_PLUGIN_REFERENT_DUPLICATE_FILTER_CRITERIA_134=The Referential Integrity \
 plugin %s has more than one value of 'check-references-filter-criteria' for \
 attribute '%s': %s. The plugin is loaded, and an entry referenced by that \
 attribute has to match all of their filters. Keep a single value for the \
 attribute, and combine the filters in an AND filter
opendj-server-legacy/src/test/java/org/opends/server/plugins/ReferentialIntegrityPluginTestCase.java
@@ -50,10 +50,12 @@
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.internal.SearchRequest;
import org.opends.server.types.Attributes;
import org.opends.server.types.Control;
import org.opends.server.types.Entry;
import org.opends.server.types.InitializationException;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.operation.PreOperationAddOperation;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
@@ -61,6 +63,8 @@
import org.testng.annotations.Test;
import static org.forgerock.opendj.ldap.ModificationType.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.opends.messages.PluginMessages.*;
import static org.opends.server.core.DirectoryServer.*;
import static org.opends.server.protocols.internal.InternalClientConnection.*;
@@ -526,8 +530,7 @@
            "ds-cfg-attribute-type: member",
            "ds-cfg-base-dn: o=test",
            "ds-cfg-check-references: true",
            "ds-cfg-check-references-filter-criteria: member:(o=urn:example)",
            "ds-cfg-check-references-filter-criteria: member:(cn:dn:=x)"
            "ds-cfg-check-references-filter-criteria: member:(&(o=urn:example)(cn:dn:=x))"
    );
    Object[][] array = new Object[entries.size()][1];
    for (int i=0; i < array.length; i++)
@@ -1072,6 +1075,243 @@
    }
  }
  /**
   * Issue #1172: two check-references-filter-criteria values that name the same attribute type, spelled the same,
   * as its name and its OID, or with a space before the colon. The filters are chosen so that an entry matching only
   * one of them shows which one is enforced.
   */
  @DataProvider
  public Object[][] duplicateFilterCriteria()
  {
    return new Object[][] {
      { "manager:(employeeType=manager)", "manager:(description=approved)" },
      { "manager:(employeeType=manager)", "0.9.2342.19200300.100.1.10:(description=approved)" },
      { "manager:(employeeType=manager)", "manager :(description=approved)" },
    };
  }
  /**
   * Issue #1172: enabling the plugin or changing its configuration is refused when two filter criteria name the same
   * attribute type, with one reason that names the attribute and both values.
   */
  @Test(dataProvider = "duplicateFilterCriteria")
  public void testDuplicateFilterCriteriaIsNotAcceptable(String first, String second) throws Exception
  {
    List<LocalizableMessage> reasons = new ArrayList<>();
    boolean acceptable = new ReferentialIntegrityPlugin().isConfigurationAcceptable(
        InitializationUtils.getConfiguration(ReferentialIntegrityPluginCfgDefn.getInstance(),
            filterCriteriaEntry(first, second)), reasons);
    assertFalse(acceptable);
    assertEquals(reasons.size(), 1, reasons.toString());
    String reason = reasons.get(0).toString();
    assertTrue(reason.startsWith("The property 'check-references-filter-criteria' has more than one value for "
        + "attribute 'manager'"), reason);
    assertTrue(reason.contains(first) && reason.contains(second), reason);
  }
  /**
   * Issue #1172: a configuration stored before the check existed is still loaded when the server starts, with a
   * warning that names the attribute and both values.
   */
  @Test(dataProvider = "duplicateFilterCriteria")
  public void testDuplicateFilterCriteriaIsLoadedWithWarning(String first, String second) throws Exception
  {
    Entry e = filterCriteriaEntry(first, second);
    TestCaseUtils.ERROR_TEXT_WRITER.clear();
    ReferentialIntegrityPlugin plugin = initializePlugin(e);
    plugin.finalizePlugin();
    String msgID = "msgID=" + WARN_PLUGIN_REFERENT_DUPLICATE_FILTER_CRITERIA.ordinal() + " msg=";
    List<String> logged = TestCaseUtils.ERROR_TEXT_WRITER.getMessages();
    assertTrue(logged.stream().anyMatch(line -> line.contains(msgID) && line.contains(e.getName().toString())
        && line.contains("'manager'") && line.contains(first) && line.contains(second)), logged.toString());
  }
  /**
   * Issue #1172: a loaded configuration with two filter criteria for one attribute type enforces both of them, not
   * the one whose value happens to sort last.
   */
  @Test(dataProvider = "duplicateFilterCriteria")
  public void testDuplicateFilterCriteriaAreAllEnforced(String first, String second) throws Exception
  {
    String manager = "uid=manager,ou=people,ou=dept,dc=example,dc=com";
    addEntry(manager);
    PreOperationAddOperation addEmployee = addEmployeeOf(manager);
    ReferentialIntegrityPlugin plugin = initializePlugin(filterCriteriaEntry(first, second));
    try
    {
      assertEquals(replaceAttrEntry(DN.valueOf(manager), "employeeType", "manager").getResultCode(),
          ResultCode.SUCCESS);
      assertEquals(plugin.doPreOperation(addEmployee).getResultCode(), ResultCode.CONSTRAINT_VIOLATION,
          "The manager matches only " + first);
      assertEquals(replaceAttrEntry(DN.valueOf(manager), "employeeType").getResultCode(), ResultCode.SUCCESS);
      assertEquals(replaceAttrEntry(DN.valueOf(manager), "description", "approved").getResultCode(),
          ResultCode.SUCCESS);
      assertEquals(plugin.doPreOperation(addEmployee).getResultCode(), ResultCode.CONSTRAINT_VIOLATION,
          "The manager matches only " + second);
      assertEquals(replaceAttrEntry(DN.valueOf(manager), "employeeType", "manager").getResultCode(),
          ResultCode.SUCCESS);
      assertTrue(plugin.doPreOperation(addEmployee).continueProcessing(), "The manager matches both filters");
    }
    finally
    {
      plugin.finalizePlugin();
    }
  }
  /**
   * Issue #1172: adding a second filter criteria value for an attribute type that already has one is refused on a
   * running plugin, and the first value stays enforced alone.
   */
  @Test
  public void testDuplicateFilterCriteriaIsRejected() throws Exception
  {
    replaceAttrEntry(configDN, "ds-cfg-enabled", "false");
    replaceAttrEntry(configDN, dsConfigPluginType,
                               "postoperationdelete",
                               "postoperationmodifydn",
                               "subordinatemodifydn",
                               "subordinatedelete",
                               "preoperationadd",
                               "preoperationmodify");
    addAttrEntry(configDN, dsConfigBaseDN, "dc=example,dc=com");
    replaceAttrEntry(configDN, dsConfigEnforceIntegrity, "true");
    replaceAttrEntry(configDN, dsConfigAttrType, "manager");
    assertEquals(addAttrEntry(configDN, dsConfigAttrFiltMapping, "manager:(employeeType=manager)").getResultCode(),
        ResultCode.SUCCESS);
    assertEquals(replaceAttrEntry(configDN, "ds-cfg-enabled", "true").getResultCode(), ResultCode.SUCCESS);
    ModifyOperation op = addAttrEntry(configDN, dsConfigAttrFiltMapping, "manager :(description=approved)");
    assertNotEquals(op.getResultCode(), ResultCode.SUCCESS);
    String reason = op.getErrorMessage().toString();
    assertTrue(reason.contains("manager:(employeeType=manager)") && reason.contains("manager :(description=approved)"),
        reason);
  }
  /**
   * Issue #1172: filter criteria for two different attribute types are not duplicates. The configuration is
   * acceptable, and each filter is enforced only for its own attribute type.
   */
  @Test
  public void testFilterCriteriaForTwoAttributeTypesAreNotDuplicates() throws Exception
  {
    Entry e = filterCriteriaEntry("manager:(employeeType=manager)", "member:(description=groupMember)");
    e.replaceAttribute(Attributes.create(dsConfigAttrType, "manager", "member"));
    List<LocalizableMessage> reasons = new ArrayList<>();
    boolean acceptable = new ReferentialIntegrityPlugin().isConfigurationAcceptable(
        InitializationUtils.getConfiguration(ReferentialIntegrityPluginCfgDefn.getInstance(), e), reasons);
    assertTrue(acceptable, reasons.toString());
    String manager = "uid=manager,ou=people,ou=dept,dc=example,dc=com";
    addEntry(manager);
    assertEquals(replaceAttrEntry(DN.valueOf(manager), "employeeType", "manager").getResultCode(),
        ResultCode.SUCCESS);
    ReferentialIntegrityPlugin plugin = initializePlugin(e);
    try
    {
      assertTrue(plugin.doPreOperation(addEmployeeOf(manager)).continueProcessing(),
          "The manager does not have to match the filter of member");
    }
    finally
    {
      plugin.finalizePlugin();
    }
  }
  /**
   * Issues #1118 and #1172: a plugin loaded with a warning can be disabled, without fixing its configuration first,
   * but the same configuration is still refused while the plugin stays enabled.
   */
  @Test(dataProvider = "duplicateFilterCriteria")
  public void testDuplicateFilterCriteriaCanBeDisabled(String first, String second) throws Exception
  {
    assertOnlyDisablingIsAcceptable(filterCriteriaEntry(first, second));
  }
  /** Issue #1118: see {@link #testDuplicateFilterCriteriaCanBeDisabled(String, String)}. */
  @Test(dataProvider = "checkReferencesWithoutPreOperationTypes")
  public void testCheckReferencesWithoutPreOperationTypesCanBeDisabled(Entry e, PluginType[] missing)
      throws Exception
  {
    assertOnlyDisablingIsAcceptable(e);
  }
  private void assertOnlyDisablingIsAcceptable(Entry e) throws Exception
  {
    Entry disabled = e.duplicate(false);
    disabled.replaceAttribute(Attributes.create("ds-cfg-enabled", "false"));
    ReferentialIntegrityPlugin plugin = initializePlugin(e);
    try
    {
      List<LocalizableMessage> reasons = new ArrayList<>();
      assertTrue(plugin.isConfigurationChangeAcceptable(
          InitializationUtils.getConfiguration(ReferentialIntegrityPluginCfgDefn.getInstance(), disabled), reasons),
          reasons.toString());
      assertFalse(plugin.isConfigurationChangeAcceptable(
          InitializationUtils.getConfiguration(ReferentialIntegrityPluginCfgDefn.getInstance(), e), reasons));
    }
    finally
    {
      plugin.finalizePlugin();
    }
  }
  /** A pre-operation add of an employee whose manager is the given entry. */
  private static PreOperationAddOperation addEmployeeOf(String manager) throws Exception
  {
    Entry employee = TestCaseUtils.makeEntry(
      "dn: uid=employee,ou=people,ou=dept,dc=example,dc=com",
      "objectclass: top",
      "objectclass: person",
      "objectclass: organizationalperson",
      "objectclass: inetorgperson",
      "uid: employee",
      "cn: employee",
      "sn: employee",
      "givenname: employee",
      "manager: " + manager);
    PreOperationAddOperation addEmployee = mock(PreOperationAddOperation.class);
    when(addEmployee.getEntryToAdd()).thenReturn(employee);
    return addEmployee;
  }
  /**
   * An enabled plugin entry that checks the references held by {@code manager} below {@code dc=example,dc=com}, with
   * the given filter criteria.
   */
  private static Entry filterCriteriaEntry(String... filterCriteria) throws Exception
  {
    List<String> ldif = newArrayList(
        "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-plugin-type: preOperationAdd",
        "ds-cfg-plugin-type: preOperationModify",
        "ds-cfg-attribute-type: manager",
        "ds-cfg-base-dn: dc=example,dc=com",
        "ds-cfg-check-references: true");
    for (String criteria : filterCriteria)
    {
      ldif.add("ds-cfg-check-references-filter-criteria: " + criteria);
    }
    return TestCaseUtils.makeEntry(ldif.toArray(new String[0]));
  }
  /** The lines of the Referential Integrity plugin entry in the fresh-install config.ldif template. */
  private List<String> shippedEntryLines() throws Exception
  {