diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java index 4627f0cfa5a..1ff51e88b60 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java @@ -474,7 +474,7 @@ public class UserAdapter implements UserModel, JpaModel { em.remove(entity); } em.flush(); - GroupMemberLeaveEvent.fire(group, session); + GroupMemberLeaveEvent.fire(group, this, session); } @Override diff --git a/server-spi-private/src/main/java/org/keycloak/models/workflow/ResourceOperationType.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/ResourceOperationType.java index f64ed5919d5..44b2eac964e 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/workflow/ResourceOperationType.java +++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/ResourceOperationType.java @@ -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; diff --git a/server-spi/src/main/java/org/keycloak/models/GroupModel.java b/server-spi/src/main/java/org/keycloak/models/GroupModel.java index 953cdfa090c..1643cf1d2aa 100755 --- a/server-spi/src/main/java/org/keycloak/models/GroupModel.java +++ b/server-spi/src/main/java/org/keycloak/models/GroupModel.java @@ -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 { diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/GroupMembershipLeaveWorkflowTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/GroupMembershipLeaveWorkflowTest.java new file mode 100644 index 00000000000..f5b5d4189a8 --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/GroupMembershipLeaveWorkflowTest.java @@ -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")); + } + +}