From 490181201bfd34a4ca940caca019e49ef824f921 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Fri, 25 Sep 2026 06:25:43 +0000
Subject: [PATCH] [#1050] Document the replication repair control, and test that a repaired modify stays on its replica (#1052)

---
 opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/MultimasterReplication.java |   55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 55 insertions(+), 0 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/MultimasterReplication.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/MultimasterReplication.java
index 3e1f4a9..49181cf 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/MultimasterReplication.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/MultimasterReplication.java
@@ -52,6 +52,7 @@
 import org.opends.server.api.ImportTaskListener;
 import org.opends.server.api.RestoreTaskListener;
 import org.opends.server.api.SynchronizationProvider;
+import org.opends.server.core.AccessControlConfigManager;
 import org.opends.server.core.BackendConfigManager;
 import org.opends.server.core.DirectoryServer;
 import org.opends.server.core.ServerContext;
@@ -64,6 +65,7 @@
 import org.opends.server.types.LDIFImportConfig;
 import org.opends.server.types.Modification;
 import org.opends.server.types.Operation;
+import org.opends.server.types.OperationType;
 import org.opends.server.types.RestoreConfig;
 import org.opends.server.types.SynchronizationProviderResult;
 import org.opends.server.types.operation.PluginOperation;
@@ -161,6 +163,15 @@
           Control c = it.next();
           if (OID_REPLICATION_REPAIR_CONTROL.equals(c.getOID()))
           {
+            if (!mayUseRepairControl(dn, op, c))
+            {
+              /*
+              Leave the control on the request: the backend drops it when it is not critical,
+              and refuses the request when it is, as it does with any control the client may
+              not use.
+              */
+              break;
+            }
             op.setSynchronizationOperation(true);
             op.setDontSynchronize(true);
             /*
@@ -197,6 +208,50 @@
   }
 
   /**
+   * Whether the client may use the repair control on this operation, as the access control of
+   * the controls decides it: a client with the {@code bypass-acl} privilege, or one an ACI allows
+   * to use the control. The question is asked at the entry being repaired, on every operation:
+   * on an add as well, where the backend judges the other controls at the parent entry.
+   * <p>
+   * On a modify and a modify DN the backend has already asked it of every control by the time
+   * the replication plugin runs. An add and a delete reach the plugin before the backend checks
+   * their controls, and the plugin takes the control off the request, so on those operations the
+   * answer given here is the only one. They also reach it before the backend has applied a
+   * proxied authorization control: the answer would be given for the bound client rather than
+   * for the one the operation runs as, so a repair is refused on an add or a delete which
+   * carries one.
+   */
+  private static boolean mayUseRepairControl(DN dn, Operation op, Control control)
+  {
+    final OperationType type = op.getOperationType();
+    if ((type == OperationType.ADD || type == OperationType.DELETE) && carriesProxiedAuthorization(op))
+    {
+      return false;
+    }
+    try
+    {
+      return AccessControlConfigManager.getInstance().getAccessControlHandler().isAllowed(dn, op, control);
+    }
+    catch (DirectoryException e)
+    {
+      logger.traceException(e);
+      return false;
+    }
+  }
+
+  private static boolean carriesProxiedAuthorization(Operation op)
+  {
+    for (Control c : op.getRequestControls())
+    {
+      if (OID_PROXIED_AUTH_V1.equals(c.getOID()) || OID_PROXIED_AUTH_V2.equals(c.getOID()))
+      {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
    * Creates a new domain from its configEntry, do the
    * necessary initialization and starts it so that it is
    * fully operational when this method returns.

--
Gitblit v1.10.0