Prevent User LDAP filter from leaking into group lookupById - #48234
Prevent User LDAP filter from leaking into group lookupById#48234JoWe112 wants to merge 3 commits into
Conversation
martin-kanis
left a comment
There was a problem hiding this comment.
@JoWe112 Thanks for the PR. The fix looks correct but the PR is missing an integration test. Can you please add some test coverage?
b5a5f4e to
7e0aa98
Compare
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>
7e0aa98 to
b93376e
Compare
There was a problem hiding this comment.
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
userQueryflag toLDAPQueryand set it for user searches. - Thread the flag through
LDAPIdentityStoreintoLDAPOperationManager.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.
| public Condition getFilterById(String id) { | ||
| return getFilterById(id, true); | ||
| } | ||
|
|
||
| public Condition getFilterById(String id, boolean applyCustomUserFilter) { | ||
| LDAPQueryConditionsBuilder builder = new LDAPQueryConditionsBuilder(); | ||
| Condition conditionId; |
| 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(); |
| // 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; |
| 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)"); |
| ldapQuery.setSearchScope(config.getSearchScope()); | ||
| ldapQuery.setSearchDn(config.getUsersDn()); | ||
| ldapQuery.addObjectClasses(config.getUserObjectClasses()); | ||
| ldapQuery.setUserQuery(true); |
There was a problem hiding this comment.
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
userQueryis ambiguous: it reads like 'this query returns users', but it actually controls whether the custom user search filter is applied duringlookupById. Consider renaming to something explicit (e.g.,applyCustomUserSearchFilterToLookupByIdorapplyCustomUserFilter) 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
userQueryis ambiguous: it reads like 'this query returns users', but it actually controls whether the custom user search filter is applied duringlookupById. Consider renaming to something explicit (e.g.,applyCustomUserSearchFilterToLookupByIdorapplyCustomUserFilter) 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
lookupByIdapplycustomUserSearchFilteronly whenidentityQuery.isUserQuery()is true, butLDAPQuery.userQuerydefaults tofalse. Any user-facing query path that triggers the UUID fast-path but does not go throughLDAPUtils.createQueryForUserSearch(...)(and therefore doesn’t setuserQuery=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 renamingapplyCustomUserFiltertoapplyCustomUserSearchFilterand 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 renamingapplyCustomUserFiltertoapplyCustomUserSearchFilterand 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();
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:
Query 2 - Member lookup using CNs from the group's member attribute, and with User LDAP filter "(LoginDisabled=false)" added to the filter: