This is an automated email from the ASF dual-hosted git repository.
lmccay 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 3f1cf239a KNOX-3247 - Knox LDAP Server with Pluggable Backend (#1144)
3f1cf239a is described below
commit 3f1cf239aefc059967c5482c8486ad65f1eac4f6
Author: lmccay <[email protected]>
AuthorDate: Thu Feb 12 17:16:23 2026 -0500
KNOX-3247 - Knox LDAP Server with Pluggable Backend (#1144)
* KNOX-3247 - Knox LDAP Server with Pluggable Backend
---
gateway-server/pom.xml | 54 +++
.../org/apache/knox/gateway/GatewayMessages.java | 13 +
.../gateway/config/impl/GatewayConfigImpl.java | 62 +++
.../gateway/services/DefaultGatewayServices.java | 8 +
.../services/ldap/GroupLookupInterceptor.java | 147 +++++++
.../services/ldap/KnoxLDAPServerManager.java | 237 +++++++++++
.../gateway/services/ldap/KnoxLDAPService.java | 130 ++++++
.../knox/gateway/services/ldap/LdapMessages.java | 87 ++++
.../services/ldap/backend/BackendFactory.java | 49 +++
.../gateway/services/ldap/backend/FileBackend.java | 144 +++++++
.../gateway/services/ldap/backend/LdapBackend.java | 68 ++++
.../services/ldap/backend/LdapProxyBackend.java | 452 +++++++++++++++++++++
....knox.gateway.services.ldap.backend.LdapBackend | 21 +
.../src/main/resources/conf/gateway-site.xml | 181 +++++++++
.../services/AbstractGatewayServicesTest.java | 3 +-
.../services/ldap/KnoxLDAPServerManagerTest.java | 188 +++++++++
.../gateway/services/ldap/KnoxLDAPServiceTest.java | 194 +++++++++
.../services/ldap/backend/BackendFactoryTest.java | 124 ++++++
.../org/apache/knox/gateway/GatewayTestConfig.java | 36 ++
.../apache/knox/gateway/config/GatewayConfig.java | 47 +++
.../apache/knox/gateway/services/ServiceType.java | 3 +-
knox-token-management-ui/package.json | 2 +-
pom.xml | 22 +-
23 files changed, 2268 insertions(+), 4 deletions(-)
diff --git a/gateway-server/pom.xml b/gateway-server/pom.xml
index 75ca1dfe5..e50154095 100644
--- a/gateway-server/pom.xml
+++ b/gateway-server/pom.xml
@@ -233,6 +233,10 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-pool2</artifactId>
+ </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
@@ -465,6 +469,56 @@
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
+ <dependency>
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ </dependency>
+ <!-- ApacheDS dependencies for embedded LDAP server (provided to avoid
transitive issues) -->
+ <dependency>
+ <groupId>org.apache.directory.server</groupId>
+ <artifactId>apacheds-core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.directory.server</groupId>
+ <artifactId>apacheds-core-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.directory.server</groupId>
+ <artifactId>apacheds-protocol-shared</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.directory.server</groupId>
+ <artifactId>apacheds-protocol-ldap</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.directory.server</groupId>
+ <artifactId>apacheds-ldif-partition</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.directory.server</groupId>
+ <artifactId>apacheds-jdbm-partition</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.directory.api</groupId>
+ <artifactId>api-ldap-model</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.directory.api</groupId>
+ <artifactId>api-ldap-schema-data</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.directory.api</groupId>
+ <artifactId>api-ldap-client-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
<!-- ********** ********** ********** ********** ********** **********
-->
<!-- ********** Test Dependencies **********
-->
<!-- ********** ********** ********** ********** ********** **********
-->
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
index dc133cfdf..7e8240879 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
@@ -812,4 +812,17 @@ public interface GatewayMessages {
@Message( level = MessageLevel.DEBUG, text = "Strict-Transport-Security
header enabled with \"{0}\" option" )
void strictTransportHeaderEnabled(String option);
+
+ // LDAP Service messages
+ @Message(level = MessageLevel.INFO, text = "LDAP service is enabled and will
be started on port {0}")
+ void ldapServiceEnabled(int port);
+
+ @Message(level = MessageLevel.INFO, text = "LDAP service is disabled")
+ void ldapServiceDisabled();
+
+ @Message(level = MessageLevel.ERROR, text = "Failed to start LDAP service:
{0}")
+ void ldapServiceStartFailed(@StackTrace(level = MessageLevel.DEBUG)
Exception e);
+
+ @Message(level = MessageLevel.ERROR, text = "LDAP service not found or not
properly registered")
+ void ldapServiceNotFound();
}
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 e2c099c4e..df70c37ac 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
@@ -32,6 +32,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -1698,4 +1699,65 @@ public class GatewayConfigImpl extends Configuration
implements GatewayConfig {
public String getStrictTransportOption() {
return get(STRICT_TRANSPORT_OPTION, DEFAULT_STRICT_TRANSPORT_OPTION);
}
+
+ // LDAP Service Configuration
+ @Override
+ public boolean isLDAPEnabled() {
+ return Boolean.parseBoolean(get(LDAP_ENABLED, "false"));
+ }
+
+ @Override
+ public int getLDAPPort() {
+ return Integer.parseInt(get(LDAP_PORT, "3890"));
+ }
+
+ @Override
+ public String getLDAPBaseDN() {
+ return get(LDAP_BASE_DN, "dc=proxy,dc=com");
+ }
+
+ @Override
+ public String getLDAPBackendType() {
+ return get(LDAP_BACKEND_TYPE, "file");
+ }
+
+ @Override
+ public String getLDAPBackendDataFile() {
+ String configuredPath = get(LDAP_BACKEND_DATA_FILE, null);
+ if (configuredPath != null && !configuredPath.isEmpty()) {
+ // Support ${GATEWAY_DATA_HOME} variable substitution
+ configuredPath = configuredPath.replace("${GATEWAY_DATA_HOME}",
getGatewayDataDir());
+ return configuredPath;
+ }
+ // Default to data directory if not configured
+ return getGatewayDataDir() + File.separator + "ldap-users.json";
+ }
+
+ @Override
+ public Set<String> getPropertyNames() {
+ Set<String> names = new HashSet<>();
+ Iterator<Map.Entry<String, String>> iterator = this.iterator();
+ while (iterator.hasNext()) {
+ Map.Entry<String, String> entry = iterator.next();
+ names.add(entry.getKey());
+ }
+ return names;
+ }
+
+ @Override
+ public Map<String, String> getLDAPBackendConfig(String backendType) {
+ Map<String, String> config = new HashMap<>();
+ String prefix = "gateway.ldap.backend." + backendType + ".";
+
+ for (String key : getPropertyNames()) {
+ if (key != null && key.startsWith(prefix)) {
+ String configKey = key.substring(prefix.length());
+ String value = get(key);
+ if (value != null) {
+ config.put(configKey, value);
+ }
+ }
+ }
+ return config;
+ }
}
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
b/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
index 676202d5f..06ce95d93 100644
---
a/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
@@ -26,6 +26,7 @@ import org.apache.knox.gateway.deploy.DeploymentContext;
import org.apache.knox.gateway.descriptor.FilterParamDescriptor;
import org.apache.knox.gateway.descriptor.ResourceDescriptor;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.ldap.KnoxLDAPService;
import org.apache.knox.gateway.services.security.KeystoreService;
import org.apache.knox.gateway.services.security.KeystoreServiceException;
import org.apache.knox.gateway.topology.Provider;
@@ -82,6 +83,13 @@ public class DefaultGatewayServices extends
AbstractGatewayServices {
addService(ServiceType.CONCURRENT_SESSION_VERIFIER,
gatewayServiceFactory.create(this, ServiceType.CONCURRENT_SESSION_VERIFIER,
config, options));
addService(ServiceType.GATEWAY_STATUS_SERVICE,
gatewayServiceFactory.create(this, ServiceType.GATEWAY_STATUS_SERVICE, config,
options));
+
+ // LDAP Service - infrastructure service for embedded LDAP server
+ if (config.isLDAPEnabled()) {
+ KnoxLDAPService ldapService = new KnoxLDAPService();
+ ldapService.init(config, options);
+ addService(ServiceType.LDAP_SERVICE, ldapService);
+ }
}
@Override
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/GroupLookupInterceptor.java
new file mode 100644
index 000000000..ea397fea7
--- /dev/null
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/GroupLookupInterceptor.java
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.services.ldap;
+
+import 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.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.BindOperationContext;
+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.backend.LdapBackend;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Interceptor for LDAP operations to proxy user searches to backend when not
found locally
+ */
+public class GroupLookupInterceptor 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=([^)]+)\\).*");
+
+ public GroupLookupInterceptor(DirectoryService directoryService,
LdapBackend backend) {
+ this.directoryService = directoryService;
+ this.backend = backend;
+ }
+
+ @Override
+ public EntryFilteringCursor search(SearchOperationContext ctx) throws
LdapException {
+ String filter = ctx.getFilter() != null ? ctx.getFilter().toString() :
"";
+ String baseDn = ctx.getDn() != null ? ctx.getDn().toString() : "";
+
+ LOG.ldapSearch(baseDn, filter);
+
+ // First try the normal search
+ EntryFilteringCursor originalResults;
+ try {
+ originalResults = next(ctx);
+ } catch (Exception e) {
+ throw new LdapException(e);
+ }
+
+ // Check if this is a user search and if we got no results, try the
backend
+ if (isUserSearch(filter)) {
+ String username = extractUser(filter);
+
+ // Check if we have any results from local search
+ List<Entry> entries = new ArrayList<>();
+ try {
+ while (originalResults.next()) {
+ entries.add(originalResults.get());
+ }
+ originalResults.close();
+ } catch (Exception e) {
+ // If we get an error or no results, try the backend
+ }
+
+ // If no local results, try backend
+ if (entries.isEmpty() && 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);
+ } 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);
+ }
+ }
+ } catch (Exception e) {
+ LOG.ldapServiceStopFailed(e);
+ }
+ }
+
+ // Return cursor with our results - use a simple approach
+ return new EntryFilteringCursorImpl(new ListCursor<>(entries),
ctx, directoryService.getSchemaManager());
+ }
+
+ return originalResults;
+ }
+
+ @Override
+ public void bind(BindOperationContext ctx) {
+ // Allow anonymous bind or simple bind
+ LOG.ldapBind(ctx.getDn() != null ? ctx.getDn().toString() :
"anonymous");
+ try {
+ next(ctx);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private boolean isUserSearch(String filter) {
+ return UID_PATTERN.matcher(filter).matches() ||
CN_PATTERN.matcher(filter).matches();
+ }
+
+ private String extractUser(String filter) {
+ Matcher uidMatcher = UID_PATTERN.matcher(filter);
+ if (uidMatcher.matches()) {
+ return uidMatcher.group(1);
+ }
+
+ Matcher cnMatcher = CN_PATTERN.matcher(filter);
+ if (cnMatcher.matches()) {
+ return cnMatcher.group(1);
+ }
+
+ return null;
+ }
+}
+
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
new file mode 100644
index 000000000..849f8e3a9
--- /dev/null
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManager.java
@@ -0,0 +1,237 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.services.ldap;
+
+import org.apache.directory.api.ldap.model.entry.DefaultEntry;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.api.ldap.model.schema.SchemaManager;
+import org.apache.directory.api.ldap.schema.loader.JarLdifSchemaLoader;
+import org.apache.directory.api.ldap.schema.manager.impl.DefaultSchemaManager;
+import org.apache.directory.server.core.DefaultDirectoryService;
+import org.apache.directory.server.core.api.DirectoryService;
+import org.apache.directory.server.core.api.InstanceLayout;
+import org.apache.directory.server.core.api.schema.SchemaPartition;
+import
org.apache.directory.server.core.partition.impl.btree.jdbm.JdbmPartition;
+import org.apache.directory.server.core.partition.ldif.LdifPartition;
+import org.apache.directory.server.ldap.LdapServer;
+import org.apache.directory.server.protocol.shared.transport.TcpTransport;
+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 java.io.File;
+import java.util.Map;
+
+/**
+ * Manages the ApacheDS LDAP server instance with pluggable backends
+ */
+public class KnoxLDAPServerManager {
+ private static final LdapMessages LOG =
MessagesFactory.get(LdapMessages.class);
+
+ private DirectoryService directoryService;
+ private LdapServer ldapServer;
+ private LdapBackend backend;
+ private File workDir;
+ private int port;
+ private String baseDn;
+ private String remoteBaseDn;
+
+ /**
+ * Initialize the LDAP server with the given configuration
+ *
+ * @param workDir Directory for LDAP data storage
+ * @param port Port for LDAP server to listen on
+ * @param baseDn Base DN for LDAP entries in the proxy server
+ * @param backendType Type of backend to use
+ * @param backendConfig Backend-specific configuration
+ * @param remoteBaseDn Base DN of the remote LDAP server (for proxy
backends)
+ */
+ public void initialize(File workDir, int port, String baseDn, String
backendType, Map<String, String> backendConfig, String remoteBaseDn) throws
Exception {
+ this.workDir = workDir;
+ this.port = port;
+ this.baseDn = baseDn;
+ this.remoteBaseDn = remoteBaseDn;
+
+ // Initialize backend
+ backendConfig.put("baseDn", baseDn);
+ backend = BackendFactory.createBackend(backendType, backendConfig);
+
+ // Clean up previous run if it didn't shut down cleanly
+ File lockFile = new File(workDir, "run/instance.lock");
+ if (lockFile.exists()) {
+ LOG.ldapCleaningLockFile(lockFile.getAbsolutePath());
+ lockFile.delete();
+ }
+
+ workDir.mkdirs();
+ }
+
+ /**
+ * Start the LDAP server
+ */
+ public void start() throws Exception {
+ LOG.ldapServiceStarting(port, baseDn);
+
+ // Initialize DirectoryService
+ directoryService = new DefaultDirectoryService();
+ directoryService.setInstanceLayout(new InstanceLayout(workDir));
+
+ // Create and load schema manager manually
+ JarLdifSchemaLoader loader = new JarLdifSchemaLoader();
+ SchemaManager schemaManager = new DefaultSchemaManager(loader);
+ schemaManager.loadAllEnabled();
+ directoryService.setSchemaManager(schemaManager);
+
+ // Initialize schema partition
+ LdifPartition schemaPartition = new LdifPartition(schemaManager,
directoryService.getDnFactory());
+ schemaPartition.setPartitionPath(new File(workDir, "schema").toURI());
+ SchemaPartition schemaLdifPartition = new
SchemaPartition(schemaManager);
+ schemaLdifPartition.setWrappedPartition(schemaPartition);
+ directoryService.setSchemaPartition(schemaLdifPartition);
+
+ // Create system partition (required)
+ JdbmPartition systemPartition = new JdbmPartition(schemaManager,
directoryService.getDnFactory());
+ systemPartition.setId("system");
+ systemPartition.setSuffixDn(new Dn(schemaManager, "ou=system"));
+ systemPartition.setPartitionPath(new File(workDir, "system").toURI());
+ directoryService.setSystemPartition(systemPartition);
+
+ // Create our custom partition for proxy base DN
+ JdbmPartition partition = new JdbmPartition(schemaManager,
directoryService.getDnFactory());
+ partition.setId("proxy");
+ partition.setSuffixDn(new Dn(schemaManager, baseDn));
+ 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);
+ }
+
+ // Add our interceptor for group lookups
+ directoryService.addLast(new GroupLookupInterceptor(directoryService,
backend));
+
+ // Allow anonymous access
+ directoryService.setAllowAnonymousAccess(true);
+
+ // Start the service
+ directoryService.startup();
+
+ // Add base entries to the partition
+ createBaseEntries(schemaManager);
+
+ // Create LDAP server on configured port
+ ldapServer = new LdapServer();
+ ldapServer.setTransports(new TcpTransport(port));
+ ldapServer.setDirectoryService(directoryService);
+
+ ldapServer.start();
+
+ LOG.ldapServiceStarted(port);
+ }
+
+ /**
+ * Stop the LDAP server
+ */
+ public void stop() throws Exception {
+ LOG.ldapServiceStopping(port);
+
+ if (ldapServer != null) {
+ try {
+ ldapServer.stop();
+ } catch (Exception e) {
+ LOG.ldapServiceStopFailed(e);
+ }
+ }
+
+ if (directoryService != null) {
+ try {
+ directoryService.shutdown();
+ } catch (Exception e) {
+ LOG.ldapServiceStopFailed(e);
+ }
+ }
+
+ 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 createBaseEntriesForDn(SchemaManager schemaManager, String
dn) throws Exception {
+ Dn baseDnName = new Dn(schemaManager, dn);
+ if (!directoryService.getAdminSession().exists(baseDnName)) {
+ Entry baseDnEntry = new DefaultEntry(schemaManager);
+ baseDnEntry.setDn(baseDnName);
+ baseDnEntry.add("objectClass", "top", "domain");
+ // Extract dc value from baseDn (e.g., "dc=proxy,dc=com" ->
"proxy")
+ String dcValue = dn.split(",")[0].split("=")[1];
+ baseDnEntry.add("dc", dcValue);
+ directoryService.getAdminSession().add(baseDnEntry);
+ }
+
+ Dn usersOuDn = new Dn(schemaManager, "ou=people," + dn);
+ if (!directoryService.getAdminSession().exists(usersOuDn)) {
+ Entry usersOu = new DefaultEntry(schemaManager);
+ usersOu.setDn(usersOuDn);
+ usersOu.add("objectClass", "top", "organizationalUnit");
+ usersOu.add("ou", "people");
+ directoryService.getAdminSession().add(usersOu);
+ }
+
+ Dn groupsOuDn = new Dn(schemaManager, "ou=groups," + dn);
+ if (!directoryService.getAdminSession().exists(groupsOuDn)) {
+ Entry groupsOu = new DefaultEntry(schemaManager);
+ groupsOu.setDn(groupsOuDn);
+ groupsOu.add("objectClass", "top", "organizationalUnit");
+ groupsOu.add("ou", "groups");
+ directoryService.getAdminSession().add(groupsOu);
+ }
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public String getBaseDn() {
+ return baseDn;
+ }
+
+ /**
+ * Check if the LDAP server is currently running.
+ *
+ * @return true if the server is running, false otherwise
+ */
+ public boolean isRunning() {
+ return ldapServer != null && ldapServer.isStarted();
+ }
+
+}
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPService.java
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPService.java
new file mode 100644
index 000000000..1ec748da9
--- /dev/null
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/KnoxLDAPService.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.services.ldap;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.Service;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+
+import java.io.File;
+import java.util.Map;
+
+/**
+ * Knox LDAP Service - provides an embedded LDAP server with pluggable backends
+ * for user and group lookups.
+ */
+public class KnoxLDAPService implements Service {
+ private static final LdapMessages LOG =
MessagesFactory.get(LdapMessages.class);
+
+ private KnoxLDAPServerManager ldapServerManager;
+ private boolean enabled;
+
+ @Override
+ public void init(GatewayConfig config, Map<String, String> options) throws
ServiceLifecycleException {
+ this.enabled = config.isLDAPEnabled();
+
+ if (!enabled) {
+ return;
+ }
+
+ try {
+ // Initialize the LDAP server manager with configuration
+ ldapServerManager = new KnoxLDAPServerManager();
+
+ // Prepare work directory for LDAP data
+ File gatewayDataDir = new File(config.getGatewayDataDir());
+ File ldapWorkDir = new File(gatewayDataDir, "ldap-server");
+
+ // Get configuration
+ int port = config.getLDAPPort();
+ String 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());
+ }
+
+ // For proxy backends, extract remoteBaseDn if present
+ String remoteBaseDn = backendConfig.get("remoteBaseDn");
+
+ // Initialize but don't start yet
+ ldapServerManager.initialize(ldapWorkDir, port, baseDn,
backendType, backendConfig, remoteBaseDn);
+
+ } catch (Exception e) {
+ throw new ServiceLifecycleException("Failed to initialize LDAP
service", e);
+ }
+ }
+
+ @Override
+ public void start() throws ServiceLifecycleException {
+ if (!enabled) {
+ return;
+ }
+
+ try {
+ // Start the LDAP server
+ ldapServerManager.start();
+ } catch (Exception e) {
+ LOG.ldapServiceStartFailed(e);
+ throw new ServiceLifecycleException("Failed to start LDAP
service", e);
+ }
+ }
+
+ @Override
+ public void stop() throws ServiceLifecycleException {
+ if (!enabled || ldapServerManager == null) {
+ return;
+ }
+
+ try {
+ ldapServerManager.stop();
+ } catch (Exception e) {
+ LOG.ldapServiceStopFailed(e);
+ throw new ServiceLifecycleException("Failed to stop LDAP service",
e);
+ }
+ }
+
+ /**
+ * Get the port the LDAP server is listening on
+ */
+ public int getLdapPort() {
+ return ldapServerManager != null ? ldapServerManager.getPort() : -1;
+ }
+
+ /**
+ * Get the base DN for LDAP entries
+ */
+ public String getBaseDn() {
+ return ldapServerManager != null ? ldapServerManager.getBaseDn() :
null;
+ }
+
+ /**
+ * Check if the LDAP service is enabled
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+}
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
new file mode 100644
index 000000000..a03c0b689
--- /dev/null
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/LdapMessages.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;
+
+import org.apache.knox.gateway.i18n.messages.Message;
+import org.apache.knox.gateway.i18n.messages.MessageLevel;
+import org.apache.knox.gateway.i18n.messages.Messages;
+import org.apache.knox.gateway.i18n.messages.StackTrace;
+
+@Messages(logger = "org.apache.knox.gateway.services.ldap")
+public interface LdapMessages {
+
+ @Message(level = MessageLevel.INFO,
+ text = "Starting LDAP service on port {0} with base DN: {1}")
+ void ldapServiceStarting(int port, String baseDn);
+
+ @Message(level = MessageLevel.INFO,
+ text = "LDAP service started successfully on port {0}")
+ void ldapServiceStarted(int port);
+
+ @Message(level = MessageLevel.INFO,
+ text = "Stopping LDAP service on port {0}")
+ void ldapServiceStopping(int port);
+
+ @Message(level = MessageLevel.INFO,
+ text = "LDAP service stopped successfully")
+ void ldapServiceStopped();
+
+ @Message(level = MessageLevel.ERROR,
+ text = "Failed to start LDAP service: {0}")
+ void ldapServiceStartFailed(@StackTrace(level = MessageLevel.DEBUG)
Exception e);
+
+ @Message(level = MessageLevel.ERROR,
+ text = "Failed to stop LDAP service: {0}")
+ void ldapServiceStopFailed(@StackTrace(level = MessageLevel.DEBUG)
Exception e);
+
+ @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.WARN,
+ text = "Data file not found: {0}, creating sample data")
+ void ldapDataFileNotFound(String dataFile);
+
+ @Message(level = MessageLevel.INFO,
+ text = "Loaded {0} users from {1}")
+ void ldapUsersLoaded(int count, String dataFile);
+
+ @Message(level = MessageLevel.INFO,
+ text = "Created sample data file: {0}")
+ void ldapSampleDataCreated(String path);
+
+ @Message(level = MessageLevel.DEBUG,
+ text = "LDAP Search: {0} | {1}")
+ void ldapSearch(String baseDn, String filter);
+
+ @Message(level = MessageLevel.DEBUG,
+ text = "LDAP Bind: {0}")
+ void ldapBind(String dn);
+
+ @Message(level = MessageLevel.INFO,
+ text = "Loaded user from backend: {0}")
+ void ldapUserLoaded(String username);
+
+ @Message(level = MessageLevel.INFO,
+ text = "Cleaning up old lock file: {0}")
+ void ldapCleaningLockFile(String lockFile);
+}
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
new file mode 100644
index 000000000..f432a6dc5
--- /dev/null
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/BackendFactory.java
@@ -0,0 +1,49 @@
+/*
+ * 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 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 loading backend implementations using ServiceLoader for full
extensibility.
+ * Backends are discovered via
META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackend
+ * Built-in backends (file, ldap) are registered via ServiceLoader along with
any external plugins.
+ */
+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);
+ return backend;
+ }
+ }
+
+ // No matching backend found
+ LOG.ldapBackendNotFound(backendName);
+ throw new IllegalArgumentException("No LDAP backend found for type: "
+ 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
new file mode 100644
index 000000000..b0cdcd860
--- /dev/null
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/FileBackend.java
@@ -0,0 +1,144 @@
+/*
+ * 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 com.google.gson.Gson;
+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.schema.SchemaManager;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.ldap.LdapMessages;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * File-based backend that reads user/group data from JSON
+ */
+public class FileBackend implements LdapBackend {
+ private static final LdapMessages LOG =
MessagesFactory.get(LdapMessages.class);
+
+ private Map<String, UserData> users = new HashMap<>();
+ private String dataFile;
+ private String baseDn;
+
+ static class UserData {
+ String username;
+ String cn;
+ String sn;
+ List<String> groups;
+ Map<String, String> attributes;
+ }
+
+ static class BackendData {
+ List<UserData> users;
+ }
+
+ @Override
+ public String getName() {
+ return "file";
+ }
+
+ @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();
+ }
+
+ private void loadData() throws Exception {
+ Path path = Paths.get(dataFile);
+
+ if (!Files.exists(path)) {
+ LOG.ldapDataFileNotFound(dataFile);
+ throw new Exception("LDAP data file not found: " + dataFile + ".
Please create the file with user data before starting the service.");
+ }
+
+ String json = Files.readString(path);
+ Gson gson = new Gson();
+ BackendData data = gson.fromJson(json, BackendData.class);
+
+ if (data != null && data.users != null) {
+ for (UserData user : data.users) {
+ users.put(user.username, user);
+ }
+ LOG.ldapUsersLoaded(users.size(), dataFile);
+ }
+ }
+
+ @Override
+ public Entry getUser(String username, SchemaManager schemaManager) throws
Exception {
+ UserData userData = users.get(username);
+ if (userData == null) {
+ return null;
+ }
+
+ Entry entry = new DefaultEntry(schemaManager);
+ entry.setDn("uid=" + userData.username + ",ou=Users," + baseDn);
+ entry.add("objectClass", "top");
+ entry.add("objectClass", "person");
+ entry.add("objectClass", "organizationalPerson");
+ entry.add("objectClass", "inetOrgPerson");
+ entry.add("uid", userData.username);
+ entry.add("cn", userData.cn);
+ entry.add("sn", userData.sn);
+
+ // Add groups as description
+ if (userData.groups != null && !userData.groups.isEmpty()) {
+ entry.add("description", "Groups: " + String.join(", ",
userData.groups));
+ }
+
+ // Add custom attributes
+ if (userData.attributes != null) {
+ for (Map.Entry<String, String> attr :
userData.attributes.entrySet()) {
+ entry.add(attr.getKey(), attr.getValue());
+ }
+ }
+
+ return entry;
+ }
+
+ @Override
+ public List<String> getUserGroups(String username) throws Exception {
+ UserData userData = users.get(username);
+ return userData != null && userData.groups != null ? userData.groups :
Collections.emptyList();
+ }
+
+ @Override
+ public List<Entry> searchUsers(String filter, SchemaManager schemaManager)
throws Exception {
+ List<Entry> results = new ArrayList<>();
+
+ // Simple filter matching - just check if username matches
+ for (String username : users.keySet()) {
+ if (filter.contains("uid=" + username) || filter.contains("*")) {
+ Entry entry = getUser(username, schemaManager);
+ if (entry != null) {
+ results.add(entry);
+ }
+ }
+ }
+
+ return results;
+ }
+}
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
new file mode 100644
index 000000000..6530c13b3
--- /dev/null
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapBackend.java
@@ -0,0 +1,68 @@
+/*
+ * 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 org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.schema.SchemaManager;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Interface for pluggable LDAP backends.
+ * Implementations can provide user/group data from various sources:
+ * - File-based (JSON, LDIF, properties)
+ * - JDBC databases
+ * - Remote LDAP servers (proxy/federation)
+ * - REST APIs (Knox, Ranger, etc.)
+ */
+public interface LdapBackend {
+ /**
+ * Get the name of this backend implementation
+ */
+ String getName();
+
+ /**
+ * Initialize the backend with configuration
+ * @param config Configuration properties
+ */
+ void initialize(Map<String, String> config) throws Exception;
+
+ /**
+ * Get a user entry by username
+ * @param username The username to look up
+ * @param schemaManager Schema manager for creating entries
+ * @return Entry or null if not found
+ */
+ Entry getUser(String username, SchemaManager schemaManager) throws
Exception;
+
+ /**
+ * Get groups for a user
+ * @param username The username
+ * @return List of group names
+ */
+ List<String> getUserGroups(String username) throws Exception;
+
+ /**
+ * Search for users matching a filter
+ * @param filter LDAP filter string (simplified)
+ * @param schemaManager Schema manager for creating entries
+ * @return List of matching entries
+ */
+ List<Entry> searchUsers(String filter, SchemaManager schemaManager) throws
Exception;
+}
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
new file mode 100644
index 000000000..2c37cb135
--- /dev/null
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackend.java
@@ -0,0 +1,452 @@
+/*
+ * 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 org.apache.directory.api.ldap.model.cursor.CursorException;
+import org.apache.directory.api.ldap.model.cursor.EntryCursor;
+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.exception.LdapException;
+import org.apache.directory.api.ldap.model.message.SearchScope;
+import org.apache.directory.api.ldap.model.schema.SchemaManager;
+import org.apache.directory.ldap.client.api.DefaultLdapConnectionFactory;
+import org.apache.directory.ldap.client.api.LdapConnection;
+import org.apache.directory.ldap.client.api.LdapConnectionConfig;
+import org.apache.directory.ldap.client.api.LdapConnectionPool;
+import
org.apache.directory.ldap.client.api.ValidatingPoolableLdapConnectionFactory;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.ldap.LdapMessages;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * LDAP backend that proxies to an external LDAP server.
+ * Can use central LDAP configuration or backend-specific configuration.
+ */
+public class LdapProxyBackend implements LdapBackend {
+ private static final LdapMessages LOG =
MessagesFactory.get(LdapMessages.class);
+
+ private String ldapUrl;
+ private String bindDn;
+ private String bindPassword;
+ private String userSearchBase;
+ private String groupSearchBase;
+ private String proxyBaseDn; // Base DN for proxy entries (e.g.,
dc=proxy,dc=com)
+ private String remoteBaseDn; // Base DN for remote server searches (e.g.,
dc=hadoop,dc=apache,dc=org)
+ private int port;
+ private String host;
+
+ // Configurable attributes for AD/LDAP compatibility
+ private String userIdentifierAttribute = "uid"; // uid for LDAP,
sAMAccountName for AD
+ private String userSearchFilter = "({userIdAttr}={username})"; // Will be
populated with userIdentifierAttribute
+ private String groupMemberAttribute = "memberUid"; // member for AD,
memberUid for POSIX
+ private boolean useMemberOf; // Use memberOf attribute for group lookup
(efficient for AD)
+
+ // 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 {
+ // Proxy base DN is for entries created in the proxy LDAP server
+ proxyBaseDn = config.get("baseDn");
+ if (proxyBaseDn == null || proxyBaseDn.isEmpty()) {
+ throw new IllegalArgumentException("baseDn is required for LDAP
proxy backend");
+ }
+
+ // Remote base DN is for searching the remote LDAP server
+ remoteBaseDn = config.get("remoteBaseDn");
+ if (remoteBaseDn == null || remoteBaseDn.isEmpty()) {
+ throw new IllegalArgumentException("remoteBaseDn is required for
LDAP proxy backend - this is the base DN of the remote LDAP server");
+ }
+
+ // Support both url and host/port configuration
+ ldapUrl = config.get("url");
+ if (ldapUrl != null && !ldapUrl.isEmpty()) {
+ // Parse URL to extract host and port
+ parseLdapUrl(ldapUrl);
+ } else {
+ host = config.get("host");
+ if (host == null || host.isEmpty()) {
+ throw new IllegalArgumentException("Either 'url' or 'host' is
required for LDAP proxy backend");
+ }
+ String portStr = config.get("port");
+ if (portStr == null || portStr.isEmpty()) {
+ throw new IllegalArgumentException("'port' is required when
using 'host' configuration");
+ }
+ port = Integer.parseInt(portStr);
+ ldapUrl = "ldap://" + host + ":" + port;
+ }
+
+ // Support both naming conventions: bindDn/bindPassword and
systemUsername/systemPassword
+ bindDn = config.get("bindDn");
+ if (bindDn == null || bindDn.isEmpty()) {
+ bindDn = config.get("systemUsername");
+ }
+
+ bindPassword = config.get("bindPassword");
+ if (bindPassword == null || bindPassword.isEmpty()) {
+ bindPassword = config.get("systemPassword");
+ }
+
+ // Search bases use the remote server's base DN
+ userSearchBase = config.getOrDefault("userSearchBase", "ou=people," +
remoteBaseDn);
+ groupSearchBase = config.getOrDefault("groupSearchBase", "ou=groups,"
+ remoteBaseDn);
+
+ // Configure attribute mappings for AD/LDAP compatibility
+ userIdentifierAttribute =
config.getOrDefault("userIdentifierAttribute", "uid");
+ config.getOrDefault("userDnTemplate",
"uid={username},ou=Users,{baseDn}");
+ groupMemberAttribute = config.getOrDefault("groupMemberAttribute",
"memberUid");
+ useMemberOf = Boolean.parseBoolean(config.getOrDefault("useMemberOf",
"false"));
+
+ // Build search filter template
+ userSearchFilter = "(" + userIdentifierAttribute + "={username})";
+
+ LOG.ldapBackendLoading(getName(), "Proxying " + proxyBaseDn + " to " +
ldapUrl + " (" + remoteBaseDn + ") with " +
+ userIdentifierAttribute + " attribute" +
+ (useMemberOf ? " using memberOf lookups" : "
using group searches"));
+
+ // Initialize connection pool
+ initializeConnectionPool(config);
+ }
+
+ /**
+ * 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 {
+ // Configure connection settings
+ LdapConnectionConfig connectionConfig = new LdapConnectionConfig();
+ connectionConfig.setLdapHost(host);
+ connectionConfig.setLdapPort(port);
+
+ if (bindDn != null && !bindDn.isEmpty()) {
+ connectionConfig.setName(bindDn);
+ connectionConfig.setCredentials(bindPassword);
+ }
+
+ // Connection pool configuration (with sensible defaults)
+ int maxActive = Integer.parseInt(config.getOrDefault("poolMaxActive",
"8"));
+
+ // Create connection factory
+ DefaultLdapConnectionFactory factory = new
DefaultLdapConnectionFactory(connectionConfig);
+
+ // Create validating poolable connection factory to test connections
+ ValidatingPoolableLdapConnectionFactory poolFactory = new
ValidatingPoolableLdapConnectionFactory(factory);
+
+ // Create the pool with max size
+ connectionPool = new LdapConnectionPool(poolFactory);
+ connectionPool.setMaxTotal(maxActive);
+ connectionPool.setTestOnBorrow(true);
+
+ LOG.ldapBackendLoading(getName(), "Initialized connection pool with
maxActive=" + maxActive);
+ }
+
+ private void parseLdapUrl(String url) {
+ // Simple URL parsing for ldap://host:port
+ if (url.startsWith("ldap://")) {
+ String hostPort = url.substring(7);
+ int colonIdx = hostPort.indexOf(':');
+ if (colonIdx > 0) {
+ host = hostPort.substring(0, colonIdx);
+ try {
+ port = Integer.parseInt(hostPort.substring(colonIdx + 1));
+ } catch (NumberFormatException e) {
+ port = 389;
+ }
+ } else {
+ host = hostPort;
+ port = 389;
+ }
+ } else if (url.startsWith("ldaps://")) {
+ String hostPort = url.substring(8);
+ int colonIdx = hostPort.indexOf(':');
+ if (colonIdx > 0) {
+ host = hostPort.substring(0, colonIdx);
+ try {
+ port = Integer.parseInt(hostPort.substring(colonIdx + 1));
+ } catch (NumberFormatException e) {
+ port = 636;
+ }
+ } else {
+ host = hostPort;
+ port = 636;
+ }
+ }
+ }
+
+ /**
+ * Gets a connection from the connection pool.
+ * Connections obtained from this method should be released back to the
pool
+ * using releaseConnection() when done.
+ *
+ * @return An LDAP connection from the pool
+ * @throws Exception if unable to get a connection from the pool
+ */
+ private LdapConnection getConnection() throws Exception {
+ return connectionPool.getConnection();
+ }
+
+ /**
+ * Releases a connection back to the pool.
+ * This method should be called in a finally block to ensure connections
are returned.
+ *
+ * @param connection The connection to release back to the pool
+ */
+ private void releaseConnection(LdapConnection connection) {
+ if (connection != null) {
+ try {
+ connectionPool.releaseConnection(connection);
+ } catch (Exception e) {
+ LOG.ldapServiceStopFailed(e);
+ }
+ }
+ }
+
+ /**
+ * Closes the connection pool and releases all resources.
+ * Should be called when the backend is being shut down.
+ */
+ public void close() {
+ if (connectionPool != null) {
+ try {
+ connectionPool.close();
+ } catch (Exception e) {
+ LOG.ldapServiceStopFailed(e);
+ }
+ }
+ }
+
+ @Override
+ public Entry getUser(String username, SchemaManager schemaManager) throws
Exception {
+ LdapConnection connection = null;
+ try {
+ connection = getConnection();
+ // Search for user using configurable attribute
+ String filter = userSearchFilter.replace("{username}", username);
+ EntryCursor cursor = connection.search(userSearchBase, filter,
SearchScope.SUBTREE, "*");
+
+ if (cursor.next()) {
+ Entry sourceEntry = cursor.get();
+ Entry entry = createProxyEntry(sourceEntry, username,
connection, schemaManager);
+ cursor.close();
+ return entry;
+ }
+
+ cursor.close();
+ return null;
+ } finally {
+ releaseConnection(connection);
+ }
+ }
+
+ @Override
+ public List<String> getUserGroups(String username) throws Exception {
+ LdapConnection connection = null;
+ try {
+ connection = getConnection();
+ if (useMemberOf) {
+ // Use memberOf attribute for efficient AD lookups
+ return getUserGroupsViaMemberOf(connection, username);
+ } else {
+ // Use traditional group search approach
+ String filter = userSearchFilter.replace("{username}",
username);
+ EntryCursor cursor = connection.search(userSearchBase, filter,
SearchScope.SUBTREE, "dn");
+
+ if (cursor.next()) {
+ String userDn = cursor.get().getDn().toString();
+ cursor.close();
+ return getUserGroupsInternal(connection, userDn, username);
+ }
+
+ cursor.close();
+ }
+ return Collections.emptyList();
+ } finally {
+ releaseConnection(connection);
+ }
+ }
+
+ private List<String> getUserGroupsViaMemberOf(LdapConnection connection,
String username) throws LdapException, CursorException, IOException {
+ List<String> groups = new ArrayList<>();
+
+ // Search for user and retrieve memberOf attribute
+ String filter = userSearchFilter.replace("{username}", username);
+ EntryCursor cursor = connection.search(userSearchBase, filter,
SearchScope.SUBTREE, "memberOf");
+
+ if (cursor.next()) {
+ Entry userEntry = cursor.get();
+ Attribute memberOfAttr = userEntry.get("memberOf");
+
+ if (memberOfAttr != null) {
+ // Extract group names from DNs
+ for (org.apache.directory.api.ldap.model.entry.Value value :
memberOfAttr) {
+ String groupDn = value.getString();
+ String groupName = extractGroupNameFromDn(groupDn);
+ if (groupName != null) {
+ groups.add(groupName);
+ }
+ }
+ }
+ }
+
+ cursor.close();
+ return groups;
+ }
+
+ private String extractGroupNameFromDn(String groupDn) {
+ // Extract CN from DN like "CN=Domain
Admins,CN=Users,DC=company,DC=com"
+ if (groupDn.toLowerCase(Locale.ROOT).startsWith("cn=")) {
+ int commaIdx = groupDn.indexOf(',');
+ if (commaIdx > 0) {
+ return groupDn.substring(3, commaIdx);
+ }
+ }
+ return null;
+ }
+
+ private List<String> getUserGroupsInternal(LdapConnection connection,
String userDn, String username) throws LdapException, CursorException,
IOException {
+ List<String> groups = new ArrayList<>();
+
+ // Search for groups where user is a member - build filter based on
configuration
+ String filter;
+ if ("member".equals(groupMemberAttribute)) {
+ // AD style - uses full DN
+ filter = "(|" +
+ "(member=" + userDn + ")" +
+ "(uniqueMember=" + userDn + ")" +
+ ")";
+ } else {
+ // POSIX style - uses username
+ filter = "(|" +
+ "(memberUid=" + username + ")" +
+ "(member=" + userDn + ")" +
+ "(uniqueMember=" + userDn + ")" +
+ ")";
+ }
+
+ EntryCursor cursor = connection.search(groupSearchBase, filter,
SearchScope.SUBTREE, "cn");
+
+ while (cursor.next()) {
+ Entry groupEntry = cursor.get();
+ Attribute cnAttr = groupEntry.get("cn");
+ if (cnAttr != null) {
+ groups.add(cnAttr.getString());
+ }
+ }
+
+ cursor.close();
+ return groups;
+ }
+
+ @Override
+ public List<Entry> searchUsers(String filter, SchemaManager schemaManager)
throws Exception {
+ List<Entry> results = new ArrayList<>();
+ LdapConnection connection = null;
+
+ try {
+ connection = getConnection();
+ String searchValue = filter.contains("*") ? "*" : filter;
+ String ldapFilter = "(" + userIdentifierAttribute + "=" +
searchValue + ")";
+ EntryCursor cursor = connection.search(userSearchBase, ldapFilter,
SearchScope.SUBTREE, "*");
+
+ while (cursor.next()) {
+ Entry sourceEntry = cursor.get();
+ Attribute idAttr = sourceEntry.get(userIdentifierAttribute);
+ if (idAttr != null) {
+ String username = idAttr.getString();
+ Entry entry = createProxyEntry(sourceEntry, username,
connection, schemaManager);
+ results.add(entry);
+ }
+ }
+
+ cursor.close();
+ return results;
+ } finally {
+ releaseConnection(connection);
+ }
+ }
+
+ /**
+ * Creates a proxy entry from a backend source entry with all required
attributes.
+ * This method standardizes the conversion of backend LDAP entries to
proxy entries,
+ * preserving the backend DN and copying all standard user attributes.
+ *
+ * @param sourceEntry The entry from the backend LDAP server
+ * @param username The username for the entry
+ * @param connection The LDAP connection for fetching group information
+ * @param schemaManager The schema manager for creating entries
+ * @return A new Entry with backend DN and all copied attributes
+ * @throws Exception if entry creation or attribute copying fails
+ */
+ private Entry createProxyEntry(Entry sourceEntry, String username,
LdapConnection connection, SchemaManager schemaManager) throws Exception {
+ // Standard proxy approach: return entry with backend DN unchanged
+ // This preserves DN integrity for bind operations and DN references
+ Entry entry = new DefaultEntry(schemaManager);
+ entry.setDn(sourceEntry.getDn());
+
+ // Copy all attributes as-is from backend
+ copyAttribute(sourceEntry, entry, "objectClass");
+ copyAttribute(sourceEntry, entry, userIdentifierAttribute);
+
+ // Map identifier attribute to uid for consistency if needed
+ if (!"uid".equals(userIdentifierAttribute)) {
+ Attribute idAttr = sourceEntry.get(userIdentifierAttribute);
+ if (idAttr != null) {
+ entry.add("uid", idAttr.getString());
+ }
+ }
+
+ copyAttribute(sourceEntry, entry, "cn");
+ copyAttribute(sourceEntry, entry, "sn");
+ copyAttribute(sourceEntry, entry, "mail");
+ copyAttribute(sourceEntry, entry, "description");
+ copyAttribute(sourceEntry, entry, "memberOf"); // Preserve group
memberships
+
+ // Get user's groups
+ List<String> groups = getUserGroupsInternal(connection,
sourceEntry.getDn().toString(), username);
+ if (!groups.isEmpty()) {
+ entry.add("description", "Groups: " + String.join(", ", groups));
+ }
+
+ return entry;
+ }
+
+ private void copyAttribute(Entry source, Entry target, String
attributeName) throws LdapException {
+ Attribute attr = source.get(attributeName);
+ if (attr != null) {
+ // Copy all values of the attribute (important for multi-valued
attributes like objectClass)
+ for (org.apache.directory.api.ldap.model.entry.Value value : attr)
{
+ target.add(attributeName, value.getString());
+ }
+ }
+ }
+}
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.LdapBackend
new file mode 100644
index 000000000..c8bd82de6
--- /dev/null
+++
b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ldap.backend.LdapBackend
@@ -0,0 +1,21 @@
+##########################################################################
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##########################################################################
+
+# Built-in LDAP backend implementations
+org.apache.knox.gateway.services.ldap.backend.FileBackend
+org.apache.knox.gateway.services.ldap.backend.LdapProxyBackend
\ 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 ff5293b56..9669a1941 100644
--- a/gateway-server/src/main/resources/conf/gateway-site.xml
+++ b/gateway-server/src/main/resources/conf/gateway-site.xml
@@ -37,4 +37,185 @@ limitations under the License.
<description>The directory within GATEWAY_HOME that contains gateway
topology files and deployments.</description>
</property>
+ <!-- LDAP Proxy Service Configuration -->
+ <property>
+ <name>gateway.ldap.enabled</name>
+ <value>true</value>
+ <description>Enable the embedded LDAP service for user and group
lookups. Set to true to enable.</description>
+ </property>
+
+ <property>
+ <name>gateway.ldap.port</name>
+ <value>3890</value>
+ <description>Port for the LDAP service to listen on. Default is
3890.</description>
+ </property>
+
+ <property>
+ <name>gateway.ldap.base.dn</name>
+ <value>dc=proxy,dc=com</value>
+ <description>Base DN for LDAP entries in the proxy server. Default is
dc=proxy,dc=com.</description>
+ </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>
+ </property>
+
+ <property>
+ <name>gateway.ldap.backend.data.file</name>
+ <value>${GATEWAY_DATA_HOME}/ldap-users.json</value>
+ <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) -->
+ <property>
+ <name>gateway.ldap.backend.ldap.url</name>
+ <value>ldap://localhost:33389</value>
+ <description>LDAP server URL for proxy backend</description>
+ </property>
+ <property>
+ <name>gateway.ldap.backend.ldap.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>
+ <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>
+ <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 -->
+
+ <!-- File backend configuration (gateway.ldap.backend.type=file) -->
+ <!--
+ <property>
+ <name>gateway.ldap.backend.file.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) -->
+ <!-- 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>
+ <value>ldap://localhost:33389</value>
+ <description>LDAP server URL</description>
+ </property>
+ <property>
+ <name>gateway.ldap.backend.ldap.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>
+ <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>
+ <value>guest-password</value>
+ <description>LDAP bind password</description>
+ </property>
+ Note: Entries from the remote server will be re-created under
gateway.ldap.base.dn (e.g., dc=proxy,dc=com)
+ -->
+ <!--
+ Example 2: Using external LDAP with authentication (supports both naming
conventions)
+ <property>
+ <name>gateway.ldap.backend.ldap.url</name>
+ <value>ldap://ldap.example.com:389</value>
+ <description>LDAP server URL</description>
+ </property>
+ <property>
+ <name>gateway.ldap.backend.ldap.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>
+ <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>
+ <value>secret</value>
+ <description>LDAP bind password (or use bindPassword)</description>
+ </property>
+ <property>
+ <name>gateway.ldap.backend.ldap.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>
+ <value>ou=groups,dc=example,dc=com</value>
+ <description>Base DN for group searches on remote server (defaults to
ou=groups,{remoteBaseDn})</description>
+ </property>
+ -->
+ <!--
+ Alternative: Use host and port instead of URL
+ <property>
+ <name>gateway.ldap.backend.ldap.host</name>
+ <value>localhost</value>
+ <description>LDAP server hostname</description>
+ </property>
+ <property>
+ <name>gateway.ldap.backend.ldap.port</name>
+ <value>33389</value>
+ <description>LDAP server port</description>
+ </property>
+ -->
+
+ <!-- Database backend configuration (gateway.ldap.backend.type=database)
-->
+ <!--
+ <property>
+ <name>gateway.ldap.backend.database.jdbc-url</name>
+ <value>jdbc:mysql://localhost:3306/knox</value>
+ <description>JDBC connection URL</description>
+ </property>
+ <property>
+ <name>gateway.ldap.backend.database.driver</name>
+ <value>com.mysql.cj.jdbc.Driver</value>
+ <description>JDBC driver class name</description>
+ </property>
+ <property>
+ <name>gateway.ldap.backend.database.username</name>
+ <value>knox_user</value>
+ <description>Database username</description>
+ </property>
+ <property>
+ <name>gateway.ldap.backend.database.password</name>
+ <value>secret</value>
+ <description>Database password</description>
+ </property>
+ -->
+
+ <!-- Knox Auth backend configuration (gateway.ldap.backend.type=knox-auth)
-->
+ <!--
+ <property>
+ <name>gateway.ldap.backend.knox-auth.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>
+ <value>admin</value>
+ <description>Knox admin username</description>
+ </property>
+ <property>
+ <name>gateway.ldap.backend.knox-auth.password</name>
+ <value>admin-password</value>
+ <description>Knox admin password</description>
+ </property>
+ -->
+
</configuration>
\ No newline at end of file
diff --git
a/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
b/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
index f38bc8b27..b27358c7c 100644
---
a/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
+++
b/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
@@ -65,7 +65,8 @@ public class AbstractGatewayServicesTest {
ServiceType.SERVICE_REGISTRY_SERVICE,
ServiceType.CONCURRENT_SESSION_VERIFIER,
ServiceType.REMOTE_CONFIGURATION_MONITOR,
- ServiceType.GATEWAY_STATUS_SERVICE
+ ServiceType.GATEWAY_STATUS_SERVICE,
+ ServiceType.LDAP_SERVICE
};
assertNotEquals(ServiceType.values(), orderedServiceTypes);
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
new file mode 100644
index 000000000..a9450a96f
--- /dev/null
+++
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServerManagerTest.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.services.ldap;
+
+import org.junit.Test;
+import org.junit.Before;
+import org.junit.After;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
+/**
+ * Unit tests for KnoxLDAPServerManager.
+ */
+public class KnoxLDAPServerManagerTest {
+
+ private KnoxLDAPServerManager serverManager;
+ private File tempWorkDir;
+ private File tempLdapFile;
+
+ @Before
+ public void setUp() throws Exception {
+ serverManager = new KnoxLDAPServerManager();
+
+ // Create temporary work directory
+ tempWorkDir = File.createTempFile("knox-ldap-work", "");
+ tempWorkDir.delete();
+ tempWorkDir.mkdirs();
+ tempWorkDir.deleteOnExit();
+
+ // Create temporary LDAP data file
+ tempLdapFile = File.createTempFile("ldap-test", ".json");
+ tempLdapFile.deleteOnExit();
+
+ 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\":[]}");
+ }
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (serverManager != null) {
+ try {
+ serverManager.stop();
+ } catch (Exception e) {
+ // Ignore cleanup errors
+ }
+ }
+ cleanupTempFiles();
+ }
+
+ @Test
+ public void testInitializeWithFileBackend() throws Exception {
+ Map<String, String> backendConfig = createFileBackendConfig();
+
+ serverManager.initialize(tempWorkDir, 3890, "dc=test,dc=com", "file",
backendConfig, null);
+
+ assertEquals("Port should be set correctly", 3890,
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 testInitializeWithLdapBackend() throws Exception {
+ Map<String, String> backendConfig = createLdapBackendConfig();
+
+ serverManager.initialize(tempWorkDir, 3891, "dc=proxy,dc=com", "ldap",
backendConfig, "dc=hadoop,dc=apache,dc=org");
+
+ assertEquals("Port should be set correctly", 3891,
serverManager.getPort());
+ assertEquals("Base DN should be set correctly", "dc=proxy,dc=com",
serverManager.getBaseDn());
+ assertFalse("Should not be running after initialize",
serverManager.isRunning());
+ }
+
+ @Test(expected = Exception.class)
+ public void testInitializeWithInvalidBackendType() throws Exception {
+ Map<String, String> backendConfig = new HashMap<>();
+ backendConfig.put("baseDn", "dc=test,dc=com");
+
+ serverManager.initialize(tempWorkDir, 3890, "dc=test,dc=com",
"invalid", backendConfig, null);
+ }
+
+ @Test
+ public void testLockFileCleanup() throws Exception {
+ // Create a lock file to simulate previous unclean shutdown
+ File runDir = new File(tempWorkDir, "run");
+ runDir.mkdirs();
+ File lockFile = new File(runDir, "instance.lock");
+ lockFile.createNewFile();
+ assertTrue("Lock file should exist before initialization",
lockFile.exists());
+
+ Map<String, String> backendConfig = createFileBackendConfig();
+ serverManager.initialize(tempWorkDir, 3890, "dc=test,dc=com", "file",
backendConfig, null);
+
+ assertFalse("Lock file should be cleaned up during initialization",
lockFile.exists());
+ }
+
+ @Test
+ public void testGettersBeforeInitialization() {
+ assertEquals("Port should be 0 before initialization", 0,
serverManager.getPort());
+ assertEquals("Base DN should be null before initialization", null,
serverManager.getBaseDn());
+ assertFalse("Should not be running before initialization",
serverManager.isRunning());
+ }
+
+ @Test
+ public void testStopBeforeStart() throws Exception {
+ Map<String, String> backendConfig = createFileBackendConfig();
+ serverManager.initialize(tempWorkDir, 3890, "dc=test,dc=com", "file",
backendConfig, null);
+
+ // Should not throw exception when stopping before starting
+ serverManager.stop();
+ }
+
+ @Test
+ public void testMultipleStopCalls() throws Exception {
+ Map<String, String> backendConfig = createFileBackendConfig();
+ serverManager.initialize(tempWorkDir, 3890, "dc=test,dc=com", "file",
backendConfig, null);
+
+ // Multiple stop calls should not throw exceptions
+ serverManager.stop();
+ serverManager.stop();
+ serverManager.stop();
+ }
+
+ @Test(expected = Exception.class)
+ public void testStartWithoutInitialize() throws Exception {
+ // Should throw exception when starting without initialization
+ serverManager.start();
+ }
+
+ private Map<String, String> createFileBackendConfig() {
+ Map<String, String> config = new HashMap<>();
+ config.put("baseDn", "dc=test,dc=com");
+ config.put("dataFile", tempLdapFile.getAbsolutePath());
+ return config;
+ }
+
+ private Map<String, String> createLdapBackendConfig() {
+ Map<String, String> config = new HashMap<>();
+ 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();
+ }
+ if (tempWorkDir != null && tempWorkDir.exists()) {
+ // Clean up work directory recursively
+ deleteRecursively(tempWorkDir);
+ }
+ }
+
+ private void deleteRecursively(File file) {
+ if (file.isDirectory()) {
+ File[] children = file.listFiles();
+ if (children != null) {
+ for (File child : children) {
+ deleteRecursively(child);
+ }
+ }
+ }
+ file.delete();
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 000000000..dbdb081da
--- /dev/null
+++
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/KnoxLDAPServiceTest.java
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.services.ldap;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.junit.Test;
+import org.junit.Before;
+import org.junit.After;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+/**
+ * Unit tests for KnoxLDAPService.
+ */
+public class KnoxLDAPServiceTest {
+
+ private KnoxLDAPService ldapService;
+ private GatewayConfig mockConfig;
+ private File tempDataDir;
+ private File tempLdapFile;
+
+ @Before
+ public void setUp() throws Exception {
+ ldapService = new KnoxLDAPService();
+ mockConfig = createMock(GatewayConfig.class);
+
+ // Create temporary directories and files
+ tempDataDir = File.createTempFile("knox-ldap-test", "");
+ tempDataDir.delete();
+ tempDataDir.mkdirs();
+ tempDataDir.deleteOnExit();
+
+ tempLdapFile = new File(tempDataDir, "ldap-users.json");
+ try (java.io.BufferedWriter writer =
java.nio.file.Files.newBufferedWriter(tempLdapFile.toPath(),
java.nio.charset.StandardCharsets.UTF_8)) {
+ writer.write("{\"users\":[],\"groups\":[]}");
+ }
+ tempLdapFile.deleteOnExit();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (ldapService != null) {
+ try {
+ ldapService.stop();
+ } catch (Exception e) {
+ // Ignore cleanup errors
+ }
+ }
+ if (tempLdapFile != null && tempLdapFile.exists()) {
+ tempLdapFile.delete();
+ }
+ if (tempDataDir != null && tempDataDir.exists()) {
+ tempDataDir.delete();
+ }
+ }
+
+ @Test
+ public void testInitWithLdapDisabled() throws Exception {
+ expect(mockConfig.isLDAPEnabled()).andReturn(false);
+ replay(mockConfig);
+
+ ldapService.init(mockConfig, new HashMap<>());
+
+ assertFalse("LDAP service should not be enabled",
ldapService.isEnabled());
+ assertEquals("LDAP port should be -1 when disabled", -1,
ldapService.getLdapPort());
+
+ verify(mockConfig);
+ }
+
+ @Test
+ public void testInitWithLdapEnabledFileBackend() throws Exception {
+ setupMockConfigForFileBackend();
+ replay(mockConfig);
+
+ ldapService.init(mockConfig, new HashMap<>());
+
+ assertTrue("LDAP service should be enabled", ldapService.isEnabled());
+ assertEquals("Base DN should match config", "dc=test,dc=com",
ldapService.getBaseDn());
+
+ verify(mockConfig);
+ }
+
+ @Test
+ public void testInitWithLdapEnabledLdapBackend() throws Exception {
+ setupMockConfigForLdapBackend();
+ replay(mockConfig);
+
+ ldapService.init(mockConfig, new HashMap<>());
+
+ assertTrue("LDAP service should be enabled", ldapService.isEnabled());
+ assertEquals("Base DN should match config", "dc=proxy,dc=com",
ldapService.getBaseDn());
+
+ verify(mockConfig);
+ }
+
+ @Test(expected = ServiceLifecycleException.class)
+ public void testInitWithInvalidBackendType() throws Exception {
+ expect(mockConfig.isLDAPEnabled()).andReturn(true);
+
expect(mockConfig.getGatewayDataDir()).andReturn(tempDataDir.getAbsolutePath());
+ expect(mockConfig.getLDAPPort()).andReturn(3890);
+ expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com");
+ expect(mockConfig.getLDAPBackendType()).andReturn("invalid");
+ expect(mockConfig.getLDAPBackendConfig("invalid")).andReturn(new
HashMap<>());
+ replay(mockConfig);
+
+ ldapService.init(mockConfig, new HashMap<>());
+ }
+
+ @Test
+ public void testStartWhenDisabled() throws Exception {
+ expect(mockConfig.isLDAPEnabled()).andReturn(false);
+ replay(mockConfig);
+
+ ldapService.init(mockConfig, new HashMap<>());
+
+ // Should not throw exception
+ ldapService.start();
+
+ verify(mockConfig);
+ }
+
+ @Test
+ public void testStopWhenDisabled() throws Exception {
+ expect(mockConfig.isLDAPEnabled()).andReturn(false);
+ replay(mockConfig);
+
+ ldapService.init(mockConfig, new HashMap<>());
+
+ // Should not throw exception
+ ldapService.stop();
+
+ verify(mockConfig);
+ }
+
+ @Test
+ public void testGettersWhenNotInitialized() {
+ assertEquals("LDAP port should be -1 when not initialized", -1,
ldapService.getLdapPort());
+ assertEquals("Base DN should be null when not initialized", null,
ldapService.getBaseDn());
+ assertFalse("Should not be enabled when not initialized",
ldapService.isEnabled());
+ }
+
+ private void setupMockConfigForFileBackend() {
+ expect(mockConfig.isLDAPEnabled()).andReturn(true);
+
expect(mockConfig.getGatewayDataDir()).andReturn(tempDataDir.getAbsolutePath());
+ expect(mockConfig.getLDAPPort()).andReturn(3890);
+ expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com");
+ expect(mockConfig.getLDAPBackendType()).andReturn("file");
+
+ Map<String, String> fileBackendConfig = new HashMap<>();
+ fileBackendConfig.put("dataFile", tempLdapFile.getAbsolutePath());
+
expect(mockConfig.getLDAPBackendConfig("file")).andReturn(fileBackendConfig);
+ }
+
+ private void setupMockConfigForLdapBackend() {
+ expect(mockConfig.isLDAPEnabled()).andReturn(true);
+
expect(mockConfig.getGatewayDataDir()).andReturn(tempDataDir.getAbsolutePath());
+ expect(mockConfig.getLDAPPort()).andReturn(3890);
+ expect(mockConfig.getLDAPBaseDN()).andReturn("dc=proxy,dc=com");
+ expect(mockConfig.getLDAPBackendType()).andReturn("ldap");
+
+ Map<String, String> ldapBackendConfig = new HashMap<>();
+ ldapBackendConfig.put("url", "ldap://localhost:33389");
+ ldapBackendConfig.put("remoteBaseDn", "dc=hadoop,dc=apache,dc=org");
+ ldapBackendConfig.put("systemUsername",
"cn=admin,dc=hadoop,dc=apache,dc=org");
+ ldapBackendConfig.put("systemPassword", "admin-password");
+
expect(mockConfig.getLDAPBackendConfig("ldap")).andReturn(ldapBackendConfig);
+ }
+}
\ No newline at end of file
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
new file mode 100644
index 000000000..fde29a087
--- /dev/null
+++
b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/BackendFactoryTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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 org.junit.Test;
+import org.junit.Before;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.ServiceLoader;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Unit tests to verify ServiceLoader discovery of LDAP backends.
+ */
+public class BackendFactoryTest {
+
+ private Map<String, String> config;
+
+ @Before
+ public void setUp() throws Exception {
+ config = new HashMap<>();
+ config.put("baseDn", "dc=test,dc=com");
+
+ // Create a temporary data file for FileBackend tests
+ java.io.File tempFile = java.io.File.createTempFile("ldap-test",
".json");
+ tempFile.deleteOnExit();
+
+ // Write minimal valid JSON
+ try (java.io.BufferedWriter writer =
java.nio.file.Files.newBufferedWriter(tempFile.toPath(),
java.nio.charset.StandardCharsets.UTF_8)) {
+ writer.write("{\"users\":[],\"groups\":[]}\n");
+ }
+
+ config.put("dataFile", tempFile.getAbsolutePath());
+ }
+
+ @Test
+ public void testServiceLoaderDiscovery() {
+ ServiceLoader<LdapBackend> loader =
ServiceLoader.load(LdapBackend.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)) {
+ foundFileBackend = true;
+ assertTrue("File backend should be FileBackend instance",
backend instanceof FileBackend);
+ } else if ("ldap".equals(backendName)) {
+ foundLdapBackend = true;
+ assertTrue("LDAP backend should be LdapProxyBackend instance",
backend instanceof LdapProxyBackend);
+ }
+ }
+
+ assertTrue("ServiceLoader should discover file backend",
foundFileBackend);
+ assertTrue("ServiceLoader should discover ldap backend",
foundLdapBackend);
+ }
+
+ @Test
+ public void testCreateFileBackend() throws Exception {
+ LdapBackend fileBackend = BackendFactory.createBackend("file", 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());
+ }
+
+ @Test
+ public void testCreateLdapBackend() throws Exception {
+ config.put("url", "ldap://localhost:389");
+ config.put("remoteBaseDn", "dc=hadoop,dc=apache,dc=org");
+
+ LdapBackend ldapBackend = BackendFactory.createBackend("ldap", 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());
+ }
+
+ @Test
+ public void testCaseInsensitiveBackendNames() throws Exception {
+ // Test uppercase
+ LdapBackend upperCaseBackend = BackendFactory.createBackend("FILE",
config);
+ assertTrue("Should create FileBackend with uppercase name",
upperCaseBackend instanceof FileBackend);
+
+ // Test mixed case
+ LdapBackend mixedCaseBackend = BackendFactory.createBackend("File",
config);
+ assertTrue("Should create FileBackend with mixed case name",
mixedCaseBackend instanceof FileBackend);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testUnknownBackendThrowsException() throws Exception {
+ BackendFactory.createBackend("unknown", config);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testNullBackendNameThrowsException() throws Exception {
+ BackendFactory.createBackend(null, config);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testEmptyBackendNameThrowsException() throws Exception {
+ BackendFactory.createBackend("", config);
+ }
+}
\ 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 5d96dfbb4..7c848d236 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
@@ -1202,4 +1202,40 @@ public class GatewayTestConfig extends Configuration
implements GatewayConfig {
public boolean isTopologyAsyncSupported(String topology) {
return false;
}
+
+ // LDAP Service Configuration
+ @Override
+ public boolean isLDAPEnabled() {
+ return false;
+ }
+
+ @Override
+ public int getLDAPPort() {
+ return 3890;
+ }
+
+ @Override
+ public String getLDAPBaseDN() {
+ return "dc=test,dc=com";
+ }
+
+ @Override
+ public String getLDAPBackendType() {
+ return "file";
+ }
+
+ @Override
+ public String getLDAPBackendDataFile() {
+ return getGatewayDataPath().resolve("ldap-users.json").toString();
+ }
+
+ @Override
+ public Set<String> getPropertyNames() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Map<String, String> getLDAPBackendConfig(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 ad3fee6d8..153f2c568 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
@@ -123,6 +123,13 @@ public interface GatewayConfig {
String DEPLOYMENT_PATH_ALIAS = ".path.alias.";
+ // LDAP Service Configuration
+ 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_BACKEND_DATA_FILE = "gateway.ldap.backend.data.file";
+
/**
* The location of the gateway configuration.
* Subdirectories will be: topologies
@@ -1031,4 +1038,44 @@ public interface GatewayConfig {
* @return the strict transport option if set; otherwise return the default
value 'max-age=31536000'
*/
String getStrictTransportOption();
+
+ /**
+ * @return true if the embedded LDAP service is enabled; otherwise false
+ */
+ boolean isLDAPEnabled();
+
+ /**
+ * @return the port for the LDAP service to listen on
+ */
+ int getLDAPPort();
+
+ /**
+ * @return the base DN for LDAP entries
+ */
+ String getLDAPBaseDN();
+
+ /**
+ * @return the backend type for LDAP (file, ldap, jdbc, etc.)
+ */
+ String getLDAPBackendType();
+
+ /**
+ * @return the path to the data file for file-based backend
+ */
+ String getLDAPBackendDataFile();
+
+ /**
+ * Get backend-specific configuration properties.
+ * Returns all properties with prefix "gateway.ldap.backend.{backendType}."
+ * with the prefix stripped from the keys.
+ *
+ * @param backendType the backend type (e.g., "file", "ldap", "database")
+ * @return map of configuration key-value pairs for the specified backend
+ */
+ Map<String, String> getLDAPBackendConfig(String backendType);
+
+ /**
+ * @return set of all property names in the configuration
+ */
+ Set<String> getPropertyNames();
}
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
index de4a83db9..d96db8d49 100644
---
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
@@ -38,7 +38,8 @@ public enum ServiceType {
TOPOLOGY_SERVICE("TopologyService"),
CONCURRENT_SESSION_VERIFIER("ConcurrentSessionVerifier"),
REMOTE_CONFIGURATION_MONITOR("RemoteConfigurationMonitor"),
- GATEWAY_STATUS_SERVICE("GatewayStatusService");
+ GATEWAY_STATUS_SERVICE("GatewayStatusService"),
+ LDAP_SERVICE("LDAPService");
private final String serviceTypeName;
private final String shortName;
diff --git a/knox-token-management-ui/package.json
b/knox-token-management-ui/package.json
index c3c363404..85a8a83fd 100644
--- a/knox-token-management-ui/package.json
+++ b/knox-token-management-ui/package.json
@@ -6,7 +6,7 @@
"scripts": {
"start": "ng serve --verbose=true",
"build": "ng build",
- "build-prod": "ng build --configuration production",
+ "build-prod": "NODE_OPTIONS='--no-warnings' ng build --configuration
production",
"lint": "eslint token-management"
},
"private": true,
diff --git a/pom.xml b/pom.xml
index 4309d8b54..db2df1d3e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -189,6 +189,7 @@
<commons-logging.version>1.3.5</commons-logging.version>
<commons-math3.version>3.6.1</commons-math3.version>
<commons-net.version>3.9.0</commons-net.version>
+ <commons-pool2.version>2.7.0</commons-pool2.version>
<commons-text.version>1.10.0</commons-text.version>
<cors-filter.version>2.9.1</cors-filter.version>
<cryptacular.version>1.2.7</cryptacular.version>
@@ -2087,6 +2088,11 @@
<artifactId>commons-net</artifactId>
<version>${commons-net.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-pool2</artifactId>
+ <version>${commons-pool2.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
@@ -2124,7 +2130,6 @@
<artifactId>apacheds-jdbm</artifactId>
<version>${apacheds-jdbm.version}</version>
</dependency>
-
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-core</artifactId>
@@ -2167,6 +2172,16 @@
</exclusion>
</exclusions>
</dependency>
+ <dependency>
+ <groupId>org.apache.directory.server</groupId>
+ <artifactId>apacheds-ldif-partition</artifactId>
+ <version>${apacheds.directory.server.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.directory.server</groupId>
+ <artifactId>apacheds-jdbm-partition</artifactId>
+ <version>${apacheds.directory.server.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.directory.api</groupId>
@@ -2183,6 +2198,11 @@
<artifactId>api-ldap-model</artifactId>
<version>${apacheds.directory.api.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.directory.api</groupId>
+ <artifactId>api-ldap-schema-data</artifactId>
+ <version>${apacheds.directory.api.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.directory.api</groupId>
<artifactId>api-util</artifactId>