Expose role enrichment helper for cross-client authorization evaluation - #49049
Expose role enrichment helper for cross-client authorization evaluation#49049hakdogan wants to merge 1 commit into
Conversation
06352da to
08471ca
Compare
There was a problem hiding this comment.
Pull request overview
Exposes reusable token role enrichment for cross-client authorization while preserving admin-console evaluation behavior.
Changes:
- Adds
TokenIdentityEnricher.addAllUserRoles. - Delegates policy evaluation enrichment to the helper.
- Adds unit and integration coverage.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
TokenIdentityEnricher.java |
Implements role projection. |
PolicyEvaluationService.java |
Delegates existing enrichment. |
TokenIdentityEnricherTest.java |
Tests null arguments. |
KeycloakIdentityCrossClientRoleTest.java |
Tests cross-client evaluation. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| String roleValues = "[{\"id\":\"" + role.getId() + "\",\"required\": true}]"; | ||
| Map<String, String> config = new HashMap<>(); | ||
| config.put("roles", roleValues); | ||
| config.put("fetchRoles", Boolean.TRUE.toString()); |
There was a problem hiding this comment.
Good catch — fixed in the latest push.
fetchRoles is gone from the role policy, so RolePolicyProvider no longer reloads the subject and now resolves the role from the identity's token claims, which is the path the helper actually feeds.
To make the coverage self-checking rather than merely passing, I also added a negative test, plainTokenIdentity_missesCrossClientRole_yieldsDeny: an identity built from the raw client token is denied, because the client-b role never reaches the token under fullScopeAllowed=false. Together with the existing PERMIT test, removing or breaking addAllUserRoles now turns the pair red.
All three tests pass locally against the embedded Undertow auth server.
| throw new IllegalArgumentException("user must not be null"); | ||
| } | ||
|
|
||
| user.getRoleMappingsStream().forEach(roleModel -> { |
There was a problem hiding this comment.
The observation is correct: getRoleMappingsStream() returns direct assignments only, while token issuance projects RoleUtils.getDeepUserRoleMappings(user).
I have deliberately not changed the semantics here, because this PR is a behavior-preserving extraction: the loop is lifted verbatim out of PolicyEvaluationService.createIdentity(), where it has always used the direct mappings. Switching to the deep mappings would change what the admin console's Evaluate tab reports for group- and composite-derived roles — a user-visible behavior change that seems out of scope for a refactoring PR and one I would not want to slip in unannounced.
What I did change is the promise: the class and method Javadoc now state that only directly assigned mappings are projected, and point at RoleUtils#getDeepUserRoleMappings(UserModel) as the deeper alternative used during token issuance, so the helper no longer over-promises.
That leaves a real question for maintainers: should the admin-console enrichment (and therefore this helper) project deep role mappings? If yes, I am happy to do it in this PR, with coverage for both the group and the composite inheritance path; if you would rather keep this one purely mechanical, I will open a follow-up issue for it. Leaving this thread open until there is a call either way.
0e0a3a3 to
a8c0089
Compare
|
Rebased onto current
Verified locally against the embedded Undertow auth server: This PR has been open since May without a human review — a look from |
A KeycloakIdentity constructed from a bearer access token derives its role attributes from the token's realm_access and resource_access claims. When a resource is protected by a policy referencing a role defined in a different client, and that client is not part of the requesting client's scope, the role is absent from the token and the policy evaluates to DENY even when the user has been granted the role. PolicyEvaluationService.createIdentity() already compensates for this internally by projecting all of the user's role mappings onto the access token before constructing the identity. This commit promotes that enrichment to a public utility, TokenIdentityEnricher.addAllUserRoles(token, user), located alongside KeycloakIdentity in the services module. PolicyEvaluationService now delegates to the same helper, eliminating the duplication noted in discussion keycloak#46661. Closes keycloak#49048 Signed-off-by: Hüseyin Akdoğan <huseyin@keymate.io>
a8c0089 to
2ee1bc7
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (2)
services/src/main/java/org/keycloak/authorization/common/TokenIdentityEnricher.java:90
- No test exercises the realm-role path or the new null-
realm_accessinitialization here; the functional integration scenario assigns onlyCLIENT_B_ROLE. Add a case with a directly assigned realm role and anAccessTokenlacking realm access so this public contract is regression-protected.
AccessToken.Access realmAccess = token.getRealmAccess();
if (realmAccess == null) {
realmAccess = new AccessToken.Access();
token.setRealmAccess(realmAccess);
tests/base/src/test/java/org/keycloak/tests/authz/services/KeycloakIdentityCrossClientRoleTest.java:96
- The test plan claims this regression test was run with
-pl testsuite/integration-arquillian/tests/base, but this class is in the separatetests/basemodule and the legacy module does not depend on or scan it. Update the verification command and results to targettests/base; the documented command did not execute this class.
@KeycloakIntegrationTest
public class KeycloakIdentityCrossClientRoleTest {
|
Migrated the integration test out of the deprecated testsuite, which the
The authorization artifacts (resource server, scope, resource, role policy, scope permission) are still created server-side in a Verified locally: |
Closes #49048
Summary
Extracts the role enrichment loop from
PolicyEvaluationService.createIdentity()into a public static helper,org.keycloak.authorization.common.TokenIdentityEnricher.addAllUserRoles(AccessToken, UserModel).PolicyEvaluationServicenow delegates to the helper. The change is additive — no existing public API is modified.The motivation, scope, and the rationale for choosing a minimum-surface helper over the originally proposed
fromUser(...)factory are in the linked issue.Changes
services/.../common/TokenIdentityEnricher.javaservices/.../admin/PolicyEvaluationService.javacreateIdentity()delegates the role-projection loop to the helper; admin-console behavior preservedservices/.../common/TokenIdentityEnricherTest.javatestsuite/.../authz/KeycloakIdentityCrossClientRoleTest.javaTest plan
Verified locally with the
auth-server-quarkusprofile:./mvnw test -pl services -Dtest=TokenIdentityEnricherTest— 2/2 pass./mvnw test -Pauth-server-quarkus -pl testsuite/integration-arquillian/tests/base -Dtest=KeycloakIdentityCrossClientRoleTest,PolicyEvaluationCompositeRoleTest,PolicyEvaluationTest -Ddocker.database.skip=true -Ddocker.infinispan.skip=trueKeycloakIdentityCrossClientRoleTest: 2/2 pass (admin-console parity + enriched-helper proof)PolicyEvaluationCompositeRoleTest: 1/1 pass (regression guard)PolicyEvaluationTest: 14/14 pass (regression guard)./mvnw spotless:check— clean