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 @@ -1748,8 +1748,8 @@ public String getLDAPBaseDN() {
}

@Override
public String getLDAPBackendType() {
return get(LDAP_BACKEND_TYPE, "file");
public List<String> getLDAPInterceptorNames() {
return splitConfigValueToList(LDAP_INTERCEPTOR_NAMES);
}

@Override
Expand All @@ -1776,9 +1776,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,8 +17,10 @@
*/
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.exception.LdapException;
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;
Expand All @@ -32,27 +34,33 @@
import org.apache.directory.server.protocol.shared.transport.TcpTransport;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.services.ldap.backend.BackendFactory;
import org.apache.knox.gateway.services.ldap.backend.LdapBackend;
import org.apache.knox.gateway.services.ldap.interceptor.InterceptorFactory;
import org.apache.knox.gateway.services.ldap.interceptor.UserSearchInterceptor;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
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 = new ArrayList<>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is redundant, as you initialize interceptors anyway (i.e. not simply add entries into it).
See new line 80: this.interceptors = createInterceptors(config)

private File workDir;
private int port;
private String baseDn;
private String remoteBaseDn;
// Collection of DNs for the proxied backend LDAP servers
private Set<String> baseDns;

/**
* Initialize the LDAP server with the given configuration
Expand All @@ -67,24 +75,8 @@ public void initialize(GatewayConfig config) throws Exception {
// Get configuration
this.port = config.getLDAPPort();
this.baseDn = config.getLDAPBaseDN();
String backendType = config.getLDAPBackendType();

// Get backend-specific configuration using prefixed properties
Map<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
this.remoteBaseDn = backendConfig.get("remoteBaseDn");

// Initialize backend
backend = BackendFactory.createBackend(backendType, backendConfig);
this.interceptors = createInterceptors(config);

// Clean up previous run if it didn't shut down cleanly
File lockFile = new File(workDir, "run/instance.lock");
Expand All @@ -96,6 +88,30 @@ public void initialize(GatewayConfig config) throws Exception {
workDir.mkdirs();
}

private List<Interceptor> createInterceptors(GatewayConfig config) throws Exception {
List<String> interceptorNames = config.getLDAPInterceptorNames();
List<Interceptor> interceptors = new ArrayList<>(interceptorNames.size());
for (String interceptorName : interceptorNames) {
// Get backend-specific configuration using prefixed properties
Map<String, String> interceptorConfig = config.getLDAPInterceptorConfig(interceptorName);

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

// Add 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));
}
return interceptors;
}

/**
* Start the LDAP server
*/
Expand Down Expand Up @@ -131,26 +147,18 @@ public void start() throws Exception {
partition.setPartitionPath(new File(workDir, "proxy").toURI());
directoryService.addPartition(partition);

// Create partition for remote base DN if different from proxy base DN
// This allows backend entries with remote DNs to be returned in search results
if (remoteBaseDn != null && !remoteBaseDn.equals(baseDn)) {
JdbmPartition remotePartition = new JdbmPartition(schemaManager, directoryService.getDnFactory());
remotePartition.setId("remote");
remotePartition.setSuffixDn(new Dn(schemaManager, remoteBaseDn));
remotePartition.setPartitionPath(new File(workDir, "remote").toURI());
directoryService.addPartition(remotePartition);
}

addGroupLookupInterceptor();
baseDns = new HashSet<>();
baseDns.add(baseDn);
addInterceptors();

// 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 @@ -162,25 +170,44 @@ public void start() throws Exception {
LOG.ldapServiceStarted(port);
}

private void addGroupLookupInterceptor() {
private void addInterceptors() throws LdapException {
// Add our interceptor for group lookups and bind proxying
// We need to insert it before AuthenticationInterceptor to intercept bind requests
final List<Interceptor> interceptors = new ArrayList<>(directoryService.getInterceptors());
final List<Interceptor> dsInterceptors = new ArrayList<>(directoryService.getInterceptors());
int authIdx = -1;
for (int i = 0; i < interceptors.size(); i++) {
if (interceptors.get(i).getName().equalsIgnoreCase("authenticationInterceptor")) {
for (int i = 0; i < dsInterceptors.size(); i++) {
if (dsInterceptors.get(i).getName().equalsIgnoreCase("authenticationInterceptor")) {
authIdx = i;
break;
}
}

final GroupLookupInterceptor interceptor = new GroupLookupInterceptor(directoryService, backend);
if (authIdx != -1) {
interceptors.add(authIdx, interceptor);
} else {
interceptors.add(interceptor);
// Add our configured interceptors
SchemaManager schemaManager = directoryService.getSchemaManager();
for (Interceptor interceptor : interceptors) {
if (interceptor instanceof UserSearchInterceptor) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally am not a big fan of the instanceof operator, so this feel free to ignore this comment :)

It might be an option to store the interceptors above in a Map<String, Interceptor>, where the key is the interceptor type. If that was the case, we could simply check if the Map.Entry.key.equals("backend") (this is where my comment about creating the backend interceptor type constant would be useful).

// 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
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);
}
Comment on lines +193 to +202
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: a new private method (addRemotePartition, for instance) would increase the readability of this part of the code.

}
if (authIdx != -1) {
dsInterceptors.add(authIdx, interceptor);
} else {
dsInterceptors.add(interceptor);
}
}
directoryService.setInterceptors(interceptors);
directoryService.setInterceptors(dsInterceptors);
}

/**
Expand All @@ -192,6 +219,7 @@ public void stop() throws Exception {
if (ldapServer != null) {
try {
ldapServer.stop();
ldapServer = null;
} catch (Exception e) {
LOG.ldapServiceStopFailed(e);
}
Expand All @@ -200,6 +228,7 @@ public void stop() throws Exception {
if (directoryService != null) {
try {
directoryService.shutdown();
directoryService = null;
} catch (Exception e) {
LOG.ldapServiceStopFailed(e);
}
Expand All @@ -208,13 +237,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 @@ -47,7 +47,6 @@ public void init(GatewayConfig config, Map<String, String> options) throws Servi
// Initialize the LDAP server manager with configuration
ldapServerManager = new KnoxLDAPServerManager();
ldapServerManager.initialize(config);

} 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 @@ -41,9 +41,12 @@
public class FileBackend implements LdapBackend {
private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class);

static final String TYPE = "file";

private Map<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 @@ -58,16 +61,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