Skip to content

KNOX-3341: LDAP proxy general search — review fixes and hardening#1284

Open
smolnar82 wants to merge 10 commits into
apache:masterfrom
smolnar82:KNOX-3341
Open

KNOX-3341: LDAP proxy general search — review fixes and hardening#1284
smolnar82 wants to merge 10 commits into
apache:masterfrom
smolnar82:KNOX-3341

Conversation

@smolnar82

@smolnar82 smolnar82 commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

KNOX-3341 - LDAP proxy general search: review follow-ups and hardening

What changes were proposed in this pull request?

This is a follow-up to #1274 (KNOX-3341, "LDAP proxy backend handles general searches"). A post-merge review surfaced a few correctness gaps and some hardening/cleanup opportunities in the new proxy/search code.

Correctness fixes

  • FileBackend.searchUsers NPE on non-user filters. extractUser() returns null when a filter targets no recognized user identifier (e.g. (objectclass=inetOrgPerson)). Since FileBackend.search() now delegates every filter to
    searchUsers(), such general searches threw an NPE on toLowerCase(). The null is now guarded (the user-only backend matches nothing for filters it cannot satisfy).
  • Group CNs dropped in recursive memberOf resolution. getUserGroups() with useMemberOf=true and recursiveGroupResolution=true returned an incomplete list: parent groups were looked up requesting only the memberOf attribute, so the cached entries carried no cn and getCnsFromEntries silently dropped them. The lookup now requests cn as well, and getCnsFromEntries falls back to the CN in the entry's DN. Also fixes an NPE when LdapConnection.lookup() returns null (rather than throwing) for a missing entry.

Hardening

  • Don't expose secrets or corrupt non-DN values when proxying entries. convertRemoteEntryToProxyEntry copied every remote attribute verbatim (including credential-bearing ones such as userPassword) and ran the remote→proxy DN rewrite on every value, which could mangle non-DN values (mail, description) containing a base-DN substring. Sensitive attributes are now skipped, and DN rewriting is applied only to known DN-valued attributes (member, uniqueMember , memberOf, manager, owner, seeAlso).
  • Regex-safe DN conversion. convertRemoteDnToProxyDn/convertProxyDnToRemoteDn used the base DNs directly as regex patterns and the targets as replacement strings. They now use Pattern.quote/Matcher.quoteReplacement (case-insensitive, most-specific-first) so a DN with regex metacharacters can't break the conversion.
  • Only forward searches under the backend's namespace. UserSearchInterceptor.search forwarded every search to the backend, including ApacheDS internal/operational searches (ou=schema, cn=config, root-DSE). It now skips the backend call when the search base is not under the backend's base DN.

Cleanups

  • Removed a dead catch (Exception e) { throw e; } in DisabledUserInterceptor
  • dropped a stale tODO comment
  • null-checked the objectclass attribute before use
  • narrowed convertProxyFilterToRemoteFilter from throws Exception to throws ParseException
  • and switched a misleading ldapAttributeCopyError log in search() to ldapSearchFailed.

Docs

  • Fixed knox-site/docs/service_ldap_server.md: removed a duplicated proxy.poolMaxActive table row and completed the interceptorType value list (disableduserfilter, rolesLookup).

How was this patch tested?

  • Unit tests (gateway-server): the full org.apache.knox.gateway.services.ldap.** suite passes.
    Added testGetUserGroupsUseMemberOfRecursive and testGetUserGroupsUseMemberOfRecursiveDepth2 to cover the previously-untested use MemberOf + recursive getUserGroups() path, and fixed two data bugs in ldap-recursive-test.ldif (a mismatched cn and a wrong-OU member reference) that had masked the dropped-CN bug.
  • Docker integration tests: the full compose suite passes (32 passed), including the new LDAP search test, and pylint rates the test sources 10.00/10.

Integration Tests

Added .github/workflows/tests/test_knox_ldap_proxy_search.py (and ldap3 to requirements.txt).

The existing integration coverage only exercised bind and recursive group resolution (Shiro authenticates via a userDnTemplate bind, not a search) so the new LdapProxyBackend.search() path had no end-to-end coverage.

The new test binds to the embedded Knox LDAP service (which proxies to the demo LDAP backend) and searches by objectClass, cn wildcard, and uid, validating that general searches are proxied and converted correctly.

UI changes

N/A

smolnar82 and others added 10 commits June 29, 2026 15:59
extractUser() returns null when the filter targets no recognized user
identifier (uid/cn/sAMAccountName), e.g. (objectclass=inetOrgPerson).
Since FileBackend.search() now delegates every filter to searchUsers(),
such general searches threw a NullPointerException on toLowerCase().

Guard the null and return no matches, consistent with the pre-existing
behavior of this user-only backend for filters it cannot satisfy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
getUserGroups() with useMemberOf=true and recursiveGroupResolution=true
returned an incomplete group list: populateParentCacheViaMemberOf looked
up group entries requesting only the "memberOf" attribute, so the cached
entries carried no "cn", and getCnsFromEntries silently dropped them.

Request "cn" alongside "memberOf" in the lookup, and harden
getCnsFromEntries to fall back to the CN in the entry's DN so resolved
groups are never dropped when the cn attribute is absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…g entries

convertRemoteEntryToProxyEntry copied every attribute from the remote
entry verbatim, including credential-bearing attributes (e.g.
userPassword), and ran the remote->proxy DN rewrite on every value,
which could mangle non-DN values (mail, description) that happen to
contain a base DN substring.

Skip a denylist of sensitive attributes when building the proxy entry,
and only apply DN rewriting to known DN-valued attributes
(member, uniqueMember, memberOf, manager, owner, seeAlso).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
convertRemoteDnToProxyDn and convertProxyDnToRemoteDn used the base DNs
directly as regex patterns and the targets as replacement strings, so a
base DN containing regex metacharacters (or a replacement containing $
or \) would break or corrupt the conversion.

Introduce a replaceIgnoreCase helper that treats both operands as
literals via Pattern.quote / Matcher.quoteReplacement while keeping the
case-insensitive, most-specific-first replacement behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Minor correctness/readability cleanups with no behavior change:
- DisabledUserInterceptor.search: remove the dead catch(Exception){throw e;}
- LdapProxyBackend: drop stale "tODO" comment; log search failures in
  search() via ldapSearchFailed instead of the misleading
  ldapAttributeCopyError
- RemoteSchemaConverter: null-check the objectclass attribute before use,
  and narrow convertProxyFilterToRemoteFilter from throws Exception to
  throws ParseException

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UserSearchInterceptor.search forwarded every search to the backend after
the PR, including ApacheDS internal/operational searches (ou=schema,
cn=config, root-DSE) that should never be proxied to a remote LDAP
server.

Add isUnderBackendBaseDn() and skip the backend.search() call when the
search base is not a suffix of the backend's base DN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erage

Two bugs in ldap-recursive-test.ldif:
- memberOflevel2 had a mismatched cn value ("memberOflevel2level2")
- its member attribute referenced the wrong OU (recursiveGroups instead
  of recursiveMemberOfGroups), so the chain was broken

Also fix a null-return from LdapConnection.lookup() in
populateParentCacheViaMemberOf: the API returns null (not LdapException)
when an entry does not exist, which caused an NPE on the returned entry.
Fall back to a skeleton entry in both the exception and null cases.

Add testGetUserGroupsUseMemberOfRecursive and
testGetUserGroupsUseMemberOfRecursiveDepth2 to cover the
useMemberOf+recursiveGroupResolution path through getUserGroups(),
which was previously untested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ptorType values

service_ldap_server.md had a duplicated proxy.poolMaxActive table row
introduced in the PR. Also the interceptorType summary property row only
listed "backend" and "duplicateuserfilter", omitting the newly-added
"disableduserfilter" and the existing "rolesLookup" types.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The existing integration coverage only exercised bind and recursive
group resolution (Shiro authenticates via a userDnTemplate bind, not a
search), so the new LdapProxyBackend.search() path had no end-to-end
coverage.

Add test_knox_ldap_proxy_search.py: it binds to the embedded Knox LDAP
service (which proxies to the demo LDAP backend) and searches by
objectClass, cn wildcard and uid, validating that general searches are
proxied and converted correctly. Adds ldap3 to the test requirements.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@smolnar82 smolnar82 self-assigned this Jun 29, 2026
@smolnar82 smolnar82 added the ldap label Jun 29, 2026
@smolnar82

Copy link
Copy Markdown
Contributor Author

Cc. @handavid

@smolnar82 smolnar82 changed the title Knox 3341 KNOX-3341: LDAP proxy general search — review fixes and hardening Jun 29, 2026
@github-actions

Copy link
Copy Markdown

Test Results

32 tests   32 ✅  3s ⏱️
 1 suites   0 💤
 1 files     0 ❌

Results for commit aa086a4.

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.

1 participant