Include version in system-info for manage-realm and restrict view-system mapping

Closes #45776

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2026-01-29 15:17:41 +01:00 committed by Marek Posolda
parent 14fc381eaa
commit d4e9b16ea9
7 changed files with 144 additions and 57 deletions

View file

@ -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;
}

View file

@ -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

View file

@ -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);
}
}

View file

@ -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();

View file

@ -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());

View file

@ -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);

View file

@ -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);
}
}
}