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 97d22d24970e7c0bebfac22ec7b8ec95dbf0f92e
Author: Sandor Molnar <[email protected]>
AuthorDate: Tue Jun 9 18:07:10 2026 +0200

    KNOX-3337: Add optional LDAP role lookup support (#1247)
---
 .../org/apache/knox/gateway/GatewayServer.java     |   4 +
 .../gateway/config/impl/GatewayConfigImpl.java     |  15 +++
 .../knox/gateway/services/CLIGatewayServices.java  |  11 +-
 .../gateway/services/DefaultGatewayServices.java   |   2 +
 .../factory/LDAPRolesLookupServiceFactory.java     |  54 ++++++++
 .../services/factory/LdapServiceFactory.java       |   5 +-
 .../services/ldap/KnoxLDAPServerManager.java       |  27 ++--
 .../gateway/services/ldap/KnoxLDAPService.java     |   3 +-
 .../knox/gateway/services/ldap/LdapMessages.java   |  20 ++-
 .../knox/gateway/services/ldap/LdapUtils.java      |  31 +++++
 .../services/ldap/backend/LdapProxyBackend.java    |   2 +-
 .../DuplicateUserFilteringInterceptorFactory.java  |   3 +-
 .../ldap/interceptor/InterceptorFactory.java       |   8 +-
 .../interceptor/KnoxLdapInterceptorFactory.java    |   6 +-
 .../interceptor/LDAPRolesLookupInterceptor.java    | 146 +++++++++++++++++++++
 .../LDAPRolesLookupInterceptorFactory.java         |  50 +++++++
 .../ldap/interceptor/UserSearchInterceptor.java    |   2 +-
 .../interceptor/UserSearchInterceptorFactory.java  |   5 +-
 .../ldap/roles/DefaultLDAPRolesLookupService.java  |  74 +++++++++++
 .../ldap/roles/FileBasedLdapRolesLookup.java       | 123 +++++++++++++++++
 .../LdapRolesLookup.java}                          |  28 ++--
 .../ldap/roles/LdapRolesLookupFactory.java         |  52 ++++++++
 .../LookupRolesRequest.java}                       |  38 ++++--
 .../LookupRolesResponse.java}                      |  39 ++++--
 .../ldap/roles/RestApiLdapRolesLookup.java         |  88 +++++++++++++
 .../RoleLookupException.java}                      |  22 +---
 ...org.apache.knox.gateway.services.ServiceFactory |   1 +
 ...ces.ldap.interceptor.KnoxLdapInterceptorFactory |   1 +
 .../org/apache/knox/gateway/GatewayServerTest.java |   6 +
 .../services/AbstractGatewayServicesTest.java      |   1 +
 .../gateway/services/ldap/KnoxLDAPServiceTest.java |  12 +-
 .../LDAPRolesLookupInterceptorFactoryTest.java     |  87 ++++++++++++
 .../LDAPRolesLookupInterceptorTest.java            |  93 +++++++++++++
 .../ldap/roles/FileBasedLdapRolesLookupTest.java   | 110 ++++++++++++++++
 .../ldap/roles/RestApiLdapRolesLookupTest.java     |  90 +++++++++++++
 .../gateway/service/auth/AbstractAuthResource.java |  32 ++++-
 .../knox/gateway/service/auth/AuthMessages.java    |   4 +
 .../gateway/service/auth/ExtAuthzResourceTest.java |  38 ++++++
 .../gateway/service/auth/PreAuthResourceTest.java  |  40 ++++++
 .../org/apache/knox/gateway/GatewayTestConfig.java |  15 +++
 .../apache/knox/gateway/config/GatewayConfig.java  |  18 +++
 .../apache/knox/gateway/services/ServiceType.java  |   1 +
 .../services/ldap/LDAPRolesLookupService.java      |  27 ++--
 .../knox/gateway/services/ldap/RoleAssignment.java |  61 +++++++++
 knox-site/docs/service_ldap_server.md              |  15 ++-
 45 files changed, 1398 insertions(+), 112 deletions(-)

diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java 
b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
index a39e5e6c1..c5db37cc9 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayServer.java
@@ -251,6 +251,10 @@ public class GatewayServer {
     configChangeListeners.remove(listener);
   }
 
+  static void emptyConfigChangeListener() {
+    configChangeListeners.clear();
+  }
+
   private static void setupGatewayConfigRefresh(GatewayConfigImpl config) {
     Path resourcePath = Paths.get(config.getGatewayConfDir(), 
RELOADABLE_CONFIG_FILENAME);
     int refreshInterval = config.getConfigRefreshInterval();
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 995fa81be..a8aa19fcb 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
@@ -1808,6 +1808,21 @@ public class GatewayConfigImpl extends Configuration 
implements GatewayConfig {
     return getInt(LDAP_RECURSIVE_GROUP_RESOLUTION_MAX_DEPTH, 3);
   }
 
+  @Override
+  public String getLdapRolesLookupStrategy() {
+    return get(LDAP_ROLES_LOOKUP_STRATEGY);
+  }
+
+  @Override
+  public String getLdapRolesLookupRestApiEndpoint() {
+    return get(LDAP_ROLES_LOOKUP_REST_API_ENDPOINT);
+  }
+
+  @Override
+  public String getLdapRolesLookupFilePath() {
+    return get(LDAP_ROLES_LOOKUP_FILE_PATH);
+  }
+
   @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 9c2ea6e62..23ac4ea77 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,7 +25,6 @@ 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 {
@@ -57,12 +56,10 @@ public class CLIGatewayServices extends 
AbstractGatewayServices {
 
     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);
-    }
+    addService(ServiceType.LDAP_ROLES_LOOKUP_SERVICE, 
gatewayServiceFactory.create(this, ServiceType.LDAP_ROLES_LOOKUP_SERVICE, 
config, options));
+
+    addService(ServiceType.LDAP_SERVICE, gatewayServiceFactory.create(this, 
ServiceType.LDAP_SERVICE, config, options));
+
   }
 
   @Override
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
index 131d2e1c1..2d7d12f13 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
@@ -83,6 +83,8 @@ public class DefaultGatewayServices extends 
AbstractGatewayServices {
 
     addService(ServiceType.GATEWAY_STATUS_SERVICE, 
gatewayServiceFactory.create(this, ServiceType.GATEWAY_STATUS_SERVICE, config, 
options));
 
+    addService(ServiceType.LDAP_ROLES_LOOKUP_SERVICE,  
gatewayServiceFactory.create(this, ServiceType.LDAP_ROLES_LOOKUP_SERVICE, 
config, options));
+
     addService(ServiceType.LDAP_SERVICE, gatewayServiceFactory.create(this, 
ServiceType.LDAP_SERVICE, config, options));
 
     addService(ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE, 
gatewayServiceFactory.create(this, 
ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE, config, options));
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/LDAPRolesLookupServiceFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/LDAPRolesLookupServiceFactory.java
new file mode 100644
index 000000000..f285845d4
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/LDAPRolesLookupServiceFactory.java
@@ -0,0 +1,54 @@
+/*
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+package org.apache.knox.gateway.services.factory;
+
+import org.apache.knox.gateway.GatewayMessages;
+import org.apache.knox.gateway.GatewayServer;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.Service;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.ServiceType;
+import 
org.apache.knox.gateway.services.ldap.roles.DefaultLDAPRolesLookupService;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public class LDAPRolesLookupServiceFactory extends AbstractServiceFactory {
+
+    private static final GatewayMessages LOG = 
MessagesFactory.get(GatewayMessages.class);
+
+    @Override
+    protected Service createService(GatewayServices gatewayServices, 
ServiceType serviceType, GatewayConfig gatewayConfig, Map<String, String> 
options, String implementation) throws ServiceLifecycleException {
+        final DefaultLDAPRolesLookupService ldapRolesLookupService = new 
DefaultLDAPRolesLookupService();
+        GatewayServer.registerConfigChangeListener(ldapRolesLookupService);
+        logServiceUsage(ldapRolesLookupService.getClass().getName(), 
serviceType);
+        return ldapRolesLookupService;
+    }
+
+    @Override
+    protected ServiceType getServiceType() {
+        return ServiceType.LDAP_ROLES_LOOKUP_SERVICE;
+    }
+
+    @Override
+    protected Collection<String> getKnownImplementations() {
+        return List.of(DefaultLDAPRolesLookupService.class.getName());
+    }
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/LdapServiceFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/LdapServiceFactory.java
index a02aa2b53..c68ea5d9d 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/LdapServiceFactory.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/LdapServiceFactory.java
@@ -34,10 +34,11 @@ public class LdapServiceFactory extends 
AbstractServiceFactory {
     @Override
     protected Service createService(GatewayServices gatewayServices, 
ServiceType serviceType, GatewayConfig gatewayConfig, Map<String, String> 
options,
                                     String implementation) throws 
ServiceLifecycleException {
-        Service service = null;
+        KnoxLDAPService service = null;
         if (shouldCreateService(implementation)) {
             service = new KnoxLDAPService();
-            GatewayServer.registerConfigChangeListener((KnoxLDAPService) 
service);
+            GatewayServer.registerConfigChangeListener(service);
+            logServiceUsage(service.getClass().getName(), serviceType);
         }
         return service;
     }
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 f56b98529..2cee21626 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
@@ -53,6 +53,7 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.IntStream;
 
 /**
  * Manages the ApacheDS LDAP server instance with pluggable backends
@@ -104,25 +105,22 @@ public class KnoxLDAPServerManager {
         List<Interceptor> interceptors = new 
ArrayList<>(interceptorNames.size());
         for (String interceptorName : interceptorNames) {
             // Get backend-specific configuration using prefixed properties
-            Map<String, String> interceptorConfig = 
config.getLDAPInterceptorConfig(interceptorName);
+            final Map<String, String> interceptorConfig = 
config.getLDAPInterceptorConfig(interceptorName);
 
             // Add common configuration
             interceptorConfig.put("baseDn", baseDn);
 
             // Add common LDAP Proxy configurations to backends
-            String interceptorType = interceptorConfig.get("interceptorType");
-            String backendType = interceptorConfig.get("backendType");
-            if ("backend".equalsIgnoreCase(interceptorType)) {
+            if 
("backend".equalsIgnoreCase(interceptorConfig.get("interceptorType"))) {
                 interceptorConfig.put("recursiveGroupResolution", 
String.valueOf(config.isLDAPRecursiveGroupResolutionEnabled()));
                 interceptorConfig.put("recursiveGroupResolutionMaxDepth", 
String.valueOf(config.getLDAPRecursiveGroupResolutionMaxDepth()));
-                if ("file".equalsIgnoreCase(backendType) &&
-                        !interceptorConfig.containsKey("dataFile")) {
+                if 
("file".equalsIgnoreCase(interceptorConfig.get("backendType")) && 
!interceptorConfig.containsKey("dataFile")) {
                     // Add legacy dataFile property for backwards 
compatibility with file backend
                     interceptorConfig.put("dataFile", 
config.getLDAPBackendDataFile());
                 }
             }
 
-            
interceptors.add(InterceptorFactory.createInterceptor(interceptorName, 
interceptorConfig));
+            interceptors.add(InterceptorFactory.createInterceptor(config, 
interceptorName, interceptorConfig));
         }
         this.interceptors = interceptors;
     }
@@ -224,13 +222,7 @@ public class KnoxLDAPServerManager {
         // Find location of AuthenticationInterceptor.
         // We need to insert interceptors before AuthenticationInterceptor to 
intercept bind requests
         final List<Interceptor> dsInterceptors = new 
ArrayList<>(directoryService.getInterceptors());
-        int authIdx = -1;
-        for (int i = 0; i < dsInterceptors.size(); i++) {
-            if 
(dsInterceptors.get(i).getName().equalsIgnoreCase("authenticationInterceptor")) 
{
-                authIdx = i;
-                break;
-            }
-        }
+        final int authIdx = 
fetchAuthenticationInterceptorIndex(dsInterceptors);
 
         // Add our configured interceptors for group lookups and bind proxying
         for (Interceptor interceptor : interceptors) {
@@ -243,6 +235,13 @@ public class KnoxLDAPServerManager {
         directoryService.setInterceptors(dsInterceptors);
     }
 
+    private int fetchAuthenticationInterceptorIndex(final List<Interceptor> 
dsInterceptors) {
+        return IntStream.range(0, dsInterceptors.size())
+                .filter(i -> 
"authenticationInterceptor".equalsIgnoreCase(dsInterceptors.get(i).getName()))
+                .findFirst()
+                .orElse(-1);
+    }
+
     /**
      * Stop the LDAP server
      */
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 66bcfdcb2..0859935c1 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
@@ -33,7 +33,7 @@ import java.util.Map;
 public class KnoxLDAPService implements Service, GatewayConfigChangeListener {
     private static final LdapMessages LOG = 
MessagesFactory.get(LdapMessages.class);
 
-    private KnoxLDAPServerManager ldapServerManager;
+    KnoxLDAPServerManager ldapServerManager;
     private boolean enabled;
 
     @Override
@@ -93,6 +93,7 @@ public class KnoxLDAPService implements Service, 
GatewayConfigChangeListener {
                 ldapServerManager.stop();
                 ldapServerManager.initialize(config);
                 ldapServerManager.start();
+                //LDAP roles lookup service also implements 
onGatewayConfigChanged -> no need to do anything here
             } else if (ldapServerManager != null) {
                 ldapServerManager.stop();
                 ldapServerManager = null;
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 947581247..506bc6ba3 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
@@ -58,7 +58,7 @@ public interface LdapMessages {
     void ldapInterceptorTypeNotFound(String interceptorName);
 
     @Message(level = MessageLevel.INFO,
-            text = "Creating interceptor: {0} (via {1})")
+            text = "Creating LDAP interceptor: {0} (via {1})")
     void ldapInterceptorCreating(String interceptorName, String source);
 
     @Message(level = MessageLevel.INFO,
@@ -154,4 +154,22 @@ public interface LdapMessages {
 
     @Message(level = MessageLevel.DEBUG, text = "Added parent {1} to cache for 
group {0}")
     void ldapRecursiveGroupSearchCacheAdd(String groupDn, String parentDn);
+
+    @Message(level = MessageLevel.INFO, text = "Reloading LDAP roles lookup 
configuration...")
+    void ldapRolesLookupReloadingConfig();
+
+    @Message(level = MessageLevel.ERROR, text = "Failed to reload LDAP roles 
lookup: {0}")
+    void ldapRolesLookupReloadFailed(@StackTrace(level = MessageLevel.DEBUG) 
Exception e);
+
+    @Message(level = MessageLevel.INFO, text = "LDAP roles lookup is enabled 
with strategy: {0}")
+    void ldapRolesLookupEnabled(String strategy);
+
+    @Message(level = MessageLevel.INFO, text = "LDAP roles lookup is disabled")
+    void ldapRolesLookupDisabled();
+
+    @Message(level = MessageLevel.DEBUG, text = "LDAP roles lookup for user 
{0} and groups {1} returned roles: {2}")
+    void ldapRolesLookupResult(String user, String groups, String roles);
+
+    @Message(level = MessageLevel.ERROR, text = "Failed to lookup roles for 
user {0}: {1}")
+    void ldapRolesLookupFailed(String user, @StackTrace(level = 
MessageLevel.DEBUG) Exception e);
 }
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapUtils.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapUtils.java
index 117c778ea..3b2ad14b6 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapUtils.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapUtils.java
@@ -16,7 +16,11 @@
  */
 package org.apache.knox.gateway.services.ldap;
 
+import org.apache.directory.api.ldap.model.entry.Attribute;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.exception.LdapException;
 import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.api.ldap.model.name.Rdn;
 
 public class LdapUtils {
 
@@ -33,4 +37,31 @@ public class LdapUtils {
             return null;
         }
     }
+
+    public static String extractUsernameFromEntry(Entry entry, String... 
attributeNames) {
+        String userName = null;
+        for (String attributeName : attributeNames) {
+            Attribute attribute = entry.get(attributeName);
+            if (attribute != null) {
+                try {
+                    userName = attribute.getString();
+                    if (userName != null) {
+                        break;
+                    }
+                } catch (LdapException ignored) {
+                }
+            }
+        }
+        return userName;
+    }
+
+    public static String extractGroupName(Dn dn) {
+        if (!dn.isEmpty()) {
+            Rdn rdn = dn.getRdn();
+            if (rdn.getType().equalsIgnoreCase("cn")) {
+                return rdn.getValue();
+            }
+        }
+        return null;
+    }
 }
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 07337b049..d92603270 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
@@ -525,7 +525,7 @@ public class LdapProxyBackend implements LdapBackend {
 
     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())),
+                groups.stream().map(e -> 
e.getDn().getRdn().getValue()).collect(Collectors.joining(",")),
                 depth);
     }
 
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorFactory.java
index b443c2def..9af5c58cd 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorFactory.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorFactory.java
@@ -18,6 +18,7 @@
 package org.apache.knox.gateway.services.ldap.interceptor;
 
 import org.apache.directory.server.core.api.interceptor.Interceptor;
+import org.apache.knox.gateway.config.GatewayConfig;
 
 import java.util.Map;
 
@@ -25,7 +26,7 @@ public class DuplicateUserFilteringInterceptorFactory 
implements KnoxLdapInterce
     public static final String TYPE = "duplicateuserfilter";
 
     @Override
-    public Interceptor create(String name, Map<String, String> config) {
+    public Interceptor create(GatewayConfig gatewayConfig, String name, 
Map<String, String> interceptorConfig) {
         return new DuplicateUserFilteringInterceptor(name);
     }
 
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/InterceptorFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/InterceptorFactory.java
index b2f5525a5..3adab3a5a 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/InterceptorFactory.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/InterceptorFactory.java
@@ -18,6 +18,7 @@
 package org.apache.knox.gateway.services.ldap.interceptor;
 
 import org.apache.directory.server.core.api.interceptor.Interceptor;
+import org.apache.knox.gateway.config.GatewayConfig;
 import org.apache.knox.gateway.i18n.messages.MessagesFactory;
 import org.apache.knox.gateway.services.ldap.LdapMessages;
 
@@ -32,8 +33,9 @@ import java.util.ServiceLoader;
 public class InterceptorFactory {
     private static final LdapMessages LOG = 
MessagesFactory.get(LdapMessages.class);
 
-    public static Interceptor createInterceptor(String interceptorName, 
Map<String, String> config) throws Exception {
-        String interceptorType = config.get("interceptorType");
+    public static Interceptor createInterceptor(final GatewayConfig 
gatewayConfig, final String interceptorName,
+                                                final Map<String, String> 
interceptorConfig) throws Exception {
+        final String interceptorType = 
interceptorConfig.get("interceptorType");
         if (interceptorType == null) {
             // No backend type configured found
             LOG.ldapInterceptorTypeNotFound(interceptorName);
@@ -47,7 +49,7 @@ public class InterceptorFactory {
         for (KnoxLdapInterceptorFactory interceptorFactory : loader) {
             if 
(interceptorFactory.getType().equalsIgnoreCase(interceptorType)) {
                 LOG.ldapInterceptorCreating(interceptorType, "ServiceLoader");
-                return interceptorFactory.create(interceptorName, config);
+                return interceptorFactory.create(gatewayConfig, 
interceptorName, interceptorConfig);
             }
         }
 
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/KnoxLdapInterceptorFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/KnoxLdapInterceptorFactory.java
index 2eb4fb5f9..86964f32f 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/KnoxLdapInterceptorFactory.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/KnoxLdapInterceptorFactory.java
@@ -18,6 +18,7 @@
 package org.apache.knox.gateway.services.ldap.interceptor;
 
 import org.apache.directory.server.core.api.interceptor.Interceptor;
+import org.apache.knox.gateway.config.GatewayConfig;
 
 import java.util.Map;
 
@@ -27,11 +28,12 @@ import java.util.Map;
 public interface KnoxLdapInterceptorFactory {
     /**
      * Instantiate and interceptor
+     * @param gatewayConfig the Knox Gateway configuration
      * @param name the name of the interceptor
-     * @param config the configuration for the interceptor
+     * @param interceptorConfig the configuration for the interceptor
      * @return the interceptor
      */
-    Interceptor create(String name, Map<String, String> config) throws 
Exception;
+    Interceptor create(GatewayConfig gatewayConfig, String name, Map<String, 
String> interceptorConfig) throws Exception;
 
     /**
      * Get the type of interceptor this factory creates
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptor.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptor.java
new file mode 100644
index 000000000..a860c2a4e
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptor.java
@@ -0,0 +1,146 @@
+/*
+ * 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.
+ */
+package org.apache.knox.gateway.services.ldap.interceptor;
+
+import org.apache.directory.api.ldap.model.cursor.ListCursor;
+import org.apache.directory.api.ldap.model.entry.Attribute;
+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.exception.LdapInvalidDnException;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.api.ldap.model.name.Rdn;
+import org.apache.directory.server.core.api.filtering.EntryFilteringCursor;
+import org.apache.directory.server.core.api.filtering.EntryFilteringCursorImpl;
+import org.apache.directory.server.core.api.interceptor.BaseInterceptor;
+import 
org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.ldap.LDAPRolesLookupService;
+import org.apache.knox.gateway.services.ldap.LdapMessages;
+import org.apache.knox.gateway.services.ldap.LdapUtils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Interceptor that replaces group names in memberOf attributes with role names
+ * if LDAP roles lookup is enabled.
+ */
+public class LDAPRolesLookupInterceptor extends BaseInterceptor {
+    private static final LdapMessages LOG = 
MessagesFactory.get(LdapMessages.class);
+    private final LDAPRolesLookupService rolesLookupService;
+
+    public LDAPRolesLookupInterceptor(LDAPRolesLookupService 
rolesLookupService) {
+        this.rolesLookupService = rolesLookupService;
+    }
+
+    @Override
+    public EntryFilteringCursor search(SearchOperationContext ctx) throws 
LdapException {
+        final List<Entry> entries = new ArrayList<>();
+        try (EntryFilteringCursor cursor = next(ctx)) {
+            while (cursor.next()) {
+                entries.add(cursor.get());
+            }
+        } catch (Exception e) {
+            LOG.ldapRolesLookupFailed(LdapUtils.extractGroupName(ctx.getDn()), 
e);
+            throw new LdapException(e);
+        }
+
+        if (!entries.isEmpty()) {
+            for (Entry entry : entries) {
+                try {
+                    final String username = 
LdapUtils.extractUsernameFromEntry(entry, "uid", "cn");
+                    final Set<String> groups = fetchGroups(entry);
+                    final Collection<String> roles = 
rolesLookupService.lookupRoles(username, groups);
+                    modifyEntry(entry, roles);
+                } catch (Exception e) {
+                    LOG.ldapRolesLookupFailed("Error while updating entry with 
roles lookup results", e);
+                    throw new LdapException(e);
+                }
+            }
+        }
+
+        return new EntryFilteringCursorImpl(new ListCursor<>(entries), ctx, 
ctx.getSession().getDirectoryService().getSchemaManager());
+    }
+
+    private Set<String> fetchGroups(final Entry entry) {
+        final Set<String> groups = new HashSet<>();
+        final Attribute memberOf = entry.get("memberOf");
+        if (memberOf != null) {
+            for (Value value : memberOf) {
+                try {
+                    Dn groupDn = new Dn(value.getString());
+                    String groupName = LdapUtils.extractGroupName(groupDn);
+                    if (groupName != null) {
+                        groups.add(groupName);
+                    }
+                } catch (LdapInvalidDnException ignore) {
+                }
+            }
+        }
+        return groups;
+    }
+
+    Entry modifyEntry(Entry entry, Collection<String> roles) throws 
LdapException {
+        if (entry != null) {
+            final Attribute memberOfAttr = entry.get("memberOf");
+            final List<Dn> groupDns = new ArrayList<>();
+            if (memberOfAttr != null) {
+                for (Value value : memberOfAttr) {
+                    groupDns.add(new Dn(value.getString()));
+                }
+            }
+
+            // Only modify if there are existing attributes to wipe or new 
roles to add
+            if (memberOfAttr != null || (roles != null && !roles.isEmpty())) {
+                updateMemberOfAttributes(entry, roles, groupDns);
+            }
+        }
+        return entry;
+    }
+
+    private void updateMemberOfAttributes(Entry entry, Collection<String> 
roles, List<Dn> groupDns) throws LdapException {
+        // Always wipe the old attributes if role lookup is active
+        entry.removeAttributes("memberOf");
+
+        if (!roles.isEmpty()) {
+            Dn templateDn = groupDns.isEmpty() ? null : groupDns.get(0);
+            for (String role : roles) {
+                addRoleAttribute(entry, role, templateDn);
+            }
+        }
+    }
+
+    private void addRoleAttribute(Entry entry, String role, Dn templateDn) 
throws LdapException {
+        if (templateDn != null) {
+            // Create a new DN by replacing the CN of the template DN
+            List<Rdn> rdns = new ArrayList<>(templateDn.getRdns());
+            if (!rdns.isEmpty() && 
rdns.get(0).getType().equalsIgnoreCase("cn")) {
+                rdns.set(0, new Rdn("cn", role));
+                Dn roleDn = new Dn(rdns.toArray(new Rdn[0]));
+                entry.add("memberOf", roleDn.getName());
+                return;
+            }
+        }
+        entry.add("memberOf", "cn=" + role);
+    }
+
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorFactory.java
new file mode 100644
index 000000000..4ab8d903e
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorFactory.java
@@ -0,0 +1,50 @@
+/*
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+package org.apache.knox.gateway.services.ldap.interceptor;
+
+import org.apache.directory.server.core.api.interceptor.Interceptor;
+import org.apache.knox.gateway.GatewayServer;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.ldap.LDAPRolesLookupService;
+
+import java.util.Map;
+
+public class LDAPRolesLookupInterceptorFactory implements 
KnoxLdapInterceptorFactory {
+    private static final String TYPE = "rolesLookup";
+
+    @Override
+    public Interceptor create(GatewayConfig gatewayConfig, String name, 
Map<String, String> interceptorConfig) throws Exception {
+        final LDAPRolesLookupService ldapRolesLookupService = 
getLDAPRolesLookupService();
+        if (ldapRolesLookupService == null || 
!ldapRolesLookupService.enabled()) {
+            throw new ServiceLifecycleException("LDAP roles lookup service not 
found or disabled");
+        }
+        return new LDAPRolesLookupInterceptor(ldapRolesLookupService);
+    }
+
+    protected LDAPRolesLookupService getLDAPRolesLookupService() {
+        final GatewayServices gatewayServices = 
GatewayServer.getGatewayServices();
+        return 
gatewayServices.getService(ServiceType.LDAP_ROLES_LOOKUP_SERVICE);
+    }
+
+    @Override
+    public String getType() {
+        return TYPE;
+    }
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java
index 1ac2ef5a2..0f67f2575 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java
@@ -128,7 +128,7 @@ public class UserSearchInterceptor extends BaseInterceptor {
                             if (backendEntry != null) {
                                 // Return backend result directly without 
caching
                                 entries.add(backendEntry);
-                                LOG.ldapUserEntry(backendEntry.toString());
+                                
LOG.ldapUserEntry(backendEntry.getDn().toString());
                             } else {
                                 LOG.ldapUserNull(username);
                             }
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptorFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptorFactory.java
index 862d3a964..f601dd335 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptorFactory.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptorFactory.java
@@ -18,6 +18,7 @@
 package org.apache.knox.gateway.services.ldap.interceptor;
 
 import org.apache.directory.server.core.api.interceptor.Interceptor;
+import org.apache.knox.gateway.config.GatewayConfig;
 
 import java.util.Map;
 
@@ -25,8 +26,8 @@ public class UserSearchInterceptorFactory implements 
KnoxLdapInterceptorFactory
     public static final String TYPE = "backend";
 
     @Override
-    public Interceptor create(String name, Map<String, String> config) throws 
Exception {
-        return new UserSearchInterceptor(name, config);
+    public Interceptor create(GatewayConfig gatewayConfig, String name, 
Map<String, String> interceptorConfig) throws Exception {
+        return new UserSearchInterceptor(name, interceptorConfig);
     }
 
     @Override
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/DefaultLDAPRolesLookupService.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/DefaultLDAPRolesLookupService.java
new file mode 100644
index 000000000..60f31144f
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/DefaultLDAPRolesLookupService.java
@@ -0,0 +1,74 @@
+/*
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+package org.apache.knox.gateway.services.ldap.roles;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.config.GatewayConfigChangeListener;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.ldap.LDAPRolesLookupService;
+import org.apache.knox.gateway.services.ldap.LdapMessages;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public class DefaultLDAPRolesLookupService implements LDAPRolesLookupService, 
GatewayConfigChangeListener {
+
+    private static final LdapMessages LOG = 
MessagesFactory.get(LdapMessages.class);
+
+    private LdapRolesLookup ldapRolesLookup;
+
+    @Override
+    public void init(GatewayConfig config, Map<String, String> options) throws 
ServiceLifecycleException {
+        try {
+            this.ldapRolesLookup = LdapRolesLookupFactory.create(config);
+            logStatus(config);
+        } catch (RoleLookupException e) {
+            throw new ServiceLifecycleException("Error while initializing LDAP 
roles lookup service", e);
+        }
+    }
+
+    private void logStatus(GatewayConfig config) {
+        if (enabled()) {
+            LOG.ldapRolesLookupEnabled(config.getLdapRolesLookupStrategy());
+        } else {
+            LOG.ldapRolesLookupDisabled();
+        }
+    }
+
+    @Override
+    public boolean enabled() {
+        return ldapRolesLookup != null;
+    }
+
+    @Override
+    public Collection<String> lookupRoles(String userId, Collection<String> 
groups) throws RoleLookupException {
+        return enabled() ? ldapRolesLookup.lookupRoles(userId, groups) : 
List.of();
+    }
+
+    @Override
+    public void onGatewayConfigChanged(GatewayConfig config) {
+        LOG.ldapRolesLookupReloadingConfig();
+        try {
+            this.ldapRolesLookup = LdapRolesLookupFactory.create(config);
+            logStatus(config);
+        } catch (RoleLookupException e) {
+            LOG.ldapRolesLookupReloadFailed(e);
+        }
+    }
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/FileBasedLdapRolesLookup.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/FileBasedLdapRolesLookup.java
new file mode 100644
index 000000000..05787732c
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/FileBasedLdapRolesLookup.java
@@ -0,0 +1,123 @@
+/*
+ * 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.
+ */
+package org.apache.knox.gateway.services.ldap.roles;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.io.FileUtils;
+import org.apache.knox.gateway.services.ldap.RoleAssignment;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * File-based implementation of LdapRolesLookup.
+ * Mimics the REST API backend by reading a JSON file representing the role 
assignments database.
+ * Expects a JSON file with a list of mapping entries:
+ * [
+ *   {
+ *     "id": "alice",
+ *     "type": "user",
+ *     "roles": [ {"scope": "platform", "name": "awc-admin"} ]
+ *   },
+ *   {
+ *     "id": "engineering",
+ *     "type": "group",
+ *     "roles": [ {"scope": "ml-workspace-abc", "name": "viewer"} ]
+ *   }
+ * ]
+ */
+public class FileBasedLdapRolesLookup implements LdapRolesLookup {
+    private final String filePath;
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    public FileBasedLdapRolesLookup(String filePath) {
+        this.filePath = filePath;
+    }
+
+    @Override
+    public Collection<String> lookupRoles(String userId, Collection<String> 
groups) throws RoleLookupException {
+        File roleMappingFile = new File(filePath);
+        if (!roleMappingFile.exists()) {
+            throw new RoleLookupException("Role mapping file " + 
roleMappingFile + " does not exist");
+        }
+
+        try {
+            final String jsonInput = 
FileUtils.readFileToString(roleMappingFile, StandardCharsets.UTF_8);
+            final List<RoleMappingEntry> entries = mapper.readValue(jsonInput, 
new TypeReference<>() {});
+            final Set<String> roles = new HashSet<>();
+
+            for (RoleMappingEntry entry : entries) {
+                if ("user".equalsIgnoreCase(entry.getType())) {
+                    if (entry.getId() != null && entry.getId().equals(userId)) 
{
+                        addRoles(roles, entry.getRoles());
+                    }
+                } else if ("group".equalsIgnoreCase(entry.getType())) {
+                    if (groups != null && groups.contains(entry.getId())) {
+                        addRoles(roles, entry.getRoles());
+                    }
+                }
+            }
+
+            return new ArrayList<>(roles);
+        } catch (IOException e) {
+            throw new RoleLookupException("Error reading roles mapping file: " 
+ filePath, e);
+        }
+    }
+
+    private void addRoles(Set<String> roles, List<RoleAssignment> 
roleAssignments) {
+        if (roleAssignments != null) {
+            for (RoleAssignment assignment : roleAssignments) {
+                String displayValue = assignment.getDisplayValue();
+                if (displayValue != null) {
+                    roles.add(displayValue);
+                }
+            }
+        }
+    }
+
+    private static class RoleMappingEntry {
+        @JsonProperty("id")
+        private String id;
+
+        @JsonProperty("type")
+        private String type;
+
+        @JsonProperty("roles")
+        private List<RoleAssignment> roles;
+
+        public String getId() {
+            return id;
+        }
+
+        public String getType() {
+            return type;
+        }
+
+        public List<RoleAssignment> getRoles() {
+            return roles;
+        }
+    }
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/KnoxLdapInterceptorFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/LdapRolesLookup.java
similarity index 55%
copy from 
gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/KnoxLdapInterceptorFactory.java
copy to 
gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/LdapRolesLookup.java
index 2eb4fb5f9..5920738dd 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/KnoxLdapInterceptorFactory.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/LdapRolesLookup.java
@@ -15,27 +15,21 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.knox.gateway.services.ldap.interceptor;
+package org.apache.knox.gateway.services.ldap.roles;
 
-import org.apache.directory.server.core.api.interceptor.Interceptor;
-
-import java.util.Map;
+import java.util.Collection;
 
 /**
- * Factory interface for creating Interceptor instances.
+ * Interface for looking up roles for a user, potentially replacing their LDAP 
groups.
  */
-public interface KnoxLdapInterceptorFactory {
-    /**
-     * Instantiate and interceptor
-     * @param name the name of the interceptor
-     * @param config the configuration for the interceptor
-     * @return the interceptor
-     */
-    Interceptor create(String name, Map<String, String> config) throws 
Exception;
-
+public interface LdapRolesLookup {
     /**
-     * Get the type of interceptor this factory creates
-     * @return the type of interceptor ths factory creates
+     * Look up roles for the given user and their groups.
+     *
+     * @param userId the user ID
+     * @param groups the list of groups for the user
+     * @return a list of roles (in the format "scope:name" or just "name" 
depending on implementation)
+     * @throws RoleLookupException if an error occurs during lookup
      */
-    String getType();
+    Collection<String> lookupRoles(String userId, Collection<String> groups) 
throws RoleLookupException;
 }
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/LdapRolesLookupFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/LdapRolesLookupFactory.java
new file mode 100644
index 000000000..40d228ddb
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/LdapRolesLookupFactory.java
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+package org.apache.knox.gateway.services.ldap.roles;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.knox.gateway.config.GatewayConfig;
+
+import static 
org.apache.knox.gateway.config.GatewayConfig.LDAP_ROLES_LOOKUP_FILE_PATH;
+import static 
org.apache.knox.gateway.config.GatewayConfig.LDAP_ROLES_LOOKUP_REST_API_ENDPOINT;
+
+public class LdapRolesLookupFactory {
+
+    public static LdapRolesLookup create(GatewayConfig config) throws 
RoleLookupException {
+        final String strategy = config.getLdapRolesLookupStrategy();
+
+        if ("file".equalsIgnoreCase(strategy)) {
+            String filePath = config.getLdapRolesLookupFilePath();
+            if (filePath != null && !filePath.isEmpty()) {
+                return new FileBasedLdapRolesLookup(filePath);
+            } else {
+                throw new RoleLookupException(LDAP_ROLES_LOOKUP_FILE_PATH + 
"is required for file-based role lookups");
+            }
+        } else if ("rest".equalsIgnoreCase(strategy)) {
+            String endpoint = config.getLdapRolesLookupRestApiEndpoint();
+            if (endpoint != null && !endpoint.isEmpty()) {
+                return new RestApiLdapRolesLookup(endpoint);
+            } else {
+                throw new 
RoleLookupException(LDAP_ROLES_LOOKUP_REST_API_ENDPOINT + "is required for REST 
API based role lookups");
+            }
+        } else if (StringUtils.isNotBlank(strategy)) {
+            throw new RoleLookupException("Invalid role lookup strategy: " + 
strategy);
+        }
+
+        //role lookup is not configured
+        return null;
+    }
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/LookupRolesRequest.java
similarity index 52%
copy from 
gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorFactory.java
copy to 
gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/LookupRolesRequest.java
index b443c2def..bc25fcd32 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorFactory.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/LookupRolesRequest.java
@@ -15,22 +15,38 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.knox.gateway.services.ldap.interceptor;
+package org.apache.knox.gateway.services.ldap.roles;
 
-import org.apache.directory.server.core.api.interceptor.Interceptor;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
 
-import java.util.Map;
+public class LookupRolesRequest {
+    @JsonProperty("user_id")
+    private String userId;
 
-public class DuplicateUserFilteringInterceptorFactory implements 
KnoxLdapInterceptorFactory {
-    public static final String TYPE = "duplicateuserfilter";
+    @JsonProperty("groups")
+    private List<String> groups;
 
-    @Override
-    public Interceptor create(String name, Map<String, String> config) {
-        return new DuplicateUserFilteringInterceptor(name);
+    public LookupRolesRequest() {}
+
+    public LookupRolesRequest(String userId, List<String> groups) {
+        this.userId = userId;
+        this.groups = groups;
+    }
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public List<String> getGroups() {
+        return groups;
     }
 
-    @Override
-    public String getType() {
-        return TYPE;
+    public void setGroups(List<String> groups) {
+        this.groups = groups;
     }
 }
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorFactory.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/LookupRolesResponse.java
similarity index 50%
copy from 
gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorFactory.java
copy to 
gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/LookupRolesResponse.java
index b443c2def..c8af98bc6 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorFactory.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/LookupRolesResponse.java
@@ -15,22 +15,39 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.knox.gateway.services.ldap.interceptor;
+package org.apache.knox.gateway.services.ldap.roles;
 
-import org.apache.directory.server.core.api.interceptor.Interceptor;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.knox.gateway.services.ldap.RoleAssignment;
+import java.util.List;
 
-import java.util.Map;
+public class LookupRolesResponse {
+    @JsonProperty("user_id")
+    private String userId;
 
-public class DuplicateUserFilteringInterceptorFactory implements 
KnoxLdapInterceptorFactory {
-    public static final String TYPE = "duplicateuserfilter";
+    @JsonProperty("roles")
+    private List<RoleAssignment> roles;
 
-    @Override
-    public Interceptor create(String name, Map<String, String> config) {
-        return new DuplicateUserFilteringInterceptor(name);
+    public LookupRolesResponse() {}
+
+    public LookupRolesResponse(String userId, List<RoleAssignment> roles) {
+        this.userId = userId;
+        this.roles = roles;
+    }
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public List<RoleAssignment> getRoles() {
+        return roles;
     }
 
-    @Override
-    public String getType() {
-        return TYPE;
+    public void setRoles(List<RoleAssignment> roles) {
+        this.roles = roles;
     }
 }
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/RestApiLdapRolesLookup.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/RestApiLdapRolesLookup.java
new file mode 100644
index 000000000..d2446a07a
--- /dev/null
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/RestApiLdapRolesLookup.java
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+package org.apache.knox.gateway.services.ldap.roles;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import org.apache.knox.gateway.services.ldap.RoleAssignment;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * REST API based implementation of LdapRolesLookup.
+ */
+public class RestApiLdapRolesLookup implements LdapRolesLookup {
+    private final ObjectMapper mapper = new ObjectMapper();
+    private final String endpoint;
+
+    public RestApiLdapRolesLookup(String endpoint) {
+        this.endpoint = endpoint;
+    }
+
+    @Override
+    public Collection<String> lookupRoles(String userId, Collection<String> 
groups) throws RoleLookupException {
+
+        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
+            LookupRolesRequest request = new LookupRolesRequest(userId, new 
ArrayList<>(groups));
+            String jsonRequest = mapper.writeValueAsString(request);
+            HttpPost httpPost = new HttpPost(endpoint);
+            httpPost.setEntity(new StringEntity(jsonRequest, 
ContentType.APPLICATION_JSON));
+
+            try (CloseableHttpResponse response = 
httpClient.execute(httpPost)) {
+                int statusCode = response.getStatusLine().getStatusCode();
+                if (statusCode != 200) {
+                    throw new RoleLookupException("Failed to lookup roles: 
HTTP " + statusCode);
+                }
+
+                HttpEntity entity = response.getEntity();
+                if (entity == null) {
+                    throw new RoleLookupException("Empty response from role 
lookup API");
+                }
+
+                final String jsonResponse = EntityUtils.toString(entity);
+                final LookupRolesResponse lookupResponse = 
mapper.readValue(jsonResponse, LookupRolesResponse.class);
+                return parseResponse(lookupResponse);
+            }
+        } catch (IOException e) {
+            throw new RoleLookupException("Error while executing role lookup", 
e);
+        }
+    }
+
+    private static List<String> parseResponse(LookupRolesResponse 
lookupResponse) {
+        List<String> roles = new ArrayList<>();
+        if (lookupResponse.getRoles() != null) {
+            for (RoleAssignment assignment : lookupResponse.getRoles()) {
+                String displayValue = assignment.getDisplayValue();
+                if (displayValue != null) {
+                    roles.add(displayValue);
+                }
+            }
+        }
+        return roles;
+    }
+}
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapUtils.java
 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/RoleLookupException.java
similarity index 62%
copy from 
gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapUtils.java
copy to 
gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/RoleLookupException.java
index 117c778ea..b482a41cb 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapUtils.java
+++ 
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/roles/RoleLookupException.java
@@ -14,23 +14,15 @@
  * License for the specific language governing permissions and limitations 
under
  * the License.
  */
-package org.apache.knox.gateway.services.ldap;
+package org.apache.knox.gateway.services.ldap.roles;
 
-import org.apache.directory.api.ldap.model.name.Dn;
+public class RoleLookupException extends Exception {
 
-public class LdapUtils {
-
-    public static String extractUsernameFromDn(Dn dn) {
-        if (dn == null || dn.isEmpty()) {
-            return null;
-        }
+    public RoleLookupException(String message) {
+        super(message);
+    }
 
-        try {
-            return "uid".equalsIgnoreCase(dn.getRdn().getType())
-                    ? dn.getRdn().getValue()
-                    : null;
-        } catch (Exception ignored) {
-            return null;
-        }
+    public RoleLookupException(String message, Throwable cause) {
+        super(message, cause);
     }
 }
diff --git 
a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory
 
b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory
index 8c8a4fb1b..a73f6712a 100644
--- 
a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory
+++ 
b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory
@@ -37,3 +37,4 @@ 
org.apache.knox.gateway.services.factory.TokenStateServiceFactory
 org.apache.knox.gateway.services.factory.TopologyServiceFactory
 org.apache.knox.gateway.services.factory.TokenServiceFactory
 org.apache.knox.gateway.services.factory.LdapServiceFactory
+org.apache.knox.gateway.services.factory.LDAPRolesLookupServiceFactory
diff --git 
a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.interceptor.KnoxLdapInterceptorFactory
 
b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.interceptor.KnoxLdapInterceptorFactory
index 3275b3329..22629d400 100644
--- 
a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.interceptor.KnoxLdapInterceptorFactory
+++ 
b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.interceptor.KnoxLdapInterceptorFactory
@@ -17,5 +17,6 @@
 ##########################################################################
 
 # Built-in LDAP interceptor factory implementations
+org.apache.knox.gateway.services.ldap.interceptor.LDAPRolesLookupInterceptorFactory
 org.apache.knox.gateway.services.ldap.interceptor.UserSearchInterceptorFactory
 
org.apache.knox.gateway.services.ldap.interceptor.DuplicateUserFilteringInterceptorFactory
\ No newline at end of file
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/GatewayServerTest.java 
b/gateway-server/src/test/java/org/apache/knox/gateway/GatewayServerTest.java
index a5938d7cd..7140e9467 100644
--- 
a/gateway-server/src/test/java/org/apache/knox/gateway/GatewayServerTest.java
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/GatewayServerTest.java
@@ -20,6 +20,7 @@ package org.apache.knox.gateway;
 import org.apache.knox.gateway.config.GatewayConfigChangeListener;
 import org.apache.knox.gateway.config.impl.GatewayConfigImpl;
 import org.easymock.EasyMock;
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
@@ -40,6 +41,11 @@ public class GatewayServerTest {
   @Rule
   public TemporaryFolder folder = new TemporaryFolder();
 
+  @Before
+  public void setup() {
+    GatewayServer.emptyConfigChangeListener();
+  }
+
   @Test
   public void testRefreshGatewayConfig() throws Exception {
     GatewayConfigImpl config = 
EasyMock.createNiceMock(GatewayConfigImpl.class);
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
index 1a5483c53..57271324c 100644
--- 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
@@ -67,6 +67,7 @@ public class AbstractGatewayServicesTest {
         ServiceType.REMOTE_CONFIGURATION_MONITOR,
         ServiceType.GATEWAY_STATUS_SERVICE,
         ServiceType.LDAP_SERVICE,
+        ServiceType.LDAP_ROLES_LOOKUP_SERVICE,
         ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE
     };
 
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 399e05485..530cdb102 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
@@ -19,22 +19,22 @@ package org.apache.knox.gateway.services.ldap;
 
 import org.apache.knox.gateway.config.GatewayConfig;
 import org.apache.knox.gateway.services.ServiceLifecycleException;
-import org.junit.Test;
-import org.junit.Before;
 import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
 
 import java.io.File;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.assertFalse;
 import static org.easymock.EasyMock.createMock;
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.replay;
 import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 /**
  * Unit tests for KnoxLDAPService.
@@ -163,7 +163,7 @@ public class KnoxLDAPServiceTest {
         verify(mockConfig);
     }
 
-    private void setupMockConfig(String backendType) {
+    private void setupMockConfig(String backendType) throws Exception {
         expect(mockConfig.isLDAPEnabled()).andReturn(true).atLeastOnce();
         
expect(mockConfig.isLDAPRecursiveGroupResolutionEnabled()).andReturn(false).atLeastOnce();
         
expect(mockConfig.getLDAPRecursiveGroupResolutionMaxDepth()).andReturn(0).atLeastOnce();
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorFactoryTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorFactoryTest.java
new file mode 100644
index 000000000..ca28b7959
--- /dev/null
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorFactoryTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+package org.apache.knox.gateway.services.ldap.interceptor;
+
+import org.apache.directory.server.core.api.interceptor.Interceptor;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.ldap.LDAPRolesLookupService;
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import java.util.Collections;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class LDAPRolesLookupInterceptorFactoryTest {
+
+    @Test
+    public void testCreateWithEnabledService() throws Exception {
+        LDAPRolesLookupService mockService = 
EasyMock.createMock(LDAPRolesLookupService.class);
+        EasyMock.expect(mockService.enabled()).andReturn(true).anyTimes();
+        EasyMock.replay(mockService);
+
+        LDAPRolesLookupInterceptorFactory factory = new 
LDAPRolesLookupInterceptorFactory() {
+            @Override
+            protected LDAPRolesLookupService getLDAPRolesLookupService() {
+                return mockService;
+            }
+        };
+
+        GatewayConfig mockConfig = EasyMock.createMock(GatewayConfig.class);
+        EasyMock.replay(mockConfig);
+
+        Interceptor interceptor = factory.create(mockConfig, "test", 
Collections.emptyMap());
+        assertNotNull(interceptor);
+        assertTrue(interceptor instanceof LDAPRolesLookupInterceptor);
+    }
+
+    @Test(expected = ServiceLifecycleException.class)
+    public void testCreateWithDisabledService() throws Exception {
+        LDAPRolesLookupService mockService = 
EasyMock.createMock(LDAPRolesLookupService.class);
+        EasyMock.expect(mockService.enabled()).andReturn(false).anyTimes();
+        EasyMock.replay(mockService);
+
+        LDAPRolesLookupInterceptorFactory factory = new 
LDAPRolesLookupInterceptorFactory() {
+            @Override
+            protected LDAPRolesLookupService getLDAPRolesLookupService() {
+                return mockService;
+            }
+        };
+
+        GatewayConfig mockConfig = EasyMock.createMock(GatewayConfig.class);
+        EasyMock.replay(mockConfig);
+
+        factory.create(mockConfig, "test", Collections.emptyMap());
+    }
+
+    @Test(expected = ServiceLifecycleException.class)
+    public void testCreateWithNullService() throws Exception {
+        LDAPRolesLookupInterceptorFactory factory = new 
LDAPRolesLookupInterceptorFactory() {
+            @Override
+            protected LDAPRolesLookupService getLDAPRolesLookupService() {
+                return null;
+            }
+        };
+
+        GatewayConfig mockConfig = EasyMock.createMock(GatewayConfig.class);
+        EasyMock.replay(mockConfig);
+
+        factory.create(mockConfig, "test", Collections.emptyMap());
+    }
+}
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorTest.java
new file mode 100644
index 000000000..637e1fc0f
--- /dev/null
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/LDAPRolesLookupInterceptorTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+package org.apache.knox.gateway.services.ldap.interceptor;
+
+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.knox.gateway.services.ldap.LDAPRolesLookupService;
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class LDAPRolesLookupInterceptorTest {
+
+    @Test
+    public void testModifyEntryWithRoles() throws Exception {
+        final Entry userEntry = createUserEntry("alice", 
"cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org");
+        final Collection<String> roles = Arrays.asList("roleA", "roleG");
+
+        final Entry modifiedEntry = createInterceptor().modifyEntry(userEntry, 
roles);
+
+        assertMemberOf(modifiedEntry,
+                "cn=roleA,ou=groups,dc=hadoop,dc=apache,dc=org",
+                "cn=roleG,ou=groups,dc=hadoop,dc=apache,dc=org");
+    }
+
+    @Test
+    public void testModifyEntryWithNoRoles() throws Exception {
+        final Entry userEntry = createUserEntry("bob", 
"cn=group1,ou=groups,dc=hadoop,dc=apache,dc=org");
+        final Collection<String> roles = Collections.emptyList();
+
+        final Entry modifiedEntry = createInterceptor().modifyEntry(userEntry, 
roles);
+
+        assertNull("memberOf attribute should be removed when no roles are 
found", modifiedEntry.get("memberOf"));
+    }
+
+    @Test
+    public void testModifyEntryNoMemberOfNoRoles() throws Exception {
+        final Entry userEntry = createUserEntry("charlie");
+        final Collection<String> roles = Collections.emptyList();
+
+        final Entry modifiedEntry = createInterceptor().modifyEntry(userEntry, 
roles);
+
+        assertEquals(userEntry, modifiedEntry);
+        assertNull(modifiedEntry.get("memberOf"));
+    }
+
+    private LDAPRolesLookupInterceptor createInterceptor() {
+        final LDAPRolesLookupService mockRolesService = 
EasyMock.createMock(LDAPRolesLookupService.class);
+        replay(mockRolesService);
+        return new LDAPRolesLookupInterceptor(mockRolesService);
+    }
+
+    private Entry createUserEntry(final String username, final String... 
memberOfDns) throws Exception {
+        final Entry entry = new DefaultEntry();
+        entry.add("uid", username);
+        for (final String dn : memberOfDns) {
+            entry.add("memberOf", dn);
+        }
+        return entry;
+    }
+
+    private void assertMemberOf(final Entry entry, final String... 
expectedDns) {
+        final Attribute memberOf = entry.get("memberOf");
+        assertEquals("Unexpected number of memberOf attributes", 
expectedDns.length, memberOf.size());
+        for (final String expected : expectedDns) {
+            assertTrue("Missing expected role DN: " + expected, 
memberOf.contains(expected));
+        }
+    }
+}
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/roles/FileBasedLdapRolesLookupTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/roles/FileBasedLdapRolesLookupTest.java
new file mode 100644
index 000000000..3617967ea
--- /dev/null
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/roles/FileBasedLdapRolesLookupTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+package org.apache.knox.gateway.services.ldap.roles;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class FileBasedLdapRolesLookupTest {
+    private File testFile;
+
+    @Before
+    public void setUp() throws Exception {
+        testFile = File.createTempFile("roles-mapping", ".json");
+        String json = """
+                [
+                  {
+                    "id": "alice",
+                    "type": "user",
+                    "roles": [ {"scope": "platform", "name": "awc-admin"} ]
+                  },
+                  {
+                    "id": "bob",
+                    "type": "user",
+                    "roles": [ {"name": "viewer"} ]
+                  },
+                  {
+                    "id": "engineering",
+                    "type": "group",
+                    "roles": [ {"scope": "ml-workspace-abc", "name": "viewer"} 
]
+                  },
+                  {
+                    "id": "admins",
+                    "type": "group",
+                    "roles": [ {"scope": "platform", "name": "super-user"} ]
+                  }
+                ]""";
+        FileUtils.writeStringToFile(testFile, json, StandardCharsets.UTF_8);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (testFile != null && testFile.exists()) {
+            testFile.delete();
+        }
+    }
+
+    @Test
+    public void testUserLookup() throws Exception {
+        FileBasedLdapRolesLookup lookup = new 
FileBasedLdapRolesLookup(testFile.getAbsolutePath());
+        Collection<String> roles = lookup.lookupRoles("alice", 
List.of("other-group"));
+        assertEquals(1, roles.size());
+        assertTrue(roles.contains("platform:awc-admin"));
+    }
+
+    @Test
+    public void testGroupLookup() throws Exception {
+        FileBasedLdapRolesLookup lookup = new 
FileBasedLdapRolesLookup(testFile.getAbsolutePath());
+        Collection<String> roles = lookup.lookupRoles("unknown-user", 
List.of("engineering"));
+        assertEquals(1, roles.size());
+        assertTrue(roles.contains("ml-workspace-abc:viewer"));
+    }
+
+    @Test
+    public void testUserAndGroupLookup() throws Exception {
+        FileBasedLdapRolesLookup lookup = new 
FileBasedLdapRolesLookup(testFile.getAbsolutePath());
+        Collection<String> roles = lookup.lookupRoles("alice", 
List.of("engineering", "admins"));
+        assertEquals(3, roles.size());
+        assertTrue(roles.contains("platform:awc-admin"));
+        assertTrue(roles.contains("ml-workspace-abc:viewer"));
+        assertTrue(roles.contains("platform:super-user"));
+    }
+
+    @Test
+    public void testNoScope() throws Exception {
+        FileBasedLdapRolesLookup lookup = new 
FileBasedLdapRolesLookup(testFile.getAbsolutePath());
+        Collection<String> roles = lookup.lookupRoles("bob", null);
+        assertEquals(1, roles.size());
+        assertTrue(roles.contains("viewer"));
+    }
+
+    @Test(expected=RoleLookupException.class)
+    public void testFileNotFound() throws Exception {
+        new 
FileBasedLdapRolesLookup("non-existent-file.json").lookupRoles("alice", null);
+    }
+}
diff --git 
a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/roles/RestApiLdapRolesLookupTest.java
 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/roles/RestApiLdapRolesLookupTest.java
new file mode 100644
index 000000000..5760b5034
--- /dev/null
+++ 
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/roles/RestApiLdapRolesLookupTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+package org.apache.knox.gateway.services.ldap.roles;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.easymock.EasyMock;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.api.easymock.PowerMock;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({HttpClients.class, RestApiLdapRolesLookup.class})
+public class RestApiLdapRolesLookupTest {
+
+    @Test
+    public void testLookupRoles() throws Exception {
+        String endpoint = "http://localhost:8080/api/v0/auth/roles/lookup";;
+        String jsonResponse = "{\n" +
+                "  \"user_id\": \"alice\",\n" +
+                "  \"roles\": [\n" +
+                "    { \"scope\": \"platform\", \"name\": \"awc-admin\" },\n" +
+                "    { \"scope\": \"ml-workspace-abc\", \"name\": \"viewer\" 
}\n" +
+                "  ]\n" +
+                "}";
+
+        CloseableHttpClient httpClient = 
EasyMock.createMock(CloseableHttpClient.class);
+        CloseableHttpResponse response = 
EasyMock.createMock(CloseableHttpResponse.class);
+        StatusLine statusLine = EasyMock.createMock(StatusLine.class);
+        HttpEntity entity = EasyMock.createMock(HttpEntity.class);
+
+        PowerMock.mockStatic(HttpClients.class);
+        EasyMock.expect(HttpClients.createDefault()).andReturn(httpClient);
+        
EasyMock.expect(httpClient.execute(EasyMock.anyObject(HttpPost.class))).andReturn(response);
+        EasyMock.expect(response.getStatusLine()).andReturn(statusLine);
+        EasyMock.expect(statusLine.getStatusCode()).andReturn(200);
+        EasyMock.expect(response.getEntity()).andReturn(entity);
+        EasyMock.expect(entity.getContentType()).andReturn(null).anyTimes();
+        EasyMock.expect(entity.getContent()).andReturn(new 
ByteArrayInputStream(jsonResponse.getBytes(StandardCharsets.UTF_8)));
+        // EntityUtils.toString uses entity.getContent() or other methods.
+        // We might need to mock EntityUtils or provide a better mock for 
entity.
+        EasyMock.expect(entity.getContentLength()).andReturn((long) 
jsonResponse.length()).anyTimes();
+
+        response.close();
+        EasyMock.expectLastCall().anyTimes();
+        httpClient.close();
+        EasyMock.expectLastCall().anyTimes();
+
+        PowerMock.replayAll();
+        EasyMock.replay(httpClient, response, statusLine, entity);
+
+        RestApiLdapRolesLookup lookup = new RestApiLdapRolesLookup(endpoint);
+        Collection<String> roles = lookup.lookupRoles("alice", 
List.of("group1"));
+
+        assertEquals(2, roles.size());
+        assertTrue(roles.contains("platform:awc-admin"));
+        assertTrue(roles.contains("ml-workspace-abc:viewer"));
+
+        PowerMock.verifyAll();
+    }
+}
diff --git 
a/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/AbstractAuthResource.java
 
b/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/AbstractAuthResource.java
index f59919584..b87dfe685 100644
--- 
a/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/AbstractAuthResource.java
+++ 
b/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/AbstractAuthResource.java
@@ -19,12 +19,16 @@ package org.apache.knox.gateway.service.auth;
 
 import org.apache.knox.gateway.i18n.messages.MessagesFactory;
 import org.apache.knox.gateway.security.SubjectUtils;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.ldap.LDAPRolesLookupService;
 import org.apache.knox.gateway.util.GroupUtils;
 
 import javax.security.auth.Subject;
 import javax.servlet.ServletContext;
 import javax.servlet.http.HttpServletResponse;
 import javax.ws.rs.core.Response;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
@@ -46,6 +50,7 @@ public abstract class AbstractAuthResource {
 
   static final String DEFAULT_AUTH_ACTOR_ID_HEADER_NAME = "X-Knox-Actor-ID";
   static final String DEFAULT_AUTH_ACTOR_GROUPS_HEADER_PREFIX = 
"X-Knox-Actor-Groups";
+
   static final Pattern DEFAULT_GROUP_FILTER_PATTERN = Pattern.compile(".*");
 
   private static final String DEFAULT_GROUP_HEADER_LENGTH_LIMIT = "1000";
@@ -57,6 +62,8 @@ public abstract class AbstractAuthResource {
   private int groupHeaderLengthLimit;
   private int groupHeaderSizeLimit;
   protected Pattern groupFilterPattern;
+  protected String authHeaderActorRolesName;
+  private LDAPRolesLookupService ldapRolesLookupService;
 
   protected void initialize() {
     authHeaderActorIDName = getInitParameter(AUTH_ACTOR_ID_HEADER_NAME, 
DEFAULT_AUTH_ACTOR_ID_HEADER_NAME);
@@ -65,6 +72,12 @@ public abstract class AbstractAuthResource {
     groupHeaderSizeLimit = 
Integer.parseInt(getInitParameter(GROUP_HEADER_SIZE_LIMIT, 
DEFAULT_GROUP_HEADER_SIZE_LIMIT));
     final String groupFilterPatternString = 
getInitParameter(GROUP_FILTER_PATTERN, null);
     groupFilterPattern = groupFilterPatternString == null ? 
DEFAULT_GROUP_FILTER_PATTERN : Pattern.compile(groupFilterPatternString);
+
+    final GatewayServices gatewayServices = (GatewayServices) 
getContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+    if (gatewayServices != null) {
+      ldapRolesLookupService = 
gatewayServices.getService(ServiceType.LDAP_ROLES_LOOKUP_SERVICE);
+    }
+
   }
 
   /* abstract method to get the response instance */
@@ -88,12 +101,13 @@ public abstract class AbstractAuthResource {
     }
     getResponse().setHeader(authHeaderActorIDName, primaryPrincipalName);
 
-    // Populate actor groups headers
+    // Populate actor groups/roles headers
     final Set<String> matchingGroupNames = subject == null ? 
Collections.emptySet()
             : SubjectUtils.getGroupPrincipals(subject).stream().filter(group 
-> groupFilterPattern.matcher(group.getName()).matches()).map(group -> 
group.getName())
             .collect(Collectors.toSet());
     if (!matchingGroupNames.isEmpty()) {
-      final List<String> groupStrings = 
GroupUtils.getGroupStrings(matchingGroupNames, groupHeaderLengthLimit, 
groupHeaderSizeLimit);
+      final Collection<String> roles = lookupRoles(primaryPrincipalName, 
matchingGroupNames);
+      final List<String> groupStrings = GroupUtils.getGroupStrings(roles == 
null ? matchingGroupNames : roles, groupHeaderLengthLimit, 
groupHeaderSizeLimit);
       for (int i = 0; i < groupStrings.size(); i++) {
         getResponse().addHeader(String.format(Locale.ROOT, 
ACTOR_GROUPS_HEADER_FORMAT, authHeaderActorGroupsPrefix, i + 1), 
groupStrings.get(i));
       }
@@ -101,4 +115,18 @@ public abstract class AbstractAuthResource {
     return ok().build();
   }
 
+  private Collection<String> lookupRoles(String userName, Collection<String> 
groups) {
+      try {
+        if (ldapRolesLookupService != null && 
ldapRolesLookupService.enabled()) {
+          return ldapRolesLookupService.lookupRoles(userName, groups);
+        } else {
+          return null;
+        }
+      } catch (Exception e) {
+        // Couldn't lookup roles: log and return null so that the API will 
return the groups
+        LOG.ldapRolesLookupFailed(userName, e);
+        return null;
+      }
+  }
+
 }
diff --git 
a/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/AuthMessages.java
 
b/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/AuthMessages.java
index 662833f26..bf83724ec 100644
--- 
a/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/AuthMessages.java
+++ 
b/gateway-service-auth/src/main/java/org/apache/knox/gateway/service/auth/AuthMessages.java
@@ -20,6 +20,7 @@ package org.apache.knox.gateway.service.auth;
 import org.apache.knox.gateway.i18n.messages.Message;
 import org.apache.knox.gateway.i18n.messages.MessageLevel;
 import org.apache.knox.gateway.i18n.messages.Messages;
+import org.apache.knox.gateway.i18n.messages.StackTrace;
 
 @Messages(logger = "org.apache.knox.gateway.service.auth")
 public interface AuthMessages {
@@ -30,4 +31,7 @@ public interface AuthMessages {
   @Message(level = MessageLevel.INFO, text = "Serving request for path: {0}")
   void pathValue(String path);
 
+  @Message(level = MessageLevel.ERROR, text = "Failed to lookup roles for user 
{0}: {1}")
+  void ldapRolesLookupFailed(String user, @StackTrace(level = 
MessageLevel.DEBUG) Exception e);
+
 }
diff --git 
a/gateway-service-auth/src/test/java/org/apache/knox/gateway/service/auth/ExtAuthzResourceTest.java
 
b/gateway-service-auth/src/test/java/org/apache/knox/gateway/service/auth/ExtAuthzResourceTest.java
index ead516211..8822d6cf4 100644
--- 
a/gateway-service-auth/src/test/java/org/apache/knox/gateway/service/auth/ExtAuthzResourceTest.java
+++ 
b/gateway-service-auth/src/test/java/org/apache/knox/gateway/service/auth/ExtAuthzResourceTest.java
@@ -20,6 +20,9 @@ package org.apache.knox.gateway.service.auth;
 import org.apache.knox.gateway.security.GroupPrincipal;
 import org.apache.knox.gateway.security.PrimaryPrincipal;
 import org.apache.knox.gateway.security.SubjectUtils;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.ldap.LDAPRolesLookupService;
 import org.easymock.EasyMock;
 import org.junit.Before;
 import org.junit.Test;
@@ -36,6 +39,7 @@ import javax.ws.rs.core.UriInfo;
 import java.net.URI;
 import java.security.PrivilegedActionException;
 import java.security.PrivilegedExceptionAction;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -93,6 +97,40 @@ public class ExtAuthzResourceTest {
     EasyMock.replay(context, request, response);
   }
 
+  @Test
+  public void testPopulatingGroupsWithRoles() throws Exception {
+    final String role1 = "platform:admin";
+    final String role2 = "ml-workspace:viewer";
+    final List<String> groups = Collections.singletonList("engineering");
+
+    context = EasyMock.createNiceMock(ServletContext.class);
+    response = EasyMock.createNiceMock(HttpServletResponse.class);
+
+    LDAPRolesLookupService mockRolesService = 
EasyMock.createNiceMock(LDAPRolesLookupService.class);
+    EasyMock.expect(mockRolesService.enabled()).andReturn(true).anyTimes();
+    EasyMock.expect(mockRolesService.lookupRoles(EasyMock.eq(USER_NAME), 
EasyMock.anyObject())).andReturn(Arrays.asList(role1, role2)).anyTimes();
+
+    GatewayServices mockGatewayServices = 
EasyMock.createNiceMock(GatewayServices.class);
+    
EasyMock.expect(mockGatewayServices.getService(ServiceType.LDAP_ROLES_LOOKUP_SERVICE)).andReturn(mockRolesService).anyTimes();
+
+    
EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(mockGatewayServices).anyTimes();
+    
EasyMock.expect(context.getInitParameter(ExtAuthzResource.IGNORE_ADDITIONAL_PATH)).andReturn("true").anyTimes();
+
+    response.setHeader(AbstractAuthResource.DEFAULT_AUTH_ACTOR_ID_HEADER_NAME, 
USER_NAME);
+    EasyMock.expectLastCall();
+
+    EasyMock.replay(context, response, mockRolesService, mockGatewayServices);
+
+    groups.forEach(group -> subject.getPrincipals().add(new 
GroupPrincipal(group)));
+
+    final ExtAuthzResource extAuthzResource = new ExtAuthzResource();
+    extAuthzResource.context = context;
+    extAuthzResource.response = response;
+    executeResourceWithAdditionalPath(extAuthzResource);
+
+    EasyMock.verify(response);
+  }
+
   private int calculateGroupStringSize(Collection<String> groups) {
     final AtomicInteger size = new AtomicInteger(0);
     groups.forEach(group -> size.addAndGet(group.length()));
diff --git 
a/gateway-service-auth/src/test/java/org/apache/knox/gateway/service/auth/PreAuthResourceTest.java
 
b/gateway-service-auth/src/test/java/org/apache/knox/gateway/service/auth/PreAuthResourceTest.java
index 01ca3c5e2..fe2e9b755 100644
--- 
a/gateway-service-auth/src/test/java/org/apache/knox/gateway/service/auth/PreAuthResourceTest.java
+++ 
b/gateway-service-auth/src/test/java/org/apache/knox/gateway/service/auth/PreAuthResourceTest.java
@@ -21,6 +21,7 @@ import static org.junit.Assert.assertEquals;
 
 import java.security.PrivilegedActionException;
 import java.security.PrivilegedExceptionAction;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
@@ -36,6 +37,9 @@ import javax.ws.rs.core.Response;
 import org.apache.knox.gateway.security.GroupPrincipal;
 import org.apache.knox.gateway.security.PrimaryPrincipal;
 import org.apache.knox.gateway.security.SubjectUtils;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.ldap.LDAPRolesLookupService;
 import org.easymock.EasyMock;
 import org.junit.Before;
 import org.junit.Test;
@@ -57,6 +61,10 @@ public class PreAuthResourceTest {
   }
 
   private void configureCommonExpectations(String actorIdHeaderName, String 
groupsHeaderPrefix, Collection<String> groups) {
+    configureCommonExpectations(actorIdHeaderName, groupsHeaderPrefix, groups, 
null);
+  }
+
+  private void configureCommonExpectations(String actorIdHeaderName, String 
groupsHeaderPrefix, Collection<String> groups, GatewayServices gatewayServices) 
{
     context = EasyMock.createNiceMock(ServletContext.class);
     
EasyMock.expect(context.getInitParameter(PreAuthResource.AUTH_ACTOR_ID_HEADER_NAME)).andReturn(actorIdHeaderName).anyTimes();
     
EasyMock.expect(context.getInitParameter(PreAuthResource.AUTH_ACTOR_GROUPS_HEADER_PREFIX)).andReturn(groupsHeaderPrefix).anyTimes();
@@ -81,6 +89,11 @@ public class PreAuthResourceTest {
       }
     }
 
+    if (gatewayServices != null) {
+      
EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(gatewayServices);
+
+    }
+
     EasyMock.replay(context, request, response);
   }
 
@@ -142,6 +155,33 @@ public class PreAuthResourceTest {
     EasyMock.verify(response);
   }
 
+  @Test
+  public void testPopulatingGroupsWithRoles() throws Exception {
+    final GatewayServices gatewayServices = 
configureLdapRolesLookupExpectations();
+    
configureCommonExpectations(PreAuthResource.DEFAULT_AUTH_ACTOR_ID_HEADER_NAME, 
null, Collections.singleton("engineering"), gatewayServices);
+    final PreAuthResource preAuthResource = new PreAuthResource();
+    preAuthResource.context = context;
+    preAuthResource.response = response;
+    Response preAuthResponse = executeResourceWithSubject(preAuthResource);
+    assertEquals(HttpServletResponse.SC_OK, preAuthResponse.getStatus());
+    EasyMock.verify(response);
+  }
+
+  private GatewayServices configureLdapRolesLookupExpectations() throws 
Exception {
+    final String role1 = "platform:admin";
+    final String role2 = "ml-workspace:viewer";
+    final Set<String> groups = Collections.singleton("engineering");
+    final LDAPRolesLookupService rolesLookupService = 
EasyMock.createNiceMock(LDAPRolesLookupService.class);
+    EasyMock.expect(rolesLookupService.enabled()).andReturn(true).anyTimes();
+    EasyMock.expect(rolesLookupService.lookupRoles(EasyMock.eq(USER_NAME), 
EasyMock.anyObject())).andReturn(Arrays.asList(role1, role2)).anyTimes();
+
+    final GatewayServices gatewayServices = 
EasyMock.createNiceMock(GatewayServices.class);
+    
EasyMock.expect(gatewayServices.getService(ServiceType.LDAP_ROLES_LOOKUP_SERVICE)).andReturn(rolesLookupService).anyTimes();
+
+    EasyMock.replay(rolesLookupService, gatewayServices);
+    return gatewayServices;
+  }
+
   @Test
   public void testPopulatingMultipleGroupsHeaderWithLargeGroupNames() throws 
Exception {
     doTestPopulatingMultipleGroupsHeaderWithLargeGroupNames(100);
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 745002ae3..5331f2bc2 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
@@ -1275,6 +1275,21 @@ public class GatewayTestConfig extends Configuration 
implements GatewayConfig {
     return 0;
   }
 
+  @Override
+  public String getLdapRolesLookupStrategy() {
+    return "";
+  }
+
+  @Override
+  public String getLdapRolesLookupRestApiEndpoint() {
+    return "";
+  }
+
+  @Override
+  public String getLdapRolesLookupFilePath() {
+    return "";
+  }
+
   @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 7ecb2980d..0eac12d0d 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
@@ -132,6 +132,9 @@ public interface GatewayConfig {
   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";
+  String LDAP_ROLES_LOOKUP_STRATEGY = "gateway.ldap.roles.lookup.strategy";
+  String LDAP_ROLES_LOOKUP_REST_API_ENDPOINT = 
"gateway.ldap.roles.lookup.rest.api.endpoint";
+  String LDAP_ROLES_LOOKUP_FILE_PATH = "gateway.ldap.roles.lookup.file.path";
 
   /**
    * The location of the gateway configuration.
@@ -1099,6 +1102,21 @@ public interface GatewayConfig {
    */
   int getLDAPRecursiveGroupResolutionMaxDepth();
 
+  /**
+   * @return the LDAP roles lookup strategy (file or rest)
+   */
+  String getLdapRolesLookupStrategy();
+
+  /**
+   * @return the LDAP roles lookup REST API endpoint
+   */
+  String getLdapRolesLookupRestApiEndpoint();
+
+  /**
+   * @return the LDAP roles lookup file path
+   */
+  String getLdapRolesLookupFilePath();
+
   /**
    * @return set of all property names in the configuration
    */
diff --git 
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java 
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
index 5fbcba3ab..21794bc21 100644
--- 
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
+++ 
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
@@ -40,6 +40,7 @@ public enum ServiceType {
   REMOTE_CONFIGURATION_MONITOR("RemoteConfigurationMonitor"),
   GATEWAY_STATUS_SERVICE("GatewayStatusService"),
   LDAP_SERVICE("LDAPService"),
+  LDAP_ROLES_LOOKUP_SERVICE("LDAPRoleLookupService"),
   KNOXIDF_FEDERATED_IDENTITY_SERVICE("KnoxIDFFederatedIdentityService");
 
   private final String serviceTypeName;
diff --git 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapUtils.java
 
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ldap/LDAPRolesLookupService.java
similarity index 64%
copy from 
gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapUtils.java
copy to 
gateway-spi/src/main/java/org/apache/knox/gateway/services/ldap/LDAPRolesLookupService.java
index 117c778ea..029df2a23 100644
--- 
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapUtils.java
+++ 
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ldap/LDAPRolesLookupService.java
@@ -16,21 +16,20 @@
  */
 package org.apache.knox.gateway.services.ldap;
 
-import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.knox.gateway.services.Service;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
 
-public class LdapUtils {
+import java.util.Collection;
 
-    public static String extractUsernameFromDn(Dn dn) {
-        if (dn == null || dn.isEmpty()) {
-            return null;
-        }
+public interface LDAPRolesLookupService extends Service {
 
-        try {
-            return "uid".equalsIgnoreCase(dn.getRdn().getType())
-                    ? dn.getRdn().getValue()
-                    : null;
-        } catch (Exception ignored) {
-            return null;
-        }
-    }
+    boolean enabled();
+
+    Collection<String> lookupRoles(String userId, Collection<String> groups) 
throws Exception;
+
+    @Override
+    default void start() throws ServiceLifecycleException {};
+
+    @Override
+    default void stop() throws ServiceLifecycleException {};
 }
diff --git 
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/ldap/RoleAssignment.java
 
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ldap/RoleAssignment.java
new file mode 100644
index 000000000..86bc4b89e
--- /dev/null
+++ 
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ldap/RoleAssignment.java
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+package org.apache.knox.gateway.services.ldap;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class RoleAssignment {
+    @JsonProperty("scope")
+    private String scope;
+
+    @JsonProperty("name")
+    private String name;
+
+    public RoleAssignment() {}
+
+    public RoleAssignment(String scope, String name) {
+        this.scope = scope;
+        this.name = name;
+    }
+
+    public String getScope() {
+        return scope;
+    }
+
+    public void setScope(String scope) {
+        this.scope = scope;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getDisplayValue() {
+        if (name == null) {
+            return null;
+        }
+        if (scope != null && !scope.isEmpty()) {
+            return scope + ":" + name;
+        }
+        return name;
+    }
+}
diff --git a/knox-site/docs/service_ldap_server.md 
b/knox-site/docs/service_ldap_server.md
index 45f409b8a..6e0d1a56b 100644
--- a/knox-site/docs/service_ldap_server.md
+++ b/knox-site/docs/service_ldap_server.md
@@ -40,6 +40,9 @@ The service is configured in `gateway-site.xml`.
 | `gateway.ldap.port` | `3890` | The port on which the LDAP server listens. |
 | `gateway.ldap.base.dn` | `dc=proxy,dc=com` | The base DN for the LDAP 
server. |
 | `gateway.ldap.interceptor.names` | N/A | A comma separated list of 
interceptors to use. A separate interceptor configuration block will be used 
for each name. |
+| `gateway.ldap.roles.lookup.strategy` | N/A | The LDAP roles lookup strategy 
(`file` or `rest`). |
+| `gateway.ldap.roles.lookup.rest.api.endpoint` | N/A | The LDAP roles lookup 
REST API endpoint. |
+| `gateway.ldap.roles.lookup.file.path` | N/A | The LDAP roles lookup file 
path. |
 
 ### Interceptor Types
 
@@ -139,7 +142,17 @@ To configure Knox to act as an LDAP proxy for a local file 
and an Active Directo
 
 <property>
     <name>gateway.ldap.interceptor.names</name>
-    <value>localfile,adexample,extrenalldap,duplicatefilter</value>
+    <value>localfile,adexample,extrenalldap,duplicatefilter,rolesLookup</value>
+</property>
+
+<property>
+    <name>gateway.ldap.roles.lookup.strategy</name>
+    <value>rest</value>
+</property>
+
+<property>
+    <name>gateway.ldap.roles.lookup.rest.api.endpoint</name>
+    <value>http://localhost:8080/auth/roles</value>
 </property>
 
 <!-- File-based LDAP backend -->

Reply via email to