Ensure GroupMemberLeaveEvent has a reference to the user leaving the group

Closes #44400

Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
Stefan Guilhen
2025-11-14 23:33:54 -03:00
committed by Pedro Igor
parent 3e312d91d8
commit be714d935d
4 changed files with 100 additions and 5 deletions
@@ -474,7 +474,7 @@ public class UserAdapter implements UserModel, JpaModel<UserEntity> {
em.remove(entity);
}
em.flush();
GroupMemberLeaveEvent.fire(group, session);
GroupMemberLeaveEvent.fire(group, this, session);
}
@Override
@@ -11,8 +11,10 @@ import org.keycloak.models.FederatedIdentityModel.FederatedIdentityCreatedEvent;
import org.keycloak.models.FederatedIdentityModel.FederatedIdentityRemovedEvent;
import org.keycloak.models.GroupModel;
import org.keycloak.models.GroupModel.GroupMemberJoinEvent;
import org.keycloak.models.GroupModel.GroupMemberLeaveEvent;
import org.keycloak.models.RoleModel;
import org.keycloak.models.RoleModel.RoleGrantedEvent;
import org.keycloak.models.RoleModel.RoleRevokedEvent;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.ProviderEvent;
@@ -70,7 +72,10 @@ public enum ResourceOperationType {
}
public String getResourceId(ProviderEvent event) {
if (event instanceof GroupMemberJoinEvent gme) {
if (event instanceof GroupMemberJoinEvent gme) {
return gme.getUser().getId();
}
if (event instanceof GroupMemberLeaveEvent gme) {
return gme.getUser().getId();
}
if (event instanceof FederatedIdentityModel.FederatedIdentityCreatedEvent fie) {
@@ -79,10 +84,10 @@ public enum ResourceOperationType {
if (event instanceof FederatedIdentityModel.FederatedIdentityRemovedEvent fie) {
return fie.getUser().getId();
}
if (event instanceof RoleModel.RoleGrantedEvent rge) {
if (event instanceof RoleGrantedEvent rge) {
return rge.getUser().getId();
}
if (event instanceof RoleModel.RoleRevokedEvent rre) {
if (event instanceof RoleRevokedEvent rre) {
return rre.getUser().getId();
}
return null;
@@ -138,7 +138,7 @@ public interface GroupModel extends RoleMapperModel {
}
interface GroupMemberLeaveEvent extends GroupEvent {
static void fire(GroupModel group, KeycloakSession session) {
static void fire(GroupModel group, UserModel user, KeycloakSession session) {
session.getKeycloakSessionFactory().publish(new GroupMemberLeaveEvent() {
@Override
public RealmModel getRealm() {
@@ -150,12 +150,19 @@ public interface GroupModel extends RoleMapperModel {
return group;
}
@Override
public UserModel getUser() {
return user;
}
@Override
public KeycloakSession getKeycloakSession() {
return session;
}
});
}
UserModel getUser();
}
interface GroupPathChangeEvent extends GroupEvent {
@@ -0,0 +1,83 @@
package org.keycloak.tests.admin.model.workflow;
import java.time.Duration;
import jakarta.ws.rs.core.Response;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.WorkflowsResource;
import org.keycloak.models.workflow.ResourceOperationType;
import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.userprofile.config.UPConfig;
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.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
@KeycloakIntegrationTest(config = WorkflowsBlockingServerConfig.class)
public class GroupMembershipLeaveWorkflowTest extends AbstractWorkflowTest {
private static final String GROUP_NAME = "generic-group";
@Test
public void testEventsOnGroupMembershipLeave() {
UPConfig upConfig = managedRealm.admin().users().userProfile().getConfiguration();
upConfig.setUnmanagedAttributePolicy(UPConfig.UnmanagedAttributePolicy.ENABLED);
managedRealm.admin().users().userProfile().update(upConfig);
String groupId;
// create a test group
try (Response response = managedRealm.admin().groups().add(GroupConfigBuilder.create().name(GROUP_NAME).build())) {
groupId = ApiUtil.getCreatedId(response);
}
WorkflowRepresentation expectedWorkflow = WorkflowRepresentation.withName("myworkflow")
.onEvent(ResourceOperationType.USER_GROUP_MEMBERSHIP_REMOVED.name() + "(" + GROUP_NAME + ")")
.withSteps(
WorkflowStepRepresentation.create()
.of(SetUserAttributeStepProviderFactory.ID)
.withConfig("attribute", "attr1")
.after(Duration.ofDays(5))
.build()
).build();
// create the workflow that activates on group membership removal
WorkflowsResource workflows = managedRealm.admin().workflows();
try (Response response = workflows.create(expectedWorkflow)) {
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
}
// now create a user and add them to the group
String userId;
try (Response response = managedRealm.admin().users().create(UserConfigBuilder.create()
.username("generic-user").email("generic-user@example.com").build())) {
userId = ApiUtil.getCreatedId(response);
}
UserResource userResource = managedRealm.admin().users().get(userId);
userResource.joinGroup(groupId);
// set offset to 6 days - no steps should run as the workflow shouldn't have activated yet
runScheduledSteps(Duration.ofDays(6));
UserRepresentation rep = userResource.toRepresentation();
assertNull(rep.getAttributes());
// now remove the user from the group - this should trigger the workflow
userResource.leaveGroup(groupId);
// set offset to 6 days - set attribute step should run now
runScheduledSteps(Duration.ofDays(6));
rep = userResource.toRepresentation();
assertNotNull(rep.getAttributes());
assertThat(rep.getAttributes().get("attribute").get(0), is("attr1"));
}
}