Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.google.common.base.Strings;
import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
Expand Down Expand Up @@ -1748,8 +1749,16 @@ public String getLDAPBaseDN() {
}

@Override
public String getLDAPBackendType() {
return get(LDAP_BACKEND_TYPE, "file");
public List<String> getLDAPInterceptorNames() {
String names = get(LDAP_INTERCEPTOR_NAMES, "");
String[] namesArray = names.split(",");
List<String> namesList = new ArrayList<>();
for (String name : namesArray) {
if (!Strings.isNullOrEmpty(name)) {
namesList.add(name.trim());
}
}
return namesList;
}

@Override
Expand All @@ -1776,9 +1785,9 @@ public Set<String> getPropertyNames() {
}

@Override
public Map<String, String> getLDAPBackendConfig(String backendType) {
public Map<String, String> getLDAPInterceptorConfig(String interceptorName) {
Map<String, String> config = new HashMap<>();
String prefix = "gateway.ldap.backend." + backendType + ".";
String prefix = "gateway.ldap.interceptor." + interceptorName + ".";

for (String key : getPropertyNames()) {
if (key != null && key.startsWith(prefix)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,58 +17,57 @@
*/
package org.apache.knox.gateway.services.ldap;

import com.google.common.annotations.VisibleForTesting;
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.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.interceptor.Interceptor;
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 org.apache.knox.gateway.services.ldap.interceptor.UserSearchInterceptor;

import java.io.File;
import java.util.Map;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
* Manages the ApacheDS LDAP server instance with pluggable backends
*/
public class KnoxLDAPServerManager {
private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class);

private DirectoryService directoryService;
@VisibleForTesting
DirectoryService directoryService;
private LdapServer ldapServer;
private LdapBackend backend;
private List<Interceptor> interceptors;
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)
* @param interceptors List of LDAP interceptors
*/
public void initialize(File workDir, int port, String baseDn, String backendType, Map<String, String> backendConfig, String remoteBaseDn) throws Exception {
public void initialize(File workDir, int port, String baseDn, List<Interceptor> interceptors) 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);
this.interceptors = interceptors;

// Clean up previous run if it didn't shut down cleanly
File lockFile = new File(workDir, "run/instance.lock");
Expand Down Expand Up @@ -115,27 +114,36 @@ public void start() throws Exception {
partition.setPartitionPath(new File(workDir, "proxy").toURI());
directoryService.addPartition(partition);

// Add our interceptor for user search
// 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);
Set<String> baseDns = new HashSet<>();
baseDns.add(baseDn);
for (Interceptor interceptor : interceptors) {
if (interceptor instanceof UserSearchInterceptor) {
LdapBackend backend = ((UserSearchInterceptor) interceptor).getBackend();
String remoteBaseDn = backend.getBaseDn();
if (!baseDns.contains(remoteBaseDn)) {
//create partition
String id = backend.getName().replaceAll("\\s+", "");
JdbmPartition remotePartition = new JdbmPartition(schemaManager, directoryService.getDnFactory());
remotePartition.setId(id);
remotePartition.setSuffixDn(new Dn(schemaManager, remoteBaseDn));
remotePartition.setPartitionPath(new File(workDir, id).toURI());
directoryService.addPartition(remotePartition);
baseDns.add(remoteBaseDn);
} }
directoryService.addLast(interceptor);
}

// 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);
// Add base entries to the partitions
createBaseEntries(baseDns, schemaManager);

// Create LDAP server on configured port
ldapServer = new LdapServer();
Expand All @@ -156,6 +164,7 @@ public void stop() throws Exception {
if (ldapServer != null) {
try {
ldapServer.stop();
ldapServer = null;
} catch (Exception e) {
LOG.ldapServiceStopFailed(e);
}
Expand All @@ -164,6 +173,7 @@ public void stop() throws Exception {
if (directoryService != null) {
try {
directoryService.shutdown();
directoryService = null;
} catch (Exception e) {
LOG.ldapServiceStopFailed(e);
}
Expand All @@ -172,13 +182,10 @@ public void stop() throws Exception {
LOG.ldapServiceStopped();
}

private void createBaseEntries(SchemaManager schemaManager) throws Exception {
// Create base entries for proxy base DN
createBaseEntriesForDn(schemaManager, baseDn);

// Create base entries for remote base DN if different
if (remoteBaseDn != null && !remoteBaseDn.equals(baseDn)) {
createBaseEntriesForDn(schemaManager, remoteBaseDn);
private void createBaseEntries(Collection<String> baseDns, SchemaManager schemaManager) throws Exception {
// Create base entries for proxy base DN and remote base DNs
for (String baseDn : baseDns) {
createBaseEntriesForDn(schemaManager, baseDn);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@
*/
package org.apache.knox.gateway.services.ldap;

import org.apache.directory.server.core.api.interceptor.Interceptor;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.services.Service;
import org.apache.knox.gateway.services.ServiceLifecycleException;
import org.apache.knox.gateway.services.ldap.interceptor.InterceptorFactory;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
Expand Down Expand Up @@ -54,24 +59,32 @@ public void init(GatewayConfig config, Map<String, String> options) throws Servi
// 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());
List<String> interceptorNames = config.getLDAPInterceptorNames();
List<Interceptor> interceptors = new ArrayList<>(interceptorNames.size());
for (String interceptorName : interceptorNames) {
// Get backend-specific configuration using prefixed properties
Map<String, String> interceptorConfig = config.getLDAPInterceptorConfig(interceptorName);

// Add common configuration
interceptorConfig.put("baseDn", baseDn);

// Add legacy dataFile property for backwards compatibility with file backend
String interceptorType = interceptorConfig.get("interceptorType");
String backendType = interceptorConfig.get("backendType");
if ("backend".equalsIgnoreCase(interceptorType) &&
"file".equalsIgnoreCase(backendType) &&
!interceptorConfig.containsKey("dataFile")) {
interceptorConfig.put("dataFile", config.getLDAPBackendDataFile());
}

interceptors.add(InterceptorFactory.createInterceptor(interceptorName, interceptorConfig));
}

// For proxy backends, extract remoteBaseDn if present
String remoteBaseDn = backendConfig.get("remoteBaseDn");
// Reverse order of interceptors so the highest priority interceptor is last in the list.
Collections.reverse(interceptors);

// Initialize but don't start yet
ldapServerManager.initialize(ldapWorkDir, port, baseDn, backendType, backendConfig, remoteBaseDn);
ldapServerManager.initialize(ldapWorkDir, port, baseDn, interceptors);

} catch (Exception e) {
throw new ServiceLifecycleException("Failed to initialize LDAP service", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,29 @@ public interface LdapMessages {
text = "Failed to stop LDAP service: {0}")
void ldapServiceStopFailed(@StackTrace(level = MessageLevel.DEBUG) Exception e);

@Message(level = MessageLevel.ERROR,
text = "Interceptor type ''{0}'' for interceptor ''{1}'' not found")
void ldapInterceptorNotFound(String interceptorType, String interceptorName);

@Message(level = MessageLevel.ERROR,
text = "Interceptor type not found for interceptor ''{0}''")
void ldapInterceptorTypeNotFound(String interceptorName);

@Message(level = MessageLevel.INFO,
text = "Creating interceptor: {0} (via {1})")
void ldapInterceptorCreating(String interceptorName, String source);

@Message(level = MessageLevel.INFO,
text = "Loading backend: {0} (via {1})")
void ldapBackendLoading(String backendName, String source);

@Message(level = MessageLevel.WARN,
text = "Backend ''{0}'' not found, using FileBackend")
void ldapBackendNotFound(String backendName);
@Message(level = MessageLevel.ERROR,
text = "Backend type ''{0}'' for backend ''{1}'' not found")
void ldapBackendNotFound(String backendType, String backendName);

@Message(level = MessageLevel.ERROR,
text = "Backend type not found for backend ''{0}''")
void ldapBackendTypeNotFound(String backendName);

@Message(level = MessageLevel.WARN,
text = "Data file not found: {0}, creating sample data")
Expand All @@ -73,6 +89,10 @@ public interface LdapMessages {
text = "LDAP Search: {0} | {1}")
void ldapSearch(String baseDn, String filter);

@Message(level = MessageLevel.ERROR,
text = "LDAP Search failed: {0} | {1}, {2}")
void ldapSearchFailed(String baseDn, String filter, @StackTrace(level = MessageLevel.DEBUG) Exception e);

@Message(level = MessageLevel.DEBUG,
text = "LDAP Bind: {0}")
void ldapBind(String dn);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,25 @@ public class BackendFactory {
private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class);

public static LdapBackend createBackend(String backendName, Map<String, String> config) throws Exception {
// Use ServiceLoader to discover all available backends (built-in and external plugins)
ServiceLoader<LdapBackend> loader = ServiceLoader.load(LdapBackend.class);
for (LdapBackend backend : loader) {
if (backend.getName().equalsIgnoreCase(backendName)) {
LOG.ldapBackendLoading(backend.getName(), "ServiceLoader");
backend.initialize(config);
String backendType = config.get("backendType");
if (backendType == null) {
// No backend type configured found
LOG.ldapBackendTypeNotFound(backendName);
throw new IllegalArgumentException("No LDAP backend type configured for : " + backendName);
}

// Use ServiceLoader to discover all available backend factories (built-in and external plugins)
ServiceLoader<LdapBackendFactory> loader = ServiceLoader.load(LdapBackendFactory.class);
for (LdapBackendFactory backendFactory : loader) {
if (backendFactory.getType().equalsIgnoreCase(backendType)) {
LOG.ldapBackendLoading(backendType, "ServiceLoader");
LdapBackend backend = backendFactory.create(backendName, config);
return backend;
}
}

// No matching backend found
LOG.ldapBackendNotFound(backendName);
throw new IllegalArgumentException("No LDAP backend found for type: " + backendName);
LOG.ldapBackendNotFound(backendType, backendName);
throw new IllegalArgumentException("No LDAP backend factory of type " + backendType + " found for : " + backendName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@
public class FileBackend implements LdapBackend {
private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class);

static final String TYPE = "file";

private Map<String, UserData> users = new HashMap<>();
private String dataFile;
private String baseDn;
private final String dataFile;
private final String baseDn;
private final String name;

static class UserData {
String username;
Expand All @@ -55,16 +58,26 @@ static class BackendData {
List<UserData> users;
}

public FileBackend(String name, Map<String, String> config) throws Exception {
this.name = name;
dataFile = config.getOrDefault("dataFile", "ldap-users.json");
baseDn = config.getOrDefault("baseDn", "dc=proxy,dc=com");
loadData();
}

@Override
public String getName() {
return "file";
return name;
}

@Override
public void initialize(Map<String, String> config) throws Exception {
dataFile = config.getOrDefault("dataFile", "ldap-users.json");
baseDn = config.getOrDefault("baseDn", "dc=proxy,dc=com");
loadData();
public String getType() {
return TYPE;
}

@Override
public String getBaseDn() {
return baseDn;
}

private void loadData() throws Exception {
Expand Down
Loading
Loading