Expose system-info information in the serverinfo endpoint only for users in the admin realm

Closes #42828


(cherry picked from commit 1d28c0cd35a186551cf4114cbd6cdf75b9e3fe58)

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
Ricardo Martin 2025-09-29 18:21:50 +02:00 committed by GitHub
parent 27121d010c
commit 69685b54f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 88 additions and 14 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -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);