This is an automated email from the ASF dual-hosted git repository.
smolnar82 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git
The following commit(s) were added to refs/heads/master by this push:
new edbe02436 KNOX-3328: Implement recursive group resolution for LDAP
proxy (#1236)
edbe02436 is described below
commit edbe02436f9677a9a52898a80fb86509334b3982
Author: Sandor Molnar <[email protected]>
AuthorDate: Wed Jun 3 06:39:48 2026 +0200
KNOX-3328: Implement recursive group resolution for LDAP proxy (#1236)
---
.../workflows/build/conf/topologies/knoxldap.xml | 6 +-
.github/workflows/build/gateway-site.xml | 36 +--
.../tests/test_knox_auth_service_and_LDAP.py | 37 ++-
.../pom.xml | 4 +
.../groups/filter/HadoopGroupProviderFilter.java | 60 ++--
.../groups/filter/HadoopGroupProviderMessages.java | 3 +
.../filter/HadoopGroupProviderFilterTest.java | 61 ++++
.../gateway/shirorealm/KnoxLdapContextFactory.java | 15 +-
.../shirorealm/KnoxLdapContextFactoryTest.java | 16 +-
gateway-release/home/conf/users.ldif | 32 ++
.../gateway/config/impl/GatewayConfigImpl.java | 10 +
.../knox/gateway/services/CLIGatewayServices.java | 8 +
.../services/ldap/KnoxLDAPServerManager.java | 12 +
.../gateway/services/ldap/KnoxLDAPService.java | 11 +-
.../knox/gateway/services/ldap/LdapMessages.java | 24 ++
.../services/ldap/backend/LdapProxyBackend.java | 302 ++++++++++++++----
.../java/org/apache/knox/gateway/util/KnoxCLI.java | 103 +++++--
.../src/main/resources/conf/gateway-site.xml | 6 +
.../gateway/services/ldap/KnoxLDAPServiceTest.java | 82 ++---
.../ldap/backend/LdapProxyBackendTest.java | 340 +++++++++++----------
.../src/test/resources/ldap-recursive-test.ldif | 95 ++++++
.../org/apache/knox/gateway/GatewayTestConfig.java | 10 +
.../apache/knox/gateway/config/GatewayConfig.java | 12 +
.../security/principal/SimplePrincipalMapper.java | 2 -
24 files changed, 916 insertions(+), 371 deletions(-)
diff --git a/.github/workflows/build/conf/topologies/knoxldap.xml
b/.github/workflows/build/conf/topologies/knoxldap.xml
index dc1b32237..659c31c84 100644
--- a/.github/workflows/build/conf/topologies/knoxldap.xml
+++ b/.github/workflows/build/conf/topologies/knoxldap.xml
@@ -48,12 +48,16 @@ limitations under the License.
</provider>
<provider>
<role>identity-assertion</role>
- <name>Default</name>
+ <name>HadoopGroupProvider</name>
<enabled>true</enabled>
<param>
<name>group.principal.mapping</name>
<value>admin=longGroupName1,longGroupName2,longGroupName3,longGroupName4</value>
</param>
+ <param>
+ <name>CENTRAL_GROUP_CONFIG_PREFIX</name>
+ <value>gateway.group.config.</value>
+ </param>
</provider>
</gateway>
<service>
diff --git a/.github/workflows/build/gateway-site.xml
b/.github/workflows/build/gateway-site.xml
index 74a13a4a0..d8c1bbbd1 100644
--- a/.github/workflows/build/gateway-site.xml
+++ b/.github/workflows/build/gateway-site.xml
@@ -104,36 +104,8 @@ limitations under the License.
<value>org.apache.hadoop.security.LdapGroupsMapping</value>
</property>
<property>
-
<name>gateway.group.config.hadoop.security.group.mapping.ldap.bind.user</name>
- <value>uid=guest,ou=people,dc=hadoop,dc=apache,dc=org</value>
- </property>
- <property>
-
<name>gateway.group.config.hadoop.security.group.mapping.ldap.bind.password</name>
- <value>guest-password</value>
- </property>
- <property>
-
<name>gateway.group.config.hadoop.security.group.mapping.ldap.url</name>
- <value>ldap://localhost:33389</value>
- </property>
- <property>
-
<name>gateway.group.config.hadoop.security.group.mapping.ldap.base</name>
- <value></value>
- </property>
- <property>
-
<name>gateway.group.config.hadoop.security.group.mapping.ldap.search.filter.user</name>
-
<value>(&(|(objectclass=person)(objectclass=applicationProcess))(cn={0}))</value>
- </property>
- <property>
-
<name>gateway.group.config.hadoop.security.group.mapping.ldap.search.filter.group</name>
- <value>(objectclass=groupOfNames)</value>
- </property>
- <property>
-
<name>gateway.group.config.hadoop.security.group.mapping.ldap.search.attr.member</name>
- <value>member</value>
- </property>
- <property>
-
<name>gateway.group.config.hadoop.security.group.mapping.ldap.search.attr.group.name</name>
- <value>cn</value>
+ <name>gateway.group.config.use.ldap.service</name>
+ <value>true</value>
</property>
<property>
<name>gateway.dispatch.whitelist.services</name>
@@ -177,6 +149,10 @@ limitations under the License.
<name>gateway.ldap.backend.type</name>
<value>ldap</value>
</property>
+ <property>
+ <name>gateway.ldap.recursive.group.resolution</name>
+ <value>true</value>
+ </property>
<!-- LDAP Backend specific configuration (proxying to demo ldap) -->
<property>
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 43fb886ab..575d63a92 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,11 @@ class TestKnoxAuthService(unittest.TestCase):
self.assertEqual(response.headers[actor_id_header], 'guest')
print(f"Verified {actor_id_header}:
{response.headers[actor_id_header]}")
+ # Check for Actor Group header - should be empty for guest
+ prefix = 'x-knox-actor-groups'
+ all_groups = collect_actor_group_values(response, prefix=prefix)
+ self.assertEqual(len(all_groups), 0, f"Guest user should not have any
group headers starting with {prefix}")
+
def test_auth_service_admin_groups(self):
"""
Verify that admin user gets actor ID and group headers.
@@ -81,10 +86,38 @@ class TestKnoxAuthService(unittest.TestCase):
if h.lower().startswith(prefix.lower()):
print(f"Found group header {h}: {response.headers[h]}")
- expected_groups = ['longGroupName1', 'longGroupName2',
'longGroupName3', 'longGroupName4']
+ expected_groups = ['admin', 'longGroupName1', 'longGroupName2',
'longGroupName3', 'longGroupName4']
+ for group in expected_groups:
+ self.assertIn(group, all_groups)
+
+ def test_auth_service_recursive_user_groups(self):
+ """
+ Verify that recursiveUser user gets actor ID and recursive group
headers.
+ """
+ print(f"\nTesting recursiveUser authentication against
{self.topology_url}")
+ response = knox_get(
+ self.topology_url,
+ auth=HTTPBasicAuth('recursiveUser', 'recursiveUser-password'),
+ )
+
+ print(f"Status Code: {response.status_code}")
+ self.assertEqual(response.status_code, 200)
+
+ # Check for Actor ID header
+ actor_id_header = 'x-knox-actor-username'
+ self.assertIn(actor_id_header, response.headers)
+ self.assertEqual(response.headers[actor_id_header], 'recursiveUser')
+ print(f"Verified {actor_id_header}:
{response.headers[actor_id_header]}")
+
+ # Check for Actor Group headers
+ prefix = 'x-knox-actor-groups'
+ all_groups = collect_actor_group_values(response, prefix=prefix)
+
+ expected_groups = ['level1', 'level2', 'level3']
+ self.assertEqual(len(all_groups), len(expected_groups))
for group in expected_groups:
self.assertIn(group, all_groups)
+ print(f"Verified recursive groups: {all_groups}")
if __name__ == '__main__':
unittest.main()
-
diff --git a/gateway-provider-identity-assertion-hadoop-groups/pom.xml
b/gateway-provider-identity-assertion-hadoop-groups/pom.xml
index 31766b1cf..fa42ccc98 100644
--- a/gateway-provider-identity-assertion-hadoop-groups/pom.xml
+++ b/gateway-provider-identity-assertion-hadoop-groups/pom.xml
@@ -41,6 +41,10 @@
<groupId>org.apache.knox</groupId>
<artifactId>gateway-provider-identity-assertion-common</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.knox</groupId>
+ <artifactId>gateway-server</artifactId>
+ </dependency>
<dependency>
<groupId>commons-io</groupId>
diff --git
a/gateway-provider-identity-assertion-hadoop-groups/src/main/java/org/apache/knox/gateway/identityasserter/hadoop/groups/filter/HadoopGroupProviderFilter.java
b/gateway-provider-identity-assertion-hadoop-groups/src/main/java/org/apache/knox/gateway/identityasserter/hadoop/groups/filter/HadoopGroupProviderFilter.java
index b53695c83..e819e1a7a 100644
---
a/gateway-provider-identity-assertion-hadoop-groups/src/main/java/org/apache/knox/gateway/identityasserter/hadoop/groups/filter/HadoopGroupProviderFilter.java
+++
b/gateway-provider-identity-assertion-hadoop-groups/src/main/java/org/apache/knox/gateway/identityasserter/hadoop/groups/filter/HadoopGroupProviderFilter.java
@@ -17,7 +17,6 @@
*/
package org.apache.knox.gateway.identityasserter.hadoop.groups.filter;
-import java.io.IOException;
import java.util.Enumeration;
import java.util.List;
@@ -26,10 +25,15 @@ import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.security.LdapGroupsMapping;
+import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import
org.apache.knox.gateway.identityasserter.common.filter.CommonIdentityAssertionFilter;
import org.apache.hadoop.security.GroupMappingServiceProvider;
import org.apache.hadoop.security.Groups;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.ldap.KnoxLDAPService;
/**
* A filter that integrates the Hadoop {@link GroupMappingServiceProvider} for
@@ -42,8 +46,9 @@ public class HadoopGroupProviderFilter extends
CommonIdentityAssertionFilter {
/**
* Logging
*/
- public static final HadoopGroupProviderMessages LOG = MessagesFactory
- .get(HadoopGroupProviderMessages.class);
+ public static final HadoopGroupProviderMessages LOG =
MessagesFactory.get(HadoopGroupProviderMessages.class);
+
+ static final String USE_LDAP_SERVICE = "use.ldap.service";
/**
* Configuration object needed by for hadoop classes
@@ -55,6 +60,8 @@ public class HadoopGroupProviderFilter extends
CommonIdentityAssertionFilter {
*/
private Groups hadoopGroups;
+ private KnoxLDAPService ldapService;
+
/* create an instance */
public HadoopGroupProviderFilter() {
super();
@@ -64,26 +71,34 @@ public class HadoopGroupProviderFilter extends
CommonIdentityAssertionFilter {
public void init(final FilterConfig filterConfig) throws ServletException {
super.init(filterConfig);
- try {
- hadoopConfig = new Configuration(false);
-
- if (filterConfig.getInitParameterNames() != null) {
-
- for (final Enumeration<String> keys = filterConfig
- .getInitParameterNames(); keys.hasMoreElements();) {
+ if (shouldUseLdapService(filterConfig)) {
+ final GatewayServices services = (GatewayServices)
filterConfig.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+ ldapService = services.getService(ServiceType.LDAP_SERVICE);
+ } else {
+ try {
+ hadoopConfig = new Configuration(false);
- final String key = keys.nextElement();
- hadoopConfig.set(key, filterConfig.getInitParameter(key));
+ if (filterConfig.getInitParameterNames() != null) {
+ for (final Enumeration<String> keys =
filterConfig.getInitParameterNames(); keys.hasMoreElements(); ) {
+ final String key = keys.nextElement();
+ hadoopConfig.set(key, filterConfig.getInitParameter(key));
+ }
}
-
+ hadoopGroups = new Groups(hadoopConfig);
+ } catch (final Exception e) {
+ throw new ServletException(e);
}
- hadoopGroups = new Groups(hadoopConfig);
-
- } catch (final Exception e) {
- throw new ServletException(e);
}
+ }
+ private boolean shouldUseLdapService(final FilterConfig filterConfig) {
+ final String groupMappingsProvider =
filterConfig.getInitParameter(GroupMappingServiceProvider.GROUP_MAPPING_CONFIG_PREFIX);
+ final boolean ldapGroupsMappingEnabled =
LdapGroupsMapping.class.getName().equals(groupMappingsProvider);
+ final boolean useLdapService =
Boolean.parseBoolean(filterConfig.getInitParameter(USE_LDAP_SERVICE));
+ final GatewayConfig gatewayConfig = (GatewayConfig)
filterConfig.getServletContext().getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
+ final boolean ldapEnabled = gatewayConfig != null &&
gatewayConfig.isLDAPEnabled();
+ return ldapGroupsMappingEnabled && useLdapService && ldapEnabled;
}
/**
@@ -100,7 +115,7 @@ public class HadoopGroupProviderFilter extends
CommonIdentityAssertionFilter {
LOG.groupsFound(mappedPrincipalName, groupList.toString());
groups = groupList.toArray(new String[0]);
- } catch (final IOException e) {
+ } catch (final Exception e) {
if (e.toString().contains("No groups found for user")) {
/* no groups found move on */
LOG.noGroupsFound(mappedPrincipalName);
@@ -113,8 +128,13 @@ public class HadoopGroupProviderFilter extends
CommonIdentityAssertionFilter {
return groups;
}
- protected List<String> hadoopGroups(String mappedPrincipalName) throws
IOException {
- return hadoopGroups.getGroups(mappedPrincipalName);
+ protected List<String> hadoopGroups(String mappedPrincipalName) throws
Exception {
+ if (ldapService == null) {
+ return hadoopGroups.getGroups(mappedPrincipalName);
+ } else {
+ LOG.useKnoxLDAPService();
+ return ldapService.getUserGroups(mappedPrincipalName);
+ }
}
@Override
diff --git
a/gateway-provider-identity-assertion-hadoop-groups/src/main/java/org/apache/knox/gateway/identityasserter/hadoop/groups/filter/HadoopGroupProviderMessages.java
b/gateway-provider-identity-assertion-hadoop-groups/src/main/java/org/apache/knox/gateway/identityasserter/hadoop/groups/filter/HadoopGroupProviderMessages.java
index c014c9424..86ce89d02 100644
---
a/gateway-provider-identity-assertion-hadoop-groups/src/main/java/org/apache/knox/gateway/identityasserter/hadoop/groups/filter/HadoopGroupProviderMessages.java
+++
b/gateway-provider-identity-assertion-hadoop-groups/src/main/java/org/apache/knox/gateway/identityasserter/hadoop/groups/filter/HadoopGroupProviderMessages.java
@@ -41,4 +41,7 @@ public interface HadoopGroupProviderMessages {
@Message( level = MessageLevel.DEBUG, text = "Found group mapping
configuration in gateway-site" )
void groupMappingFound();
+
+ @Message( level = MessageLevel.INFO, text = "Using Knox LDAP service to
fetch groups..." )
+ void useKnoxLDAPService();
}
diff --git
a/gateway-provider-identity-assertion-hadoop-groups/src/test/java/org/apache/knox/gateway/identityasserter/hadoop/groups/filter/HadoopGroupProviderFilterTest.java
b/gateway-provider-identity-assertion-hadoop-groups/src/test/java/org/apache/knox/gateway/identityasserter/hadoop/groups/filter/HadoopGroupProviderFilterTest.java
index 39848a23e..f85d3f260 100644
---
a/gateway-provider-identity-assertion-hadoop-groups/src/test/java/org/apache/knox/gateway/identityasserter/hadoop/groups/filter/HadoopGroupProviderFilterTest.java
+++
b/gateway-provider-identity-assertion-hadoop-groups/src/test/java/org/apache/knox/gateway/identityasserter/hadoop/groups/filter/HadoopGroupProviderFilterTest.java
@@ -39,8 +39,14 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
+import org.apache.hadoop.security.GroupMappingServiceProvider;
+import org.apache.hadoop.security.LdapGroupsMapping;
+import org.apache.knox.gateway.config.GatewayConfig;
import
org.apache.knox.gateway.identityasserter.common.filter.CommonIdentityAssertionFilter;
import org.apache.knox.gateway.security.PrimaryPrincipal;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.ldap.KnoxLDAPService;
import org.easymock.EasyMock;
import org.junit.Test;
@@ -265,4 +271,59 @@ public class HadoopGroupProviderFilterTest {
assertEquals(
new HashSet<>(Arrays.asList("hadoop-group",
"test-virtual-group")), calculatedGroups);
}
+
+ @Test
+ public void testLdapServiceIntegration() throws Exception {
+ KnoxLDAPService ldapService =
EasyMock.createNiceMock(KnoxLDAPService.class);
+ doTestLdapServiceIntegration(true, Arrays.asList("group1", "group2"),
ldapService);
+ }
+
+ @Test
+ public void testFallbackToHadoopGroupsWhenLdapDisabled() throws Exception {
+ doTestLdapServiceIntegration(false,
Collections.singletonList("hadoop-group"), null);
+ }
+
+ private void doTestLdapServiceIntegration(boolean ldapEnabled, List<String>
expectedGroups, KnoxLDAPService ldapService) throws Exception {
+ final String principalName = "test-user";
+
+ FilterConfig config = EasyMock.createNiceMock(FilterConfig.class);
+
EasyMock.expect(config.getInitParameter(GroupMappingServiceProvider.GROUP_MAPPING_CONFIG_PREFIX)).andReturn(LdapGroupsMapping.class.getName()).anyTimes();
+
EasyMock.expect(config.getInitParameter(HadoopGroupProviderFilter.USE_LDAP_SERVICE)).andReturn("true").anyTimes();
+ ServletContext context = EasyMock.createNiceMock(ServletContext.class);
+ GatewayConfig gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class);
+
+ EasyMock.expect(config.getServletContext()).andReturn(context).anyTimes();
+
EasyMock.expect(context.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE)).andReturn(gatewayConfig).anyTimes();
+
EasyMock.expect(gatewayConfig.isLDAPEnabled()).andReturn(ldapEnabled).anyTimes();
+
+ if (ldapEnabled) {
+ GatewayServices services =
EasyMock.createNiceMock(GatewayServices.class);
+
EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(services).anyTimes();
+
EasyMock.expect(services.getService(ServiceType.LDAP_SERVICE)).andReturn(ldapService).anyTimes();
+
EasyMock.expect(ldapService.getUserGroups(principalName)).andReturn(expectedGroups).anyTimes();
+ EasyMock.replay(services, ldapService);
+ } else {
+
EasyMock.expect(config.getInitParameterNames()).andReturn(Collections.emptyEnumeration()).anyTimes();
+ }
+
+ EasyMock.replay(config, context, gatewayConfig);
+
+ HadoopGroupProviderFilter filter = new HadoopGroupProviderFilter() {
+ @Override
+ protected List<String> hadoopGroups(String mappedPrincipalName) throws
Exception {
+ return ldapEnabled ? super.hadoopGroups(mappedPrincipalName) :
expectedGroups;
+ }
+ };
+ filter.init(config);
+
+ Subject subject = new Subject();
+ subject.getPrincipals().add(new PrimaryPrincipal(principalName));
+
+ String[] groups = filter.mapGroupPrincipals(principalName, subject);
+
+ assertThat(Arrays.asList(groups), is(expectedGroups));
+ if (ldapEnabled) {
+ EasyMock.verify(ldapService);
+ }
+ }
}
diff --git
a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/shirorealm/KnoxLdapContextFactory.java
b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/shirorealm/KnoxLdapContextFactory.java
index 08773d246..57c6a0e62 100644
---
a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/shirorealm/KnoxLdapContextFactory.java
+++
b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/shirorealm/KnoxLdapContextFactory.java
@@ -70,15 +70,12 @@ public class KnoxLdapContextFactory extends
JndiLdapContextFactory {
}
@Override
- public void setSystemPassword(final String systemPass) {
- if (StringUtils.isBlank(systemPass)) {
- return;
- }
-
+ public String getSystemPassword() {
+ final String systemPass = super.getSystemPassword();
final AliasService aliasService = getAliasService();
- if (!aliasService.isAlias(systemPass)) {
- super.setSystemPassword(systemPass);
+ if (StringUtils.isBlank(systemPass) ||
!aliasService.isAlias(systemPass)) {
+ return systemPass;
} else {
final String systemPasswordAlias =
aliasService.extractAlias(systemPass);
char[] systemPassword = null;
@@ -93,10 +90,10 @@ public class KnoxLdapContextFactory extends
JndiLdapContextFactory {
LOG.unableToGetPassword(e);
}
if (systemPassword != null) {
- super.setSystemPassword(new String(systemPassword));
+ return new String(systemPassword);
} else {
- super.setSystemPassword(""); //needs to be set to blank
LOG.aliasValueNotFound(clusterName, systemPasswordAlias);
+ return ""; //needs to be set to blank
}
}
}
diff --git
a/gateway-provider-security-shiro/src/test/java/org/apache/knox/gateway/shirorealm/KnoxLdapContextFactoryTest.java
b/gateway-provider-security-shiro/src/test/java/org/apache/knox/gateway/shirorealm/KnoxLdapContextFactoryTest.java
index 0db8b8e91..dce734731 100644
---
a/gateway-provider-security-shiro/src/test/java/org/apache/knox/gateway/shirorealm/KnoxLdapContextFactoryTest.java
+++
b/gateway-provider-security-shiro/src/test/java/org/apache/knox/gateway/shirorealm/KnoxLdapContextFactoryTest.java
@@ -46,17 +46,21 @@ public class KnoxLdapContextFactoryTest {
}
@Test
- public void testSetSystemPasswordBlank() {
+ public void testGetSystemPasswordBlank() {
final KnoxLdapContextFactory factory = new
TestKnoxLdapContextFactory();
factory.setSystemPassword("");
- assertNull(factory.getSystemPassword());
+ assertEquals("", factory.getSystemPassword());
+ }
+ @Test
+ public void testGetSystemPasswordNull() {
+ final KnoxLdapContextFactory factory = new
TestKnoxLdapContextFactory();
factory.setSystemPassword(null);
assertNull(factory.getSystemPassword());
}
@Test
- public void testSetSystemPasswordIsNotAlias() {
+ public void testGetSystemPasswordIsNotAlias() {
final KnoxLdapContextFactory factory = new
TestKnoxLdapContextFactory();
final String password = "password";
factory.setSystemPassword(password);
@@ -64,17 +68,17 @@ public class KnoxLdapContextFactoryTest {
}
@Test
- public void testSetSystemPasswordIsAliasFoundInClusterCredentialStore()
throws Exception {
+ public void testGetSystemPasswordIsAliasFoundInClusterCredentialStore()
throws Exception {
testAliasFoundInCredentialStore(PasswordLocation.CLUSTER);
}
@Test
- public void testSetSystemPasswordIsAliasFoundInGatewayCredentialStore()
throws Exception {
+ public void testGetSystemPasswordIsAliasFoundInGatewayCredentialStore()
throws Exception {
testAliasFoundInCredentialStore(PasswordLocation.GATEWAY);
}
@Test
- public void testSetSystemPasswordNotFound() throws Exception {
+ public void testGetSystemPasswordNotFound() throws Exception {
testAliasFoundInCredentialStore(PasswordLocation.NOWHERE);
}
diff --git a/gateway-release/home/conf/users.ldif
b/gateway-release/home/conf/users.ldif
index 986704dc4..4f1c6a955 100644
--- a/gateway-release/home/conf/users.ldif
+++ b/gateway-release/home/conf/users.ldif
@@ -108,3 +108,35 @@ cn: admin
description: admin group
member: uid=admin,ou=people,dc=hadoop,dc=apache,dc=org
+# entry for sample user recursiveUser
+dn: uid=recursiveUser,ou=people,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass:person
+objectclass:organizationalPerson
+objectclass:inetOrgPerson
+cn: Recursive
+sn: User
+uid: recursiveUser
+userPassword:recursiveUser-password
+
+# Level 1 Group
+dn: cn=level1,ou=groups,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass: groupofnames
+cn: level1
+member: uid=recursiveUser,ou=people,dc=hadoop,dc=apache,dc=org
+
+# Level 2 Group (Member is Level 1)
+dn: cn=level2,ou=groups,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass: groupofnames
+cn: level2
+member: cn=level1,ou=groups,dc=hadoop,dc=apache,dc=org
+
+# Level 3 Group (Member is Level 2)
+dn: cn=level3,ou=groups,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass: groupofnames
+cn: level3
+member: cn=level2,ou=groups,dc=hadoop,dc=apache,dc=org
+
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 68dfbd6b2..eb3f9f227 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
@@ -1798,6 +1798,16 @@ public class GatewayConfigImpl extends Configuration
implements GatewayConfig {
return config;
}
+ @Override
+ public boolean isLDAPRecursiveGroupResolutionEnabled() {
+ return Boolean.parseBoolean(get(LDAP_RECURSIVE_GROUP_RESOLUTION, "false"));
+ }
+
+ @Override
+ public int getLDAPRecursiveGroupResolutionMaxDepth() {
+ return getInt(LDAP_RECURSIVE_GROUP_RESOLUTION_MAX_DEPTH, 3);
+ }
+
@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/CLIGatewayServices.java
b/gateway-server/src/main/java/org/apache/knox/gateway/services/CLIGatewayServices.java
index 872578716..9c2ea6e62 100644
---
a/gateway-server/src/main/java/org/apache/knox/gateway/services/CLIGatewayServices.java
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/CLIGatewayServices.java
@@ -25,6 +25,7 @@ import org.apache.knox.gateway.deploy.DeploymentContext;
import org.apache.knox.gateway.descriptor.FilterParamDescriptor;
import org.apache.knox.gateway.descriptor.ResourceDescriptor;
import org.apache.knox.gateway.services.security.impl.CLIMasterService;
+import org.apache.knox.gateway.services.ldap.KnoxLDAPService;
import org.apache.knox.gateway.topology.Provider;
public class CLIGatewayServices extends AbstractGatewayServices {
@@ -55,6 +56,13 @@ public class CLIGatewayServices extends
AbstractGatewayServices {
addService(ServiceType.TOPOLOGY_SERVICE,
gatewayServiceFactory.create(this, ServiceType.TOPOLOGY_SERVICE, config,
options));
addService(ServiceType.TOKEN_STATE_SERVICE,
gatewayServiceFactory.create(this, ServiceType.TOKEN_STATE_SERVICE, config,
options));
+
+ // LDAP Service - infrastructure service for embedded LDAP server
+ if (config.isLDAPEnabled()) {
+ KnoxLDAPService ldapService = new KnoxLDAPService();
+ ldapService.init(config, options);
+ addService(ServiceType.LDAP_SERVICE, ldapService);
+ }
}
@Override
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 cd7bb1a80..eebf9251e 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
@@ -80,6 +80,9 @@ public class KnoxLDAPServerManager {
backendConfig.put("dataFile", config.getLDAPBackendDataFile());
}
+ backendConfig.put("recursiveGroupResolution",
String.valueOf(config.isLDAPRecursiveGroupResolutionEnabled()));
+ backendConfig.put("recursiveGroupResolutionMaxDepth",
String.valueOf(config.getLDAPRecursiveGroupResolutionMaxDepth()));
+
// For proxy backends, extract remoteBaseDn if present
this.remoteBaseDn = backendConfig.get("remoteBaseDn");
@@ -257,6 +260,15 @@ public class KnoxLDAPServerManager {
return baseDn;
}
+ /**
+ * Get groups for a user from the configured backend
+ * @param username The username
+ * @return List of group names
+ */
+ public List<String> getUserGroups(String username) throws Exception {
+ return backend.getUserGroups(username);
+ }
+
/**
* Check if the LDAP server is currently running.
*
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 a6f66a67e..66bcfdcb2 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
@@ -23,6 +23,7 @@ import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.services.Service;
import org.apache.knox.gateway.services.ServiceLifecycleException;
+import java.util.List;
import java.util.Map;
/**
@@ -47,7 +48,6 @@ public class KnoxLDAPService implements Service,
GatewayConfigChangeListener {
// Initialize the LDAP server manager with configuration
ldapServerManager = new KnoxLDAPServerManager();
ldapServerManager.initialize(config);
-
} catch (Exception e) {
throw new ServiceLifecycleException("Failed to initialize LDAP
service", e);
}
@@ -122,4 +122,13 @@ public class KnoxLDAPService implements Service,
GatewayConfigChangeListener {
public boolean isEnabled() {
return enabled;
}
+
+ /**
+ * Get groups for a user from the configured backend
+ * @param username The username
+ * @return List of group names
+ */
+ public List<String> getUserGroups(String username) throws Exception {
+ return ldapServerManager == null ? List.of() :
ldapServerManager.getUserGroups(username);
+ }
}
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 cc92a099d..221e36e4d 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
@@ -110,4 +110,28 @@ public interface LdapMessages {
@Message(level = MessageLevel.ERROR,
text = "Failed to reload LDAP service: {0}")
void ldapServiceReloadFailed(@StackTrace(level = MessageLevel.DEBUG)
Exception e);
+
+ @Message(level = MessageLevel.DEBUG, text = "Recursive group search
enabled: {0}, max depth: {1}")
+ void ldapRecursiveGroupSearchConfig(boolean enabled, int maxDepth);
+
+ @Message(level = MessageLevel.DEBUG, text = "Recursive group search for
user {0} found {1} group(s) ({2}) at depth {3}")
+ void ldapRecursiveGroupSearchProgress(String user, int count, String
groups, int depth);
+
+ @Message(level = MessageLevel.DEBUG, text = "Recursive group search for
user {0} completed. Total groups found: {1}")
+ void ldapRecursiveGroupSearchFinished(String user, int count);
+
+ @Message(level = MessageLevel.WARN, text = "Recursive group search for
user {0} reached max depth {1}")
+ void ldapRecursiveGroupSearchMaxDepthReached(String user, int maxDepth);
+
+ @Message(level = MessageLevel.DEBUG, text = "Cycle detected in recursive
group search for user {0} at group {1}")
+ void ldapRecursiveGroupSearchCycleDetected(String user, String groupDn);
+
+ @Message(level = MessageLevel.DEBUG, text = "Created skeleton group entry
for {0} as actual group entry was not found in the backend")
+ void ldapSkeletonGroupEntryCreated(String groupDn);
+
+ @Message(level = MessageLevel.DEBUG, text = "Found {1} parent(s) in cache
for group {0}")
+ void ldapRecursiveGroupSearchCacheHit(String groupDn, int count);
+
+ @Message(level = MessageLevel.DEBUG, text = "Added parent {1} to cache for
group {0}")
+ void ldapRecursiveGroupSearchCacheAdd(String groupDn, String parentDn);
}
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 053a487bb..59355e4ad 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
@@ -22,6 +22,7 @@ import org.apache.directory.api.ldap.model.cursor.EntryCursor;
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.SearchScope;
import org.apache.directory.api.ldap.model.name.Dn;
@@ -38,10 +39,13 @@ import org.apache.knox.gateway.services.ldap.LdapMessages;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
/**
* LDAP backend that proxies to an external LDAP server.
@@ -65,6 +69,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 recursiveGroupResolutionMaxDepth;
private List<String> proxyEntryAttributeTypes = List.of(
// "uid" will always be filled
@@ -133,13 +139,16 @@ public class LdapProxyBackend implements LdapBackend {
userIdentifierAttribute =
config.getOrDefault("userIdentifierAttribute", "uid");
groupMemberAttribute = config.getOrDefault("groupMemberAttribute",
"memberUid");
useMemberOf = Boolean.parseBoolean(config.getOrDefault("useMemberOf",
"false"));
+ recursiveGroupResolution =
Boolean.parseBoolean(config.getOrDefault("recursiveGroupResolution", "false"));
+ recursiveGroupResolutionMaxDepth =
Integer.parseInt(config.getOrDefault("recursiveGroupResolutionMaxDepth", "3"));
// 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 ? " using memberOf lookups" : "
using group searches") +
+ (recursiveGroupResolution ? " with recursive
group resolution (max depth: " + recursiveGroupResolutionMaxDepth + ")" : ""));
// Initialize connection pool
initializeConnectionPool(config);
@@ -281,15 +290,12 @@ public class LdapProxyBackend implements LdapBackend {
connection = getConnection();
// Search for user using configurable attribute
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;
+ try (EntryCursor cursor = connection.search(userSearchBase,
filter, SearchScope.SUBTREE, "*")) {
+ if (cursor.next()) {
+ Entry sourceEntry = cursor.get();
+ return createProxyEntry(sourceEntry, username, connection,
schemaManager, createResolvedParentsCache());
+ }
}
- cursor.close();
return null;
} finally {
releaseConnection(connection);
@@ -301,53 +307,214 @@ public class LdapProxyBackend implements LdapBackend {
LdapConnection connection = null;
try {
connection = getConnection();
- if (useMemberOf) {
- // Use memberOf attribute for efficient AD lookups
- return getUserGroupsViaMemberOf(connection, username);
- } else {
- // Use traditional group search approach
- String filter = userSearchFilter.replace("{username}",
username);
- EntryCursor cursor = connection.search(userSearchBase, filter,
SearchScope.SUBTREE, "dn");
+ List<Entry> groups = getUserGroupsEntries(connection, username,
createResolvedParentsCache());
+ return getCnsFromEntries(groups);
+ } finally {
+ releaseConnection(connection);
+ }
+ }
+ private List<Entry> getUserGroupsEntries(LdapConnection connection, String
username, Map<String, Set<Entry>> resolvedParentsCache) throws Exception {
+ List<Entry> groups = new ArrayList<>();
+ if (useMemberOf) {
+ // Use memberOf attribute for efficient AD lookups
+ List<String> groupDns =
getUserGroupsViaMemberOfInternal(connection, username);
+ for (String groupDn : groupDns) {
+ if (recursiveGroupResolution) {
+ // We only need the Entry if we are doing recursive group
resolution
+ try {
+ Entry groupEntry = connection.lookup(groupDn, "cn");
+ if (groupEntry != null) {
+ groups.add(groupEntry);
+ } else {
+ groups.add(createSkeletonGroupEntry(groupDn));
+ }
+ } catch (LdapException e) {
+ groups.add(createSkeletonGroupEntry(groupDn));
+ }
+ } else {
+ // Optimized path: just use skeleton entry to extract CN
from DN
+ groups.add(createSkeletonGroupEntry(groupDn));
+ }
+ }
+ } else {
+ // Use traditional group search approach
+ String filter = userSearchFilter.replace("{username}", username);
+ try (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));
+ groups = getUserGroupsInternal(connection, userDn,
username);
+ }
+ }
+ }
+
+ if (recursiveGroupResolution && !groups.isEmpty()) {
+ groups = resolveGroupsRecursive(connection, groups, username,
resolvedParentsCache);
+ }
+ return groups;
+ }
+
+ private List<String> getUserGroupsViaMemberOfInternal(LdapConnection
connection, String username) throws LdapException, CursorException, IOException
{
+ final List<String> groupDns = new ArrayList<>();
+ // Search for user and retrieve memberOf attribute
+ final String filter = userSearchFilter.replace("{username}", username);
+ try (EntryCursor cursor = connection.search(userSearchBase, filter,
SearchScope.SUBTREE, "memberOf")) {
+ if (cursor.next()) {
+ final Attribute memberOfAttr = cursor.get().get("memberOf");
+ if (memberOfAttr != null) {
+ for (Value value : memberOfAttr) {
+ groupDns.add(value.getString());
+ }
}
+ }
+ }
+ return groupDns;
+ }
- cursor.close();
+ /**
+ * Creates a skeleton group entry from a DN when the actual group entry
cannot be
+ * found in the backend. This ensures backward compatibility with the
original
+ * implementation which extracted group names directly from memberOf DNs
even
+ * if the referenced group entries did not exist.
+ *
+ * @param groupDn The Distinguished Name of the group
+ * @return A skeleton Entry containing the DN and CN (if extractable), or
null if creation fails
+ */
+ private Entry createSkeletonGroupEntry(String groupDn) {
+ try {
+ Entry entry = new DefaultEntry(groupDn);
+ String groupName = extractGroupNameFromDn(groupDn);
+ if (groupName != null) {
+ entry.add("cn", groupName);
}
- return Collections.emptyList();
- } finally {
- releaseConnection(connection);
+ LOG.ldapSkeletonGroupEntryCreated(groupDn);
+ return entry;
+ } catch (LdapException e) {
+ LOG.ldapSearch(groupDn, "failed to create skeleton entry");
+ return null;
}
}
- private List<String> getUserGroupsViaMemberOf(LdapConnection connection,
String username) throws LdapException, CursorException, IOException {
- List<String> groups = new ArrayList<>();
+ private List<Entry> resolveGroupsRecursive(LdapConnection connection,
List<Entry> initialGroups, String username,
+ Map<String, Set<Entry>>
resolvedParentsCache) throws LdapException, CursorException, IOException {
+ LOG.ldapRecursiveGroupSearchConfig(recursiveGroupResolution,
recursiveGroupResolutionMaxDepth);
- // Search for user and retrieve memberOf attribute
- 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) {
- // 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);
+ Set<String> allGroupDns = new HashSet<>();
+ List<Entry> allGroups = new ArrayList<>();
+ List<Entry> currentLevelGroups = new ArrayList<>(initialGroups);
+
+ for (Entry group : initialGroups) {
+ allGroupDns.add(group.getDn().getNormName());
+ allGroups.add(group);
+ }
+
+ logRecursiveSearchProgress(username, initialGroups, 0);
+
+ int depth = 1;
+ while (!currentLevelGroups.isEmpty() && depth <
recursiveGroupResolutionMaxDepth) {
+ List<Entry> nextLevelGroups = new ArrayList<>();
+ List<Entry> groupsToSearch = new ArrayList<>();
+
+ // Check cache first
+ populateFromCache(resolvedParentsCache, currentLevelGroups,
allGroupDns, allGroups, nextLevelGroups, groupsToSearch);
+
+ if (!groupsToSearch.isEmpty()) {
+ StringBuilder filterBuilder = new StringBuilder("(|");
+ for (Entry group : groupsToSearch) {
+ String dn = group.getDn().getName();
+
filterBuilder.append("(member=").append(dn).append(")(uniqueMember=").append(dn).append(")");
+ }
+ filterBuilder.append(")");
+
+ try (EntryCursor cursor = connection.search(groupSearchBase,
filterBuilder.toString(), SearchScope.SUBTREE, "cn", "member", "uniqueMember"))
{
+ while (cursor.next()) {
+ Entry parentGroup = cursor.get();
+ String parentDn = parentGroup.getDn().getNormName();
+
+ // Update cache for all groups found in this search
+ updateCache(resolvedParentsCache, groupsToSearch,
parentGroup);
+
+ if (!allGroupDns.contains(parentDn)) {
+ allGroupDns.add(parentDn);
+ allGroups.add(parentGroup);
+ nextLevelGroups.add(parentGroup);
+ } else {
+
LOG.ldapRecursiveGroupSearchCycleDetected(username, parentDn);
+ }
}
}
+
+ // If some groups had no parents, we still need to mark them
in cache to avoid re-searching
+ for (Entry child : groupsToSearch) {
+
resolvedParentsCache.putIfAbsent(child.getDn().getNormName(), new HashSet<>());
+ }
+ }
+
+ logRecursiveSearchProgress(username, nextLevelGroups, depth);
+ currentLevelGroups = nextLevelGroups;
+ depth++;
+
+ if (depth == recursiveGroupResolutionMaxDepth &&
!currentLevelGroups.isEmpty()) {
+ LOG.ldapRecursiveGroupSearchMaxDepthReached(username,
recursiveGroupResolutionMaxDepth);
}
}
- cursor.close();
- return groups;
+ LOG.ldapRecursiveGroupSearchFinished(username, allGroups.size());
+ return allGroups;
+ }
+
+ private void populateFromCache(Map<String, Set<Entry>>
resolvedParentsCache, List<Entry> currentLevelGroups, Set<String> allGroupDns,
List<Entry> allGroups, List<Entry> nextLevelGroups, List<Entry> groupsToSearch)
{
+ for (Entry group : currentLevelGroups) {
+ Set<Entry> parents =
resolvedParentsCache.get(group.getDn().getNormName());
+ if (parents != null) {
+ LOG.ldapRecursiveGroupSearchCacheHit(group.getDn().getName(),
parents.size());
+ parents.forEach(parent -> {
+ String parentDn = parent.getDn().getNormName();
+ if (!allGroupDns.contains(parentDn)) {
+ allGroupDns.add(parentDn);
+ allGroups.add(parent);
+ nextLevelGroups.add(parent);
+ }
+ });
+ } else {
+ groupsToSearch.add(group);
+ }
+ }
+ }
+
+ private void updateCache(Map<String, Set<Entry>> resolvedParentsCache,
List<Entry> groupsToSearch, Entry parentGroup) {
+ for (Entry child : groupsToSearch) {
+ if (isMember(parentGroup, child.getDn())) {
+ LOG.ldapRecursiveGroupSearchCacheAdd(child.getDn().getName(),
parentGroup.getDn().getName());
+
resolvedParentsCache.computeIfAbsent(child.getDn().getNormName(), k -> new
HashSet<>()).add(parentGroup);
+ }
+ }
+ }
+
+ private boolean isMember(Entry group, Dn memberDn) {
+ return checkMemberAttribute(group, "member", memberDn) ||
checkMemberAttribute(group, "uniqueMember", memberDn);
+ }
+
+ private boolean checkMemberAttribute(Entry group, String attributeName, Dn
memberDn) {
+ final Attribute attr = group.get(attributeName);
+ if (attr != null) {
+ for (Value value : attr) {
+ try {
+ if (memberDn.equals(new Dn(value.getString()))) {
+ return true;
+ }
+ } catch (LdapException e) {
+ // Ignore invalid DNs in member attribute
+ }
+ }
+ }
+ return false;
+ }
+
+ private void logRecursiveSearchProgress(String username, List<Entry>
groups, int depth) {
+ LOG.ldapRecursiveGroupSearchProgress(username, groups.size(),
+ String.join(",", groups.stream().map(e ->
e.getDn().getRdn().getValue()).collect(Collectors.joining())),
+ depth);
}
private String extractGroupNameFromDn(String groupDn) {
@@ -381,13 +548,12 @@ public class LdapProxyBackend implements LdapBackend {
")";
}
- EntryCursor cursor = connection.search(groupSearchBase, filter,
SearchScope.SUBTREE, "cn");
-
- while (cursor.next()) {
- groups.add(cursor.get());
+ try (EntryCursor cursor = connection.search(groupSearchBase, filter,
SearchScope.SUBTREE, "cn")) {
+ while (cursor.next()) {
+ groups.add(cursor.get());
+ }
}
- cursor.close();
return groups;
}
@@ -406,29 +572,36 @@ public class LdapProxyBackend implements LdapBackend {
public List<Entry> searchUsers(String filter, SchemaManager schemaManager)
throws Exception {
List<Entry> results = new ArrayList<>();
LdapConnection connection = null;
+ Map<String, Set<Entry>> resolvedParentsCache =
createResolvedParentsCache();
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) {
- String username = idAttr.getString();
- Entry entry = createProxyEntry(sourceEntry, username,
connection, schemaManager);
- results.add(entry);
+ try (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, resolvedParentsCache);
+ results.add(entry);
+ }
}
}
-
- cursor.close();
return results;
} finally {
releaseConnection(connection);
}
}
+ /**
+ * Factory method for the resolved parents cache.
+ * Overridden in tests to verify caching behavior.
+ */
+ protected Map<String, Set<Entry>> createResolvedParentsCache() {
+ return new HashMap<>();
+ }
+
/**
* 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,
@@ -438,10 +611,11 @@ public class LdapProxyBackend implements LdapBackend {
* @param username The username for the entry
* @param connection The LDAP connection for fetching group information
* @param schemaManager The schema manager for creating entries
+ * @param resolvedParentsCache Cache for direct parent groups to avoid
redundant searches
* @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 {
+ private Entry createProxyEntry(Entry sourceEntry, String username,
LdapConnection connection, SchemaManager schemaManager, Map<String, Set<Entry>>
resolvedParentsCache) 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);
@@ -463,10 +637,10 @@ public class LdapProxyBackend implements LdapBackend {
copyAttribute(sourceEntry, entry, attributeType);
}
- if (useMemberOf) {
+ if (useMemberOf && !recursiveGroupResolution) {
copyAttribute(sourceEntry, entry,
proxyEntryGroupMembershipAttributeType);
} else {
- List<Entry> groups = getUserGroupsInternal(connection,
sourceEntry.getDn().toString(), username);
+ List<Entry> groups = getUserGroupsEntries(connection, username,
resolvedParentsCache);
for (Entry groupEntry : groups) {
entry.add(proxyEntryGroupMembershipAttributeType,
groupEntry.getDn().getName());
}
@@ -476,10 +650,10 @@ public class LdapProxyBackend implements LdapBackend {
}
private void copyAttribute(Entry source, Entry target, String
attributeName) throws LdapException {
- Attribute attr = source.get(attributeName);
- if (attr != null) {
+ final Attribute attribute = source.get(attributeName);
+ if (attribute != 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)
{
+ for (Value value : attribute) {
try {
target.add(attributeName, value.getString());
} catch (LdapException e) {
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/util/KnoxCLI.java
b/gateway-server/src/main/java/org/apache/knox/gateway/util/KnoxCLI.java
index 141f2e816..b1465a7af 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/util/KnoxCLI.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/util/KnoxCLI.java
@@ -82,6 +82,7 @@ import
org.apache.knox.gateway.services.security.MasterService;
import org.apache.knox.gateway.services.security.token.TokenMigrationTarget;
import org.apache.knox.gateway.services.security.token.TokenStateService;
import org.apache.knox.gateway.services.topology.TopologyService;
+import org.apache.knox.gateway.services.ldap.KnoxLDAPService;
import org.apache.knox.gateway.topology.Provider;
import org.apache.knox.gateway.topology.Topology;
import org.apache.knox.gateway.topology.validation.TopologyValidator;
@@ -302,6 +303,8 @@ public class KnoxCLI extends Configured implements Tool {
} else {
command = new LDAPAuthCommand();
}
+ } else if(args[i].equals("ldap-user-groups-test")) {
+ command = new LDAPGroupTestCommand();
} else if(args[i].equals("system-user-auth-test")) {
if (i + 1 >= args.length){
printKnoxShellUsage();
@@ -714,6 +717,30 @@ public class KnoxCLI extends Configured implements Tool {
return services.getService(ServiceType.REMOTE_REGISTRY_CLIENT_SERVICE);
}
+ protected String ensureNotNullUserName() {
+ return ensureNotNullUserName(null);
+ }
+
+ protected String ensureNotNullUserName(String userName) {
+ if (userName != null) {
+ return userName;
+ }
+
+ final Console c = System.console();
+ if (c != null) {
+ return c.readLine("Username: ");
+ } else {
+ try (InputStreamReader inputStreamReader = new
InputStreamReader(System.in, StandardCharsets.UTF_8);
+ BufferedReader reader = new BufferedReader(inputStreamReader)) {
+ out.println("Username: ");
+ return reader.readLine();
+ } catch (IOException e) {
+ out.println(e.toString());
+ return "";
+ }
+ }
+ }
+
}
private class AliasListCommand extends Command {
@@ -1763,37 +1790,26 @@ public class KnoxCLI extends Configured implements Tool
{
* populates the username and password members.
*/
protected void promptCredentials() {
- if(this.username == null){
- Console c = System.console();
- if( c != null) {
- this.username = c.readLine("Username: ");
- } else {
- try(InputStreamReader inputStreamReader = new
InputStreamReader(System.in, StandardCharsets.UTF_8);
- BufferedReader reader = new BufferedReader(inputStreamReader)) {
- out.println("Username: ");
- this.username = reader.readLine();
- } catch (IOException e){
- out.println(e.toString());
- this.username = "";
- }
- }
- }
+ this.username = ensureNotNullUserName(this.username);
+ populatePassword();
+ }
- if(this.password == null){
+ private void populatePassword() {
+ if (this.password == null) {
Console c = System.console();
- if( c != null) {
+ if (c != null) {
this.password = c.readPassword("Password: ");
- }else{
- try(InputStreamReader inputStreamReader = new
InputStreamReader(System.in, StandardCharsets.UTF_8);
- BufferedReader reader = new BufferedReader(inputStreamReader)) {
+ } else {
+ try (InputStreamReader inputStreamReader = new
InputStreamReader(System.in, StandardCharsets.UTF_8);
+ BufferedReader reader = new BufferedReader(inputStreamReader)) {
out.println("Password: ");
String pw = reader.readLine();
- if(pw != null){
+ if (pw != null) {
this.password = pw.toCharArray();
} else {
this.password = new char[0];
}
- } catch (IOException e){
+ } catch (IOException e) {
out.println(e.toString());
this.password = new char[0];
}
@@ -1868,6 +1884,49 @@ public class KnoxCLI extends Configured implements Tool {
}
}
+ private class LDAPGroupTestCommand extends Command {
+ public static final String USAGE = "ldap-user-groups-test [--u username]
[--d]";
+ public static final String DESC = """
+ This command tests the KnoxLDAPService ability to retrieve groups
for a user directly from the configured LDAP backend.
+ Optional: [--u username]: Provide a username argument to the
command""";
+
+ private String username;
+
+ @Override
+ public String getUsage() {
+ return USAGE + ":\n\n" + DESC;
+ }
+
+ @Override
+ public void execute() throws Exception {
+ if (user != null) {
+ this.username = user;
+ } else {
+ this.username = ensureNotNullUserName();
+ }
+
+ GatewayConfig config = getGatewayConfig();
+ if (!config.isLDAPEnabled()) {
+ out.println("KnoxLDAPService is not enabled in gateway-site.xml;
cannot lookup LDAP groups");
+ return;
+ }
+
+ try {
+ final KnoxLDAPService ldapService =
services.getService(ServiceType.LDAP_SERVICE);
+ out.println("Querying KnoxLDAPService for groups of user: " +
username);
+ List<String> groups = ldapService.getUserGroups(username);
+ if (groups == null || groups.isEmpty()) {
+ out.println(username + " does not belong to any groups");
+ } else {
+ out.println(username + " is a member of: " + String.join(", ",
groups));
+ }
+ } catch (Exception e) {
+ out.println("Error retrieving groups: " + e.getMessage());
+ throw e;
+ }
+ }
+ }
+
private class LDAPAuthCommand extends LDAPCommand {
public static final String USAGE = "user-auth-test [--cluster clustername]
[--u username] [--p password] [--g]";
diff --git a/gateway-server/src/main/resources/conf/gateway-site.xml
b/gateway-server/src/main/resources/conf/gateway-site.xml
index 9669a1941..ce7525e2d 100644
--- a/gateway-server/src/main/resources/conf/gateway-site.xml
+++ b/gateway-server/src/main/resources/conf/gateway-site.xml
@@ -56,6 +56,12 @@ limitations under the License.
<description>Base DN for LDAP entries in the proxy server. Default is
dc=proxy,dc=com.</description>
</property>
+ <property>
+ <name>gateway.ldap.recursive.group.resolution</name>
+ <value>false</value>
+ <desciption>Boolean value indicating whether recursive group
resolution is enabled in KnoxLDAPService.</desciption>
+ </property>
+
<property>
<name>gateway.ldap.backend.type</name>
<value>ldap</value>
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 fe045194b..40777b253 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
@@ -95,8 +95,7 @@ public class KnoxLDAPServiceTest {
@Test
public void testInitWithLdapEnabledFileBackend() throws Exception {
- setupMockConfigForFileBackend();
- replay(mockConfig);
+ setupMockConfig("file");
ldapService.init(mockConfig, new HashMap<>());
@@ -108,8 +107,7 @@ public class KnoxLDAPServiceTest {
@Test
public void testInitWithLdapEnabledLdapBackend() throws Exception {
- setupMockConfigForLdapBackend();
- replay(mockConfig);
+ setupMockConfig("ldap");
ldapService.init(mockConfig, new HashMap<>());
@@ -121,19 +119,13 @@ public class KnoxLDAPServiceTest {
@Test(expected = ServiceLifecycleException.class)
public void testInitWithInvalidBackendType() throws Exception {
- expect(mockConfig.isLDAPEnabled()).andReturn(true);
-
expect(mockConfig.getGatewayDataDir()).andReturn(tempDataDir.getAbsolutePath());
- expect(mockConfig.getLDAPPort()).andReturn(3890);
- expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com");
- expect(mockConfig.getLDAPBackendType()).andReturn("invalid");
- expect(mockConfig.getLDAPBackendConfig("invalid")).andReturn(new
HashMap<>());
- replay(mockConfig);
+ setupMockConfig("invalid");
ldapService.init(mockConfig, new HashMap<>());
}
@Test
- public void testStartWhenDisabled() throws Exception {
+ public void testStartAndStopWhenDisabled() throws Exception {
expect(mockConfig.isLDAPEnabled()).andReturn(false);
replay(mockConfig);
@@ -142,16 +134,6 @@ public class KnoxLDAPServiceTest {
// Should not throw exception
ldapService.start();
- verify(mockConfig);
- }
-
- @Test
- public void testStopWhenDisabled() throws Exception {
- expect(mockConfig.isLDAPEnabled()).andReturn(false);
- replay(mockConfig);
-
- ldapService.init(mockConfig, new HashMap<>());
-
// Should not throw exception
ldapService.stop();
@@ -167,17 +149,7 @@ public class KnoxLDAPServiceTest {
@Test
public void testOnGatewayConfigChanged() throws Exception {
- expect(mockConfig.isLDAPEnabled()).andReturn(true).anyTimes();
-
expect(mockConfig.getGatewayDataDir()).andReturn(tempDataDir.getAbsolutePath()).anyTimes();
-
expect(mockConfig.getLDAPPort()).andReturn(3890).times(1).andReturn(3891).anyTimes();
-
expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
- expect(mockConfig.getLDAPBackendType()).andReturn("file").anyTimes();
-
- Map<String, String> fileBackendConfig = new HashMap<>();
- fileBackendConfig.put("dataFile", tempLdapFile.getAbsolutePath());
-
expect(mockConfig.getLDAPBackendConfig("file")).andReturn(fileBackendConfig).anyTimes();
-
- replay(mockConfig);
+ setupMockConfig("file");
ldapService.init(mockConfig, new HashMap<>());
assertEquals("Initial port should be 3890", 3890,
ldapService.getLdapPort());
@@ -190,30 +162,28 @@ public class KnoxLDAPServiceTest {
verify(mockConfig);
}
- private void setupMockConfigForFileBackend() {
- expect(mockConfig.isLDAPEnabled()).andReturn(true);
-
expect(mockConfig.getGatewayDataDir()).andReturn(tempDataDir.getAbsolutePath());
- expect(mockConfig.getLDAPPort()).andReturn(3890);
- expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com");
- expect(mockConfig.getLDAPBackendType()).andReturn("file");
-
- Map<String, String> fileBackendConfig = new HashMap<>();
- fileBackendConfig.put("dataFile", tempLdapFile.getAbsolutePath());
-
expect(mockConfig.getLDAPBackendConfig("file")).andReturn(fileBackendConfig);
+ private void setupMockConfig(String backendType) {
+ expect(mockConfig.isLDAPEnabled()).andReturn(true).atLeastOnce();
+
expect(mockConfig.isLDAPRecursiveGroupResolutionEnabled()).andReturn(false).atLeastOnce();
+
expect(mockConfig.getLDAPRecursiveGroupResolutionMaxDepth()).andReturn(0).atLeastOnce();
+
expect(mockConfig.getGatewayDataDir()).andReturn(tempDataDir.getAbsolutePath()).atLeastOnce();
+
expect(mockConfig.getLDAPPort()).andReturn(3890).times(1).andReturn(3891).anyTimes();
+
expect(mockConfig.getLDAPBaseDN()).andReturn("file".equals(backendType) ?
"dc=test,dc=com" : "dc=proxy,dc=com").atLeastOnce();
+
expect(mockConfig.getLDAPBackendType()).andReturn(backendType).atLeastOnce();
+
expect(mockConfig.getLDAPBackendConfig(backendType)).andReturn(buildBackendConfig(backendType)).atLeastOnce();
+ replay(mockConfig);
}
- private void setupMockConfigForLdapBackend() {
- expect(mockConfig.isLDAPEnabled()).andReturn(true);
-
expect(mockConfig.getGatewayDataDir()).andReturn(tempDataDir.getAbsolutePath());
- expect(mockConfig.getLDAPPort()).andReturn(3890);
- expect(mockConfig.getLDAPBaseDN()).andReturn("dc=proxy,dc=com");
- expect(mockConfig.getLDAPBackendType()).andReturn("ldap");
-
- Map<String, String> ldapBackendConfig = new HashMap<>();
- ldapBackendConfig.put("url", "ldap://localhost:33389");
- ldapBackendConfig.put("remoteBaseDn", "dc=hadoop,dc=apache,dc=org");
- ldapBackendConfig.put("systemUsername",
"cn=admin,dc=hadoop,dc=apache,dc=org");
- ldapBackendConfig.put("systemPassword", "admin-password");
-
expect(mockConfig.getLDAPBackendConfig("ldap")).andReturn(ldapBackendConfig);
+ private Map<String, String> buildBackendConfig(String backendType) {
+ final Map<String, String> backendConfig = new HashMap<>();
+ if ("ldap".equals(backendType)) {
+ backendConfig.put("url", "ldap://localhost:33389");
+ backendConfig.put("remoteBaseDn", "dc=hadoop,dc=apache,dc=org");
+ backendConfig.put("systemUsername",
"cn=admin,dc=hadoop,dc=apache,dc=org");
+ backendConfig.put("systemPassword", "admin-password");
+ } else if ("file".equals(backendType)) {
+ backendConfig.put("dataFile", tempLdapFile.getAbsolutePath());
+ }
+ return backendConfig;
}
}
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 a6b132b35..0aba49487 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
@@ -46,11 +46,11 @@ import org.junit.Test;
import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
public class LdapProxyBackendTest {
private static Map<String, String> ldapBackendConfig;
@@ -99,13 +99,10 @@ public class LdapProxyBackendTest {
// Start the DirectoryService
directoryService.startup();
- // load test data from ldif file
- File ldifFile = new File(
- LdapProxyBackendTest.class.getResource(
"/ldap-proxy-backend-test.ldif" )
- .toURI());
+ // load test data from ldif files
CoreSession session = directoryService.getAdminSession();
- LdifFileLoader lfl = new LdifFileLoader(session, ldifFile, null);
- lfl.execute();
+ loadLdif(session, "/ldap-proxy-backend-test.ldif");
+ loadLdif(session, "/ldap-recursive-test.ldif");
// Create and start the LDAP server
ldapServer = new LdapServer();
@@ -124,6 +121,12 @@ public class LdapProxyBackendTest {
"groupSearchBase", "ou=groups,dc=hadoop,dc=apache,dc=org");
}
+ private static void loadLdif(CoreSession session, String ldifResource)
throws Exception {
+ File ldifFile = new
File(LdapProxyBackendTest.class.getResource(ldifResource).toURI());
+ LdifFileLoader lfl = new LdifFileLoader(session, ldifFile, null);
+ lfl.execute();
+ }
+
@AfterClass
public static void tearDownAfterClass() throws Exception {
if(ldapServer != null) {
@@ -150,21 +153,10 @@ public class LdapProxyBackendTest {
ldapProxyBackend.initialize(ldapBackendConfig);
Entry entry = ldapProxyBackend.getUser("ldaptest1", schemaManager);
- assertEquals("ldaptest1", entry.get("uid").getString());
- assertEquals("TestCn1", entry.get("cn").getString());
- assertEquals("[email protected]", entry.get("mail").getString());
- assertEquals("Test user ldaptest1",
entry.get("description").getString());
- assertNull(entry.get("sAMAccountName"));
- assertEquals(2, entry.get("memberOf").size());
- Set<String> expectedMemberOf = Set.of(
+ validateUserEntry(entry, "ldaptest1", "TestCn1",
"[email protected]", "Test user ldaptest1");
+ validateMemberOf(entry, Set.of(
"cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org",
- "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org");
- Set<String> foundMemberOf = new HashSet<>(2);
- Iterator<Value> memberOfs = entry.get("memberOf").iterator();
- while (memberOfs.hasNext()) {
- foundMemberOf.add(memberOfs.next().getString());
- }
- assertEquals(expectedMemberOf, foundMemberOf);
+ "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org"));
}
@Test
@@ -177,74 +169,39 @@ public class LdapProxyBackendTest {
@Test
public void testGetUserByUID() throws Exception {
- Map<String, String> config = new HashMap<>(ldapBackendConfig);
- config.put("userIdentifierAttribute", "uid");
+ Map<String, String> config = createConfigWithUserAttr("uid");
ldapProxyBackend.initialize(config);
Entry entry = ldapProxyBackend.getUser("ldaptest1", schemaManager);
- assertEquals("ldaptest1", entry.get("uid").getString());
- assertEquals("TestCn1", entry.get("cn").getString());
- assertEquals("[email protected]", entry.get("mail").getString());
- assertEquals("Test user ldaptest1",
entry.get("description").getString());
- assertNull(entry.get("sAMAccountName"));
- assertEquals(2, entry.get("memberOf").size());
- Set<String> expectedMemberOf = Set.of(
+ validateUserEntry(entry, "ldaptest1", "TestCn1",
"[email protected]", "Test user ldaptest1");
+ validateMemberOf(entry, Set.of(
"cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org",
- "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org");
- Set<String> foundMemberOf = new HashSet<>(2);
- Iterator<Value> memberOfs = entry.get("memberOf").iterator();
- while (memberOfs.hasNext()) {
- foundMemberOf.add(memberOfs.next().getString());
- }
- assertEquals(expectedMemberOf, foundMemberOf);
+ "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org"));
}
@Test
public void testGetUserByCN() throws Exception {
- Map<String, String> config = new HashMap<>(ldapBackendConfig);
- config.put("userIdentifierAttribute", "cn");
+ Map<String, String> config = createConfigWithUserAttr("cn");
ldapProxyBackend.initialize(config);
Entry entry = ldapProxyBackend.getUser("TestCn1", schemaManager);
- assertEquals("TestCn1", entry.get("uid").getString());
- assertEquals("TestCn1", entry.get("cn").getString());
- assertEquals("[email protected]", entry.get("mail").getString());
- assertEquals("Test user ldaptest1",
entry.get("description").getString());
- assertNull(entry.get("sAMAccountName"));
- assertEquals(2, entry.get("memberOf").size());
- Set<String> expectedMemberOf = Set.of(
+ validateUserEntry(entry, "TestCn1", "TestCn1",
"[email protected]", "Test user ldaptest1");
+ validateMemberOf(entry, Set.of(
"cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org",
- "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org");
- Set<String> foundMemberOf = new HashSet<>(2);
- Iterator<Value> memberOfs = entry.get("memberOf").iterator();
- while (memberOfs.hasNext()) {
- foundMemberOf.add(memberOfs.next().getString());
- }
- assertEquals(expectedMemberOf, foundMemberOf);
+ "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org"));
}
@Test
public void testGetUserBySAMAccountName() throws Exception {
- Map<String, String> config = new HashMap<>(ldapBackendConfig);
- config.put("userIdentifierAttribute", "sAMAccountName");
+ Map<String, String> config =
createConfigWithUserAttr("sAMAccountName");
ldapProxyBackend.initialize(config);
Entry entry = ldapProxyBackend.getUser("TestSam1", schemaManager);
- assertEquals("TestSam1", entry.get("uid").getString());
- assertEquals("TestCn1", entry.get("cn").getString());
- assertEquals("[email protected]", entry.get("mail").getString());
- assertEquals("Test user ldaptest1",
entry.get("description").getString());
+ validateUserEntry(entry, "TestSam1", "TestCn1",
"[email protected]", "Test user ldaptest1");
assertEquals("TestSam1", entry.get("sAMAccountName").getString());
- assertEquals(2, entry.get("memberOf").size());
- Set<String> expectedMemberOf = Set.of(
+ validateMemberOf(entry, Set.of(
"cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org",
- "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org");
- Set<String> foundMemberOf = new HashSet<>(2);
- Iterator<Value> memberOfs = entry.get("memberOf").iterator();
- while (memberOfs.hasNext()) {
- foundMemberOf.add(memberOfs.next().getString());
- }
- assertEquals(expectedMemberOf, foundMemberOf);
+ "cn=group2,ou=groups,dc=hadoop,dc=apache,dc=org"));
}
@Test
@@ -254,22 +211,10 @@ public class LdapProxyBackendTest {
ldapProxyBackend.initialize(config);
Entry entry = ldapProxyBackend.getUser("ldaptest2", schemaManager);
- assertEquals("ldaptest2", entry.get("uid").getString());
- assertEquals("TestCn2", entry.get("cn").getString());
- assertEquals("[email protected]", entry.get("mail").getString());
- assertEquals("Test user ldaptest2",
entry.get("description").getString());
- assertNull(entry.get("sAMAccountName"));
- assertEquals(2, entry.get("memberOf").size());
- Set<String> expectedMemberOf = Set.of(
+ validateUserEntry(entry, "ldaptest2", "TestCn2",
"[email protected]", "Test user ldaptest2");
+ validateMemberOf(entry, Set.of(
"cn=groupMemberOf1,ou=groups,dc=hadoop,dc=apache,dc=org",
- "cn=groupMemberOf2,ou=groups,dc=hadoop,dc=apache,dc=org");
- Set<String> foundMemberOf = new HashSet<>(2);
- Iterator<Value> memberOfs = entry.get("memberOf").iterator();
- while (memberOfs.hasNext()) {
- foundMemberOf.add(memberOfs.next().getString());
- }
- assertEquals(expectedMemberOf, foundMemberOf);
-
+ "cn=groupMemberOf2,ou=groups,dc=hadoop,dc=apache,dc=org"));
}
@Test
@@ -330,126 +275,205 @@ public class LdapProxyBackendTest {
@Test
public void testSearchUsers() throws Exception {
- // searches by uid by default
ldapProxyBackend.initialize(ldapBackendConfig);
-
- List<Entry> entries = ldapProxyBackend.searchUsers("*", schemaManager);
- Set<String> foundUids = new HashSet<>();
- for (Entry entry : entries) {
- foundUids.add(entry.get("uid").getString());
- }
- assertEquals(3, foundUids.size());
- assertTrue(foundUids.contains("ldaptest1"));
- assertTrue(foundUids.contains("ldaptest2"));
- assertTrue(foundUids.contains("guest"));
+ validateUserSearch("*", 3, Set.of("ldaptest1", "ldaptest2", "guest"));
}
@Test
public void testSearchUsersPartial() throws Exception {
- // searches by uid by default
ldapProxyBackend.initialize(ldapBackendConfig);
-
- List<Entry> entries = ldapProxyBackend.searchUsers("ldap*",
schemaManager);
- Set<String> foundUids = new HashSet<>();
- for (Entry entry : entries) {
- foundUids.add(entry.get("uid").getString());
- }
- assertEquals(2, foundUids.size());
- assertTrue(foundUids.contains("ldaptest1"));
- assertTrue(foundUids.contains("ldaptest2"));
+ validateUserSearch("ldap*", 2, Set.of("ldaptest1", "ldaptest2"));
}
@Test
public void testSearchUsersNoneFound() throws Exception {
- // searches by uid by default
ldapProxyBackend.initialize(ldapBackendConfig);
-
List<Entry> entries = ldapProxyBackend.searchUsers("nobody*",
schemaManager);
assertTrue(entries.isEmpty());
}
@Test
public void testSearchUsersByCn() throws Exception {
- Map<String, String> config = new HashMap<>(ldapBackendConfig);
- config.put("userIdentifierAttribute", "cn");
+ Map<String, String> config = createConfigWithUserAttr("cn");
ldapProxyBackend.initialize(config);
-
- List<Entry> entries = ldapProxyBackend.searchUsers("*", schemaManager);
- Set<String> foundUids = new HashSet<>();
- for (Entry entry : entries) {
- foundUids.add(entry.get("uid").getString());
- }
- assertEquals(3, foundUids.size());
- assertTrue(foundUids.contains("TestCn1"));
- assertTrue(foundUids.contains("TestCn2"));
- assertTrue(foundUids.contains("Guest"));
+ validateUserSearch("*", 3, Set.of("TestCn1", "TestCn2", "Guest"));
}
@Test
public void testSearchUsersPartialByCn() throws Exception {
- Map<String, String> config = new HashMap<>(ldapBackendConfig);
- config.put("userIdentifierAttribute", "cn");
+ Map<String, String> config = createConfigWithUserAttr("cn");
ldapProxyBackend.initialize(config);
-
- List<Entry> entries = ldapProxyBackend.searchUsers("TestCn*",
schemaManager);
- Set<String> foundUids = new HashSet<>();
- for (Entry entry : entries) {
- foundUids.add(entry.get("uid").getString());
- }
- assertEquals(2, foundUids.size());
- assertTrue(foundUids.contains("TestCn1"));
- assertTrue(foundUids.contains("TestCn2"));
+ validateUserSearch("TestCn*", 2, Set.of("TestCn1", "TestCn2"));
}
@Test
public void testSearchUsersNoneFoundByCn() throws Exception {
- Map<String, String> config = new HashMap<>(ldapBackendConfig);
- config.put("userIdentifierAttribute", "cn");
+ Map<String, String> config = createConfigWithUserAttr("cn");
ldapProxyBackend.initialize(config);
-
List<Entry> entries = ldapProxyBackend.searchUsers("nobody*",
schemaManager);
assertTrue(entries.isEmpty());
}
@Test
public void testSearchUsersBySAMAccountName() throws Exception {
- Map<String, String> config = new HashMap<>(ldapBackendConfig);
- config.put("userIdentifierAttribute", "sAMAccountName");
+ Map<String, String> config =
createConfigWithUserAttr("sAMAccountName");
ldapProxyBackend.initialize(config);
-
- List<Entry> entries = ldapProxyBackend.searchUsers("*", schemaManager);
- Set<String> foundUids = new HashSet<>();
- for (Entry entry : entries) {
- foundUids.add(entry.get("uid").getString());
- }
- assertEquals(2, foundUids.size());
- assertTrue(foundUids.contains("TestSam1"));
- assertTrue(foundUids.contains("TestSam2"));
+ validateUserSearch("*", 2, Set.of("TestSam1", "TestSam2"));
}
@Test
public void testSearchUsersPartialBySAMAccountName() throws Exception {
- Map<String, String> config = new HashMap<>(ldapBackendConfig);
- config.put("userIdentifierAttribute", "sAMAccountName");
+ Map<String, String> config =
createConfigWithUserAttr("sAMAccountName");
ldapProxyBackend.initialize(config);
-
- List<Entry> entries = ldapProxyBackend.searchUsers("TestSam*",
schemaManager);
- Set<String> foundUids = new HashSet<>();
- for (Entry entry : entries) {
- foundUids.add(entry.get("uid").getString());
- }
- assertEquals(2, foundUids.size());
- assertTrue(foundUids.contains("TestSam1"));
- assertTrue(foundUids.contains("TestSam2"));
+ validateUserSearch("TestSam*", 2, Set.of("TestSam1", "TestSam2"));
}
@Test
public void testSearchUsersNoneFoundBySAMAccountName() throws Exception {
- Map<String, String> config = new HashMap<>(ldapBackendConfig);
- config.put("userIdentifierAttribute", "sAMAccountName");
+ Map<String, String> config =
createConfigWithUserAttr("sAMAccountName");
ldapProxyBackend.initialize(config);
-
List<Entry> entries = ldapProxyBackend.searchUsers("nobody*",
schemaManager);
assertTrue(entries.isEmpty());
}
-}
\ No newline at end of file
+
+ @Test
+ public void testGetRecursiveUserGroupsDepth2() throws Exception {
+ Map<String, String> config = createRecursiveConfig(2);
+ ldapProxyBackend.initialize(config);
+
+ List<String> userGroups =
ldapProxyBackend.getUserGroups("recursiveUser");
+ assertEquals(4, userGroups.size());
+ assertTrue(userGroups.contains("level1Group"));
+ assertTrue(userGroups.contains("level2Group"));
+ assertTrue(userGroups.contains("cycleGroupA"));
+ assertTrue(userGroups.contains("cycleGroupB"));
+ }
+
+ @Test
+ public void testGetRecursiveUserGroupsDepth4() throws Exception {
+ Map<String, String> config = createRecursiveConfig(4);
+ ldapProxyBackend.initialize(config);
+
+ List<String> userGroups =
ldapProxyBackend.getUserGroups("recursiveUser");
+ assertEquals(6, userGroups.size());
+ assertTrue(userGroups.contains("level1Group"));
+ assertTrue(userGroups.contains("level2Group"));
+ assertTrue(userGroups.contains("level3Group"));
+ assertTrue(userGroups.contains("level4Group"));
+ assertTrue(userGroups.contains("cycleGroupA"));
+ assertTrue(userGroups.contains("cycleGroupB"));
+ }
+
+ @Test
+ public void testGetRecursiveUserGroupsWithCycle() throws Exception {
+ Map<String, String> config = createRecursiveConfig(10);
+ ldapProxyBackend.initialize(config);
+
+ List<String> userGroups =
ldapProxyBackend.getUserGroups("recursiveUser");
+ assertTrue(userGroups.contains("cycleGroupA"));
+ assertTrue(userGroups.contains("cycleGroupB"));
+ }
+
+ @Test
+ public void testGetUserRecursiveGroups() throws Exception {
+ Map<String, String> config = createRecursiveConfig(5);
+ ldapProxyBackend.initialize(config);
+
+ Entry entry = ldapProxyBackend.getUser("recursiveUser", schemaManager);
+ validateMemberOf(entry, Set.of(
+ "cn=level1Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
+ "cn=level2Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
+ "cn=level3Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
+ "cn=level4Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
+ "cn=cycleGroupA,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
+
"cn=cycleGroupB,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org"));
+ }
+
+ @Test
+ public void testSearchUsersRecursiveWithSharedGroups() throws Exception {
+ Map<String, String> config = createRecursiveConfig(5);
+
+ final AtomicInteger cacheHits = new AtomicInteger(0);
+ ldapProxyBackend = new LdapProxyBackend() {
+ @Override
+ protected Map<String, Set<Entry>> createResolvedParentsCache() {
+ return new HashMap<>() {
+ @Override
+ public Set<
org.apache.directory.api.ldap.model.entry.Entry> get(Object key) {
+ if (super.get(key) != null) {
+ cacheHits.incrementAndGet();
+ }
+ return super.get(key);
+ }
+ };
+ }
+ };
+ ldapProxyBackend.initialize(config);
+
+ // Search for all recursive users (recursiveUser and recursiveUser2)
+ // They share level1Group, cycleGroupA, and all their ancestors.
+ List<Entry> entries = ldapProxyBackend.searchUsers("recursiveUser*",
schemaManager);
+ assertEquals(2, entries.size());
+
+ Set<String> expectedGroups = Set.of(
+ "cn=level1Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
+ "cn=level2Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
+ "cn=level3Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
+ "cn=level4Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
+ "cn=cycleGroupA,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org",
+
"cn=cycleGroupB,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org");
+
+ for (Entry entry : entries) {
+ validateMemberOf(entry, expectedGroups);
+ }
+
+ // Verify that caching actually happened.
+ // For the second user, many groups should have been found in the
cache.
+ assertEquals("Expected 6 cache hits for shared groups, but got " +
cacheHits.get(), 6, cacheHits.get());
+ }
+
+ // Helper methods for refactoring
+
+ private Map<String, String> createConfigWithUserAttr(String attr) {
+ Map<String, String> config = new HashMap<>(ldapBackendConfig);
+ config.put("userIdentifierAttribute", attr);
+ return config;
+ }
+
+ private Map<String, String> createRecursiveConfig(int depth) {
+ Map<String, String> config = new HashMap<>(ldapBackendConfig);
+ config.put("recursiveGroupResolution", "true");
+ config.put("recursiveGroupResolutionMaxDepth", String.valueOf(depth));
+ config.put("userSearchBase",
"ou=recursivePeople,dc=hadoop,dc=apache,dc=org");
+ config.put("groupSearchBase",
"ou=recursiveGroups,dc=hadoop,dc=apache,dc=org");
+ return config;
+ }
+
+ private void validateUserEntry(Entry entry, String expectedUid, String
expectedCn, String expectedMail, String expectedDesc) throws Exception {
+ assertEquals(expectedUid, entry.get("uid").getString());
+ assertEquals(expectedCn, entry.get("cn").getString());
+ assertEquals(expectedMail, entry.get("mail").getString());
+ assertEquals(expectedDesc, entry.get("description").getString());
+ }
+
+ private void validateMemberOf(Entry entry, Set<String> expectedGroups)
throws Exception {
+ assertEquals(expectedGroups.size(), entry.get("memberOf").size());
+ Set<String> foundGroups = new HashSet<>();
+ for (Value value : entry.get("memberOf")) {
+ foundGroups.add(value.getString());
+ }
+ assertEquals(expectedGroups, foundGroups);
+ }
+
+ private void validateUserSearch(String filter, int expectedSize,
Set<String> expectedUids) throws Exception {
+ List<Entry> entries = ldapProxyBackend.searchUsers(filter,
schemaManager);
+ assertEquals(expectedSize, entries.size());
+ Set<String> foundUids = new HashSet<>();
+ for (Entry entry : entries) {
+ foundUids.add(entry.get("uid").getString());
+ }
+ for (String uid : expectedUids) {
+ assertTrue("Expected UID " + uid + " not found",
foundUids.contains(uid));
+ }
+ }
+}
diff --git a/gateway-server/src/test/resources/ldap-recursive-test.ldif
b/gateway-server/src/test/resources/ldap-recursive-test.ldif
new file mode 100644
index 000000000..e6182e0dd
--- /dev/null
+++ b/gateway-server/src/test/resources/ldap-recursive-test.ldif
@@ -0,0 +1,95 @@
+# 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
+
+# entry for recursive people container
+dn: ou=recursivePeople,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass:organizationalUnit
+ou: recursivePeople
+
+# entry for recursive group container
+dn: ou=recursiveGroups,dc=hadoop,dc=apache,dc=org
+objectClass: top
+objectClass: organizationalUnit
+ou: recursiveGroups
+
+# entry for the end user
+dn: uid=recursiveUser,ou=recursivePeople,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass:person
+objectclass:organizationalPerson
+objectclass:inetOrgPerson
+cn: RecursiveUser
+sn: User
+uid: recursiveUser
+userPassword: password
+
+# entry for the second end user (for cache testing)
+dn: uid=recursiveUser2,ou=recursivePeople,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass:person
+objectclass:organizationalPerson
+objectclass:inetOrgPerson
+cn: RecursiveUser2
+sn: User2
+uid: recursiveUser2
+userPassword: password
+
+# Level 1 Group
+dn: cn=level1Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass:groupOfNames
+cn: level1Group
+member: uid=recursiveUser,ou=recursivePeople,dc=hadoop,dc=apache,dc=org
+member: uid=recursiveUser2,ou=recursivePeople,dc=hadoop,dc=apache,dc=org
+
+# Level 2 Group
+dn: cn=level2Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass:groupOfNames
+cn: level2Group
+member: cn=level1Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org
+
+# Level 3 Group
+dn: cn=level3Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass:groupOfNames
+cn: level3Group
+member: cn=level2Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org
+
+# Level 4 Group
+dn: cn=level4Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass:groupOfNames
+cn: level4Group
+member: cn=level3Group,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org
+
+# Cycle: Group A -> Group B -> Group A
+dn: cn=cycleGroupA,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass:groupOfNames
+cn: cycleGroupA
+member: uid=recursiveUser,ou=recursivePeople,dc=hadoop,dc=apache,dc=org
+member: uid=recursiveUser2,ou=recursivePeople,dc=hadoop,dc=apache,dc=org
+member: cn=cycleGroupB,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org
+
+dn: cn=cycleGroupB,ou=recursiveGroups,dc=hadoop,dc=apache,dc=org
+objectclass:top
+objectclass:groupOfNames
+cn: cycleGroupB
+member: cn=cycleGroupA,ou=recursiveGroups,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 ec22c1dc9..80ab4e0c8 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
@@ -1265,6 +1265,16 @@ public class GatewayTestConfig extends Configuration
implements GatewayConfig {
return Collections.emptyMap();
}
+ @Override
+ public boolean isLDAPRecursiveGroupResolutionEnabled() {
+ return false;
+ }
+
+ @Override
+ public int getLDAPRecursiveGroupResolutionMaxDepth() {
+ 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 cc174f62a..6116a3d4c 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_RECURSIVE_GROUP_RESOLUTION_MAX_DEPTH =
"gateway.ldap.recursive.group.resolution.max.depth";
/**
* The location of the gateway configuration.
@@ -1087,6 +1089,16 @@ public interface GatewayConfig {
*/
Map<String, String> getLDAPBackendConfig(String backendType);
+ /**
+ * @return true if recursive group resolution is enabled for LDAP service
+ */
+ boolean isLDAPRecursiveGroupResolutionEnabled();
+
+ /**
+ * @return the maximum depth for recursive group search
+ */
+ int getLDAPRecursiveGroupResolutionMaxDepth();
+
/**
* @return set of all property names in the configuration
*/
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/security/principal/SimplePrincipalMapper.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/security/principal/SimplePrincipalMapper.java
index 12b97c906..090cb7b9e 100644
---
a/gateway-spi/src/main/java/org/apache/knox/gateway/security/principal/SimplePrincipalMapper.java
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/security/principal/SimplePrincipalMapper.java
@@ -31,10 +31,8 @@ public class SimplePrincipalMapper implements
PrincipalMapper {
@Override
public void loadMappingTable(String principalMapping, String groupMapping)
throws PrincipalMappingException {
- if (principalMapping != null) {
principalMappings = parseMapping(principalMapping);
groupMappings = parseMapping(groupMapping);
- }
}
private Map<String, String[]> parseMapping(String mappings)