Skip to content

KNOX-3337: Add optional LDAP role lookup support#1247

Merged
smolnar82 merged 1 commit into
apache:masterfrom
smolnar82:KNOX-3337
Jun 9, 2026
Merged

KNOX-3337: Add optional LDAP role lookup support#1247
smolnar82 merged 1 commit into
apache:masterfrom
smolnar82:KNOX-3337

Conversation

@smolnar82

@smolnar82 smolnar82 commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

KNOX-3337 - Introduce LDAP Role Lookup Service

What changes were proposed in this pull request?

This PR introduces a new LDAPRolesLookupService to Apache Knox, allowing for dynamic mapping of users and groups to specific roles during the authentication process. This is particularly useful for downstream services that require fine-grained role information (e.g., platform:admin, workspace:viewer) rather than just raw LDAP groups.

The implementation has been evolved to decouple role resolution from the core LDAP service and move it into the request processing pipeline.

Key changes include:

  • New Service Architecture: Implementation of LDAPRolesLookupService with a pluggable strategy pattern.
    • Lookup Strategies:
      • FileBasedLdapRolesLookup: Maps roles based on a local JSON configuration file.
      • RestApiLdapRolesLookup: Queries an external REST API to retrieve role assignments.
  • LDAP Interceptor Integration: Introduced LDAPRolesLookupInterceptor within the embedded LDAP server (ApacheDS). This interceptor dynamically intercepts LDAP search results and replaces memberOf group DNs with resolved role DNs. This ensures that any LDAP-client-based authentication receives roles instead of raw groups.
  • Internal Auth Resource Integration: Updated AbstractAuthResource (the base for PreAuth and ExtAuthz) to utilize the LDAPRolesLookupService directly. When enabled, resolved roles are propagated in the X-Knox-Actor-Groups HTTP headers, seamlessly replacing raw groups for downstream services.
  • Configuration: Added new GatewayConfig properties to configure the lookup strategy, file paths, and API endpoints.

How was this patch tested?

The changes were verified through both existing and newly added unit tests:

  • Unit Tests for Lookup Logic:
    • FileBasedLdapRolesLookupTest: Verified JSON mapping logic for users and groups.
    • RestApiLdapRolesLookupTest: Verified REST API integration using mocked HTTP clients.
  • Interceptor and Service Tests:
    • LDAPRolesLookupInterceptorTest: Verified the logic of replacing LDAP attributes within the ApacheDS interceptor.
    • KnoxLDAPServiceTest: Updated to ensure getUserGroups correctly returns raw groups while the interceptor handles role replacement for LDAP searches.
  • Authentication Resource Tests:
    • PreAuthResourceTest & ExtAuthzResourceTest: Added testPopulatingGroupsWithRoles to verify that X-Knox-Actor-Groups headers are correctly populated with resolved roles.

Integration Tests

No automated test added this time, but I configured Knox with MockServer as role lookup provider

$ curl -X PUT "http://localhost:55000/mockserver/expectation" \
-H "Content-Type: application/json" \
-d '{
  "httpRequest": {
    "method": "POST",
    "path": "/auth/roles"
  },
  "httpResponse": {
    "statusCode": 200,
    "headers": {
      "Content-Type": ["application/json"]
    },
    "body": {
      "user_id": "alice",
      "roles": [
        {
          "scope": "platform",
          "name": "awc-admin"
        },
        {
          "scope": "ml-workspace-abc",
          "name": "viewer"
        }
      ]
    }
  }
}'

and set the following configs in gateway-reloadable.xml:

<configuration>
    <property>
        <name>gateway.ldap.roles.lookup.strategy</name>
        <value>rest</value>
    </property>
    <property>
        <name>gateway.ldap.roles.lookup.rest.api.endpoint</name>
        <value>http://localhost:55000/auth/roles</value>
    </property>
</configuration>

Verified that the LDAP roles lookup service (as well as the Knox LDAP service) was reconfigured properly:

2026-06-05 16:16:12,378  INFO  knox.gateway (GatewayServer.java:refreshGatewayConfig(279)) - Refreshed gateway config
2026-06-05 16:16:12,380  INFO  services.ldap (DefaultLDAPRolesLookupService.java:onGatewayConfigChanged(66)) - Reloading LDAP roles lookup configuration...
2026-06-05 16:16:12,390  INFO  services.ldap (DefaultLDAPRolesLookupService.java:logStatus(48)) - LDAP roles lookup is enabled with strategy: rest
2026-06-05 16:16:12,390  INFO  services.ldap (KnoxLDAPService.java:onGatewayConfigChanged(96)) - Reloading LDAP configuration
2026-06-05 16:16:12,390  INFO  services.ldap (KnoxLDAPServerManager.java:stop(201)) - Stopping LDAP service on port 33,390
2026-06-05 16:16:12,392  INFO  services.ldap (KnoxLDAPServerManager.java:stop(219)) - LDAP service stopped successfully
2026-06-05 16:16:12,393  INFO  services.ldap (BackendFactory.java:createBackend(39)) - Loading backend: ldap (via ServiceLoader)
2026-06-05 16:16:12,393  INFO  services.ldap (LdapProxyBackend.java:initialize(148)) - Loading backend: ldap (via Proxying dc=hadoop,dc=apache,dc=org to ldap://localhost:33389 (dc=hadoop,dc=apache,dc=org) with uid attribute using group searches with recursive group resolution (max depth: 3))
2026-06-05 16:16:12,394  INFO  services.ldap (LdapProxyBackend.java:initializeConnectionPool(189)) - Loading backend: ldap (via Initialized connection pool with maxActive=8)
2026-06-05 16:16:12,394  INFO  services.ldap (KnoxLDAPServerManager.java:start(107)) - Starting LDAP service on port 33,390 with base DN: dc=hadoop,dc=apache,dc=org
2026-06-05 16:16:12,473  INFO  services.ldap (KnoxLDAPServerManager.java:start(166)) - LDAP service started successfully on port 33,390

Then issued the following curl command:

$ curl -iku admin:admin-password http://localhost:8443/gateway/sandbox/auth/api/v1/pre
HTTP/1.1 200 OK
Date: Fri, 05 Jun 2026 14:19:52 GMT
Set-Cookie: KNOXSESSIONID=node014m3tqgshbkoz1l1pl3j90w7ax2.node0; Path=/gateway/sandbox; Secure; HttpOnly
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: rememberMe=deleteMe; Path=/gateway/sandbox; Max-Age=0; Expires=Thu, 04-Jun-2026 14:19:52 GMT; SameSite=lax
X-Knox-Actor-ID: admin
X-Knox-Actor-Groups-1: platform:awc-admin,ml-workspace-abc:viewer
Content-Length: 0

Checked the logs:

2026-06-05 16:19:52,958 cc8aa344-6a18-4da5-b891-ab7f96a567d1 DEBUG knox.gateway (GatewayFilter.java:doFilter(130)) - Received request: GET /auth/api/v1/pre
2026-06-05 16:19:52,960 cc8aa344-6a18-4da5-b891-ab7f96a567d1 INFO  knox.gateway (KnoxLdapRealm.java:getUserDn(688)) - Computed userDn: uid=admin,ou=people,dc=hadoop,dc=apache,dc=org using dnTemplate for principal: admin
2026-06-05 16:19:52,962  DEBUG services.ldap (GroupLookupInterceptor.java:bind(144)) - LDAP Bind: uid=admin,ou=people,dc=hadoop,dc=apache,dc=org
2026-06-05 16:19:52,964  DEBUG services.ldap (LdapProxyBackend.java:authenticate(278)) - LDAP authentication succeeded for user: uid=admin,ou=people,dc=hadoop,dc=apache,dc=org
2026-06-05 16:19:52,965 cc8aa344-6a18-4da5-b891-ab7f96a567d1 INFO  knox.gateway (HadoopGroupProviderFilter.java:hadoopGroups(135)) - Using Knox LDAP service to fetch groups...
2026-06-05 16:19:52,968 cc8aa344-6a18-4da5-b891-ab7f96a567d1 DEBUG services.ldap (LdapProxyBackend.java:resolveGroupsRecursive(400)) - Recursive group search enabled: true, max depth: 3
2026-06-05 16:19:52,968 cc8aa344-6a18-4da5-b891-ab7f96a567d1 DEBUG services.ldap (LdapProxyBackend.java:logRecursiveSearchProgress(515)) - Recursive group search for user admin found 1 group(s) (admin) at depth 0
2026-06-05 16:19:52,969 cc8aa344-6a18-4da5-b891-ab7f96a567d1 DEBUG services.ldap (LdapProxyBackend.java:logRecursiveSearchProgress(515)) - Recursive group search for user admin found 0 group(s) () at depth 1
2026-06-05 16:19:52,969 cc8aa344-6a18-4da5-b891-ab7f96a567d1 DEBUG services.ldap (LdapProxyBackend.java:resolveGroupsRecursive(462)) - Recursive group search for user admin completed. Total groups found: 1
2026-06-05 16:19:52,969 cc8aa344-6a18-4da5-b891-ab7f96a567d1 DEBUG knox.gateway (HadoopGroupProviderFilter.java:mapGroupPrincipals(115)) - Found groups for principal admin : [admin]
2026-06-05 16:19:52,969 cc8aa344-6a18-4da5-b891-ab7f96a567d1 DEBUG knox.gateway (VirtualGroupMapper.java:mapGroups(59)) - User admin (with group(s) [admin]) added to group(s) []

Tested ldapsearch too:

$ ldapsearch -x -H ldap://localhost:33390 -D "uid=recursiveUser,ou=people,dc=hadoop,dc=apache,dc=org" -w recursiveUser-password -b "dc=hadoop,dc=apache,dc=org" "(uid=recursiveUser)" cn mail memberOf
# extended LDIF
#
# LDAPv3
# base <dc=hadoop,dc=apache,dc=org> with scope subtree
# filter: (uid=recursiveUser)
# requesting: cn mail memberOf 
#

# recursiveUser, people, hadoop.apache.org
dn: uid=recursiveUser,ou=people,dc=hadoop,dc=apache,dc=org
cn: Recursive
memberOf: cn=platform:awc-admin,ou=groups,dc=hadoop,dc=apache,dc=org
memberOf: cn=ml-workspace-abc:viewer,ou=groups,dc=hadoop,dc=apache,dc=org

As you can see, the cn attribute was changed with the resolved roles.

Logs:


2026-06-05 16:21:20,959  DEBUG services.ldap (GroupLookupInterceptor.java:bind(144)) - LDAP Bind: uid=recursiveUser,ou=people,dc=hadoop,dc=apache,dc=org
2026-06-05 16:21:20,961  DEBUG services.ldap (LdapProxyBackend.java:authenticate(278)) - LDAP authentication succeeded for user: uid=recursiveUser,ou=people,dc=hadoop,dc=apache,dc=org
2026-06-05 16:21:20,962  DEBUG services.ldap (GroupLookupInterceptor.java:search(79)) - LDAP Search: dc=hadoop,dc=apache,dc=org | (|(uid=recursiveUser)(objectClass=referral))
2026-06-05 16:21:20,962  INFO  services.ldap (GroupLookupInterceptor.java:search(119)) - Loaded user from backend: recursiveUser
2026-06-05 16:21:20,965  DEBUG services.ldap (LdapProxyBackend.java:resolveGroupsRecursive(400)) - Recursive group search enabled: true, max depth: 3
2026-06-05 16:21:20,965  DEBUG services.ldap (LdapProxyBackend.java:logRecursiveSearchProgress(515)) - Recursive group search for user recursiveUser found 1 group(s) (level1) at depth 0
2026-06-05 16:21:20,965  DEBUG services.ldap (LdapProxyBackend.java:updateCache(488)) - Added parent cn=level2,ou=groups,dc=hadoop,dc=apache,dc=org to cache for group cn=level1,ou=groups,dc=hadoop,dc=apache,dc=org
2026-06-05 16:21:20,965  DEBUG services.ldap (LdapProxyBackend.java:logRecursiveSearchProgress(515)) - Recursive group search for user recursiveUser found 1 group(s) (level2) at depth 1
2026-06-05 16:21:20,966  DEBUG services.ldap (LdapProxyBackend.java:updateCache(488)) - Added parent cn=level3,ou=groups,dc=hadoop,dc=apache,dc=org to cache for group cn=level2,ou=groups,dc=hadoop,dc=apache,dc=org
2026-06-05 16:21:20,966  DEBUG services.ldap (LdapProxyBackend.java:logRecursiveSearchProgress(515)) - Recursive group search for user recursiveUser found 1 group(s) (level3) at depth 2
2026-06-05 16:21:20,966  WARN  services.ldap (LdapProxyBackend.java:resolveGroupsRecursive(458)) - Recursive group search for user recursiveUser reached max depth 3
2026-06-05 16:21:20,966  DEBUG services.ldap (LdapProxyBackend.java:resolveGroupsRecursive(462)) - Recursive group search for user recursiveUser completed. Total groups found: 3
2026-06-05 16:21:20,966  DEBUG services.ldap (GroupLookupInterceptor.java:search(125)) - Backend user found: dn: uid=recursiveUser,ou=people,dc=hadoop,dc=apache,dc=org
2026-06-05 16:21:20,971  DEBUG services.ldap (LDAPRolesLookupInterceptor.java:modifyEntry(99)) - LDAP roles lookup for user Recursive and groups level1,level2,level3 returned roles: platform:awc-admin,ml-workspace-abc:viewer

UI changes

N/A

@smolnar82 smolnar82 self-assigned this Jun 3, 2026
@smolnar82 smolnar82 added the ldap label Jun 3, 2026
@smolnar82

Copy link
Copy Markdown
Contributor Author

Cc. @handavid

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown

Test Results

22 tests   22 ✅  2s ⏱️
 1 suites   0 💤
 1 files     0 ❌

Results for commit 1c43561.

♻️ This comment has been updated with latest results.

@smolnar82

Copy link
Copy Markdown
Contributor Author

@handavid - I implemented what we discussed offline the other day. I'd to force push my changes due to master rebase.
Please review it whenever you have time. Thanks!

final List<Entry> entries = new ArrayList<>();
try (EntryFilteringCursor cursor = next(ctx)) {
while (cursor.next()) {
entries.add(modifyEntry(cursor.get()));

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 don't think it makes sense to do this entry-by-entry. this will be very inefficient if we have an entry set where many users contain the same groups. Instead, I think this should be done in 3 steps.

  1. collect all the users and groups
  2. map the users and groups to roles. you can re-use the RoleMapping class
  3. replace the memberOf groups with roles and add any roles for the user.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you for the feedback regarding the batching strategy! I looked into implementing the 3-step approach (Collect -> Batch Map -> Merge), but I found that because our current REST API endpoint is designed for single-context lookups (one user + their associated groups), batching actually introduces more overhead rather than reducing it.

Specifically, since the API doesn't support a 'Bulk' request (e.g., fetching roles for multiple users at once), we must still make $N$ requests for $N$ users. Using a separate 3-step process actually increases the total number of HTTP round-trips.

Here is a comparison for a search result returning 100 users sharing the same 5 groups:

Metric Per-Entry Approach (Proposed) 3-Step Strategy (Batching)
Total HTTP Requests 100 105
Backend DB Lookups 600 105
Network Bottleneck 100 round-trips 105 round-trips
Implementation Simple & Stateless Complex (manual result merging)

Conclusion:
While the 3-step strategy reduces the number of lookups on the backend (database) side, it increases the number of HTTP round-trips, which is the primary latency bottleneck in this architecture. Furthermore, the REST API backend was specifically built to perform the User/Group 'OR' logic in a single query. By using the per-entry approach, we leverage the backend's design to get the final role set in exactly one trip per user.

If we ever introduce a 'Bulk' endpoint to the REST API, the 3-step strategy would absolutely be the way to go. But for the current API, the per-entry approach is both faster and simpler.

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.

thanks for looking into this and providing such a detailed analysis.

@handavid handavid left a comment

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.

lgtm

@smolnar82 smolnar82 merged commit 9c6647e into apache:master Jun 9, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants