diff --git a/.github/workflows/build/gateway-site.xml b/.github/workflows/build/gateway-site.xml index 00698bebdb..002fadea5d 100644 --- a/.github/workflows/build/gateway-site.xml +++ b/.github/workflows/build/gateway-site.xml @@ -145,6 +145,14 @@ limitations under the License. gateway.ldap.base.dn dc=proxy,dc=org + + gateway.ldap.max.size.limit + 1000 + + + gateway.ldap.max.time.limit + 60000 + gateway.ldap.recursive.group.resolution true @@ -211,5 +219,10 @@ limitations under the License. gateway.ldap.interceptor.demoldap.groupMemberAttribute member + + + gateway.ldap.interceptor.demoldap.pageSize + 3 + 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 94d613ff94..791a3b2a5d 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. gateway.ldap.base.dn dc=proxy,dc=org + + gateway.ldap.max.size.limit + 1000 + + + gateway.ldap.max.time.limit + 60000 + gateway.ldap.recursive.group.resolution true @@ -203,5 +211,10 @@ limitations under the License. gateway.ldap.interceptor.demoldap.groupMemberAttribute member + + + gateway.ldap.interceptor.demoldap.pageSize + 3 + diff --git a/.github/workflows/compose/single-eku/gateway-site.xml b/.github/workflows/compose/single-eku/gateway-site.xml index 4272c7b739..76f488b85f 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. gateway.ldap.base.dn dc=proxy,dc=org + + gateway.ldap.max.size.limit + 1000 + + + gateway.ldap.max.time.limit + 60000 + gateway.ldap.recursive.group.resolution true @@ -257,5 +265,10 @@ limitations under the License. gateway.ldap.interceptor.demoldap.groupMemberAttribute member + + + gateway.ldap.interceptor.demoldap.pageSize + 3 + 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 061518537d..d0e34d0fc2 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 String getStrictTransportOption() { // 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 Map getLDAPInterceptorConfig(String interceptorName) { @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 String getLdapRolesLookupFilePath() { @Override public boolean isLDAPSSLEnabled() { - return Boolean.parseBoolean(get(LDAP_SSL_ENABLED, "false")); + return getBoolean(LDAP_SSL_ENABLED, false); } @Override @@ -1884,6 +1891,16 @@ public List getLDAPSSLEnabledCipherSuites() { 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 3e42c2117c..e4445276e1 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 interceptors; private boolean hasRolesLookupInterceptor; @@ -87,6 +88,8 @@ public class KnoxLDAPServerManager { private List sslEnabledCipherSuites; // Collection of DNs for the proxied backend LDAP servers private Set baseDns; + private int maxSizeLimit; + private int maxTimeLimit; KnoxLDAPServerManager(AliasService aliasService) { this(aliasService, null); @@ -114,6 +117,9 @@ public void initialize(GatewayConfig config) throws Exception { 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 @@ private void createInterceptors(GatewayConfig config) throws Exception { // 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 void start() throws Exception { 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 816fef0035..8683f46457 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 0c56168520..32fd033b24 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 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.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 LdapProxyBackend(String name, Map config) { 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 List getUserGroups(String username, SchemaManager schemaManager) return List.of(); } - LdapConnection connection = null; - try { - connection = getConnection(); - List groups = getUserGroupsEntries(connection, user, createEntryCache(), createResolvedParentsCache()); - List cns = getCnsFromEntries(groups); - return cns; - } finally { - releaseConnection(connection); + List 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 List searchUsers(String filter, SchemaManager schemaManager) throw 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 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 List search(String searchBase, SearchScope searchScope, String fil try { connection = getConnection(); List 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 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 @@ private List resolveGroupsRecursive(LdapConnection connection, List 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 @@ private List getUserGroupsInternal(LdapConnection connection, Dn... dns) 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 performPagedSearch(LdapConnection connection, String baseDn, String filter, SearchScope scope, String... attributes ) throws LdapException, CursorException, IOException { + List 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 @@ private String buildMultipleGroupMemberFilter(Dn... dns) { return filterBuilder.toString(); } - private List getCnsFromEntries(Collection entries) throws LdapException { - List 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 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 fda674c179..f549c413f7 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. Base DN for LDAP entries in the proxy server. Default is dc=proxy,dc=com. + + gateway.ldap.max.size.limit + 1000 + Maximum number of entries returned by a search request. + + + + gateway.ldap.max.time.limit + 60000 + Maximum time for a search request in milliseconds. + + gateway.ldap.recursive.group.resolution false 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 327d701c30..a5e7df664d 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 void testGetUserGroupsIgnoresBareRdnWhenRolesLookupInactive() throws Exce 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 ce681a5617..f32c036f84 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 @@ private void setupMockConfig(String backendType) throws Exception { 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 96b8e5a8f3..3ae14d812f 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 void testGetUserOverLdaps() throws Exception { 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 void testGetUserGroupsOverLdaps() throws Exception { List 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 58432f1374..7264527db8 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 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.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.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.atomic.AtomicInteger; public class LdapProxyBackendTest { + private static final int PAGE_SIZE = 2; + private static Map 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 static void setupBeforeClass() throws Exception { // 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 static void tearDownAfterClass() throws Exception { @After public void tearDown() throws Exception { + capturingSearchRequestHandler.reset(); if (ldapProxyBackend != null) { ldapProxyBackend.close(); } @@ -157,7 +175,8 @@ public void testGetUserByDefaultUserSearchFilter() throws Exception { validateUserEntry(entry, "ldaptest1", "TestCn1", "ldaptest1@example.com", "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 void testGetUserByUID() throws Exception { validateUserEntry(entry, "ldaptest1", "TestCn1", "ldaptest1@example.com", "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 void testGetUserByCN() throws Exception { validateUserEntry(entry, "ldaptest1", "TestCn1", "ldaptest1@example.com", "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 void testGetUserBySAMAccountName() throws Exception { 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 void testGetUserUseMemberOf() throws Exception { config.put("useMemberOf", "true"); ldapProxyBackend = new LdapProxyBackend("testbackend", config); - Entry entry = ldapProxyBackend.getUser("ldaptest2", schemaManager); - validateUserEntry(entry, "ldaptest2", "TestCn2", "ldaptest2@example.com", "Test user ldaptest2"); + Entry entry = ldapProxyBackend.getUser("ldapmemberof", schemaManager); + validateUserEntry(entry, "ldapmemberof", "TestMemberOf", "ldapmemberof@example.com", "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 void testGetUserGroups() throws Exception { List userGroups = ldapProxyBackend.getUserGroups("ldaptest1", schemaManager); assertTrue(userGroups.contains("group1")); assertTrue(userGroups.contains("group2")); + assertTrue(userGroups.contains("group3")); + } + + @Test + public void testGetUserGroupsPaging() throws Exception { + Map config = new HashMap<>(ldapBackendConfig); + config.put("pageSize", Integer.toString(PAGE_SIZE)); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); + + List 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 config = new HashMap<>(ldapBackendConfig); + config.put("pageSize", Integer.toString(PAGE_SIZE)); + config.put("maxResultSetSize", "1"); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); + + List 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 void testGetUserGroupsUseMemberOf() throws Exception { config.put("useMemberOf", "true"); ldapProxyBackend = new LdapProxyBackend("testbackend", config); - List userGroups = ldapProxyBackend.getUserGroups("ldaptest2", schemaManager); + List userGroups = ldapProxyBackend.getUserGroups("ldapmemberof", schemaManager); assertTrue(userGroups.contains("groupMemberOf1")); assertTrue(userGroups.contains("groupMemberOf2")); } @@ -323,13 +377,42 @@ public void testGetUserGroupsUseMemberOfRecursiveDepth2() throws Exception { @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 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 config = new HashMap<>(ldapBackendConfig); + config.put("pageSize", Integer.toString(PAGE_SIZE)); + config.put("maxResultSetSize", "1"); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); + + List 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 void testSearchUsersNoneFound() throws Exception { public void testSearchUsersByCn() throws Exception { Map 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 void testSearchUsersNoneFoundByCn() throws Exception { public void testSearchUsersBySAMAccountName() throws Exception { Map 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 void testSearchRecursiveWithSharedGroups() throws Exception { @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 void testSearchByUid() throws Exception { @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 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 config = new HashMap<>(ldapBackendConfig); + config.put("pageSize", Integer.toString(PAGE_SIZE)); + config.put("maxResultSetSize", "1"); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); + List 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 void testSearchByCn() throws Exception { @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 Set get(Object key) { // 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 { + private final LdapRequestHandler delegate; + private final List requests = Collections.synchronizedList(new ArrayList<>()); + + CapturingSearchRequestHandler(LdapRequestHandler delegate) { + this.delegate = delegate; + } + + public void reset() { + requests.clear(); + } + + public List 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 91fb4136c9..456105f986 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: ldaptest2@example.com 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: ldapmemberof@example.com +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 a3b3d0d9a8..e2dbd08d51 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 List getLDAPSSLEnabledCipherSuites() { 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 6b36e729bf..4aec1fba43 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 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 8d524395b6..347c40aa89 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..groupMemberAttribute` | `memberUid` | Attribute used for group membership (e.g., `member` for AD). | | `gateway.ldap.interceptor..useMemberOf` | `false` | If `true`, use the `memberOf` attribute for efficient group lookups. | | `gateway.ldap.interceptor..proxy.poolMaxActive` | `8` | Maximum number of active connections in the pool. | +| `gateway.ldap.interceptor..pageSize` | `1000` | Page size for search requests. | +| `gateway.ldap.interceptor..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..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