Skip to content

Add Client resource type and scopes to authorization schema#5

Open
ShashankFC wants to merge 1 commit into
feature-clients-authz-baselinefrom
feature-clients-authz-implementation
Open

Add Client resource type and scopes to authorization schema#5
ShashankFC wants to merge 1 commit into
feature-clients-authz-baselinefrom
feature-clients-authz-implementation

Conversation

@ShashankFC

@ShashankFC ShashankFC commented Jan 5, 2026

Copy link
Copy Markdown
Collaborator

Test 5

Summary by CodeRabbit

Release Notes

  • New Features
    • Added fine-grained authorization support for client management operations with granular permission scopes including configure, manage, view, and role mapping capabilities.
    • Administrators can now assign detailed access controls for client and client scope operations, enabling better permission delegation and compliance.

✏️ Tip: You can customize this high-level summary in your review settings.


Replicated from ai-code-review-evaluation/keycloak-coderabbit#5

…valuation implementation for ClientsPermissionsV2

Closes #35564

Signed-off-by: Martin Kanis <mkanis@redhat.com>
@ShashankFC

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jan 12, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jan 12, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The changes introduce support for fine-grained client permissions in Keycloak's authorization system. A new CLIENTS resource type is added to AdminPermissionsSchema with associated scopes. ClientPermissionsV2, a new V2-tier permission evaluation class, is introduced to check client resource permissions. ClientPermissionEvaluator expands with new public permission check methods. Test utilities are refactored to accept client parameters for permission operations.

Changes

Cohort / File(s) Summary
Schema and Constants
server-spi-private/src/main/java/org/keycloak/authorization/AdminPermissionsSchema.java
Adds CLIENTS resource type with scopes: configure, manage, view, map-roles, map-roles-client-scope, map-roles-composite. Introduces public constants CLIENTS_RESOURCE_TYPE, CONFIGURE, MAP_ROLES_CLIENT_SCOPE, MAP_ROLES_COMPOSITE, and CLIENTS. Extends schema map initialization to include CLIENTS alongside USERS.
Feature Gating
services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissions.java
Wraps event listener logic (RoleRemovedEvent, ClientRemovedEvent, GroupRemovedEvent handling) inside ADMIN_FINE_GRAINED_AUTHZ feature flag guard.
Permission Evaluator Expansion
services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionEvaluator.java
Expands public API with 25+ new permission check and requirement methods: canManage, canView, canList, canConfigure, canMapRoles, canMapCompositeRoles, canMapClientScopeRoles, and variants accepting ClientModel or ClientScopeModel parameters.
V2-Tier Permission Implementation
services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionsV2.java
Introduces new ClientPermissionsV2 class extending ClientPermissions. Implements V2-tier permission evaluation with overridden methods for configure, manage, view, map-roles operations. Provides getClientsWithPermission(String scope) helper. Throws UnsupportedOperationException for V2-unsupported legacy operations.
Permission Integration
services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissionsV2.java
Adds private clientPermissions field and public clients() method with lazy initialization pattern, mirroring existing users() method.
Test Utilities Refactoring
tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/AbstractPermissionTest.java
Converts instance methods to static with explicit ManagedClient parameter: getPermissionsResource, getScopePermissionsResource, createPermission variants. Adds static factory methods createUserPolicy, createClientPolicy, createAllPermission.
Client Permission Tests
tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/PermissionClientTest.java
Introduces extensive client-level permission tests: testManageOnlyOneClient, testConfigureOnlyOneClient, testManageAllClients, testViewAllClients, testMapRolesAndCompositesOnlyOneClient. Adds realmAdminClient and realmClient fields, @AfterEach cleanup.
Test Utility Updates
tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/PermissionRESTTest.java, UserResourceTypeEvaluationTest.java, UserResourceTypePermissionTest.java
Updates all test helper calls to pass ManagedClient parameter: getScopePermissionsResource(client), createPermission(client, ...). Refactors resource retrieval to use client-scoped access patterns.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Admin Client
    participant CPV2 as ClientPermissionsV2
    participant AuthZ as AuthorizationProvider
    participant RS as ResourceServer
    participant PE as Policy Evaluator

    Client->>CPV2: canManage(ClientModel)
    activate CPV2
    CPV2->>CPV2: hasPermission(ClientModel, "manage")
    activate CPV2
    CPV2->>AuthZ: getEvaluationContext(ClientModel)
    activate AuthZ
    AuthZ-->>CPV2: EvaluationContext
    deactivate AuthZ
    
    CPV2->>RS: getResources(CLIENTS_RESOURCE_TYPE)
    activate RS
    RS-->>CPV2: Resource list
    deactivate RS
    
    CPV2->>PE: evaluate(EvaluationContext, Policy)
    activate PE
    PE-->>CPV2: boolean result
    deactivate PE
    
    CPV2-->>CPV2: hasGrantedPermission check
    deactivate CPV2
    
    CPV2-->>Client: permission result
    deactivate CPV2
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Clients now dance with CONFIGURE scope,
V2 permissions give us hope,
Fine-grained checks in every place,
Schemas bloom with CLIENTS grace,
Authorization hops ahead! 🌟

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is minimal and vague, containing only 'Test 5' and a reference link without substantive implementation details, objectives, or context about the changes. Expand the description to explain the purpose of adding Client resource types, the scope changes involved, and how this relates to the authorization schema implementation. Include links to relevant issues and design documents.
Docstring Coverage ⚠️ Warning Docstring coverage is 22.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding Client resource type and scopes to the authorization schema, which is the core focus of the changes across multiple files.

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

✨ Finishing touches
  • 📝 Generate docstrings

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: 6

🤖 Fix all issues with AI agents
In
@services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissions.java:
- Around line 77-96: The event cleanup block in AdminPermissions (handling
RoleContainerModel.RoleRemovedEvent, ClientModel.ClientRemovedEvent,
GroupModel.GroupRemovedEvent) is currently guarded only by
Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ so it won't run when only
ADMIN_FINE_GRAINED_AUTHZ_V2 is enabled; update the feature check to require
either flag (i.e., check
Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ) ||
Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ_V2)) so the
management(...) .roles().setPermissionsEnabled(...),
.clients().setPermissionsEnabled(...), and .groups().setPermissionsEnabled(...)
cleanup handlers always run for V1 or V2.

In
@services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionEvaluator.java:
- Around line 95-98: The Javadoc for requireView() is incorrect: it claims a
return value but the method is void and throws ForbiddenException on failure;
update the Javadoc for requireView() in ClientPermissionEvaluator to describe
that it enforces view permission, throws ForbiddenException when canView() is
false, and replace the incorrect "Returns ..." line with a short description and
a @throws ForbiddenException tag referencing canView().
- Around line 100-103: The Javadoc for requireViewClientScopes() incorrectly
uses "Returns" for a void method; update the Javadoc to describe the throwing
behavior instead (e.g., state that the method throws an appropriate exception if
canViewClientScopes() is false) similar to the fix for requireView(), and ensure
the comment references the canViewClientScopes() check and the exception
type/condition thrown by requireViewClientScopes().

In
@services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionsV2.java:
- Around line 262-272: The private method getEvaluationContext (which constructs
a ClientModelIdentity and anonymous DefaultEvaluationContext overriding
getBaseAttributes) is unused; remove the getEvaluationContext method entirely
or, if it's intended for future use, add a clear TODO comment above it
explaining expected usage. Update references to ClientModelIdentity and
DefaultEvaluationContext only if you remove related imports or fields; ensure no
remaining unused imports or variables (e.g., authorizedClient, session) remain
after deletion.

In
@tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/PermissionClientTest.java:
- Around line 330-341: The test creates client roles in the variables role and
subRole but never cleans them up; after creating each role (or immediately after
fetching their representations) register cleanup to delete them via the client
roles resource (use
realm.admin().clients().get(myclient.getId()).roles().delete(roleName) or delete
by id using role.getId()/subRole.getId()) so both "myclient-role" and
"myclient-subRole" are removed in test teardown.
- Around line 319-323: The test passes null to
realmAdminClient.realm(realm.getName()).clients().create(null), which can
trigger server-side validation errors instead of an authorization check; replace
the null with a minimal, valid ClientRepresentation (e.g., set clientId and
required fields such as protocol/enabled) and call clients().create(validClient)
so the request reaches the authorization layer and then assert the response
status is Status.FORBIDDEN; update the test case around PermissionClientTest to
construct and use that valid representation before calling create().
🧹 Nitpick comments (5)
server-spi-private/src/main/java/org/keycloak/authorization/AdminPermissionsSchema.java (1)

304-326: Missing CLIENTS handling in getResourceName.

The getResourceName method handles USERS resource type to provide human-readable names (e.g., username instead of ID), but there's no corresponding handling for CLIENTS. Consider adding a similar branch to return the client's clientId for better readability in permission displays.

♻️ Suggested addition for CLIENTS handling
             if (USERS.getType().equals(resourceType)) {
                 if (resource.getName().equals(USERS_RESOURCE_TYPE)) {
                     return "All users";
                 }

                 UserModel user = session.users().getUserById(session.getContext().getRealm(), resource.getName());

                 if (user == null) {
                     throw new ModelIllegalStateException("User not found for resource [" + resource.getId() + "]");
                 }

                 return user.getUsername();
+            } else if (CLIENTS.getType().equals(resourceType)) {
+                if (resource.getName().equals(CLIENTS_RESOURCE_TYPE)) {
+                    return "All clients";
+                }
+
+                ClientModel client = session.clients().getClientById(session.getContext().getRealm(), resource.getName());
+
+                if (client == null) {
+                    throw new ModelIllegalStateException("Client not found for resource [" + resource.getId() + "]");
+                }
+
+                return client.getClientId();
             }
tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/AbstractPermissionTest.java (1)

163-179: Minor inconsistency: instance methods vs static pattern.

createPermission at lines 163-179 are instance methods while the other createPermission overloads (lines 66-74) and createAllPermission (line 151) are static. Consider making these static as well for consistency, or document why these need instance access.

services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionsV2.java (2)

46-46: Unused import.

TOKEN_EXCHANGE is imported but not used in this class.

♻️ Remove unused import
-import static org.keycloak.services.resources.admin.permissions.AdminPermissionManagement.TOKEN_EXCHANGE;

207-260: Duplicate code in hasPermission overloads.

The scope evaluation logic (lines 224-234 and 249-257) is nearly identical. Consider extracting a shared helper method to reduce duplication.

♻️ Extract shared scope-matching logic
+    private boolean hasScopeInPermissions(Collection<Permission> permissions, String scope) {
+        for (Permission permission : permissions) {
+            for (String s : permission.getScopes()) {
+                if (scope.equals(s)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     private boolean hasPermission(ClientModel client, String scope) {
         if (!root.isAdminSameRealm()) {
             return false;
         }
         ResourceServer server = root.realmResourceServer();
         if (server == null) return false;

         Resource resource =  resourceStore.findByName(server, client.getId(), server.getId());
         if (resource == null) {
             resource = AdminPermissionsSchema.SCHEMA.getResourceTypeResource(session, server, AdminPermissionsSchema.CLIENTS_RESOURCE_TYPE);

             if (authz.getStoreFactory().getPolicyStore().findByResource(server, resource).isEmpty()) {
                 return false;
             }
         }

         Collection<Permission> permissions = root.evaluatePermission(new ResourcePermission(resource, resource.getScopes(), server), server);
-        List<String> expectedScopes = Arrays.asList(scope);
-        for (Permission permission : permissions) {
-            for (String s : permission.getScopes()) {
-                if (expectedScopes.contains(s)) {
-                    return true;
-                }
-            }
-        }
-
-        return false;
+        return hasScopeInPermissions(permissions, scope);
     }
tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/PermissionClientTest.java (1)

147-153: Consider adding a guard for index access.

Accessing getDefaultClientScopes().get(1) directly may throw IndexOutOfBoundsException if the realm has fewer than 2 default client scopes. While this is unlikely in typical Keycloak setups, consider adding an assertion or check.

♻️ Suggested improvement
+        List<ClientScopeRepresentation> scopes = clientResource.getDefaultClientScopes();
+        assertThat(scopes.size(), org.hamcrest.Matchers.greaterThanOrEqualTo(2));
         try {
-            ClientScopeRepresentation clientScopeRep = clientResource.getDefaultClientScopes().get(1);
+            ClientScopeRepresentation clientScopeRep = scopes.get(1);
             clientResource.removeDefaultClientScope(clientScopeRep.getId());
             fail("Expected exception wasn't thrown.");
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between baa6ebb and 7e55408.

📒 Files selected for processing (10)
  • server-spi-private/src/main/java/org/keycloak/authorization/AdminPermissionsSchema.java
  • services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissions.java
  • services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionEvaluator.java
  • services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionsV2.java
  • services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissionsV2.java
  • tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/AbstractPermissionTest.java
  • tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/PermissionClientTest.java
  • tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/PermissionRESTTest.java
  • tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/UserResourceTypeEvaluationTest.java
  • tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/UserResourceTypePermissionTest.java
🧰 Additional context used
🧬 Code graph analysis (4)
tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/AbstractPermissionTest.java (1)
core/src/main/java/org/keycloak/representations/idm/authorization/ScopePermissionRepresentation.java (1)
  • ScopePermissionRepresentation (22-28)
tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/PermissionClientTest.java (2)
server-spi-private/src/main/java/org/keycloak/authorization/AdminPermissionsSchema.java (1)
  • AdminPermissionsSchema (52-327)
tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/AbstractPermissionTest.java (1)
  • PermissionBuilder (76-115)
tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/UserResourceTypeEvaluationTest.java (2)
server-spi-private/src/main/java/org/keycloak/authorization/AdminPermissionsSchema.java (1)
  • AdminPermissionsSchema (52-327)
test-framework/core/src/main/java/org/keycloak/testframework/realm/UserConfigBuilder.java (1)
  • UserConfigBuilder (10-89)
services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionsV2.java (3)
server-spi-private/src/main/java/org/keycloak/authorization/AdminPermissionsSchema.java (1)
  • AdminPermissionsSchema (52-327)
server-spi-private/src/main/java/org/keycloak/authorization/AuthorizationProvider.java (1)
  • AuthorizationProvider (80-555)
js/apps/account-ui/src/api/request.ts (1)
  • token (65-74)
🪛 Gitleaks (8.30.0)
services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionsV2.java

[high] 53-53: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (32)
server-spi-private/src/main/java/org/keycloak/authorization/AdminPermissionsSchema.java (3)

53-68: LGTM! CLIENTS resource type definition follows established patterns.

The new CLIENTS resource type and its scopes (CONFIGURE, MANAGE, MAP_ROLES, MAP_ROLES_CLIENT_SCOPE, MAP_ROLES_COMPOSITE, VIEW) are well-defined and consistent with the existing USERS resource type pattern.


93-94: LGTM! Client resolution branch added correctly.

The resolution logic for CLIENTS resources mirrors the existing USERS pattern appropriately.


175-184: LGTM! resolveClient method follows the established pattern.

The method correctly mirrors resolveUser by first attempting lookup by ID, then falling back to clientId.

tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/AbstractPermissionTest.java (2)

54-64: LGTM! Refactored to static methods with explicit client parameter.

Converting these to static utilities with explicit ManagedClient parameter improves testability and makes dependencies explicit.


117-149: LGTM! Policy factory methods with proper cleanup registration.

The cleanup callbacks properly remove policies after test execution, preventing test pollution. The pattern of registering cleanup during creation is a good practice.

services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionsV2.java (2)

49-55: LGTM! Constructor properly extends ClientPermissions.

The static analysis hint about an API key at line 53 is a false positive - root is simply a constructor parameter name, not a secret.


57-123: LGTM! Permission check methods correctly delegate to V2 evaluation logic.

The override methods properly combine admin role checks with fine-grained permission evaluation.

tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/PermissionRESTTest.java (2)

129-133: LGTM! Updated to use client-scoped permission creation.

The change to pass client as the first argument aligns with the refactored test utilities in AbstractPermissionTest.


160-173: LGTM! Consistent client parameter propagation in error case tests.

All permission creation calls in the error validation tests correctly pass the client context.

services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissionsV2.java (2)

29-29: LGTM! New field for ClientPermissionsV2.

Follows the same pattern as the existing userPermissions field.


62-68: LGTM! Lazy initialization of ClientPermissionsV2.

The clients() method correctly mirrors the existing users() pattern with proper lazy initialization and caching.

tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/UserResourceTypePermissionTest.java (3)

67-71: LGTM! Cleanup method updated to use client-scoped resource.

The @AfterEach cleanup correctly uses the updated getScopePermissionsResource(client) signature.


77-97: LGTM! Test methods updated for client-scoped permission access.

Both testCreateResourceTypePermission and testCreateResourceObjectPermission correctly propagate the client context.


186-198: LGTM! Helper methods updated consistently.

Both createUserPermission and createAllUserPermission correctly pass the client to createPermission.

tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/UserResourceTypeEvaluationTest.java (8)

66-67: Good addition of type constant.

Using AdminPermissionsSchema.USERS.getType() instead of a hardcoded string improves maintainability and ensures consistency with the schema definition.


70-79: LGTM!

The cleanup method correctly uses the client-scoped permission resource retrieval and properly removes all scope permissions after each test.


81-90: LGTM!

The test correctly uses the updated client-parameterized helpers (createUserPolicy, createPermission) while maintaining the original test logic.


92-133: LGTM!

The impersonation test properly exercises both positive and negative permission scenarios with the refactored helper methods.


135-186: LGTM!

The manage permission tests correctly exercise the full permission lifecycle using the updated client-scoped helpers.


188-227: LGTM!

The view permission tests properly validate both allow and deny scenarios using the refactored permission helpers.


229-330: LGTM!

The map-roles and group-membership tests correctly use the updated helper method signatures and maintain proper test coverage for the permission evaluation logic.


332-367: LGTM!

The transitive permissions test properly validates that MANAGE scope grants transitive access to VIEW, MAP_ROLES, and MANAGE_GROUP_MEMBERSHIP, while correctly asserting that IMPERSONATE is not included.

tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/PermissionClientTest.java (5)

70-83: LGTM!

The test setup correctly injects the required managed realm client and adds proper cleanup to remove all scope permissions after each test, preventing test pollution.


85-124: LGTM!

The policy type support tests correctly use the updated client-parameterized permission resource helpers while maintaining the original validation logic.


191-247: LGTM!

The test properly validates that CONFIGURE permission grants secret management and view access while correctly denying client scope modifications, demonstrating the distinction from MANAGE permission.


249-281: LGTM!

The test comprehensively validates that all-clients MANAGE permission enables client creation, deletion, and listing operations.


357-365: LGTM!

The permission setup correctly grants both user-level MAP_ROLES permission and client-level permissions (MAP_ROLES, MAP_ROLES_COMPOSITE, CONFIGURE) to enable role assignment operations.

services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionEvaluator.java (5)

35-38: Javadoc inaccuracy: method returns void, not boolean.

The Javadoc for requireListClientScopes() says "Throws ForbiddenException" but doesn't indicate the return type is void. The method signature at line 38 shows it returns void, which is correct, but the documentation could be clearer.


40-104: Well-documented interface methods.

The Javadoc clearly explains the dual behavior for V1 (role-based) and V2 (fine-grained permission) authorization, providing good guidance for implementers and consumers of this API.


105-145: LGTM!

The Javadoc for client-specific permission methods (canManage(ClientModel), canConfigure(ClientModel), canView(ClientModel)) accurately documents the permission evaluation hierarchy and V1/V2 behavior.


147-190: LGTM!

The new ClientScopeModel permission methods and role mapping permission methods (canMapRoles, canMapCompositeRoles, canMapClientScopeRoles) are well-documented and align with the AdminPermissionsSchema scopes.


192-195: LGTM!

The existing getAccess and getClientsWithPermission methods are retained, maintaining backward compatibility.

Comment on lines +77 to 96
if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) {
if (event instanceof RoleContainerModel.RoleRemovedEvent) {
RoleContainerModel.RoleRemovedEvent cast = (RoleContainerModel.RoleRemovedEvent) event;
RoleModel role = cast.getRole();
RealmModel realm;
if (role.getContainer() instanceof ClientModel) {
realm = ((ClientModel) role.getContainer()).getRealm();

} else {
realm = (RealmModel)role.getContainer();
} else {
realm = (RealmModel) role.getContainer();
}
management(cast.getKeycloakSession(), realm).roles().setPermissionsEnabled(role, false);
} else if (event instanceof ClientModel.ClientRemovedEvent) {
ClientModel.ClientRemovedEvent cast = (ClientModel.ClientRemovedEvent) event;
management(cast.getKeycloakSession(), cast.getClient().getRealm()).clients().setPermissionsEnabled(cast.getClient(), false);
} else if (event instanceof GroupModel.GroupRemovedEvent) {
GroupModel.GroupRemovedEvent cast = (GroupModel.GroupRemovedEvent) event;
management(cast.getKeycloakSession(), cast.getRealm()).groups().setPermissionsEnabled(cast.getGroup(), false);
}
management(cast.getKeycloakSession(), realm).roles().setPermissionsEnabled(role, false);
} else if (event instanceof ClientModel.ClientRemovedEvent) {
ClientModel.ClientRemovedEvent cast = (ClientModel.ClientRemovedEvent)event;
management(cast.getKeycloakSession(), cast.getClient().getRealm()).clients().setPermissionsEnabled(cast.getClient(), false);
} else if (event instanceof GroupModel.GroupRemovedEvent) {
GroupModel.GroupRemovedEvent cast = (GroupModel.GroupRemovedEvent)event;
management(cast.getKeycloakSession(), cast.getRealm()).groups().setPermissionsEnabled(cast.getGroup(), false);
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for both feature flags usage patterns
echo "=== ADMIN_FINE_GRAINED_AUTHZ usage ==="
rg -n "ADMIN_FINE_GRAINED_AUTHZ" --type java | head -150

echo ""
echo "=== Files containing both V1 and V2 flags ==="
rg -l "ADMIN_FINE_GRAINED_AUTHZ" --type java | xargs -I {} sh -c 'if grep -q "ADMIN_FINE_GRAINED_AUTHZ_V2" "$1"; then echo "$1"; fi' _ {}

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 12484


🏁 Script executed:

#!/bin/bash
# Get the AdminPermissions.java file and check surrounding context
fd "AdminPermissions.java" --type f -x cat -n {} | head -150

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 5594


🏁 Script executed:

#!/bin/bash
# Examine MgmtPermissions.java line 85 for context
sed -n '80,90p' services/src/main/java/org/keycloak/services/resources/admin/permissions/MgmtPermissions.java

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 685


🏁 Script executed:

#!/bin/bash
# Check what MgmtPermissionsV2 does - verify if it also relies on cleanup handlers
fd "MgmtPermissionsV2.java" --type f -x wc -l {}
fd "MgmtPermissionsV2.java" --type f -x head -100 {}

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 2554


Fix event listener feature flag check to support both V1 and V2.

The cleanup handlers for RoleRemovedEvent, ClientRemovedEvent, and GroupRemovedEvent at line 77 are gated behind only ADMIN_FINE_GRAINED_AUTHZ (V1). If V2 is enabled without V1, these cleanup handlers won't execute, leaving orphaned permission resources. Change the condition to check for either flag, matching the pattern used in MgmtPermissions.java line 85:

if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ) || Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ_V2)) {
🤖 Prompt for AI Agents
In
@services/src/main/java/org/keycloak/services/resources/admin/permissions/AdminPermissions.java
around lines 77 - 96, The event cleanup block in AdminPermissions (handling
RoleContainerModel.RoleRemovedEvent, ClientModel.ClientRemovedEvent,
GroupModel.GroupRemovedEvent) is currently guarded only by
Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ so it won't run when only
ADMIN_FINE_GRAINED_AUTHZ_V2 is enabled; update the feature check to require
either flag (i.e., check
Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ) ||
Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ_V2)) so the
management(...) .roles().setPermissionsEnabled(...),
.clients().setPermissionsEnabled(...), and .groups().setPermissionsEnabled(...)
cleanup handlers always run for V1 or V2.

Comment on lines +95 to 98
/**
* Returns {@code true} if {@link #canView()} returns {@code true}.
*/
void requireView();

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

Javadoc incorrectly states "Returns" for a void method.

The Javadoc says "Returns {@code true} if {@link #canView()} returns {@code true}." but requireView() returns void and throws ForbiddenException on failure.

📝 Proposed fix
-    /**
-     * Returns {@code true} if {@link #canView()} returns {@code true}.
-     */
+    /**
+     * Throws ForbiddenException if {@link #canView()} returns {@code false}.
+     */
     void requireView();
🤖 Prompt for AI Agents
In
@services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionEvaluator.java
around lines 95 - 98, The Javadoc for requireView() is incorrect: it claims a
return value but the method is void and throws ForbiddenException on failure;
update the Javadoc for requireView() in ClientPermissionEvaluator to describe
that it enforces view permission, throws ForbiddenException when canView() is
false, and replace the incorrect "Returns ..." line with a short description and
a @throws ForbiddenException tag referencing canView().

Comment on lines +100 to 103
/**
* Returns {@code true} if {@link #canViewClientScopes()} returns {@code true}.
*/
void requireViewClientScopes();

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

Javadoc incorrectly states "Returns" for a void method.

Same issue as requireView() - the Javadoc should describe throwing behavior, not return value.

📝 Proposed fix
-    /**
-     * Returns {@code true} if {@link #canViewClientScopes()} returns {@code true}.
-     */
+    /**
+     * Throws ForbiddenException if {@link #canViewClientScopes()} returns {@code false}.
+     */
     void requireViewClientScopes();
📝 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
/**
* Returns {@code true} if {@link #canViewClientScopes()} returns {@code true}.
*/
void requireViewClientScopes();
/**
* Throws ForbiddenException if {@link #canViewClientScopes()} returns {@code false}.
*/
void requireViewClientScopes();
🤖 Prompt for AI Agents
In
@services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionEvaluator.java
around lines 100 - 103, The Javadoc for requireViewClientScopes() incorrectly
uses "Returns" for a void method; update the Javadoc to describe the throwing
behavior instead (e.g., state that the method throws an appropriate exception if
canViewClientScopes() is false) similar to the fix for requireView(), and ensure
the comment references the canViewClientScopes() check and the exception
type/condition thrown by requireViewClientScopes().

Comment on lines +262 to +272
private EvaluationContext getEvaluationContext(ClientModel authorizedClient, AccessToken token) {
ClientModelIdentity identity = new ClientModelIdentity(session, authorizedClient, token);
return new DefaultEvaluationContext(identity, session) {
@Override
public Map<String, Collection<String>> getBaseAttributes() {
Map<String, Collection<String>> attributes = super.getBaseAttributes();
attributes.put("kc.client.id", List.of(authorizedClient.getClientId()));
return attributes;
}
};
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for usages of getEvaluationContext across the entire codebase
rg -n "getEvaluationContext" --type java

Repository: AI-Code-Review-Evals/coderabbit-keycloak

Length of output: 287


Remove unused getEvaluationContext method.

This method is never called within the codebase. Either remove it or add a TODO comment if it's intended for future use.

🤖 Prompt for AI Agents
In
@services/src/main/java/org/keycloak/services/resources/admin/permissions/ClientPermissionsV2.java
around lines 262 - 272, The private method getEvaluationContext (which
constructs a ClientModelIdentity and anonymous DefaultEvaluationContext
overriding getBaseAttributes) is unused; remove the getEvaluationContext method
entirely or, if it's intended for future use, add a clear TODO comment above it
explaining expected usage. Update references to ClientModelIdentity and
DefaultEvaluationContext only if you remove related imports or fields; ensure no
remaining unused imports or variables (e.g., authorizedClient, session) remain
after deletion.

Comment on lines +319 to +323
// can't create a new client
try (Response response = realmAdminClient.realm(realm.getName()).clients().create(null)) {
Assertions.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
}
}

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

Passing null to clients().create() may not reliably test authorization.

The null parameter could cause a server-side error (e.g., NullPointerException or 400 Bad Request) before the authorization check is evaluated. Consider using a valid ClientRepresentation to ensure the 403 Forbidden response is genuinely from the authorization layer.

🐛 Proposed fix
         // can't create a new client
-        try (Response response = realmAdminClient.realm(realm.getName()).clients().create(null)) {
+        ClientRepresentation newClient = new ClientRepresentation();
+        newClient.setClientId("forbidden-client");
+        newClient.setProtocol("openid-connect");
+        try (Response response = realmAdminClient.realm(realm.getName()).clients().create(newClient)) {
             Assertions.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
         }
📝 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
// can't create a new client
try (Response response = realmAdminClient.realm(realm.getName()).clients().create(null)) {
Assertions.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
}
}
// can't create a new client
ClientRepresentation newClient = new ClientRepresentation();
newClient.setClientId("forbidden-client");
newClient.setProtocol("openid-connect");
try (Response response = realmAdminClient.realm(realm.getName()).clients().create(newClient)) {
Assertions.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
}
}
🤖 Prompt for AI Agents
In
@tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/PermissionClientTest.java
around lines 319 - 323, The test passes null to
realmAdminClient.realm(realm.getName()).clients().create(null), which can
trigger server-side validation errors instead of an authorization check; replace
the null with a minimal, valid ClientRepresentation (e.g., set clientId and
required fields such as protocol/enabled) and call clients().create(validClient)
so the request reaches the authorization layer and then assert the response
status is Status.FORBIDDEN; update the test case around PermissionClientTest to
construct and use that valid representation before calling create().

Comment on lines +330 to +341
// create a role and sub-role
RoleRepresentation role = new RoleRepresentation();
role.setName("myclient-role");
role.setClientRole(true);
realm.admin().clients().get(myclient.getId()).roles().create(role);
role = realm.admin().clients().get(myclient.getId()).roles().get("myclient-role").toRepresentation();

RoleRepresentation subRole = new RoleRepresentation();
subRole.setName("myclient-subRole");
subRole.setClientRole(true);
realm.admin().clients().get(myclient.getId()).roles().create(subRole);
subRole = realm.admin().clients().get(myclient.getId()).roles().get("myclient-subRole").toRepresentation();

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

Missing cleanup for created client roles.

The test creates myclient-role and myclient-subRole but doesn't register cleanup for them. While the test uses an existing client (myclient), the roles may persist after the test, potentially affecting other tests.

🧹 Suggested cleanup registration
         realm.admin().clients().get(myclient.getId()).roles().create(role);
         role = realm.admin().clients().get(myclient.getId()).roles().get("myclient-role").toRepresentation();
+        realm.cleanup().add(r -> r.clients().get(myclient.getId()).roles().deleteRole("myclient-role"));

         RoleRepresentation subRole = new RoleRepresentation();
         subRole.setName("myclient-subRole");
         subRole.setClientRole(true);
         realm.admin().clients().get(myclient.getId()).roles().create(subRole);
         subRole = realm.admin().clients().get(myclient.getId()).roles().get("myclient-subRole").toRepresentation();
+        realm.cleanup().add(r -> r.clients().get(myclient.getId()).roles().deleteRole("myclient-subRole"));
📝 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
// create a role and sub-role
RoleRepresentation role = new RoleRepresentation();
role.setName("myclient-role");
role.setClientRole(true);
realm.admin().clients().get(myclient.getId()).roles().create(role);
role = realm.admin().clients().get(myclient.getId()).roles().get("myclient-role").toRepresentation();
RoleRepresentation subRole = new RoleRepresentation();
subRole.setName("myclient-subRole");
subRole.setClientRole(true);
realm.admin().clients().get(myclient.getId()).roles().create(subRole);
subRole = realm.admin().clients().get(myclient.getId()).roles().get("myclient-subRole").toRepresentation();
// create a role and sub-role
RoleRepresentation role = new RoleRepresentation();
role.setName("myclient-role");
role.setClientRole(true);
realm.admin().clients().get(myclient.getId()).roles().create(role);
role = realm.admin().clients().get(myclient.getId()).roles().get("myclient-role").toRepresentation();
realm.cleanup().add(r -> r.clients().get(myclient.getId()).roles().deleteRole("myclient-role"));
RoleRepresentation subRole = new RoleRepresentation();
subRole.setName("myclient-subRole");
subRole.setClientRole(true);
realm.admin().clients().get(myclient.getId()).roles().create(subRole);
subRole = realm.admin().clients().get(myclient.getId()).roles().get("myclient-subRole").toRepresentation();
realm.cleanup().add(r -> r.clients().get(myclient.getId()).roles().deleteRole("myclient-subRole"));
🤖 Prompt for AI Agents
In
@tests/base/src/test/java/org/keycloak/tests/admin/authz/fgap/PermissionClientTest.java
around lines 330 - 341, The test creates client roles in the variables role and
subRole but never cleans them up; after creating each role (or immediately after
fetching their representations) register cleanup to delete them via the client
roles resource (use
realm.admin().clients().get(myclient.getId()).roles().delete(roleName) or delete
by id using role.getId()/subRole.getId()) so both "myclient-role" and
"myclient-subRole" are removed in test teardown.

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