From 7dfce9f6b0617fea6f06a176c71b555e330adcd4 Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Wed, 20 May 2026 14:40:48 +0200 Subject: [PATCH 1/5] KNOX-3328: Implement recursive group resolution for LDAP proxy --- .../config/impl/GatewayConfigImpl.java | 10 + .../services/ldap/KnoxLDAPService.java | 2 + .../ldap/backend/LdapProxyBackend.java | 222 +++++++++++------- .../ldap/backend/LdapProxyBackendTest.java | 79 ++++++- .../resources/ldap-proxy-backend-test.ldif | 87 ++++++- .../knox/gateway/GatewayTestConfig.java | 10 + .../knox/gateway/config/GatewayConfig.java | 12 + 7 files changed, 313 insertions(+), 109 deletions(-) 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 79c67f5a2f..abf738a6c4 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 @@ -1764,6 +1764,16 @@ public String getLDAPBackendDataFile() { return getGatewayDataDir() + File.separator + "ldap-users.json"; } + @Override + public boolean isLDAPRecursiveGroupResolutionEnabled() { + return Boolean.parseBoolean(get(LDAP_RECURSIVE_GROUP_RESOLUTION, "false")); + } + + @Override + public int getLDAPGroupMaxDepth() { + return Integer.parseInt(get(LDAP_GROUP_MAX_DEPTH, "10")); + } + @Override public Set getPropertyNames() { Set names = new HashSet<>(); diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPService.java index 1ec748da9d..d3c65830a6 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPService.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPService.java @@ -61,6 +61,8 @@ public void init(GatewayConfig config, Map options) throws Servi // Add common configuration backendConfig.put("baseDn", baseDn); + backendConfig.put("recursiveGroupResolution", String.valueOf(config.isLDAPRecursiveGroupResolutionEnabled())); + backendConfig.put("groupMaxDepth", String.valueOf(config.getLDAPGroupMaxDepth())); // Add legacy dataFile property for backwards compatibility with file backend if ("file".equalsIgnoreCase(backendType) && !backendConfig.containsKey("dataFile")) { 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 38328b5713..e9451dab50 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 @@ -35,11 +35,11 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; /** * LDAP backend that proxies to an external LDAP server. @@ -63,6 +63,8 @@ public class LdapProxyBackend implements LdapBackend { private String userSearchFilter = "({userIdAttr}={username})"; // Will be populated with userIdentifierAttribute private String groupMemberAttribute = "memberUid"; // member for AD, memberUid for POSIX private boolean useMemberOf; // Use memberOf attribute for group lookup (efficient for AD) + private boolean recursiveGroupResolution; + private int groupMaxDepth; private List proxyEntryAttributeTypes = List.of( // "uid" will always be filled @@ -131,13 +133,16 @@ public void initialize(Map config) throws Exception { userIdentifierAttribute = config.getOrDefault("userIdentifierAttribute", "uid"); groupMemberAttribute = config.getOrDefault("groupMemberAttribute", "memberUid"); useMemberOf = Boolean.parseBoolean(config.getOrDefault("useMemberOf", "false")); + recursiveGroupResolution = Boolean.parseBoolean(config.getOrDefault("recursiveGroupResolution", "false")); + groupMaxDepth = Integer.parseInt(config.getOrDefault("groupMaxDepth", "10")); // Build search filter template userSearchFilter = "(" + userIdentifierAttribute + "={username})"; LOG.ldapBackendLoading(getName(), "Proxying " + proxyBaseDn + " to " + ldapUrl + " (" + remoteBaseDn + ") with " + userIdentifierAttribute + " attribute" + - (useMemberOf ? " using memberOf lookups" : " using group searches")); + (useMemberOf ? (recursiveGroupResolution ? " using recursive memberOf lookups" : " using memberOf lookups") : + (recursiveGroupResolution ? " using recursive group searches" : " using group searches"))); // Initialize connection pool initializeConnectionPool(config); @@ -258,9 +263,9 @@ public Entry getUser(String username, SchemaManager schemaManager) throws Except LdapConnection connection = null; try { connection = getConnection(); - // Search for user using configurable attribute + // 1. Try search in user base String filter = userSearchFilter.replace("{username}", username); - EntryCursor cursor = connection.search(userSearchBase, filter, SearchScope.SUBTREE, "*"); + EntryCursor cursor = connection.search(userSearchBase, filter, SearchScope.SUBTREE, "*", "+"); if (cursor.next()) { Entry sourceEntry = cursor.get(); @@ -269,6 +274,18 @@ public Entry getUser(String username, SchemaManager schemaManager) throws Except return entry; } cursor.close(); + + // 2. Try search in group base if not found in user base + String groupFilter = "(cn=" + username + ")"; + cursor = connection.search(groupSearchBase, groupFilter, SearchScope.SUBTREE, "*", "+"); + if (cursor.next()) { + Entry sourceEntry = cursor.get(); + Entry entry = createProxyEntry(sourceEntry, username, connection, schemaManager); + cursor.close(); + return entry; + } + cursor.close(); + return null; } finally { releaseConnection(connection); @@ -280,127 +297,161 @@ public List getUserGroups(String username) throws Exception { LdapConnection connection = null; try { connection = getConnection(); + Set allGroupDns = new HashSet<>(); if (useMemberOf) { - // Use memberOf attribute for efficient AD lookups - return getUserGroupsViaMemberOf(connection, username); + allGroupDns.addAll(getUserGroupDnsViaMemberOf(connection, username)); + if (recursiveGroupResolution && !allGroupDns.isEmpty()) { + resolveRecursiveGroupDnsViaMemberOf(connection, allGroupDns, 0); + } } else { - // Use traditional group search approach String filter = userSearchFilter.replace("{username}", username); EntryCursor cursor = connection.search(userSearchBase, filter, SearchScope.SUBTREE, "dn"); - if (cursor.next()) { String userDn = cursor.get().getDn().toString(); cursor.close(); - return getCnsFromEntries(getUserGroupsInternal(connection, userDn, username)); + Set visited = new HashSet<>(); + visited.add(userDn); + List initialGroups = getUserGroupsInternal(connection, userDn, username); + for (Entry group : initialGroups) { + allGroupDns.add(group.getDn().toString()); + } + if (recursiveGroupResolution && !allGroupDns.isEmpty()) { + resolveRecursiveGroupDnsInternal(connection, allGroupDns, visited, 0); + } + } else { + cursor.close(); } + } - cursor.close(); + List groupNames = new ArrayList<>(); + for (String dn : allGroupDns) { + String name = extractGroupNameFromDn(dn); + if (name != null) { + groupNames.add(name); + } } - return Collections.emptyList(); + return groupNames; } finally { releaseConnection(connection); } } - private List getUserGroupsViaMemberOf(LdapConnection connection, String username) throws LdapException, CursorException, IOException { - List groups = new ArrayList<>(); - - // Search for user and retrieve memberOf attribute + private List getUserGroupDnsViaMemberOf(LdapConnection connection, String username) throws LdapException, CursorException, IOException { + List dns = new ArrayList<>(); String filter = userSearchFilter.replace("{username}", username); - EntryCursor cursor = connection.search(userSearchBase, filter, SearchScope.SUBTREE, "memberOf"); - + EntryCursor cursor = connection.search(userSearchBase, filter, SearchScope.SUBTREE, "memberOf", "+"); if (cursor.next()) { Entry userEntry = cursor.get(); Attribute memberOfAttr = userEntry.get("memberOf"); - if (memberOfAttr != null) { - // Extract group names from DNs for (org.apache.directory.api.ldap.model.entry.Value value : memberOfAttr) { - String groupDn = value.getString(); - String groupName = extractGroupNameFromDn(groupDn); - if (groupName != null) { - groups.add(groupName); + dns.add(value.getString()); + } + } + } + cursor.close(); + return dns; + } + + private void resolveRecursiveGroupDnsViaMemberOf(LdapConnection connection, Set allGroupDns, int depth) throws LdapException, CursorException, IOException { + if (depth >= groupMaxDepth) { + return; + } + + Set nextLevelDns = new HashSet<>(); + for (String dn : allGroupDns) { + EntryCursor cursor = connection.search(dn, "(objectClass=*)", SearchScope.OBJECT, "memberOf", "+"); + if (cursor.next()) { + Attribute memberOfAttr = cursor.get().get("memberOf"); + if (memberOfAttr != null) { + for (org.apache.directory.api.ldap.model.entry.Value value : memberOfAttr) { + String parentDn = value.getString(); + if (!allGroupDns.contains(parentDn)) { + nextLevelDns.add(parentDn); + } } } } + cursor.close(); } - cursor.close(); - return groups; + if (!nextLevelDns.isEmpty()) { + allGroupDns.addAll(nextLevelDns); + resolveRecursiveGroupDnsViaMemberOf(connection, allGroupDns, depth + 1); + } + } + + private void resolveRecursiveGroupDnsInternal(LdapConnection connection, Set allGroupDns, Set visited, int depth) throws LdapException, CursorException, IOException { + if (depth >= groupMaxDepth) { + return; + } + + Set nextLevelDns = new HashSet<>(); + for (String dn : new HashSet<>(allGroupDns)) { + if (visited.contains(dn)) { + continue; + } + visited.add(dn); + + List parents = getUserGroupsInternal(connection, dn, null); + for (Entry parent : parents) { + String parentDn = parent.getDn().toString(); + if (!allGroupDns.contains(parentDn)) { + nextLevelDns.add(parentDn); + } + } + } + + if (!nextLevelDns.isEmpty()) { + allGroupDns.addAll(nextLevelDns); + resolveRecursiveGroupDnsInternal(connection, allGroupDns, visited, depth + 1); + } } private String extractGroupNameFromDn(String groupDn) { - // Extract CN from DN like "CN=Domain Admins,CN=Users,DC=company,DC=com" if (groupDn.toLowerCase(Locale.ROOT).startsWith("cn=")) { int commaIdx = groupDn.indexOf(','); if (commaIdx > 0) { return groupDn.substring(3, commaIdx); } + return groupDn.substring(3); } return null; } private List getUserGroupsInternal(LdapConnection connection, String userDn, String username) throws LdapException, CursorException, IOException { List groups = new ArrayList<>(); - - // Search for groups where user is a member - build filter based on configuration String filter; if ("member".equals(groupMemberAttribute)) { - // AD style - uses full DN - filter = "(|" + - "(member=" + userDn + ")" + - "(uniqueMember=" + userDn + ")" + - ")"; + filter = "(|(member=" + userDn + ")(uniqueMember=" + userDn + "))"; } else { - // POSIX style - uses username - filter = "(|" + - "(memberUid=" + username + ")" + - "(member=" + userDn + ")" + - "(uniqueMember=" + userDn + ")" + - ")"; + filter = "(|(memberUid=" + (username != null ? username : "") + ")(member=" + userDn + ")(uniqueMember=" + userDn + "))"; } EntryCursor cursor = connection.search(groupSearchBase, filter, SearchScope.SUBTREE, "cn"); - while (cursor.next()) { groups.add(cursor.get()); } - cursor.close(); return groups; } - 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()); - } - } - return cns; - } - @Override public List searchUsers(String filter, SchemaManager schemaManager) throws Exception { List results = new ArrayList<>(); LdapConnection connection = null; - try { connection = getConnection(); String ldapFilter = "(" + userIdentifierAttribute + "=" + filter.trim() + ")"; - EntryCursor cursor = connection.search(userSearchBase, ldapFilter, SearchScope.SUBTREE, "*"); - + EntryCursor cursor = connection.search(userSearchBase, ldapFilter, SearchScope.SUBTREE, "*", "+"); while (cursor.next()) { Entry sourceEntry = cursor.get(); Attribute idAttr = sourceEntry.get(userIdentifierAttribute); if (idAttr != null) { - String username = idAttr.getString(); - Entry entry = createProxyEntry(sourceEntry, username, connection, schemaManager); + Entry entry = createProxyEntry(sourceEntry, idAttr.getString(), connection, schemaManager); results.add(entry); } } - cursor.close(); return results; } finally { @@ -408,63 +459,56 @@ public List searchUsers(String filter, SchemaManager schemaManager) throw } } - /** - * Creates a proxy entry from a backend source entry with all required attributes. - * This method standardizes the conversion of backend LDAP entries to proxy entries, - * preserving the backend DN and copying all standard user attributes. - * - * @param sourceEntry The entry from the backend LDAP server - * @param username The username for the entry - * @param connection The LDAP connection for fetching group information - * @param schemaManager The schema manager for creating entries - * @return A new Entry with backend DN and all copied attributes - * @throws Exception if entry creation or attribute copying fails - */ private Entry createProxyEntry(Entry sourceEntry, String username, LdapConnection connection, SchemaManager schemaManager) throws Exception { - // Standard proxy approach: return entry with backend DN unchanged - // This preserves DN integrity for bind operations and DN references Entry entry = new DefaultEntry(schemaManager); entry.setDn(sourceEntry.getDn()); - - // Copy all attributes as-is from backend copyAttribute(sourceEntry, entry, "objectClass"); copyAttribute(sourceEntry, entry, userIdentifierAttribute); - - // Map identifier attribute to uid for consistency if needed if (!"uid".equals(userIdentifierAttribute)) { Attribute idAttr = sourceEntry.get(userIdentifierAttribute); if (idAttr != null) { entry.add("uid", idAttr.getString()); } } - for (String attributeType : proxyEntryAttributeTypes) { copyAttribute(sourceEntry, entry, attributeType); } + Set allGroupDns = new HashSet<>(); if (useMemberOf) { - copyAttribute(sourceEntry, entry, proxyEntryGroupMembershipAttributeType); + Attribute memberOfAttr = sourceEntry.get("memberOf"); + if (memberOfAttr != null) { + for (org.apache.directory.api.ldap.model.entry.Value value : memberOfAttr) { + allGroupDns.add(value.getString()); + } + } + if (recursiveGroupResolution && !allGroupDns.isEmpty()) { + resolveRecursiveGroupDnsViaMemberOf(connection, allGroupDns, 0); + } } else { - List groups = getUserGroupsInternal(connection, sourceEntry.getDn().toString(), username); - for (Entry groupEntry : groups) { - entry.add(proxyEntryGroupMembershipAttributeType, groupEntry.getDn().getName()); + List initialGroups = getUserGroupsInternal(connection, sourceEntry.getDn().toString(), username); + for (Entry group : initialGroups) { + allGroupDns.add(group.getDn().toString()); + } + if (recursiveGroupResolution && !allGroupDns.isEmpty()) { + Set visited = new HashSet<>(); + visited.add(sourceEntry.getDn().toString()); + resolveRecursiveGroupDnsInternal(connection, allGroupDns, visited, 0); } } + for (String dn : allGroupDns) { + entry.add(proxyEntryGroupMembershipAttributeType, dn); + } + return entry; } private void copyAttribute(Entry source, Entry target, String attributeName) throws LdapException { Attribute attr = source.get(attributeName); if (attr != null) { - // Copy all values of the attribute (important for multi-valued attributes like objectClass) for (org.apache.directory.api.ldap.model.entry.Value value : attr) { - try { - target.add(attributeName, value.getString()); - } catch (LdapException e) { - LOG.ldapAttributeCopyError(e); - throw e; - } + target.add(attributeName, value.getString()); } } } 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 a6b132b356..86faf21a61 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 @@ -155,10 +155,12 @@ public void testGetUserByDefaultUserSearchFilter() throws Exception { assertEquals("ldaptest1@example.com", entry.get("mail").getString()); assertEquals("Test user ldaptest1", entry.get("description").getString()); assertNull(entry.get("sAMAccountName")); - assertEquals(2, entry.get("memberOf").size()); + assertEquals(4, entry.get("memberOf").size()); Set expectedMemberOf = 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=recursive-leaf,ou=groups,dc=hadoop,dc=apache,dc=org", + "cn=circular-a,ou=groups,dc=hadoop,dc=apache,dc=org"); Set foundMemberOf = new HashSet<>(2); Iterator memberOfs = entry.get("memberOf").iterator(); while (memberOfs.hasNext()) { @@ -187,10 +189,12 @@ public void testGetUserByUID() throws Exception { assertEquals("ldaptest1@example.com", entry.get("mail").getString()); assertEquals("Test user ldaptest1", entry.get("description").getString()); assertNull(entry.get("sAMAccountName")); - assertEquals(2, entry.get("memberOf").size()); + assertEquals(4, entry.get("memberOf").size()); Set expectedMemberOf = 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=recursive-leaf,ou=groups,dc=hadoop,dc=apache,dc=org", + "cn=circular-a,ou=groups,dc=hadoop,dc=apache,dc=org"); Set foundMemberOf = new HashSet<>(2); Iterator memberOfs = entry.get("memberOf").iterator(); while (memberOfs.hasNext()) { @@ -211,10 +215,12 @@ public void testGetUserByCN() throws Exception { assertEquals("ldaptest1@example.com", entry.get("mail").getString()); assertEquals("Test user ldaptest1", entry.get("description").getString()); assertNull(entry.get("sAMAccountName")); - assertEquals(2, entry.get("memberOf").size()); + assertEquals(4, entry.get("memberOf").size()); Set expectedMemberOf = 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=recursive-leaf,ou=groups,dc=hadoop,dc=apache,dc=org", + "cn=circular-a,ou=groups,dc=hadoop,dc=apache,dc=org"); Set foundMemberOf = new HashSet<>(2); Iterator memberOfs = entry.get("memberOf").iterator(); while (memberOfs.hasNext()) { @@ -235,10 +241,12 @@ public void testGetUserBySAMAccountName() throws Exception { assertEquals("ldaptest1@example.com", entry.get("mail").getString()); assertEquals("Test user ldaptest1", entry.get("description").getString()); assertEquals("TestSam1", entry.get("sAMAccountName").getString()); - assertEquals(2, entry.get("memberOf").size()); + assertEquals(4, entry.get("memberOf").size()); Set expectedMemberOf = 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=recursive-leaf,ou=groups,dc=hadoop,dc=apache,dc=org", + "cn=circular-a,ou=groups,dc=hadoop,dc=apache,dc=org"); Set foundMemberOf = new HashSet<>(2); Iterator memberOfs = entry.get("memberOf").iterator(); while (memberOfs.hasNext()) { @@ -452,4 +460,57 @@ public void testSearchUsersNoneFoundBySAMAccountName() throws Exception { List entries = ldapProxyBackend.searchUsers("nobody*", schemaManager); assertTrue(entries.isEmpty()); } -} \ No newline at end of file + + @Test + public void testGetUserGroupsRecursive() throws Exception { + Map config = new HashMap<>(ldapBackendConfig); + config.put("recursiveGroupResolution", "true"); + ldapProxyBackend.initialize(config); + + List userGroups = ldapProxyBackend.getUserGroups("ldaptest1"); + assertTrue(userGroups.contains("group1")); + assertTrue(userGroups.contains("group2")); + assertTrue(userGroups.contains("recursive-leaf")); + assertTrue(userGroups.contains("recursive-mid")); + assertTrue(userGroups.contains("recursive-top")); + } + + @Test + public void testGetUserGroupsRecursiveCircular() throws Exception { + Map config = new HashMap<>(ldapBackendConfig); + config.put("recursiveGroupResolution", "true"); + ldapProxyBackend.initialize(config); + + List userGroups = ldapProxyBackend.getUserGroups("ldaptest1"); + assertTrue(userGroups.contains("circular-a")); + assertTrue(userGroups.contains("circular-b")); + } + + @Test + public void testGetUserGroupsRecursiveMaxDepth() throws Exception { + Map config = new HashMap<>(ldapBackendConfig); + config.put("recursiveGroupResolution", "true"); + config.put("groupMaxDepth", "1"); // Only leaf level + ldapProxyBackend.initialize(config); + + List userGroups = ldapProxyBackend.getUserGroups("ldaptest1"); + assertTrue(userGroups.contains("recursive-leaf")); + assertTrue(userGroups.contains("recursive-mid")); + // recursive-top is depth 2 from leaf (which is depth 0 relative to user groups search) + assertTrue(!userGroups.contains("recursive-top")); + } + + @Test + public void testGetUserGroupsRecursiveUseMemberOf() throws Exception { + Map config = new HashMap<>(ldapBackendConfig); + config.put("useMemberOf", "true"); + config.put("recursiveGroupResolution", "true"); + ldapProxyBackend.initialize(config); + + List userGroups = ldapProxyBackend.getUserGroups("ldaptest2"); + assertTrue(userGroups.contains("groupMemberOf1")); + assertTrue(userGroups.contains("groupMemberOf2")); + assertTrue(userGroups.contains("recursive-mid-mo")); + assertTrue(userGroups.contains("recursive-top-mo")); + } + } \ No newline at end of file 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 1863427806..c37033a192 100644 --- a/gateway-server/src/test/resources/ldap-proxy-backend-test.ldif +++ b/gateway-server/src/test/resources/ldap-proxy-backend-test.ldif @@ -35,17 +35,6 @@ objectClass: top objectClass: organizationalUnit ou: groups -# entry for the end user -dn: uid=guest,ou=people,dc=hadoop,dc=apache,dc=org -objectclass:top -objectclass:person -objectclass:organizationalPerson -objectclass:inetOrgPerson -cn: Guest -sn: User -uid: guest -userPassword:guest-password - dn: cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org objectclass:top objectclass:groupOfNames @@ -58,6 +47,82 @@ objectclass:groupOfNames cn: group2 member: uid=ldaptest1,ou=people,dc=hadoop,dc=apache,dc=org +dn: cn=recursive-leaf,ou=groups,dc=hadoop,dc=apache,dc=org +objectclass:top +objectclass:groupOfNames +cn: recursive-leaf +member: uid=ldaptest1,ou=people,dc=hadoop,dc=apache,dc=org + +dn: cn=recursive-mid,ou=groups,dc=hadoop,dc=apache,dc=org +objectclass:top +objectclass:groupOfNames +cn: recursive-mid +member: cn=recursive-leaf,ou=groups,dc=hadoop,dc=apache,dc=org + +dn: cn=recursive-top,ou=groups,dc=hadoop,dc=apache,dc=org +objectclass:top +objectclass:groupOfNames +cn: recursive-top +member: cn=recursive-mid,ou=groups,dc=hadoop,dc=apache,dc=org + +dn: cn=circular-a,ou=groups,dc=hadoop,dc=apache,dc=org +objectclass:top +objectclass:groupOfNames +cn: circular-a +member: uid=ldaptest1,ou=people,dc=hadoop,dc=apache,dc=org +member: cn=circular-b,ou=groups,dc=hadoop,dc=apache,dc=org + +dn: cn=circular-b,ou=groups,dc=hadoop,dc=apache,dc=org +objectclass:top +objectclass:groupOfNames +cn: circular-b +member: cn=circular-a,ou=groups,dc=hadoop,dc=apache,dc=org + +dn: cn=groupMemberOf1,ou=groups,dc=hadoop,dc=apache,dc=org +objectclass:top +objectclass:person +objectclass:organizationalPerson +objectclass:inetOrgPerson +cn: groupMemberOf1 +sn: groupMemberOf1 +memberOf: cn=recursive-mid-mo,ou=groups,dc=hadoop,dc=apache,dc=org + +dn: cn=groupMemberOf2,ou=groups,dc=hadoop,dc=apache,dc=org +objectclass:top +objectclass:person +objectclass:organizationalPerson +objectclass:inetOrgPerson +cn: groupMemberOf2 +sn: groupMemberOf2 + +dn: cn=recursive-mid-mo,ou=groups,dc=hadoop,dc=apache,dc=org +objectclass:top +objectclass:person +objectclass:organizationalPerson +objectclass:inetOrgPerson +cn: recursive-mid-mo +sn: recursive-mid-mo +memberOf: cn=recursive-top-mo,ou=groups,dc=hadoop,dc=apache,dc=org + +dn: cn=recursive-top-mo,ou=groups,dc=hadoop,dc=apache,dc=org +objectclass:top +objectclass:person +objectclass:organizationalPerson +objectclass:inetOrgPerson +cn: recursive-top-mo +sn: recursive-top-mo + +# entry for the end users +dn: uid=guest,ou=people,dc=hadoop,dc=apache,dc=org +objectclass:top +objectclass:person +objectclass:organizationalPerson +objectclass:inetOrgPerson +cn: Guest +sn: User +uid: guest +userPassword:guest-password + dn: uid=ldaptest1,ou=people,dc=hadoop,dc=apache,dc=org objectclass:top objectclass:person 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 8a55d37368..5d1f9f200b 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 @@ -1244,6 +1244,16 @@ public String getLDAPBackendDataFile() { return getGatewayDataPath().resolve("ldap-users.json").toString(); } + @Override + public boolean isLDAPRecursiveGroupResolutionEnabled() { + return false; + } + + @Override + public int getLDAPGroupMaxDepth() { + return 10; + } + @Override public Set getPropertyNames() { return Collections.emptySet(); 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 a3c62e011b..863ade0aed 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 @@ -130,6 +130,8 @@ public interface GatewayConfig { String LDAP_BASE_DN = "gateway.ldap.base.dn"; String LDAP_BACKEND_TYPE = "gateway.ldap.backend.type"; String LDAP_BACKEND_DATA_FILE = "gateway.ldap.backend.data.file"; + String LDAP_RECURSIVE_GROUP_RESOLUTION = "gateway.ldap.recursive.group.resolution"; + String LDAP_GROUP_MAX_DEPTH = "gateway.ldap.group.max.depth"; /** * The location of the gateway configuration. @@ -1075,6 +1077,16 @@ public interface GatewayConfig { */ String getLDAPBackendDataFile(); + /** + * @return true if recursive group resolution is enabled for LDAP; otherwise false + */ + boolean isLDAPRecursiveGroupResolutionEnabled(); + + /** + * @return the maximum depth for recursive group resolution + */ + int getLDAPGroupMaxDepth(); + /** * Get backend-specific configuration properties. * Returns all properties with prefix "gateway.ldap.backend.{backendType}." From e1fba842da22ae601a4931bdb85019db577594f6 Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Wed, 20 May 2026 16:13:36 +0200 Subject: [PATCH 2/5] KNOX-3328: Update KnoxLDAPServiceTest to account for new recursive group resolution config --- .../knox/gateway/services/ldap/KnoxLDAPServiceTest.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 dbdb081dac..942a51faac 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 @@ -126,6 +126,8 @@ public void testInitWithInvalidBackendType() throws Exception { expect(mockConfig.getLDAPPort()).andReturn(3890); expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com"); expect(mockConfig.getLDAPBackendType()).andReturn("invalid"); + expect(mockConfig.isLDAPRecursiveGroupResolutionEnabled()).andReturn(false).anyTimes(); + expect(mockConfig.getLDAPGroupMaxDepth()).andReturn(10).anyTimes(); expect(mockConfig.getLDAPBackendConfig("invalid")).andReturn(new HashMap<>()); replay(mockConfig); @@ -171,6 +173,8 @@ private void setupMockConfigForFileBackend() { expect(mockConfig.getLDAPPort()).andReturn(3890); expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com"); expect(mockConfig.getLDAPBackendType()).andReturn("file"); + expect(mockConfig.isLDAPRecursiveGroupResolutionEnabled()).andReturn(false).anyTimes(); + expect(mockConfig.getLDAPGroupMaxDepth()).andReturn(10).anyTimes(); Map fileBackendConfig = new HashMap<>(); fileBackendConfig.put("dataFile", tempLdapFile.getAbsolutePath()); @@ -183,6 +187,8 @@ private void setupMockConfigForLdapBackend() { expect(mockConfig.getLDAPPort()).andReturn(3890); expect(mockConfig.getLDAPBaseDN()).andReturn("dc=proxy,dc=com"); expect(mockConfig.getLDAPBackendType()).andReturn("ldap"); + expect(mockConfig.isLDAPRecursiveGroupResolutionEnabled()).andReturn(false).anyTimes(); + expect(mockConfig.getLDAPGroupMaxDepth()).andReturn(10).anyTimes(); Map ldapBackendConfig = new HashMap<>(); ldapBackendConfig.put("url", "ldap://localhost:33389"); From db40ef93aa53bfa194ef3a5fbe3637fffadd21c9 Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Wed, 20 May 2026 22:46:34 +0200 Subject: [PATCH 3/5] KNOX-3328: Add integration test coverage for recursive LDAP group resolution --- .github/workflows/build/Dockerfile.local | 1 + .../build/conf/topologies/knoxldap.xml | 2 +- .github/workflows/build/conf/users.ldif | 86 +++++++++++++++++++ .github/workflows/build/gateway-site.xml | 56 ++++++++++++ .../tests/test_knox_auth_service_and_LDAP.py | 11 +++ 5 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build/conf/users.ldif diff --git a/.github/workflows/build/Dockerfile.local b/.github/workflows/build/Dockerfile.local index 73e1c9bfe8..f53295de21 100644 --- a/.github/workflows/build/Dockerfile.local +++ b/.github/workflows/build/Dockerfile.local @@ -60,6 +60,7 @@ ADD conf/topologies/knoxtoken.xml /knox-runtime/conf/topologies/knoxtoken.xml ADD conf/topologies/health.xml /knox-runtime/conf/topologies/health.xml ADD conf/topologies/knoxldap.xml /knox-runtime/conf/topologies/knoxldap.xml ADD conf/topologies/remoteauth.xml /knox-runtime/conf/topologies/remoteauth.xml +ADD conf/users.ldif /knox-runtime/conf/users.ldif RUN chown -R gateway /knox-runtime/ diff --git a/.github/workflows/build/conf/topologies/knoxldap.xml b/.github/workflows/build/conf/topologies/knoxldap.xml index 102797634f..3ff631098d 100644 --- a/.github/workflows/build/conf/topologies/knoxldap.xml +++ b/.github/workflows/build/conf/topologies/knoxldap.xml @@ -35,7 +35,7 @@ limitations under the License. main.ldapRealm.contextFactory.url - ldap://ldap:33389 + ldap://localhost:33389 main.ldapRealm.contextFactory.authenticationMechanism diff --git a/.github/workflows/build/conf/users.ldif b/.github/workflows/build/conf/users.ldif new file mode 100644 index 0000000000..8e19cc3625 --- /dev/null +++ b/.github/workflows/build/conf/users.ldif @@ -0,0 +1,86 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +version: 1 + +dn: dc=hadoop,dc=apache,dc=org +objectclass: organization +objectclass: dcObject +o: Hadoop at Apache.org +dc: hadoop +description: Makers of Hadoop + +# entry for the people container +dn: ou=people,dc=hadoop,dc=apache,dc=org +objectclass:top +objectclass:organizationalUnit +ou: people + +# entry for group container +dn: ou=groups,dc=hadoop,dc=apache,dc=org +objectClass: top +objectClass: organizationalUnit +ou: groups + +dn: cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org +objectclass:top +objectclass:groupOfNames +cn: group1 +member: uid=guest,ou=people,dc=hadoop,dc=apache,dc=org + +dn: cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org +objectclass:top +objectclass:groupOfNames +cn: group2 +member: uid=guest,ou=people,dc=hadoop,dc=apache,dc=org + +dn: cn=recursive-leaf,ou=groups,dc=hadoop,dc=apache,dc=org +objectclass:top +objectclass:groupOfNames +cn: recursive-leaf +member: uid=guest,ou=people,dc=hadoop,dc=apache,dc=org + +dn: cn=recursive-mid,ou=groups,dc=hadoop,dc=apache,dc=org +objectclass:top +objectclass:groupOfNames +cn: recursive-mid +member: cn=recursive-leaf,ou=groups,dc=hadoop,dc=apache,dc=org + +dn: cn=recursive-top,ou=groups,dc=hadoop,dc=apache,dc=org +objectclass:top +objectclass:groupOfNames +cn: recursive-top +member: cn=recursive-mid,ou=groups,dc=hadoop,dc=apache,dc=org + +dn: uid=guest,ou=people,dc=hadoop,dc=apache,dc=org +objectclass:top +objectclass:person +objectclass:organizationalPerson +objectclass:inetOrgPerson +cn: Guest +sn: User +uid: guest +userPassword:guest-password + +dn: uid=admin,ou=people,dc=hadoop,dc=apache,dc=org +objectclass:top +objectclass:person +objectclass:organizationalPerson +objectclass:inetOrgPerson +cn: Admin +sn: User +uid: admin +userPassword:admin-password diff --git a/.github/workflows/build/gateway-site.xml b/.github/workflows/build/gateway-site.xml index 5f333f0639..7a865f8b84 100644 --- a/.github/workflows/build/gateway-site.xml +++ b/.github/workflows/build/gateway-site.xml @@ -160,4 +160,60 @@ limitations under the License. max-age=300; includeSubDomains + + + gateway.ldap.enabled + true + + + gateway.ldap.port + 33389 + + + gateway.ldap.base.dn + dc=hadoop,dc=apache,dc=org + + + gateway.ldap.backend.type + ldap + + + gateway.ldap.recursive.group.resolution + true + + + gateway.ldap.group.max.depth + 10 + + + + + gateway.ldap.backend.ldap.url + ldap://ldap:33389 + + + gateway.ldap.backend.ldap.remoteBaseDn + dc=hadoop,dc=apache,dc=org + + + gateway.ldap.backend.ldap.systemUsername + uid=guest,ou=people,dc=hadoop,dc=apache,dc=org + + + gateway.ldap.backend.ldap.systemPassword + guest-password + + + gateway.ldap.backend.ldap.userSearchBase + ou=people,dc=hadoop,dc=apache,dc=org + + + gateway.ldap.backend.ldap.groupSearchBase + ou=groups,dc=hadoop,dc=apache,dc=org + + + gateway.ldap.backend.ldap.groupMemberAttribute + member + + diff --git a/.github/workflows/tests/test_knox_auth_service_and_LDAP.py b/.github/workflows/tests/test_knox_auth_service_and_LDAP.py index 51a42d84e4..cba88c2532 100644 --- a/.github/workflows/tests/test_knox_auth_service_and_LDAP.py +++ b/.github/workflows/tests/test_knox_auth_service_and_LDAP.py @@ -53,6 +53,17 @@ def test_auth_service_guest(self): self.assertEqual(response.headers[actor_id_header], 'guest') print(f"Verified {actor_id_header}: {response.headers[actor_id_header]}") + # Check for groups (recursive) + prefix = 'x-knox-actor-groups' + all_groups = collect_actor_group_values(response, prefix=prefix) + self.assertTrue(len(all_groups) > 0, f"No headers found starting with {prefix}") + + expected_groups = ['group1', 'group2', 'recursive-leaf', 'recursive-mid', 'recursive-top'] + print(f"Found groups: {all_groups}") + for group in expected_groups: + self.assertIn(group, all_groups) + print(f"Verified all expected recursive groups for guest") + def test_auth_service_admin_groups(self): """ Verify that admin user gets actor ID and group headers. From 512998868e267b3b4b5b4ee15ef3ca717a420334 Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Thu, 21 May 2026 13:37:48 +0200 Subject: [PATCH 4/5] KNOX-3328: Addressed review comments and did some cleanup --- .../services/ldap/KnoxLDAPServerManager.java | 4 + .../services/ldap/backend/FileBackend.java | 5 + .../services/ldap/backend/LdapBackend.java | 5 + .../ldap/backend/LdapProxyBackend.java | 183 +++++++++--------- 4 files changed, 106 insertions(+), 91 deletions(-) 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 f0a87dc5b8..8d34137c45 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 @@ -153,6 +153,10 @@ public void start() throws Exception { public void stop() throws Exception { LOG.ldapServiceStopping(port); + if (backend != null) { + backend.close(); + } + if (ldapServer != null) { try { ldapServer.stop(); diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackend.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackend.java index b0cdcd8607..03cf59f53b 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackend.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackend.java @@ -141,4 +141,9 @@ public List searchUsers(String filter, SchemaManager schemaManager) throw return results; } + + @Override + public void close() { + //NOP + } } diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackend.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackend.java index 6530c13b37..ae70e3457c 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackend.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackend.java @@ -65,4 +65,9 @@ public interface LdapBackend { * @return List of matching entries */ List searchUsers(String filter, SchemaManager schemaManager) throws Exception; + + /** + * Closing underlying resources on the backend, if any + */ + void close(); } 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 e9451dab50..b91369fa33 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 @@ -23,6 +23,7 @@ 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.exception.LdapException; +import org.apache.directory.api.ldap.model.filter.FilterEncoder; import org.apache.directory.api.ldap.model.message.SearchScope; import org.apache.directory.api.ldap.model.schema.SchemaManager; import org.apache.directory.ldap.client.api.DefaultLdapConnectionFactory; @@ -47,32 +48,30 @@ */ public class LdapProxyBackend implements LdapBackend { private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class); + private static final String MEMBER_OF = "memberOf"; - private String ldapUrl; private String bindDn; private String bindPassword; private String userSearchBase; private String groupSearchBase; - private String proxyBaseDn; // Base DN for proxy entries (e.g., dc=proxy,dc=com) - private String remoteBaseDn; // Base DN for remote server searches (e.g., dc=hadoop,dc=apache,dc=org) private int port; private String host; // Configurable attributes for AD/LDAP compatibility private String userIdentifierAttribute = "uid"; // uid for LDAP, sAMAccountName for AD + private String groupIdentifierAttribute = "cn"; // cn is standard for groups private String userSearchFilter = "({userIdAttr}={username})"; // Will be populated with userIdentifierAttribute private String groupMemberAttribute = "memberUid"; // member for AD, memberUid for POSIX private boolean useMemberOf; // Use memberOf attribute for group lookup (efficient for AD) private boolean recursiveGroupResolution; private int groupMaxDepth; - private List proxyEntryAttributeTypes = List.of( + private final List proxyEntryAttributeTypes = List.of( // "uid" will always be filled "cn", "dn", "mail", "description"); - private final String proxyEntryGroupMembershipAttributeType = "memberOf"; // Connection pool for efficient connection reuse private LdapConnectionPool connectionPool; @@ -85,19 +84,20 @@ public String getName() { @Override public void initialize(Map config) throws Exception { // Proxy base DN is for entries created in the proxy LDAP server - proxyBaseDn = config.get("baseDn"); + // Base DN for proxy entries (e.g., dc=proxy,dc=com) + String proxyBaseDn = config.get("baseDn"); if (proxyBaseDn == null || proxyBaseDn.isEmpty()) { throw new IllegalArgumentException("baseDn is required for LDAP proxy backend"); } - // Remote base DN is for searching the remote LDAP server - remoteBaseDn = config.get("remoteBaseDn"); + // Base DN for remote server searches (e.g., dc=hadoop,dc=apache,dc=org) + String remoteBaseDn = config.get("remoteBaseDn"); if (remoteBaseDn == null || remoteBaseDn.isEmpty()) { throw new IllegalArgumentException("remoteBaseDn is required for LDAP proxy backend - this is the base DN of the remote LDAP server"); } // Support both url and host/port configuration - ldapUrl = config.get("url"); + String ldapUrl = config.get("url"); if (ldapUrl != null && !ldapUrl.isEmpty()) { // Parse URL to extract host and port parseLdapUrl(ldapUrl); @@ -131,6 +131,7 @@ public void initialize(Map config) throws Exception { // Configure attribute mappings for AD/LDAP compatibility userIdentifierAttribute = config.getOrDefault("userIdentifierAttribute", "uid"); + groupIdentifierAttribute = config.getOrDefault("groupIdentifierAttribute", "cn"); groupMemberAttribute = config.getOrDefault("groupMemberAttribute", "memberUid"); useMemberOf = Boolean.parseBoolean(config.getOrDefault("useMemberOf", "false")); recursiveGroupResolution = Boolean.parseBoolean(config.getOrDefault("recursiveGroupResolution", "false")); @@ -248,6 +249,7 @@ private void releaseConnection(LdapConnection connection) { * Closes the connection pool and releases all resources. * Should be called when the backend is being shut down. */ + @Override public void close() { if (connectionPool != null) { try { @@ -264,34 +266,30 @@ public Entry getUser(String username, SchemaManager schemaManager) throws Except try { connection = getConnection(); // 1. Try search in user base - String filter = userSearchFilter.replace("{username}", username); - EntryCursor cursor = connection.search(userSearchBase, filter, SearchScope.SUBTREE, "*", "+"); - - if (cursor.next()) { - Entry sourceEntry = cursor.get(); - Entry entry = createProxyEntry(sourceEntry, username, connection, schemaManager); - cursor.close(); - return entry; - } - cursor.close(); + final String filter = userSearchFilter.replace("{username}", username); + final Entry userBaseEntry = fetchEntry(username, schemaManager, connection, userSearchBase, filter); - // 2. Try search in group base if not found in user base - String groupFilter = "(cn=" + username + ")"; - cursor = connection.search(groupSearchBase, groupFilter, SearchScope.SUBTREE, "*", "+"); - if (cursor.next()) { - Entry sourceEntry = cursor.get(); - Entry entry = createProxyEntry(sourceEntry, username, connection, schemaManager); - cursor.close(); - return entry; + if (userBaseEntry != null) { + return userBaseEntry; + } else { + // 2. Try search in group base if not found in user base. + // This allows the proxy to enrich group entries with recursive 'memberOf' attributes + // when a client searches for a group name. + String groupFilter = "(" + groupIdentifierAttribute + "=" + username + ")"; + return fetchEntry(username, schemaManager, connection, groupSearchBase, groupFilter); } - cursor.close(); - - return null; } finally { releaseConnection(connection); } } + private Entry fetchEntry(String username, SchemaManager schemaManager, LdapConnection connection, String searchBase, String searchFilter) throws Exception { + try (final EntryCursor cursor = connection.search(searchBase, searchFilter, SearchScope.SUBTREE, "*", "+")) { + final Entry sourceEntry = cursor.next() ? cursor.get() : null; + return sourceEntry == null ? null : createProxyEntry(sourceEntry, username, connection, schemaManager); + } + } + @Override public List getUserGroups(String username) throws Exception { LdapConnection connection = null; @@ -305,21 +303,19 @@ public List getUserGroups(String username) throws Exception { } } else { String filter = userSearchFilter.replace("{username}", username); - EntryCursor cursor = connection.search(userSearchBase, filter, SearchScope.SUBTREE, "dn"); - if (cursor.next()) { - String userDn = cursor.get().getDn().toString(); - cursor.close(); - Set visited = new HashSet<>(); - visited.add(userDn); - List initialGroups = getUserGroupsInternal(connection, userDn, username); - for (Entry group : initialGroups) { - allGroupDns.add(group.getDn().toString()); - } - if (recursiveGroupResolution && !allGroupDns.isEmpty()) { - resolveRecursiveGroupDnsInternal(connection, allGroupDns, visited, 0); + try (EntryCursor cursor = connection.search(userSearchBase, filter, SearchScope.SUBTREE, "dn")) { + if (cursor.next()) { + String userDn = cursor.get().getDn().toString(); + Set visited = new HashSet<>(); + visited.add(userDn); + List initialGroups = getUserGroupsInternal(connection, userDn, username); + for (Entry group : initialGroups) { + allGroupDns.add(group.getDn().toString()); + } + if (recursiveGroupResolution && !allGroupDns.isEmpty()) { + resolveRecursiveGroupDnsInternal(connection, allGroupDns, visited, 0); + } } - } else { - cursor.close(); } } @@ -337,20 +333,20 @@ public List getUserGroups(String username) throws Exception { } private List getUserGroupDnsViaMemberOf(LdapConnection connection, String username) throws LdapException, CursorException, IOException { - List dns = new ArrayList<>(); - String filter = userSearchFilter.replace("{username}", username); - EntryCursor cursor = connection.search(userSearchBase, filter, SearchScope.SUBTREE, "memberOf", "+"); - if (cursor.next()) { - Entry userEntry = cursor.get(); - Attribute memberOfAttr = userEntry.get("memberOf"); - if (memberOfAttr != null) { - for (org.apache.directory.api.ldap.model.entry.Value value : memberOfAttr) { - dns.add(value.getString()); + final String filter = userSearchFilter.replace("{username}", username); + try (EntryCursor cursor = connection.search(userSearchBase, filter, SearchScope.SUBTREE, MEMBER_OF, "+")) { + List dns = new ArrayList<>(); + if (cursor.next()) { + Entry userEntry = cursor.get(); + Attribute memberOfAttr = userEntry.get(MEMBER_OF); + if (memberOfAttr != null) { + for (org.apache.directory.api.ldap.model.entry.Value value : memberOfAttr) { + dns.add(value.getString()); + } } } + return dns; } - cursor.close(); - return dns; } private void resolveRecursiveGroupDnsViaMemberOf(LdapConnection connection, Set allGroupDns, int depth) throws LdapException, CursorException, IOException { @@ -358,22 +354,7 @@ private void resolveRecursiveGroupDnsViaMemberOf(LdapConnection connection, Set< return; } - Set nextLevelDns = new HashSet<>(); - for (String dn : allGroupDns) { - EntryCursor cursor = connection.search(dn, "(objectClass=*)", SearchScope.OBJECT, "memberOf", "+"); - if (cursor.next()) { - Attribute memberOfAttr = cursor.get().get("memberOf"); - if (memberOfAttr != null) { - for (org.apache.directory.api.ldap.model.entry.Value value : memberOfAttr) { - String parentDn = value.getString(); - if (!allGroupDns.contains(parentDn)) { - nextLevelDns.add(parentDn); - } - } - } - } - cursor.close(); - } + final Set nextLevelDns = fetchNextLevelDNs(connection, allGroupDns); if (!nextLevelDns.isEmpty()) { allGroupDns.addAll(nextLevelDns); @@ -381,6 +362,26 @@ private void resolveRecursiveGroupDnsViaMemberOf(LdapConnection connection, Set< } } + private Set fetchNextLevelDNs(LdapConnection connection, Set allGroupDns) throws IOException, LdapException, CursorException { + final Set nextLevelDns = new HashSet<>(); + for (String dn : new HashSet<>(allGroupDns)) { + try (final EntryCursor cursor = connection.search(dn, "(objectClass=*)", SearchScope.OBJECT, MEMBER_OF, "+")) { + if (cursor.next()) { + Attribute memberOfAttr = cursor.get().get(MEMBER_OF); + if (memberOfAttr != null) { + for (org.apache.directory.api.ldap.model.entry.Value value : memberOfAttr) { + String parentDn = value.getString(); + if (!allGroupDns.contains(parentDn)) { + nextLevelDns.add(parentDn); + } + } + } + } + } + } + return nextLevelDns; + } + private void resolveRecursiveGroupDnsInternal(LdapConnection connection, Set allGroupDns, Set visited, int depth) throws LdapException, CursorException, IOException { if (depth >= groupMaxDepth) { return; @@ -420,40 +421,40 @@ private String extractGroupNameFromDn(String groupDn) { } private List getUserGroupsInternal(LdapConnection connection, String userDn, String username) throws LdapException, CursorException, IOException { - List groups = new ArrayList<>(); - String filter; + final String filter; if ("member".equals(groupMemberAttribute)) { filter = "(|(member=" + userDn + ")(uniqueMember=" + userDn + "))"; } else { filter = "(|(memberUid=" + (username != null ? username : "") + ")(member=" + userDn + ")(uniqueMember=" + userDn + "))"; } - EntryCursor cursor = connection.search(groupSearchBase, filter, SearchScope.SUBTREE, "cn"); - while (cursor.next()) { - groups.add(cursor.get()); + try (final EntryCursor cursor = connection.search(groupSearchBase, filter, SearchScope.SUBTREE, "cn") ) { + List groups = new ArrayList<>(); + while (cursor.next()) { + groups.add(cursor.get()); + } + return groups; } - cursor.close(); - return groups; } @Override public List searchUsers(String filter, SchemaManager schemaManager) throws Exception { - List results = new ArrayList<>(); LdapConnection connection = null; try { connection = getConnection(); - String ldapFilter = "(" + userIdentifierAttribute + "=" + filter.trim() + ")"; - EntryCursor cursor = connection.search(userSearchBase, ldapFilter, SearchScope.SUBTREE, "*", "+"); - while (cursor.next()) { - Entry sourceEntry = cursor.get(); - Attribute idAttr = sourceEntry.get(userIdentifierAttribute); - if (idAttr != null) { - Entry entry = createProxyEntry(sourceEntry, idAttr.getString(), connection, schemaManager); - results.add(entry); + final String userSearchFilter = "(" + userIdentifierAttribute + "=" + filter.trim() + ")"; + try (final EntryCursor cursor = connection.search(userSearchBase, userSearchFilter, SearchScope.SUBTREE, "*", "+")) { + final List users = new ArrayList<>(); + while (cursor.next()) { + Entry sourceEntry = cursor.get(); + Attribute idAttr = sourceEntry.get(userIdentifierAttribute); + if (idAttr != null) { + Entry entry = createProxyEntry(sourceEntry, idAttr.getString(), connection, schemaManager); + users.add(entry); + } } + return users; } - cursor.close(); - return results; } finally { releaseConnection(connection); } @@ -476,7 +477,7 @@ private Entry createProxyEntry(Entry sourceEntry, String username, LdapConnectio Set allGroupDns = new HashSet<>(); if (useMemberOf) { - Attribute memberOfAttr = sourceEntry.get("memberOf"); + Attribute memberOfAttr = sourceEntry.get(MEMBER_OF); if (memberOfAttr != null) { for (org.apache.directory.api.ldap.model.entry.Value value : memberOfAttr) { allGroupDns.add(value.getString()); @@ -498,7 +499,7 @@ private Entry createProxyEntry(Entry sourceEntry, String username, LdapConnectio } for (String dn : allGroupDns) { - entry.add(proxyEntryGroupMembershipAttributeType, dn); + entry.add(MEMBER_OF, dn); } return entry; From 9f6d69b1025ae702e5a6b7d433182c33a8029d3a Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Thu, 21 May 2026 13:51:14 +0200 Subject: [PATCH 5/5] KNOX-3328: Fix checkstyle issues --- .../gateway/services/ldap/backend/LdapProxyBackend.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 b91369fa33..25599cf1c8 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 @@ -23,7 +23,6 @@ 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.exception.LdapException; -import org.apache.directory.api.ldap.model.filter.FilterEncoder; import org.apache.directory.api.ldap.model.message.SearchScope; import org.apache.directory.api.ldap.model.schema.SchemaManager; import org.apache.directory.ldap.client.api.DefaultLdapConnectionFactory; @@ -284,7 +283,7 @@ public Entry getUser(String username, SchemaManager schemaManager) throws Except } private Entry fetchEntry(String username, SchemaManager schemaManager, LdapConnection connection, String searchBase, String searchFilter) throws Exception { - try (final EntryCursor cursor = connection.search(searchBase, searchFilter, SearchScope.SUBTREE, "*", "+")) { + try (EntryCursor cursor = connection.search(searchBase, searchFilter, SearchScope.SUBTREE, "*", "+")) { final Entry sourceEntry = cursor.next() ? cursor.get() : null; return sourceEntry == null ? null : createProxyEntry(sourceEntry, username, connection, schemaManager); } @@ -365,7 +364,7 @@ private void resolveRecursiveGroupDnsViaMemberOf(LdapConnection connection, Set< private Set fetchNextLevelDNs(LdapConnection connection, Set allGroupDns) throws IOException, LdapException, CursorException { final Set nextLevelDns = new HashSet<>(); for (String dn : new HashSet<>(allGroupDns)) { - try (final EntryCursor cursor = connection.search(dn, "(objectClass=*)", SearchScope.OBJECT, MEMBER_OF, "+")) { + try (EntryCursor cursor = connection.search(dn, "(objectClass=*)", SearchScope.OBJECT, MEMBER_OF, "+")) { if (cursor.next()) { Attribute memberOfAttr = cursor.get().get(MEMBER_OF); if (memberOfAttr != null) { @@ -428,7 +427,7 @@ private List getUserGroupsInternal(LdapConnection connection, String user filter = "(|(memberUid=" + (username != null ? username : "") + ")(member=" + userDn + ")(uniqueMember=" + userDn + "))"; } - try (final EntryCursor cursor = connection.search(groupSearchBase, filter, SearchScope.SUBTREE, "cn") ) { + try (EntryCursor cursor = connection.search(groupSearchBase, filter, SearchScope.SUBTREE, "cn") ) { List groups = new ArrayList<>(); while (cursor.next()) { groups.add(cursor.get()); @@ -443,7 +442,7 @@ public List searchUsers(String filter, SchemaManager schemaManager) throw try { connection = getConnection(); final String userSearchFilter = "(" + userIdentifierAttribute + "=" + filter.trim() + ")"; - try (final EntryCursor cursor = connection.search(userSearchBase, userSearchFilter, SearchScope.SUBTREE, "*", "+")) { + try (EntryCursor cursor = connection.search(userSearchBase, userSearchFilter, SearchScope.SUBTREE, "*", "+")) { final List users = new ArrayList<>(); while (cursor.next()) { Entry sourceEntry = cursor.get();