You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[FGAP V2] manage scope on Clients does not imply view #47804
I have read and understood the above terms for submitting issues, and I understand that my issue may be closed without action if I do not follow them.
Area
admin/fine-grained-permissions
Describe the bug
With FGAP V2, a user granted only the manage scope on Clients cannot list or create clients - canView() only checks the view scope, so canList() returns false.
Version
nightly
Regression
The issue is a regression
Expected behavior
FGAP manage should imply view, matching the role-based behavior where MANAGE_CLIENTS implies VIEW_CLIENTS.
Actual behavior
A user with FGAP manage but not view can update an existing client but cannot:
list clients (canList() -> canView() -> false)
view a specific client (canView(client) -> only checks VIEW scope)
How to Reproduce?
Enable admin permissions on a realm
Create a user with no realm-management roles
Grant an all-clients FGAP permission with only the manage scope
Try to create a client via POST /admin/api/{realm}/clients/v2 - returns 403
Add view scope to the permission - returns 201
Anything else?
Both Admin API v1 and v2 endpoints as both use ClientPermissionsV2 when FGAP V2 is enabled.
The 403 @Pepo48 is highlighting is from an anti-phishing check. We're causing some new behaviors because that check was added to a service layer, rather than the rest layer. To be consistent with client admin api v1 we only need to apply the check when obtaining the client resource. Correcting that will cause the 403 on a POST operation to go away.
With that resolved, someone with manage only would still get a 403 on a PUT of a non-existant client - with view they would get a 404 instead. But this doesn't seem like a strong check - if the manage only user were then to attempt to do a POST they would get a 409 conflict if the resource already exists.
A bigger concern is that having view be independent of manage does not follow well with what we're implementing with admin api v2 - that is we're returning the full resource from PUT / POST / PATCH requests - so sending a minimiltic patch effectively gives you view ability.
To confirm, what should someone having solely manage (without view) be able to do? It appears with admin api v1 they can set the state via POST/create PUT/update, but not see the actual state - are there server managed / generated fields they cannot have access to?
In FGAP v1, transitive relationships between scopes (e.g., manage implying map-roles) caused confusion — it was unclear what granting a scope actually meant in terms of other scopes being implicitly granted. For v2, all scopes were made independent to keep the permission model predictable and easy to reason about, as Pedro pointed out the docs.
I agree this causes a practical paradox, where it makes no sense to have manage without view.
To address this we could change the behavior that:
manage implies view
This would be a narrow fix targeting the most obvious case. However, the same problem exists for other scopes — map-roles without view etc. Treating manage implies view as a special case doesn't address these, and introducing selective transitive dependencies risks bringing back v1-style confusion about which scopes imply what.
any permission on a resource makes it discoverable
Scopes remain fully independent for their operations (granting manage still does not grant map-roles, etc.), but having any FGAP permission on a resource — regardless of scope — makes that resource visible in queries. The specific scope is then enforced when the admin actually performs the operation.
This avoids transitive scope dependencies while addressing all "write-blind" scenarios uniformly.
view as a mandatory default scope
Rather than changing evaluation semantics, we could enforce that view is always included when creating a permission. The minimal permission would be "view-only", and admins add other scopes on top. Since view is always present, the existing partial evaluation logic (which filters by view (and view-members for groups)) works without changes.
But that would need to be enforced both in admin UI as well as endpoints. And we'd need to be aware of negative permission scenarios, which makes this option a bit too much complex.
To me, right now the best approach seems to be option 2. We also need to consider backward compatibility as this would be a behavioral change. Thoughts?
@pedroigor today when using partial evaluator, we are querying for all permissions of a given resource type that references an admin user (via user, group or role policy) and filters by view scope (or view-members for groups).
Then it for each returned permission, we evaluate its associated policies and create allowedResources and deniedResources (deny wins).
If the resource type has a group relationship (Users -> Groups), it runs a second runEvaluation for the group resource type, filtering by view-members. It produces allowedGroups and deniedGroups sets, which are attached to the context.
So to change it to option 2, we'd need to fetch all permissions, but without the view filter, so that we'd get permissions with any scope. We'd need to consider users-groups permissions, like manage-members.
Then when evaluating and creating allowedResources and deniedResources we'd have to add scope awareness to "deny" branch: we'd only add to deniedResources when the permission denies to view (view-members respectively).
This way we'd get all permissions that references the admin user and filter out those resources which specifically denies viewing, right?
Just to clarify on a specific example - a user has FGAP manage permission on all clients but not view. In FGAP v1 this works because the manage role implies view. In v2 the scopes are independent, so manage alone isn't enough to pass the list/view checks. This affects any code path that checks canView() or canList() before performing a manage operation.
The list check exists to prevent probing for client IDs. If a user can only view specific clients (say client A), they could guess other IDs and distinguish 403 (exists) from 404 (doesn't) to discover other clients. This is a valid concern for per-client FGAP scenarios. On create it doesn't apply - creating a client always requires realm-level manage, not a per-client permission. A user who can't create gets 403 regardless of whether the client exists or not, so there's no information leak. And if they can create, they get 201 or 409 (conflict) - which already reveals existence anyway.
In Admin API v1 this was never an issue - the phishing check only ran on single-client lookups (GET/DELETE) at the REST layer, and the manage role implied view. In v2 the check moved to the service layer where it also ran on create (PR #47029 removed it from POST as a workaround) and FGAP v2 scopes are independent - both changes together surfaced this.
Keeping manage and view independent also doesn't hold up in practice - POST, PUT and PATCH all return the full client representation. A user with manage can send a minimal update and get back all fields, effectively gaining view through any write.
The manage scope has never implied view in FGAP v2 since it was introduced. Thus, I think we can label this back as a bug.
Activity
pedroigor commentedon Apr 7, 2026
@Pepo48 AFAIK, it is not a bug but an expected behavior. See https://www.keycloak.org/docs/latest/server_admin/#understanding-the-scopes-of-access.
c/c @vramik
keycloak-github-bot commentedon Apr 7, 2026
Thanks for reporting this issue, but there is insufficient information or lack of steps to reproduce.
Please provide additional details, otherwise this issue will be automatically closed within 14 days.
shawkins commentedon Apr 8, 2026
The 403 @Pepo48 is highlighting is from an anti-phishing check. We're causing some new behaviors because that check was added to a service layer, rather than the rest layer. To be consistent with client admin api v1 we only need to apply the check when obtaining the client resource. Correcting that will cause the 403 on a POST operation to go away.
With that resolved, someone with manage only would still get a 403 on a PUT of a non-existant client - with view they would get a 404 instead. But this doesn't seem like a strong check - if the manage only user were then to attempt to do a POST they would get a 409 conflict if the resource already exists.
A bigger concern is that having view be independent of manage does not follow well with what we're implementing with admin api v2 - that is we're returning the full resource from PUT / POST / PATCH requests - so sending a minimiltic patch effectively gives you view ability.
To confirm, what should someone having solely manage (without view) be able to do? It appears with admin api v1 they can set the state via POST/create PUT/update, but not see the actual state - are there server managed / generated fields they cannot have access to?
cc @vmuzikar
vramik commentedon Apr 8, 2026
In FGAP v1, transitive relationships between scopes (e.g.,
manageimplyingmap-roles) caused confusion — it was unclear what granting a scope actually meant in terms of other scopes being implicitly granted. For v2, all scopes were made independent to keep the permission model predictable and easy to reason about, as Pedro pointed out the docs.I agree this causes a practical paradox, where it makes no sense to have
managewithoutview.To address this we could change the behavior that:
manageimpliesviewThis would be a narrow fix targeting the most obvious case. However, the same problem exists for other scopes —
map-roleswithoutviewetc. Treatingmanageimpliesviewas a special case doesn't address these, and introducing selective transitive dependencies risks bringing back v1-style confusion about which scopes imply what.any permission on a resource makes it discoverable
Scopes remain fully independent for their operations (granting
managestill does not grantmap-roles, etc.), but having any FGAP permission on a resource — regardless of scope — makes that resource visible in queries. The specific scope is then enforced when the admin actually performs the operation.This avoids transitive scope dependencies while addressing all "write-blind" scenarios uniformly.
viewas a mandatory default scopeRather than changing evaluation semantics, we could enforce that
viewis always included when creating a permission. The minimal permission would be "view-only", and admins add other scopes on top. Sinceviewis always present, the existing partial evaluation logic (which filters byview(andview-membersfor groups)) works without changes.But that would need to be enforced both in admin UI as well as endpoints. And we'd need to be aware of negative permission scenarios, which makes this option a bit too much complex.
To me, right now the best approach seems to be option 2. We also need to consider backward compatibility as this would be a behavioral change. Thoughts?
vramik commentedon Apr 8, 2026
As the described behavior was a design choice, I'm changing the issue to
enhancement.pedroigor commentedon Apr 8, 2026
@vramik But how 2 will play out with partial evaluation ? Or it won't apply when querying resources?
vramik commentedon Apr 8, 2026
@pedroigor today when using partial evaluator, we are querying for all permissions of a given resource type that references an admin user (via user, group or role policy) and filters by
viewscope (orview-membersfor groups).Then it for each returned permission, we evaluate its associated policies and create
allowedResourcesanddeniedResources(denywins).If the resource type has a group relationship (Users -> Groups), it runs a second runEvaluation for the group resource type, filtering by
view-members. It producesallowedGroupsanddeniedGroupssets, which are attached to the context.So to change it to option 2, we'd need to fetch all permissions, but without the
viewfilter, so that we'd get permissions with any scope. We'd need to consider users-groups permissions, likemanage-members.Then when evaluating and creating
allowedResourcesanddeniedResourceswe'd have to add scope awareness to "deny" branch: we'd only add to deniedResources when the permission denies toview(view-membersrespectively).This way we'd get all permissions that references the admin user and filter out those resources which specifically denies viewing, right?
Pepo48 commentedon Apr 14, 2026
Just to clarify on a specific example - a user has FGAP manage permission on all clients but not view. In FGAP v1 this works because the manage role implies view. In v2 the scopes are independent, so manage alone isn't enough to pass the list/view checks. This affects any code path that checks
canView()orcanList()before performing a manage operation.The list check exists to prevent probing for client IDs. If a user can only view specific clients (say client A), they could guess other IDs and distinguish 403 (exists) from 404 (doesn't) to discover other clients. This is a valid concern for per-client FGAP scenarios. On create it doesn't apply - creating a client always requires realm-level manage, not a per-client permission. A user who can't create gets 403 regardless of whether the client exists or not, so there's no information leak. And if they can create, they get 201 or 409 (conflict) - which already reveals existence anyway.
In Admin API v1 this was never an issue - the phishing check only ran on single-client lookups (GET/DELETE) at the REST layer, and the manage role implied view. In v2 the check moved to the service layer where it also ran on create (PR #47029 removed it from POST as a workaround) and FGAP v2 scopes are independent - both changes together surfaced this.
Keeping manage and view independent also doesn't hold up in practice - POST, PUT and PATCH all return the full client representation. A user with manage can send a minimal update and get back all fields, effectively gaining view through any write.
The manage scope has never implied view in FGAP v2 since it was introduced. Thus, I think we can label this back as a bug.