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