This is an automated email from the ASF dual-hosted git repository.

smolnar82 pushed a commit to branch v3.0.0
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/v3.0.0 by this push:
     new 7f9b68670 KNOX-3386: LDAP Proxy pages backends (#1331)
7f9b68670 is described below

commit 7f9b686708aa2e9aa379d48fd4da27fce1ad6266
Author: David Han <[email protected]>
AuthorDate: Fri Jul 31 09:11:08 2026 -0500

    KNOX-3386: LDAP Proxy pages backends (#1331)
---
 .github/workflows/build/gateway-site.xml           |  13 ++
 .../compose/single-eku-no-mtls/gateway-site.xml    |  13 ++
 .../workflows/compose/single-eku/gateway-site.xml  |  13 ++
 .../gateway/config/impl/GatewayConfigImpl.java     |  27 +++-
 .../services/ldap/KnoxLDAPServerManager.java       |  18 ++-
 .../knox/gateway/services/ldap/LdapMessages.java   |  20 ++-
 .../services/ldap/backend/LdapProxyBackend.java    | 160 ++++++++++++-------
 .../src/main/resources/conf/gateway-site.xml       |  12 ++
 .../services/ldap/KnoxLDAPServerManagerTest.java   |  21 +++
 .../gateway/services/ldap/KnoxLDAPServiceTest.java |   2 +
 .../ldap/backend/LdapProxyBackendSslTest.java      |   4 +-
 .../ldap/backend/LdapProxyBackendTest.java         | 170 ++++++++++++++++++---
 .../test/resources/ldap-proxy-backend-test.ldif    |  19 +++
 .../org/apache/knox/gateway/GatewayTestConfig.java |  10 ++
 .../apache/knox/gateway/config/GatewayConfig.java  |  12 ++
 knox-site/docs/service_ldap_server.md              |   6 +
 16 files changed, 436 insertions(+), 84 deletions(-)

diff --git a/.github/workflows/build/gateway-site.xml 
b/.github/workflows/build/gateway-site.xml
index 00698bebd..002fadea5 100644
--- a/.github/workflows/build/gateway-site.xml
+++ b/.github/workflows/build/gateway-site.xml
@@ -145,6 +145,14 @@ limitations under the License.
         <name>gateway.ldap.base.dn</name>
         <value>dc=proxy,dc=org</value>
     </property>
+    <property>
+        <name>gateway.ldap.max.size.limit</name>
+        <value>1000</value>
+    </property>
+    <property>
+        <name>gateway.ldap.max.time.limit</name>
+        <value>60000</value>
+    </property>
     <property>
         <name>gateway.ldap.recursive.group.resolution</name>
         <value>true</value>
@@ -211,5 +219,10 @@ limitations under the License.
         <name>gateway.ldap.interceptor.demoldap.groupMemberAttribute</name>
         <value>member</value>
     </property>
+    <!-- Set an unnaturally low page size to ensure that paging is used in 
tests -->
+    <property>
+        <name>gateway.ldap.interceptor.demoldap.pageSize</name>
+        <value>3</value>
+    </property>
 
 </configuration>
diff --git a/.github/workflows/compose/single-eku-no-mtls/gateway-site.xml 
b/.github/workflows/compose/single-eku-no-mtls/gateway-site.xml
index 94d613ff9..791a3b2a5 100644
--- a/.github/workflows/compose/single-eku-no-mtls/gateway-site.xml
+++ b/.github/workflows/compose/single-eku-no-mtls/gateway-site.xml
@@ -157,6 +157,14 @@ limitations under the License.
         <name>gateway.ldap.base.dn</name>
         <value>dc=proxy,dc=org</value>
     </property>
+    <property>
+        <name>gateway.ldap.max.size.limit</name>
+        <value>1000</value>
+    </property>
+    <property>
+        <name>gateway.ldap.max.time.limit</name>
+        <value>60000</value>
+    </property>
     <property>
         <name>gateway.ldap.recursive.group.resolution</name>
         <value>true</value>
@@ -203,5 +211,10 @@ limitations under the License.
         <name>gateway.ldap.interceptor.demoldap.groupMemberAttribute</name>
         <value>member</value>
     </property>
+    <!-- Set an unnaturally low page size to ensure that paging is used in 
tests -->
+    <property>
+        <name>gateway.ldap.interceptor.demoldap.pageSize</name>
+        <value>3</value>
+    </property>
 
 </configuration>
diff --git a/.github/workflows/compose/single-eku/gateway-site.xml 
b/.github/workflows/compose/single-eku/gateway-site.xml
index 4272c7b73..76f488b85 100644
--- a/.github/workflows/compose/single-eku/gateway-site.xml
+++ b/.github/workflows/compose/single-eku/gateway-site.xml
@@ -211,6 +211,14 @@ limitations under the License.
         <name>gateway.ldap.base.dn</name>
         <value>dc=proxy,dc=org</value>
     </property>
+    <property>
+        <name>gateway.ldap.max.size.limit</name>
+        <value>1000</value>
+    </property>
+    <property>
+        <name>gateway.ldap.max.time.limit</name>
+        <value>60000</value>
+    </property>
     <property>
         <name>gateway.ldap.recursive.group.resolution</name>
         <value>true</value>
@@ -257,5 +265,10 @@ limitations under the License.
         <name>gateway.ldap.interceptor.demoldap.groupMemberAttribute</name>
         <value>member</value>
     </property>
+    <!-- Set an unnaturally low page size to ensure that paging is used in 
tests -->
+    <property>
+        <name>gateway.ldap.interceptor.demoldap.pageSize</name>
+        <value>3</value>
+    </property>
 
 </configuration>
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
index 061518537..d0e34d0fc 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
@@ -392,6 +392,13 @@ public class GatewayConfigImpl extends Configuration 
implements GatewayConfig {
   public static final String STRICT_TRANSPORT_ENABLED = 
GATEWAY_CONFIG_FILE_PREFIX + ".strict.transport.enabled";
   public static final String STRICT_TRANSPORT_OPTION = 
GATEWAY_CONFIG_FILE_PREFIX + ".strict.transport.option";
 
+  // Gateway LDAP Properties
+  public static final int DEFAULT_LDAP_PORT = 3890;
+  public static final String DEFAULT_LDAP_BASE_DN = "dc=proxy,dc=com";
+  public static final int DEFAULT_LDAP_MAX_SIZE_LIMIT = 1000;
+  /* The default max time for LDAP search in milliseconds */
+  public static final int DEFAULT_LDAP_MAX_TIME_LIMIT = 60 * 1000;
+
   public GatewayConfigImpl() {
     init();
   }
@@ -1775,17 +1782,17 @@ public class GatewayConfigImpl extends Configuration 
implements GatewayConfig {
   // LDAP Service Configuration
   @Override
   public boolean isLDAPEnabled() {
-    return Boolean.parseBoolean(get(LDAP_ENABLED, "false"));
+    return getBoolean(LDAP_ENABLED, false);
   }
 
   @Override
   public int getLDAPPort() {
-    return Integer.parseInt(get(LDAP_PORT, "3890"));
+    return getInt(LDAP_PORT, DEFAULT_LDAP_PORT);
   }
 
   @Override
   public String getLDAPBaseDN() {
-    return get(LDAP_BASE_DN, "dc=proxy,dc=com");
+    return get(LDAP_BASE_DN, DEFAULT_LDAP_BASE_DN);
   }
 
   @Override
@@ -1840,7 +1847,7 @@ public class GatewayConfigImpl extends Configuration 
implements GatewayConfig {
 
   @Override
   public boolean isLDAPRecursiveGroupResolutionEnabled() {
-    return Boolean.parseBoolean(get(LDAP_RECURSIVE_GROUP_RESOLUTION, "false"));
+    return getBoolean(LDAP_RECURSIVE_GROUP_RESOLUTION, false);
   }
 
   @Override
@@ -1865,7 +1872,7 @@ public class GatewayConfigImpl extends Configuration 
implements GatewayConfig {
 
   @Override
   public boolean isLDAPSSLEnabled() {
-    return Boolean.parseBoolean(get(LDAP_SSL_ENABLED, "false"));
+    return getBoolean(LDAP_SSL_ENABLED, false);
   }
 
   @Override
@@ -1884,6 +1891,16 @@ public class GatewayConfigImpl extends Configuration 
implements GatewayConfig {
     return cipherSuites == null ? Collections.emptyList() : cipherSuites;
   }
 
+  @Override
+  public int getLDAPMaxSizeLimit() {
+    return getInt(LDAP_MAX_SIZE_LIMIT, DEFAULT_LDAP_MAX_SIZE_LIMIT);
+  }
+
+  @Override
+  public int getLDAPMaxTimeLimit() {
+    return getInt(LDAP_MAX_TIME_LIMIT, DEFAULT_LDAP_MAX_TIME_LIMIT);
+  }
+
   @Override
   public boolean getGroupUIServicesOnHomepage() {
     return getBoolean(KNOX_HOMEPAGE_GROUP_UI_SERVICES, 
DEFAULT_GROUP_UI_SERVICES);
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java
index 3e42c2117..e4445276e 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java
@@ -72,7 +72,8 @@ public class KnoxLDAPServerManager {
 
     @VisibleForTesting
     DirectoryService directoryService;
-    private LdapServer ldapServer;
+    @VisibleForTesting
+    LdapServer ldapServer;
     private GatewayConfig gatewayConfig;
     private List<Interceptor> interceptors;
     private boolean hasRolesLookupInterceptor;
@@ -87,6 +88,8 @@ public class KnoxLDAPServerManager {
     private List<String> sslEnabledCipherSuites;
     // Collection of DNs for the proxied backend LDAP servers
     private Set<String> baseDns;
+    private int maxSizeLimit;
+    private int maxTimeLimit;
 
     KnoxLDAPServerManager(AliasService aliasService) {
         this(aliasService, null);
@@ -114,6 +117,9 @@ public class KnoxLDAPServerManager {
         this.baseDn = config.getLDAPBaseDN();
         this.bindUser = config.getLDAPBindUser();
 
+        maxSizeLimit = config.getLDAPMaxSizeLimit();
+        maxTimeLimit = config.getLDAPMaxTimeLimit();
+
         // Secure (LDAPS) transport configuration. When enabled but no 
dedicated keystore is
         // configured, fall back to the gateway identity keystore so the 
embedded server can
         // reuse the gateway's own TLS material out of the box.
@@ -148,6 +154,13 @@ public class KnoxLDAPServerManager {
 
             // Add common configuration
             interceptorConfig.put("baseDn", baseDn);
+            if (!interceptorConfig.containsKey("maxResultSetSize")) {
+                // Set the backend to return more results than the proxy's 
size limit.
+                // This will ensure that the proxy will return "Size limit 
exceeded"
+                if (maxSizeLimit != 0) {
+                    interceptorConfig.put("maxResultSetSize", 
Integer.toString(maxSizeLimit + 1));
+                }
+            }
 
             // Add common LDAP Proxy configurations to backends
             if 
("backend".equalsIgnoreCase(interceptorConfig.get("interceptorType"))) {
@@ -241,6 +254,9 @@ public class KnoxLDAPServerManager {
         ldapServer.setTransports(transport);
         ldapServer.setDirectoryService(directoryService);
 
+        ldapServer.setMaxSizeLimit(maxSizeLimit);
+        ldapServer.setMaxTimeLimit(maxTimeLimit);
+
         ldapServer.start();
 
         LOG.ldapServiceStarted(port);
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapMessages.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapMessages.java
index 816fef003..8683f4645 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapMessages.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapMessages.java
@@ -73,6 +73,10 @@ public interface LdapMessages {
             text = "Creating LDAP interceptor: {0} (via {1})")
     void ldapInterceptorCreating(String interceptorName, String source);
 
+    @Message(level = MessageLevel.INFO,
+            text = "Configuring LDAP interceptor {0}: {1} = {2}")
+    void ldapInterceptorConfiguring(String interceptorName, String configName, 
String configValue);
+
     @Message(level = MessageLevel.INFO,
             text = "Loading backend: {0} (via {1})")
     void ldapBackendLoading(String backendName, String source);
@@ -101,6 +105,18 @@ public interface LdapMessages {
             text = "LDAP Search: {0} | {1}")
     void ldapSearch(String baseDn, String filter);
 
+    @Message(level = MessageLevel.DEBUG,
+            text = "LDAP Paged Search: {0} | {1}, page size {2}, page {3}")
+    void ldapPagedSearch(String baseDn, String filter, int pageSize, int 
pageNumber);
+
+    @Message(level = MessageLevel.ERROR,
+            text = "LDAP Paged Search Exceeded Max Result Set Size: {0} | {1}")
+    void ldapPagedSearchExceededMaxResultSetSize(int resultSetSize, int 
maxResultSetSize);
+
+    @Message(level = MessageLevel.DEBUG,
+            text = "LDAP Paged Search Completed: {0} | {1}")
+    void ldapPagedSearchCompleted(String baseDn, String filter);
+
     @Message(level = MessageLevel.ERROR,
             text = "LDAP Search failed: {0} | {1}, {2}")
     void ldapSearchFailed(String baseDn, String filter, @StackTrace(level = 
MessageLevel.DEBUG) Exception e);
@@ -133,9 +149,9 @@ public interface LdapMessages {
             text = "Backend user not found: {0}")
     void ldapUserNull(String username);
 
-    @Message(level = MessageLevel.ERROR,
+    @Message(level = MessageLevel.DEBUG,
             text = "Failed to copy attribute: {0}")
-    void ldapAttributeCopyError(@StackTrace(level = MessageLevel.DEBUG) 
Exception e);
+    void ldapAttributeCopyError(@StackTrace(level = MessageLevel.TRACE) 
Exception e);
 
     @Message(level = MessageLevel.DEBUG, text = "LDAP authentication succeeded 
for user: {0}")
     void ldapAuthSucceeded(String user);
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackend.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackend.java
index 0c5616852..32fd033b2 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackend.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackend.java
@@ -21,12 +21,20 @@ import static java.util.Locale.ROOT;
 
 import org.apache.directory.api.ldap.model.cursor.CursorException;
 import org.apache.directory.api.ldap.model.cursor.EntryCursor;
+import org.apache.directory.api.ldap.model.cursor.SearchCursor;
 import org.apache.directory.api.ldap.model.entry.Attribute;
 import org.apache.directory.api.ldap.model.entry.DefaultEntry;
 import org.apache.directory.api.ldap.model.entry.Entry;
 import org.apache.directory.api.ldap.model.entry.Value;
 import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.message.Response;
+import org.apache.directory.api.ldap.model.message.SearchRequest;
+import org.apache.directory.api.ldap.model.message.SearchRequestImpl;
+import org.apache.directory.api.ldap.model.message.SearchResultDone;
+import org.apache.directory.api.ldap.model.message.SearchResultEntry;
 import org.apache.directory.api.ldap.model.message.SearchScope;
+import org.apache.directory.api.ldap.model.message.controls.PagedResults;
+import org.apache.directory.api.ldap.model.message.controls.PagedResultsImpl;
 import org.apache.directory.api.ldap.model.name.Dn;
 import org.apache.directory.api.ldap.model.schema.SchemaManager;
 import org.apache.directory.ldap.client.api.DefaultLdapConnectionFactory;
@@ -50,7 +58,6 @@ import java.security.KeyStore;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -96,6 +103,8 @@ public class LdapProxyBackend implements LdapBackend {
     private boolean useMemberOf; // Use memberOf attribute for group lookup 
(efficient for AD)
     private boolean recursiveGroupResolution;
     private int recursiveGroupResolutionMaxDepth;
+    private int pageSize;
+    private int maxResultSetSize;
 
     private final String proxyEntryGroupMembershipAttributeType = "memberOf";
 
@@ -184,6 +193,11 @@ public class LdapProxyBackend implements LdapBackend {
         recursiveGroupResolution = 
Boolean.parseBoolean(config.getOrDefault("recursiveGroupResolution", "false"));
         recursiveGroupResolutionMaxDepth = 
Integer.parseInt(config.getOrDefault("recursiveGroupResolutionMaxDepth", "3"));
 
+        // Configure search parameters
+        pageSize = Integer.parseInt(config.getOrDefault("pageSize", "1000"));
+        maxResultSetSize = 
Integer.parseInt(config.getOrDefault("maxResultSetSize", "0")); // 0 means 
unlimited
+        LOG.ldapInterceptorConfiguring(name, "maxResultSetSize", 
Integer.toString(maxResultSetSize));
+
         // Configure secure transport (LDAPS) to the remote server. An 
ldaps:// URL enables it
         // by default; an explicit useSsl setting always wins.
         final boolean ldapsFromUrl = ldapUrl != null && 
ldapUrl.toLowerCase(Locale.ROOT).startsWith("ldaps://");
@@ -490,15 +504,14 @@ public class LdapProxyBackend implements LdapBackend {
             return List.of();
         }
 
-        LdapConnection connection = null;
-        try {
-            connection = getConnection();
-            List<Entry> groups = getUserGroupsEntries(connection, user, 
createEntryCache(), createResolvedParentsCache());
-            List<String> cns = getCnsFromEntries(groups);
-            return cns;
-        } finally {
-            releaseConnection(connection);
+        List<String> groups = new ArrayList<>();
+        Attribute groupsAttribute = 
user.get(proxyEntryGroupMembershipAttributeType);
+        if (groupsAttribute != null) {
+            for (Value value : groupsAttribute) {
+                groups.add(new Dn(value.getString()).getRdn().getValue());
+            }
         }
+        return groups;
     }
 
     @Override
@@ -511,12 +524,10 @@ public class LdapProxyBackend implements LdapBackend {
         try {
             connection = getConnection();
             String ldapFilter = "(" + remoteUserIdentifierAttribute + "=" + 
filter.trim() + ")";
-            try (EntryCursor cursor = connection.search(remoteUserSearchBase, 
ldapFilter, SearchScope.SUBTREE, "*")) {
-                while (cursor.next()) {
-                    Entry sourceEntry = cursor.get();
-                    addGroupMemberships(sourceEntry, connection, entryCache, 
resolvedParentsCache);
-                    
results.add(remoteSchemaConverter.convertRemoteEntryToProxyEntry(sourceEntry, 
schemaManager));
-                }
+            List<Entry> searchResults = performPagedSearch(connection, 
remoteUserSearchBase, ldapFilter, SearchScope.SUBTREE, "*");
+            for (Entry sourceEntry : searchResults) {
+                addGroupMemberships(sourceEntry, connection, entryCache, 
resolvedParentsCache);
+                
results.add(remoteSchemaConverter.convertRemoteEntryToProxyEntry(sourceEntry, 
schemaManager));
             }
             return results;
         } finally {
@@ -535,14 +546,10 @@ public class LdapProxyBackend implements LdapBackend {
         try {
             connection = getConnection();
             List<Entry> results = new ArrayList<>();
-            try (EntryCursor cursor = connection.search(remoteSearchBase, 
remoteFilter, searchScope, "*")) {
-                while (cursor.next()) {
-                    Entry entry = cursor.get();
-                    addGroupMemberships(entry, connection, entryCache, 
resolvedParentsCache);
-                    
results.add(remoteSchemaConverter.convertRemoteEntryToProxyEntry(entry, 
schemaManager));
-                }
-            } catch (LdapException e) {
-                LOG.ldapSearchFailed(remoteSearchBase, remoteFilter, e);
+            List<Entry> searchResults = performPagedSearch(connection, 
remoteSearchBase, remoteFilter, searchScope, "*");
+            for (Entry entry : searchResults) {
+                addGroupMemberships(entry, connection, entryCache, 
resolvedParentsCache);
+                
results.add(remoteSchemaConverter.convertRemoteEntryToProxyEntry(entry, 
schemaManager));
             }
             return results;
         } finally {
@@ -721,21 +728,19 @@ public class LdapProxyBackend implements LdapBackend {
                 }
                 String filter = 
buildMultipleGroupMemberFilter(groupDns.toArray(new Dn[0]));
 
-                try (EntryCursor cursor = 
connection.search(remoteGroupSearchBase, filter, SearchScope.SUBTREE, "cn", 
"memberUid", "member", "uniqueMember")) {
-                    while (cursor.next()) {
-                        Entry parentGroup = cursor.get();
-                        String parentDn = parentGroup.getDn().getNormName();
-
-                        // Update cache for all groups found in this search
-                        updateCache(entryCache, resolvedParentsCache, 
groupsToSearch, parentGroup);
-
-                        if (!allGroupDns.contains(parentDn)) {
-                            allGroupDns.add(parentDn);
-                            allGroups.add(parentGroup);
-                            nextLevelGroups.add(parentGroup);
-                        } else {
-                            
LOG.ldapRecursiveGroupSearchCycleDetected(entryName, parentDn);
-                        }
+                List<Entry> searchResults = performPagedSearch(connection, 
remoteGroupSearchBase, filter, SearchScope.SUBTREE, "cn", "memberUid", 
"member", "uniqueMember");
+                for (Entry parentGroup : searchResults) {
+                    String parentDn = parentGroup.getDn().getNormName();
+
+                    // Update cache for all groups found in this search
+                    updateCache(entryCache, resolvedParentsCache, 
groupsToSearch, parentGroup);
+
+                    if (!allGroupDns.contains(parentDn)) {
+                        allGroupDns.add(parentDn);
+                        allGroups.add(parentGroup);
+                        nextLevelGroups.add(parentGroup);
+                    } else {
+                        LOG.ldapRecursiveGroupSearchCycleDetected(entryName, 
parentDn);
                     }
                 }
 
@@ -839,13 +844,69 @@ public class LdapProxyBackend implements LdapBackend {
 
         String filter = buildMultipleGroupMemberFilter(dns);
 
-        try (EntryCursor cursor = connection.search(remoteGroupSearchBase, 
filter, SearchScope.SUBTREE, "cn")) {
-            while (cursor.next()) {
-                groups.add(cursor.get());
+        groups.addAll(performPagedSearch(connection, remoteGroupSearchBase, 
filter, SearchScope.SUBTREE, "cn"));
+
+        return groups;
+    }
+
+    protected List<Entry> performPagedSearch(LdapConnection connection, String 
baseDn, String filter, SearchScope scope, String... attributes ) throws 
LdapException, CursorException, IOException {
+        List<Entry> results = new ArrayList<>();
+
+        // 1. Setup basic search parameters
+        SearchRequest searchRequest = new SearchRequestImpl();
+        searchRequest.setBase(new Dn(baseDn));
+        searchRequest.setFilter(filter);
+        searchRequest.setScope(scope);
+        searchRequest.addAttributes(attributes);
+
+        // 2. Initialize the PagedResults control
+        PagedResults pagedControl = new PagedResultsImpl();
+        pagedControl.setSize(pageSize);
+        searchRequest.addControl(pagedControl);
+
+        byte[] cookie = null;
+
+        // 3. Loop until no more pages remain
+        int pageNumber = 1;
+        do {
+            // Update cookie for the subsequent pages
+            if (cookie != null) {
+                pagedControl.setCookie(cookie);
             }
+
+            try (SearchCursor cursor = connection.search(searchRequest)) {
+                LOG.ldapPagedSearch(baseDn, filter, pageSize, pageNumber);
+                while (cursor.next()) {
+                    Response response = cursor.get();
+
+                    // Process matching entries
+                    if (response instanceof SearchResultEntry) {
+                        Entry entry = ((SearchResultEntry) 
response).getEntry();
+                        results.add(entry);
+                    }
+                }
+                if (cursor.isDone()) {
+                    SearchResultDone done = cursor.getSearchResultDone();
+                    PagedResults responseControl = (PagedResults) 
done.getControl(PagedResults.OID);
+
+                    if (responseControl != null) {
+                        cookie = responseControl.getCookie();
+                    } else {
+                        cookie = null;
+                    }
+                }
+                pageNumber++;
+            }
+        } while (cookie != null && cookie.length > 0 &&
+                (maxResultSetSize == 0 || results.size() < maxResultSetSize));
+
+        if (maxResultSetSize != 0 && results.size() >= maxResultSetSize) {
+            LOG.ldapPagedSearchExceededMaxResultSetSize(results.size(), 
maxResultSetSize);
+        } else {
+            LOG.ldapPagedSearchCompleted(baseDn, filter);
         }
 
-        return groups;
+        return results;
     }
 
     private String buildMultipleGroupMemberFilter(Dn... dns) {
@@ -877,21 +938,6 @@ public class LdapProxyBackend implements LdapBackend {
         return filterBuilder.toString();
     }
 
-    private List<String> getCnsFromEntries(Collection<Entry> entries) throws 
LdapException {
-        List<String> cns = new ArrayList<>();
-        for (Entry entry : entries) {
-            Attribute cnAttr = entry.get("cn");
-            if (cnAttr != null) {
-                cns.add(cnAttr.getString());
-            } else if (entry.getDn() != null && entry.getDn().getRdn() != 
null) {
-                // Fall back to the CN carried in the DN when the entry was 
fetched without the
-                // cn attribute, so resolved groups are not silently dropped 
from the result.
-                cns.add(entry.getDn().getRdn().getValue());
-            }
-        }
-        return cns;
-    }
-
     protected Map<String, Entry> createEntryCache() {
         return new HashMap<>();
     }
diff --git a/gateway-server/src/main/resources/conf/gateway-site.xml 
b/gateway-server/src/main/resources/conf/gateway-site.xml
index fda674c17..f549c413f 100644
--- a/gateway-server/src/main/resources/conf/gateway-site.xml
+++ b/gateway-server/src/main/resources/conf/gateway-site.xml
@@ -56,6 +56,18 @@ limitations under the License.
         <description>Base DN for LDAP entries in the proxy server. Default is 
dc=proxy,dc=com.</description>
     </property>
 
+    <property>
+        <name>gateway.ldap.max.size.limit</name>
+        <value>1000</value>
+        <description>Maximum number of entries returned by a search 
request.</description>
+    </property>
+
+    <property>
+        <name>gateway.ldap.max.time.limit</name>
+        <value>60000</value>
+        <description>Maximum time for a search request in 
milliseconds.</description>
+    </property>
+
     <property>
         <name>gateway.ldap.recursive.group.resolution</name>
         <value>false</value>
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java
index 327d701c3..a5e7df664 100644
--- 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java
@@ -465,6 +465,27 @@ public class KnoxLDAPServerManagerTest {
                 List.of("analysts"), groups);
     }
 
+    @Test
+    public void testStartSetsMaxSizeAndTime() throws Exception {
+        final int expectedMaxSize = 3158;
+        final int expectedMaxTime = 245000;
+
+        GatewayConfig mockConfig = 
EasyMock.createNiceMock(GatewayConfig.class);
+        
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
+        expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
+        
expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
+        
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of()).anyTimes();
+        
expect(mockConfig.getLDAPMaxSizeLimit()).andReturn(expectedMaxSize).anyTimes();
+        
expect(mockConfig.getLDAPMaxTimeLimit()).andReturn(expectedMaxTime).anyTimes();
+        replay(mockConfig);
+
+        serverManager.initialize(mockConfig);
+        serverManager.start();
+
+        assertEquals(expectedMaxSize, 
serverManager.ldapServer.getMaxSizeLimit());
+        assertEquals(expectedMaxTime, 
serverManager.ldapServer.getMaxTimeLimit());
+    }
+
     @Test(expected = LdapException.class)
     public void testBindRequiredRejectsAnonymous() throws Exception {
         useBindPassword(BIND_PASSWORD);
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServiceTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServiceTest.java
index ce681a561..f32c036f8 100644
--- 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServiceTest.java
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServiceTest.java
@@ -180,6 +180,8 @@ public class KnoxLDAPServiceTest {
         expect(mockConfig.getLDAPBindUser()).andReturn(null).anyTimes();
         
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("testbackend")).atLeastOnce();
         
expect(mockConfig.getLDAPInterceptorConfig("testbackend")).andReturn(buildBackendConfig(backendType)).atLeastOnce();
+        expect(mockConfig.getLDAPMaxSizeLimit()).andReturn(1000).atLeastOnce();
+        
expect(mockConfig.getLDAPMaxTimeLimit()).andReturn(60000).atLeastOnce();
         replay(mockConfig);
     }
 
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendSslTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendSslTest.java
index 96b8e5a8f..3ae14d812 100644
--- 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendSslTest.java
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendSslTest.java
@@ -158,7 +158,8 @@ public class LdapProxyBackendSslTest {
         assertEquals("ldaptest1", entry.get("uid").getString());
         validateMemberOf(entry, Set.of(
                 "cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org",
-                "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org"));
+                "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=group3,ou=groups,dc=hadoop,dc=apache,dc=org"));
     }
 
     @Test
@@ -168,6 +169,7 @@ public class LdapProxyBackendSslTest {
         List<String> groups = ldapProxyBackend.getUserGroups("ldaptest1", 
schemaManager);
         assertTrue(groups.contains("group1"));
         assertTrue(groups.contains("group2"));
+        assertTrue(groups.contains("group3"));
     }
 
     @Test
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendTest.java
index 58432f137..7264527db 100644
--- 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendTest.java
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendTest.java
@@ -25,6 +25,7 @@ import static org.junit.Assert.assertTrue;
 
 import org.apache.directory.api.ldap.model.entry.Entry;
 import org.apache.directory.api.ldap.model.entry.Value;
+import org.apache.directory.api.ldap.model.message.SearchRequest;
 import org.apache.directory.api.ldap.model.message.SearchScope;
 import org.apache.directory.api.ldap.model.name.Dn;
 import org.apache.directory.api.ldap.model.schema.SchemaManager;
@@ -37,6 +38,8 @@ import 
org.apache.directory.server.core.factory.JdbmPartitionFactory;
 import org.apache.directory.server.core.factory.PartitionFactory;
 import org.apache.directory.server.core.partition.ldif.LdifPartition;
 import org.apache.directory.server.ldap.LdapServer;
+import org.apache.directory.server.ldap.LdapSession;
+import org.apache.directory.server.ldap.handlers.LdapRequestHandler;
 import org.apache.directory.server.protocol.shared.store.LdifFileLoader;
 import org.apache.directory.server.protocol.shared.transport.TcpTransport;
 import org.apache.knox.gateway.security.ldap.SimpleDirectoryService;
@@ -47,6 +50,8 @@ import org.junit.BeforeClass;
 import org.junit.Test;
 
 import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -57,12 +62,15 @@ import java.util.concurrent.Callable;
 import java.util.concurrent.atomic.AtomicInteger;
 
 public class LdapProxyBackendTest {
+    private static final int PAGE_SIZE = 2;
+
     private static Map<String, String> ldapBackendConfig;
 
     private static TcpTransport transport;
     private static DirectoryService directoryService;
     private static LdapServer ldapServer;
     private static SchemaManager schemaManager;
+    private static CapturingSearchRequestHandler capturingSearchRequestHandler;
 
     private LdapProxyBackend ldapProxyBackend;
 
@@ -110,10 +118,19 @@ public class LdapProxyBackendTest {
 
         // Create and start the LDAP server
         ldapServer = new LdapServer();
+
         ldapServer.setTransports(transport);
         ldapServer.setDirectoryService(directoryService);
+
         ldapServer.start();
 
+        capturingSearchRequestHandler = new 
CapturingSearchRequestHandler(ldapServer.getSearchRequestHandler());
+        ldapServer.setSearchHandlers(
+                capturingSearchRequestHandler,
+                ldapServer.getSearchResultEntryHandler(),
+                ldapServer.getSearchResultReferenceHandler(),
+                ldapServer.getSearchResultDoneHandler());
+
         // Setup common backend config values for tests
         ldapBackendConfig = Map.of(
                 "baseDn", "dc=hadoop,dc=apache,dc=org",
@@ -143,6 +160,7 @@ public class LdapProxyBackendTest {
 
     @After
     public void tearDown() throws Exception {
+        capturingSearchRequestHandler.reset();
         if (ldapProxyBackend != null) {
             ldapProxyBackend.close();
         }
@@ -157,7 +175,8 @@ public class LdapProxyBackendTest {
         validateUserEntry(entry, "ldaptest1", "TestCn1", 
"[email protected]", "Test user ldaptest1");
         validateMemberOf(entry, Set.of(
                 "cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org",
-                "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org"));
+                "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=group3,ou=groups,dc=hadoop,dc=apache,dc=org"));
     }
 
     @Test
@@ -177,7 +196,8 @@ public class LdapProxyBackendTest {
         validateUserEntry(entry, "ldaptest1", "TestCn1", 
"[email protected]", "Test user ldaptest1");
         validateMemberOf(entry, Set.of(
                 "cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org",
-                "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org"));
+                "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=group3,ou=groups,dc=hadoop,dc=apache,dc=org"));
     }
 
     @Test
@@ -189,7 +209,8 @@ public class LdapProxyBackendTest {
         validateUserEntry(entry, "ldaptest1", "TestCn1", 
"[email protected]", "Test user ldaptest1");
         validateMemberOf(entry, Set.of(
                 "cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org",
-                "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org"));
+                "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=group3,ou=groups,dc=hadoop,dc=apache,dc=org"));
     }
 
     @Test
@@ -211,7 +232,8 @@ public class LdapProxyBackendTest {
         assertEquals("TestSam1", entry.get("sAMAccountName").getString());
         validateMemberOf(entry, Set.of(
                 "cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org",
-                "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org"));
+                "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=group3,ou=groups,dc=hadoop,dc=apache,dc=org"));
     }
 
     @Test
@@ -230,8 +252,8 @@ public class LdapProxyBackendTest {
         config.put("useMemberOf", "true");
         ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
-        Entry entry = ldapProxyBackend.getUser("ldaptest2", schemaManager);
-        validateUserEntry(entry, "ldaptest2", "TestCn2", 
"[email protected]", "Test user ldaptest2");
+        Entry entry = ldapProxyBackend.getUser("ldapmemberof", schemaManager);
+        validateUserEntry(entry, "ldapmemberof", "TestMemberOf", 
"[email protected]", "Test user ldapmemberof");
         validateMemberOf(entry, Set.of(
                 "cn=groupMemberOf1,ou=groups,dc=hadoop,dc=apache,dc=org",
                 "cn=groupMemberOf2,ou=groups,dc=hadoop,dc=apache,dc=org"));
@@ -244,6 +266,38 @@ public class LdapProxyBackendTest {
         List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest1", 
schemaManager);
         assertTrue(userGroups.contains("group1"));
         assertTrue(userGroups.contains("group2"));
+        assertTrue(userGroups.contains("group3"));
+    }
+
+    @Test
+    public void testGetUserGroupsPaging() throws Exception {
+        Map<String, String> config = new HashMap<>(ldapBackendConfig);
+        config.put("pageSize", Integer.toString(PAGE_SIZE));
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+
+        List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest1", 
schemaManager);
+        assertEquals(3, userGroups.size());
+        int matchingRequests = (int) 
capturingSearchRequestHandler.getRequests().stream()
+                .filter(request -> 
request.getBase().getName().equals("ou=groups,dc=hadoop,dc=apache,dc=org") &&
+                        request.getFilter().toString().contains("ldaptest1"))
+                .count();
+        assertEquals(2, matchingRequests);
+    }
+
+    @Test
+    public void testGetUserGroupsPagingExceedsMaxResultSetSize() throws 
Exception {
+        Map<String, String> config = new HashMap<>(ldapBackendConfig);
+        config.put("pageSize", Integer.toString(PAGE_SIZE));
+        config.put("maxResultSetSize", "1");
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+
+        List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest1", 
schemaManager);
+        assertEquals(PAGE_SIZE, userGroups.size()); // only retrieve 1 page 
because that will exceed the maxResultSetSize
+        int matchingRequests = (int) 
capturingSearchRequestHandler.getRequests().stream()
+                .filter(request -> 
request.getBase().getName().equals("ou=groups,dc=hadoop,dc=apache,dc=org") &&
+                        request.getFilter().toString().contains("ldaptest1"))
+                .count();
+        assertEquals(1, matchingRequests);
     }
 
     @Test
@@ -268,7 +322,7 @@ public class LdapProxyBackendTest {
         config.put("useMemberOf", "true");
         ldapProxyBackend = new LdapProxyBackend("testbackend", config);
 
-        List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest2", 
schemaManager);
+        List<String> userGroups = 
ldapProxyBackend.getUserGroups("ldapmemberof", schemaManager);
         assertTrue(userGroups.contains("groupMemberOf1"));
         assertTrue(userGroups.contains("groupMemberOf2"));
     }
@@ -323,13 +377,42 @@ public class LdapProxyBackendTest {
     @Test
     public void testSearchUsers() throws Exception {
         ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
-        validateUserSearch("*", 3, Set.of("ldaptest1", "ldaptest2", "guest"));
+        validateUserSearch("*", 4, Set.of("ldaptest1", "ldaptest2", 
"ldapmemberof", "guest"));
+    }
+
+    @Test
+    public void testSearchUsersWithPaging() throws Exception {
+        Map<String, String> config = new HashMap<>(ldapBackendConfig);
+        config.put("pageSize", Integer.toString(PAGE_SIZE));
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+        validateUserSearch("*", 4, Set.of("ldaptest1", "ldaptest2", 
"ldapmemberof", "guest"));
+        int matchingRequests = (int) 
capturingSearchRequestHandler.getRequests().stream()
+                .filter(request -> 
request.getBase().getName().equals("ou=people,dc=hadoop,dc=apache,dc=org") &&
+                        request.getFilter().toString().contains("uid=*"))
+                .count();
+        assertEquals(2, matchingRequests);
+    }
+
+    @Test
+    public void testSearchUsersWithPagingExceedsMaxResultSetSize() throws 
Exception {
+        Map<String, String> config = new HashMap<>(ldapBackendConfig);
+        config.put("pageSize", Integer.toString(PAGE_SIZE));
+        config.put("maxResultSetSize", "1");
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+
+        List<Entry> entries = ldapProxyBackend.searchUsers("*", schemaManager);
+        assertEquals(PAGE_SIZE, entries.size()); // only expect 1 page of 
results
+        int matchingRequests = (int) 
capturingSearchRequestHandler.getRequests().stream()
+                .filter(request -> 
request.getBase().getName().equals("ou=people,dc=hadoop,dc=apache,dc=org") &&
+                        request.getFilter().toString().contains("uid=*"))
+                .count();
+        assertEquals(1, matchingRequests);
     }
 
     @Test
     public void testSearchUsersPartial() throws Exception {
         ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
-        validateUserSearch("ldap*", 2, Set.of("ldaptest1", "ldaptest2"));
+        validateUserSearch("ldap*", 3, Set.of("ldaptest1", "ldaptest2", 
"ldapmemberof"));
     }
 
     @Test
@@ -343,7 +426,7 @@ public class LdapProxyBackendTest {
     public void testSearchUsersByCn() throws Exception {
         Map<String, String> config = createConfigWithUserAttr("cn");
         ldapProxyBackend = new LdapProxyBackend("testbackend", config);
-        validateUserSearch("*", 4, Set.of("ldaptest1", "ldaptest2", "Guest", 
"TestCn3"));
+        validateUserSearch("*", 5, Set.of("ldaptest1", "ldaptest2", 
"ldapmemberof", "Guest", "TestCn3"));
     }
 
     @Test
@@ -365,7 +448,7 @@ public class LdapProxyBackendTest {
     public void testSearchUsersBySAMAccountName() throws Exception {
         Map<String, String> config = 
createConfigWithUserAttr("sAMAccountName");
         ldapProxyBackend = new LdapProxyBackend("testbackend", config);
-        validateUserSearch("*", 3, Set.of("ldaptest1", "ldaptest2", 
"TestSam3"));
+        validateUserSearch("*", 4, Set.of("ldaptest1", "ldaptest2", 
"ldapmemberof", "TestSam3"));
     }
 
     @Test
@@ -451,7 +534,7 @@ public class LdapProxyBackendTest {
     @Test
     public void testSearchObjectClassInetOrgPerson() throws Exception {
         ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
-        validateSearch("ou=people,dc=hadoop,dc=apache,dc=org", 
"(objectClass=inetOrgPerson)", 4, Set.of("ldaptest1", "ldaptest2", "guest", 
"TestCn3"));
+        validateSearch("ou=people,dc=hadoop,dc=apache,dc=org", 
"(objectClass=inetOrgPerson)", 5, Set.of("ldaptest1", "ldaptest2", 
"ldapmemberof", "guest", "TestCn3"));
     }
 
     @Test
@@ -463,19 +546,47 @@ public class LdapProxyBackendTest {
     @Test
     public void testSearchByUidWildcard() throws Exception {
         ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
-        validateSearch("ou=people,dc=hadoop,dc=apache,dc=org", "(uid=*)", 3, 
Set.of("ldaptest1", "ldaptest2", "guest"));
+        validateSearch("ou=people,dc=hadoop,dc=apache,dc=org", "(uid=*)", 4, 
Set.of("ldaptest1", "ldaptest2", "ldapmemberof", "guest"));
+    }
+
+    @Test
+    public void testSearchWithPaging() throws Exception {
+        Map<String, String> config = new HashMap<>(ldapBackendConfig);
+        config.put("pageSize", Integer.toString(PAGE_SIZE));
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+        validateSearch("ou=people,dc=hadoop,dc=apache,dc=org", "(uid=*)", 4, 
Set.of("ldaptest1", "ldaptest2", "ldapmemberof", "guest"));
+        int matchingRequests = (int) 
capturingSearchRequestHandler.getRequests().stream()
+                .filter(request -> 
request.getBase().getName().equals("ou=people,dc=hadoop,dc=apache,dc=org") &&
+                        request.getFilter().toString().contains("uid=*"))
+                .count();
+        assertEquals(2, matchingRequests);
+    }
+
+    @Test
+    public void testSearchWithPagingExceedsMaxResultSize() throws Exception {
+        Map<String, String> config = new HashMap<>(ldapBackendConfig);
+        config.put("pageSize", Integer.toString(PAGE_SIZE));
+        config.put("maxResultSetSize", "1");
+        ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+        List<Entry> entries = 
ldapProxyBackend.search("ou=people,dc=hadoop,dc=apache,dc=org", 
SearchScope.SUBTREE, "(uid=*)", schemaManager);
+        assertEquals(PAGE_SIZE, entries.size()); // only expect 1 page because 
that will exceed the maxResultSetSize
+        int matchingRequests = (int) 
capturingSearchRequestHandler.getRequests().stream()
+                .filter(request -> 
request.getBase().getName().equals("ou=people,dc=hadoop,dc=apache,dc=org") &&
+                        request.getFilter().toString().contains("uid=*"))
+                .count();
+        assertEquals(1, matchingRequests);
     }
 
     @Test
     public void testSearchByUidSubstringWildcard() throws Exception {
         ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
-        validateSearch("ou=people,dc=hadoop,dc=apache,dc=org", "(uid=ldap*)", 
2, Set.of("ldaptest1", "ldaptest2"));
+        validateSearch("ou=people,dc=hadoop,dc=apache,dc=org", "(uid=ldap*)", 
3, Set.of("ldaptest1", "ldaptest2", "ldapmemberof"));
     }
 
     @Test
     public void testSearchObjectClassGroupOfNames() throws Exception {
         ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
-        validateSearch("ou=groups,dc=hadoop,dc=apache,dc=org", 
"(objectClass=groupOfNames)", 3, Set.of("group1", "group2", 
"nameddifferently"));
+        validateSearch("ou=groups,dc=hadoop,dc=apache,dc=org", 
"(objectClass=groupOfNames)", 4, Set.of("group1", "group2", "group3", 
"nameddifferently"));
     }
 
     @Test
@@ -487,19 +598,19 @@ public class LdapProxyBackendTest {
     @Test
     public void testSearchByCnWildcard() throws Exception {
         ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
-        validateSearch("ou=groups,dc=hadoop,dc=apache,dc=org", "(cn=*)", 3, 
Set.of("group1", "group2", "nameddifferently"));
+        validateSearch("ou=groups,dc=hadoop,dc=apache,dc=org", "(cn=*)", 4, 
Set.of("group1", "group2", "group3", "nameddifferently"));
     }
 
     @Test
     public void testSearchByCnWSubstringildcard() throws Exception {
         ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
-        validateSearch("ou=groups,dc=hadoop,dc=apache,dc=org", "(cn=group*)", 
2, Set.of("group1", "group2"));
+        validateSearch("ou=groups,dc=hadoop,dc=apache,dc=org", "(cn=group*)", 
3, Set.of("group1", "group2", "group3"));
     }
 
     @Test
     public void testSearchByUidOrCnWildcard() throws Exception {
         ldapProxyBackend = new LdapProxyBackend("testbackend", 
ldapBackendConfig);
-        validateSearch("dc=hadoop,dc=apache,dc=org", 
"(|(uid=ldap*)(cn=group*))", 4, Set.of("ldaptest1", "ldaptest2", "group1", 
"group2"));
+        validateSearch("dc=hadoop,dc=apache,dc=org", 
"(|(uid=ldap*)(cn=group*))", 6, Set.of("ldaptest1", "ldaptest2", 
"ldapmemberof", "group1", "group2", "group3"));
     }
 
     @Test
@@ -809,4 +920,27 @@ public class LdapProxyBackendTest {
         // For the second user, many groups should have been found in the 
cache.
         assertEquals("Expected " + expectedCacheHits + " cache hits for shared 
groups, but got " + cacheHits.get(), expectedCacheHits, cacheHits.get());
     }
+
+    private static class CapturingSearchRequestHandler extends 
LdapRequestHandler<SearchRequest> {
+        private final LdapRequestHandler<SearchRequest> delegate;
+        private final List<SearchRequest> requests = 
Collections.synchronizedList(new ArrayList<>());
+
+        CapturingSearchRequestHandler(LdapRequestHandler<SearchRequest> 
delegate) {
+            this.delegate = delegate;
+        }
+
+        public void reset() {
+            requests.clear();
+        }
+
+        public List<SearchRequest> getRequests() {
+            return List.copyOf(requests);
+        }
+
+        @Override
+        public void handle(LdapSession session, SearchRequest message) throws 
Exception {
+            requests.add(message);
+            delegate.handle(session, message);
+        }
+    }
 }
diff --git a/gateway-server/src/test/resources/ldap-proxy-backend-test.ldif 
b/gateway-server/src/test/resources/ldap-proxy-backend-test.ldif
index 91fb4136c..456105f98 100644
--- a/gateway-server/src/test/resources/ldap-proxy-backend-test.ldif
+++ b/gateway-server/src/test/resources/ldap-proxy-backend-test.ldif
@@ -58,6 +58,12 @@ objectclass:groupOfNames
 cn: group2
 member: uid=ldaptest1,ou=people,dc=hadoop,dc=apache,dc=org
 
+dn: cn=group3,ou=groups,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass:groupOfNames
+cn: group3
+member: uid=ldaptest1,ou=people,dc=hadoop,dc=apache,dc=org
+
 dn: cn=nameddifferently,ou=groups,dc=hadoop,dc=apache,dc=org
 objectclass:top
 objectclass:groupOfNames
@@ -89,6 +95,19 @@ sAMAccountName: TestSam2
 userPassword: 12345
 mail: [email protected]
 description: Test user ldaptest2
+
+dn: uid=ldapmemberof,ou=people,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass:person
+objectclass:organizationalPerson
+objectclass:inetOrgPerson
+cn: TestMemberOf
+sn: Ldap
+uid: ldapmemberof
+sAMAccountName: TestMemberOf
+userPassword: 12345
+mail: [email protected]
+description: Test user ldapmemberof
 memberOf: cn=groupMemberOf1,ou=groups,dc=hadoop,dc=apache,dc=org
 memberOf: cn=groupMemberOf2,ou=groups,dc=hadoop,dc=apache,dc=org
 
diff --git 
a/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
 
b/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
index a3b3d0d9a..e2dbd08d5 100644
--- 
a/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
+++ 
b/gateway-spi-common/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
@@ -1355,6 +1355,16 @@ public class GatewayTestConfig extends Configuration 
implements GatewayConfig {
     return Collections.emptyList();
   }
 
+  @Override
+  public int getLDAPMaxSizeLimit() {
+    return 0;
+  }
+
+  @Override
+  public int getLDAPMaxTimeLimit() {
+    return 0;
+  }
+
   @Override
   public boolean getGroupUIServicesOnHomepage() {
     return false;
diff --git 
a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java 
b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
index 6b36e729b..4aec1fba4 100644
--- 
a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
+++ 
b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
@@ -155,6 +155,8 @@ public interface GatewayConfig {
   String LDAP_SSL_KEYSTORE_PATH = "gateway.ldap.ssl.keystore.path";
   String LDAP_SSL_KEYSTORE_PASSWORD_ALIAS = 
"gateway.ldap.ssl.keystore.password.alias";
   String LDAP_SSL_ENABLED_CIPHER_SUITES = 
"gateway.ldap.ssl.enabled.cipher.suites";
+  String LDAP_MAX_SIZE_LIMIT = "gateway.ldap.max.size.limit";
+  String LDAP_MAX_TIME_LIMIT = "gateway.ldap.max.time.limit";
 
   /**
    * The location of the gateway configuration.
@@ -1211,6 +1213,16 @@ public interface GatewayConfig {
    */
   List<String> getLDAPSSLEnabledCipherSuites();
 
+  /**
+   * @return the maximum size limit for LDAP search
+   */
+  int getLDAPMaxSizeLimit();
+
+  /**
+   * @return the maximum time limit for LDAP search in milliseconds
+   */
+  int getLDAPMaxTimeLimit();
+
   /**
    * @return set of all property names in the configuration
    */
diff --git a/knox-site/docs/service_ldap_server.md 
b/knox-site/docs/service_ldap_server.md
index 8d524395b..347c40aa8 100644
--- a/knox-site/docs/service_ldap_server.md
+++ b/knox-site/docs/service_ldap_server.md
@@ -44,6 +44,8 @@ The service is configured in `gateway-site.xml`.
 | `gateway.ldap.roles.lookup.strategy` | N/A | The LDAP roles lookup strategy 
(`file` or `rest`). |
 | `gateway.ldap.roles.lookup.rest.api.endpoint` | N/A | The LDAP roles lookup 
REST API endpoint. |
 | `gateway.ldap.roles.lookup.file.path` | N/A | The LDAP roles lookup file 
path. |
+| `gateway.ldap.max.size.limit` | 1000 | The maximum size limit of the result 
set returned by search requests. |
+| `gateway.ldap.max.time.limit` | 60000 | The maximum time limit for search 
requests in milliseconds. |
 
 ### Bind Credentials
 
@@ -198,6 +200,10 @@ The proxy backend delegates lookups to a remote LDAP or 
Active Directory server.
 | `gateway.ldap.interceptor.<name>.groupMemberAttribute` | `memberUid` | 
Attribute used for group membership (e.g., `member` for AD). |
 | `gateway.ldap.interceptor.<name>.useMemberOf` | `false` | If `true`, use the 
`memberOf` attribute for efficient group lookups. |
 | `gateway.ldap.interceptor.<name>.proxy.poolMaxActive` | `8` | Maximum number 
of active connections in the pool. |
+| `gateway.ldap.interceptor.<name>.pageSize` | `1000` | Page size for search 
requests. |
+| `gateway.ldap.interceptor.<name>.maxResultSetSize` | `0` | Maximum number of 
results to return from a search, regardless of paging. 0 means unlimited. |
+
+NOTE: If this value is undefined and the interceptor was created by the 
KnoxLDAPServerManager, the KnoxLDAPServerManager will set the 
`gateway.ldap.interceptor.<name>.maxResultSetSize` value to be 1 greater than 
the proxy's `gateway.ldap.max.size.limit` configuration. This will ensure that 
the proxy returns a "Size limit exceeded" result if the backend has more 
results than the proxy's limit.
 
 ## Active Directory (AD) Integration
 

Reply via email to