mirror of
https://github.com/keycloak/keycloak.git
synced 2026-02-03 20:39:33 -05:00
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:
parent
38b5466093
commit
c13a1772f8
16 changed files with 497 additions and 26 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
Loading…
Reference in a new issue