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

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

commit 5b5246b8f6a5b5b92ca1c09450bb5fba0110b247
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>(&amp;(|(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 c148b6865..999f82403 100644
--- a/gateway-release/home/conf/users.ldif
+++ b/gateway-release/home/conf/users.ldif
@@ -116,3 +116,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)


Reply via email to