Ensure workflow is only restarted on events that match the activation condition

Closes #44399

Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
Stefan Guilhen 2025-11-21 23:26:38 -03:00 committed by Pedro Igor
parent 6653b72f88
commit cd350082f7
3 changed files with 91 additions and 3 deletions

View File

@ -261,7 +261,7 @@ public class DefaultWorkflowProvider implements WorkflowProvider {
// workflow is active for the resource, check if the provider wants to reset or deactivate it based on the event
String executionId = scheduledStep.executionId();
String resourceId = scheduledStep.resourceId();
if (provider.reset(context)) {
if (provider.restart(context)) {
new DefaultWorkflowExecutionContext(session, workflow, event, scheduledStep).restart();
} else if (provider.deactivate(context)) {
log.debugf("Workflow '%s' cancelled for resource %s (execution id: %s)", workflow.getName(), resourceId, executionId);

View File

@ -47,12 +47,12 @@ final class EventBasedWorkflow {
return false;
}
boolean reset(WorkflowExecutionContext executionContext) throws WorkflowInvalidStateException {
boolean restart(WorkflowExecutionContext executionContext) throws WorkflowInvalidStateException {
WorkflowEvent event = executionContext.getEvent();
if (event == null) {
return false;
}
return supports(event.getResourceType()) && isCancelIfRunning() && validateResourceConditions(executionContext);
return isCancelIfRunning() && activate(executionContext);
}
public boolean validateResourceConditions(WorkflowExecutionContext context) {

View File

@ -17,15 +17,19 @@
package org.keycloak.tests.admin.model.workflow;
import java.io.IOException;
import java.time.Duration;
import java.util.List;
import jakarta.mail.internet.MimeMessage;
import jakarta.ws.rs.core.Response;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.workflow.DisableUserStepProviderFactory;
import org.keycloak.models.workflow.NotifyUserStepProviderFactory;
import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory;
import org.keycloak.models.workflow.WorkflowStateProvider;
import org.keycloak.representations.workflows.WorkflowRepresentation;
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
import org.keycloak.testframework.annotations.InjectUser;
@ -33,9 +37,11 @@ import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.mail.MailServer;
import org.keycloak.testframework.mail.annotations.InjectMailServer;
import org.keycloak.testframework.realm.GroupConfigBuilder;
import org.keycloak.testframework.realm.ManagedUser;
import org.keycloak.testframework.realm.UserConfig;
import org.keycloak.testframework.realm.UserConfigBuilder;
import org.keycloak.testframework.util.ApiUtil;
import org.junit.jupiter.api.Test;
@ -45,6 +51,10 @@ import static org.keycloak.tests.admin.model.workflow.WorkflowManagementTest.fin
import static org.keycloak.tests.admin.model.workflow.WorkflowManagementTest.findEmailsByRecipient;
import static org.keycloak.tests.admin.model.workflow.WorkflowManagementTest.verifyEmailContent;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -59,6 +69,84 @@ public class UserSessionRefreshTimeWorkflowTest extends AbstractWorkflowTest {
@InjectMailServer
private MailServer mailServer;
@Test
public void testWorkflowIsRestartedOnSameEvent() throws IOException {
// create a workflow that can restarted on the same event - i.e. has concurrency setting to cancel if running
managedRealm.admin().workflows().create(WorkflowRepresentation.withName("myworkflow")
.onEvent(USER_LOGGED_IN.toString())
.concurrency().cancelIfRunning() // this setting enables restarting the workflow
.withSteps(
WorkflowStepRepresentation.create()
.of(SetUserAttributeStepProviderFactory.ID)
.withConfig("attribute", "attr1")
.after(Duration.ofDays(1))
.build(),
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
.after(Duration.ofDays(5))
.build()
).build()).close();
// login with alice - this will attach the workflow to the user and schedule the first step
oauth.openLoginForm();
String userId = userAlice.getId();
String username = userAlice.getUsername();
loginPage.fillLogin(username, userAlice.getPassword());
loginPage.submit();
assertTrue(driver.getPageSource() != null && driver.getPageSource().contains("Happy days"));
// store the first step id for later comparison
String firstStepId = runOnServer.fetch(session-> {
WorkflowStateProvider provider = session.getProvider(WorkflowStateProvider.class);
List< WorkflowStateProvider.ScheduledStep> steps = provider.getScheduledStepsByResource(userId);
assertThat(steps, hasSize(1));
return steps.get(0).stepId();
}, String.class);
// run the first schedule task - workflow should now be waiting to run the second step
runScheduledSteps(Duration.ofDays(2));
String secondStepId = runOnServer.fetch(session -> {
RealmModel realm = session.getContext().getRealm();
UserModel user = session.users().getUserByUsername(realm, username);
// first step should have run and the attribute should be set
assertThat(user.getFirstAttribute("attribute"), is("attr1"));
assertTrue(user.isEnabled());
WorkflowStateProvider provider = session.getProvider(WorkflowStateProvider.class);
List< WorkflowStateProvider.ScheduledStep> steps = provider.getScheduledStepsByResource(userId);
assertThat(steps, hasSize(1));
return steps.get(0).stepId();
}, String.class);
assertThat(secondStepId, is(not(firstStepId)));
String groupId;
// trigger an unrelated event - like user joining a group. The workflow must not be restarted
try (Response response = managedRealm.admin().groups().add(GroupConfigBuilder.create()
.name("generic-group").build())) {
groupId = ApiUtil.getCreatedId(response);
}
managedRealm.admin().users().get(userAlice.getId()).joinGroup(groupId);
runOnServer.run(session -> {
WorkflowStateProvider provider = session.getProvider(WorkflowStateProvider.class);
List< WorkflowStateProvider.ScheduledStep> steps = provider.getScheduledStepsByResource(userId);
// step id must remain the same as before
assertThat(steps, hasSize(1));
assertThat(steps.get(0).stepId(), is(secondStepId));
});
// now trigger the same event again that can restart the workflow
oauth.openLoginForm();
// workflow should be restarted and the first step should be scheduled again
runOnServer.run(session -> {
WorkflowStateProvider provider = session.getProvider(WorkflowStateProvider.class);
List< WorkflowStateProvider.ScheduledStep> steps = provider.getScheduledStepsByResource(userId);
// step id must be the first one now as the workflow was restarted
assertThat(steps, hasSize(1));
assertThat(steps.get(0).stepId(), is(firstStepId));
});
}
@Test
public void testDisabledUserAfterInactivityPeriod() {
managedRealm.admin().workflows().create(WorkflowRepresentation.withName("myworkflow")