diff --git a/.github/workflows/build/gateway-site.xml b/.github/workflows/build/gateway-site.xml index d8c1bbbd1c..8f35a939f8 100644 --- a/.github/workflows/build/gateway-site.xml +++ b/.github/workflows/build/gateway-site.xml @@ -145,42 +145,50 @@ limitations under the License. gateway.ldap.base.dn dc=hadoop,dc=apache,dc=org - - gateway.ldap.backend.type - ldap - gateway.ldap.recursive.group.resolution true + + gateway.ldap.interceptor.names + demoldap + - gateway.ldap.backend.ldap.url + gateway.ldap.interceptor.demoldap.interceptorType + backend + + + gateway.ldap.interceptor.demoldap.backendType + ldap + + + gateway.ldap.interceptor.demoldap.url ldap://ldap:33389 - gateway.ldap.backend.ldap.remoteBaseDn + gateway.ldap.interceptor.demoldap.remoteBaseDn dc=hadoop,dc=apache,dc=org - gateway.ldap.backend.ldap.systemUsername + gateway.ldap.interceptor.demoldap.systemUsername uid=guest,ou=people,dc=hadoop,dc=apache,dc=org - gateway.ldap.backend.ldap.systemPassword + gateway.ldap.interceptor.demoldap.systemPassword guest-password - gateway.ldap.backend.ldap.userSearchBase + gateway.ldap.interceptor.demoldap.userSearchBase ou=people,dc=hadoop,dc=apache,dc=org - gateway.ldap.backend.ldap.groupSearchBase + gateway.ldap.interceptor.demoldap.groupSearchBase ou=groups,dc=hadoop,dc=apache,dc=org - gateway.ldap.backend.ldap.groupMemberAttribute + gateway.ldap.interceptor.demoldap.groupMemberAttribute member 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 eb3f9f2276..995fa81be8 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 @@ -1754,8 +1754,8 @@ public String getLDAPBaseDN() { } @Override - public String getLDAPBackendType() { - return get(LDAP_BACKEND_TYPE, "file"); + public List getLDAPInterceptorNames() { + return splitConfigValueToList(LDAP_INTERCEPTOR_NAMES); } @Override @@ -1782,9 +1782,9 @@ public Set getPropertyNames() { } @Override - public Map getLDAPBackendConfig(String backendType) { + public Map getLDAPInterceptorConfig(String interceptorName) { Map config = new HashMap<>(); - String prefix = "gateway.ldap.backend." + backendType + "."; + String prefix = "gateway.ldap.interceptor." + interceptorName + "."; for (String key : getPropertyNames()) { if (key != null && key.startsWith(prefix)) { 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 eebf9251e6..ff4edb20e5 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 @@ -17,12 +17,22 @@ */ package org.apache.knox.gateway.services.ldap; +import com.google.common.annotations.VisibleForTesting; +import org.apache.commons.lang3.StringUtils; +import org.apache.directory.api.ldap.model.cursor.Cursor; +import org.apache.directory.api.ldap.model.entry.Attribute; import org.apache.directory.api.ldap.model.entry.DefaultEntry; import org.apache.directory.api.ldap.model.entry.Entry; +import org.apache.directory.api.ldap.model.entry.Value; +import org.apache.directory.api.ldap.model.exception.LdapException; +import org.apache.directory.api.ldap.model.message.SearchRequest; +import org.apache.directory.api.ldap.model.message.SearchRequestImpl; +import org.apache.directory.api.ldap.model.message.SearchScope; import org.apache.directory.api.ldap.model.name.Dn; import org.apache.directory.api.ldap.model.schema.SchemaManager; import org.apache.directory.server.core.DefaultDirectoryService; import org.apache.directory.server.core.api.DirectoryService; +import org.apache.directory.server.core.api.DnFactory; import org.apache.directory.server.core.api.InstanceLayout; import org.apache.directory.server.core.api.interceptor.Interceptor; import org.apache.directory.server.core.api.schema.SchemaPartition; @@ -32,13 +42,16 @@ import org.apache.directory.server.protocol.shared.transport.TcpTransport; import org.apache.knox.gateway.config.GatewayConfig; import org.apache.knox.gateway.i18n.messages.MessagesFactory; -import org.apache.knox.gateway.services.ldap.backend.BackendFactory; -import org.apache.knox.gateway.services.ldap.backend.LdapBackend; +import org.apache.knox.gateway.services.ldap.interceptor.InterceptorFactory; import java.io.File; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Set; /** * Manages the ApacheDS LDAP server instance with pluggable backends @@ -46,13 +59,16 @@ public class KnoxLDAPServerManager { private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class); - private DirectoryService directoryService; + @VisibleForTesting + DirectoryService directoryService; private LdapServer ldapServer; - private LdapBackend backend; + private GatewayConfig gatewayConfig; + private List interceptors; private File workDir; private int port; private String baseDn; - private String remoteBaseDn; + // Collection of DNs for the proxied backend LDAP servers + private Set baseDns; /** * Initialize the LDAP server with the given configuration @@ -60,6 +76,8 @@ public class KnoxLDAPServerManager { * @param config Gateway configuration */ public void initialize(GatewayConfig config) throws Exception { + this.gatewayConfig = config; + // Prepare work directory for LDAP data File gatewayDataDir = new File(config.getGatewayDataDir()); this.workDir = new File(gatewayDataDir, "ldap-server"); @@ -67,27 +85,8 @@ public void initialize(GatewayConfig config) throws Exception { // Get configuration this.port = config.getLDAPPort(); this.baseDn = config.getLDAPBaseDN(); - String backendType = config.getLDAPBackendType(); - - // Get backend-specific configuration using prefixed properties - Map backendConfig = config.getLDAPBackendConfig(backendType); - - // Add common configuration - backendConfig.put("baseDn", baseDn); - - // Add legacy dataFile property for backwards compatibility with file backend - if ("file".equalsIgnoreCase(backendType) && !backendConfig.containsKey("dataFile")) { - backendConfig.put("dataFile", config.getLDAPBackendDataFile()); - } - - backendConfig.put("recursiveGroupResolution", String.valueOf(config.isLDAPRecursiveGroupResolutionEnabled())); - backendConfig.put("recursiveGroupResolutionMaxDepth", String.valueOf(config.getLDAPRecursiveGroupResolutionMaxDepth())); - // For proxy backends, extract remoteBaseDn if present - this.remoteBaseDn = backendConfig.get("remoteBaseDn"); - - // Initialize backend - backend = BackendFactory.createBackend(backendType, backendConfig); + this.interceptors = createInterceptors(config); // Clean up previous run if it didn't shut down cleanly File lockFile = new File(workDir, "run/instance.lock"); @@ -99,6 +98,34 @@ public void initialize(GatewayConfig config) throws Exception { workDir.mkdirs(); } + private List createInterceptors(GatewayConfig config) throws Exception { + List interceptorNames = config.getLDAPInterceptorNames(); + List interceptors = new ArrayList<>(interceptorNames.size()); + for (String interceptorName : interceptorNames) { + // Get backend-specific configuration using prefixed properties + Map 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)) { + interceptorConfig.put("recursiveGroupResolution", String.valueOf(config.isLDAPRecursiveGroupResolutionEnabled())); + interceptorConfig.put("recursiveGroupResolutionMaxDepth", String.valueOf(config.getLDAPRecursiveGroupResolutionMaxDepth())); + if ("file".equalsIgnoreCase(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)); + } + return interceptors; + } + /** * Start the LDAP server */ @@ -134,17 +161,11 @@ public void start() throws Exception { partition.setPartitionPath(new File(workDir, "proxy").toURI()); directoryService.addPartition(partition); - // Create partition for remote base DN if different from proxy base DN - // This allows backend entries with remote DNs to be returned in search results - if (remoteBaseDn != null && !remoteBaseDn.equals(baseDn)) { - JdbmPartition remotePartition = new JdbmPartition(schemaManager, directoryService.getDnFactory()); - remotePartition.setId("remote"); - remotePartition.setSuffixDn(new Dn(schemaManager, remoteBaseDn)); - remotePartition.setPartitionPath(new File(workDir, "remote").toURI()); - directoryService.addPartition(remotePartition); - } + baseDns = new HashSet<>(); + baseDns.add(baseDn); + addRemotePartitions(); - addGroupLookupInterceptor(); + addInterceptors(); // Allow anonymous access directoryService.setAllowAnonymousAccess(true); @@ -152,8 +173,8 @@ public void start() throws Exception { // Start the service directoryService.startup(); - // Add base entries to the partition - createBaseEntries(schemaManager); + // Add base entries to the partitions + createBaseEntries(baseDns, schemaManager); // Create LDAP server on configured port ldapServer = new LdapServer(); @@ -165,25 +186,51 @@ public void start() throws Exception { LOG.ldapServiceStarted(port); } - private void addGroupLookupInterceptor() { - // Add our interceptor for group lookups and bind proxying - // We need to insert it before AuthenticationInterceptor to intercept bind requests - final List interceptors = new ArrayList<>(directoryService.getInterceptors()); + private void addRemotePartitions() throws LdapException { + SchemaManager schemaManager = directoryService.getSchemaManager(); + DnFactory dnFactory = directoryService.getDnFactory(); + List interceptorNames = gatewayConfig.getLDAPInterceptorNames(); + for (String interceptorName : interceptorNames) { + // Get backend-specific configuration using prefixed properties + Map interceptorConfig = gatewayConfig.getLDAPInterceptorConfig(interceptorName); + + String remoteBaseDn = interceptorConfig.get("remoteBaseDn"); + if (StringUtils.isNotBlank(remoteBaseDn)) { + if (!baseDns.contains(remoteBaseDn)) { + //create partition + String id = interceptorName.replaceAll("\\s+", ""); + JdbmPartition remotePartition = new JdbmPartition(schemaManager, dnFactory); + remotePartition.setId(id); + remotePartition.setSuffixDn(new Dn(schemaManager, remoteBaseDn)); + remotePartition.setPartitionPath(new File(workDir, id).toURI()); + directoryService.addPartition(remotePartition); + baseDns.add(remoteBaseDn); + } + } + } + } + + private void addInterceptors() throws LdapException { + // Find location of AuthenticationInterceptor. + // We need to insert interceptors before AuthenticationInterceptor to intercept bind requests + final List dsInterceptors = new ArrayList<>(directoryService.getInterceptors()); int authIdx = -1; - for (int i = 0; i < interceptors.size(); i++) { - if (interceptors.get(i).getName().equalsIgnoreCase("authenticationInterceptor")) { + for (int i = 0; i < dsInterceptors.size(); i++) { + if (dsInterceptors.get(i).getName().equalsIgnoreCase("authenticationInterceptor")) { authIdx = i; break; } } - final GroupLookupInterceptor interceptor = new GroupLookupInterceptor(directoryService, backend); - if (authIdx != -1) { - interceptors.add(authIdx, interceptor); - } else { - interceptors.add(interceptor); + // Add our configured interceptors for group lookups and bind proxying + for (Interceptor interceptor : interceptors) { + if (authIdx != -1) { + dsInterceptors.add(authIdx, interceptor); + } else { + dsInterceptors.add(interceptor); + } } - directoryService.setInterceptors(interceptors); + directoryService.setInterceptors(dsInterceptors); } /** @@ -195,6 +242,7 @@ public void stop() throws Exception { if (ldapServer != null) { try { ldapServer.stop(); + ldapServer = null; } catch (Exception e) { LOG.ldapServiceStopFailed(e); } @@ -203,6 +251,7 @@ public void stop() throws Exception { if (directoryService != null) { try { directoryService.shutdown(); + directoryService = null; } catch (Exception e) { LOG.ldapServiceStopFailed(e); } @@ -211,13 +260,10 @@ public void stop() throws Exception { LOG.ldapServiceStopped(); } - private void createBaseEntries(SchemaManager schemaManager) throws Exception { - // Create base entries for proxy base DN - createBaseEntriesForDn(schemaManager, baseDn); - - // Create base entries for remote base DN if different - if (remoteBaseDn != null && !remoteBaseDn.equals(baseDn)) { - createBaseEntriesForDn(schemaManager, remoteBaseDn); + private void createBaseEntries(Collection baseDns, SchemaManager schemaManager) throws Exception { + // Create base entries for proxy base DN and remote base DNs + for (String baseDn : baseDns) { + createBaseEntriesForDn(schemaManager, baseDn); } } @@ -266,7 +312,32 @@ public String getBaseDn() { * @return List of group names */ public List getUserGroups(String username) throws Exception { - return backend.getUserGroups(username); + SearchRequest searchRequest = new SearchRequestImpl(); + searchRequest.setBase(new Dn(directoryService.getSchemaManager(), baseDn)); + searchRequest.setScope(SearchScope.SUBTREE); + searchRequest.setFilter("(uid=" + username + ")"); + searchRequest.addAttributes("*"); + + List groups = new ArrayList<>(); + try (Cursor cursor = directoryService.getAdminSession().search(searchRequest)) { + if (cursor.next()) { + Entry entry = cursor.get(); + Attribute memberOf = entry.get("memberOf"); + if (memberOf != null) { + for (Value value : memberOf) { + String groupDn = value.getString(); + if (groupDn.toLowerCase(Locale.ROOT).startsWith("cn=")) { + int commaIdx = groupDn.indexOf(','); + if (commaIdx > 0) { + groups.add(groupDn.substring(3, commaIdx)); + } + } + + } + } + } + } + return groups; } /** 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 221e36e4dd..947581247c 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 @@ -49,13 +49,29 @@ public interface LdapMessages { text = "Failed to stop LDAP service: {0}") void ldapServiceStopFailed(@StackTrace(level = MessageLevel.DEBUG) Exception e); + @Message(level = MessageLevel.ERROR, + text = "Interceptor type ''{0}'' for interceptor ''{1}'' not found") + void ldapInterceptorNotFound(String interceptorType, String interceptorName); + + @Message(level = MessageLevel.ERROR, + text = "Interceptor type not found for interceptor ''{0}''") + void ldapInterceptorTypeNotFound(String interceptorName); + + @Message(level = MessageLevel.INFO, + text = "Creating interceptor: {0} (via {1})") + void ldapInterceptorCreating(String interceptorName, String source); + @Message(level = MessageLevel.INFO, text = "Loading backend: {0} (via {1})") void ldapBackendLoading(String backendName, String source); - @Message(level = MessageLevel.WARN, - text = "Backend ''{0}'' not found, using FileBackend") - void ldapBackendNotFound(String backendName); + @Message(level = MessageLevel.ERROR, + text = "Backend type ''{0}'' for backend ''{1}'' not found") + void ldapBackendNotFound(String backendType, String backendName); + + @Message(level = MessageLevel.ERROR, + text = "Backend type not found for backend ''{0}''") + void ldapBackendTypeNotFound(String backendName); @Message(level = MessageLevel.WARN, text = "Data file not found: {0}, creating sample data") @@ -73,6 +89,10 @@ public interface LdapMessages { text = "LDAP Search: {0} | {1}") void ldapSearch(String baseDn, String filter); + @Message(level = MessageLevel.ERROR, + text = "LDAP Search failed: {0} | {1}, {2}") + void ldapSearchFailed(String baseDn, String filter, @StackTrace(level = MessageLevel.DEBUG) Exception e); + @Message(level = MessageLevel.DEBUG, text = "LDAP Bind: {0}") void ldapBind(String dn); diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/BackendFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/BackendFactory.java index f432a6dc5a..a204ac460b 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/BackendFactory.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/BackendFactory.java @@ -32,18 +32,25 @@ public class BackendFactory { private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class); public static LdapBackend createBackend(String backendName, Map config) throws Exception { - // Use ServiceLoader to discover all available backends (built-in and external plugins) - ServiceLoader loader = ServiceLoader.load(LdapBackend.class); - for (LdapBackend backend : loader) { - if (backend.getName().equalsIgnoreCase(backendName)) { - LOG.ldapBackendLoading(backend.getName(), "ServiceLoader"); - backend.initialize(config); + String backendType = config.get("backendType"); + if (backendType == null) { + // No backend type configured found + LOG.ldapBackendTypeNotFound(backendName); + throw new IllegalArgumentException("No LDAP backend type configured for : " + backendName); + } + + // Use ServiceLoader to discover all available backend factories (built-in and external plugins) + ServiceLoader loader = ServiceLoader.load(LdapBackendFactory.class); + for (LdapBackendFactory backendFactory : loader) { + if (backendFactory.getType().equalsIgnoreCase(backendType)) { + LOG.ldapBackendLoading(backendType, "ServiceLoader"); + LdapBackend backend = backendFactory.create(backendName, config); return backend; } } // No matching backend found - LOG.ldapBackendNotFound(backendName); - throw new IllegalArgumentException("No LDAP backend found for type: " + backendName); + LOG.ldapBackendNotFound(backendType, backendName); + throw new IllegalArgumentException("No LDAP backend factory of type " + backendType + " found for : " + backendName); } } diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackend.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackend.java index 9c3d96aed4..8546a3acde 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackend.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackend.java @@ -41,9 +41,12 @@ public class FileBackend implements LdapBackend { private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class); + static final String TYPE = "file"; + private Map users = new HashMap<>(); - private String dataFile; - private String baseDn; + private final String dataFile; + private final String baseDn; + private final String name; static class UserData { String username; @@ -58,16 +61,26 @@ static class BackendData { List users; } + public FileBackend(String name, Map config) throws Exception { + this.name = name; + dataFile = config.getOrDefault("dataFile", "ldap-users.json"); + baseDn = config.getOrDefault("baseDn", "dc=proxy,dc=com"); + loadData(); + } + @Override public String getName() { - return "file"; + return name; } @Override - public void initialize(Map config) throws Exception { - dataFile = config.getOrDefault("dataFile", "ldap-users.json"); - baseDn = config.getOrDefault("baseDn", "dc=proxy,dc=com"); - loadData(); + public String getType() { + return TYPE; + } + + @Override + public String getBaseDn() { + return baseDn; } private void loadData() throws Exception { diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackendFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackendFactory.java new file mode 100644 index 0000000000..8477539405 --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackendFactory.java @@ -0,0 +1,32 @@ +/* + * 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.backend; + +import java.util.Map; + +public class FileBackendFactory implements LdapBackendFactory { + @Override + public LdapBackend create(String name, Map config) throws Exception { + return new FileBackend(name, config); + } + + @Override + public String getType() { + return FileBackend.TYPE; + } +} diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackend.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackend.java index 4c459ff265..f5cc9c75a2 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackend.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackend.java @@ -22,7 +22,6 @@ import org.apache.directory.api.ldap.model.schema.SchemaManager; import java.util.List; -import java.util.Map; /** * Interface for pluggable LDAP backends. @@ -33,16 +32,21 @@ * - REST APIs (Knox, Ranger, etc.) */ public interface LdapBackend { + /** - * Get the name of this backend implementation + * Get the name of this backend */ String getName(); /** - * Initialize the backend with configuration - * @param config Configuration properties + * Get the type of this backend implementation + */ + String getType(); + + /** + * Get the base dn of this backend */ - void initialize(Map config) throws Exception; + String getBaseDn(); /** * Get a user entry by username diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackendFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackendFactory.java new file mode 100644 index 0000000000..2ec00eb373 --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackendFactory.java @@ -0,0 +1,25 @@ +/* + * 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.backend; + +import java.util.Map; + +public interface LdapBackendFactory { + LdapBackend create(String name, Map config) throws Exception; + String getType(); +} 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 59355e4ad6..07337b0494 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 @@ -54,6 +54,9 @@ public class LdapProxyBackend implements LdapBackend { private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class); + static final String TYPE = "ldap"; + + private String name; private String ldapUrl; private String bindDn; private String bindPassword; @@ -83,13 +86,8 @@ public class LdapProxyBackend implements LdapBackend { // Connection pool for efficient connection reuse private LdapConnectionPool connectionPool; - @Override - public String getName() { - return "ldap"; - } - - @Override - public void initialize(Map config) throws Exception { + public LdapProxyBackend(String name, Map config) { + this.name = name; // Proxy base DN is for entries created in the proxy LDAP server proxyBaseDn = config.get("baseDn"); if (proxyBaseDn == null || proxyBaseDn.isEmpty()) { @@ -154,14 +152,28 @@ public void initialize(Map config) throws Exception { initializeConnectionPool(config); } + @Override + public String getName() { + return name; + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public String getBaseDn() { + return remoteBaseDn; + } + /** * Initializes the LDAP connection pool with configurable parameters. * Uses a validating pool to ensure connections remain healthy. * * @param config Configuration map that may contain pool settings - * @throws Exception if connection pool initialization fails */ - private void initializeConnectionPool(Map config) throws Exception { + private void initializeConnectionPool(Map config) { // Configure connection settings LdapConnectionConfig connectionConfig = new LdapConnectionConfig(); connectionConfig.setLdapHost(host); @@ -186,7 +198,7 @@ private void initializeConnectionPool(Map config) throws Excepti connectionPool.setMaxTotal(maxActive); connectionPool.setTestOnBorrow(true); - LOG.ldapBackendLoading(getName(), "Initialized connection pool with maxActive=" + maxActive); + LOG.ldapBackendLoading(getType(), "Initialized connection pool with maxActive=" + maxActive); } private void parseLdapUrl(String url) { diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendFactory.java new file mode 100644 index 0000000000..f138a827d5 --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendFactory.java @@ -0,0 +1,32 @@ +/* + * 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.backend; + +import java.util.Map; + +public class LdapProxyBackendFactory implements LdapBackendFactory { + @Override + public LdapBackend create(String name, Map config) throws Exception { + return new LdapProxyBackend(name, config); + } + + @Override + public String getType() { + return LdapProxyBackend.TYPE; + } +} diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptor.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptor.java new file mode 100644 index 0000000000..fa7287df15 --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptor.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 + * + * 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 com.google.common.annotations.VisibleForTesting; +import org.apache.directory.api.ldap.model.cursor.CursorException; +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.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 java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class DuplicateUserFilteringInterceptor extends BaseInterceptor { + + public DuplicateUserFilteringInterceptor(String name) { + super(name); + } + + @Override + public EntryFilteringCursor search(SearchOperationContext ctx) throws LdapException { + // First execute the interceptor chain to get the results + List filteredEntries = List.of(); + try (EntryFilteringCursor originalResults = next(ctx)) { + List originalEntries = new ArrayList<>(); + try { + while (originalResults.next()) { + originalEntries.add(originalResults.get()); + } + originalResults.close(); + } catch (CursorException e) { + // rethrow exception on incomplete iteration + throw new LdapException(e); + } + filteredEntries = filterDuplicateUsers(originalEntries); + } catch (IOException e) { + // IOException would only occur after finishing iterating over results + // we can ignore this exception and return the filtered entries + } + return new EntryFilteringCursorImpl(new ListCursor<>(filteredEntries), ctx, schemaManager); + } + + @VisibleForTesting + List filterDuplicateUsers(List originalEntries) { + Set seenUids = new HashSet<>(); + List filteredEntries = new ArrayList<>(); + + for (Entry entry : originalEntries) { + Attribute uid = entry.get("uid"); + if (uid == null) { + // keep entry because it's not a user + filteredEntries.add(entry); + } else { + Value uidValue = uid.get(); + if (!seenUids.contains(uidValue)) { + filteredEntries.add(entry); + seenUids.add(uidValue); + } + } + } + return filteredEntries; + } +} 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 new file mode 100644 index 0000000000..b443c2def7 --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorFactory.java @@ -0,0 +1,36 @@ +/* + * 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.server.core.api.interceptor.Interceptor; + +import java.util.Map; + +public class DuplicateUserFilteringInterceptorFactory implements KnoxLdapInterceptorFactory { + public static final String TYPE = "duplicateuserfilter"; + + @Override + public Interceptor create(String name, Map config) { + return new DuplicateUserFilteringInterceptor(name); + } + + @Override + public String getType() { + return TYPE; + } +} 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 new file mode 100644 index 0000000000..6b26cc1162 --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/InterceptorFactory.java @@ -0,0 +1,59 @@ +/* + * 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.server.core.api.interceptor.Interceptor; +import org.apache.knox.gateway.i18n.messages.MessagesFactory; +import org.apache.knox.gateway.services.ldap.LdapMessages; + +import java.util.Map; +import java.util.ServiceLoader; + +/** + * Factory for creating LDAP Interceptor implementations using ServiceLoader for full extensibility. + * Backends are discovered via META-INF/services/org.apache.knox.gateway.services.ldap.interceptor.KnoxLdapInterceptorFactory + * Built-in interceptors are registered via ServiceLoader along with any external plugins. + */ +public class InterceptorFactory { + private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class); + + public static Interceptor createInterceptor(String interceptorName, Map config) throws Exception { + String interceptorType = config.get("interceptorType"); + if (interceptorType == null) { + // No backend type configured found + LOG.ldapInterceptorTypeNotFound(interceptorName); + throw new IllegalArgumentException("No LDAP interceptor type configured for : " + interceptorName); + } + + // Use ServiceLoader to discover all available interceptors (built-in and external plugins) + // Indirect instantiation through a factory is used to allow configuration of multiple instances + // of the same class of interceptor. e.g., if multiple backends are configured + ServiceLoader loader = ServiceLoader.load(KnoxLdapInterceptorFactory.class); + for (KnoxLdapInterceptorFactory interceptorFactory : loader) { + if (interceptorFactory.getType().equalsIgnoreCase(interceptorType)) { + LOG.ldapInterceptorCreating(interceptorType, "ServiceLoader"); + Interceptor interceptor = interceptorFactory.create(interceptorName, config); + return interceptor; + } + } + + // No matching interceptor found + LOG.ldapInterceptorNotFound(interceptorType, interceptorName); + throw new IllegalArgumentException("No LDAP interceptor of type " + interceptorType + " found for : " + interceptorName); + } +} 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 new file mode 100644 index 0000000000..2eb4fb5f9f --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/KnoxLdapInterceptorFactory.java @@ -0,0 +1,41 @@ +/* + * 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.server.core.api.interceptor.Interceptor; + +import java.util.Map; + +/** + * Factory interface for creating Interceptor instances. + */ +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 config) throws Exception; + + /** + * Get the type of interceptor this factory creates + * @return the type of interceptor ths factory creates + */ + String getType(); +} diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/GroupLookupInterceptor.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java similarity index 77% rename from gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/GroupLookupInterceptor.java rename to gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java index 1218c8c78b..1ac2ef5a2f 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/GroupLookupInterceptor.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptor.java @@ -15,13 +15,12 @@ * See the 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.interceptor; import org.apache.directory.api.ldap.model.constants.AuthenticationLevel; import org.apache.directory.api.ldap.model.cursor.ListCursor; 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.schema.SchemaManager; import org.apache.directory.server.core.api.CoreSession; import org.apache.directory.server.core.api.DirectoryService; import org.apache.directory.server.core.api.LdapPrincipal; @@ -32,27 +31,41 @@ import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext; 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.LdapMessages; +import org.apache.knox.gateway.services.ldap.LdapUtils; +import org.apache.knox.gateway.services.ldap.backend.BackendFactory; import org.apache.knox.gateway.services.ldap.backend.LdapBackend; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** - * Interceptor for LDAP operations to proxy user searches to backend when not found locally + * Interceptor for LDAP operations to proxy user searches to backends when not found locally */ -public class GroupLookupInterceptor extends BaseInterceptor { +public class UserSearchInterceptor extends BaseInterceptor { + private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class); - private DirectoryService directoryService; - private LdapBackend backend; private static final Pattern UID_PATTERN = Pattern.compile(".*\\(uid=([^)]+)\\).*"); private static final Pattern CN_PATTERN = Pattern.compile(".*\\(cn=([^)]+)\\).*"); private static final Pattern SAMAACCOUNTNAME_PATTERN = Pattern.compile(".*\\(sAMAccountName=([^)]+)\\).*"); - public GroupLookupInterceptor(DirectoryService directoryService, LdapBackend backend) { - this.directoryService = directoryService; - this.backend = backend; + private final LdapBackend backend; + + public UserSearchInterceptor(String name, Map config) throws Exception { + super(name); + backend = BackendFactory.createBackend(name, config); + } + + public LdapBackend getBackend() { + return backend; + } + + @Override + public void init(DirectoryService directoryService) throws LdapException { + super.init(directoryService); } @Override @@ -78,15 +91,11 @@ public EntryFilteringCursor search(SearchOperationContext ctx) throws LdapExcept LOG.ldapSearch(baseDn, filter); - // First try the normal search + // First execute the next interceptor in the chain EntryFilteringCursor originalResults; - try { - originalResults = next(ctx); - } catch (Exception e) { - throw new LdapException(e); - } + originalResults = next(ctx); - // Check if this is a user search and if we got no results, try the backend + // Check if this is a user search and call the backends if (isUserSearch(filter)) { String username = extractUser(filter); @@ -98,42 +107,40 @@ public EntryFilteringCursor search(SearchOperationContext ctx) throws LdapExcept } originalResults.close(); } catch (Exception e) { - // If we get an error or no results, try the backend + // If we get an error or no results, try the backends } - // If no local results, try backend - if (entries.isEmpty() && username != null) { + if (username != null) { try { - SchemaManager schemaManager = directoryService.getSchemaManager(); - if (username.contains("*")) { // Wildcard search - use searchUsers LOG.ldapSearch(baseDn, "wildcard user search: " + username); - List backendEntries = backend.searchUsers(username, schemaManager); - // Return backend results directly without caching to avoid deadlock // (caching during an active search can cause ApacheDS locking issues) - entries.addAll(backendEntries); + entries.addAll(backend.searchUsers(username, schemaManager)); } else { - // Specific user lookup - LOG.ldapUserLoaded(username); - Entry backendEntry = backend.getUser(username, schemaManager); - - if (backendEntry != null) { - // Return backend result directly without caching - entries.add(backendEntry); - LOG.ldapUserEntry(backendEntry.toString()); - } else { - LOG.ldapUserNull(username); + // if no results, perform single-user search + if (entries.isEmpty()) { + // Specific user lookup + Entry backendEntry = backend.getUser(username, schemaManager); + LOG.ldapUserLoaded(username); + + if (backendEntry != null) { + // Return backend result directly without caching + entries.add(backendEntry); + LOG.ldapUserEntry(backendEntry.toString()); + } else { + LOG.ldapUserNull(username); + } } } } catch (Exception e) { - LOG.ldapServiceStopFailed(e); + LOG.ldapSearchFailed(baseDn, filter, e); } } // Return cursor with our results - use a simple approach - return new EntryFilteringCursorImpl(new ListCursor<>(entries), ctx, directoryService.getSchemaManager()); + return new EntryFilteringCursorImpl(new ListCursor<>(entries), ctx, schemaManager); } return originalResults; 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 new file mode 100644 index 0000000000..862d3a9641 --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/interceptor/UserSearchInterceptorFactory.java @@ -0,0 +1,36 @@ +/* + * 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.server.core.api.interceptor.Interceptor; + +import java.util.Map; + +public class UserSearchInterceptorFactory implements KnoxLdapInterceptorFactory { + public static final String TYPE = "backend"; + + @Override + public Interceptor create(String name, Map config) throws Exception { + return new UserSearchInterceptor(name, config); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackend b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackendFactory similarity index 83% rename from gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackend rename to gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackendFactory index c8bd82de6c..424d400b80 100644 --- a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackend +++ b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackendFactory @@ -16,6 +16,6 @@ # limitations under the License. ########################################################################## -# Built-in LDAP backend implementations -org.apache.knox.gateway.services.ldap.backend.FileBackend -org.apache.knox.gateway.services.ldap.backend.LdapProxyBackend \ No newline at end of file +# Built-in LDAP backend factory implementations +org.apache.knox.gateway.services.ldap.backend.FileBackendFactory +org.apache.knox.gateway.services.ldap.backend.LdapProxyBackendFactory \ No newline at end of file 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 new file mode 100644 index 0000000000..3275b33298 --- /dev/null +++ b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.interceptor.KnoxLdapInterceptorFactory @@ -0,0 +1,21 @@ +########################################################################## +# 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. +########################################################################## + +# Built-in LDAP interceptor factory implementations +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/main/resources/conf/gateway-site.xml b/gateway-server/src/main/resources/conf/gateway-site.xml index ce7525e2d1..70ab3b188a 100644 --- a/gateway-server/src/main/resources/conf/gateway-site.xml +++ b/gateway-server/src/main/resources/conf/gateway-site.xml @@ -63,9 +63,9 @@ limitations under the License. - gateway.ldap.backend.type - ldap - Backend type for LDAP service. Currently supported: file, ldap. Future: jdbc, knox. + gateway.ldap.interceptor.names + ldapproxy,duplicatefilter + Interceptor names for LDAP service in priority order. @@ -74,61 +74,86 @@ limitations under the License. Path to JSON data file for file-based backend. Supports ${GATEWAY_DATA_HOME} variable. - + + + gateway.ldap.interceptor.ldapproxy.interceptorType + backend + Interceptor type. + + + gateway.ldap.interceptor.ldapproxy.backendType + ldap + Backend type for LDAP service. Currently supported: file, ldap. Future: jdbc, knox. + - gateway.ldap.backend.ldap.url + gateway.ldap.interceptor.ldapproxy.url ldap://localhost:33389 LDAP server URL for proxy backend - gateway.ldap.backend.ldap.remoteBaseDn + gateway.ldap.interceptor.ldapproxy.remoteBaseDn dc=hadoop,dc=apache,dc=org Base DN of the remote LDAP server - gateway.ldap.backend.ldap.systemUsername + gateway.ldap.interceptor.ldapproxy.systemUsername uid=guest,ou=people,dc=hadoop,dc=apache,dc=org LDAP bind DN for proxy backend authentication - gateway.ldap.backend.ldap.systemPassword + gateway.ldap.interceptor.ldapproxy.systemPassword guest-password LDAP bind password for proxy backend authentication - + - + - + - + - + + + + gateway.ldap.interceptor.duplicatefilter.interceptorType + duplicateuserfilter + Interceptor type. + + \ No newline at end of file diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java index 4528c8b4a8..c41afb0c00 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java @@ -19,17 +19,22 @@ import org.apache.knox.gateway.config.GatewayConfig; import org.easymock.EasyMock; +import org.apache.directory.api.ldap.model.name.Dn; +import org.apache.knox.gateway.services.ldap.interceptor.UserSearchInterceptor; import org.junit.Test; import org.junit.Before; import org.junit.After; import java.io.File; +import java.net.ServerSocket; import java.util.HashMap; +import java.util.List; import java.util.Map; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.replay; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertFalse; @@ -41,6 +46,7 @@ public class KnoxLDAPServerManagerTest { private KnoxLDAPServerManager serverManager; private File tempWorkDir; private File tempLdapFile; + private int port; @Before public void setUp() throws Exception { @@ -59,6 +65,11 @@ public void setUp() throws Exception { try (java.io.BufferedWriter writer = java.nio.file.Files.newBufferedWriter(tempLdapFile.toPath(), java.nio.charset.StandardCharsets.UTF_8)) { writer.write("{\"users\":[{\"dn\":\"uid=admin,ou=people,dc=test,dc=com\",\"uid\":\"admin\",\"cn\":\"Administrator\",\"userPassword\":\"admin-password\"}],\"groups\":[]}"); } + + // pick an unused port + try (ServerSocket socket = new ServerSocket(0)) { + port = socket.getLocalPort(); + } } @After @@ -77,16 +88,17 @@ public void tearDown() throws Exception { public void testInitializeWithFileBackend() throws Exception { GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class); expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes(); - expect(mockConfig.getLDAPPort()).andReturn(3890).anyTimes(); + expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes(); expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes(); - expect(mockConfig.getLDAPBackendType()).andReturn("file").anyTimes(); + expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes(); expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes(); - expect(mockConfig.getLDAPBackendConfig("file")).andReturn(new HashMap<>()).anyTimes(); + Map backendConfig = createFileBackendInterceptorConfig(); + expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(backendConfig).anyTimes(); replay(mockConfig); serverManager.initialize(mockConfig); - assertEquals("Port should be set correctly", 3890, serverManager.getPort()); + assertEquals("Port should be set correctly", port, serverManager.getPort()); assertEquals("Base DN should be set correctly", "dc=test,dc=com", serverManager.getBaseDn()); assertFalse("Should not be running after initialize", serverManager.isRunning()); } @@ -95,44 +107,62 @@ public void testInitializeWithFileBackend() throws Exception { public void testInitializeWithLdapBackend() throws Exception { GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class); expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes(); - expect(mockConfig.getLDAPPort()).andReturn(3891).anyTimes(); - expect(mockConfig.getLDAPBaseDN()).andReturn("dc=proxy,dc=com").anyTimes(); - expect(mockConfig.getLDAPBackendType()).andReturn("ldap").anyTimes(); - - Map backendConfig = new HashMap<>(); - backendConfig.put("url", "ldap://localhost:33389"); - backendConfig.put("remoteBaseDn", "dc=hadoop,dc=apache,dc=org"); - backendConfig.put("systemUsername", "cn=admin,dc=hadoop,dc=apache,dc=org"); - backendConfig.put("systemPassword", "admin-password"); - - expect(mockConfig.getLDAPBackendConfig("ldap")).andReturn(backendConfig).anyTimes(); + expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes(); + expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes(); + expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("ldapbackend")).anyTimes(); + Map backendConfig = createLdapBackendInterceptorConfig(); + expect(mockConfig.getLDAPInterceptorConfig("ldapbackend")).andReturn(backendConfig).anyTimes(); replay(mockConfig); serverManager.initialize(mockConfig); - assertEquals("Port should be set correctly", 3891, serverManager.getPort()); - assertEquals("Base DN should be set correctly", "dc=proxy,dc=com", serverManager.getBaseDn()); + assertEquals("Port should be set correctly", port, serverManager.getPort()); + assertEquals("Base DN should be set correctly", "dc=test,dc=com", serverManager.getBaseDn()); assertFalse("Should not be running after initialize", serverManager.isRunning()); } @Test(expected = Exception.class) - public void testInitializeWithInvalidBackendType() throws Exception { + public void testInitializeWithInvalidInterceptorType() throws Exception { GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class); expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes(); - expect(mockConfig.getLDAPBackendType()).andReturn("invalid").anyTimes(); - expect(mockConfig.getLDAPBackendConfig("invalid")).andReturn(new HashMap<>()).anyTimes(); + expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("invalid")).anyTimes(); + Map backendConfig = new HashMap<>(); + backendConfig.put("interceptorType", "badinterceptor"); + expect(mockConfig.getLDAPInterceptorConfig("invalid")).andReturn(new HashMap<>()).anyTimes(); replay(mockConfig); serverManager.initialize(mockConfig); } + @Test + public void testInitializeWithMultipleBackends() throws Exception { + GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class); + expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes(); + expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes(); + expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes(); + expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend", "ldapbackend")).anyTimes(); + expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes(); + Map fileBackendConfig = createFileBackendInterceptorConfig(); + expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(fileBackendConfig).anyTimes(); + Map ldapBackendConfig = createLdapBackendInterceptorConfig(); + expect(mockConfig.getLDAPInterceptorConfig("ldapbackend")).andReturn(ldapBackendConfig).anyTimes(); + replay(mockConfig); + + serverManager.initialize(mockConfig); + + assertEquals("Port should be set correctly", port, serverManager.getPort()); + assertEquals("Base DN should be set correctly", "dc=test,dc=com", serverManager.getBaseDn()); + assertFalse("Should not be running after initialize", serverManager.isRunning()); + } + @Test public void testLockFileCleanup() throws Exception { GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class); expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes(); - expect(mockConfig.getLDAPBackendType()).andReturn("file").anyTimes(); + expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes(); expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes(); - expect(mockConfig.getLDAPBackendConfig("file")).andReturn(new HashMap<>()).anyTimes(); + Map backendConfig = createFileBackendInterceptorConfig(); + expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(backendConfig).anyTimes(); replay(mockConfig); // The manager will use tempWorkDir.getParent()/ldap-server as workDir @@ -162,9 +192,11 @@ public void testGettersBeforeInitialization() { public void testStopBeforeStart() throws Exception { GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class); expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes(); - expect(mockConfig.getLDAPBackendType()).andReturn("file").anyTimes(); + expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes(); expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes(); - expect(mockConfig.getLDAPBackendConfig("file")).andReturn(new HashMap<>()).anyTimes(); + Map backendConfig = createFileBackendInterceptorConfig(); + expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(backendConfig).anyTimes(); + replay(mockConfig); serverManager.initialize(mockConfig); @@ -177,9 +209,10 @@ public void testStopBeforeStart() throws Exception { public void testMultipleStopCalls() throws Exception { GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class); expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes(); - expect(mockConfig.getLDAPBackendType()).andReturn("file").anyTimes(); + expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes(); expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes(); - expect(mockConfig.getLDAPBackendConfig("file")).andReturn(new HashMap<>()).anyTimes(); + Map backendConfig = createFileBackendInterceptorConfig(); + expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(backendConfig).anyTimes(); replay(mockConfig); serverManager.initialize(mockConfig); @@ -196,6 +229,119 @@ public void testStartWithoutInitialize() throws Exception { serverManager.start(); } + @Test + public void testStartWithFileBackend() throws Exception { + GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class); + expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes(); + expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes(); + expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes(); + expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes(); + expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes(); + Map backendConfig = createFileBackendInterceptorConfig(); + expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(backendConfig).anyTimes(); + replay(mockConfig); + + serverManager.initialize(mockConfig); + + serverManager.start(); + + UserSearchInterceptor interceptor = (UserSearchInterceptor) serverManager.directoryService.getInterceptor("filebackend"); + assertNotNull("Interceptor should not be null", interceptor); + assertEquals("File backend dn should match configuration", + backendConfig.get("baseDn"), interceptor.getBackend().getBaseDn()); + // LdapNoSuchObjectException will be thrown if expected partition does not exist + serverManager.directoryService.getPartitionNexus().getPartition( + new Dn(serverManager.directoryService.getSchemaManager(), backendConfig.get("baseDn"))); + } + + @Test + public void testStartWithLdapProxyBackend() throws Exception { + GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class); + expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes(); + expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes(); + expect(mockConfig.getLDAPBaseDN()).andReturn("dc=proxy,dc=com").anyTimes(); + expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("ldapbackend")).anyTimes(); + Map backendConfig = createLdapBackendInterceptorConfig(); + expect(mockConfig.getLDAPInterceptorConfig("ldapbackend")).andReturn(backendConfig).anyTimes(); + replay(mockConfig); + + serverManager.initialize(mockConfig); + + serverManager.start(); + + // Ensure that the partitions are created and backends registered with the UserSearchInterceptor + UserSearchInterceptor interceptor = (UserSearchInterceptor) serverManager.directoryService.getInterceptor("ldapbackend"); + assertNotNull("Interceptor should not be null", interceptor); + assertEquals("LDAP backend dn should match configuration", + backendConfig.get("remoteBaseDn"), interceptor.getBackend().getBaseDn()); + // LdapNoSuchObjectException will be thrown if expected partition does not exist + serverManager.directoryService.getPartitionNexus().getPartition( + new Dn(serverManager.directoryService.getSchemaManager(), backendConfig.get("baseDn"))); + } + + @Test + public void testStartWithMultipleBackends() throws Exception { + GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class); + expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes(); + expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes(); + expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes(); + expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend", "ldapbackend")).anyTimes(); + expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes(); + Map fileBackendConfig = createFileBackendInterceptorConfig(); + expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(fileBackendConfig).anyTimes(); + Map ldapBackendConfig = createLdapBackendInterceptorConfig(); + expect(mockConfig.getLDAPInterceptorConfig("ldapbackend")).andReturn(ldapBackendConfig).anyTimes(); + replay(mockConfig); + + serverManager.initialize(mockConfig); + + serverManager.start(); + + // Ensure that the partitions are created and backends registered with the file backend interceptor + UserSearchInterceptor fileInterceptor = (UserSearchInterceptor) serverManager.directoryService.getInterceptor("filebackend"); + assertNotNull("Interceptor should not be null", fileInterceptor); + assertEquals("File backend dn should match configuration", + fileBackendConfig.get("baseDn"), fileInterceptor.getBackend().getBaseDn()); + // LdapNoSuchObjectException will be thrown if expected partition does not exist + serverManager.directoryService.getPartitionNexus().getPartition( + new Dn(serverManager.directoryService.getSchemaManager(), fileBackendConfig.get("baseDn"))); + + // Ensure that the partitions are created and backends registered with the ldap backend interceptor + UserSearchInterceptor ldapInterceptor = (UserSearchInterceptor) serverManager.directoryService.getInterceptor("ldapbackend"); + assertNotNull("Interceptor should not be null", ldapInterceptor); + assertEquals("LDAP backend dn should match configuration", + ldapBackendConfig.get("remoteBaseDn"), ldapInterceptor.getBackend().getBaseDn()); + // LdapNoSuchObjectException will be thrown if expected partition does not exist + serverManager.directoryService.getPartitionNexus().getPartition( + new Dn(serverManager.directoryService.getSchemaManager(), ldapBackendConfig.get("baseDn"))); + } + + @Test + public void testGetUserGroups() { + + } + + private Map createFileBackendInterceptorConfig() { + Map config = new HashMap<>(); + config.put("interceptorType", "backend"); + config.put("backendType", "file"); + config.put("baseDn", "dc=file,dc=com"); + config.put("dataFile", tempLdapFile.getAbsolutePath()); + return config; + } + + private Map createLdapBackendInterceptorConfig() { + Map config = new HashMap<>(); + config.put("interceptorType", "backend"); + config.put("backendType", "ldap"); + config.put("baseDn", "dc=proxy,dc=com"); + config.put("url", "ldap://localhost:33389"); + config.put("remoteBaseDn", "dc=hadoop,dc=apache,dc=org"); + config.put("systemUsername", "cn=admin,dc=hadoop,dc=apache,dc=org"); + config.put("systemPassword", "admin-password"); + return config; + } + private void cleanupTempFiles() { if (tempLdapFile != null && tempLdapFile.exists()) { tempLdapFile.delete(); 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 40777b2534..399e054854 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 @@ -25,6 +25,7 @@ import java.io.File; import java.util.HashMap; +import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; @@ -169,13 +170,15 @@ private void setupMockConfig(String backendType) { expect(mockConfig.getGatewayDataDir()).andReturn(tempDataDir.getAbsolutePath()).atLeastOnce(); expect(mockConfig.getLDAPPort()).andReturn(3890).times(1).andReturn(3891).anyTimes(); expect(mockConfig.getLDAPBaseDN()).andReturn("file".equals(backendType) ? "dc=test,dc=com" : "dc=proxy,dc=com").atLeastOnce(); - expect(mockConfig.getLDAPBackendType()).andReturn(backendType).atLeastOnce(); - expect(mockConfig.getLDAPBackendConfig(backendType)).andReturn(buildBackendConfig(backendType)).atLeastOnce(); + expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("testbackend")).atLeastOnce(); + expect(mockConfig.getLDAPInterceptorConfig("testbackend")).andReturn(buildBackendConfig(backendType)).atLeastOnce(); replay(mockConfig); } private Map buildBackendConfig(String backendType) { final Map backendConfig = new HashMap<>(); + backendConfig.put("interceptorType", "backend"); + backendConfig.put("backendType", backendType); if ("ldap".equals(backendType)) { backendConfig.put("url", "ldap://localhost:33389"); backendConfig.put("remoteBaseDn", "dc=hadoop,dc=apache,dc=org"); diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/BackendFactoryTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/BackendFactoryTest.java index fde29a0870..cfb8fa2d5d 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/BackendFactoryTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/BackendFactoryTest.java @@ -54,20 +54,20 @@ public void setUp() throws Exception { @Test public void testServiceLoaderDiscovery() { - ServiceLoader loader = ServiceLoader.load(LdapBackend.class); + ServiceLoader loader = ServiceLoader.load(LdapBackendFactory.class); // Should discover at least the built-in backends boolean foundFileBackend = false; boolean foundLdapBackend = false; - for (LdapBackend backend : loader) { - String backendName = backend.getName(); - if ("file".equals(backendName)) { + for (LdapBackendFactory factory : loader) { + String backendType = factory.getType(); + if ("file".equals(backendType)) { foundFileBackend = true; - assertTrue("File backend should be FileBackend instance", backend instanceof FileBackend); - } else if ("ldap".equals(backendName)) { + assertTrue("File backend should be FileBackend instance", factory instanceof FileBackendFactory); + } else if ("ldap".equals(backendType)) { foundLdapBackend = true; - assertTrue("LDAP backend should be LdapProxyBackend instance", backend instanceof LdapProxyBackend); + assertTrue("LDAP backend should be LdapProxyBackend instance", factory instanceof LdapProxyBackendFactory); } } @@ -77,48 +77,57 @@ public void testServiceLoaderDiscovery() { @Test public void testCreateFileBackend() throws Exception { - LdapBackend fileBackend = BackendFactory.createBackend("file", config); + config.put("backendType", "file"); + LdapBackend fileBackend = BackendFactory.createBackend("testbackend", config); assertNotNull("File backend should be created", fileBackend); assertTrue("Should create FileBackend instance", fileBackend instanceof FileBackend); - assertEquals("Backend name should be 'file'", "file", fileBackend.getName()); + assertEquals("Backend type should be 'file'", "file", fileBackend.getType()); + assertEquals("Backend name should be 'testbackend'", "testbackend", fileBackend.getName()); } @Test public void testCreateLdapBackend() throws Exception { + config.put("backendType", "ldap"); config.put("url", "ldap://localhost:389"); config.put("remoteBaseDn", "dc=hadoop,dc=apache,dc=org"); - LdapBackend ldapBackend = BackendFactory.createBackend("ldap", config); + LdapBackend ldapBackend = BackendFactory.createBackend("testbackend", config); assertNotNull("LDAP backend should be created", ldapBackend); assertTrue("Should create LdapProxyBackend instance", ldapBackend instanceof LdapProxyBackend); - assertEquals("Backend name should be 'ldap'", "ldap", ldapBackend.getName()); + assertEquals("Backend type should be 'ldap'", "ldap", ldapBackend.getType()); + assertEquals("Backend name should be 'testbackend'", "testbackend", ldapBackend.getName()); } @Test - public void testCaseInsensitiveBackendNames() throws Exception { + public void testCaseInsensitiveFileBackend() throws Exception { // Test uppercase - LdapBackend upperCaseBackend = BackendFactory.createBackend("FILE", config); - assertTrue("Should create FileBackend with uppercase name", upperCaseBackend instanceof FileBackend); + config.put("backendType", "FILE"); + LdapBackend upperCaseBackend = BackendFactory.createBackend("UPPER", config); + assertTrue("Should create FileBackend with uppercase type", upperCaseBackend instanceof FileBackend); // Test mixed case - LdapBackend mixedCaseBackend = BackendFactory.createBackend("File", config); - assertTrue("Should create FileBackend with mixed case name", mixedCaseBackend instanceof FileBackend); + config.put("backendType", "File"); + LdapBackend mixedCaseBackend = BackendFactory.createBackend("Mixed", config); + assertTrue("Should create FileBackend with mixed case type", mixedCaseBackend instanceof FileBackend); } @Test(expected = IllegalArgumentException.class) public void testUnknownBackendThrowsException() throws Exception { - BackendFactory.createBackend("unknown", config); + config.put("backendType", "unknown"); + BackendFactory.createBackend("testbackend", config); } @Test(expected = IllegalArgumentException.class) public void testNullBackendNameThrowsException() throws Exception { - BackendFactory.createBackend(null, config); + config.put("backendType", null); + BackendFactory.createBackend("testbackend", config); } @Test(expected = IllegalArgumentException.class) public void testEmptyBackendNameThrowsException() throws Exception { - BackendFactory.createBackend("", config); + config.put("backendType", ""); + BackendFactory.createBackend("testbackend", config); } } \ No newline at end of file diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendTest.java index 0aba49487b..85152cea0a 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendTest.java @@ -39,7 +39,6 @@ import org.apache.knox.gateway.services.ldap.SchemaManagerFactory; import org.junit.After; import org.junit.AfterClass; -import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -137,20 +136,17 @@ public static void tearDownAfterClass() throws Exception { } } - @Before - public void setUp() throws Exception { - ldapProxyBackend = new LdapProxyBackend(); - } - @After public void tearDown() throws Exception { - ldapProxyBackend.close(); + if (ldapProxyBackend != null) { + ldapProxyBackend.close(); + } } @Test public void testGetUserByDefaultUserSearchFilter() throws Exception { // default searches by uid and uses group search for membership - ldapProxyBackend.initialize(ldapBackendConfig); + ldapProxyBackend = new LdapProxyBackend("testbackend", ldapBackendConfig); Entry entry = ldapProxyBackend.getUser("ldaptest1", schemaManager); validateUserEntry(entry, "ldaptest1", "TestCn1", "ldaptest1@example.com", "Test user ldaptest1"); @@ -161,7 +157,7 @@ public void testGetUserByDefaultUserSearchFilter() throws Exception { @Test public void testGetUserNotFound() throws Exception { - ldapProxyBackend.initialize(ldapBackendConfig); + ldapProxyBackend = new LdapProxyBackend("testbackend", ldapBackendConfig); Entry entry = ldapProxyBackend.getUser("nouser", schemaManager); assertNull(entry); @@ -170,7 +166,7 @@ public void testGetUserNotFound() throws Exception { @Test public void testGetUserByUID() throws Exception { Map config = createConfigWithUserAttr("uid"); - ldapProxyBackend.initialize(config); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); Entry entry = ldapProxyBackend.getUser("ldaptest1", schemaManager); validateUserEntry(entry, "ldaptest1", "TestCn1", "ldaptest1@example.com", "Test user ldaptest1"); @@ -182,7 +178,7 @@ public void testGetUserByUID() throws Exception { @Test public void testGetUserByCN() throws Exception { Map config = createConfigWithUserAttr("cn"); - ldapProxyBackend.initialize(config); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); Entry entry = ldapProxyBackend.getUser("TestCn1", schemaManager); validateUserEntry(entry, "TestCn1", "TestCn1", "ldaptest1@example.com", "Test user ldaptest1"); @@ -194,7 +190,7 @@ public void testGetUserByCN() throws Exception { @Test public void testGetUserBySAMAccountName() throws Exception { Map config = createConfigWithUserAttr("sAMAccountName"); - ldapProxyBackend.initialize(config); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); Entry entry = ldapProxyBackend.getUser("TestSam1", schemaManager); validateUserEntry(entry, "TestSam1", "TestCn1", "ldaptest1@example.com", "Test user ldaptest1"); @@ -208,7 +204,7 @@ public void testGetUserBySAMAccountName() throws Exception { public void testGetUserUseMemberOf() throws Exception { Map config = new HashMap<>(ldapBackendConfig); config.put("useMemberOf", "true"); - ldapProxyBackend.initialize(config); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); Entry entry = ldapProxyBackend.getUser("ldaptest2", schemaManager); validateUserEntry(entry, "ldaptest2", "TestCn2", "ldaptest2@example.com", "Test user ldaptest2"); @@ -219,7 +215,7 @@ public void testGetUserUseMemberOf() throws Exception { @Test public void testGetUserGroups() throws Exception { - ldapProxyBackend.initialize(ldapBackendConfig); + ldapProxyBackend = new LdapProxyBackend("testbackend", ldapBackendConfig); List userGroups = ldapProxyBackend.getUserGroups("ldaptest1"); assertTrue(userGroups.contains("group1")); @@ -228,7 +224,7 @@ public void testGetUserGroups() throws Exception { @Test public void testGetUserGroupsNoGroups() throws Exception { - ldapProxyBackend.initialize(ldapBackendConfig); + ldapProxyBackend = new LdapProxyBackend("testbackend", ldapBackendConfig); List userGroups = ldapProxyBackend.getUserGroups("ldaptest2"); assertTrue(userGroups.isEmpty()); @@ -236,7 +232,7 @@ public void testGetUserGroupsNoGroups() throws Exception { @Test public void testGetUserGroupsNoUser() throws Exception { - ldapProxyBackend.initialize(ldapBackendConfig); + ldapProxyBackend = new LdapProxyBackend("testbackend", ldapBackendConfig); List userGroups = ldapProxyBackend.getUserGroups("nobody"); assertTrue(userGroups.isEmpty()); @@ -246,7 +242,7 @@ public void testGetUserGroupsNoUser() throws Exception { public void testGetUserGroupsUseMemberOf() throws Exception { Map config = new HashMap<>(ldapBackendConfig); config.put("useMemberOf", "true"); - ldapProxyBackend.initialize(config); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); List userGroups = ldapProxyBackend.getUserGroups("ldaptest2"); assertTrue(userGroups.contains("groupMemberOf1")); @@ -257,7 +253,7 @@ public void testGetUserGroupsUseMemberOf() throws Exception { public void testGetUserGroupsUseMemberOfNoGroups() throws Exception { Map config = new HashMap<>(ldapBackendConfig); config.put("useMemberOf", "true"); - ldapProxyBackend.initialize(config); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); List userGroups = ldapProxyBackend.getUserGroups("ldaptest1"); assertTrue(userGroups.isEmpty()); @@ -267,7 +263,7 @@ public void testGetUserGroupsUseMemberOfNoGroups() throws Exception { public void testGetUserGroupsUseMemberOfNoUser() throws Exception { Map config = new HashMap<>(ldapBackendConfig); config.put("useMemberOf", "true"); - ldapProxyBackend.initialize(config); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); List userGroups = ldapProxyBackend.getUserGroups("nobody"); assertTrue(userGroups.isEmpty()); @@ -275,19 +271,19 @@ public void testGetUserGroupsUseMemberOfNoUser() throws Exception { @Test public void testSearchUsers() throws Exception { - ldapProxyBackend.initialize(ldapBackendConfig); + ldapProxyBackend = new LdapProxyBackend("testbackend", ldapBackendConfig); validateUserSearch("*", 3, Set.of("ldaptest1", "ldaptest2", "guest")); } @Test public void testSearchUsersPartial() throws Exception { - ldapProxyBackend.initialize(ldapBackendConfig); + ldapProxyBackend = new LdapProxyBackend("testbackend", ldapBackendConfig); validateUserSearch("ldap*", 2, Set.of("ldaptest1", "ldaptest2")); } @Test public void testSearchUsersNoneFound() throws Exception { - ldapProxyBackend.initialize(ldapBackendConfig); + ldapProxyBackend = new LdapProxyBackend("testbackend", ldapBackendConfig); List entries = ldapProxyBackend.searchUsers("nobody*", schemaManager); assertTrue(entries.isEmpty()); } @@ -295,21 +291,21 @@ public void testSearchUsersNoneFound() throws Exception { @Test public void testSearchUsersByCn() throws Exception { Map config = createConfigWithUserAttr("cn"); - ldapProxyBackend.initialize(config); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); validateUserSearch("*", 3, Set.of("TestCn1", "TestCn2", "Guest")); } @Test public void testSearchUsersPartialByCn() throws Exception { Map config = createConfigWithUserAttr("cn"); - ldapProxyBackend.initialize(config); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); validateUserSearch("TestCn*", 2, Set.of("TestCn1", "TestCn2")); } @Test public void testSearchUsersNoneFoundByCn() throws Exception { Map config = createConfigWithUserAttr("cn"); - ldapProxyBackend.initialize(config); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); List entries = ldapProxyBackend.searchUsers("nobody*", schemaManager); assertTrue(entries.isEmpty()); } @@ -317,21 +313,21 @@ public void testSearchUsersNoneFoundByCn() throws Exception { @Test public void testSearchUsersBySAMAccountName() throws Exception { Map config = createConfigWithUserAttr("sAMAccountName"); - ldapProxyBackend.initialize(config); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); validateUserSearch("*", 2, Set.of("TestSam1", "TestSam2")); } @Test public void testSearchUsersPartialBySAMAccountName() throws Exception { Map config = createConfigWithUserAttr("sAMAccountName"); - ldapProxyBackend.initialize(config); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); validateUserSearch("TestSam*", 2, Set.of("TestSam1", "TestSam2")); } @Test public void testSearchUsersNoneFoundBySAMAccountName() throws Exception { Map config = createConfigWithUserAttr("sAMAccountName"); - ldapProxyBackend.initialize(config); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); List entries = ldapProxyBackend.searchUsers("nobody*", schemaManager); assertTrue(entries.isEmpty()); } @@ -339,7 +335,7 @@ public void testSearchUsersNoneFoundBySAMAccountName() throws Exception { @Test public void testGetRecursiveUserGroupsDepth2() throws Exception { Map config = createRecursiveConfig(2); - ldapProxyBackend.initialize(config); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); List userGroups = ldapProxyBackend.getUserGroups("recursiveUser"); assertEquals(4, userGroups.size()); @@ -352,7 +348,7 @@ public void testGetRecursiveUserGroupsDepth2() throws Exception { @Test public void testGetRecursiveUserGroupsDepth4() throws Exception { Map config = createRecursiveConfig(4); - ldapProxyBackend.initialize(config); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); List userGroups = ldapProxyBackend.getUserGroups("recursiveUser"); assertEquals(6, userGroups.size()); @@ -367,7 +363,7 @@ public void testGetRecursiveUserGroupsDepth4() throws Exception { @Test public void testGetRecursiveUserGroupsWithCycle() throws Exception { Map config = createRecursiveConfig(10); - ldapProxyBackend.initialize(config); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); List userGroups = ldapProxyBackend.getUserGroups("recursiveUser"); assertTrue(userGroups.contains("cycleGroupA")); @@ -377,7 +373,7 @@ public void testGetRecursiveUserGroupsWithCycle() throws Exception { @Test public void testGetUserRecursiveGroups() throws Exception { Map config = createRecursiveConfig(5); - ldapProxyBackend.initialize(config); + ldapProxyBackend = new LdapProxyBackend("testbackend", config); Entry entry = ldapProxyBackend.getUser("recursiveUser", schemaManager); validateMemberOf(entry, Set.of( @@ -394,7 +390,7 @@ public void testSearchUsersRecursiveWithSharedGroups() throws Exception { Map config = createRecursiveConfig(5); final AtomicInteger cacheHits = new AtomicInteger(0); - ldapProxyBackend = new LdapProxyBackend() { + ldapProxyBackend = new LdapProxyBackend("testbackend", config) { @Override protected Map> createResolvedParentsCache() { return new HashMap<>() { @@ -408,7 +404,6 @@ public Set< org.apache.directory.api.ldap.model.entry.Entry> get(Object key) { }; } }; - ldapProxyBackend.initialize(config); // Search for all recursive users (recursiveUser and recursiveUser2) // They share level1Group, cycleGroupA, and all their ancestors. diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorTest.java new file mode 100644 index 0000000000..828937074e --- /dev/null +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/interceptor/DuplicateUserFilteringInterceptorTest.java @@ -0,0 +1,166 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +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.DefaultEntry; +import org.apache.directory.api.ldap.model.entry.Entry; +import org.apache.directory.api.ldap.model.entry.Value; +import org.apache.directory.api.ldap.model.exception.LdapException; +import org.apache.directory.api.ldap.model.schema.SchemaManager; +import org.apache.directory.server.core.api.CoreSession; +import org.apache.directory.server.core.api.DirectoryService; +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.security.ldap.SimpleDirectoryService; +import org.apache.knox.gateway.services.ldap.SchemaManagerFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +/** + * Unit tests for DuplicateUserFilteringInterceptor. + */ +public class DuplicateUserFilteringInterceptorTest { + + private static final String TEST_INTERCEPTOR = "TEST"; + private static final String NEXT_INTERCEPTOR = "NEXT"; + + private DuplicateUserFilteringInterceptor interceptor; + + private DirectoryService directoryService; + private SchemaManager schemaManager; + private ConfigurableEntriesTestInterceptor nextInterceptor; + private SearchOperationContext ctx; + private CoreSession session; + + @Before + public void setUp() throws Exception { + directoryService = new SimpleDirectoryService(); + directoryService.setShutdownHookEnabled(false); + schemaManager = SchemaManagerFactory.createSchemaManager(); + directoryService.setSchemaManager(schemaManager); + + interceptor = new DuplicateUserFilteringInterceptor(TEST_INTERCEPTOR); + interceptor.init(directoryService); + directoryService.addLast(interceptor); + + nextInterceptor = new ConfigurableEntriesTestInterceptor(NEXT_INTERCEPTOR); + nextInterceptor.init(directoryService); + directoryService.addLast(nextInterceptor); + + session = directoryService.getSession(); + + ctx = new SearchOperationContext(session); + ctx.setInterceptors(List.of(TEST_INTERCEPTOR, NEXT_INTERCEPTOR)); + + } + + @After + public void tearDown() throws Exception { + directoryService.shutdown(); + } + + @Test + public void testEmptyCursor() throws Exception { + nextInterceptor.setEntries(List.of()); + + try (EntryFilteringCursor results = interceptor.search(ctx)) { + assertFalse("Results should be empty", results.next()); + } + + EntryFilteringCursor nextInterceptorCursor = nextInterceptor.getCursor(); + assertTrue("Cursor must be closed", nextInterceptorCursor.isClosed()); + } + + @Test + public void testNoDuplicateEntries() throws Exception { + Entry entry1 = new DefaultEntry(schemaManager); + entry1.add("uid", "user1"); + Entry entry2 = new DefaultEntry(schemaManager); + entry2.add("uid", "user2"); + nextInterceptor.setEntries(List.of(entry1, entry2)); + + try (EntryFilteringCursor results = interceptor.search(ctx)) { + assertNextEntryUid(results, "user1"); + assertNextEntryUid(results, "user2"); + assertFalse("No more entries expected", results.next()); + } + + EntryFilteringCursor nextInterceptorCursor = nextInterceptor.getCursor(); + assertTrue("Cursor must be closed", nextInterceptorCursor.isClosed()); + } + + @Test + public void testDuplicateEntries() throws Exception { + Entry entry1 = new DefaultEntry(schemaManager); + entry1.add("uid", "user1"); + Entry entry2 = new DefaultEntry(schemaManager); + entry2.add("uid", "user1"); + nextInterceptor.setEntries(List.of(entry1, entry2)); + + try (EntryFilteringCursor results = interceptor.search(ctx)) { + assertNextEntryUid(results, "user1"); + assertFalse("No more entries expected", results.next()); + } + + EntryFilteringCursor nextInterceptorCursor = nextInterceptor.getCursor(); + assertTrue("Cursor must be closed", nextInterceptorCursor.isClosed()); + } + + private void assertNextEntryUid(EntryFilteringCursor cursor, String uid) throws Exception { + assertTrue("Cursor should have another entry", cursor.next()); + Entry entry = cursor.get(); + Attribute uidAttr = entry.get("uid"); + assertEquals("Attribute should have only one value", 1, uidAttr.size()); + Value value = uidAttr.get(); + assertEquals("Uid should match " + uid, uid, value.getString()); + } + + private static class ConfigurableEntriesTestInterceptor extends BaseInterceptor { + private List entries; + private EntryFilteringCursor cursor; + + ConfigurableEntriesTestInterceptor(String name) { + super(name); + } + + public void setEntries(List entries) { + this.entries = entries; + } + + public EntryFilteringCursor getCursor() { + return cursor; + } + + @Override + public EntryFilteringCursor search(SearchOperationContext searchContext) throws LdapException { + cursor = new EntryFilteringCursorImpl(new ListCursor<>(entries), searchContext, schemaManager); + return cursor; + } + } +} \ No newline at end of file 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 80ab4e0c82..745002ae3b 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 @@ -1246,8 +1246,8 @@ public String getLDAPBaseDN() { } @Override - public String getLDAPBackendType() { - return "file"; + public List getLDAPInterceptorNames() { + return List.of("testinterceptor"); } @Override @@ -1261,7 +1261,7 @@ public Set getPropertyNames() { } @Override - public Map getLDAPBackendConfig(String backendType) { + public Map getLDAPInterceptorConfig(String backendType) { return Collections.emptyMap(); } 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 6116a3d4c5..7ecb2980d3 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 @@ -128,7 +128,7 @@ public interface GatewayConfig { String LDAP_ENABLED = "gateway.ldap.enabled"; String LDAP_PORT = "gateway.ldap.port"; String LDAP_BASE_DN = "gateway.ldap.base.dn"; - String LDAP_BACKEND_TYPE = "gateway.ldap.backend.type"; + String LDAP_INTERCEPTOR_NAMES = "gateway.ldap.interceptor.names"; 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"; @@ -1069,10 +1069,10 @@ public interface GatewayConfig { */ String getLDAPBaseDN(); - /** - * @return the backend type for LDAP (file, ldap, jdbc, etc.) - */ - String getLDAPBackendType(); + /** + * @return the list of interceptor names for LDAP server + */ + List getLDAPInterceptorNames(); /** * @return the path to the data file for file-based backend @@ -1080,14 +1080,14 @@ public interface GatewayConfig { String getLDAPBackendDataFile(); /** - * Get backend-specific configuration properties. - * Returns all properties with prefix "gateway.ldap.backend.{backendType}." + * Get interceptor-specific configuration properties. + * Returns all properties with prefix "gateway.ldap.interceptor.{interceptorName}." * with the prefix stripped from the keys. * - * @param backendType the backend type (e.g., "file", "ldap", "database") + * @param interceptor the interceptor name * @return map of configuration key-value pairs for the specified backend */ - Map getLDAPBackendConfig(String backendType); + Map getLDAPInterceptorConfig(String interceptor); /** * @return true if recursive group resolution is enabled for LDAP service