mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-16 20:15:46 -06:00
Adding grant and revoke role steps
Closes #44648 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user