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