Description
ClientPermissions.isInternal denylists only the realm-management client (and, in the master realm, client IDs ending in the -realm suffix). The admin-permissions client — the resource server holding the realm's entire FGAP V2 model — is not in that list.
As a result, a delegated administrator holding a FGAP Clients:manage permission (and no realm-management roles) can update and disable the admin-permissions client through the Admin API. The API's own access report (getAccess) correctly returns manage: false for this client, but the enforcement path in canManage does not agree — it falls through to the FGAP permission check and allows the write.
This is a gap in the isInternal denylist introduced in 26.7.0. No privilege escalation results from this — FGAP enforcement resolves the client by internal ID, not by clientId, and disabling the client does not relax the delegated admin's own restrictions.
Version affected
26.7.0 and main. The isInternal guard was introduced in 26.7.0; 26.6.x has a different code path and is not affected.
Expected behavior
The admin-permissions client should be treated as internal, consistent with realm-management. A delegated admin with Clients:manage should not be able to update or disable it. The enforcement path and the access report should agree.
Actual behavior
isInternal returns false for the admin-permissions client
canManage falls through to the FGAP permission check, which grants access via Clients:manage
getAccess returns manage: false (it special-cases this client), contradicting enforcement
- The delegated admin can set
serviceAccountsEnabled, add redirect URIs, enable directAccessGrantsEnabled, and rename clientId
- Deleting the client is correctly refused (HTTP 400)
Steps to reproduce
- Start Keycloak 26.7.0 with default features (FGAP V2 enabled by default).
- In a realm, enable
adminPermissionsEnabled (FGAP V2 configuration).
- Create a delegated admin user with a FGAP
Clients:manage permission but no realm-management roles.
- As the delegated admin,
GET /admin/realms/{realm}/clients/{admin-permissions-client-id} — note access.manage = false.
- As the delegated admin,
PUT /admin/realms/{realm}/clients/{admin-permissions-client-id} with a modified body (e.g., add a redirect URI) — observe HTTP 204 success.
- As the master admin, verify the change persisted.
Root cause
ClientPermissions.isInternal (line 719) checks only for realm-management and -realm suffix clients. Constants.ADMIN_PERMISSIONS_CLIENT_ID is missing from the predicate. The getAccess method at line 676 correctly special-cases this client, but canManage at line 63 does not mirror the same check.
Suggested fix
Add Constants.ADMIN_PERMISSIONS_CLIENT_ID to ClientPermissions.isInternal. This is the smallest change and matches how realm-management is already handled.
Acknowledgement
Mahdi Alhakim (GitHub: mahdi-al-hakim)
This issue was originally tracked in the private repository. Migrated by @abstractj.
Description
ClientPermissions.isInternaldenylists only therealm-managementclient (and, in the master realm, client IDs ending in the-realmsuffix). Theadmin-permissionsclient — the resource server holding the realm's entire FGAP V2 model — is not in that list.As a result, a delegated administrator holding a FGAP
Clients:managepermission (and no realm-management roles) can update and disable theadmin-permissionsclient through the Admin API. The API's own access report (getAccess) correctly returnsmanage: falsefor this client, but the enforcement path incanManagedoes not agree — it falls through to the FGAP permission check and allows the write.This is a gap in the
isInternaldenylist introduced in 26.7.0. No privilege escalation results from this — FGAP enforcement resolves the client by internal ID, not byclientId, and disabling the client does not relax the delegated admin's own restrictions.Version affected
26.7.0 and main. The
isInternalguard was introduced in 26.7.0; 26.6.x has a different code path and is not affected.Expected behavior
The
admin-permissionsclient should be treated as internal, consistent withrealm-management. A delegated admin withClients:manageshould not be able to update or disable it. The enforcement path and the access report should agree.Actual behavior
isInternalreturnsfalsefor theadmin-permissionsclientcanManagefalls through to the FGAP permission check, which grants access viaClients:managegetAccessreturnsmanage: false(it special-cases this client), contradicting enforcementserviceAccountsEnabled, add redirect URIs, enabledirectAccessGrantsEnabled, and renameclientIdSteps to reproduce
adminPermissionsEnabled(FGAP V2 configuration).Clients:managepermission but no realm-management roles.GET /admin/realms/{realm}/clients/{admin-permissions-client-id}— noteaccess.manage = false.PUT /admin/realms/{realm}/clients/{admin-permissions-client-id}with a modified body (e.g., add a redirect URI) — observe HTTP 204 success.Root cause
ClientPermissions.isInternal(line 719) checks only forrealm-managementand-realmsuffix clients.Constants.ADMIN_PERMISSIONS_CLIENT_IDis missing from the predicate. ThegetAccessmethod at line 676 correctly special-cases this client, butcanManageat line 63 does not mirror the same check.Suggested fix
Add
Constants.ADMIN_PERMISSIONS_CLIENT_IDtoClientPermissions.isInternal. This is the smallest change and matches howrealm-managementis already handled.Acknowledgement
Mahdi Alhakim (GitHub: mahdi-al-hakim)
This issue was originally tracked in the private repository. Migrated by @abstractj.