Only realm admins can manage workflows
Some checks failed
Weblate Sync / Trigger Weblate to pull the latest changes (push) Has been cancelled

Closes #45875

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2026-01-30 17:18:06 -03:00 committed by GitHub
parent 2dab08d5ed
commit 13cf35ded3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 124 additions and 51 deletions

View file

@ -18,6 +18,7 @@ to realm administrators but doing so will skip the mechanisms provided by this f
Enforcing access to realm resources only applies when managing resources through the administration console or the Admin API.
[[_understanding_different_types_realm_admins_]]
==== Understanding the different types of realm administrators
There are three different types of realm administrators when managing a realm:
@ -26,7 +27,7 @@ There are three different types of realm administrators when managing a realm:
* **Realm administrators**: These are users (or service accounts) that have been granted with the `realm-admin` role in a specific realm.
* **Delegated realm administrators**: These are users (or service accounts) that do not have the `realm-admin` role but have been granted access to manage
* **Delegated realm administrators**: These are users (or service accounts) other than the types above but granted access to manage
a realm through the fine-grained admin permissions feature.
Both server and realm administrators can manage a realm with full access to all their resources and

View file

@ -6,6 +6,9 @@
Workflows can be managed through the Admin Console or the Admin REST API.
Only realm administrators with the appropriate permissions can manage workflows as they are considered sensitive operations.
For more details, see <<_understanding_different_types_realm_admins_,Understanding different types of Realm Admins>>.
== Managing workflows through the Admin Console
To manage workflows through the Admin Console, you can follow these steps:

View file

@ -27,6 +27,7 @@ public interface AdminPermissionEvaluator {
void requireAnyAdminRole();
boolean hasOneAdminRole(String... adminRoles);
void requireRealmAdmin();
AdminAuth adminAuth();
@ -35,6 +36,8 @@ public interface AdminPermissionEvaluator {
ClientPermissionEvaluator clients();
GroupPermissionEvaluator groups();
boolean isRealmAdmin();
/**
* Useful as a function pointer, i.e. RoleMapperResource is reused bewteen GroupResource and UserResource to manage role mappings.
* We don't know what type of resource we're managing here (user or group), so we don't know how to query the policy engine to determine

View file

@ -42,6 +42,7 @@ import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.authorization.Permission;
@ -140,6 +141,14 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
}
}
@Override
public void requireRealmAdmin() {
if (isRealmAdmin()) {
return;
}
throw new ForbiddenException();
}
public boolean hasAnyAdminRole() {
return hasOneAdminRole(AdminRoles.ALL_REALM_ROLES);
}
@ -392,7 +401,42 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
}
}
@Override
public boolean isRealmAdmin() {
RealmModel masterRealm = getMasterRealm();
UserModel admin = admin();
RoleModel masterAdminRole = masterRealm.getRole(AdminRoles.ADMIN);
if (admin.hasRole(masterAdminRole)) {
// server admin
return true;
}
ClientModel realmManagementClient = getRealmManagementClient();
if (realmManagementClient != null && !realmManagementClient.getRealm().equals(masterRealm)) {
RoleModel realmAdminRole = realmManagementClient.getRole(AdminRoles.REALM_ADMIN);
if (realmAdminRole != null && admin.hasRole(realmAdminRole)) {
// realm admin
return true;
}
}
return false;
}
RealmModel getMasterRealm() {
return adminsRealm().getName().equals(Config.getAdminRealm()) ?
adminsRealm():
session.realms().getRealmByName(Config.getAdminRealm());
}
ClientModel getRealmManagementClient() {
if (realm.getName().equals(Config.getAdminRealm())) {
return realm.getClientByClientId(Config.getAdminRealm() + "-realm");
} else {
return realm.getClientByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID);
}
}
}

View file

@ -36,7 +36,6 @@ import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
@ -155,7 +154,7 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme
if (AdminRoles.ALL_ROLES.contains(role.getName())) {
if (root.admin().hasRole(role)) return true;
ClientModel adminClient = getRealmManagementClient();
ClientModel adminClient = root.getRealmManagementClient();
// is this an admin role in 'realm-management' client of the realm we are managing?
if (adminClient.equals(role.getContainer())) {
// if this is realm admin role, then check to see if admin has similar permissions
@ -669,13 +668,4 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme
}
return resourceServer;
}
protected ClientModel getRealmManagementClient() {
if (realm.getName().equals(Config.getAdminRealm())) {
return realm.getClientByClientId(Config.getAdminRealm() + "-realm");
} else {
return realm.getClientByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID);
}
}
}

View file

@ -32,7 +32,6 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.resources.admin.fgap.ModelRecord.RoleModelRecord;
import static org.keycloak.authorization.fgap.AdminPermissionsSchema.ROLES_RESOURCE_TYPE;
@ -46,32 +45,12 @@ class RolePermissionsV2 extends RolePermissions {
this.eval = new FineGrainedAdminPermissionEvaluator(session, root, resourceStore, policyStore);
}
private boolean isRealmAdmin() {
RealmModel masterRealm = getMasterRealm();
UserModel admin = root.admin();
RoleModel masterAdminRole = masterRealm.getRole(AdminRoles.ADMIN);
if (admin.hasRole(masterAdminRole)) {
return true;
}
ClientModel realmManagementClient = getRealmManagementClient();
if (realmManagementClient != null) {
RoleModel realmAdminRole = realmManagementClient.getRole(AdminRoles.REALM_ADMIN);
return realmAdminRole != null && admin.hasRole(realmAdminRole);
}
return false;
}
@Override
public boolean canMapRole(RoleModel role) {
if (isRealmAdminRole(role)) {
if (realm.isAdminPermissionsEnabled()) {
// only server or realm admins can map roles if FGAP is enabled
return isRealmAdmin();
return root.isRealmAdmin();
}
// otherwise, check if the user is granted with manage-users and is granted with the role being granted
return root.hasOneAdminRole(AdminRoles.MANAGE_USERS) && checkAdminRoles(role);
@ -96,7 +75,7 @@ class RolePermissionsV2 extends RolePermissions {
if (isRealmAdminRole(role)) {
if (realm.isAdminPermissionsEnabled()) {
// only server or realm admins can map roles if FGAP is enabled
return isRealmAdmin();
return root.isRealmAdmin();
}
// otherwise, check if the user is granted with manage-realm or manage-client roles and is granted with the role being granted
return canManageDefault(role) && checkAdminRoles(role);
@ -203,11 +182,11 @@ class RolePermissionsV2 extends RolePermissions {
private boolean isRealmAdminRole(RoleModel role) {
RoleContainerModel container = role.getContainer();
boolean isMasterRealmRole = container.equals(getMasterRealm());
boolean isMasterRealmRole = container.equals(root.getMasterRealm());
boolean isMasterRealmManagementAdminRole = (container instanceof ClientModel c)
&& c.getRealm().getName().equals(Config.getAdminRealm())
&& c.getClientId().endsWith("-realm");
boolean isRealmManagementAdminRole = container.equals(getRealmManagementClient());
boolean isRealmManagementAdminRole = container.equals(root.getRealmManagementClient());
if (isMasterRealmRole|| isRealmManagementAdminRole || isMasterRealmManagementAdminRole) {
return AdminRoles.ALL_ROLES.contains(role.getName());
@ -215,10 +194,4 @@ class RolePermissionsV2 extends RolePermissions {
return false;
}
private RealmModel getMasterRealm() {
return root.adminsRealm().getName().equals(Config.getAdminRealm()) ?
root.adminsRealm():
session.realms().getRealmByName(Config.getAdminRealm());
}
}

View file

@ -47,6 +47,7 @@ public class WorkflowsResource {
if (!Profile.isFeatureEnabled(Feature.WORKFLOWS)) {
throw new NotFoundException();
}
auth.requireRealmAdmin();
this.session = session;
this.provider = session.getProvider(WorkflowProvider.class);
this.auth = auth;
@ -64,8 +65,6 @@ public class WorkflowsResource {
@APIResponse(responseCode = "400", description = "Bad Request")
})
public Response create(WorkflowRepresentation rep) {
auth.realm().requireManageRealm();
try {
Workflow workflow = provider.toModel(rep);
return Response.created(session.getContext().getUri().getRequestUriBuilder().path(workflow.getId()).build()).build();
@ -88,8 +87,6 @@ public class WorkflowsResource {
@Parameter(description = "Workflow identifier")
@PathParam("id") String id
) {
auth.realm().requireManageRealm();
Workflow workflow = provider.getWorkflow(id);
if (workflow == null) {
@ -120,8 +117,6 @@ public class WorkflowsResource {
@Parameter(description = "The maximum number of results to be returned - defaults to 10")
@QueryParam("max") @DefaultValue("10") Integer maxResults
) {
auth.realm().requireManageRealm();
int first = Optional.ofNullable(firstResult).orElse(0);
int max = Optional.ofNullable(maxResults).orElse(10);
return provider.getWorkflows(search, exact, first, max).map(provider::toRepresentation).toList();
@ -143,7 +138,6 @@ public class WorkflowsResource {
@Parameter(description = "Identifier of the resource associated with the scheduled workflows")
@PathParam("resource-id") String resourceId
) {
auth.realm().requireManageRealm();
return provider.getScheduledWorkflowsByResource(resourceId).toList();
}

View file

@ -20,8 +20,10 @@ package org.keycloak.tests.admin.authz.fgap;
import java.util.List;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.core.Response;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.common.Profile.Feature;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.Constants;
import org.keycloak.representations.idm.ClientRepresentation;
@ -32,6 +34,11 @@ import org.keycloak.testframework.admin.AdminClientFactory;
import org.keycloak.testframework.annotations.InjectAdminClient;
import org.keycloak.testframework.annotations.InjectAdminClientFactory;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.realm.UserConfigBuilder;
import org.keycloak.testframework.server.KeycloakServerConfig;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
import org.keycloak.testframework.util.ApiUtil;
import org.keycloak.tests.admin.authz.fgap.RealmAdminAccessTest.ServerConfig;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@ -43,7 +50,7 @@ import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.fail;
@KeycloakIntegrationTest
@KeycloakIntegrationTest(config = ServerConfig.class)
public class RealmAdminAccessTest extends AbstractPermissionTest {
@InjectAdminClient(mode = InjectAdminClient.Mode.MANAGED_REALM, client = "myclient", user = "myadmin")
@ -110,9 +117,67 @@ public class RealmAdminAccessTest extends AbstractPermissionTest {
fail("Should not have access to other realm");
} catch (ForbiddenException ignore) {
}
assertWorkflowAccess(client);
} finally {
client.realm(myrealm.getRealm()).remove();
}
}
}
private void assertWorkflowAccess(Keycloak serverAdminClient) {
// server admin can access workflows
serverAdminClient.realm(realm.getName()).workflows().list();
UserRepresentation myadmin = realm.admin().users().search("myadmin").get(0);
ClientRepresentation realmManagement = realm.admin().clients().findByClientId("realm-management").get(0);
RoleRepresentation realmAdminRole = realm.admin().clients().get(realmManagement.getId()).roles().get(AdminRoles.REALM_ADMIN).toRepresentation();
// can access workflows with realm-admin role
realm.admin().users().get(myadmin.getId()).roles().clientLevel(realmManagement.getId()).add(List.of(realmAdminRole));
realmAdminClient.realm(realm.getName()).workflows().list();
// cannot access workflows without realm-admin role
realm.admin().users().get(myadmin.getId()).roles().clientLevel(realmManagement.getId()).remove(List.of(realmAdminRole));
try {
realmAdminClient.realm(realm.getName()).workflows().list();
fail("Should not have access to workflows");
} catch (ForbiddenException ignore) {
}
UserRepresentation masterUserRealmAdmin = UserConfigBuilder.create()
.username("mymasteradmin")
.password("password")
.firstName("f")
.lastName("l")
.email("mymasteradmin@keycloak.org")
.build();
try (Response response = serverAdminClient.realm("master").users().create(masterUserRealmAdmin)) {
masterUserRealmAdmin.setId(ApiUtil.getCreatedId(response));
}
ClientRepresentation myRealmMasterClient = serverAdminClient.realm("master").clients().findByClientId(realm.getName() + "-realm").get(0);
RoleRepresentation masterRealmAdminRole = serverAdminClient.realm("master").clients().get(myRealmMasterClient.getId())
.roles().get(AdminRoles.MANAGE_REALM).toRepresentation();
serverAdminClient.realm("master").users().get(masterUserRealmAdmin.getId())
.roles().clientLevel(myRealmMasterClient.getId()).add(List.of(masterRealmAdminRole));
try (Keycloak masterRealmAdminClient = adminClientFactory.create().realm("master")
.username("mymasteradmin").password("password").clientId(Constants.ADMIN_CLI_CLIENT_ID).build()) {
// can not access workflows with manage-realm role in master realm
try {
masterRealmAdminClient.realm(realm.getName()).workflows().list();
fail("Should not have access to manage workflows if user is master realm admin with manage-realm role in a realm");
} catch (ForbiddenException ignore) {}
}
}
public static class ServerConfig implements KeycloakServerConfig {
@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return config.features(Feature.WORKFLOWS);
}
}
}