Adding grant and revoke role steps

Closes #44648

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor
2025-12-10 21:11:07 -03:00
parent 138d1e0588
commit 84a0324d60
8 changed files with 394 additions and 1 deletions

View File

@@ -0,0 +1,23 @@
package org.keycloak.models.workflow;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.jboss.logging.Logger;
public class GrantRoleStepProvider extends RoleBasedStepProvider {
private final Logger log = Logger.getLogger(GrantRoleStepProvider.class);
protected GrantRoleStepProvider(KeycloakSession session, ComponentModel model) {
super(session, model);
}
@Override
protected void run(UserModel user, RoleModel role) {
log.debugv("Granting role %s to user %s)", role.getName(), user.getId());
user.grantRole(role);
}
}

View File

@@ -0,0 +1,54 @@
package org.keycloak.models.workflow;
import java.util.List;
import org.keycloak.Config;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
public class GrantRoleStepProviderFactory implements WorkflowStepProviderFactory<GrantRoleStepProvider> {
public static final String ID = "grant-role";
@Override
public GrantRoleStepProvider create(KeycloakSession session, ComponentModel model) {
return new GrantRoleStepProvider(session, model);
}
@Override
public void init(Config.Scope config) {
// no-op
}
@Override
public void postInit(KeycloakSessionFactory factory) {
// no-op
}
@Override
public void close() {
// no-op
}
@Override
public String getId() {
return ID;
}
@Override
public ResourceType getType() {
return ResourceType.USERS;
}
@Override
public String getHelpText() {
return "Grant a role to a user.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return List.of();
}
}

View File

@@ -0,0 +1,23 @@
package org.keycloak.models.workflow;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.jboss.logging.Logger;
public class RevokeRoleStepProvider extends RoleBasedStepProvider {
private final Logger log = Logger.getLogger(RevokeRoleStepProvider.class);
protected RevokeRoleStepProvider(KeycloakSession session, ComponentModel model) {
super(session, model);
}
@Override
protected void run(UserModel user, RoleModel role) {
log.debugv("Revoking role %s from user %s)", role.getName(), user.getId());
user.deleteRoleMapping(role);
}
}

View File

@@ -0,0 +1,54 @@
package org.keycloak.models.workflow;
import java.util.List;
import org.keycloak.Config;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
public class RevokeRoleStepProviderFactory implements WorkflowStepProviderFactory<RevokeRoleStepProvider> {
public static final String ID = "revoke-role";
@Override
public RevokeRoleStepProvider create(KeycloakSession session, ComponentModel model) {
return new RevokeRoleStepProvider(session, model);
}
@Override
public void init(Config.Scope config) {
// no-op
}
@Override
public void postInit(KeycloakSessionFactory factory) {
// no-op
}
@Override
public void close() {
// no-op
}
@Override
public String getId() {
return ID;
}
@Override
public ResourceType getType() {
return ResourceType.USERS;
}
@Override
public String getHelpText() {
return "Revoke a user role.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return List.of();
}
}

View File

@@ -0,0 +1,77 @@
package org.keycloak.models.workflow;
import java.util.List;
import java.util.stream.Stream;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.jboss.logging.Logger;
public abstract class RoleBasedStepProvider implements WorkflowStepProvider {
private final Logger log = Logger.getLogger(RoleBasedStepProvider.class);
public static final String CONFIG_ROLE = "role";
private final KeycloakSession session;
private final ComponentModel model;
public RoleBasedStepProvider(KeycloakSession session, ComponentModel model) {
this.session = session;
this.model = model;
}
@Override
public void run(WorkflowExecutionContext context) {
UserModel user = session.users().getUserById(getRealm(), context.getResourceId());
if (user != null) {
try {
getRoles().forEach(role -> run(user, role));
} catch (Exception e) {
log.errorf(e, "Failed to grant role to user %s", user.getId());
}
}
}
protected abstract void run(UserModel user, RoleModel role);
@Override
public void close() {
}
private Stream<RoleModel> getRoles() {
return model.getConfig().getOrDefault(CONFIG_ROLE, List.of()).stream().map(this::getRole);
}
private RoleModel getRole(String name) {
RoleModel role;
String[] parts = name.split("/");
if (parts.length > 1) {
ClientModel client = getRealm().getClientByClientId(parts[0]);
if (client == null) {
throw new IllegalStateException("Client with clientId " + parts[0] + " not found");
}
role = client.getRole(parts[1]);
} else {
role = getRealm().getRole(name);
}
if (role == null) {
throw new IllegalStateException("Role " + name + " not found");
}
return role;
}
private RealmModel getRealm() {
return session.getContext().getRealm();
}
}

View File

@@ -19,4 +19,6 @@ org.keycloak.models.workflow.DisableUserStepProviderFactory
org.keycloak.models.workflow.NotifyUserStepProviderFactory
org.keycloak.models.workflow.DeleteUserStepProviderFactory
org.keycloak.models.workflow.SetUserAttributeStepProviderFactory
org.keycloak.models.workflow.AddRequiredActionStepProviderFactory
org.keycloak.models.workflow.AddRequiredActionStepProviderFactory
org.keycloak.models.workflow.GrantRoleStepProviderFactory
org.keycloak.models.workflow.RevokeRoleStepProviderFactory

View File

@@ -2,8 +2,12 @@ package org.keycloak.tests.admin.model.workflow;
import java.time.Duration;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.keycloak.common.util.Time;
import org.keycloak.models.workflow.WorkflowProvider;
import org.keycloak.representations.workflows.WorkflowRepresentation;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.oauth.OAuthClient;
@@ -17,6 +21,9 @@ import org.keycloak.testframework.ui.annotations.InjectWebDriver;
import org.keycloak.testframework.ui.page.LoginPage;
import org.keycloak.testframework.ui.webdriver.ManagedWebDriver;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
public abstract class AbstractWorkflowTest {
protected static final String DEFAULT_REALM_NAME = "default";
@@ -36,6 +43,12 @@ public abstract class AbstractWorkflowTest {
@InjectOAuthClient(realmRef = DEFAULT_REALM_NAME)
OAuthClient oauth;
protected void create(WorkflowRepresentation workflow) {
try (Response response = managedRealm.admin().workflows().create(workflow)) {
assertThat(response.getStatus(), is(Status.CREATED.getStatusCode()));
}
}
protected void runScheduledSteps(Duration duration) {
runOnServer.run((RunOnServer) session -> {
WorkflowProvider provider = session.getProvider(WorkflowProvider.class);

View File

@@ -0,0 +1,147 @@
package org.keycloak.tests.admin.model.workflow;
import java.time.Duration;
import java.util.List;
import java.util.stream.Stream;
import jakarta.ws.rs.core.Response;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.RolesResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.models.workflow.GrantRoleStepProvider;
import org.keycloak.models.workflow.GrantRoleStepProviderFactory;
import org.keycloak.models.workflow.ResourceOperationType;
import org.keycloak.models.workflow.RevokeRoleStepProvider;
import org.keycloak.models.workflow.RevokeRoleStepProviderFactory;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.workflows.WorkflowRepresentation;
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.realm.ClientConfigBuilder;
import org.keycloak.testframework.realm.RoleConfigBuilder;
import org.keycloak.testframework.realm.UserConfigBuilder;
import org.keycloak.testframework.util.ApiUtil;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.keycloak.models.workflow.ResourceOperationType.USER_ADDED;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.not;
@KeycloakIntegrationTest(config = WorkflowsBlockingServerConfig.class)
public class RoleBasedStepTest extends AbstractWorkflowTest {
@BeforeEach
public void setupRoles() {
RealmResource admin = managedRealm.admin();
RolesResource realmRoles = admin.roles();
List.of("a", "b", "c").forEach(name -> realmRoles.create(RoleConfigBuilder.create().name("realm-role-" + name).build()));
ClientsResource clients = admin.clients();
clients.create(ClientConfigBuilder.create().clientId("myclient").build()).close();
ClientRepresentation client = clients.findByClientId("myclient").get(0);
RolesResource clientRoles = clients.get(client.getId()).roles();
List.of("a", "b", "c").forEach(name -> clientRoles.create(RoleConfigBuilder.create().name("client-role-" + name).build()));
}
@Test
public void testGrantRole() {
List<String> expectedRealmRoles = List.of("realm-role-a", "realm-role-b");
List<String> expectedClientRoles = List.of("myclient/client-role-a", "myclient/client-role-c");
List<String> expectedRoles = Stream.concat(expectedRealmRoles.stream(), expectedClientRoles.stream()).toList();
create(WorkflowRepresentation.withName("grant-roles")
.onEvent(USER_ADDED.name())
.withSteps(
WorkflowStepRepresentation.create()
.of(GrantRoleStepProviderFactory.ID)
.withConfig(GrantRoleStepProvider.CONFIG_ROLE, expectedRoles.toArray(new String[0]))
.build()
).build());
UserResource user = getUserResource(UserConfigBuilder.create().username("myuser").build());
Awaitility.await()
.timeout(Duration.ofSeconds(30))
.pollInterval(Duration.ofSeconds(1))
.untilAsserted(() -> {
var actualRealmRoles = user.roles().getAll().getRealmMappings().stream()
.map(RoleRepresentation::getName).toList();
assertThat(actualRealmRoles, hasItems(expectedRealmRoles.toArray(new String[0])));
var actualClientRoles = user.roles().getAll().getClientMappings().get("myclient").getMappings().stream()
.map((r) -> "myclient/" + r.getName()).toList();
assertThat(actualClientRoles, hasItems(expectedClientRoles.toArray(new String[0])));
});
}
@Test
public void testRevokeRole() {
UserResource user = getUserResource(UserConfigBuilder.create()
.username("myuser")
.build());
grantRole(user, "realm-role-a", "realm-role-b", "realm-role-c", "myclient/client-role-a", "myclient/client-role-c");
create(WorkflowRepresentation.withName("revoke-roles")
.onEvent(ResourceOperationType.USER_ROLE_REMOVED.name())
.withSteps(
WorkflowStepRepresentation.create()
.of(RevokeRoleStepProviderFactory.ID)
.withConfig(RevokeRoleStepProvider.CONFIG_ROLE, "realm-role-a", "myclient/client-role-c")
.build()
).build());
user.roles().realmLevel().remove(List.of(
managedRealm.admin().roles().get("realm-role-b").toRepresentation()
));
Awaitility.await()
.timeout(Duration.ofSeconds(30))
.pollInterval(Duration.ofSeconds(1))
.untilAsserted(() -> {
var actualRealmRoles = user.roles().getAll().getRealmMappings().stream()
.map(RoleRepresentation::getName).toList();
assertThat(actualRealmRoles, not(hasItems(List.of("realm-role-a", "realm-role-b").toArray(new String[0]))));
assertThat(actualRealmRoles, hasItems(List.of("realm-role-c").toArray(new String[0])));
var actualClientRoles = user.roles().getAll().getClientMappings().get("myclient").getMappings().stream()
.map((r) -> "myclient/" + r.getName()).toList();
assertThat(actualClientRoles, containsInAnyOrder(List.of("myclient/client-role-a").toArray(new String[0])));
});
}
private UserResource getUserResource(UserRepresentation user) {
UsersResource users = managedRealm.admin().users();
try (Response response = users.create(user)) {
user.setId(ApiUtil.getCreatedId(response));
}
return users.get(user.getId());
}
private void grantRole(UserResource user, String... roles) {
RealmResource admin = managedRealm.admin();
for (String name : roles) {
String[] parts = name.split("/");
if (parts.length > 1) {
ClientsResource clients = admin.clients();
ClientRepresentation client = clients.findByClientId(parts[0]).get(0);
RoleRepresentation clientRole = clients.get(client.getId()).roles().get(parts[1]).toRepresentation();
user.roles().clientLevel(client.getId()).add(List.of(clientRole));
} else {
RoleRepresentation realmRole = admin.roles().get(name).toRepresentation();
user.roles().realmLevel().add(List.of(realmRole));
}
}
}
}