mirror of
https://github.com/keycloak/keycloak.git
synced 2026-05-28 04:13:22 -04:00
Adding join and leave group steps (#44841)
Closes #44649 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
08e96435c8
commit
f36819e943
7 changed files with 390 additions and 1 deletions
|
|
@ -0,0 +1,74 @@
|
|||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.GroupProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
public abstract class GroupBasedStepProvider implements WorkflowStepProvider {
|
||||
|
||||
private final Logger log = Logger.getLogger(GroupBasedStepProvider.class);
|
||||
public static final String CONFIG_GROUP = "group";
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final ComponentModel model;
|
||||
|
||||
public GroupBasedStepProvider(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 {
|
||||
getGroups().forEach(group -> run(user, group));
|
||||
} catch (Exception e) {
|
||||
log.errorf(e, "Failed to manage group membership for user %s", user.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void run(UserModel user, GroupModel group);
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
private Stream<GroupModel> getGroups() {
|
||||
return model.getConfig().getOrDefault(CONFIG_GROUP, List.of()).stream().map(this::getGroup);
|
||||
}
|
||||
|
||||
private GroupModel getGroup(String name) {
|
||||
GroupProvider groups = session.groups();
|
||||
String[] paths = name.split("/");
|
||||
RealmModel realm = getRealm();
|
||||
GroupModel group = null;
|
||||
|
||||
for (String part : paths) {
|
||||
if (part.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
group = groups.getGroupByName(realm, group, part);
|
||||
}
|
||||
|
||||
if (group == null) {
|
||||
throw new IllegalStateException("Could not find group for name or path: " + name);
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
private RealmModel getRealm() {
|
||||
return session.getContext().getRealm();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import static org.keycloak.models.utils.ModelToRepresentation.buildGroupPath;
|
||||
|
||||
public class JoinGroupStepProvider extends GroupBasedStepProvider {
|
||||
|
||||
private final Logger log = Logger.getLogger(JoinGroupStepProvider.class);
|
||||
|
||||
protected JoinGroupStepProvider(KeycloakSession session, ComponentModel model) {
|
||||
super(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run(UserModel user, GroupModel group) {
|
||||
log.debugv("Adding user %s to group %s)", user.getId(), buildGroupPath(group));
|
||||
user.joinGroup(group);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 JoinGroupStepProviderFactory implements WorkflowStepProviderFactory<JoinGroupStepProvider> {
|
||||
|
||||
public static final String ID = "join-group";
|
||||
|
||||
@Override
|
||||
public JoinGroupStepProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new JoinGroupStepProvider(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 "Join a user to a group.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import static org.keycloak.models.utils.ModelToRepresentation.buildGroupPath;
|
||||
|
||||
public class LeaveGroupStepProvider extends GroupBasedStepProvider {
|
||||
|
||||
private final Logger log = Logger.getLogger(LeaveGroupStepProvider.class);
|
||||
|
||||
protected LeaveGroupStepProvider(KeycloakSession session, ComponentModel model) {
|
||||
super(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run(UserModel user, GroupModel group) {
|
||||
log.debugv("Removing user %s from group %s)", user.getId(), buildGroupPath(group));
|
||||
user.leaveGroup(group);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 LeaveGroupStepProviderFactory implements WorkflowStepProviderFactory<LeaveGroupStepProvider> {
|
||||
|
||||
public static final String ID = "leave-group";
|
||||
|
||||
@Override
|
||||
public LeaveGroupStepProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new LeaveGroupStepProvider(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 "Remove a user from a group.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
|
@ -21,4 +21,6 @@ org.keycloak.models.workflow.DeleteUserStepProviderFactory
|
|||
org.keycloak.models.workflow.SetUserAttributeStepProviderFactory
|
||||
org.keycloak.models.workflow.AddRequiredActionStepProviderFactory
|
||||
org.keycloak.models.workflow.GrantRoleStepProviderFactory
|
||||
org.keycloak.models.workflow.RevokeRoleStepProviderFactory
|
||||
org.keycloak.models.workflow.RevokeRoleStepProviderFactory
|
||||
org.keycloak.models.workflow.JoinGroupStepProviderFactory
|
||||
org.keycloak.models.workflow.LeaveGroupStepProviderFactory
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package org.keycloak.tests.workflow;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.admin.client.resource.GroupsResource;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.admin.client.resource.UsersResource;
|
||||
import org.keycloak.models.workflow.JoinGroupStepProvider;
|
||||
import org.keycloak.models.workflow.JoinGroupStepProviderFactory;
|
||||
import org.keycloak.models.workflow.LeaveGroupStepProvider;
|
||||
import org.keycloak.models.workflow.LeaveGroupStepProviderFactory;
|
||||
import org.keycloak.models.workflow.ResourceOperationType;
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
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.GroupConfigBuilder;
|
||||
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_CREATED;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
|
||||
@KeycloakIntegrationTest(config = WorkflowsBlockingServerConfig.class)
|
||||
public class GroupBasedStepTest extends AbstractWorkflowTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setupRoles() {
|
||||
RealmResource admin = managedRealm.admin();
|
||||
GroupsResource groups = admin.groups();
|
||||
for (Entry<String, List<?>> group : Map.of("a", List.of("a1"), "b", List.of("b1", "b2"), "c", List.of()).entrySet()) {
|
||||
GroupRepresentation rep = GroupConfigBuilder.create().name(group.getKey()).build();
|
||||
try (Response response = groups.add(rep)) {
|
||||
rep.setId(ApiUtil.getCreatedId(response));
|
||||
}
|
||||
for (Object subGroup : group.getValue()) {
|
||||
groups.group(rep.getId()).subGroup(GroupConfigBuilder.create().name(subGroup.toString()).build()).close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJoinGroup() {
|
||||
List<String> expectedGroups = List.of("/a", "/b/b1", "c");
|
||||
|
||||
create(WorkflowRepresentation.withName("join-group")
|
||||
.onEvent(USER_CREATED.name())
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create()
|
||||
.of(JoinGroupStepProviderFactory.ID)
|
||||
.withConfig(JoinGroupStepProvider.CONFIG_GROUP, expectedGroups.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(() -> {
|
||||
GroupsResource groups = managedRealm.admin().groups();
|
||||
var actualGroups = user.groups().stream().map(g -> groups.group(g.getId()).toRepresentation().getPath()).toList();
|
||||
assertThat(actualGroups, containsInAnyOrder(expectedGroups.stream().map(n -> {
|
||||
if (!n.startsWith("/")) {
|
||||
return "/" + n;
|
||||
}
|
||||
return n;
|
||||
}).toArray(String[]::new)));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLeaveGroup() {
|
||||
UserResource user = getUserResource(UserConfigBuilder.create()
|
||||
.username("myuser")
|
||||
.build());
|
||||
joinGroup(user, "a", "/a/a1", "b/b1", "b/b2", "/c");
|
||||
|
||||
create(WorkflowRepresentation.withName("leave-group")
|
||||
.onEvent(ResourceOperationType.USER_GROUP_MEMBERSHIP_REMOVED.name())
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create()
|
||||
.of(LeaveGroupStepProviderFactory.ID)
|
||||
.withConfig(LeaveGroupStepProvider.CONFIG_GROUP, "a", "/b/b1", "/b/b2")
|
||||
.build()
|
||||
).build());
|
||||
|
||||
user.leaveGroup(managedRealm.admin().groups().groups("c", true, null, null, true).get(0).getId());
|
||||
|
||||
Awaitility.await()
|
||||
.timeout(Duration.ofSeconds(30))
|
||||
.pollInterval(Duration.ofSeconds(1))
|
||||
.untilAsserted(() -> {
|
||||
GroupsResource groups = managedRealm.admin().groups();
|
||||
var actualGroups = user.groups().stream().map(g -> groups.group(g.getId()).toRepresentation().getPath()).toList();
|
||||
assertThat(actualGroups, containsInAnyOrder("/a/a1"));
|
||||
});
|
||||
}
|
||||
|
||||
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 joinGroup(UserResource user, String... groups) {
|
||||
RealmResource admin = managedRealm.admin();
|
||||
|
||||
for (String name : groups) {
|
||||
String[] parts = name.split("/");
|
||||
|
||||
if (parts.length == 1) {
|
||||
GroupsResource groupsResource = admin.groups();
|
||||
GroupRepresentation group = groupsResource.groups(parts[0], true, null, null, true).get(0);
|
||||
user.joinGroup(group.getId());
|
||||
} else {
|
||||
GroupsResource groupsResource = admin.groups();
|
||||
GroupRepresentation group = null;
|
||||
|
||||
for (String part : parts) {
|
||||
if (part.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (group == null) {
|
||||
group = groupsResource.groups(part, true, null, null, true).get(0);
|
||||
} else {
|
||||
group = groupsResource.group(group.getId()).getSubGroups(part, true, null, null, true).get(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (group != null) {
|
||||
user.joinGroup(group.getId());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue