mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
Adding grant and revoke role steps
Closes #44648 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
138d1e0588
commit
84a0324d60
@ -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.NotifyUserStepProviderFactory
|
||||||
org.keycloak.models.workflow.DeleteUserStepProviderFactory
|
org.keycloak.models.workflow.DeleteUserStepProviderFactory
|
||||||
org.keycloak.models.workflow.SetUserAttributeStepProviderFactory
|
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 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.common.util.Time;
|
||||||
import org.keycloak.models.workflow.WorkflowProvider;
|
import org.keycloak.models.workflow.WorkflowProvider;
|
||||||
|
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||||
import org.keycloak.testframework.annotations.InjectRealm;
|
import org.keycloak.testframework.annotations.InjectRealm;
|
||||||
import org.keycloak.testframework.injection.LifeCycle;
|
import org.keycloak.testframework.injection.LifeCycle;
|
||||||
import org.keycloak.testframework.oauth.OAuthClient;
|
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.page.LoginPage;
|
||||||
import org.keycloak.testframework.ui.webdriver.ManagedWebDriver;
|
import org.keycloak.testframework.ui.webdriver.ManagedWebDriver;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
|
||||||
public abstract class AbstractWorkflowTest {
|
public abstract class AbstractWorkflowTest {
|
||||||
|
|
||||||
protected static final String DEFAULT_REALM_NAME = "default";
|
protected static final String DEFAULT_REALM_NAME = "default";
|
||||||
@ -36,6 +43,12 @@ public abstract class AbstractWorkflowTest {
|
|||||||
@InjectOAuthClient(realmRef = DEFAULT_REALM_NAME)
|
@InjectOAuthClient(realmRef = DEFAULT_REALM_NAME)
|
||||||
OAuthClient oauth;
|
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) {
|
protected void runScheduledSteps(Duration duration) {
|
||||||
runOnServer.run((RunOnServer) session -> {
|
runOnServer.run((RunOnServer) session -> {
|
||||||
WorkflowProvider provider = session.getProvider(WorkflowProvider.class);
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user