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/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java |   69 +++++++++++++++++++++++++++++++---
 1 files changed, 62 insertions(+), 7 deletions(-)

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")

--
Gitblit v1.10.0