diff --git a/docs/documentation/upgrading/topics/changes/changes-26_2_10.adoc b/docs/documentation/upgrading/topics/changes/changes-26_2_10.adoc new file mode 100644 index 00000000000..58809d82f46 --- /dev/null +++ b/docs/documentation/upgrading/topics/changes/changes-26_2_10.adoc @@ -0,0 +1,15 @@ +// ------------------------ Notable changes ------------------------ // +== Notable changes + +Notable changes where an internal behavior changed to prevent common misconfigurations, fix bugs or simplify running {project_name}. + +=== The `serverinfo` endpoint only returns the system info for administrators in the administrator realm + +Starting with this version, the `serverinfo` endpoint, which is used by the admin console to obtain some general information of the {project_name} installation, will only return the system information for administrators in the administration (master) realm. This change was done for security reasons. + +If, for whatever reason, an administrator in a common realm needs to access the `systemInfo`, `cpuInfo` or `memoryInfo` fields of the `serverinfo` response, you need to create and assign a new *view-system* role to that admin user: + +. In the affected realm, select the management client *realm-management*, and, in the *Roles* tab, create a new role called *view-system*. +. In *Users* select the administrator account, and, in the *Role mapping* tab, assign the just created *view-system* client role to the admin user. + +The previous workaround is marked as deprecated and it can be removed in a future version of {project_name}. diff --git a/docs/documentation/upgrading/topics/changes/changes.adoc b/docs/documentation/upgrading/topics/changes/changes.adoc index b6910f62f31..0db91204422 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.2.10 + +include::changes-26_2_10.adoc[leveloffset=2] + === Migrating to 26.2.9 include::changes-26_2_9.adoc[leveloffset=2] 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 528b58d5e56..27203e2a3c6 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 @@ -35,12 +35,14 @@ public class AdminRoles { public static String CREATE_REALM = "create-realm"; public static String CREATE_CLIENT = "create-client"; - public static String VIEW_REALM = "view-realm"; - public static String VIEW_USERS = "view-users"; - public static String VIEW_CLIENTS = "view-clients"; - public static String VIEW_EVENTS = "view-events"; - public static String VIEW_IDENTITY_PROVIDERS = "view-identity-providers"; - public static String VIEW_AUTHORIZATION = "view-authorization"; + public static final String VIEW_REALM = "view-realm"; + public static final String VIEW_USERS = "view-users"; + public static final String VIEW_CLIENTS = "view-clients"; + 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.2.9", forRemoval = true) + public static final String VIEW_SYSTEM = "view-system"; public static String MANAGE_REALM = "manage-realm"; public static String MANAGE_USERS = "manage-users"; diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java index c91710ed274..e1030987bc3 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java @@ -291,7 +291,7 @@ public class AdminRoot { Cors.builder().allowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").auth().add(); - return new ServerInfoAdminResource(session); + return new ServerInfoAdminResource(session, auth); } private HttpRequest getHttpRequest() { 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 609aece9aa2..4c3bfd49015 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 @@ -34,7 +34,9 @@ import org.keycloak.crypto.ClientSignatureVerifierProvider; 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.KeycloakSession; +import org.keycloak.models.RealmModel; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.policy.PasswordPolicyProvider; import org.keycloak.policy.PasswordPolicyProviderFactory; @@ -62,7 +64,10 @@ import org.keycloak.representations.info.ServerInfoRepresentation; import org.keycloak.representations.info.SpiInfoRepresentation; import org.keycloak.representations.info.SystemInfoRepresentation; import org.keycloak.representations.info.ThemeInfoRepresentation; +import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.KeycloakOpenAPI; +import org.keycloak.services.resources.admin.AdminAuth; +import org.keycloak.services.resources.admin.permissions.AdminPermissions; import org.keycloak.theme.Theme; import jakarta.ws.rs.GET; @@ -92,9 +97,11 @@ public class ServerInfoAdminResource { private static final Map> ENUMS = createEnumsMap(EventType.class, OperationType.class, ResourceType.class); private final KeycloakSession session; + private final AdminAuth auth; - public ServerInfoAdminResource(KeycloakSession session) { + public ServerInfoAdminResource(KeycloakSession session, AdminAuth auth) { this.session = session; + this.auth = auth; } /** @@ -109,8 +116,13 @@ public class ServerInfoAdminResource { @Operation( summary = "Get themes, social providers, auth providers, and event listeners available on this server") public ServerInfoRepresentation getInfo() { ServerInfoRepresentation info = new ServerInfoRepresentation(); - info.setSystemInfo(SystemInfoRepresentation.create(session.getKeycloakSessionFactory().getServerStartupTimestamp(), Version.VERSION)); - info.setMemoryInfo(MemoryInfoRepresentation.create()); + RealmModel userRealm = session.getContext().getRealm(); + if (RealmManager.isAdministrationRealm(userRealm) + || AdminPermissions.evaluator(session, userRealm, auth).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.setMemoryInfo(MemoryInfoRepresentation.create()); + } info.setProfileInfo(createProfileInfo()); info.setFeatures(createFeatureRepresentations()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java index ae24e0f7a24..56523a3d88b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java @@ -24,13 +24,15 @@ import org.junit.Rule; import org.junit.Test; import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.resource.AuthorizationResource; +import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UserResource; import org.keycloak.common.Profile; import org.keycloak.models.AdminRoles; -import org.keycloak.models.CibaConfig; import org.keycloak.models.Constants; import org.keycloak.models.UserModel; import org.keycloak.models.credential.OTPCredentialModel; +import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.KeyStoreConfig; import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation; import org.keycloak.representations.idm.AuthenticationExecutionRepresentation; @@ -58,11 +60,11 @@ import org.keycloak.representations.idm.authorization.ResourcePermissionRepresen import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; import org.keycloak.representations.idm.authorization.ScopeRepresentation; +import org.keycloak.representations.info.ServerInfoRepresentation; import org.keycloak.services.resources.admin.AdminAuth.Resource; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.ProfileAssume; -import org.keycloak.testsuite.updaters.RealmAttributeUpdater; import org.keycloak.testsuite.util.AdminClientUtil; import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.CredentialBuilder; @@ -71,9 +73,9 @@ import org.keycloak.testsuite.util.GreenMailRule; import org.keycloak.testsuite.util.IdentityProviderBuilder; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.UserBuilder; -import org.keycloak.userprofile.DeclarativeUserProfileProvider; import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.core.Response; import java.lang.reflect.Method; import java.util.Arrays; @@ -93,7 +95,6 @@ import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot; import org.keycloak.testsuite.utils.tls.TLSUtils; import org.jgroups.util.UUID; -import org.keycloak.models.utils.KeycloakModelUtils; /** * @author Stian Thorgersen @@ -1893,6 +1894,46 @@ public class PermissionsTest extends AbstractKeycloakTest { }, clients.get("REALM2"), false); } + @Test + 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(); + Assert.assertNotNull(serverInfo.getSystemInfo()); + Assert.assertNotNull(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 + serverInfo = clients.get("view-realm").serverInfo().getInfo(); + Assert.assertNull(serverInfo.getSystemInfo()); + Assert.assertNull(serverInfo.getMemoryInfo()); + serverInfo = clients.get("manage-users").serverInfo().getInfo(); + Assert.assertNull(serverInfo.getSystemInfo()); + 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 viewSystem = new RoleRepresentation(); + viewSystem.setName(AdminRoles.VIEW_SYSTEM); + realmMgtRes.roles().create(viewSystem); + viewSystem = realmMgtRes.roles().get(AdminRoles.VIEW_SYSTEM).toRepresentation(); + UserRepresentation userRep = adminClient.realm(REALM_NAME).users().search("view-realm", Boolean.TRUE).get(0); + UserResource userRes = adminClient.realm(REALM_NAME).users().get(userRep.getId()); + userRes.roles().clientLevel(realmMgtRep.getId()).add(Collections.singletonList(viewSystem)); + try (Keycloak keycloak = Keycloak.getInstance(getAuthServerContextRoot() + "/auth", REALM_NAME, + userRep.getUsername(), "password", "test-client", TLSUtils.initializeTLS())) { + serverInfo = keycloak.serverInfo().getInfo(); + Assert.assertNotNull(serverInfo.getSystemInfo()); + Assert.assertNotNull(serverInfo.getMemoryInfo()); + } finally { + userRes.roles().clientLevel(realmMgtRep.getId()).remove(Collections.singletonList(viewSystem)); + realmMgtRes.roles().get(AdminRoles.VIEW_SYSTEM).remove(); + } + } + private void verifyAnyAdminRoleReqired(Invocation invocation) { invoke(invocation, clients.get("view-realm"), true); invoke(invocation, clients.get("manage-realm"), true);