Skip to content

Prevent User LDAP filter from leaking into group lookupById - #48234

Open
JoWe112 wants to merge 3 commits into
keycloak:mainfrom
JoWe112:fix/ldap-user-filter-group-lookup
Open

Prevent User LDAP filter from leaking into group lookupById#48234
JoWe112 wants to merge 3 commits into
keycloak:mainfrom
JoWe112:fix/ldap-user-filter-group-lookup

Conversation

@JoWe112

@JoWe112 JoWe112 commented Apr 19, 2026

Copy link
Copy Markdown

Closes #47891

Verified on Keycloak 26.6.0 and 26.6.1

getFilterById() in LDAPOperationManager unconditionally appended the User LDAP filter (customUserSearchFilter) to every lookupById call, including group and role lookups. This caused the Members tab in the Admin console to return empty results when a User LDAP filter was configured, because the group object does not match user-specific filter attributes.

Added a userQuery flag to LDAPQuery so that only queries created via createQueryForUserSearch carry the filter into the lookupById path. Group and role queries default to false and are no longer affected.

Query 1 - Group lookup:

LdapOperation: lookupById
 baseDN: ou=groups,ou=demo,o=org
 filter: (cn=group3)
 searchScope: 2
 returningAttrs: [cn, member]
took: 31 ms

Query 2 - Member lookup using CNs from the group's member attribute, and with User LDAP filter "(LoginDisabled=false)" added to the filter:

LdapOperation: search
 baseDn: ou=users,ou=demo,o=org
 filter: (&(LoginDisabled=false)(groupMembership=cn=group3,ou=groups,ou=demo,o=org)(objectclass=person)(objectclass=organizationalPerson)(objectclass=inetOrgperson))
 searchScope: 2
 returningAttrs: [krb5PrincipalName, pwdChangedTime, cn, sn, createTimestamp, mail, modifyTimestamp, groupMembership]
 resultSize: 2
took: 25 ms

2026-04-19 22:56:58,251 TRACE [org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore] (executor-thread-8) Found ldap object and populated with the attributes. LDAP Object: LDAP Object [ dn: cn=ogarcia,ou=users,ou=demo,o=org , uuid: ogarcia, attributes: {sn=[Garcia], cn=[ogarcia], groupMembership=[cn=group3,ou=groups,ou=demo,o=org, cn=group9,ou=groups,ou=demo,o=org], createTimestamp=[20260410061847Z], modifyTimestamp=[20260410062301Z]}, readOnly attribute names: [createtimestamp, groupmembership, modifytimestamp, sn, cn], ranges: {} ]
2026-04-19 22:56:58,252 TRACE [org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore] (executor-thread-8) Found ldap object and populated with the attributes. LDAP Object: LDAP Object [ dn: cn=wjones,ou=users,ou=demo,o=org , uuid: wjones, attributes: {sn=[Jones], cn=[wjones], groupMembership=[cn=group3,ou=groups,ou=demo,o=org, cn=group6,ou=groups,ou=demo,o=org, cn=group8,ou=groups,ou=demo,o=org], createTimestamp=[20260410061847Z], modifyTimestamp=[20260410062301Z]}, readOnly attribute names: [createtimestamp, groupmembership, modifytimestamp, sn, cn], ranges: {} ]

@JoWe112
JoWe112 requested a review from a team as a code owner April 19, 2026 22:43

@martin-kanis martin-kanis 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.

@JoWe112 Thanks for the PR. The fix looks correct but the PR is missing an integration test. Can you please add some test coverage?

@JoWe112
JoWe112 requested review from a team as code owners June 9, 2026 11:36
@JoWe112
JoWe112 force-pushed the fix/ldap-user-filter-group-lookup branch from b5a5f4e to 7e0aa98 Compare June 9, 2026 12:09
@JoWe112

JoWe112 commented Jun 9, 2026

Copy link
Copy Markdown
Author

@JoWe112 Thanks for the PR. The fix looks correct but the PR is missing an integration test. Can you please add some test coverage?

Done.

…d queries

getFilterById() in LDAPOperationManager unconditionally appended the
User LDAP filter (customUserSearchFilter) to every lookupById call,
including group and role lookups. This caused the Members tab in the
Admin console to return empty results when a User LDAP filter was
configured, because the group object does not match user-specific
filter attributes.

Added a userQuery flag to LDAPQuery so that only queries created via
createQueryForUserSearch carry the filter into the lookupById path.
Group and role queries default to false and are no longer affected.

Signed-off-by: Joakim Westlund <joakim@westlund.it>
Regression test for keycloak#47891. A custom User LDAP filter must not be applied
when a group is resolved by its id (LDAPOperationManager.lookupById) - the
path the admin console group "Members" tab relies on, and the one that
triggers when the configured UUID attribute is also a group's naming
attribute (e.g. cn in eDirectory, the setup that reported the bug).

The test configures a User LDAP filter that matches user entries but never
a group entry, resolves an existing group by its LDAP id, and asserts the
group is still found. Without the fix the leaked filter excludes the group
entry and the lookup returns null.

Closes keycloak#47891

Signed-off-by: Joakim Westlund <joakim@westlund.it>
@JoWe112
JoWe112 force-pushed the fix/ldap-user-filter-group-lookup branch from 7e0aa98 to b93376e Compare August 11, 2026 17:09
Copilot AI balanced review requested due to automatic review settings August 11, 2026 17:09

Copilot AI 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.

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Fixes an LDAP federation bug where customUserSearchFilter was incorrectly applied to non-user lookupById operations (e.g., group/role lookups), causing Admin Console group member resolution to return empty results when a user-only filter was configured.

Changes:

  • Add a userQuery flag to LDAPQuery and set it for user searches.
  • Thread the flag through LDAPIdentityStore into LDAPOperationManager.lookupById(...) so the custom user filter is only applied for user queries.
  • Add a regression test ensuring group lookup-by-id is unaffected by customUserSearchFilter.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPGroupMapperTest.java Adds regression coverage for group lookup-by-id not being filtered by the user-only custom filter.
federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java Introduces opt-in/out flag for applying customUserSearchFilter in getFilterById/lookupById.
federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java Passes query context (isUserQuery) into lookupById so only user queries apply the custom user filter.
federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LDAPQuery.java Adds userQuery flag + accessor/mutator used to control lookupById filtering behavior.
federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPUtils.java Marks user searches as userQuery=true so custom user filter continues to apply where intended.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 402 to 408
public Condition getFilterById(String id) {
return getFilterById(id, true);
}

public Condition getFilterById(String id, boolean applyCustomUserFilter) {
LDAPQueryConditionsBuilder builder = new LDAPQueryConditionsBuilder();
Condition conditionId;
Comment on lines +420 to +432
if (applyCustomUserFilter && config.getCustomUserSearchFilter() != null) {
return builder.andCondition(new Condition[]{conditionId, builder.addCustomLDAPFilter(config.getCustomUserSearchFilter())});
} else {
return conditionId;
}
}

public SearchResult lookupById(final LdapName baseDN, final String id, final Collection<String> returningAttributes) {
final String filter = getFilterById(id).toFilter();
return lookupById(baseDN, id, returningAttributes, true);
}

public SearchResult lookupById(final LdapName baseDN, final String id, final Collection<String> returningAttributes, final boolean applyCustomUserFilter) {
final String filter = getFilterById(id, applyCustomUserFilter).toFilter();
Comment on lines +86 to +88
// When true, the custom User LDAP filter from federation settings will be applied
// to lookupById queries. Should only be true for user searches, not group/role searches.
private boolean userQuery = false;
Comment on lines +94 to +101
public boolean isUserQuery() {
return userQuery;
}

public LDAPQuery setUserQuery(boolean userQuery) {
this.userQuery = userQuery;
return this;
}
return ldapGroup.getUuid();
}, String.class);

// Configure a User LDAP filter that matches user entries but never a group entry.
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();
ctx.getLdapModel().getConfig().putSingle(LDAPConstants.CUSTOM_USER_SEARCH_FILTER, "(mail=*@email.org)");
Comment on lines 136 to +139
ldapQuery.setSearchScope(config.getSearchScope());
ldapQuery.setSearchDn(config.getUsersDn());
ldapQuery.addObjectClasses(config.getUserObjectClasses());
ldapQuery.setUserQuery(true);
Copilot AI review requested due to automatic review settings August 11, 2026 17:44
@JoWe112
JoWe112 requested a review from martin-kanis August 11, 2026 17:47

Copilot AI 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.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (5)

federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LDAPQuery.java:88

  • userQuery is ambiguous: it reads like 'this query returns users', but it actually controls whether the custom user search filter is applied during lookupById. Consider renaming to something explicit (e.g., applyCustomUserSearchFilterToLookupById or applyCustomUserFilter) so callers don’t accidentally mis-set it.
    // When true, the custom User LDAP filter from federation settings will be applied
    // to lookupById queries. Should only be true for user searches, not group/role searches.
    private boolean userQuery = false;

federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/query/internal/LDAPQuery.java:101

  • userQuery is ambiguous: it reads like 'this query returns users', but it actually controls whether the custom user search filter is applied during lookupById. Consider renaming to something explicit (e.g., applyCustomUserSearchFilterToLookupById or applyCustomUserFilter) so callers don’t accidentally mis-set it.
    public boolean isUserQuery() {
        return userQuery;
    }

    public LDAPQuery setUserQuery(boolean userQuery) {
        this.userQuery = userQuery;
        return this;
    }

federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPIdentityStore.java:270

  • This change makes lookupById apply customUserSearchFilter only when identityQuery.isUserQuery() is true, but LDAPQuery.userQuery defaults to false. Any user-facing query path that triggers the UUID fast-path but does not go through LDAPUtils.createQueryForUserSearch(...) (and therefore doesn’t set userQuery=true) will now bypass the custom user search filter. If the filter is intended to restrict all user resolution (including by ID), consider defaulting the flag to 'apply' for user queries by construction (e.g., set it in all user-query factories / constructors), or replacing the boolean with a more explicit query type/intent that cannot be forgotten.
                                .lookupById(baseDN, equalCondition.getValue().toString(), identityQuery.getReturningLdapAttributes(), identityQuery.isUserQuery());

federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java:406

  • The default overloads still apply the custom user filter (true) even though this API can be used for non-user entities (as this PR highlights). To reduce the chance of future call sites reintroducing the bug, consider making the intent explicit in the API (e.g., providing a clearly named method for user lookups vs non-user lookups, or renaming applyCustomUserFilter to applyCustomUserSearchFilter and adding Javadoc stating the default overload is only appropriate for user queries).
    public Condition getFilterById(String id) {
        return getFilterById(id, true);
    }

    public Condition getFilterById(String id, boolean applyCustomUserFilter) {

federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/LDAPOperationManager.java:432

  • The default overloads still apply the custom user filter (true) even though this API can be used for non-user entities (as this PR highlights). To reduce the chance of future call sites reintroducing the bug, consider making the intent explicit in the API (e.g., providing a clearly named method for user lookups vs non-user lookups, or renaming applyCustomUserFilter to applyCustomUserSearchFilter and adding Javadoc stating the default overload is only appropriate for user queries).
        if (applyCustomUserFilter && config.getCustomUserSearchFilter() != null) {
            return builder.andCondition(new Condition[]{conditionId, builder.addCustomLDAPFilter(config.getCustomUserSearchFilter())});
        } else {
            return conditionId;
        }
    }

    public SearchResult lookupById(final LdapName baseDN, final String id, final Collection<String> returningAttributes) {
        return lookupById(baseDN, id, returningAttributes, true);
    }

    public SearchResult lookupById(final LdapName baseDN, final String id, final Collection<String> returningAttributes, final boolean applyCustomUserFilter) {
        final String filter = getFilterById(id, applyCustomUserFilter).toFilter();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

User LDAP Filter is applied to Group lookups in getFilterById(), causing Group Members tab to return empty results

4 participants