Adds ability to migrate scheduled workflow resources from one step to another step in the same or different workflow

Closes #45174

Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
Stefan Guilhen 2026-01-26 08:40:05 -03:00 committed by Pedro Igor
parent 38b5466093
commit c13a1772f8
16 changed files with 497 additions and 26 deletions

View file

@ -50,11 +50,6 @@ public class WorkflowStepRepresentation extends AbstractWorkflowComponentReprese
this.scheduledAt = scheduledAt;
}
@JsonIgnore
public String getId() {
return super.getId();
}
public String getUses() {
return this.uses;
}

View file

@ -48,4 +48,8 @@ public interface WorkflowsResource {
@Path("{id}")
WorkflowResource workflow(@PathParam("id") String id);
@POST
@Path("migrate")
Response migrate(@QueryParam("from") String stepFrom, @QueryParam("to") String stepTo);
}

View file

@ -29,6 +29,17 @@ final class DefaultWorkflowExecutionContext implements WorkflowExecutionContext
this(session, workflow, event, null, UUID.randomUUID().toString(), event.getResourceId());
}
/**
* A new execution context for a workflow event. The execution ID is provided as a parameter
*
* @param workflow the workflow
* @param event the event
* @param executionId the execution ID
*/
DefaultWorkflowExecutionContext(KeycloakSession session, Workflow workflow, WorkflowEvent event, String executionId) {
this(session, workflow, event, null, executionId, event.getResourceId());
}
/**
* A new execution context for a workflow event, resuming a previously scheduled step. The execution ID is taken from the scheduled step
* with no current step, indicating that the workflow is being restarted due to an event.

View file

@ -200,6 +200,68 @@ public class DefaultWorkflowProvider implements WorkflowProvider {
});
}
@Override
public void migrateScheduledResources(String stepIdFrom, String stepIdTo) {
if (stepIdFrom.equals(stepIdTo)) {
return; // nothing to do as both steps are the same
}
// first, we use the steps to find the workflows involved
ComponentModel stepFromModel = getWorkflowComponent(stepIdFrom, WorkflowStepProvider.class.getName());
Workflow workflowFrom = getWorkflow(stepFromModel.getParentId());
ComponentModel stepToModel = getWorkflowComponent(stepIdTo, WorkflowStepProvider.class.getName());
Workflow workflowTo = getWorkflow(stepToModel.getParentId());
// get the scheduled steps from the source step
List<ScheduledStep> scheduledStepsFrom = stateProvider.getScheduledStepsByStep(workflowFrom.getId(), stepIdFrom).toList();
// when migrating between different workflows, we need to perform additional validations
if (!workflowFrom.getId().equals(workflowTo.getId())) {
// ensure both workflows support the same resource type
if (workflowFrom.getSupportedType() != workflowTo.getSupportedType()) {
throw new ModelValidationException("Cannot migrate scheduled resources between workflows that support different resource types.");
}
// ensure all resources scheduled in the source step satisfy the activation conditions of the destination workflow
EventBasedWorkflow eventBasedWorkflow = new EventBasedWorkflow(session, workflowTo.getSupportedType(), getWorkflowComponent(workflowTo.getId()));
for (ScheduledStep scheduledStep : scheduledStepsFrom) {
DefaultWorkflowExecutionContext context = new DefaultWorkflowExecutionContext(session, workflowTo, scheduledStep);
if (!eventBasedWorkflow.validateResourceConditions(context)) {
throw new ModelValidationException("Cannot migrate resource %s to workflow %s as it does not satisfy the workflow's activation conditions."
.formatted(scheduledStep.resourceId(), workflowTo.getName()));
}
}
}
// perform the migration - for each scheduled step in the source, we remove it and activate the destination workflow from the specified step
int stepPosition = workflowTo.getStepById(stepIdTo).getPriority() - 1;
for (ScheduledStep scheduledStep : scheduledStepsFrom) {
// remove the scheduled step from the source workflow
stateProvider.remove(scheduledStep.executionId());
// activate the destination workflow for the resource, starting from the specified step
DefaultWorkflowExecutionContext context;
if (workflowFrom.getId().equals(workflowTo.getId())) {
// we reuse the executionId when migrating within the same workflow
context = new DefaultWorkflowExecutionContext(session, workflowTo, new AdhocWorkflowEvent(workflowTo.getSupportedType(), scheduledStep.resourceId()),
scheduledStep.executionId());
} else {
context = new DefaultWorkflowExecutionContext(session, workflowTo, new AdhocWorkflowEvent(workflowTo.getSupportedType(),
scheduledStep.resourceId()));
}
context.restart(stepPosition);
if (log.isDebugEnabled()) {
WorkflowStep stepFrom = workflowFrom.getStepById(stepIdFrom);
WorkflowStep stepTo = workflowTo.getStepById(stepIdTo);
log.debugf("Migrated resource %s from workflow %s (step %s) to workflow %s (step %s). New execution id: %s",
scheduledStep.resourceId(), workflowFrom.getName(), stepFrom.getProviderId(), workflowTo.getName(),
stepTo.getProviderId(), context.getExecutionId());
}
}
}
@Override
public void activate(Workflow workflow, ResourceType type, String resourceId) {
if (type != workflow.getSupportedType()) {
@ -257,15 +319,19 @@ public class DefaultWorkflowProvider implements WorkflowProvider {
}
private ComponentModel getWorkflowComponent(String id) {
return this.getWorkflowComponent(id, WorkflowProvider.class.getName());
}
private ComponentModel getWorkflowComponent(String id, String providerType) {
ComponentModel component = realm.getComponent(id);
if (component == null || !WorkflowProvider.class.getName().equals(component.getProviderType())) {
throw new BadRequestException("Not a valid resource workflow: " + id);
if (component == null || !Objects.equals(providerType, component.getProviderType())) {
throw new BadRequestException("Not a valid workflow resource: " + id);
}
return component;
}
/* ================================= Workflows component providers and factories ================================= */
private WorkflowProvider getWorkflowProvider(Workflow workflow) {

View file

@ -116,6 +116,24 @@ public class JpaWorkflowStateProvider implements WorkflowStateProvider {
.map(this::toScheduledStep);
}
@Override
public Stream<ScheduledStep> getScheduledStepsByStep(String workflowId, String stepId) {
if (StringUtil.isBlank(workflowId) || StringUtil.isBlank(stepId)) {
return Stream.empty();
}
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<WorkflowStateEntity> query = cb.createQuery(WorkflowStateEntity.class);
Root<WorkflowStateEntity> stateRoot = query.from(WorkflowStateEntity.class);
Predicate byWorkflowAndStep = cb.and(cb.equal(stateRoot.get("workflowId"), workflowId),
cb.equal(stateRoot.get("scheduledStepId"), stepId));
query.where(byWorkflowAndStep);
return em.createQuery(query).getResultStream()
.map(this::toScheduledStep);
}
@Override
public Stream<ScheduledStep> getScheduledStepsByResource(String resourceId) {
CriteriaBuilder cb = em.getCriteriaBuilder();

View file

@ -221,7 +221,7 @@ public class Workflow {
ComponentModel component = realm.getComponent(id);
if (component == null || !Objects.equals(providerType, component.getProviderType())) {
throw new BadRequestException("Not a valid resource workflow: " + id);
throw new BadRequestException("Not a valid workflow resource: " + id);
}
return component;
}

View file

@ -68,4 +68,22 @@ public interface WorkflowProvider extends Provider {
void runScheduledSteps();
void activateForAllEligibleResources(Workflow workflow);
/**
* Migrates scheduled resources from one workflow step to another. The destination step might be a step in the same
* workflow or a step in a different workflow.
* <br/>
* If the resources are being migrated to a different workflow, the following conditions must be met:
* <ul>
* <li>the source and destination workflows must support the same resource type;</li>
* <li>all resources must satisfy the activation conditions of the destination workflow.</li>
* </ul>
* The process behaves exactly as if the resources were being activated for the first time in the destination workflow,
* except that the first step to be processed is the specified destination step. So, if the step is a scheduled step,
* the resources will be scheduled accordingly. If the step is not a scheduled step, it will run immediately.
*
* @param stepIdFrom the id of the step to migrate from.
* @param stepIdTo the id of the step to migrate to.
*/
void migrateScheduledResources(String stepIdFrom, String stepIdTo);
}

View file

@ -73,13 +73,7 @@ public interface WorkflowStateProvider extends Provider {
Stream<ScheduledStep> getScheduledStepsByWorkflow(String workflowId);
default Stream<ScheduledStep> getScheduledStepsByWorkflow(Workflow workflow) {
if (workflow == null) {
return Stream.empty();
}
return getScheduledStepsByWorkflow(workflow.getId());
}
Stream<ScheduledStep> getScheduledStepsByStep(String workflowId, String stepId);
Stream<ScheduledStep> getDueScheduledSteps(Workflow workflow);

View file

@ -96,13 +96,14 @@ public class WorkflowResource {
})
public WorkflowRepresentation toRepresentation(
@Parameter(
description = "Indicates whether the workflow id should be included in the representation or not - defaults to true"
description = "Indicates whether the workflow and step ids should be included in the representation or not - defaults to true"
)
@QueryParam("includeId") Boolean includeId
@QueryParam("includeId") Boolean includeIds
) {
WorkflowRepresentation rep = provider.toRepresentation(workflow);
if (Boolean.FALSE.equals(includeId)) {
if (Boolean.FALSE.equals(includeIds)) {
rep.setId(null);
rep.getSteps().forEach(step -> step.setId(null));
}
return rep;
}

View file

@ -146,4 +146,34 @@ public class WorkflowsResource {
auth.realm().requireManageRealm();
return provider.getScheduledWorkflowsByResource(resourceId).toList();
}
@Path("migrate")
@POST
@Tag(name = KeycloakOpenAPI.Admin.Tags.WORKFLOWS)
@Operation(
summary = "Migrate scheduled resources from one step to another",
description = "Migrate scheduled resources from one step to another step in the same or in a different workflow."
)
@APIResponses(value = {
@APIResponse(responseCode = "204", description = "No Content"),
@APIResponse(responseCode = "400", description = "Bad Request")
})
public Response migrate(
@Parameter(description = "A String representing the id of the step to migrate from")
@QueryParam("from") String stepIdFrom,
@Parameter(description = "A String representing the id of the step to migrate to")
@QueryParam("to") String stepIdTo) {
auth.realm().requireManageRealm();
if (stepIdFrom == null || stepIdTo == null) {
throw ErrorResponse.error("Both 'from' and 'to' step ids must be provided for migration.", Response.Status.BAD_REQUEST);
}
try {
provider.migrateScheduledResources(stepIdFrom, stepIdTo);
return Response.noContent().build();
} catch (ModelException me) {
throw ErrorResponse.error(me.getMessage(), Response.Status.BAD_REQUEST);
}
}
}

View file

@ -0,0 +1,334 @@
package org.keycloak.tests.workflow;
import java.time.Duration;
import java.util.List;
import jakarta.ws.rs.core.Response;
import org.keycloak.models.workflow.AddRequiredActionStepProvider;
import org.keycloak.models.workflow.AddRequiredActionStepProviderFactory;
import org.keycloak.models.workflow.DeleteUserStepProviderFactory;
import org.keycloak.models.workflow.DisableUserStepProviderFactory;
import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory;
import org.keycloak.models.workflow.WorkflowStateProvider;
import org.keycloak.models.workflow.client.DeleteClientStepProviderFactory;
import org.keycloak.models.workflow.conditions.UserAttributeWorkflowConditionFactory;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.workflows.StepExecutionStatus;
import org.keycloak.representations.workflows.WorkflowRepresentation;
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.realm.UserConfigBuilder;
import org.keycloak.testframework.remote.providers.runonserver.RunOnServer;
import org.keycloak.testframework.util.ApiUtil;
import org.keycloak.tests.workflow.config.WorkflowsBlockingServerConfig;
import org.junit.jupiter.api.Test;
import static org.keycloak.models.workflow.ResourceOperationType.CLIENT_ADDED;
import static org.keycloak.models.workflow.ResourceOperationType.USER_CREATED;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
/**
* Tests migrating resources from one workflow to another.
*/
@KeycloakIntegrationTest(config = WorkflowsBlockingServerConfig.class)
public class WorkflowMigrationTest extends AbstractWorkflowTest {
@Test
public void testMigrationFailsIfWorkflowsNotCompatible() {
// create two incompatible workflows - one that operates on users, another on clients
var response = managedRealm.admin().workflows().create(WorkflowRepresentation.withName("client-workflow")
.onEvent(CLIENT_ADDED.name())
.withSteps(
WorkflowStepRepresentation.create()
.of(DeleteClientStepProviderFactory.ID)
.after(Duration.ofDays(10))
.build()
).build());
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
String clientWorkflowId = ApiUtil.getCreatedId(response);
response.close();
response = managedRealm.admin().workflows().create(WorkflowRepresentation.withName("user-workflow")
.onEvent(USER_CREATED.name())
.withSteps(
WorkflowStepRepresentation.create()
.of(AddRequiredActionStepProviderFactory.ID)
.withConfig(AddRequiredActionStepProvider.REQUIRED_ACTION_KEY, "UPDATE_PASSWORD")
.after(Duration.ofDays(5))
.build()
).build());
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
String userWorkflowId = ApiUtil.getCreatedId(response);
response.close();
// create a few of users so that they are attached to the user workflow
for (int i = 1; i <= 3; i++) {
try (var createUserResponse = managedRealm.admin().users().create(UserConfigBuilder.create().username("user-" + i).build())) {
assertThat(createUserResponse.getStatus(), is(Response.Status.CREATED.getStatusCode()));
String userId = ApiUtil.getCreatedId(createUserResponse);
// check created user is attached to the first workflow
List<WorkflowRepresentation> activeWorkflows = managedRealm.admin().workflows().getScheduledWorkflows(userId);
assertThat(activeWorkflows, hasSize(1));
assertThat(activeWorkflows.get(0).getName(), is("user-workflow"));
}
}
// attempt to migrate the users from the user workflow to the client workflow - should fail
WorkflowRepresentation userWorkflow = managedRealm.admin().workflows().workflow(userWorkflowId).toRepresentation();
String fromStepId = userWorkflow.getSteps().get(0).getId();
WorkflowRepresentation clientWorkflow = managedRealm.admin().workflows().workflow(clientWorkflowId).toRepresentation();
String toStepId = clientWorkflow.getSteps().get(0).getId();
try (var migrateResponse = managedRealm.admin().workflows().migrate(fromStepId, toStepId)) {
assertThat(migrateResponse.getStatus(), is(Response.Status.BAD_REQUEST.getStatusCode()));
}
}
@Test
public void testMigrationFailsIfResourcesDontMeetWorkflowConditions() {
// create two user workflows, the second having a condition that the users don't meet
var response = managedRealm.admin().workflows().create(WorkflowRepresentation.withName("workflow-1")
.onEvent(USER_CREATED.name())
.withSteps(
WorkflowStepRepresentation.create()
.of(DisableUserStepProviderFactory.ID)
.after(Duration.ofDays(5))
.build()
).build());
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
String firstWorkflowId = ApiUtil.getCreatedId(response);
response.close();
response = managedRealm.admin().workflows().create(WorkflowRepresentation.withName("workflow-2")
.onCondition(UserAttributeWorkflowConditionFactory.ID + "(some-missing-key:some-missing-value)")
.withSteps(
WorkflowStepRepresentation.create()
.of(SetUserAttributeStepProviderFactory.ID)
.withConfig("key", "value")
.after(Duration.ofDays(10))
.build()
).build());
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
String secondWorkflowId = ApiUtil.getCreatedId(response);
response.close();
// create a few of users so that they are attached to the first workflow
for (int i = 1; i <= 3; i++) {
try (var createUserResponse = managedRealm.admin().users().create(UserConfigBuilder.create().username("user-" + i).build())) {
assertThat(createUserResponse.getStatus(), is(Response.Status.CREATED.getStatusCode()));
String userId = ApiUtil.getCreatedId(createUserResponse);
// check created user is attached to the first workflow
List<WorkflowRepresentation> activeWorkflows = managedRealm.admin().workflows().getScheduledWorkflows(userId);
assertThat(activeWorkflows, hasSize(1));
assertThat(activeWorkflows.get(0).getName(), is("workflow-1"));
}
}
// attempt to migrate the users from the first workflow to the second workflow - should fail
WorkflowRepresentation firstWorkflow = managedRealm.admin().workflows().workflow(firstWorkflowId).toRepresentation();
String fromStepId = firstWorkflow.getSteps().get(0).getId();
WorkflowRepresentation secondWorkflow = managedRealm.admin().workflows().workflow(secondWorkflowId).toRepresentation();
String toStepId = secondWorkflow.getSteps().get(0).getId();
try (var migrateResponse = managedRealm.admin().workflows().migrate(fromStepId, toStepId)) {
assertThat(migrateResponse.getStatus(), is(Response.Status.BAD_REQUEST.getStatusCode()));
}
}
@Test
public void testMigrateToScheduledStepInDifferentWorkflow() {
var response = managedRealm.admin().workflows().create(WorkflowRepresentation.withName("workflow-1")
.onEvent(USER_CREATED.name())
.withSteps(
WorkflowStepRepresentation.create()
.of(DisableUserStepProviderFactory.ID)
.after(Duration.ofDays(5))
.build()
).build());
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
String firstWorkflowId = ApiUtil.getCreatedId(response);
response.close();
response = managedRealm.admin().workflows().create(WorkflowRepresentation.withName("workflow-2")
.withSteps(
WorkflowStepRepresentation.create()
.of(DisableUserStepProviderFactory.ID)
.after(Duration.ofDays(5))
.build(),
WorkflowStepRepresentation.create()
.of(DeleteUserStepProviderFactory.ID)
.after(Duration.ofDays(10))
.build()
).build());
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
String secondWorkflowId = ApiUtil.getCreatedId(response);
response.close();
// create a few of users so that they are attached to the first workflow
String[] userIds = new String[4];
for (int i = 0; i <= 3; i++) {
try (var createUserResponse = managedRealm.admin().users().create(UserConfigBuilder.create().username("user-" + i).build())) {
assertThat(createUserResponse.getStatus(), is(Response.Status.CREATED.getStatusCode()));
userIds[i] = ApiUtil.getCreatedId(createUserResponse);
// check created user is attached to the first workflow
List<WorkflowRepresentation> activeWorkflows = managedRealm.admin().workflows().getScheduledWorkflows(userIds[i]);
assertThat(activeWorkflows, hasSize(1));
assertThat(activeWorkflows.get(0).getName(), is("workflow-1"));
}
}
// migrate users to the delete step in the second workflow
WorkflowRepresentation firstWorkflow = managedRealm.admin().workflows().workflow(firstWorkflowId).toRepresentation();
String fromStepId = firstWorkflow.getSteps().get(0).getId();
WorkflowRepresentation secondWorkflow = managedRealm.admin().workflows().workflow(secondWorkflowId).toRepresentation();
assertThat(secondWorkflow.getSteps().get(1).getUses(), is(DeleteUserStepProviderFactory.ID));
String toStepId = secondWorkflow.getSteps().get(1).getId();
try (var migrateResponse = managedRealm.admin().workflows().migrate(fromStepId, toStepId)) {
assertThat(migrateResponse.getStatus(), is(Response.Status.NO_CONTENT.getStatusCode()));
}
// check users are now attached to the second workflow and scheduled to run the second step
runOnServer.run((RunOnServer) session -> {
// the workflow state table should have a scheduled step for the user in the first workflow
for (int i = 0; i <= 3; i++) {
WorkflowStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session);
List<WorkflowStateProvider.ScheduledStep> steps = stateProvider.getScheduledStepsByResource(userIds[i]).toList();
assertThat(steps, hasSize(1));
assertThat(steps.get(0).workflowId(), is(secondWorkflowId));
assertThat(steps.get(0).stepId(), is(toStepId));
}
});
}
@Test
public void testMigrateToImmediateStepInDifferentWorkflow() {
var response = managedRealm.admin().workflows().create(WorkflowRepresentation.withName("workflow-1")
.onEvent(USER_CREATED.name())
.withSteps(
WorkflowStepRepresentation.create()
.of(DisableUserStepProviderFactory.ID)
.after(Duration.ofDays(5))
.build()
).build());
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
String firstWorkflowId = ApiUtil.getCreatedId(response);
response.close();
response = managedRealm.admin().workflows().create(WorkflowRepresentation.withName("workflow-2")
.withSteps(
WorkflowStepRepresentation.create()
.of(DisableUserStepProviderFactory.ID)
.build(),
WorkflowStepRepresentation.create()
.of(DeleteUserStepProviderFactory.ID)
.after(Duration.ofDays(10))
.build()
).build());
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
String secondWorkflowId = ApiUtil.getCreatedId(response);
response.close();
// create a few of users so that they are attached to the first workflow
String[] userIds = new String[4];
for (int i = 0; i <= 3; i++) {
try (var createUserResponse = managedRealm.admin().users().create(UserConfigBuilder.create().username("user-" + i).build())) {
assertThat(createUserResponse.getStatus(), is(Response.Status.CREATED.getStatusCode()));
userIds[i] = ApiUtil.getCreatedId(createUserResponse);
// check created user is attached to the first workflow
List<WorkflowRepresentation> activeWorkflows = managedRealm.admin().workflows().getScheduledWorkflows(userIds[i]);
assertThat(activeWorkflows, hasSize(1));
assertThat(activeWorkflows.get(0).getName(), is("workflow-1"));
}
}
// migrate users to the first step in the second workflow - it is an immediate step, so it should run right away
WorkflowRepresentation firstWorkflow = managedRealm.admin().workflows().workflow(firstWorkflowId).toRepresentation();
String fromStepId = firstWorkflow.getSteps().get(0).getId();
WorkflowRepresentation secondWorkflow = managedRealm.admin().workflows().workflow(secondWorkflowId).toRepresentation();
String toStepId = secondWorkflow.getSteps().get(0).getId();
try (var migrateResponse = managedRealm.admin().workflows().migrate(fromStepId, toStepId)) {
assertThat(migrateResponse.getStatus(), is(Response.Status.NO_CONTENT.getStatusCode()));
}
// check the users are now disabled, and scheduled to run the second step
for (int i = 0; i <= 3; i++) {
UserRepresentation user = managedRealm.admin().users().get(userIds[i]).toRepresentation();
assertThat(user.isEnabled(), is(false));
List<WorkflowRepresentation> activeWorkflows = managedRealm.admin().workflows().getScheduledWorkflows(userIds[i]);
assertThat(activeWorkflows, hasSize(1));
WorkflowRepresentation firstActiveWorkflow = activeWorkflows.get(0);
assertThat(firstActiveWorkflow.getName(), is("workflow-2"));
assertThat(firstActiveWorkflow.getSteps(), hasSize(2));
assertThat(firstActiveWorkflow.getSteps().get(0).getExecutionStatus(), is(StepExecutionStatus.COMPLETED));
assertThat(firstActiveWorkflow.getSteps().get(1).getExecutionStatus(), is(StepExecutionStatus.PENDING));
}
}
@Test
public void testMigrateToStepInSameWorkflow() {
var response = managedRealm.admin().workflows().create(WorkflowRepresentation.withName("workflow")
.onEvent(USER_CREATED.name())
.withSteps(
WorkflowStepRepresentation.create()
.of(AddRequiredActionStepProviderFactory.ID)
.after(Duration.ofDays(5))
.withConfig(AddRequiredActionStepProvider.REQUIRED_ACTION_KEY, "UPDATE_PASSWORD")
.build(),
WorkflowStepRepresentation.create()
.of(DisableUserStepProviderFactory.ID)
.after(Duration.ofDays(10))
.build(),
WorkflowStepRepresentation.create()
.of(DeleteUserStepProviderFactory.ID)
.after(Duration.ofDays(20))
.build()
).build());
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
String workflowId = ApiUtil.getCreatedId(response);
response.close();
// create a few of users so that they are attached to the workflow
String[] userIds = new String[4];
for (int i = 0; i <= 3; i++) {
try (var createUserResponse = managedRealm.admin().users().create(UserConfigBuilder.create().username("user-" + i).build())) {
assertThat(createUserResponse.getStatus(), is(Response.Status.CREATED.getStatusCode()));
userIds[i] = ApiUtil.getCreatedId(createUserResponse);
// check created user is attached to the first workflow
List<WorkflowRepresentation> activeWorkflows = managedRealm.admin().workflows().getScheduledWorkflows(userIds[i]);
assertThat(activeWorkflows, hasSize(1));
assertThat(activeWorkflows.get(0).getName(), is("workflow"));
assertThat(activeWorkflows.get(0).getSteps(), hasSize(3));
assertThat(activeWorkflows.get(0).getSteps().get(0).getExecutionStatus(), is(StepExecutionStatus.PENDING));
}
}
// migrate users to the delete step in the same workflow
WorkflowRepresentation workflow = managedRealm.admin().workflows().workflow(workflowId).toRepresentation();
String fromStepId = workflow.getSteps().get(0).getId();
String toStepId = workflow.getSteps().get(2).getId();
try (var migrateResponse = managedRealm.admin().workflows().migrate(fromStepId, toStepId)) {
assertThat(migrateResponse.getStatus(), is(Response.Status.NO_CONTENT.getStatusCode()));
}
// check the users are now scheduled to run the delete step instead of the first step
for (int i = 0; i <= 3; i++) {
List<WorkflowRepresentation> activeWorkflows = managedRealm.admin().workflows().getScheduledWorkflows(userIds[i]);
assertThat(activeWorkflows, hasSize(1));
WorkflowRepresentation firstActiveWorkflow = activeWorkflows.get(0);
assertThat(firstActiveWorkflow.getName(), is("workflow"));
assertThat(firstActiveWorkflow.getSteps(), hasSize(3));
assertThat(firstActiveWorkflow.getSteps().get(0).getExecutionStatus(), is(StepExecutionStatus.COMPLETED));
assertThat(firstActiveWorkflow.getSteps().get(1).getExecutionStatus(), is(StepExecutionStatus.COMPLETED));
assertThat(firstActiveWorkflow.getSteps().get(2).getExecutionStatus(), is(StepExecutionStatus.PENDING));
}
}
}

View file

@ -129,7 +129,7 @@ public class GroupWorkflowConditionTest extends AbstractWorkflowTest {
// check workflow was correctly assigned to the users
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
RealmModel realm = session.getContext().getRealm();
List<String> scheduledUsers = stateProvider.getScheduledStepsByWorkflow(workflow)
List<String> scheduledUsers = stateProvider.getScheduledStepsByWorkflow(workflow.getId())
.map(step -> session.users().getUserById(realm, step.resourceId()).getUsername()).toList();
assertThat(scheduledUsers, hasSize(10));

View file

@ -168,7 +168,7 @@ public class IdpLinkConditionWorkflowTest extends AbstractWorkflowTest {
// check no workflows are yet attached to the previous users, only to the ones created after the workflow was in place
WorkflowStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session);
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow).toList();
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow.getId()).toList();
assertEquals(3, scheduledSteps.size());
scheduledSteps.forEach(scheduledStep -> {
assertEquals(notifyStep.getId(), scheduledStep.stepId());
@ -191,7 +191,7 @@ public class IdpLinkConditionWorkflowTest extends AbstractWorkflowTest {
WorkflowStep disableStep = workflow.getSteps().toList().get(1);
WorkflowStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session);
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow).toList();
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow.getId()).toList();
assertEquals(3, scheduledSteps.size());
scheduledSteps.forEach(scheduledStep -> {
assertEquals(disableStep.getId(), scheduledStep.stepId());
@ -217,7 +217,7 @@ public class IdpLinkConditionWorkflowTest extends AbstractWorkflowTest {
Workflow workflow = registeredWorkflows.get(0);
// check workflow was correctly assigned to the old users, not affecting users already associated with the workflow.
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow).toList();
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow.getId()).toList();
assertEquals(13, scheduledSteps.size());
List<WorkflowStep> steps = workflow.getSteps().toList();

View file

@ -114,7 +114,7 @@ public class RoleWorkflowConditionTest extends AbstractWorkflowTest {
Workflow workflow = registeredWorkflows.get(0);
// check workflow was correctly assigned to the users
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow).toList();
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow.getId()).toList();
assertThat(scheduledSteps, hasSize(10));
});
});

View file

@ -109,7 +109,7 @@ public class UserAttributeWorkflowConditionTest extends AbstractWorkflowTest {
Workflow workflow = registeredWorkflows.get(0);
// check workflow was correctly assigned to the users
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow).toList();
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow.getId()).toList();
assertThat(scheduledSteps, hasSize(10));
});
});

View file

@ -107,7 +107,7 @@ public class DisableActiveWorkflowTest extends AbstractWorkflowTest {
List<Workflow> registeredWorkflow = provider.getWorkflows().toList();
assertEquals(1, registeredWorkflow.size());
WorkflowStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session);
List<WorkflowStateProvider.ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(registeredWorkflow.get(0)).toList();
List<WorkflowStateProvider.ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(registeredWorkflow.get(0).getId()).toList();
// verify that there's only one scheduled step, for the first user
assertEquals(1, scheduledSteps.size());