diff --git a/core/src/main/java/org/keycloak/representations/info/SystemInfoRepresentation.java b/core/src/main/java/org/keycloak/representations/info/SystemInfoRepresentation.java index 09483438916..23eb5ae176d 100755 --- a/core/src/main/java/org/keycloak/representations/info/SystemInfoRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/info/SystemInfoRepresentation.java @@ -26,7 +26,7 @@ public class SystemInfoRepresentation { private String version; private String serverTime; private String uptime; - private long uptimeMillis; + private Long uptimeMillis; private String javaVersion; private String javaVendor; private String javaVm; @@ -91,11 +91,11 @@ public class SystemInfoRepresentation { this.uptime = uptime; } - public long getUptimeMillis() { + public Long getUptimeMillis() { return uptimeMillis; } - public void setUptimeMillis(long uptimeMillis) { + public void setUptimeMillis(Long uptimeMillis) { this.uptimeMillis = uptimeMillis; } diff --git a/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc index 7e1c1559871..4308da0ebd3 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc @@ -99,6 +99,12 @@ They should avoid keeping the returned items in memory by for example binding th A similar change was done for the `UserSessionPersisterProvider`. +== More changes for the `server-info` system information + +In version 26.4.0, the `server-info` endpoint changed to just return the system information for administrators in the admin realm. Nevertheless, the version property was detected to be needed by some products that interact with {project_name}. Now that property is included for administrators in the realm with permission `manage-realm`. + +The workaround of the `view-system` permission is more restricted too. It can only be assigned by administrators in the master realm using link:{adminguide_link}#_fine_grained_permissions[FGAP]. This permission will be deleted in a future version. + // ------------------------ Deprecated features ------------------------ // == Deprecated features 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 b4c63686bbd..1daf51724b7 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 @@ -69,5 +69,6 @@ public class AdminRoles { ALL_ROLES.add(ADMIN); ALL_ROLES.add(CREATE_REALM); ALL_ROLES.add(REALM_ADMIN); + ALL_ROLES.add(VIEW_SYSTEM); } } 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 7e40666e748..0a0c349c85d 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,6 +48,9 @@ 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(); @@ -73,6 +76,9 @@ class RolePermissionsV2 extends RolePermissions { @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 f232c0e3733..c2fa09854b0 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 @@ -84,6 +84,7 @@ 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.fgap.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.fgap.AdminPermissions; import org.keycloak.theme.Theme; @@ -121,12 +122,17 @@ public class ServerInfoAdminResource { public ServerInfoRepresentation getInfo() { ServerInfoRepresentation info = new ServerInfoRepresentation(); RealmModel userRealm = session.getContext().getRealm(); - if (RealmManager.isAdministrationRealm(userRealm) - || AdminPermissions.evaluator(session, userRealm, auth).hasOneAdminRole(AdminRoles.VIEW_SYSTEM)) { + 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); } info.setProfileInfo(createProfileInfo()); info.setFeatures(createFeatureRepresentations()); diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/PermissionsTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/PermissionsTest.java index 53b3f12eeaf..680102860b0 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/PermissionsTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/PermissionsTest.java @@ -17,18 +17,13 @@ package org.keycloak.tests.admin; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.core.Response; -import org.keycloak.admin.client.Keycloak; -import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.RealmResource; -import org.keycloak.admin.client.resource.UserResource; import org.keycloak.models.AdminRoles; import org.keycloak.models.Constants; import org.keycloak.models.UserModel; @@ -47,7 +42,6 @@ import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.RealmEventsConfigRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.representations.info.ServerInfoRepresentation; import org.keycloak.services.resources.admin.AdminAuth.Resource; import org.keycloak.testframework.annotations.InjectRealm; import org.keycloak.testframework.annotations.KeycloakIntegrationTest; @@ -62,7 +56,6 @@ import org.keycloak.testsuite.util.IdentityProviderBuilder; import org.hamcrest.Matchers; import org.jgroups.util.UUID; -import org.junit.Assert; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; @@ -556,51 +549,6 @@ public class PermissionsTest extends AbstractPermissionsTest { invoke(realm -> realm.localization().deleteRealmLocalizationTexts("en"), 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.getCpuInfo()); - 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.getCpuInfo()); - Assert.assertNull(serverInfo.getMemoryInfo()); - serverInfo = clients.get("manage-users").serverInfo().getInfo(); - Assert.assertNull(serverInfo.getSystemInfo()); - 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 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 = 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.getCpuInfo()); - 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); 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 new file mode 100644 index 00000000000..1b0e7dfad35 --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/admin/ServerInfoPermissionsTest.java @@ -0,0 +1,120 @@ +/* + * 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; + +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; +import org.keycloak.testframework.realm.ManagedRealm; +import org.keycloak.testframework.realm.RealmConfigBuilder; + +import org.junit.Assert; +import org.junit.jupiter.api.Test; + +/** + * + * @author rmartinc + */ +@KeycloakIntegrationTest +public class ServerInfoPermissionsTest extends AbstractPermissionsTest { + + @InjectRealm(config = PermissionsTestRealm.class, ref = "realm1") + ManagedRealm managedRealm1; + + @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.getSystemInfo().getJavaVersion()); + Assert.assertNotNull(serverInfo.getCpuInfo()); + 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.getCpuInfo()); + Assert.assertNull(serverInfo.getMemoryInfo()); + serverInfo = clients.get("manage-users").serverInfo().getInfo(); + Assert.assertNull(serverInfo.getSystemInfo()); + Assert.assertNull(serverInfo.getCpuInfo()); + Assert.assertNull(serverInfo.getMemoryInfo()); + // user with manage realm can only see the version + serverInfo = clients.get("manage-realm").serverInfo().getInfo(); + Assert.assertNotNull(serverInfo.getSystemInfo()); + Assert.assertNotNull(serverInfo.getSystemInfo().getVersion()); + Assert.assertNull(serverInfo.getSystemInfo().getJavaVersion()); + Assert.assertNull(serverInfo.getSystemInfo().getOsName()); + 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 { + + @Override + public RealmConfigBuilder configure(RealmConfigBuilder realm) { + // configure with permissions enable to test view-system assignment + return super.configure(realm).adminPermissionsEnabled(true); + } + } +}