Apply FGAP view authorization to SCIM filter predicates - #51464
Conversation
There was a problem hiding this comment.
Pull request overview
Applies FGAP authorization checks to SCIM membership filter predicates.
Changes:
- Adds authorization callbacks to JPA predicate evaluation.
- Checks referenced group/user visibility.
- Adds FGAP regression tests.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
AuthorizationTest.java |
Adds membership-filter authorization tests. |
UserResourceTypeProvider.java |
Checks group visibility for user filters. |
GroupResourceTypeProvider.java |
Checks user visibility for group filters. |
ScimJPAPredicateProvider.java |
Invokes filter authorization callbacks. |
ScimJPAPredicateEvaluator.java |
Propagates authorization callbacks. |
| BiPredicate<String, String> authCheck = (path, value) -> { | ||
| if ("members.value".equals(path)) { | ||
| UserModel user = session.users().getUserById(realm, value); | ||
| return user != null && permissions.hasPermission(user, AdminPermissionsSchema.USERS_RESOURCE_TYPE, AdminPermissionsSchema.VIEW); |
There was a problem hiding this comment.
The problem is that the filter introduces a new way to query groups that bypasses the VIEW_MEMBERS gate. Without filters, membership can only be discovered by fetching a group and looking at its members attribute, which VIEW_MEMBERS properly controls. But members.value eq "<user-id>" uses membership as a selection criterion in the query itself - the group appearing in the results already reveals the relationship, making the response-level VIEW_MEMBERS check too late.
A caller with VIEW on the group and VIEW on the user (but not VIEW_MEMBERS) can probe membership status even though the GET response hides the members list. Fixing this requires either extending applyAuthorizationFilters() to support a VIEW_MEMBERS scope (touches the core authz pipeline across 4-6 files) or post-filtering results in Java (breaks pagination/count). Both approaches warrant a dedicated issue IMO.
@pedroigor FYI
30ff834 to
872247c
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
scim/model/src/main/java/org/keycloak/scim/model/group/GroupResourceTypeProvider.java:200
- This authorizes only the referenced user, not whether membership may be viewed for each returned group. The new test demonstrates the gap:
get(..., "members")is hidden with only groupVIEW(AuthorizationTest.java:916-917), yet after granting userVIEWthis predicate returns that group (lines 953-956), so callers can still infer membership without the group'sVIEW_MEMBERSscope required by #50986; constrain candidates byVIEW_MEMBERSor reject the predicate.
UserModel user = session.users().getUserById(realm, value);
return user != null && permissions.hasPermission(user, AdminPermissionsSchema.USERS_RESOURCE_TYPE, AdminPermissionsSchema.VIEW);
scim/model/src/main/java/org/keycloak/scim/model/group/GroupResourceTypeProvider.java:195
- The accepted
memberspath resolves to the same complex attribute and is mapped to the user-membership join, but onlymembers.valueis authorized here. Filters such asmembers eq "<hidden-user-id>"andmembers prconsequently bypass this check.
if ("members.value".equalsIgnoreCase(path)) {
872247c to
80cce3d
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Suppressed comments (1)
scim/model/src/main/java/org/keycloak/scim/model/group/GroupResourceTypeProvider.java:200
- This validates only permission to view the referenced user, not permission to view members of each returned group. Consequently, after the test grants user
VIEW, a group with only groupVIEWis returned even though itsmembersattribute is hidden; constrain results byGROUPS/VIEW_MEMBERS(or reject the filter without that permission) before exposing the membership predicate.
UserModel user = session.users().getUserById(realm, value);
return user != null && permissions.hasPermission(user, AdminPermissionsSchema.USERS_RESOURCE_TYPE, AdminPermissionsSchema.VIEW);
80cce3d to
b5cdfa0
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (2)
scim/model/src/main/java/org/keycloak/scim/model/user/UserResourceTypeProvider.java:235
- This check also applies when FGAP is disabled, so a caller with only
QUERY_USERScan no longer use an equality membership filter: legacyGroupPermissions.canView(group)does not grant group view for that role, and the predicate becomes empty. Make the authorization callback a no-op whenadminPermissionsEnabledis false, as already intended for the non-eqbranch.
if (value == null) {
return !realm.isAdminPermissionsEnabled();
}
GroupModel group = session.groups().getGroupById(realm, value);
return group != null && permissions.hasPermission(group, AdminPermissionsSchema.GROUPS_RESOURCE_TYPE, AdminPermissionsSchema.VIEW);
scim/model/src/main/java/org/keycloak/scim/model/group/GroupResourceTypeProvider.java:203
- This check also applies when FGAP is disabled, so a caller with only
QUERY_GROUPScan no longer use an equality member filter: that role does not grant user view, and the predicate becomes empty. Make the authorization callback a no-op whenadminPermissionsEnabledis false, matching the existing behavior for non-eqoperators.
if (value == null) {
return !realm.isAdminPermissionsEnabled();
}
UserModel user = session.users().getUserById(realm, value);
return user != null && permissions.hasPermission(user, AdminPermissionsSchema.USERS_RESOURCE_TYPE, AdminPermissionsSchema.VIEW);
b5cdfa0 to
44110af
Compare
Closes keycloak#50985 Closes keycloak#50986 Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
44110af to
6970347
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (2)
scim/model/src/main/java/org/keycloak/scim/model/user/UserResourceTypeProvider.java:233
- This also authorizes
eqfilters when FGAP is disabled. A SCIM client with onlyQUERY_USERScan search users butgroups.canView(group)is false (it requiresVIEW_USERS/MANAGE_USERS), so every existinggroups.value eq ...query now returns zero; bypass the callback entirely whenrealm.isAdminPermissionsEnabled()is false, as the non-eqbranch already does.
if ("groups.value".equalsIgnoreCase(path) || "groups".equalsIgnoreCase(path)) {
if (value == null) {
return !realm.isAdminPermissionsEnabled();
}
GroupModel group = session.groups().getGroupById(realm, value);
return group != null && permissions.hasPermission(group, AdminPermissionsSchema.GROUPS_RESOURCE_TYPE, AdminPermissionsSchema.VIEW);
scim/model/src/main/java/org/keycloak/scim/model/group/GroupResourceTypeProvider.java:201
- The user permission lookup runs for
eqeven when FGAP is disabled. A client with onlyQUERY_GROUPSis allowed to search groups but cannotVIEWa user, so legitimatemembers.value eq ...filters regress to an empty result; return true for protected paths whenever admin permissions are disabled.
if ("members.value".equalsIgnoreCase(path) || "members".equalsIgnoreCase(path)) {
if (value == null) {
return !realm.isAdminPermissionsEnabled();
}
UserModel user = session.users().getUserById(realm, value);
return user != null && permissions.hasPermission(user, AdminPermissionsSchema.USERS_RESOURCE_TYPE, AdminPermissionsSchema.VIEW);
Closes #50985
Closes #50986