Skip to content
Merged
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
54 changes: 54 additions & 0 deletions gateway-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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 ********** -->
<!-- ********** ********** ********** ********** ********** ********** -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -812,4 +812,17 @@ void failedToDiscoverClusterServices(String clusterName, String topologyName,

@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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
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;
Expand Down Expand Up @@ -1700,4 +1701,65 @@ public boolean isStrictTransportEnabled() {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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;
Expand Down Expand Up @@ -82,6 +83,13 @@ public void init(GatewayConfig config, Map<String,String> options) throws Servic
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}

Loading
Loading