This is an automated email from the ASF dual-hosted git repository.
smolnar82 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git
The following commit(s) were added to refs/heads/master by this push:
new 33df7d5da KNOX-3330: Refactor LDAP Proxy configuration to support
multiple backends (#1240)
33df7d5da is described below
commit 33df7d5da5faf02728a587953fbddaec494ef220
Author: David Han <[email protected]>
AuthorDate: Mon Jun 8 05:30:24 2026 -0500
KNOX-3330: Refactor LDAP Proxy configuration to support multiple backends
(#1240)
---
.github/workflows/build/gateway-site.xml | 30 ++--
.../gateway/config/impl/GatewayConfigImpl.java | 8 +-
.../services/ldap/KnoxLDAPServerManager.java | 185 +++++++++++++------
.../knox/gateway/services/ldap/LdapMessages.java | 26 ++-
.../services/ldap/backend/BackendFactory.java | 23 ++-
.../gateway/services/ldap/backend/FileBackend.java | 27 ++-
.../services/ldap/backend/FileBackendFactory.java | 32 ++++
.../gateway/services/ldap/backend/LdapBackend.java | 14 +-
.../services/ldap/backend/LdapBackendFactory.java | 25 +++
.../services/ldap/backend/LdapProxyBackend.java | 32 ++--
.../ldap/backend/LdapProxyBackendFactory.java | 32 ++++
.../DuplicateUserFilteringInterceptor.java | 87 +++++++++
.../DuplicateUserFilteringInterceptorFactory.java | 36 ++++
.../ldap/interceptor/InterceptorFactory.java | 59 ++++++
.../interceptor/KnoxLdapInterceptorFactory.java | 41 +++++
.../UserSearchInterceptor.java} | 79 ++++----
.../interceptor/UserSearchInterceptorFactory.java | 36 ++++
.../java/org/apache/knox/gateway/util/KnoxCLI.java | 1 +
...teway.services.ldap.backend.LdapBackendFactory} | 6 +-
...es.ldap.interceptor.KnoxLdapInterceptorFactory} | 6 +-
.../src/main/resources/conf/gateway-site.xml | 113 ++++++++----
.../services/ldap/KnoxLDAPServerManagerTest.java | 198 ++++++++++++++++++---
.../gateway/services/ldap/KnoxLDAPServiceTest.java | 7 +-
.../services/ldap/backend/BackendFactoryTest.java | 47 +++--
.../ldap/backend/LdapProxyBackendTest.java | 63 +++----
.../DuplicateUserFilteringInterceptorTest.java | 166 +++++++++++++++++
.../org/apache/knox/gateway/GatewayTestConfig.java | 6 +-
.../apache/knox/gateway/config/GatewayConfig.java | 18 +-
knox-site/docs/service_ldap_server.md | 125 +++++++++----
29 files changed, 1223 insertions(+), 305 deletions(-)
diff --git a/.github/workflows/build/gateway-site.xml
b/.github/workflows/build/gateway-site.xml
index d8c1bbbd1..8f35a939f 100644
--- a/.github/workflows/build/gateway-site.xml
+++ b/.github/workflows/build/gateway-site.xml
@@ -145,42 +145,50 @@ limitations under the License.
<name>gateway.ldap.base.dn</name>
<value>dc=hadoop,dc=apache,dc=org</value>
</property>
- <property>
- <name>gateway.ldap.backend.type</name>
- <value>ldap</value>
- </property>
<property>
<name>gateway.ldap.recursive.group.resolution</name>
<value>true</value>
</property>
+ <property>
+ <name>gateway.ldap.interceptor.names</name>
+ <value>demoldap</value>
+ </property>
<!-- LDAP Backend specific configuration (proxying to demo ldap) -->
<property>
- <name>gateway.ldap.backend.ldap.url</name>
+ <name>gateway.ldap.interceptor.demoldap.interceptorType</name>
+ <value>backend</value>
+ </property>
+ <property>
+ <name>gateway.ldap.interceptor.demoldap.backendType</name>
+ <value>ldap</value>
+ </property>
+ <property>
+ <name>gateway.ldap.interceptor.demoldap.url</name>
<value>ldap://ldap:33389</value>
</property>
<property>
- <name>gateway.ldap.backend.ldap.remoteBaseDn</name>
+ <name>gateway.ldap.interceptor.demoldap.remoteBaseDn</name>
<value>dc=hadoop,dc=apache,dc=org</value>
</property>
<property>
- <name>gateway.ldap.backend.ldap.systemUsername</name>
+ <name>gateway.ldap.interceptor.demoldap.systemUsername</name>
<value>uid=guest,ou=people,dc=hadoop,dc=apache,dc=org</value>
</property>
<property>
- <name>gateway.ldap.backend.ldap.systemPassword</name>
+ <name>gateway.ldap.interceptor.demoldap.systemPassword</name>
<value>guest-password</value>
</property>
<property>
- <name>gateway.ldap.backend.ldap.userSearchBase</name>
+ <name>gateway.ldap.interceptor.demoldap.userSearchBase</name>
<value>ou=people,dc=hadoop,dc=apache,dc=org</value>
</property>
<property>
- <name>gateway.ldap.backend.ldap.groupSearchBase</name>
+ <name>gateway.ldap.interceptor.demoldap.groupSearchBase</name>
<value>ou=groups,dc=hadoop,dc=apache,dc=org</value>
</property>
<property>
- <name>gateway.ldap.backend.ldap.groupMemberAttribute</name>
+ <name>gateway.ldap.interceptor.demoldap.groupMemberAttribute</name>
<value>member</value>
</property>
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 eb3f9f227..995fa81be 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 class GatewayConfigImpl extends Configuration
implements GatewayConfig {
}
@Override
- public String getLDAPBackendType() {
- return get(LDAP_BACKEND_TYPE, "file");
+ public List<String> getLDAPInterceptorNames() {
+ return splitConfigValueToList(LDAP_INTERCEPTOR_NAMES);
}
@Override
@@ -1782,9 +1782,9 @@ public class GatewayConfigImpl extends Configuration
implements GatewayConfig {
}
@Override
- public Map<String, String> getLDAPBackendConfig(String backendType) {
+ public Map<String, String> getLDAPInterceptorConfig(String interceptorName) {
Map<String, String> 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 eebf9251e..ff4edb20e 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.ldap.LdapServer;
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 @@ import java.util.Map;
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<Interceptor> interceptors;
private File workDir;
private int port;
private String baseDn;
- private String remoteBaseDn;
+ // Collection of DNs for the proxied backend LDAP servers
+ private Set<String> 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 class KnoxLDAPServerManager {
// Get configuration
this.port = config.getLDAPPort();
this.baseDn = config.getLDAPBaseDN();
- String backendType = config.getLDAPBackendType();
-
- // Get backend-specific configuration using prefixed properties
- Map<String, String> 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 class KnoxLDAPServerManager {
workDir.mkdirs();
}
+ private List<Interceptor> createInterceptors(GatewayConfig config) throws
Exception {
+ List<String> interceptorNames = config.getLDAPInterceptorNames();
+ List<Interceptor> interceptors = new
ArrayList<>(interceptorNames.size());
+ for (String interceptorName : interceptorNames) {
+ // Get backend-specific configuration using prefixed properties
+ Map<String, String> interceptorConfig =
config.getLDAPInterceptorConfig(interceptorName);
+
+ // 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 class KnoxLDAPServerManager {
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 class KnoxLDAPServerManager {
// 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 class KnoxLDAPServerManager {
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<Interceptor> interceptors = new
ArrayList<>(directoryService.getInterceptors());
+ private void addRemotePartitions() throws LdapException {
+ SchemaManager schemaManager = directoryService.getSchemaManager();
+ DnFactory dnFactory = directoryService.getDnFactory();
+ List<String> interceptorNames =
gatewayConfig.getLDAPInterceptorNames();
+ for (String interceptorName : interceptorNames) {
+ // Get backend-specific configuration using prefixed properties
+ Map<String, String> 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<Interceptor> 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 class KnoxLDAPServerManager {
if (ldapServer != null) {
try {
ldapServer.stop();
+ ldapServer = null;
} catch (Exception e) {
LOG.ldapServiceStopFailed(e);
}
@@ -203,6 +251,7 @@ public class KnoxLDAPServerManager {
if (directoryService != null) {
try {
directoryService.shutdown();
+ directoryService = null;
} catch (Exception e) {
LOG.ldapServiceStopFailed(e);
}
@@ -211,13 +260,10 @@ public class KnoxLDAPServerManager {
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<String> 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 class KnoxLDAPServerManager {
* @return List of group names
*/
public List<String> 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<String> groups = new ArrayList<>();
+ try (Cursor<Entry> 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 221e36e4d..947581247 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 f432a6dc5..a204ac460 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<String,
String> config) throws Exception {
- // Use ServiceLoader to discover all available backends (built-in and
external plugins)
- ServiceLoader<LdapBackend> 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<LdapBackendFactory> 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 9c3d96aed..8546a3acd 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 @@ import java.util.Map;
public class FileBackend implements LdapBackend {
private static final LdapMessages LOG =
MessagesFactory.get(LdapMessages.class);
+ static final String TYPE = "file";
+
private Map<String, UserData> 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 @@ public class FileBackend implements LdapBackend {
List<UserData> users;
}
+ public FileBackend(String name, Map<String, String> 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<String, String> 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 000000000..847753940
--- /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<String, String> 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 4c459ff26..f5cc9c75a 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.name.Dn;
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 @@ import java.util.Map;
* - 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<String, String> 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 000000000..2ec00eb37
--- /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<String, String> 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 59355e4ad..07337b049 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 @@ import java.util.stream.Collectors;
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<String, String> config) throws Exception {
+ public LdapProxyBackend(String name, Map<String, String> 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 class LdapProxyBackend implements LdapBackend {
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<String, String> config) throws
Exception {
+ private void initializeConnectionPool(Map<String, String> config) {
// Configure connection settings
LdapConnectionConfig connectionConfig = new LdapConnectionConfig();
connectionConfig.setLdapHost(host);
@@ -186,7 +198,7 @@ public class LdapProxyBackend implements LdapBackend {
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 000000000..f138a827d
--- /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<String, String> 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 000000000..fa7287df1
--- /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<Entry> filteredEntries = List.of();
+ try (EntryFilteringCursor originalResults = next(ctx)) {
+ List<Entry> 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<Entry> filterDuplicateUsers(List<Entry> originalEntries) {
+ Set<Value> seenUids = new HashSet<>();
+ List<Entry> 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 000000000..b443c2def
--- /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<String, String> 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 000000000..6b26cc116
--- /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<String, String> 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<KnoxLdapInterceptorFactory> 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 000000000..2eb4fb5f9
--- /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<String, String> 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 1218c8c78..1ac2ef5a2 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.BindOperationCon
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<String, String> 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 class GroupLookupInterceptor extends BaseInterceptor
{
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 class GroupLookupInterceptor extends
BaseInterceptor {
}
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<Entry> 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 000000000..862d3a964
--- /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<String, String> config) throws
Exception {
+ return new UserSearchInterceptor(name, config);
+ }
+
+ @Override
+ public String getType() {
+ return TYPE;
+ }
+}
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/util/KnoxCLI.java
b/gateway-server/src/main/java/org/apache/knox/gateway/util/KnoxCLI.java
index b1465a7af..784c416c1 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/util/KnoxCLI.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/util/KnoxCLI.java
@@ -1911,6 +1911,7 @@ public class KnoxCLI extends Configured implements Tool {
return;
}
+ services.start();
try {
final KnoxLDAPService ldapService =
services.getService(ServiceType.LDAP_SERVICE);
out.println("Querying KnoxLDAPService for groups of user: " +
username);
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%
copy from
gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackend
copy to
gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackendFactory
index c8bd82de6..424d400b8 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.backend.LdapBackend
b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.interceptor.KnoxLdapInterceptorFactory
similarity index 80%
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.interceptor.KnoxLdapInterceptorFactory
index c8bd82de6..3275b3329 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.interceptor.KnoxLdapInterceptorFactory
@@ -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 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 ce7525e2d..70ab3b188 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.
</property>
<property>
- <name>gateway.ldap.backend.type</name>
- <value>ldap</value>
- <description>Backend type for LDAP service. Currently supported: file,
ldap. Future: jdbc, knox.</description>
+ <name>gateway.ldap.interceptor.names</name>
+ <value>ldapproxy,duplicatefilter</value>
+ <description>Interceptor names for LDAP service in priority
order.</description>
</property>
<property>
@@ -74,61 +74,86 @@ limitations under the License.
<description>Path to JSON data file for file-based backend. Supports
${GATEWAY_DATA_HOME} variable.</description>
</property>
- <!-- LDAP backend proxy configuration (active when
gateway.ldap.backend.type=ldap) -->
+ <!-- LDAP backend proxy configuration (active when
gateway.ldap.interceptor.names includes ldapproxy) -->
+ <property>
+ <name>gateway.ldap.interceptor.ldapproxy.interceptorType</name>
+ <value>backend</value>
+ <description>Interceptor type.</description>
+ </property>
+ <property>
+ <name>gateway.ldap.interceptor.ldapproxy.backendType</name>
+ <value>ldap</value>
+ <description>Backend type for LDAP service. Currently supported: file,
ldap. Future: jdbc, knox.</description>
+ </property>
<property>
- <name>gateway.ldap.backend.ldap.url</name>
+ <name>gateway.ldap.interceptor.ldapproxy.url</name>
<value>ldap://localhost:33389</value>
<description>LDAP server URL for proxy backend</description>
</property>
<property>
- <name>gateway.ldap.backend.ldap.remoteBaseDn</name>
+ <name>gateway.ldap.interceptor.ldapproxy.remoteBaseDn</name>
<value>dc=hadoop,dc=apache,dc=org</value>
<description>Base DN of the remote LDAP server</description>
</property>
<property>
- <name>gateway.ldap.backend.ldap.systemUsername</name>
+ <name>gateway.ldap.interceptor.ldapproxy.systemUsername</name>
<value>uid=guest,ou=people,dc=hadoop,dc=apache,dc=org</value>
<description>LDAP bind DN for proxy backend
authentication</description>
</property>
<property>
- <name>gateway.ldap.backend.ldap.systemPassword</name>
+ <name>gateway.ldap.interceptor.ldapproxy.systemPassword</name>
<value>guest-password</value>
<description>LDAP bind password for proxy backend
authentication</description>
</property>
<!-- Backend-specific configuration using prefixed properties -->
- <!-- Uncomment and configure based on backend type specified in
gateway.ldap.backend.type -->
+ <!-- Uncomment and configure based on interceptor names specified in
gateway.ldap.interceptor.names -->
- <!-- File backend configuration (gateway.ldap.backend.type=file) -->
+ <!-- File backend configuration
(gateway.ldap.interceptor.<interceptorName>.backendType=file) -->
<!--
<property>
- <name>gateway.ldap.backend.file.dataFile</name>
+ <name>gateway.ldap.interceptor.ldapfile.interceptorType</name>
+ <value>backend</value>
+ <description>Interceptor type.</description>
+ </property>
+ <property>
+ <name>gateway.ldap.interceptor.ldapfile.backendType</name>
+ <value>file</value>
+ <description>Backend type for LDAP service. Currently supported: file,
ldap. Future: jdbc, knox.</description>
+ </property>
+ <property>
+ <name>gateway.ldap.interceptor.ldapfile.dataFile</name>
<value>${GATEWAY_DATA_HOME}/ldap-users.json</value>
<description>Path to JSON file containing user and group
data</description>
</property>
-->
- <!-- LDAP proxy backend configuration (gateway.ldap.backend.type=ldap) -->
+ <!-- LDAP proxy backend configuration
(gateway.ldap.interceptor.<interceptorName>.backendType=ldap) -->
<!-- This backend proxies to an external LDAP server (e.g., demo LDAP) -->
<!--
Example 1: Using Knox demo LDAP server (default port 33389)
<property>
- <name>gateway.ldap.backend.ldap.url</name>
+ <name>gateway.ldap.interceptor.demoldap.backendType</name>
+ <value>ldap</value>
+ <description>Backend type for LDAP service. Currently supported: file,
ldap. Future: jdbc, knox.</description>
+ </property>
+ <property>
+ <name>gateway.ldap.interceptor.demoldap.url</name>
<value>ldap://localhost:33389</value>
<description>LDAP server URL</description>
</property>
<property>
- <name>gateway.ldap.backend.ldap.remoteBaseDn</name>
+ <name>gateway.ldap.interceptor.demoldap.remoteBaseDn</name>
<value>dc=hadoop,dc=apache,dc=org</value>
<description>Base DN of the remote LDAP server</description>
</property>
<property>
- <name>gateway.ldap.backend.ldap.systemUsername</name>
+ <name>gateway.ldap.interceptor.demoldap.systemUsername</name>
<value>uid=guest,ou=people,dc=hadoop,dc=apache,dc=org</value>
<description>LDAP bind DN for authentication</description>
</property>
<property>
- <name>gateway.ldap.backend.ldap.systemPassword</name>
+ <name>gateway.ldap.interceptor.demoldap.systemPassword</name>
<value>guest-password</value>
<description>LDAP bind password</description>
</property>
@@ -137,32 +162,37 @@ limitations under the License.
<!--
Example 2: Using external LDAP with authentication (supports both naming
conventions)
<property>
- <name>gateway.ldap.backend.ldap.url</name>
+ <name>gateway.ldap.interceptor.externalldap.backendType</name>
+ <value>ldap</value>
+ <description>Backend type for LDAP service. Currently supported: file,
ldap. Future: jdbc, knox.</description>
+ </property>
+ <property>
+ <name>gateway.ldap.interceptor.externalldap.url</name>
<value>ldap://ldap.example.com:389</value>
<description>LDAP server URL</description>
</property>
<property>
- <name>gateway.ldap.backend.ldap.remoteBaseDn</name>
+ <name>gateway.ldap.interceptor.externalldap.remoteBaseDn</name>
<value>dc=example,dc=com</value>
<description>Base DN of the remote LDAP server</description>
</property>
<property>
- <name>gateway.ldap.backend.ldap.systemUsername</name>
+ <name>gateway.ldap.interceptor.externalldap.systemUsername</name>
<value>cn=admin,dc=example,dc=com</value>
<description>LDAP bind DN for authentication (or use
bindDn)</description>
</property>
<property>
- <name>gateway.ldap.backend.ldap.systemPassword</name>
+ <name>gateway.ldap.interceptor.externalldap.systemPassword</name>
<value>secret</value>
<description>LDAP bind password (or use bindPassword)</description>
</property>
<property>
- <name>gateway.ldap.backend.ldap.userSearchBase</name>
+ <name>gateway.ldap.interceptor.externalldap.userSearchBase</name>
<value>ou=people,dc=example,dc=com</value>
<description>Base DN for user searches on remote server (defaults to
ou=people,{remoteBaseDn})</description>
</property>
<property>
- <name>gateway.ldap.backend.ldap.groupSearchBase</name>
+ <name>gateway.ldap.interceptor.externalldap.groupSearchBase</name>
<value>ou=groups,dc=example,dc=com</value>
<description>Base DN for group searches on remote server (defaults to
ou=groups,{remoteBaseDn})</description>
</property>
@@ -170,58 +200,75 @@ limitations under the License.
<!--
Alternative: Use host and port instead of URL
<property>
- <name>gateway.ldap.backend.ldap.host</name>
+ <name>gateway.ldap.interceptor.externalldap.host</name>
<value>localhost</value>
<description>LDAP server hostname</description>
</property>
<property>
- <name>gateway.ldap.backend.ldap.port</name>
+ <name>gateway.ldap.interceptor.externalldap.port</name>
<value>33389</value>
<description>LDAP server port</description>
</property>
-->
- <!-- Database backend configuration (gateway.ldap.backend.type=database)
-->
+ <!-- Database backend configuration (gateway.ldap.backend.type=jdbc) -->
<!--
<property>
- <name>gateway.ldap.backend.database.jdbc-url</name>
+ <name>gateway.ldap.interceptor.jdbc.backendType</name>
+ <value>jdbc</value>
+ <description>Backend type for LDAP service. Currently supported: file,
ldap. Future: jdbc, knox.</description>
+ </property>
+ <property>
+ <name>gateway.ldap.interceptor.jdbc.url</name>
<value>jdbc:mysql://localhost:3306/knox</value>
<description>JDBC connection URL</description>
</property>
<property>
- <name>gateway.ldap.backend.database.driver</name>
+ <name>gateway.ldap.interceptor.jdbc.driver</name>
<value>com.mysql.cj.jdbc.Driver</value>
<description>JDBC driver class name</description>
</property>
<property>
- <name>gateway.ldap.backend.database.username</name>
+ <name>gateway.ldap.interceptor.jdbc.username</name>
<value>knox_user</value>
<description>Database username</description>
</property>
<property>
- <name>gateway.ldap.backend.database.password</name>
+ <name>gateway.ldap.interceptor.jdbc.password</name>
<value>secret</value>
<description>Database password</description>
</property>
-->
- <!-- Knox Auth backend configuration (gateway.ldap.backend.type=knox-auth)
-->
+ <!-- Knox Auth backend configuration (gateway.ldap.backend.type=knox) -->
<!--
<property>
- <name>gateway.ldap.backend.knox-auth.url</name>
+ <name>gateway.ldap.interceptor.knox.backendType</name>
+ <value>knox</value>
+ <description>Backend type for LDAP service. Currently supported: file,
ldap. Future: jdbc, knox.</description>
+ </property>
+ <property>
+ <name>gateway.ldap.interceptor.knox.url</name>
<value>https://knox-server/gateway/sandbox/knoxauth/api</value>
<description>Knox authentication service URL</description>
</property>
<property>
- <name>gateway.ldap.backend.knox-auth.username</name>
+ <name>gateway.ldap.interceptor.knox.username</name>
<value>admin</value>
<description>Knox admin username</description>
</property>
<property>
- <name>gateway.ldap.backend.knox-auth.password</name>
+ <name>gateway.ldap.interceptor.knox.password</name>
<value>admin-password</value>
<description>Knox admin password</description>
</property>
-->
+ <!-- Duplicate Filter Interceptor -->
+ <property>
+ <name>gateway.ldap.interceptor.duplicatefilter.interceptorType</name>
+ <value>duplicateuserfilter</value>
+ <description>Interceptor type.</description>
+ </property>
+
</configuration>
\ 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 4528c8b4a..c41afb0c0 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 @@ package org.apache.knox.gateway.services.ldap;
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 class KnoxLDAPServerManagerTest {
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 class KnoxLDAPServerManagerTest {
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<String, String> 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 class KnoxLDAPServerManagerTest {
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<String, String> 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<String, String> 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<String, String> 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<String, String> fileBackendConfig =
createFileBackendInterceptorConfig();
+
expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(fileBackendConfig).anyTimes();
+ Map<String, String> 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<String, String> 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 class KnoxLDAPServerManagerTest {
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<String, String> backendConfig =
createFileBackendInterceptorConfig();
+
expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(backendConfig).anyTimes();
+
replay(mockConfig);
serverManager.initialize(mockConfig);
@@ -177,9 +209,10 @@ public class KnoxLDAPServerManagerTest {
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<String, String> backendConfig =
createFileBackendInterceptorConfig();
+
expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(backendConfig).anyTimes();
replay(mockConfig);
serverManager.initialize(mockConfig);
@@ -196,6 +229,119 @@ public class KnoxLDAPServerManagerTest {
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<String, String> 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<String, String> 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<String, String> fileBackendConfig =
createFileBackendInterceptorConfig();
+
expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(fileBackendConfig).anyTimes();
+ Map<String, String> 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<String, String> createFileBackendInterceptorConfig() {
+ Map<String, String> 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<String, String> createLdapBackendInterceptorConfig() {
+ Map<String, String> 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 40777b253..399e05485 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 org.junit.After;
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 @@ public class KnoxLDAPServiceTest {
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<String, String> buildBackendConfig(String backendType) {
final Map<String, String> 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 fde29a087..cfb8fa2d5 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 class BackendFactoryTest {
@Test
public void testServiceLoaderDiscovery() {
- ServiceLoader<LdapBackend> loader =
ServiceLoader.load(LdapBackend.class);
+ ServiceLoader<LdapBackendFactory> 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 class BackendFactoryTest {
@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 0aba49487..85152cea0 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.security.ldap.SimpleDirectoryService;
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 class LdapProxyBackendTest {
}
}
- @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",
"[email protected]", "Test user ldaptest1");
@@ -161,7 +157,7 @@ public class LdapProxyBackendTest {
@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 class LdapProxyBackendTest {
@Test
public void testGetUserByUID() throws Exception {
Map<String, String> config = createConfigWithUserAttr("uid");
- ldapProxyBackend.initialize(config);
+ ldapProxyBackend = new LdapProxyBackend("testbackend", config);
Entry entry = ldapProxyBackend.getUser("ldaptest1", schemaManager);
validateUserEntry(entry, "ldaptest1", "TestCn1",
"[email protected]", "Test user ldaptest1");
@@ -182,7 +178,7 @@ public class LdapProxyBackendTest {
@Test
public void testGetUserByCN() throws Exception {
Map<String, String> config = createConfigWithUserAttr("cn");
- ldapProxyBackend.initialize(config);
+ ldapProxyBackend = new LdapProxyBackend("testbackend", config);
Entry entry = ldapProxyBackend.getUser("TestCn1", schemaManager);
validateUserEntry(entry, "TestCn1", "TestCn1",
"[email protected]", "Test user ldaptest1");
@@ -194,7 +190,7 @@ public class LdapProxyBackendTest {
@Test
public void testGetUserBySAMAccountName() throws Exception {
Map<String, String> config =
createConfigWithUserAttr("sAMAccountName");
- ldapProxyBackend.initialize(config);
+ ldapProxyBackend = new LdapProxyBackend("testbackend", config);
Entry entry = ldapProxyBackend.getUser("TestSam1", schemaManager);
validateUserEntry(entry, "TestSam1", "TestCn1",
"[email protected]", "Test user ldaptest1");
@@ -208,7 +204,7 @@ public class LdapProxyBackendTest {
public void testGetUserUseMemberOf() throws Exception {
Map<String, String> 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",
"[email protected]", "Test user ldaptest2");
@@ -219,7 +215,7 @@ public class LdapProxyBackendTest {
@Test
public void testGetUserGroups() throws Exception {
- ldapProxyBackend.initialize(ldapBackendConfig);
+ ldapProxyBackend = new LdapProxyBackend("testbackend",
ldapBackendConfig);
List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest1");
assertTrue(userGroups.contains("group1"));
@@ -228,7 +224,7 @@ public class LdapProxyBackendTest {
@Test
public void testGetUserGroupsNoGroups() throws Exception {
- ldapProxyBackend.initialize(ldapBackendConfig);
+ ldapProxyBackend = new LdapProxyBackend("testbackend",
ldapBackendConfig);
List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest2");
assertTrue(userGroups.isEmpty());
@@ -236,7 +232,7 @@ public class LdapProxyBackendTest {
@Test
public void testGetUserGroupsNoUser() throws Exception {
- ldapProxyBackend.initialize(ldapBackendConfig);
+ ldapProxyBackend = new LdapProxyBackend("testbackend",
ldapBackendConfig);
List<String> userGroups = ldapProxyBackend.getUserGroups("nobody");
assertTrue(userGroups.isEmpty());
@@ -246,7 +242,7 @@ public class LdapProxyBackendTest {
public void testGetUserGroupsUseMemberOf() throws Exception {
Map<String, String> config = new HashMap<>(ldapBackendConfig);
config.put("useMemberOf", "true");
- ldapProxyBackend.initialize(config);
+ ldapProxyBackend = new LdapProxyBackend("testbackend", config);
List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest2");
assertTrue(userGroups.contains("groupMemberOf1"));
@@ -257,7 +253,7 @@ public class LdapProxyBackendTest {
public void testGetUserGroupsUseMemberOfNoGroups() throws Exception {
Map<String, String> config = new HashMap<>(ldapBackendConfig);
config.put("useMemberOf", "true");
- ldapProxyBackend.initialize(config);
+ ldapProxyBackend = new LdapProxyBackend("testbackend", config);
List<String> userGroups = ldapProxyBackend.getUserGroups("ldaptest1");
assertTrue(userGroups.isEmpty());
@@ -267,7 +263,7 @@ public class LdapProxyBackendTest {
public void testGetUserGroupsUseMemberOfNoUser() throws Exception {
Map<String, String> config = new HashMap<>(ldapBackendConfig);
config.put("useMemberOf", "true");
- ldapProxyBackend.initialize(config);
+ ldapProxyBackend = new LdapProxyBackend("testbackend", config);
List<String> userGroups = ldapProxyBackend.getUserGroups("nobody");
assertTrue(userGroups.isEmpty());
@@ -275,19 +271,19 @@ public class LdapProxyBackendTest {
@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<Entry> entries = ldapProxyBackend.searchUsers("nobody*",
schemaManager);
assertTrue(entries.isEmpty());
}
@@ -295,21 +291,21 @@ public class LdapProxyBackendTest {
@Test
public void testSearchUsersByCn() throws Exception {
Map<String, String> 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<String, String> 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<String, String> config = createConfigWithUserAttr("cn");
- ldapProxyBackend.initialize(config);
+ ldapProxyBackend = new LdapProxyBackend("testbackend", config);
List<Entry> entries = ldapProxyBackend.searchUsers("nobody*",
schemaManager);
assertTrue(entries.isEmpty());
}
@@ -317,21 +313,21 @@ public class LdapProxyBackendTest {
@Test
public void testSearchUsersBySAMAccountName() throws Exception {
Map<String, String> config =
createConfigWithUserAttr("sAMAccountName");
- ldapProxyBackend.initialize(config);
+ ldapProxyBackend = new LdapProxyBackend("testbackend", config);
validateUserSearch("*", 2, Set.of("TestSam1", "TestSam2"));
}
@Test
public void testSearchUsersPartialBySAMAccountName() throws Exception {
Map<String, String> 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<String, String> config =
createConfigWithUserAttr("sAMAccountName");
- ldapProxyBackend.initialize(config);
+ ldapProxyBackend = new LdapProxyBackend("testbackend", config);
List<Entry> entries = ldapProxyBackend.searchUsers("nobody*",
schemaManager);
assertTrue(entries.isEmpty());
}
@@ -339,7 +335,7 @@ public class LdapProxyBackendTest {
@Test
public void testGetRecursiveUserGroupsDepth2() throws Exception {
Map<String, String> config = createRecursiveConfig(2);
- ldapProxyBackend.initialize(config);
+ ldapProxyBackend = new LdapProxyBackend("testbackend", config);
List<String> userGroups =
ldapProxyBackend.getUserGroups("recursiveUser");
assertEquals(4, userGroups.size());
@@ -352,7 +348,7 @@ public class LdapProxyBackendTest {
@Test
public void testGetRecursiveUserGroupsDepth4() throws Exception {
Map<String, String> config = createRecursiveConfig(4);
- ldapProxyBackend.initialize(config);
+ ldapProxyBackend = new LdapProxyBackend("testbackend", config);
List<String> userGroups =
ldapProxyBackend.getUserGroups("recursiveUser");
assertEquals(6, userGroups.size());
@@ -367,7 +363,7 @@ public class LdapProxyBackendTest {
@Test
public void testGetRecursiveUserGroupsWithCycle() throws Exception {
Map<String, String> config = createRecursiveConfig(10);
- ldapProxyBackend.initialize(config);
+ ldapProxyBackend = new LdapProxyBackend("testbackend", config);
List<String> userGroups =
ldapProxyBackend.getUserGroups("recursiveUser");
assertTrue(userGroups.contains("cycleGroupA"));
@@ -377,7 +373,7 @@ public class LdapProxyBackendTest {
@Test
public void testGetUserRecursiveGroups() throws Exception {
Map<String, String> 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 class LdapProxyBackendTest {
Map<String, String> config = createRecursiveConfig(5);
final AtomicInteger cacheHits = new AtomicInteger(0);
- ldapProxyBackend = new LdapProxyBackend() {
+ ldapProxyBackend = new LdapProxyBackend("testbackend", config) {
@Override
protected Map<String, Set<Entry>> createResolvedParentsCache() {
return new HashMap<>() {
@@ -408,7 +404,6 @@ public class LdapProxyBackendTest {
};
}
};
- 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 000000000..828937074
--- /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<Entry> entries;
+ private EntryFilteringCursor cursor;
+
+ ConfigurableEntriesTestInterceptor(String name) {
+ super(name);
+ }
+
+ public void setEntries(List<Entry> 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 80ab4e0c8..745002ae3 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 class GatewayTestConfig extends Configuration
implements GatewayConfig {
}
@Override
- public String getLDAPBackendType() {
- return "file";
+ public List<String> getLDAPInterceptorNames() {
+ return List.of("testinterceptor");
}
@Override
@@ -1261,7 +1261,7 @@ public class GatewayTestConfig extends Configuration
implements GatewayConfig {
}
@Override
- public Map<String, String> getLDAPBackendConfig(String backendType) {
+ public Map<String, String> 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 6116a3d4c..7ecb2980d 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<String> 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<String, String> getLDAPBackendConfig(String backendType);
+ Map<String, String> getLDAPInterceptorConfig(String interceptor);
/**
* @return true if recursive group resolution is enabled for LDAP service
diff --git a/knox-site/docs/service_ldap_server.md
b/knox-site/docs/service_ldap_server.md
index 51080f69a..1ad0f1ab0 100644
--- a/knox-site/docs/service_ldap_server.md
+++ b/knox-site/docs/service_ldap_server.md
@@ -7,7 +7,8 @@ The Knox LDAP Service provides an embedded LDAP server within
the Knox Gateway.
Introduced in [KNOX-3247](https://issues.apache.org/jira/browse/KNOX-3247),
the Knox LDAP Service leverages Apache Directory Server (ApacheDS) to provide a
lightweight, embedded LDAP server. Its primary goal is to provide a consistent
LDAP interface for authentication and group lookups, even when the underlying
identity store is not a traditional LDAP server or is a remote server that
requires proxying.
Key features include:
-- **Pluggable Backends**: Support for different data sources (JSON files,
remote LDAP/AD).
+- **Pluggable LDAP Interceptors**: Support for customization of LDAP search
behavior including combining results from multiple LDAP backends.
+- **Pluggable Backends**: Support for multiple, different data sources (JSON
files, remote LDAP/AD).
- **Embedded Server**: No need for an external LDAP server for simple use
cases or testing.
- **Active Directory Integration**: Optimized for proxying to AD with support
for `sAMAccountName`.
@@ -16,13 +17,13 @@ Key features include:
The Knox LDAP Service is integrated as a core gateway service. It consists of
the following components:
1. **KnoxLDAPServerManager**: Manages the lifecycle of the ApacheDS instance.
-2. **GroupLookupInterceptor**: A custom ApacheDS interceptor that captures
search requests. If an entry is not found in the local ApacheDS partitions, it
delegates the lookup to the configured backend.
-3. **LdapBackend**: A pluggable interface for fetching user and group data.
+2. **UserLookupInterceptor**: A custom ApacheDS interceptor that captures
search requests. If an entry is not found in the local ApacheDS partitions, it
delegates the lookup to the configured backend.
+3. **LdapBackend**: A pluggable interface for fetching user and group data.
Implementations are included for reading users from a file or connecting to a
remote LDAP server.
4. **SchemaManagerFactory**: Programmatically extends the ApacheDS schema to
include AD-specific attributes like `sAMAccountName`.
When a client performs an LDAP search:
1. The request hits the embedded ApacheDS server.
-2. The `GroupLookupInterceptor` intercepts the search.
+2. The `UserLookupInterceptor` intercepts the search.
3. The interceptor checks the results of the local search.
4. If not found, it queries the configured `LdapBackend`.
5. Results from the backend are converted into LDAP entries and returned to
the client.
@@ -38,17 +39,39 @@ The service is configured in `gateway-site.xml`.
| `gateway.ldap.enabled` | `false` | Enables or disables the embedded LDAP
service. |
| `gateway.ldap.port` | `3890` | The port on which the LDAP server listens. |
| `gateway.ldap.base.dn` | `dc=proxy,dc=com` | The base DN for the LDAP
server. |
-| `gateway.ldap.backend.type` | `file` | The type of backend to use (`file` or
`proxy`). |
+| `gateway.ldap.interceptor.names` | N/A | A comma separated list of
interceptors to use. A separate interceptor configuration block will be used
for each name. |
+
+### Interceptor Types
+
+#### Common Interceptor Properties
+
+| Property | Default Value | Description |
+| :--- | :--- | :--- |
+| `gateway.ldap.interceptor.<name>.interceptorType` | N/A | The type of
interceptor to use (`backend` or `duplicateuserfilter`). |
+
+#### Duplicate User Filter Interceptor (`backend`)
+
+The duplicate user filter interceptor ensures that each `Entry` has a unique
`uid`. This interceptor removes entries that have a duplicate `uid`.
+
+#### User Search Interceptor (`duplicateuserfilter`)
+
+The user search interceptor is created if the `interceptorType` configuration
is set to `backend`. This interceptor forwards search queries to its configured
backend.
### Backend Types
+#### Common Backend Properties
+
+| Property | Default Value | Description |
+| :--- | :--- | :--- |
+| `gateway.ldap.interceptor.<name>.backendType` | N/A | The type of backend to
use (`file` or `ldap`). |
+
#### File Backend (`file`)
The file backend reads user and group definitions from a JSON file. This is
ideal for testing or small, static environments.
| Property | Default Value | Description |
| :--- | :--- | :--- |
-| `gateway.ldap.backend.data.file` | `${GATEWAY_DATA_HOME}/ldap-users.json` |
Path to the JSON data file. |
+| `gateway.ldap.interceptor.<name>.file` |
`${GATEWAY_DATA_HOME}/ldap-users.json` | Path to the JSON data file. |
**JSON File Format Example:**
```json
@@ -67,24 +90,24 @@ The file backend reads user and group definitions from a
JSON file. This is idea
}
```
-#### Proxy Backend (`proxy`)
+#### LDAP Proxy Backend (`ldap`)
The proxy backend delegates lookups to a remote LDAP or Active Directory
server.
| Property | Default Value | Description |
| :--- | :--- | :--- |
-| `gateway.ldap.backend.proxy.url` | N/A | Remote LDAP URL (e.g.,
`ldap://remote-host:389`). |
-| `gateway.ldap.backend.proxy.host` | N/A | Host of remote LDAP (used if `url`
is not provided). |
-| `gateway.ldap.backend.proxy.port` | N/A | Port of remote LDAP (used if `url`
is not provided). |
-| `gateway.ldap.backend.proxy.remoteBaseDn` | N/A | **Required**. The base DN
of the remote LDAP server. |
-| `gateway.ldap.backend.proxy.systemUsername` | N/A | Bind DN for the remote
server (alias: `bindDn`). |
-| `gateway.ldap.backend.proxy.systemPassword` | N/A | Password for the bind DN
(alias: `bindPassword`). |
-| `gateway.ldap.backend.proxy.userSearchBase` | `ou=people,{remoteBaseDn}` |
Base DN for user searches on the remote server. |
-| `gateway.ldap.backend.proxy.groupSearchBase` | `ou=groups,{remoteBaseDn}` |
Base DN for group searches on the remote server. |
-| `gateway.ldap.backend.proxy.userIdentifierAttribute` | `uid` | Attribute
used for user lookup (e.g., `sAMAccountName` for AD). |
-| `gateway.ldap.backend.proxy.groupMemberAttribute` | `memberUid` | Attribute
used for group membership (e.g., `member` for AD). |
-| `gateway.ldap.backend.proxy.useMemberOf` | `false` | If `true`, use the
`memberOf` attribute for efficient group lookups. |
-| `gateway.ldap.backend.proxy.poolMaxActive` | `8` | Maximum number of active
connections in the pool. |
+| `gateway.ldap.interceptor.<name>.url` | N/A | Remote LDAP URL (e.g.,
`ldap://remote-host:389`). |
+| `gateway.ldap.interceptor.<name>.host` | N/A | Host of remote LDAP (used if
`url` is not provided). |
+| `gateway.ldap.interceptor.<name>.port` | N/A | Port of remote LDAP (used if
`url` is not provided). |
+| `gateway.ldap.interceptor.<name>.remoteBaseDn` | N/A | **Required**. The
base DN of the remote LDAP server. |
+| `gateway.ldap.interceptor.<name>.systemUsername` | N/A | Bind DN for the
remote server (alias: `bindDn`). |
+| `gateway.ldap.interceptor.<name>.systemPassword` | N/A | Password for the
bind DN (alias: `bindPassword`). |
+| `gateway.ldap.interceptor.<name>.userSearchBase` |
`ou=people,{remoteBaseDn}` | Base DN for user searches on the remote server. |
+| `gateway.ldap.interceptor.<name>.groupSearchBase` |
`ou=groups,{remoteBaseDn}` | Base DN for group searches on the remote server. |
+| `gateway.ldap.interceptor.<name>.userIdentifierAttribute` | `uid` |
Attribute used for user lookup (e.g., `sAMAccountName` for AD). |
+| `gateway.ldap.interceptor.<name>.groupMemberAttribute` | `memberUid` |
Attribute used for group membership (e.g., `member` for AD). |
+| `gateway.ldap.interceptor.<name>.useMemberOf` | `false` | If `true`, use the
`memberOf` attribute for efficient group lookups. |
+| `gateway.interceptor.<name>.proxy.poolMaxActive` | `8` | Maximum number of
active connections in the pool. |
## Active Directory (AD) Integration
@@ -95,11 +118,12 @@ The Knox LDAP Service includes several optimizations for
working with Active Dir
- **Schema Extensions**: AD-specific attributes (`memberOf`, `sAMAccountName`)
are programmatically added to the embedded ApacheDS schema to prevent
"attribute not found" errors during proxying.
- **Case Sensitivity**: Search filters are handled to accommodate AD's
case-insensitive nature for user identifiers.
-## Usage Example (AD Proxy)
+## Usage Example
-To configure Knox to act as an LDAP proxy for an Active Directory server:
+To configure Knox to act as an LDAP proxy for a local file and an Active
Directory server:
```xml
+<!-- LDAP Proxy Service Configuration -->
<property>
<name>gateway.ldap.enabled</name>
<value>true</value>
@@ -109,37 +133,74 @@ To configure Knox to act as an LDAP proxy for an Active
Directory server:
<value>389</value>
</property>
<property>
- <name>gateway.ldap.backend.type</name>
- <value>proxy</value>
+ <name>gateway.ldap.base.dn</name>
+ <value>dc=proxy,dc=com</value>
+</property>
+
+<property>
+ <name>gateway.ldap.interceptor.names</name>
+ <value>localfile,adexample,duplicatefilter</value>
+</property>
+
+<!-- File-based LDAP backend -->
+<property>
+ <name>gateway.ldap.interceptor.localfile.interceptorType</name>
+ <value>backend</value>
+</property>
+<property>
+ <name>gateway.ldap.interceptor.localfile.backendType</name>
+ <value>file</value>
+</property>
+<property>
+ <name>gateway.ldap.interceptor.localfile.file</name>
+ <value>${GATEWAY_DATA_HOME}/ldap-users.json</value>
+</property>
+
+<!-- LDAP backend proxy configuration -->
+<property>
+ <name>gateway.ldap.interceptor.adexample.interceptorType</name>
+ <value>backend</value>
+</property>
+<property>
+ <name>gateway.ldap.interceptor.adexample.backendType</name>
+ <value>ldap</value>
</property>
<property>
- <name>gateway.ldap.backend.proxy.url</name>
+ <name>gateway.ldap.interceptor.adexample.url</name>
<value>ldap://ad.example.com:389</value>
</property>
<property>
- <name>gateway.ldap.backend.proxy.remoteBaseDn</name>
+ <name>gateway.ldap.interceptor.adexample.remoteBaseDn</name>
<value>dc=example,dc=com</value>
</property>
<property>
- <name>gateway.ldap.backend.proxy.systemUsername</name>
- <value>CN=ReadOnlyUser,CN=Users,DC=example,DC=com</value>
+ <name>gateway.ldap.interceptor.adexample.systemUsername</name>
+ <value>cn=ReadOnlyUser,cn=Users,dc=example,dc=com</value>
</property>
<property>
- <name>gateway.ldap.backend.proxy.systemPassword</name>
- <value>secret-password</value>
+ <name>gateway.ldap.interceptor.adexample.systemPassword</name>
+ <value>guest-password</value>
</property>
<property>
- <name>gateway.ldap.backend.proxy.userIdentifierAttribute</name>
+ <name>gateway.ldap.interceptor.adexample.userIdentifierAttribute</name>
<value>sAMAccountName</value>
</property>
<property>
- <name>gateway.ldap.backend.proxy.groupMemberAttribute</name>
+ <name>gateway.ldap.interceptor.adexample.groupMemberAttribute</name>
<value>member</value>
</property>
<property>
- <name>gateway.ldap.backend.proxy.useMemberOf</name>
+ <name>gateway.ldap.interceptor.adexample.useMemberOf</name>
<value>true</value>
</property>
+
+<!-- Duplicate Filter Interceptor -->
+<property>
+ <name>gateway.ldap.interceptor.duplicatefilter.interceptorType</name>
+ <value>duplicateuserfilter</value>
+</property>
+
+<!-- End LDAP Proxy Service Configuration -->
```
## Troubleshooting