This is an automated email from the ASF dual-hosted git repository.
smolnar82 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git
The following commit(s) were added to refs/heads/master by this push:
new 9c6647e5f KNOX-3337: Add optional LDAP role lookup support (#1247)
9c6647e5f is described below
commit 9c6647e5f53902647906ac92bf15b9041691c110
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 | 3 +-
.../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 | 3 +-
.../services/ldap/LDAPRolesLookupService.java | 27 ++--
.../knox/gateway/services/ldap/RoleAssignment.java | 61 +++++++++
knox-site/docs/service_ldap_server.md | 15 ++-
45 files changed, 1400 insertions(+), 114 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 0afc0dfba..a38026f9a 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));
}
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 5065ea7f1..bd808747c 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
@@ -34,3 +34,4 @@
org.apache.knox.gateway.services.factory.TopologyServiceFactory
org.apache.knox.gateway.services.factory.ConcurrentSessionVerifierFactory
org.apache.knox.gateway.services.factory.GatewayStatusServiceFactory
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 b27358c7c..ff58ad6f9 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
@@ -66,7 +66,8 @@ public class AbstractGatewayServicesTest {
ServiceType.CONCURRENT_SESSION_VERIFIER,
ServiceType.REMOTE_CONFIGURATION_MONITOR,
ServiceType.GATEWAY_STATUS_SERVICE,
- ServiceType.LDAP_SERVICE
+ ServiceType.LDAP_SERVICE,
+ ServiceType.LDAP_ROLES_LOOKUP_SERVICE
};
assertNotEquals(ServiceType.values(), orderedServiceTypes);
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 d96db8d49..5caeac0e6 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
@@ -39,7 +39,8 @@ public enum ServiceType {
CONCURRENT_SESSION_VERIFIER("ConcurrentSessionVerifier"),
REMOTE_CONFIGURATION_MONITOR("RemoteConfigurationMonitor"),
GATEWAY_STATUS_SERVICE("GatewayStatusService"),
- LDAP_SERVICE("LDAPService");
+ LDAP_SERVICE("LDAPService"),
+ LDAP_ROLES_LOOKUP_SERVICE("LDAPRoleLookupService");
private final String serviceTypeName;
private final String shortName;
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 -->