Skip to content

Add Groups resource type and scopes to authorization schema and evalu… - #1

Open
linxia0415 wants to merge 1 commit into
mainfrom
pr-37038
Open

Add Groups resource type and scopes to authorization schema and evalu…#1
linxia0415 wants to merge 1 commit into
mainfrom
pr-37038

Conversation

@linxia0415

@linxia0415 linxia0415 commented Jun 3, 2026

Copy link
Copy Markdown

…ation implementation

Closes #35562

Summary by CodeRabbit

Release Notes

  • New Features

    • Added fine-grained authorization controls for group management, including dedicated scopes for viewing and managing group members.
    • Groups resource type now fully integrated into the admin permission framework alongside users and clients.
  • Tests

    • Added comprehensive authorization tests validating group permission evaluation and access control scenarios.

…ation implementation

Closes #35562

Signed-off-by: vramik <vramik@redhat.com>
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This pull request extends Keycloak's admin fine-grained authorization to support group-scoped permissions. It adds a GROUPS resource type to the authorization schema with membership-related scopes, refactors group permission evaluation from simple role checks to policy-based evaluation via a new GroupPermissionsV2 implementation, and updates related admin resources and user permission checks to use the new group ID-based visibility APIs.

Changes

Group-Level Fine-Grained Authorization System

Layer / File(s) Summary
Authorization schema extension for groups
server-spi-private/src/main/java/org/keycloak/authorization/AdminPermissionsSchema.java
AdminPermissionsSchema defines GROUPS resource type with VIEW_MEMBERS, MANAGE_MEMBERS, and MANAGE_MEMBERSHIP scopes; getOrCreateResource switches on resource type with group name resolution; resolveGroup helper translates group id to group id.
GroupPermissionEvaluator interface update
services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissionEvaluator.java
Removes boolean getGroupsWithViewPermission(GroupModel) and renames Set-returning method to getGroupIdsWithViewPermission(); expanded Javadoc references new group authorization scopes.
GroupPermissions V1 refactoring
services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java
Fields (root, resourceStore, policyStore) changed to protected for subclassing; canList/canManage/canView use explicit AdminRoles checks; getGroupIdsWithViewPermission replaces boolean method; canViewMembers checks both VIEW_MEMBERS_SCOPE and MANAGE_MEMBERS_SCOPE.
GroupPermissionsV2 policy-based implementation
services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissionsV2.java
New class evaluates group view/manage permissions and membership permissions using hasPermission helper, which resolves group resources and delegates to root.evaluatePermission; unsupported V2 APIs throw exceptions.
MgmtPermissionsV2 integration
services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissionsV2.java
Adds cached groupPermissions field and lazily-initialized groups() method returning GroupPermissionsV2 instance.
UserPermissions and UserPermissionsV2 refactoring
services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java, UserPermissionsV2.java
UserPermissions removes canManageDefault() and uses explicit role checks; canViewByGroup/canManageByGroup use new canViewMembers/canManageMembers predicates. UserPermissionsV2 simplifies logic with direct hasPermission combinations and narrows admin-role shortcuts.
GroupResource and GroupsResource updates
services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java, GroupsResource.java, server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
GroupResource filters subgroups using per-group canView; GroupsResource uses session.groups().searchGroupsByAttributes() with groupsEvaluator::canView filter; removed ModelToRepresentation.searchGroupModelsByAttributes().
UsersResource group-visibility refactoring
services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java, rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/BruteForceUsersResource.java
getUsersCount and searchForUser use getGroupIdsWithViewPermission() for group-scoped visibility; searchForUser always sets UserModel.GROUPS from group IDs and delegates visibility filtering to toRepresentation().
AdminPermissions listener feature-gating
services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissions.java
Listener registration now guarded with Profile.isFeatureEnabled check, moving feature gate from onEvent to registration time.
RolePermissions and other permission updates
services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java, UserPermissionEvaluator.java
RolePermissions.canMapRole uses hasOneAdminRole(MANAGE_USERS); UserPermissionEvaluator adds expanded Javadoc and deprecates isImpersonatable().
Test framework and configuration updates
test-framework/core/src/main/java/org/keycloak/testframework/util/ApiUtil.java, tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/RealmAdminPermissionsConfig.java, UserResourceTypeEvaluationTest.java
ApiUtil uses try-with-resources for Response; RealmAdminPermissionsConfig grants QUERY_GROUPS alongside QUERY_USERS; unused import removed.
GroupResourceTypeEvaluationTest integration tests
tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/GroupResourceTypeEvaluationTest.java
Comprehensive test suite validates group permissions for member visibility (VIEW_MEMBERS/MANAGE_MEMBERS), group management (MANAGE), group listing (VIEW), and membership operations (MANAGE_MEMBERSHIP).

🎯 4 (Complex) | ⏱️ ~60 minutes

🐰 Fine-grained whispers through the warren,
Group permissions now dance with policies, not just roles,
Authorization grows branches where once were rules,
And rabbits rejoice in the structured permission blooms! 🌿✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title concisely summarizes the main objective of the PR: adding a Groups resource type and scopes to the authorization schema and evaluation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr-37038

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissionsV2.java`:
- Around line 105-128: In getGroupIdsWithViewPermission (GroupPermissionsV2),
you're using the authorization resource's internal ID instead of the actual
group ID: replace usages of groupResource.getId() in the hasPermission(...) call
and the granted.add(...) call with groupResource.getName() so permission checks
and returned IDs use the resource name (which is set to the group ID in
AdminPermissionsSchema.getOrCreateResource()) rather than the internal resource
ID.
- Around line 64-71: The canManage() method in GroupPermissionsV2 improperly
treats VIEW as sufficient for manage; update the hasPermission call in
canManage() so it only checks for AdminPermissionsSchema.MANAGE (remove
AdminPermissionsSchema.VIEW) while keeping the existing
root.hasOneAdminRole(AdminRoles.MANAGE_USERS) check; this makes canManage()
consistent with canManage(GroupModel group) and ensures only the MANAGE scope
grants manage rights.

In
`@tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/GroupResourceTypeEvaluationTest.java`:
- Line 68: The declaration of the field topGroup in class
GroupResourceTypeEvaluationTest has an extra semicolon ("private final
GroupRepresentation topGroup = new GroupRepresentation();;"); remove the
duplicate semicolon so the field reads with a single terminating semicolon;
locate the topGroup field in GroupResourceTypeEvaluationTest and update its
declaration to end with just one semicolon.
- Around line 73-77: The test double-closes Response objects because the
Response returned from realm.admin().groups().add(...) is wrapped in an outer
try-with-resources while ApiUtil.handleCreatedResponse(...) now also closes the
Response; remove the outer try-with-resources and the redundant status assertion
and instead assign the response id from ApiUtil.handleCreatedResponse(...)
directly (as done by topGroup.setId(ApiUtil.handleCreatedResponse(response)));
apply the same change to the other occurrences around the blocks that call
realm.admin().groups().add(...) at the locations mirrored by the existing
pattern so only ApiUtil.handleCreatedResponse(...) manages closing the Response.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a9a12f31-7d52-4d3e-9113-dc602d44f374

📥 Commits

Reviewing files that changed from the base of the PR and between a8fb441 and 7355f05.

📒 Files selected for processing (19)
  • rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/BruteForceUsersResource.java
  • server-spi-private/src/main/java/org/keycloak/authorization/AdminPermissionsSchema.java
  • server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
  • services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java
  • services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java
  • services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
  • services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissions.java
  • services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissionEvaluator.java
  • services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissions.java
  • services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissionsV2.java
  • services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissionsV2.java
  • services/src/main/java/org/keycloak/services/resources/admin/permissions/RolePermissions.java
  • services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionEvaluator.java
  • services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissions.java
  • services/src/main/java/org/keycloak/services/resources/admin/permissions/UserPermissionsV2.java
  • test-framework/core/src/main/java/org/keycloak/testframework/util/ApiUtil.java
  • tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/GroupResourceTypeEvaluationTest.java
  • tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/RealmAdminPermissionsConfig.java
  • tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/UserResourceTypeEvaluationTest.java
💤 Files with no reviewable changes (2)
  • server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
  • tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/UserResourceTypeEvaluationTest.java

Comment on lines +64 to +71
@Override
public boolean canManage() {
if (root.hasOneAdminRole(AdminRoles.MANAGE_USERS)) {
return true;
}

return hasPermission(null, AdminPermissionsSchema.VIEW, AdminPermissionsSchema.MANAGE);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

canManage() incorrectly includes VIEW scope.

The method checks for both VIEW and MANAGE scopes, but canManage() should only grant manage permission when the user has the MANAGE scope. Compare with canManage(GroupModel group) at line 79 which correctly checks only for MANAGE.

🐛 Proposed fix
     `@Override`
     public boolean canManage() {
         if (root.hasOneAdminRole(AdminRoles.MANAGE_USERS)) {
             return true;
         }

-        return hasPermission(null, AdminPermissionsSchema.VIEW, AdminPermissionsSchema.MANAGE);
+        return hasPermission(null, AdminPermissionsSchema.MANAGE);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public boolean canManage() {
if (root.hasOneAdminRole(AdminRoles.MANAGE_USERS)) {
return true;
}
return hasPermission(null, AdminPermissionsSchema.VIEW, AdminPermissionsSchema.MANAGE);
}
`@Override`
public boolean canManage() {
if (root.hasOneAdminRole(AdminRoles.MANAGE_USERS)) {
return true;
}
return hasPermission(null, AdminPermissionsSchema.MANAGE);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissionsV2.java`
around lines 64 - 71, The canManage() method in GroupPermissionsV2 improperly
treats VIEW as sufficient for manage; update the hasPermission call in
canManage() so it only checks for AdminPermissionsSchema.MANAGE (remove
AdminPermissionsSchema.VIEW) while keeping the existing
root.hasOneAdminRole(AdminRoles.MANAGE_USERS) check; this makes canManage()
consistent with canManage(GroupModel group) and ensures only the MANAGE scope
grants manage rights.

Comment on lines +105 to +128
@Override
public Set<String> getGroupIdsWithViewPermission() {
if (root.users().canView()) return Collections.emptySet();

if (!root.isAdminSameRealm()) {
return Collections.emptySet();
}

ResourceServer server = root.realmResourceServer();

if (server == null) {
return Collections.emptySet();
}

Set<String> granted = new HashSet<>();

resourceStore.findByType(server, AdminPermissionsSchema.GROUPS_RESOURCE_TYPE, groupResource -> {
if (hasPermission(groupResource.getId(), AdminPermissionsSchema.VIEW_MEMBERS, AdminPermissionsSchema.MANAGE_MEMBERS)) {
granted.add(groupResource.getId());
}
});

return granted;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

getGroupIdsWithViewPermission() uses resource ID instead of group ID.

The method iterates over authorization resources and uses groupResource.getId() (the authorization resource's internal ID) instead of groupResource.getName() (the actual group ID). The resource name is set to the group ID in AdminPermissionsSchema.getOrCreateResource().

Both line 122 (hasPermission(groupResource.getId(), ...)) and line 123 (granted.add(groupResource.getId())) should use groupResource.getName() instead.

🐛 Proposed fix
         resourceStore.findByType(server, AdminPermissionsSchema.GROUPS_RESOURCE_TYPE, groupResource -> {
-            if (hasPermission(groupResource.getId(), AdminPermissionsSchema.VIEW_MEMBERS, AdminPermissionsSchema.MANAGE_MEMBERS)) {
-                granted.add(groupResource.getId());
+            if (hasPermission(groupResource.getName(), AdminPermissionsSchema.VIEW_MEMBERS, AdminPermissionsSchema.MANAGE_MEMBERS)) {
+                granted.add(groupResource.getName());
             }
         });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public Set<String> getGroupIdsWithViewPermission() {
if (root.users().canView()) return Collections.emptySet();
if (!root.isAdminSameRealm()) {
return Collections.emptySet();
}
ResourceServer server = root.realmResourceServer();
if (server == null) {
return Collections.emptySet();
}
Set<String> granted = new HashSet<>();
resourceStore.findByType(server, AdminPermissionsSchema.GROUPS_RESOURCE_TYPE, groupResource -> {
if (hasPermission(groupResource.getId(), AdminPermissionsSchema.VIEW_MEMBERS, AdminPermissionsSchema.MANAGE_MEMBERS)) {
granted.add(groupResource.getId());
}
});
return granted;
}
`@Override`
public Set<String> getGroupIdsWithViewPermission() {
if (root.users().canView()) return Collections.emptySet();
if (!root.isAdminSameRealm()) {
return Collections.emptySet();
}
ResourceServer server = root.realmResourceServer();
if (server == null) {
return Collections.emptySet();
}
Set<String> granted = new HashSet<>();
resourceStore.findByType(server, AdminPermissionsSchema.GROUPS_RESOURCE_TYPE, groupResource -> {
if (hasPermission(groupResource.getName(), AdminPermissionsSchema.VIEW_MEMBERS, AdminPermissionsSchema.MANAGE_MEMBERS)) {
granted.add(groupResource.getName());
}
});
return granted;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/src/main/java/org/keycloak/services/resources/admin/permissions/GroupPermissionsV2.java`
around lines 105 - 128, In getGroupIdsWithViewPermission (GroupPermissionsV2),
you're using the authorization resource's internal ID instead of the actual
group ID: replace usages of groupResource.getId() in the hasPermission(...) call
and the granted.add(...) call with groupResource.getName() so permission checks
and returned IDs use the resource name (which is set to the group ID in
AdminPermissionsSchema.getOrCreateResource()) rather than the internal resource
ID.

Keycloak realmAdminClient;

private final String groupName = "top_group";
private final GroupRepresentation topGroup = new GroupRepresentation();;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove extra semicolon.

✂️ Proposed fix
-    private final GroupRepresentation topGroup = new GroupRepresentation();;
+    private final GroupRepresentation topGroup = new GroupRepresentation();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private final GroupRepresentation topGroup = new GroupRepresentation();;
private final GroupRepresentation topGroup = new GroupRepresentation();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/GroupResourceTypeEvaluationTest.java`
at line 68, The declaration of the field topGroup in class
GroupResourceTypeEvaluationTest has an extra semicolon ("private final
GroupRepresentation topGroup = new GroupRepresentation();;"); remove the
duplicate semicolon so the field reads with a single terminating semicolon;
locate the topGroup field in GroupResourceTypeEvaluationTest and update its
declaration to end with just one semicolon.

Comment on lines +73 to +77
try (Response response = realm.admin().groups().add(topGroup)) {
assertThat(response.getStatus(), equalTo(Response.Status.CREATED.getStatusCode()));
topGroup.setId(ApiUtil.handleCreatedResponse(response));
realm.cleanup().add(r -> r.groups().group(topGroup.getId()).remove());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix resource management: double-close of Response.

The Response is wrapped in try-with-resources on line 73, then passed to ApiUtil.handleCreatedResponse on line 75. Since ApiUtil.handleCreatedResponse now uses try-with-resources internally (as changed in ApiUtil.java), the Response will be closed twice: once inside handleCreatedResponse and again when the outer try block exits.

Additionally, the status check on line 74 is redundant because handleCreatedResponse already validates the 201 status.

This pattern also appears at lines 195-199 and 261-265.

🔧 Proposed fix
-        try (Response response = realm.admin().groups().add(topGroup)) {
-            assertThat(response.getStatus(), equalTo(Response.Status.CREATED.getStatusCode()));
-            topGroup.setId(ApiUtil.handleCreatedResponse(response));
-            realm.cleanup().add(r -> r.groups().group(topGroup.getId()).remove());
-        }
+        Response response = realm.admin().groups().add(topGroup);
+        topGroup.setId(ApiUtil.handleCreatedResponse(response));
+        realm.cleanup().add(r -> r.groups().group(topGroup.getId()).remove());

Apply the same pattern at lines 195-199 and 261-265:

Lines 195-199:

-        try (Response response = realm.admin().groups().add(myGroup)) {
-            assertThat(response.getStatus(), equalTo(Response.Status.CREATED.getStatusCode()));
-            myGroup.setId(ApiUtil.handleCreatedResponse(response));
-            realm.cleanup().add(r -> r.groups().group(myGroup.getId()).remove());
-        }
+        Response response = realm.admin().groups().add(myGroup);
+        myGroup.setId(ApiUtil.handleCreatedResponse(response));
+        realm.cleanup().add(r -> r.groups().group(myGroup.getId()).remove());

Lines 261-265:

-        try (Response response = realm.admin().groups().add(myGroup)) {
-            assertThat(response.getStatus(), equalTo(Response.Status.CREATED.getStatusCode()));
-            myGroup.setId(ApiUtil.handleCreatedResponse(response));
-            realm.cleanup().add(r -> r.groups().group(myGroup.getId()).remove());
-        }
+        Response response = realm.admin().groups().add(myGroup);
+        myGroup.setId(ApiUtil.handleCreatedResponse(response));
+        realm.cleanup().add(r -> r.groups().group(myGroup.getId()).remove());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/GroupResourceTypeEvaluationTest.java`
around lines 73 - 77, The test double-closes Response objects because the
Response returned from realm.admin().groups().add(...) is wrapped in an outer
try-with-resources while ApiUtil.handleCreatedResponse(...) now also closes the
Response; remove the outer try-with-resources and the redundant status assertion
and instead assign the response id from ApiUtil.handleCreatedResponse(...)
directly (as done by topGroup.setId(ApiUtil.handleCreatedResponse(response)));
apply the same change to the other occurrences around the blocks that call
realm.admin().groups().add(...) at the locations mirrored by the existing
pattern so only ApiUtil.handleCreatedResponse(...) manages closing the Response.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants