From 924eb4a46d87170837d94291120a0fbcf0646f8c Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Wed, 05 Aug 2026 17:01:53 +0000
Subject: [PATCH] [#843] Cap the number of operations accepted per batchRequest in the DSML gateway (#844)

---
 opendj-dsml-servlet/src/test/java/org/opends/dsml/protocol/DSMLServletTestCase.java |   72 +++++++++++++++++++++++++++++++++++-
 1 files changed, 70 insertions(+), 2 deletions(-)

diff --git a/opendj-dsml-servlet/src/test/java/org/opends/dsml/protocol/DSMLServletTestCase.java b/opendj-dsml-servlet/src/test/java/org/opends/dsml/protocol/DSMLServletTestCase.java
index 9815660..4a581e3 100644
--- a/opendj-dsml-servlet/src/test/java/org/opends/dsml/protocol/DSMLServletTestCase.java
+++ b/opendj-dsml-servlet/src/test/java/org/opends/dsml/protocol/DSMLServletTestCase.java
@@ -79,7 +79,9 @@
  * body used to be silently skipped, and every batch request of a SOAP body
  * used to be answered inside a single shared batchResponse. Also covers the
  * caps on the number of batchRequest elements per SOAP body (each element
- * costs a bind) and on the size of the request body.
+ * costs a bind), on the size of the request body, and on the number of
+ * operations per batchRequest (a compare on a password attribute costs a
+ * password verification).
  */
 @SuppressWarnings("javadoc")
 @Test(groups = { "precommit", "dsml" })
@@ -125,6 +127,18 @@
         + "</batchRequest>";
   }
 
+  /** A batch request holding the given number of abandon operations. */
+  private static String multiOperationBatch(String requestID, int operationCount)
+  {
+    StringBuilder batch = new StringBuilder(
+        "<batchRequest xmlns=\"urn:oasis:names:tc:DSML:2:0:core\" requestID=\"" + requestID + "\">");
+    for (int i = 1; i <= operationCount; i++)
+    {
+      batch.append("<abandonRequest abandonID=\"").append(i).append("\"/>");
+    }
+    return batch.append("</batchRequest>").toString();
+  }
+
   private static String searchBatch(String requestID)
   {
     return "<batchRequest xmlns=\"urn:oasis:names:tc:DSML:2:0:core\" requestID=\"" + requestID + "\">"
@@ -458,6 +472,59 @@
     }
   }
 
+  /**
+   * A batchRequest holding more operations than the configured cap is rejected
+   * as a whole before the gateway even connects: a compare on a password
+   * attribute costs a password verification, and a provisioning batch applied
+   * halfway is worse than one not attempted. The requestID is kept so that the
+   * client can correlate the reply.
+   */
+  @Test
+  public void testBatchHoldingMoreOperationsThanTheCapIsRejectedWhole() throws Exception
+  {
+    try (FakeLdapServer server = new FakeLdapServer())
+    {
+      Map<String, String> params = new LinkedHashMap<>();
+      params.put("ldap.dsml.batchrequest.operations.max", "2");
+
+      Map<String, String> headers = new LinkedHashMap<>();
+      headers.put("Content-Type", SOAP_1_1_CONTENT_TYPE);
+
+      String response = doPost(server.getPort(), params, headers,
+          soap11(multiOperationBatch("1", 3)));
+
+      assertTrue(response.contains("notAttempted"), response);
+      assertTrue(response.contains("requestID=\"1\""), response);
+      assertTrue(server.getReceivedOpTypes().isEmpty(),
+          "no connection to the directory server should have been opened");
+    }
+  }
+
+  /** A batch of exactly the configured maximum is accepted: the cap fails only past the limit. */
+  @Test
+  public void testBatchOfExactlyTheMaximumOperationsIsAccepted() throws Exception
+  {
+    try (FakeLdapServer server = new FakeLdapServer())
+    {
+      Map<String, String> params = new LinkedHashMap<>();
+      params.put("ldap.dsml.batchrequest.operations.max", "2");
+
+      Map<String, String> headers = new LinkedHashMap<>();
+      headers.put("Content-Type", SOAP_1_1_CONTENT_TYPE);
+
+      String response = doPost(server.getPort(), params, headers,
+          soap11(multiOperationBatch("1", 2)));
+
+      assertFalse(response.contains("errorResponse"), response);
+
+      server.awaitDisconnect();
+      assertEquals(server.getReceivedOpTypes(),
+          list(OP_TYPE_BIND_REQUEST, OP_TYPE_ABANDON_REQUEST, OP_TYPE_ABANDON_REQUEST,
+               OP_TYPE_UNBIND_REQUEST),
+          "a batch of exactly the configured maximum must be processed");
+    }
+  }
+
   /** A cap which is not a positive number must be rejected when the servlet initialises. */
   @Test
   public void testNonPositiveCapsAreRejectedAtInit() throws Exception
@@ -465,7 +532,8 @@
     for (String[] param : new String[][] {
         { "ldap.dsml.batchrequests.max", "0" },
         { "ldap.dsml.batchrequests.max", "banana" },
-        { "ldap.dsml.request.maxsize", "-1" } })
+        { "ldap.dsml.request.maxsize", "-1" },
+        { "ldap.dsml.batchrequest.operations.max", "0" } })
     {
       Map<String, String> params = new LinkedHashMap<>();
       params.put("ldap.host", InetAddress.getLoopbackAddress().getHostAddress());

--
Gitblit v1.10.0