You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by sm...@apache.org on 2026/08/31 11:38:22 UTC
(knox) branch v3.0.0 updated (e8bd7864d -> 46eae622b)
This is an automated email from the ASF dual-hosted git repository.
smolnar82 pushed a change to branch v3.0.0
in repository https://gitbox.apache.org/repos/asf/knox.git
from e8bd7864d KNOX-3420 - Upgrade Jetty from 9.x 12.x (#1360)
new 232bc2566 KNOX-3406: LDAP Proxy backend improve error handling (#1355)
new 46eae622b KNOX-3422: Ldap Proxy used in-memory bind user (#1357)
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.github/workflows/build/gateway-site.xml | 4 +
.github/workflows/build/gateway.sh | 5 ++
.../compose/single-eku-no-mtls/gateway-site.xml | 4 +
.../workflows/compose/single-eku/gateway-site.xml | 4 +
.../workflows/tests/test_knox_ldap_proxy_search.py | 23 +++++-
gateway-server/pom.xml | 5 ++
.../services/ldap/KnoxLDAPServerManager.java | 59 +++++++++-----
.../knox/gateway/services/ldap/LdapMessages.java | 4 +-
.../ldap/authn/InMemoryBindAuthenticator.java | 63 +++++++++++++++
.../services/ldap/backend/LdapProxyBackend.java | 36 ++++++---
.../services/ldap/KnoxLDAPServerManagerTest.java | 69 +++++++++++-----
.../ldap/authn/InMemoryBindAuthenticatorTest.java | 92 ++++++++++++++++++++++
.../ldap/backend/LdapProxyBackendTest.java | 67 ++++++++++++++++
knox-site/docs/service_ldap_server.md | 16 +++-
pom.xml | 5 ++
15 files changed, 398 insertions(+), 58 deletions(-)
create mode 100644 gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/authn/InMemoryBindAuthenticator.java
create mode 100644 gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/authn/InMemoryBindAuthenticatorTest.java
(knox) 01/02: KNOX-3406: LDAP Proxy backend improve error handling (#1355)
Posted by sm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
smolnar82 pushed a commit to branch v3.0.0
in repository https://gitbox.apache.org/repos/asf/knox.git
commit 232bc25664956bbf92b6a22f352e4a6c1cb02407
Author: David Han <ha...@users.noreply.github.com>
AuthorDate: Fri Aug 28 02:43:11 2026 -0500
KNOX-3406: LDAP Proxy backend improve error handling (#1355)
* KNOX-3406: LDAP Proxy backend improve error handling
The LdapProxyBackend.getUser code was modified to ensure that the search
cursor is closed before attempting to augment the entry with group memberships
to avoid having simultaneously open cursors.
The error handling in performPagedSearch was improved such that unsuccessful
paging calls will result in exceptions for the caller to handle.
* add null-handling for SearchResultDone
---
.../workflows/tests/test_knox_ldap_proxy_search.py | 2 +-
.../knox/gateway/services/ldap/LdapMessages.java | 4 +-
.../services/ldap/backend/LdapProxyBackend.java | 36 ++++++++----
.../ldap/backend/LdapProxyBackendTest.java | 67 ++++++++++++++++++++++
4 files changed, 95 insertions(+), 14 deletions(-)
diff --git a/.github/workflows/tests/test_knox_ldap_proxy_search.py b/.github/workflows/tests/test_knox_ldap_proxy_search.py
index 064d42d3b..2baf65081 100644
--- a/.github/workflows/tests/test_knox_ldap_proxy_search.py
+++ b/.github/workflows/tests/test_knox_ldap_proxy_search.py
@@ -68,7 +68,7 @@ class TestKnoxLdapProxySearch(unittest.TestCase):
knox_host(), port=KNOX_LDAP_PORT, use_ssl=True, tls=tls, get_info=ldap3.NONE
)
self.connection = ldap3.Connection(
- server, user=BIND_DN, password=BIND_PASSWORD, auto_bind=True
+ server, user=BIND_DN, password=BIND_PASSWORD, auto_bind=True, raise_exceptions=True
)
def tearDown(self) -> None:
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
index 8683f4645..c27056800 100644
--- 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
@@ -114,8 +114,8 @@ public interface LdapMessages {
void ldapPagedSearchExceededMaxResultSetSize(int resultSetSize, int maxResultSetSize);
@Message(level = MessageLevel.DEBUG,
- text = "LDAP Paged Search Completed: {0} | {1}")
- void ldapPagedSearchCompleted(String baseDn, String filter);
+ text = "LDAP Paged Search Completed: {0} | {1}. Found {2} entries")
+ void ldapPagedSearchCompleted(String baseDn, String filter, int numResults);
@Message(level = MessageLevel.ERROR,
text = "LDAP Search failed: {0} | {1}, {2}")
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
index 536cad7a8..862cb2f62 100644
--- 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
@@ -28,7 +28,10 @@ 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.entry.Value;
import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.exception.LdapOperationException;
+import org.apache.directory.api.ldap.model.message.LdapResult;
import org.apache.directory.api.ldap.model.message.Response;
+import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
import org.apache.directory.api.ldap.model.message.SearchRequest;
import org.apache.directory.api.ldap.model.message.SearchRequestImpl;
import org.apache.directory.api.ldap.model.message.SearchResultDone;
@@ -489,14 +492,16 @@ public class LdapProxyBackend implements LdapBackend {
connection = getConnection();
// Search for user using configurable attribute
String filter = userSearchFilter.replace("{username}", username);
+ Entry sourceEntry = null;
try (EntryCursor cursor = connection.search(remoteUserSearchBase, filter, SearchScope.SUBTREE, "*")) {
if (cursor.next()) {
- Entry sourceEntry = cursor.get();
- addGroupMemberships(sourceEntry, connection, createEntryCache(), createResolvedParentsCache());
- return remoteSchemaConverter.convertRemoteEntryToProxyEntry(sourceEntry, schemaManager);
-
+ sourceEntry = cursor.get();
}
}
+ if (sourceEntry != null) {
+ addGroupMemberships(sourceEntry, connection, createEntryCache(), createResolvedParentsCache());
+ return remoteSchemaConverter.convertRemoteEntryToProxyEntry(sourceEntry, schemaManager);
+ }
return null;
} finally {
releaseConnection(connection);
@@ -563,7 +568,7 @@ public class LdapProxyBackend implements LdapBackend {
}
}
- private void addGroupMemberships(Entry entry, LdapConnection connection, Map<String, Entry> entryCache, Map<String, Set<String>> resolvedParentsCache) throws Exception {
+ private void addGroupMemberships(Entry entry, LdapConnection connection, Map<String, Entry> entryCache, Map<String, Set<String>> resolvedParentsCache) throws LdapException, CursorException, IOException {
// The memberOf attribute is already populated on the entry. Further work is only needed
// when using recursive group resolution or non using memberOf to find groups
if (recursiveGroupResolution || !useMemberOf) {
@@ -574,7 +579,7 @@ public class LdapProxyBackend implements LdapBackend {
}
}
- private List<Entry> getUserGroupsEntries(LdapConnection connection, Entry user, Map<String, Entry> entryCache, Map<String, Set<String>> resolvedParentsCache) throws Exception {
+ private List<Entry> getUserGroupsEntries(LdapConnection connection, Entry user, Map<String, Entry> entryCache, Map<String, Set<String>> resolvedParentsCache) throws LdapException, CursorException, IOException {
List<Entry> groups = new ArrayList<>();
if (useMemberOf) {
// Use memberOf attribute for efficient AD lookups
@@ -893,12 +898,21 @@ public class LdapProxyBackend implements LdapBackend {
}
if (cursor.isDone()) {
SearchResultDone done = cursor.getSearchResultDone();
- PagedResults responseControl = (PagedResults) done.getControl(PagedResults.OID);
- if (responseControl != null) {
- cookie = responseControl.getCookie();
+ if (done != null) {
+ LdapResult ldapResult = done.getLdapResult();
+ if (ldapResult.getResultCode() != ResultCodeEnum.SUCCESS) {
+ throw new LdapOperationException(ldapResult.getResultCode(), ldapResult.getDiagnosticMessage());
+ }
+ PagedResults responseControl = (PagedResults) done.getControl(PagedResults.OID);
+
+ if (responseControl != null) {
+ cookie = responseControl.getCookie();
+ } else {
+ cookie = null;
+ }
} else {
- cookie = null;
+ throw new LdapException("The LDAP search operation failed to complete fully.");
}
}
pageNumber++;
@@ -909,7 +923,7 @@ public class LdapProxyBackend implements LdapBackend {
if (maxResultSetSize != 0 && results.size() >= maxResultSetSize) {
LOG.ldapPagedSearchExceededMaxResultSetSize(results.size(), maxResultSetSize);
} else {
- LOG.ldapPagedSearchCompleted(baseDn, filter);
+ LOG.ldapPagedSearchCompleted(baseDn, filter, results.size());
}
return results;
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendTest.java
index 981c962f3..c38c8e770 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/backend/LdapProxyBackendTest.java
@@ -17,19 +17,31 @@
*/
package org.apache.knox.gateway.services.ldap.backend;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import org.apache.directory.api.ldap.model.cursor.SearchCursor;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.entry.Value;
+import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.exception.LdapOperationException;
import org.apache.directory.api.ldap.model.message.BindRequest;
+import org.apache.directory.api.ldap.model.message.LdapResult;
+import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
import org.apache.directory.api.ldap.model.message.SearchRequest;
+import org.apache.directory.api.ldap.model.message.SearchResultDone;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
+import org.apache.directory.ldap.client.api.LdapConnection;
import org.apache.directory.server.core.api.CoreSession;
import org.apache.directory.server.core.api.DirectoryService;
import org.apache.directory.server.core.api.InstanceLayout;
@@ -828,6 +840,61 @@ public class LdapProxyBackendTest {
assertFalse(ldapProxyBackend.isSupportedSearchBase("dc=other,dc=base,dc=org"));
}
+ @Test
+ public void testPerformPagedSearchThrowsOnFailure() throws Exception{
+ Map<String, String> config = new HashMap<>(ldapBackendConfig);
+ config.put("pageSize", Integer.toString(PAGE_SIZE));
+ ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+
+ // Mock out unsuccessful paging result
+ LdapResult mockResult = createMock(LdapResult.class);
+ expect(mockResult.getResultCode()).andReturn(ResultCodeEnum.UNWILLING_TO_PERFORM).atLeastOnce();
+ expect(mockResult.getDiagnosticMessage()).andReturn("test error message");
+ SearchResultDone mockDone = createMock(SearchResultDone.class);
+ expect(mockDone.getLdapResult()).andReturn(mockResult);
+ SearchCursor mockCursor = createMock(SearchCursor.class);
+ expect(mockCursor.next()).andReturn(false);
+ expect(mockCursor.isDone()).andReturn(true);
+ expect(mockCursor.getSearchResultDone()).andReturn(mockDone);
+ LdapConnection mockConnection = createMock(LdapConnection.class);
+ expect(mockConnection.search(anyObject(SearchRequest.class))).andReturn(mockCursor);
+ replay(mockResult,
+ mockDone,
+ mockCursor,
+ mockConnection);
+
+ LdapOperationException exception = assertThrows(
+ LdapOperationException.class,
+ () -> ldapProxyBackend.performPagedSearch(mockConnection, "ou=people,dc=proxy,dc=org", "(objectclass=inetOrgPerson)", SearchScope.SUBTREE, "*"));
+ assertEquals(ResultCodeEnum.UNWILLING_TO_PERFORM, exception.getResultCode());
+ assertEquals("test error message", exception.getMessage());
+ }
+
+ @Test
+ public void testPerformPagedSearchThrowsOnNullSearchResultDone() throws Exception{
+ Map<String, String> config = new HashMap<>(ldapBackendConfig);
+ config.put("pageSize", Integer.toString(PAGE_SIZE));
+ ldapProxyBackend = new LdapProxyBackend("testbackend", config);
+
+ // Mock out unsuccessful paging result
+ LdapResult mockResult = createMock(LdapResult.class);
+ expect(mockResult.getResultCode()).andReturn(ResultCodeEnum.UNWILLING_TO_PERFORM).atLeastOnce();
+ expect(mockResult.getDiagnosticMessage()).andReturn("test error message");
+ SearchCursor mockCursor = createMock(SearchCursor.class);
+ expect(mockCursor.next()).andReturn(false);
+ expect(mockCursor.isDone()).andReturn(true);
+ expect(mockCursor.getSearchResultDone()).andReturn(null);
+ LdapConnection mockConnection = createMock(LdapConnection.class);
+ expect(mockConnection.search(anyObject(SearchRequest.class))).andReturn(mockCursor);
+ replay(mockResult,
+ mockCursor,
+ mockConnection);
+
+ LdapException exception = assertThrows(
+ LdapException.class,
+ () -> ldapProxyBackend.performPagedSearch(mockConnection, "ou=people,dc=proxy,dc=org", "(objectclass=inetOrgPerson)", SearchScope.SUBTREE, "*"));
+ assertEquals("The LDAP search operation failed to complete fully.", exception.getMessage());
+ }
// Helper methods for refactoring
private Map<String, String> createConfigWithUserAttr(String attr) {
(knox) 02/02: KNOX-3422: Ldap Proxy used in-memory bind user (#1357)
Posted by sm...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
smolnar82 pushed a commit to branch v3.0.0
in repository https://gitbox.apache.org/repos/asf/knox.git
commit 46eae622bd98861f904bb66e03446f5a2173fc14
Author: David Han <ha...@users.noreply.github.com>
AuthorDate: Fri Aug 28 02:43:46 2026 -0500
KNOX-3422: Ldap Proxy used in-memory bind user (#1357)
* KNOX-3422: Ldap Proxy used in-memory bind user
The bind user created from the gateway configuration is now created
in-memory instead of being added to the embedded ldap server. This
provides the benefit of being able to rotate the user just by changing
the configuration. Previously, since the configured user is not tracked,
there wasn't a way to automatically rotate the user.
* throw LdapAuthenticationException
---
.github/workflows/build/gateway-site.xml | 4 +
.github/workflows/build/gateway.sh | 5 ++
.../compose/single-eku-no-mtls/gateway-site.xml | 4 +
.../workflows/compose/single-eku/gateway-site.xml | 4 +
.../workflows/tests/test_knox_ldap_proxy_search.py | 21 ++++-
gateway-server/pom.xml | 5 ++
.../services/ldap/KnoxLDAPServerManager.java | 59 +++++++++-----
.../ldap/authn/InMemoryBindAuthenticator.java | 63 +++++++++++++++
.../services/ldap/KnoxLDAPServerManagerTest.java | 69 +++++++++++-----
.../ldap/authn/InMemoryBindAuthenticatorTest.java | 92 ++++++++++++++++++++++
knox-site/docs/service_ldap_server.md | 16 +++-
pom.xml | 5 ++
12 files changed, 303 insertions(+), 44 deletions(-)
diff --git a/.github/workflows/build/gateway-site.xml b/.github/workflows/build/gateway-site.xml
index 002fadea5..be1c264b9 100644
--- a/.github/workflows/build/gateway-site.xml
+++ b/.github/workflows/build/gateway-site.xml
@@ -145,6 +145,10 @@ limitations under the License.
<name>gateway.ldap.base.dn</name>
<value>dc=proxy,dc=org</value>
</property>
+ <property>
+ <name>gateway.ldap.bind.user</name>
+ <value>uid=bind,ou=people,dc=proxy,dc=org</value>
+ </property>
<property>
<name>gateway.ldap.max.size.limit</name>
<value>1000</value>
diff --git a/.github/workflows/build/gateway.sh b/.github/workflows/build/gateway.sh
index e4927b7b2..d43c54baa 100755
--- a/.github/workflows/build/gateway.sh
+++ b/.github/workflows/build/gateway.sh
@@ -43,6 +43,11 @@ keytool -importcert -noprompt -alias knox-ldaps \
-file /tmp/ldaps-cert.pem
rm -f /tmp/ldaps-cert.pem
+# 4) Store a password for the LDAP Proxy bind user
+LDAP_BIND_PASSWORD=bind-password
+LDAP_BIND_PASSWORD_ALIAS=gateway_ldap_bind_password
+/knox-runtime/bin/knoxcli.sh create-alias "$LDAP_BIND_PASSWORD_ALIAS" --value "$LDAP_BIND_PASSWORD"
+
# Start Knox. Endpoint (hostname) identification is disabled for the embedded LDAPS
# connection - the dev cert is self-signed - but trust is still enforced via cacerts.
java -Dcom.sun.jndi.ldap.object.disableEndpointIdentification=true \
diff --git a/.github/workflows/compose/single-eku-no-mtls/gateway-site.xml b/.github/workflows/compose/single-eku-no-mtls/gateway-site.xml
index 791a3b2a5..56ba7b0e3 100644
--- a/.github/workflows/compose/single-eku-no-mtls/gateway-site.xml
+++ b/.github/workflows/compose/single-eku-no-mtls/gateway-site.xml
@@ -157,6 +157,10 @@ limitations under the License.
<name>gateway.ldap.base.dn</name>
<value>dc=proxy,dc=org</value>
</property>
+ <property>
+ <name>gateway.ldap.bind.user</name>
+ <value>uid=bind,ou=people,dc=proxy,dc=org</value>
+ </property>
<property>
<name>gateway.ldap.max.size.limit</name>
<value>1000</value>
diff --git a/.github/workflows/compose/single-eku/gateway-site.xml b/.github/workflows/compose/single-eku/gateway-site.xml
index 76f488b85..bd4e98963 100644
--- a/.github/workflows/compose/single-eku/gateway-site.xml
+++ b/.github/workflows/compose/single-eku/gateway-site.xml
@@ -211,6 +211,10 @@ limitations under the License.
<name>gateway.ldap.base.dn</name>
<value>dc=proxy,dc=org</value>
</property>
+ <property>
+ <name>gateway.ldap.bind.user</name>
+ <value>uid=bind,ou=people,dc=proxy,dc=org</value>
+ </property>
<property>
<name>gateway.ldap.max.size.limit</name>
<value>1000</value>
diff --git a/.github/workflows/tests/test_knox_ldap_proxy_search.py b/.github/workflows/tests/test_knox_ldap_proxy_search.py
index 2baf65081..8f05705b9 100644
--- a/.github/workflows/tests/test_knox_ldap_proxy_search.py
+++ b/.github/workflows/tests/test_knox_ldap_proxy_search.py
@@ -48,8 +48,8 @@ PROXY_PEOPLE_BASE = f"ou=people,{PROXY_BASE_DN}"
PROXY_GROUPS_BASE = f"ou=groups,{PROXY_BASE_DN}"
# A valid backend user used to bind to the proxy before searching.
-BIND_DN = f"uid=guest,{PEOPLE_BASE}"
-BIND_PASSWORD = "guest-password"
+BIND_DN = f"uid=bind,{PROXY_PEOPLE_BASE}"
+BIND_PASSWORD = "bind-password"
def knox_host() -> str:
@@ -84,6 +84,23 @@ class TestKnoxLdapProxySearch(unittest.TestCase):
values.append(first_rdn.split("=", 1)[1])
return values
+ def test_anonymous_search_rejected(self) -> None:
+ """Anonymous search not allowed when bind user configured."""
+ # Self-signed dev certificate in CI: connect over TLS but skip validation.
+ tls = ldap3.Tls(validate=ssl.CERT_NONE)
+ server = ldap3.Server(
+ knox_host(), port=KNOX_LDAP_PORT, use_ssl=True, tls=tls, get_info=ldap3.NONE
+ )
+ with self.assertRaises(ldap3.core.exceptions.LDAPInvalidCredentialsResult):
+ try:
+ connection = ldap3.Connection(
+ server, auto_bind=ldap3.AUTO_BIND_NONE, raise_exceptions=True
+ )
+ connection.bind()
+ finally:
+ if connection.bound:
+ connection.unbind()
+
def test_search_all_users_by_objectclass(self) -> None:
"""All inetOrgPerson entries under ou=people are returned."""
users = self.rdn_values(PEOPLE_BASE, "(objectClass=inetOrgPerson)")
diff --git a/gateway-server/pom.xml b/gateway-server/pom.xml
index a7485ab7b..70fe0d244 100644
--- a/gateway-server/pom.xml
+++ b/gateway-server/pom.xml
@@ -511,6 +511,11 @@
<artifactId>apacheds-ldif-partition</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.directory.server</groupId>
+ <artifactId>apacheds-interceptors-authn</artifactId>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-jdbm-partition</artifactId>
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
index e4445276e..a37be23ce 100644
--- 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
@@ -37,8 +37,11 @@ import org.apache.directory.server.core.DefaultDirectoryService;
import org.apache.directory.server.core.api.DirectoryService;
import org.apache.directory.server.core.api.DnFactory;
import org.apache.directory.server.core.api.InstanceLayout;
+import org.apache.directory.server.core.api.InterceptorEnum;
import org.apache.directory.server.core.api.interceptor.Interceptor;
import org.apache.directory.server.core.api.schema.SchemaPartition;
+import org.apache.directory.server.core.authn.AuthenticationInterceptor;
+import org.apache.directory.server.core.authn.Authenticator;
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;
@@ -46,6 +49,7 @@ 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.GatewayServices;
+import org.apache.knox.gateway.services.ldap.authn.InMemoryBindAuthenticator;
import org.apache.knox.gateway.services.ldap.control.RolesLookupBypassControlFactory;
import org.apache.knox.gateway.services.ldap.interceptor.InterceptorFactory;
import org.apache.knox.gateway.services.ldap.interceptor.LDAPRolesLookupInterceptor;
@@ -56,6 +60,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -116,6 +121,7 @@ public class KnoxLDAPServerManager {
this.port = config.getLDAPPort();
this.baseDn = config.getLDAPBaseDN();
this.bindUser = config.getLDAPBindUser();
+ validateBindUser();
maxSizeLimit = config.getLDAPMaxSizeLimit();
maxTimeLimit = config.getLDAPMaxTimeLimit();
@@ -145,6 +151,19 @@ public class KnoxLDAPServerManager {
workDir.mkdirs();
}
+ /**
+ * Validates that the bind dn, if configured, matches the base dn for the embedded
+ * LDAP service (e.g. {@code ou=system} or {@code ou=people,<baseDn>}).
+ */
+ private void validateBindUser() throws Exception {
+ if (!StringUtils.isBlank(bindUser)) {
+ Dn bindUserDn = new Dn(bindUser);
+ if (!(bindUserDn.isDescendantOf("ou=system") || bindUserDn.isDescendantOf("ou=people," + this.baseDn))) {
+ throw new IllegalArgumentException("Bind user must be a descendant of ou=system or ou=people," + this.baseDn);
+ }
+ }
+ }
+
private void createInterceptors(GatewayConfig config) throws Exception {
List<String> interceptorNames = config.getLDAPInterceptorNames();
List<Interceptor> interceptors = new ArrayList<>(interceptorNames.size());
@@ -233,6 +252,10 @@ public class KnoxLDAPServerManager {
final String bindPassword = bindPasswordChars == null ? null : new String(bindPasswordChars);
final boolean requireBind = StringUtils.isNotBlank(bindUser) && StringUtils.isNotBlank(bindPassword);
directoryService.setAllowAnonymousAccess(!requireBind);
+ if (requireBind) {
+ configureInMemoryBindUser(bindPassword);
+ LOG.ldapBindUserConfigured(bindUser);
+ }
// Start the service
directoryService.startup();
@@ -240,11 +263,6 @@ public class KnoxLDAPServerManager {
// Add base entries to the partitions
createBaseEntries(baseDns, schemaManager);
- if (requireBind) {
- createBindUser(schemaManager, bindPassword);
- LOG.ldapBindUserConfigured(bindUser);
- }
-
// Create LDAP server on configured port
ldapServer = new LdapServer();
final TcpTransport transport = new TcpTransport(port);
@@ -435,22 +453,23 @@ public class KnoxLDAPServerManager {
}
/**
- * Create the entry used by external clients to bind against the embedded LDAP server.
- * The bind DN's parent container (e.g. {@code ou=system} or {@code ou=people,<baseDn>})
- * must already exist. The entry is added using the privileged admin session, which is
- * unaffected by the anonymous-access setting.
+ * Configure the Authenticator used by external clients to bind against the embedded LDAP server.
*/
- private void createBindUser(SchemaManager schemaManager, String bindPassword) throws Exception {
- Dn bindDn = new Dn(schemaManager, bindUser);
- if (!directoryService.getAdminSession().exists(bindDn)) {
- String rdnValue = bindDn.getRdn().getValue();
- Entry bindEntry = new DefaultEntry(schemaManager);
- bindEntry.setDn(bindDn);
- bindEntry.add("objectClass", "top", "person", "organizationalPerson", "inetOrgPerson");
- bindEntry.add("cn", rdnValue);
- bindEntry.add("sn", rdnValue);
- bindEntry.add("userPassword", bindPassword);
- directoryService.getAdminSession().add(bindEntry);
+ private void configureInMemoryBindUser(String bindPassword) throws Exception {
+ Dn bindDn = new Dn(directoryService.getSchemaManager(), bindUser);
+
+ Interceptor interceptor = directoryService.getInterceptor(InterceptorEnum.AUTHENTICATION_INTERCEPTOR.getName());
+ if (interceptor instanceof AuthenticationInterceptor authInterceptor) {
+ InMemoryBindAuthenticator inMemoryBindAuthenticator = new InMemoryBindAuthenticator(bindDn, bindPassword);
+
+ // Create an ordered set so that in-memory authenticator will have priority over
+ // the default SIMPLE authenticator.
+ Set<Authenticator> authenticators = new LinkedHashSet<>();
+ authenticators.add(inMemoryBindAuthenticator);
+ authenticators.addAll(authInterceptor.getAuthenticators());
+ authInterceptor.setAuthenticators(authenticators);
+ } else {
+ throw new IllegalStateException("Unable to configure AuthenticationInterceptor for bind user");
}
}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/authn/InMemoryBindAuthenticator.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/authn/InMemoryBindAuthenticator.java
new file mode 100644
index 000000000..9456dbfdf
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/ldap/authn/InMemoryBindAuthenticator.java
@@ -0,0 +1,63 @@
+/*
+ * 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.authn;
+
+import org.apache.directory.api.ldap.model.constants.AuthenticationLevel;
+import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException;
+import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.server.core.api.LdapPrincipal;
+import org.apache.directory.server.core.api.interceptor.context.BindOperationContext;
+import org.apache.directory.server.core.authn.AbstractAuthenticator;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Authenticator for an in-memory user.
+ */
+public class InMemoryBindAuthenticator extends AbstractAuthenticator {
+
+ private final Dn bindDn;
+ private final String bindPassword;
+
+ public InMemoryBindAuthenticator(Dn bindDn, String bindPassword)
+ throws LdapException {
+ super(AuthenticationLevel.SIMPLE, bindDn);
+ this.bindDn = bindDn;
+ this.bindPassword = bindPassword;
+ }
+
+ @Override
+ public LdapPrincipal authenticate(BindOperationContext bindContext) throws LdapException {
+ Dn dn = bindContext.getDn();
+
+ // Check if authenticating as the configured in-memory user
+ if (bindDn.equals(dn)) {
+ byte[] passwordBytes = bindContext.getCredentials();
+ String password = new String(passwordBytes, StandardCharsets.UTF_8);
+ if (bindPassword.equals(password)) {
+ return new LdapPrincipal(getDirectoryService().getSchemaManager(), dn, AuthenticationLevel.SIMPLE);
+ } else {
+ throw new LdapAuthenticationException("Failed to authenticate: " + dn.getName());
+ }
+ }
+
+ // Pass-through to default authenticators if it's a normal directory user
+ return null;
+ }
+}
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
index a5e7df664..8d39ab10c 100644
--- 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
@@ -82,6 +82,7 @@ public class KnoxLDAPServerManagerTest {
private static final String BIND_PASSWORD = "knox-password";
private static final String KEYSTORE_PASSWORD = "keystore-password";
private static final String SSL_KEYSTORE_ALIAS = "gateway_ldap_ssl_keystore_password";
+ public static final String BASE_DN = "dc=test,dc=com";
private KnoxLDAPServerManager serverManager;
private File tempWorkDir;
@@ -133,7 +134,7 @@ public class KnoxLDAPServerManagerTest {
GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
- expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
+ expect(mockConfig.getLDAPBaseDN()).andReturn(BASE_DN).anyTimes();
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes();
expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
Map<String, String> backendConfig = createFileBackendInterceptorConfig();
@@ -143,7 +144,7 @@ public class KnoxLDAPServerManagerTest {
serverManager.initialize(mockConfig);
assertEquals("Port should be set correctly", port, serverManager.getPort());
- assertEquals("Base DN should be set correctly", "dc=test,dc=com", serverManager.getBaseDn());
+ assertEquals("Base DN should be set correctly", BASE_DN, serverManager.getBaseDn());
assertFalse("Should not be running after initialize", serverManager.isRunning());
}
@@ -152,7 +153,7 @@ public class KnoxLDAPServerManagerTest {
GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
- expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
+ expect(mockConfig.getLDAPBaseDN()).andReturn(BASE_DN).anyTimes();
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("ldapbackend")).anyTimes();
Map<String, String> backendConfig = createLdapBackendInterceptorConfig();
expect(mockConfig.getLDAPInterceptorConfig("ldapbackend")).andReturn(backendConfig).anyTimes();
@@ -161,7 +162,7 @@ public class KnoxLDAPServerManagerTest {
serverManager.initialize(mockConfig);
assertEquals("Port should be set correctly", port, serverManager.getPort());
- assertEquals("Base DN should be set correctly", "dc=test,dc=com", serverManager.getBaseDn());
+ assertEquals("Base DN should be set correctly", BASE_DN, serverManager.getBaseDn());
assertFalse("Should not be running after initialize", serverManager.isRunning());
}
@@ -183,7 +184,7 @@ public class KnoxLDAPServerManagerTest {
GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
- expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
+ expect(mockConfig.getLDAPBaseDN()).andReturn(BASE_DN).anyTimes();
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend", "ldapbackend")).anyTimes();
expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
Map<String, String> fileBackendConfig = createFileBackendInterceptorConfig();
@@ -195,10 +196,42 @@ public class KnoxLDAPServerManagerTest {
serverManager.initialize(mockConfig);
assertEquals("Port should be set correctly", port, serverManager.getPort());
- assertEquals("Base DN should be set correctly", "dc=test,dc=com", serverManager.getBaseDn());
+ assertEquals("Base DN should be set correctly", BASE_DN, serverManager.getBaseDn());
assertFalse("Should not be running after initialize", serverManager.isRunning());
}
+ @Test
+ public void testInitializeWithBindConfig() throws Exception {
+ useBindPassword(BIND_PASSWORD);
+ GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
+ expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
+ expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
+ expect(mockConfig.getLDAPBaseDN()).andReturn(BASE_DN).anyTimes();
+ expect(mockConfig.getLDAPBindUser()).andReturn(BIND_DN).anyTimes();
+ expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes();
+ expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
+ expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(createFileBackendInterceptorConfig()).anyTimes();
+ replay(mockConfig);
+
+ serverManager.initialize(mockConfig);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInitializeWithBadBindDn() throws Exception {
+ useBindPassword(BIND_PASSWORD);
+ GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
+ expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
+ expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
+ expect(mockConfig.getLDAPBaseDN()).andReturn(BASE_DN).anyTimes();
+ expect(mockConfig.getLDAPBindUser()).andReturn("uid=bad,dc=unknown,dc=com").anyTimes();
+ expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes();
+ expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
+ expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(createFileBackendInterceptorConfig()).anyTimes();
+ replay(mockConfig);
+
+ serverManager.initialize(mockConfig);
+ }
+
@Test
public void testLockFileCleanup() throws Exception {
GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
@@ -278,7 +311,7 @@ public class KnoxLDAPServerManagerTest {
GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
- expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
+ expect(mockConfig.getLDAPBaseDN()).andReturn(BASE_DN).anyTimes();
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes();
expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
Map<String, String> backendConfig = createFileBackendInterceptorConfig();
@@ -328,7 +361,7 @@ public class KnoxLDAPServerManagerTest {
GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
- expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
+ expect(mockConfig.getLDAPBaseDN()).andReturn(BASE_DN).anyTimes();
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend", "ldapbackend")).anyTimes();
expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
Map<String, String> fileBackendConfig = createFileBackendInterceptorConfig();
@@ -380,7 +413,7 @@ public class KnoxLDAPServerManagerTest {
GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
- expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
+ expect(mockConfig.getLDAPBaseDN()).andReturn(BASE_DN).anyTimes();
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("ldapbackend", "ldap backend")).anyTimes();
expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
Map<String, String> ldapBackendConfig = createLdapBackendInterceptorConfig();
@@ -418,7 +451,7 @@ public class KnoxLDAPServerManagerTest {
GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
- expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
+ expect(mockConfig.getLDAPBaseDN()).andReturn(BASE_DN).anyTimes();
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of()).anyTimes();
replay(mockConfig);
@@ -473,7 +506,7 @@ public class KnoxLDAPServerManagerTest {
GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
- expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
+ expect(mockConfig.getLDAPBaseDN()).andReturn(BASE_DN).anyTimes();
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of()).anyTimes();
expect(mockConfig.getLDAPMaxSizeLimit()).andReturn(expectedMaxSize).anyTimes();
expect(mockConfig.getLDAPMaxTimeLimit()).andReturn(expectedMaxTime).anyTimes();
@@ -508,7 +541,7 @@ public class KnoxLDAPServerManagerTest {
connection.bind(BIND_DN, BIND_PASSWORD);
assertTrue("Connection should be authenticated", connection.isAuthenticated());
// An authenticated client should be able to search.
- try (EntryCursor cursor = connection.search("dc=test,dc=com", "(objectClass=*)", SearchScope.SUBTREE)) {
+ try (EntryCursor cursor = connection.search(BASE_DN, "(objectClass=*)", SearchScope.SUBTREE)) {
assertTrue("Authenticated search should return at least one entry", cursor.next());
}
}
@@ -530,7 +563,7 @@ public class KnoxLDAPServerManagerTest {
GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
- expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
+ expect(mockConfig.getLDAPBaseDN()).andReturn(BASE_DN).anyTimes();
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes();
expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(createFileBackendInterceptorConfig()).anyTimes();
@@ -542,7 +575,7 @@ public class KnoxLDAPServerManagerTest {
// No bind credentials configured -> anonymous access remains allowed (backward compatible).
try (LdapConnection connection = new LdapNetworkConnection("localhost", port)) {
connection.bind();
- try (EntryCursor cursor = connection.search("dc=test,dc=com", "(objectClass=*)", SearchScope.SUBTREE)) {
+ try (EntryCursor cursor = connection.search(BASE_DN, "(objectClass=*)", SearchScope.SUBTREE)) {
assertTrue("Anonymous search should return at least one entry", cursor.next());
}
}
@@ -581,7 +614,7 @@ public class KnoxLDAPServerManagerTest {
try (LdapNetworkConnection connection = new LdapNetworkConnection("localhost", port)) {
connection.setTimeOut(5000);
connection.bind();
- try (EntryCursor cursor = connection.search("dc=test,dc=com", "(objectClass=*)", SearchScope.SUBTREE)) {
+ try (EntryCursor cursor = connection.search(BASE_DN, "(objectClass=*)", SearchScope.SUBTREE)) {
cursor.next();
}
fail("Plaintext access against an LDAPS-only transport should fail");
@@ -600,7 +633,7 @@ public class KnoxLDAPServerManagerTest {
GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
- expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
+ expect(mockConfig.getLDAPBaseDN()).andReturn(BASE_DN).anyTimes();
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes();
expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
expect(mockConfig.getLDAPInterceptorConfig("filebackend")).andReturn(createFileBackendInterceptorConfig()).anyTimes();
@@ -662,7 +695,7 @@ public class KnoxLDAPServerManagerTest {
GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getParent()).anyTimes();
expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
- expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
+ expect(mockConfig.getLDAPBaseDN()).andReturn(BASE_DN).anyTimes();
expect(mockConfig.getLDAPBindUser()).andReturn(BIND_DN).anyTimes();
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of("filebackend")).anyTimes();
expect(mockConfig.getLDAPBackendDataFile()).andReturn(tempLdapFile.getAbsolutePath()).anyTimes();
@@ -690,7 +723,7 @@ public class KnoxLDAPServerManagerTest {
GatewayConfig mockConfig = EasyMock.createNiceMock(GatewayConfig.class);
expect(mockConfig.getGatewayDataDir()).andReturn(tempWorkDir.getAbsolutePath()).anyTimes();
expect(mockConfig.getLDAPPort()).andReturn(port).anyTimes();
- expect(mockConfig.getLDAPBaseDN()).andReturn("dc=test,dc=com").anyTimes();
+ expect(mockConfig.getLDAPBaseDN()).andReturn(BASE_DN).anyTimes();
expect(mockConfig.getLDAPInterceptorNames()).andReturn(List.of()).anyTimes();
replay(mockConfig);
return mockConfig;
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/authn/InMemoryBindAuthenticatorTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/authn/InMemoryBindAuthenticatorTest.java
new file mode 100644
index 000000000..1d936a39e
--- /dev/null
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/ldap/authn/InMemoryBindAuthenticatorTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.authn;
+
+import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException;
+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.LdapPrincipal;
+import org.apache.directory.server.core.api.interceptor.context.BindOperationContext;
+import org.apache.knox.gateway.services.ldap.SchemaManagerFactory;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.nio.charset.StandardCharsets;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.mock;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+public class InMemoryBindAuthenticatorTest {
+ private static final String BIND_DN = "uid=bind-user,dc=test,dc=com";
+ private static final String BIND_PASSWORD = "test-password";
+
+ Dn bindDn;
+ SchemaManager schemaManager;
+ DirectoryService directoryService;
+ InMemoryBindAuthenticator authenticator;
+
+ @Before
+ public void setUp() throws Exception {
+ directoryService = new DefaultDirectoryService();
+ schemaManager = SchemaManagerFactory.createSchemaManager();
+ directoryService.setSchemaManager(schemaManager);
+
+ bindDn = new Dn(schemaManager, BIND_DN);
+ authenticator = new InMemoryBindAuthenticator(bindDn, BIND_PASSWORD);
+ authenticator.init(directoryService);
+ }
+
+ @Test
+ public void testAuthenticate() throws Exception {
+ BindOperationContext context = mock(BindOperationContext.class);
+ expect(context.getDn()).andReturn(bindDn).once();
+ expect(context.getCredentials()).andReturn(BIND_PASSWORD.getBytes(StandardCharsets.UTF_8)).once();
+ replay(context);
+
+ LdapPrincipal principal = authenticator.authenticate(context);
+ assertNotNull(principal);
+ assertEquals(bindDn, principal.getDn());
+ }
+
+ @Test
+ public void testAuthenticateBadDn() throws Exception {
+ Dn badDn = new Dn(schemaManager, "uid=bad-user,dc=test,dc=com");
+ BindOperationContext context = mock(BindOperationContext.class);
+ expect(context.getDn()).andReturn(badDn).anyTimes();
+ replay(context);
+
+ LdapPrincipal principal = authenticator.authenticate(context);
+ assertNull(principal);
+ }
+
+ @Test(expected = LdapAuthenticationException.class)
+ public void testAuthenticateBadPassword() throws Exception {
+ BindOperationContext context = mock(BindOperationContext.class);
+ expect(context.getDn()).andReturn(bindDn).once();
+ expect(context.getCredentials()).andReturn("bad-password".getBytes(StandardCharsets.UTF_8)).once();
+ replay(context);
+
+ authenticator.authenticate(context);
+ }
+}
\ No newline at end of file
diff --git a/knox-site/docs/service_ldap_server.md b/knox-site/docs/service_ldap_server.md
index 347c40aa8..1dc0ee143 100644
--- a/knox-site/docs/service_ldap_server.md
+++ b/knox-site/docs/service_ldap_server.md
@@ -39,7 +39,7 @@ The service is configured in `gateway-site.xml`.
| `gateway.ldap.enabled` | `false` | Enables or disables the embedded LDAP service. |
| `gateway.ldap.port` | `3890` | The port on which the LDAP server listens. |
| `gateway.ldap.base.dn` | `dc=proxy,dc=com` | The base DN for the LDAP server. |
-| `gateway.ldap.bind.user` | N/A | Full bind DN (e.g. `uid=knox,ou=system`) that clients must authenticate as. When set together with the `gateway_ldap_bind_password` credential store alias, anonymous access is disabled. The bind DN's parent container must already exist (`ou=system` always exists; `ou=people,{base.dn}` and `ou=groups,{base.dn}` are created automatically). |
+| `gateway.ldap.bind.user` | N/A | Full bind DN (e.g. `uid=knox,dc=proxy,dc=com`) that clients must authenticate as. When set together with the `gateway_ldap_bind_password` credential store alias, anonymous access is disabled. The bind DN's parent container must already exist (`ou=system` always exists; `ou=people,{base.dn}` and `ou=groups,{base.dn}` are created automatically). |
| `gateway.ldap.interceptor.names` | N/A | A comma separated list of interceptors to use. A separate interceptor configuration block will be used for each name. |
| `gateway.ldap.roles.lookup.strategy` | N/A | The LDAP roles lookup strategy (`file` or `rest`). |
| `gateway.ldap.roles.lookup.rest.api.endpoint` | N/A | The LDAP roles lookup REST API endpoint. |
@@ -99,11 +99,19 @@ Clients then bind with the full DN, e.g.:
ldapsearch -x -H ldap://localhost:3890 -D "uid=knox,ou=people,dc=hadoop,dc=apache,dc=org" -w knoxsecret -b "" "(uid=admin)"
```
-The bind entry is created as an `inetOrgPerson`, so either a `uid`-based RDN
-(`uid=knox,...`) or a `cn`-based RDN (`cn=bind,...`) is valid. Use a dedicated identity
-(e.g. `uid=knox`) rather than the built-in ApacheDS admin `uid=admin,ou=system`, which
+Either a `uid`-based RDN (`uid=knox,...`) or a `cn`-based RDN (`cn=bind,...`) are accepted as valid Bind DNs.
+Use a dedicated identity (e.g. `uid=knox`) rather than the built-in ApacheDS admin `uid=admin,ou=system`, which
remains available with its default credentials.
+#### Rotating bind credentials
+
+Knox keeps the bind dn and credentials in-memory for authentication. The bind credentials can be rotated by
+changing the `gateway.ldap.bind.user` property in the gateway configuration and aliased password. The bind
+user will be updated when the gateway is reloaded.
+
+NOTE: Knox 3.0.0 created the bind user in the embedded LDAP server. These users need to be manually deleted to disable
+access using the old credentials.
+
### Interceptor Types
#### Common Interceptor Properties
diff --git a/pom.xml b/pom.xml
index d7fef4dc9..79e60a8da 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2290,6 +2290,11 @@
<artifactId>apacheds-jdbm-partition</artifactId>
<version>${apacheds.directory.server.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.directory.server</groupId>
+ <artifactId>apacheds-interceptors-authn</artifactId>
+ <version>${apacheds.directory.server.version}</version>
+ </dependency>
<dependency>
<groupId>org.apache.directory.api</groupId>