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);
pedroigor
left a comment
There was a problem hiding this comment.
It seems we are now have a constraint/limitation when FGAP is enabled to realm so that operators other than eq silently returns an empty list.
The reason for adding this constraint makes sense for me when filtering by Users.groups and Groups.members but we should probably document this behavior because it will change when FGAP is not enabled.
Even though operators like co, sw, etc, does not make sense when using these filters, one might argue that pr does make sense to answer a question like `what are the users that have group membership".
Closes #50985
Closes #50986