From 73dead10c5a281ed9808288ac156a56871d0a63c Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Wed, 10 Jun 2026 02:50:08 -0300 Subject: [PATCH 1/2] Do not allow updates on the realm-management client Closes #49427 Signed-off-by: Pedro Igor --- .../topics/changes/changes-26_6_5.adoc | 1 + .../java/org/keycloak/models/AdminRoles.java | 3 - .../admin/RoleContainerResource.java | 2 + .../admin/fgap/ClientPermissions.java | 19 +++ .../admin/fgap/ClientPermissionsV2.java | 3 + .../resources/admin/fgap/RolePermissions.java | 6 +- .../admin/fgap/RolePermissionsV2.java | 6 - .../admin/info/ServerInfoAdminResource.java | 24 ++-- .../tests/admin/AbstractPermissionsTest.java | 17 +++ .../org/keycloak/tests/admin/ClientTest.java | 30 ++-- .../admin/ServerInfoPermissionsTest.java | 56 +++----- .../rbac/InternalClientManagementTest.java | 132 ++++++++++++++++++ 12 files changed, 231 insertions(+), 68 deletions(-) create mode 100644 tests/base/src/test/java/org/keycloak/tests/admin/authz/rbac/InternalClientManagementTest.java diff --git a/docs/documentation/upgrading/topics/changes/changes-26_6_5.adoc b/docs/documentation/upgrading/topics/changes/changes-26_6_5.adoc index b121552ef765..bffcaf7b8919 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_6_5.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_6_5.adoc @@ -50,3 +50,4 @@ The following sections provide details on deprecated features. The following features have been removed from this release. === + diff --git a/server-spi-private/src/main/java/org/keycloak/models/AdminRoles.java b/server-spi-private/src/main/java/org/keycloak/models/AdminRoles.java index 1cae6fdd1b8b..60b5c0b92351 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/AdminRoles.java +++ b/server-spi-private/src/main/java/org/keycloak/models/AdminRoles.java @@ -44,8 +44,6 @@ public class AdminRoles { public static final String VIEW_EVENTS = "view-events"; public static final String VIEW_IDENTITY_PROVIDERS = "view-identity-providers"; public static final String VIEW_AUTHORIZATION = "view-authorization"; - @Deprecated(since = "26.4", forRemoval = true) - public static final String VIEW_SYSTEM = "view-system"; public static final String MANAGE_REALM = "manage-realm"; public static final String MANAGE_USERS = "manage-users"; @@ -71,7 +69,6 @@ public class AdminRoles { ALL_ROLES.add(ADMIN); ALL_ROLES.add(CREATE_REALM); ALL_ROLES.add(REALM_ADMIN); - ALL_ROLES.add(VIEW_SYSTEM); } public static boolean isAdminRole(RoleModel role) { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java index a23c311a9a52..ae9989686e0c 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java @@ -282,6 +282,7 @@ public void deleteRole(final @Parameter(description = "role's name (not id!)") @ throw ErrorResponse.error(roleName + " is default role of the realm and cannot be removed.", Response.Status.BAD_REQUEST); } + auth.roles().requireManage(role); RoleRepresentation roleRepresentation = new RoleRepresentation(); roleRepresentation.setId(role.getId()); roleRepresentation.setName(role.getName()); @@ -322,6 +323,7 @@ public Response updateRole(final @Parameter(description = "role's name (not id!) if (role == null) { throw new NotFoundException("Could not find role"); } + auth.roles().requireManage(role); try { updateRole(rep, role, realm, session); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/fgap/ClientPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/fgap/ClientPermissions.java index 874babe8cd16..466080e55983 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/fgap/ClientPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/fgap/ClientPermissions.java @@ -27,6 +27,7 @@ import jakarta.ws.rs.ForbiddenException; +import org.keycloak.Config; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.common.ClientModelIdentity; import org.keycloak.authorization.common.DefaultEvaluationContext; @@ -42,6 +43,7 @@ import org.keycloak.models.AdminRoles; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; +import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.representations.AccessToken; @@ -383,6 +385,9 @@ public Map> getBaseAttributes() { @Override public boolean canManage(ClientModel client) { + if (isInternal(client)) { + return false; + } if (canManageClientsDefault()) return true; if (!root.isAdminSameRealm()) { return false; @@ -411,6 +416,9 @@ public boolean canManage(ClientModel client) { @Override public boolean canConfigure(ClientModel client) { + if (isInternal(client)) { + return false; + } if (canManage(client)) return true; if (!root.isAdminSameRealm()) { return false; @@ -708,4 +716,15 @@ private boolean hasPermission(Resource resource, String scope) { return false; } + protected boolean isInternal(ClientModel client) { + if (client == null) { + return false; + } + + if (realm.getName().equals(Config.getAdminRealm())) { + return client.getClientId().endsWith(AdminRoles.APP_SUFFIX); + } + + return Constants.REALM_MANAGEMENT_CLIENT_ID.equals(client.getClientId()); + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/fgap/ClientPermissionsV2.java b/services/src/main/java/org/keycloak/services/resources/admin/fgap/ClientPermissionsV2.java index 3e720273ef2a..2e9aa4bcc5bb 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/fgap/ClientPermissionsV2.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/fgap/ClientPermissionsV2.java @@ -61,6 +61,9 @@ public boolean canConfigure(ClientModel client) { @Override public boolean canManage(ClientModel client) { + if (isInternal(client)) { + return false; + } if (root.hasOneAdminRole(AdminRoles.MANAGE_CLIENTS)) { return true; } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/fgap/RolePermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/fgap/RolePermissions.java index 8dfc90959ce1..1cb25b0beb8e 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/fgap/RolePermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/fgap/RolePermissions.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; @@ -470,7 +471,7 @@ public void requireMapClientScope(RoleModel role) { @Override public boolean canManage(RoleModel role) { if (role.getContainer() instanceof RealmModel) { - return root.realm().canManageRealm(); + return root.realm().canManageRealm() && !isRealmAdminRole(role); } else if (role.getContainer() instanceof ClientModel) { ClientModel client = (ClientModel)role.getContainer(); return root.clients().canConfigure(client); @@ -668,4 +669,7 @@ private ResourceServer getResourceServer(RoleModel role) { } return resourceServer; } + private boolean isRealmAdminRole(RoleModel role) { + return role.getContainer() instanceof RealmModel && List.of(AdminRoles.ADMIN, AdminRoles.CREATE_REALM).contains(role.getName()); + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/fgap/RolePermissionsV2.java b/services/src/main/java/org/keycloak/services/resources/admin/fgap/RolePermissionsV2.java index 0a0c349c85d9..7e40666e7482 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/fgap/RolePermissionsV2.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/fgap/RolePermissionsV2.java @@ -48,9 +48,6 @@ class RolePermissionsV2 extends RolePermissions { @Override public boolean canMapRole(RoleModel role) { if (isRealmAdminRole(role)) { - if (AdminRoles.VIEW_SYSTEM.equals(role.getName()) && !root.isAdmin(root.getMasterRealm())) { - return false; - } if (realm.isAdminPermissionsEnabled()) { // only server or realm admins can map roles if FGAP is enabled return root.isRealmAdmin(); @@ -76,9 +73,6 @@ public boolean canMapRole(RoleModel role) { @Override public boolean canMapComposite(RoleModel role) { if (isRealmAdminRole(role)) { - if (AdminRoles.VIEW_SYSTEM.equals(role.getName()) && !root.isAdmin(root.getMasterRealm())) { - return false; - } if (realm.isAdminPermissionsEnabled()) { // only server or realm admins can map roles if FGAP is enabled return root.isRealmAdmin(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java index e90e19164fa9..3fd67f7e889c 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java @@ -51,7 +51,6 @@ import org.keycloak.events.EventType; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; -import org.keycloak.models.AdminRoles; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -126,17 +125,20 @@ public ServerInfoRepresentation getInfo() { ServerInfoRepresentation info = new ServerInfoRepresentation(); RealmModel userRealm = session.getContext().getRealm(); AdminPermissionEvaluator adminEvaluator = AdminPermissions.evaluator(session, userRealm, auth); - if (RealmManager.isAdministrationRealm(userRealm) || adminEvaluator.hasOneAdminRole(AdminRoles.VIEW_SYSTEM)) { - // system information is only for admins in the administration realm or fallback view-system role - info.setSystemInfo(SystemInfoRepresentation.create(session.getKeycloakSessionFactory().getServerStartupTimestamp(), Version.VERSION)); - info.setCpuInfo(CpuInfoRepresentation.create()); - info.setMemoryInfo(MemoryInfoRepresentation.create()); - } else if (adminEvaluator.realm().canManageRealm()) { - // If the user can manage his own realm just add the version information - SystemInfoRepresentation systemInfo = new SystemInfoRepresentation(); - systemInfo.setVersion(Version.VERSION); - info.setSystemInfo(systemInfo); + + if (adminEvaluator.realm().canManageRealm()) { + if (RealmManager.isAdministrationRealm(userRealm)) { + info.setSystemInfo(SystemInfoRepresentation.create(session.getKeycloakSessionFactory().getServerStartupTimestamp(), Version.VERSION)); + info.setCpuInfo(CpuInfoRepresentation.create()); + info.setMemoryInfo(MemoryInfoRepresentation.create()); + } else { + // If the user can manage his own realm just add the version information + SystemInfoRepresentation systemInfo = new SystemInfoRepresentation(); + systemInfo.setVersion(Version.VERSION); + info.setSystemInfo(systemInfo); + } } + info.setProfileInfo(createProfileInfo()); info.setFeatures(createFeatureRepresentations()); diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/AbstractPermissionsTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/AbstractPermissionsTest.java index afecbcb5324b..2d789deccc3d 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/AbstractPermissionsTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/AbstractPermissionsTest.java @@ -73,6 +73,19 @@ public void beforeEach() { // todo rewrite managedMasterRealm.admin().users().get(roleUserUuid).roles().clientLevel(clientUuid).add(Collections.singletonList(roleRep)); } + for (String role : AdminRoles.ALL_REALM_ROLES) { + response = managedMasterRealm.admin().users().create(UserConfigBuilder.create() + .username("master-user-" + role) + .password("password") + .build()); + String roleUserUuid = ApiUtil.getCreatedId(response); + managedMasterRealm.cleanup().add(r -> r.users().delete(roleUserUuid).close()); + + String clientUuid = managedMasterRealm.admin().clients().findByClientId("master-realm").get(0).getId(); + RoleRepresentation roleRep = managedMasterRealm.admin().clients().get(clientUuid).roles().get(role).toRepresentation(); + managedMasterRealm.admin().users().get(roleUserUuid).roles().clientLevel(clientUuid).add(Collections.singletonList(roleRep)); + } + clients.put(AdminRoles.REALM_ADMIN, adminClientFactory.create().realm(REALM_NAME).username(AdminRoles.REALM_ADMIN).password("password").clientId("test-client").clientSecret("secret").build()); @@ -97,6 +110,10 @@ public void beforeEach() { // todo rewrite adminClientFactory.create().realm("master").username("permissions-test-master-" + role).password("password").clientId(Constants.ADMIN_CLI_CLIENT_ID).build()); } + for (String role : AdminRoles.ALL_ROLES) { + clients.put("master-admin-" + role, + adminClientFactory.create().realm("master").username("master-user-" + role).password("password").clientId(Constants.ADMIN_CLI_CLIENT_ID).build()); + } } protected void invoke(final Invocation invocation, AdminAuth.Resource resource, boolean manage) { diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/ClientTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/ClientTest.java index a37b089837db..cf31afc31c1e 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/ClientTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/ClientTest.java @@ -29,9 +29,11 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.core.Response; @@ -97,6 +99,7 @@ import static java.util.Arrays.asList; import static org.keycloak.models.Constants.OIDC_PROTOCOL; +import static org.keycloak.models.Constants.REALM_MANAGEMENT_CLIENT_ID; import static org.keycloak.models.Constants.defaultClients; import static org.hamcrest.CoreMatchers.is; @@ -145,7 +148,7 @@ public void getClients() { public void getRealmClients() { assertTrue(managedRealm.admin().clients().findAll().stream().filter(client -> client.getAttributes().get(Constants.REALM_CLIENT).equals("true")) .map(ClientRepresentation::getClientId) - .allMatch(clientId -> clientId.equals(Constants.REALM_MANAGEMENT_CLIENT_ID) || clientId.equals(Constants.BROKER_SERVICE_CLIENT_ID) || clientId.endsWith("-realm"))); + .allMatch(clientId -> clientId.equals(REALM_MANAGEMENT_CLIENT_ID) || clientId.equals(Constants.BROKER_SERVICE_CLIENT_ID) || clientId.endsWith("-realm"))); } private ClientRepresentation createClient() { @@ -569,13 +572,7 @@ public void removeClientWithDependentCompositeRoles() { @Test public void removeInternalClientExpectingBadRequestException() { - final String testRealmClientId = AdminApiUtil.findClientByClientId(managedMasterRealm.admin(), managedRealm.getName() + "-realm") - .toRepresentation().getId(); - - assertThrows(BadRequestException.class, - () -> managedMasterRealm.admin().clients().get(testRealmClientId).remove()); - - defaultClients.forEach(defaultClient -> { + defaultClients.stream().filter(Predicate.not(REALM_MANAGEMENT_CLIENT_ID::equals)).forEach(defaultClient -> { final String defaultClientId = AdminApiUtil.findClientByClientId(managedRealm.admin(), defaultClient) .toRepresentation().getId(); @@ -584,6 +581,23 @@ public void removeInternalClientExpectingBadRequestException() { }); } + @Test + public void removeRealmManagementClientForbiddenException() { + assertThrows(ForbiddenException.class, + () -> { + String testRealmClientId = AdminApiUtil.findClientByClientId(managedMasterRealm.admin(), managedRealm.getName() + "-realm") + .toRepresentation().getId(); + managedMasterRealm.admin().clients().get(testRealmClientId).remove(); + }); + + assertThrows(ForbiddenException.class, + () -> { + String testRealmClientId = AdminApiUtil.findClientByClientId(managedRealm.admin(), REALM_MANAGEMENT_CLIENT_ID) + .toRepresentation().getId(); + managedRealm.admin().clients().get(testRealmClientId).remove(); + }); + } + @Test public void getClientRepresentation() { String id = createClient().getId(); diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/ServerInfoPermissionsTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/ServerInfoPermissionsTest.java index 1b0e7dfad359..c18982801436 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/ServerInfoPermissionsTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/ServerInfoPermissionsTest.java @@ -16,18 +16,11 @@ */ package org.keycloak.tests.admin; -import java.util.Collections; + import jakarta.ws.rs.ForbiddenException; -import org.keycloak.admin.client.Keycloak; -import org.keycloak.admin.client.resource.ClientResource; -import org.keycloak.admin.client.resource.UserResource; import org.keycloak.models.AdminRoles; -import org.keycloak.models.Constants; -import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.representations.idm.RoleRepresentation; -import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.info.ServerInfoRepresentation; import org.keycloak.testframework.annotations.InjectRealm; import org.keycloak.testframework.annotations.KeycloakIntegrationTest; @@ -51,13 +44,27 @@ public class ServerInfoPermissionsTest extends AbstractPermissionsTest { public void testServerInfo() throws Exception { // user in master with no permission => forbidden Assert.assertThrows(ForbiddenException.class, () -> clients.get("master-none").serverInfo().getInfo()); - // user in master with any permission can see the system info - ServerInfoRepresentation serverInfo = clients.get("master-view-realm").serverInfo().getInfo(); + + // user in master with manage-realm permission can see the system info + ServerInfoRepresentation serverInfo = clients.get("master-admin-" + AdminRoles.MANAGE_REALM).serverInfo().getInfo(); + Assert.assertNotNull(serverInfo.getSystemInfo()); + Assert.assertNotNull(serverInfo.getSystemInfo().getJavaVersion()); + Assert.assertNotNull(serverInfo.getCpuInfo()); + Assert.assertNotNull(serverInfo.getMemoryInfo()); + + // server admin user can see the full system info + serverInfo = clients.get("master-admin").serverInfo().getInfo(); Assert.assertNotNull(serverInfo.getSystemInfo()); Assert.assertNotNull(serverInfo.getSystemInfo().getJavaVersion()); Assert.assertNotNull(serverInfo.getCpuInfo()); Assert.assertNotNull(serverInfo.getMemoryInfo()); + // delegated admin in the master realm with view-realm role can not view full server info + serverInfo = clients.get("master-admin-" + AdminRoles.VIEW_REALM).serverInfo().getInfo(); + Assert.assertNull(serverInfo.getSystemInfo()); + Assert.assertNull(serverInfo.getCpuInfo()); + Assert.assertNull(serverInfo.getMemoryInfo()); + // user in test realm with no permission => forbidden Assert.assertThrows(ForbiddenException.class, () -> clients.get("none").serverInfo().getInfo()); // user in test realm with any permission cannot see the system info @@ -78,35 +85,6 @@ public void testServerInfo() throws Exception { Assert.assertNull(serverInfo.getSystemInfo().getServerTime()); Assert.assertNull(serverInfo.getCpuInfo()); Assert.assertNull(serverInfo.getMemoryInfo()); - - // assign the view-system permission to a test realm user and check the fallback works - ClientRepresentation realmMgtRep = adminClient.realm(REALM_NAME).clients().findByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID).get(0); - ClientResource realmMgtRes = adminClient.realm(REALM_NAME).clients().get(realmMgtRep.getId()); - RoleRepresentation createViewSystem = new RoleRepresentation(); - createViewSystem.setName(AdminRoles.VIEW_SYSTEM); - realmMgtRes.roles().create(createViewSystem); - final RoleRepresentation viewSystem = realmMgtRes.roles().get(AdminRoles.VIEW_SYSTEM).toRepresentation(); - UserRepresentation userRep = adminClient.realm(REALM_NAME).users().search("view-realm", Boolean.TRUE).get(0); - // view-system cannot be assigned by admin in the permissions realm - Assert.assertThrows(ForbiddenException.class, () -> clients.get("realm-admin") - .realm(REALM_NAME).users().get(userRep.getId()).roles().clientLevel(realmMgtRep.getId()) - .add(Collections.singletonList(viewSystem))); - // view-system can be assigned by a master realm-admin using FGAP - UserResource userRes = adminClient.realm(REALM_NAME).users().get(userRep.getId()); - userRes.roles().clientLevel(realmMgtRep.getId()) - .add(Collections.singletonList(viewSystem)); - try (Keycloak keycloak = adminClientFactory.create().realm(REALM_NAME) - .username(userRep.getUsername()).password("password").clientId("test-client") - .build()) { - serverInfo = keycloak.serverInfo().getInfo(); - Assert.assertNotNull(serverInfo.getSystemInfo()); - Assert.assertNotNull(serverInfo.getSystemInfo().getJavaVersion()); - Assert.assertNotNull(serverInfo.getCpuInfo()); - Assert.assertNotNull(serverInfo.getMemoryInfo()); - } finally { - userRes.roles().clientLevel(realmMgtRep.getId()).remove(Collections.singletonList(viewSystem)); - realmMgtRes.roles().get(AdminRoles.VIEW_SYSTEM).remove(); - } } protected static class PermissionsTestRealm extends PermissionsTestRealmConfig1 { diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/authz/rbac/InternalClientManagementTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/authz/rbac/InternalClientManagementTest.java new file mode 100644 index 000000000000..1aec5a050c3e --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/admin/authz/rbac/InternalClientManagementTest.java @@ -0,0 +1,132 @@ +/* + * Copyright 2026 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.tests.admin.authz.rbac; + +import java.util.Set; + +import org.keycloak.Config; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.models.AdminRoles; +import org.keycloak.models.Constants; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.testframework.annotations.KeycloakIntegrationTest; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +@KeycloakIntegrationTest +public class InternalClientManagementTest extends AbstractAdminRBACTest { + + private static final String TEMP_ROLE_NAME = "xyzzy-temp-role"; + + @Test + public void testManageClientsAdminCannotRenameRoleInRealmManagementClient() { + createRealm(adminClient, "myrealm"); + + grantMasterRealmManagementRole("myrealm", masterUser.getUsername(), AdminRoles.MANAGE_CLIENTS); + + runAs(masterRealm.getName(), masterUser.getUsername(), client -> { + ClientRepresentation realmMgmt = client.realm("myrealm").clients() + .findByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID).get(0); + ClientResource realmMgmtResource = client.realm("myrealm").clients().get(realmMgmt.getId()); + RoleRepresentation realmAdminRole = realmMgmtResource.roles() + .get(AdminRoles.REALM_ADMIN).toRepresentation(); + + assertForbidden("manage-clients admin must not rename realm-admin in realm-management", + () -> client.realm("myrealm").rolesById().updateRole(realmAdminRole.getId(), renamed(realmAdminRole))); + }); + } + + @Test + public void testMasterRealmManageClientsAdminCannotRenameRoleInRealmAdminContainer() { + createRealm(adminClient, "myrealm"); + + // Grant manage-clients from "master-realm" client — the admin container in master realm + grantMasterRealmManagementRole(Config.getAdminRealm(), masterUser.getUsername(), AdminRoles.MANAGE_CLIENTS); + + runAs(masterRealm.getName(), masterUser.getUsername(), client -> { + // "myrealm-realm" is the admin container for "myrealm", living in the master realm + ClientRepresentation realmAdminContainer = client.realm(masterRealm.getName()).clients() + .findByClientId("myrealm-realm").get(0); + ClientResource realmAdminContainerResource = client.realm(masterRealm.getName()).clients() + .get(realmAdminContainer.getId()); + RoleRepresentation manageRealmRole = realmAdminContainerResource.roles() + .get(AdminRoles.MANAGE_REALM).toRepresentation(); + + assertForbidden("master-realm manage-clients admin must not rename protected admin roles in myrealm-realm", + () -> client.realm(masterRealm.getName()).rolesById() + .updateRole(manageRealmRole.getId(), renamed(manageRealmRole))); + }); + } + + @Test + public void testMasterRealmManageRealmAdminCannotRenameAdminRealmRole() { + // Grant manage-realm from "master-realm" client — the admin container in master realm + grantMasterRealmManagementRole(Config.getAdminRealm(), masterUser.getUsername(), AdminRoles.MANAGE_REALM); + + runAs(masterRealm.getName(), masterUser.getUsername(), client -> { + for (String name : Set.of(AdminRoles.ADMIN, AdminRoles.CREATE_REALM)) { + RoleRepresentation adminRole = client.realm(masterRealm.getName()).roles() + .get(name).toRepresentation(); + + assertForbidden("master-realm manage-realm admin must not rename the '" + name + "' realm role", + () -> client.realm(masterRealm.getName()).rolesById() + .updateRole(adminRole.getId(), renamed(adminRole))); + } + }); + } + + @Test + public void testMasterRealmManageRealmAdminCannotRenameAdminRealmRoleViaNameBasedEndpoint() { + grantMasterRealmManagementRole(Config.getAdminRealm(), masterUser.getUsername(), AdminRoles.MANAGE_REALM); + + runAs(masterRealm.getName(), masterUser.getUsername(), client -> { + for (String name : Set.of(AdminRoles.ADMIN, AdminRoles.CREATE_REALM)) { + RoleRepresentation adminRole = client.realm(masterRealm.getName()).roles() + .get(name).toRepresentation(); + assertForbidden("master-realm manage-realm admin must not rename the '" + name + "' realm role via name-based endpoint", + () -> client.realm(masterRealm.getName()).roles().get(name).update(renamed(adminRole))); + } + }); + } + + @Test + public void testMasterRealmManageRealmAdminCannotDeleteAdminRealmRoleViaNameBasedEndpoint() { + grantMasterRealmManagementRole(Config.getAdminRealm(), masterUser.getUsername(), AdminRoles.MANAGE_REALM); + + runAs(masterRealm.getName(), masterUser.getUsername(), client -> { + for (String name : Set.of(AdminRoles.ADMIN, AdminRoles.CREATE_REALM)) { + assertForbidden("master-realm manage-realm admin must not delete the '" + name + "' realm role via name-based endpoint", + () -> client.realm(masterRealm.getName()).roles().get(name).remove()); + + // Verify the role still exists (it should not have been deleted) + String roleName = client.realm(masterRealm.getName()).roles().get(name).toRepresentation().getName(); + assertNotEquals(null, roleName); + } + }); + } + + private RoleRepresentation renamed(RoleRepresentation original) { + RoleRepresentation renamed = new RoleRepresentation(); + renamed.setId(original.getId()); + renamed.setName(TEMP_ROLE_NAME); + renamed.setDescription(original.getDescription()); + return renamed; + } +} From 38ed33a07a2aa6cf6f9e2dca97bd586c98398760 Mon Sep 17 00:00:00 2001 From: Alexander Schwartz Date: Thu, 6 Aug 2026 18:09:59 +0900 Subject: [PATCH 2/2] Moved to next patch release Signed-off-by: Alexander Schwartz --- .../topics/changes/changes-26_6_5.adoc | 22 ------------------- .../topics/changes/changes-26_6_6.adoc | 18 +++++++++++++++ .../upgrading/topics/changes/changes.adoc | 4 ++++ 3 files changed, 22 insertions(+), 22 deletions(-) create mode 100644 docs/documentation/upgrading/topics/changes/changes-26_6_6.adoc diff --git a/docs/documentation/upgrading/topics/changes/changes-26_6_5.adoc b/docs/documentation/upgrading/topics/changes/changes-26_6_5.adoc index bffcaf7b8919..abcf069c1170 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_6_5.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_6_5.adoc @@ -29,25 +29,3 @@ found, ensure the affected users and service accounts have the required admin ro through group memberships. You can verify this in the Keycloak Admin Console by checking the user's role assignments and group memberships in the target realm. -// ------------------------ Notable changes ------------------------ // -== Notable changes - -Notable changes may include internal behavior changes that prevent common misconfigurations, bugs that are fixed, or changes to simplify running {project_name}. -It also lists significant changes to internal APIs. - -=== - -// ------------------------ Deprecated features ------------------------ // -== Deprecated features - -The following sections provide details on deprecated features. - -=== - -// ------------------------ Removed features ------------------------ // -== Removed features - -The following features have been removed from this release. - -=== - diff --git a/docs/documentation/upgrading/topics/changes/changes-26_6_6.adoc b/docs/documentation/upgrading/topics/changes/changes-26_6_6.adoc new file mode 100644 index 000000000000..b1eb012eecaf --- /dev/null +++ b/docs/documentation/upgrading/topics/changes/changes-26_6_6.adoc @@ -0,0 +1,18 @@ +// ------------------------ Breaking changes ------------------------ // +== Breaking changes + +=== The `view-system` admin role no longer exists + +Since version 26.5.4, when introduced, the `view-system` admin role was marked as deprecated. In this release, the role +has been removed for security reasons. + +Access to full server information is now restricted to users in the `master` realm holding the `manage-realm` role. + +If you are still relying on the `view-system` role, you should consider accessing the full server information using a user +(or service account) from the `master` realm and granted with `manage-realm` admin roles. + +Before upgrading, you should check if any of your users, service accounts, or groups are still relying on the +`view-system` role and review their access controls to ensure they have the necessary permissions to access the server +information after the upgrade. The `manage-realm` admin role is a high-privileged role and should only be granted to +highly-trusted accounts. + diff --git a/docs/documentation/upgrading/topics/changes/changes.adoc b/docs/documentation/upgrading/topics/changes/changes.adoc index ed22c152cfca..600c09088791 100644 --- a/docs/documentation/upgrading/topics/changes/changes.adoc +++ b/docs/documentation/upgrading/topics/changes/changes.adoc @@ -1,6 +1,10 @@ [[migration-changes]] == Migration Changes +=== Migrating to 26.6.6 + +include::changes-26_6_6.adoc[leveloffset=2] + === Migrating to 26.6.5 include::changes-26_6_5.adoc[leveloffset=2]