From 60be91d8768178e3c4bbd8f01b713be382b9e9cf Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Tue, 06 Oct 2026 07:13:14 +0000
Subject: [PATCH] [#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)

---
 opendj-server-legacy/src/test/java/org/opends/server/plugins/ReferentialIntegrityPluginTestCase.java                             |  244 ++++++++++++++++++++++++++++++++++++++++
 opendj-server-legacy/src/messages/org/opends/messages/plugin.properties                                                          |    9 +
 opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-groups.adoc                                                     |    2 
 opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java                                     |   69 ++++++++++-
 opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/ReferentialIntegrityPluginConfiguration.xml |    3 
 5 files changed, 317 insertions(+), 10 deletions(-)

diff --git a/opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-groups.adoc b/opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-groups.adoc
index 1e7bdb3..d012610 100644
--- a/opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-groups.adoc
+++ b/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.
 
 
diff --git a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/ReferentialIntegrityPluginConfiguration.xml b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/ReferentialIntegrityPluginConfiguration.xml
index 1f93883..ce8139e 100644
--- a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/ReferentialIntegrityPluginConfiguration.xml
+++ b/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 />
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java b/opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java
index 2030243..635f08b 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java
+++ b/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")
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/plugin.properties b/opendj-server-legacy/src/messages/org/opends/messages/plugin.properties
index d0aa763..baa3f69 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/plugin.properties
+++ b/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
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/plugins/ReferentialIntegrityPluginTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/plugins/ReferentialIntegrityPluginTestCase.java
index 31014df..4335284 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/plugins/ReferentialIntegrityPluginTestCase.java
+++ b/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
   {

--
Gitblit v1.10.0