Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion .github/workflows/build/conf/topologies/knoxldap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ limitations under the License.
</param>
<param>
<name>main.ldapRealm.contextFactory.url</name>
<value>ldap://ldap:33389</value>
<value>ldap://localhost:33390</value> <!-- Local Knox LDAP service which is configured to proxy to the demo LDAP as a backend -->
</param>
<param>
<name>main.ldapRealm.contextFactory.authenticationMechanism</name>
Expand Down
48 changes: 48 additions & 0 deletions .github/workflows/build/gateway-site.xml
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,52 @@ limitations under the License.
<value>max-age=300; includeSubDomains</value>
</property>

<!-- KnoxLDAP Service Configuration -->
<property>
<name>gateway.ldap.enabled</name>
<value>true</value>
</property>
<property>
<name>gateway.ldap.port</name>
<value>33390</value>
</property>
<property>
<name>gateway.ldap.base.dn</name>
<value>dc=hadoop,dc=apache,dc=org</value>
</property>
<property>
<name>gateway.ldap.backend.type</name>
<value>ldap</value>
</property>

<!-- LDAP Backend specific configuration (proxying to demo ldap) -->
<property>
<name>gateway.ldap.backend.ldap.url</name>
<value>ldap://ldap:33389</value>
</property>
<property>
<name>gateway.ldap.backend.ldap.remoteBaseDn</name>
<value>dc=hadoop,dc=apache,dc=org</value>
</property>
<property>
<name>gateway.ldap.backend.ldap.systemUsername</name>
<value>uid=guest,ou=people,dc=hadoop,dc=apache,dc=org</value>
</property>
<property>
<name>gateway.ldap.backend.ldap.systemPassword</name>
<value>guest-password</value>
</property>
<property>
<name>gateway.ldap.backend.ldap.userSearchBase</name>
<value>ou=people,dc=hadoop,dc=apache,dc=org</value>
</property>
<property>
<name>gateway.ldap.backend.ldap.groupSearchBase</name>
<value>ou=groups,dc=hadoop,dc=apache,dc=org</value>
</property>
<property>
<name>gateway.ldap.backend.ldap.groupMemberAttribute</name>
<value>member</value>
</property>

</configuration>
4 changes: 2 additions & 2 deletions .github/workflows/tests/test_knox_auth_service_and_LDAP.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class TestKnoxAuthService(unittest.TestCase):
def setUp(self):
self.base_url = gateway_base_url()
# The topology name is based on the filename knoxldap.xml
self.topology_url = self.base_url + "gateway/knoxldap/auth/api/v1/extauthz"
self.topology_url = self.base_url + "gateway/knoxldap/auth/api/v1/pre"

def test_auth_service_guest(self):
"""
Expand All @@ -47,7 +47,7 @@ def test_auth_service_guest(self):
self.assertEqual(response.status_code, 200)

# Check for Actor ID header
# The config in knoxtoken.xml sets 'preauth.auth.header.actor.id.name' to 'x-knox-actor-username'
# The config in knoxldap.xml sets 'preauth.auth.header.actor.id.name' to 'x-knox-actor-username'
actor_id_header = 'x-knox-actor-username'
self.assertIn(actor_id_header, response.headers)
self.assertEqual(response.headers[actor_id_header], 'guest')
Expand Down
11 changes: 6 additions & 5 deletions gateway-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,12 @@
<artifactId>api-ldap-client-api</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.apache.mina</groupId>
<artifactId>mina-core</artifactId>
</dependency>

<!-- ********** ********** ********** ********** ********** ********** -->
<!-- ********** Test Dependencies ********** -->
<!-- ********** ********** ********** ********** ********** ********** -->
Expand Down Expand Up @@ -612,10 +618,5 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.apache.mina</groupId>
<artifactId>mina-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,21 @@
*/
package org.apache.knox.gateway.services.ldap;

import org.apache.directory.api.ldap.model.constants.AuthenticationLevel;
import org.apache.directory.api.ldap.model.cursor.ListCursor;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.name.Rdn;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.server.core.api.CoreSession;
import org.apache.directory.server.core.api.DirectoryService;
import org.apache.directory.server.core.api.LdapPrincipal;
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.LookupOperationContext;
import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.services.ldap.backend.LdapBackend;
Expand All @@ -40,6 +46,7 @@
*/
public class GroupLookupInterceptor extends BaseInterceptor {
private static final LdapMessages LOG = MessagesFactory.get(LdapMessages.class);
public static final String ANONYMOUS = "anonymous";
private DirectoryService directoryService;
private LdapBackend backend;
private static final Pattern UID_PATTERN = Pattern.compile(".*\\(uid=([^)]+)\\).*");
Expand All @@ -51,6 +58,22 @@ public GroupLookupInterceptor(DirectoryService directoryService, LdapBackend bac
this.backend = backend;
}

@Override
public Entry lookup(LookupOperationContext ctx) throws LdapException {
Entry entry = next(ctx);
if (entry == null) {
String username = extractUsernameFromDn(ctx.getDn());
if (username != null) {
try {
entry = backend.getUser(username, directoryService.getSchemaManager());
} catch (Exception e) {
LOG.ldapServiceStopFailed(e);
}
}
}
return entry;
}

@Override
public EntryFilteringCursor search(SearchOperationContext ctx) throws LdapException {
String filter = ctx.getFilter() != null ? ctx.getFilter().toString() : "";
Expand Down Expand Up @@ -120,9 +143,26 @@ public EntryFilteringCursor search(SearchOperationContext ctx) throws LdapExcept
}

@Override
public void bind(BindOperationContext ctx) {
// Allow anonymous bind or simple bind
LOG.ldapBind(ctx.getDn() != null ? ctx.getDn().toString() : "anonymous");
public void bind(BindOperationContext ctx) throws LdapException {
final String dn = ctx.getDn() != null ? ctx.getDn().toString() : ANONYMOUS;
LOG.ldapBind(dn);

// Try backend first for non-system users
if (dn != null && !dn.endsWith("ou=system") && !ANONYMOUS.equals(dn)) {
byte[] credentials = ctx.getCredentials();
if (credentials != null) {
String password = new String(credentials, java.nio.charset.StandardCharsets.UTF_8);
if (backend.authenticate(dn, password)) {
// Create session for the authenticated user and set it in context
// This is required by LdapServer to avoid NullPointerException
LdapPrincipal principal = new LdapPrincipal(directoryService.getSchemaManager(), ctx.getDn(), AuthenticationLevel.SIMPLE);
CoreSession session = directoryService.getSession(principal);
ctx.setSession(session);
return; // Successfully authenticated via backend, bypass local check
}
}
}

try {
next(ctx);
} catch (Exception e) {
Expand Down Expand Up @@ -154,5 +194,19 @@ private String extractUser(String filter) {

return null;
}

private String extractUsernameFromDn(Dn dn) {
if (dn == null || dn.isEmpty()) {
return null;
}

try {
return "uid".equalsIgnoreCase(dn.getRdn().getType())
? dn.getRdn().getValue()
: null;
} catch (Exception ignored) {
return null;
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
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;
Expand All @@ -34,6 +35,8 @@
import org.apache.knox.gateway.services.ldap.backend.LdapBackend;

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

/**
Expand Down Expand Up @@ -125,8 +128,7 @@ public void start() throws Exception {
directoryService.addPartition(remotePartition);
}

// Add our interceptor for group lookups
directoryService.addLast(new GroupLookupInterceptor(directoryService, backend));
addGroupLookupInterceptor();

// Allow anonymous access
directoryService.setAllowAnonymousAccess(true);
Expand All @@ -147,6 +149,27 @@ public void start() throws Exception {
LOG.ldapServiceStarted(port);
}

private void addGroupLookupInterceptor() {
// Add our interceptor for group lookups and bind proxying
// We need to insert it before AuthenticationInterceptor to intercept bind requests
final List<Interceptor> interceptors = new ArrayList<>(directoryService.getInterceptors());
int authIdx = -1;
for (int i = 0; i < interceptors.size(); i++) {
if (interceptors.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);
}
directoryService.setInterceptors(interceptors);
}

/**
* Stop the LDAP server
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,10 @@ public interface LdapMessages {
@Message(level = MessageLevel.ERROR,
text = "Failed to copy attribute: {0}")
void ldapAttributeCopyError(@StackTrace(level = MessageLevel.DEBUG) Exception e);

@Message(level = MessageLevel.DEBUG, text = "LDAP authentication succeeded for user: {0}")
void ldapAuthSucceeded(String user);

@Message(level = MessageLevel.DEBUG, text = "LDAP authentication failed for user: {0}")
Comment thread
smolnar82 marked this conversation as resolved.
Outdated
void ldapAuthFailed(String user, @StackTrace(level = MessageLevel.INFO) Throwable cause);
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public class FileBackend implements LdapBackend {

static class UserData {
String username;
String password;
String cn;
String sn;
List<String> groups;
Expand Down Expand Up @@ -141,4 +142,25 @@ public List<Entry> searchUsers(String filter, SchemaManager schemaManager) throw

return results;
}

@Override
public boolean authenticate(String userDn, String password) {
// Extract username from DN (e.g., uid=admin,ou=people,dc=hadoop,dc=apache,dc=org)
String username = null;
if (userDn != null && userDn.startsWith("uid=")) {
Comment thread
smolnar82 marked this conversation as resolved.
Outdated
int commaIdx = userDn.indexOf(',');
if (commaIdx > 0) {
username = userDn.substring(4, commaIdx);
} else {
username = userDn.substring(4);
}
}

if (username != null) {
UserData userData = users.get(username);
return userData != null && password != null && password.equals(userData.password);
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,13 @@ public interface LdapBackend {
* @return List of matching entries
*/
List<Entry> searchUsers(String filter, SchemaManager schemaManager) throws Exception;

/**
* Authenticate a user with password
*
* @param userDn The user's Distinguished Name
* @param password The user's password
* @return true if authentication is successful, false otherwise
*/
boolean authenticate(String userDn, String password);
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
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.LdapNetworkConnection;
import org.apache.directory.ldap.client.api.ValidatingPoolableLdapConnectionFactory;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.services.ldap.LdapMessages;
Expand Down Expand Up @@ -253,6 +254,24 @@ public void close() {
}
}

@Override
public boolean authenticate(String userDn, String password) {
// Create a temporary connection for authentication (bind)
final LdapConnectionConfig authConfig = new LdapConnectionConfig();
authConfig.setLdapHost(host);
authConfig.setLdapPort(port);
authConfig.setName(userDn);
authConfig.setCredentials(password);
try (LdapConnection connection = new LdapNetworkConnection(authConfig)){
connection.bind();
LOG.ldapAuthSucceeded(userDn);
return true;
} catch (Exception e) {
LOG.ldapAuthFailed(userDn, e);
return false;
}
}

@Override
public Entry getUser(String username, SchemaManager schemaManager) throws Exception {
LdapConnection connection = null;
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,7 @@
<ignoredNonTestScopedDependency>com.nimbusds:nimbus-jose-jwt</ignoredNonTestScopedDependency>
<ignoredNonTestScopedDependency>org.apache.httpcomponents:httpcore</ignoredNonTestScopedDependency>
<ignoredNonTestScopedDependency>org.apache.knox:gateway-server</ignoredNonTestScopedDependency>
<ignoredNonTestScopedDependency>org.apache.mina:mina-core</ignoredNonTestScopedDependency>
</ignoredNonTestScopedDependencies>
<ignoredUsedUndeclaredDependencies>
<ignoredUsedUndeclaredDependency>org.glassfish.hk2.external:jakarta.inject</ignoredUsedUndeclaredDependency>
Expand Down
Loading