Add API method that fetches the scheduled workflow steps for a resource

Closes #43660

Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
Stefan Guilhen 2025-11-24 19:13:44 -03:00 committed by Pedro Igor
parent 6fa890fd87
commit 65ab7f541d
17 changed files with 184 additions and 74 deletions

View File

@ -24,4 +24,5 @@ public final class WorkflowConstants {
// Entry configuration keys for WorkflowStep
public static final String CONFIG_AFTER = "after";
public static final String CONFIG_PRIORITY = "priority";
public static final String CONFIG_SCHEDULED_AT = "scheduled-at";
}

View File

@ -8,20 +8,23 @@ import org.keycloak.common.util.MultivaluedHashMap;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_AFTER;
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_PRIORITY;
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_SCHEDULED_AT;
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_USES;
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_WITH;
@JsonPropertyOrder({CONFIG_USES, CONFIG_AFTER, CONFIG_PRIORITY, CONFIG_WITH})
@JsonPropertyOrder({CONFIG_USES, CONFIG_AFTER, CONFIG_PRIORITY, CONFIG_WITH, CONFIG_SCHEDULED_AT})
@JsonInclude(JsonInclude.Include.NON_NULL)
public final class WorkflowStepRepresentation extends AbstractWorkflowComponentRepresentation {
public class WorkflowStepRepresentation extends AbstractWorkflowComponentRepresentation {
private final String uses;
private Long scheduledAt;
public static Builder create() {
return new Builder();
@ -36,8 +39,13 @@ public final class WorkflowStepRepresentation extends AbstractWorkflowComponentR
}
public WorkflowStepRepresentation(String id, String uses, MultivaluedHashMap<String, String> config) {
this(id, uses, config, null);
}
public WorkflowStepRepresentation(String id, String uses, MultivaluedHashMap<String, String> config, Long scheduledAt) {
super(id, config);
this.uses = uses;
this.scheduledAt = scheduledAt;
}
@JsonIgnore
@ -72,6 +80,15 @@ public final class WorkflowStepRepresentation extends AbstractWorkflowComponentR
setConfig(CONFIG_PRIORITY, String.valueOf(ms));
}
@JsonProperty(CONFIG_SCHEDULED_AT)
public Long getScheduledAt() {
return this.scheduledAt;
}
public void setScheduledAt(Long scheduledAt) {
this.scheduledAt = scheduledAt;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {

View File

@ -41,6 +41,11 @@ public interface WorkflowsResource {
@QueryParam("max") Integer maxResults
);
@Path("scheduled/{resource-id}")
@GET
@Produces(MediaType.APPLICATION_JSON)
List<WorkflowRepresentation> getScheduledWorkflows(@PathParam("resource-id") String resourceId);
@Path("{id}")
WorkflowResource workflow(@PathParam("id") String id);
}

View File

@ -116,6 +116,25 @@ public class DefaultWorkflowProvider implements WorkflowProvider {
.map(c -> new Workflow(session, c));
}
@Override
public Stream<WorkflowRepresentation> getScheduledWorkflowsByResource(String resourceId) {
return stateProvider.getScheduledStepsByResource(resourceId).map(scheduledStep -> {
Workflow workflow = getWorkflow(scheduledStep.workflowId());
// get the steps starting from the scheduled step, then add their scheduledAt
List<WorkflowStepRepresentation> steps = workflow.getSteps(scheduledStep.stepId()).map(this::toRepresentation).toList();
Long scheduledAt = null;
for (WorkflowStepRepresentation step : steps) {
if (scheduledAt == null) {
scheduledAt = scheduledStep.scheduledAt();
} else if (step.getAfter() != null) {
scheduledAt += DurationConverter.parseDuration(step.getAfter()).toMillis();
}
step.setScheduledAt(scheduledAt);
}
return new WorkflowRepresentation(workflow.getId(), workflow.getName(), workflow.getConfig(), steps);
});
}
@Override
public void submit(WorkflowEvent event) {
processEvent(getWorkflows(), event);
@ -128,7 +147,7 @@ public class DefaultWorkflowProvider implements WorkflowProvider {
log.debugf("Skipping workflow %s as it is disabled", workflow.getName());
return;
}
for (ScheduledStep scheduled : stateProvider.getDueScheduledSteps(workflow)) {
stateProvider.getDueScheduledSteps(workflow).forEach((scheduled) -> {
// check if the resource is still passes the workflow's resource conditions
DefaultWorkflowExecutionContext context = new DefaultWorkflowExecutionContext(session, workflow, scheduled);
EventBasedWorkflow provider = new EventBasedWorkflow(session, getWorkflowComponent(workflow.getId()));
@ -146,7 +165,7 @@ public class DefaultWorkflowProvider implements WorkflowProvider {
runWorkflow(context);
}
}
}
});
});
}
@ -229,7 +248,7 @@ public class DefaultWorkflowProvider implements WorkflowProvider {
private void processEvent(Stream<Workflow> workflows, WorkflowEvent event) {
Map<String, ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByResource(event.getResourceId())
.stream().collect(Collectors.toMap(ScheduledStep::workflowId, Function.identity()));
.collect(Collectors.toMap(ScheduledStep::workflowId, Function.identity()));
workflows.forEach(workflow -> {
if (!workflow.isEnabled()) {

View File

@ -19,7 +19,7 @@ package org.keycloak.models.workflow;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.stream.Stream;
import jakarta.persistence.EntityManager;
import jakarta.persistence.TypedQuery;
@ -85,7 +85,7 @@ public class JpaWorkflowStateProvider implements WorkflowStateProvider {
}
@Override
public List<ScheduledStep> getDueScheduledSteps(Workflow workflow) {
public Stream<ScheduledStep> getDueScheduledSteps(Workflow workflow) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<WorkflowStateEntity> query = cb.createQuery(WorkflowStateEntity.class);
Root<WorkflowStateEntity> stateRoot = query.from(WorkflowStateEntity.class);
@ -96,14 +96,13 @@ public class JpaWorkflowStateProvider implements WorkflowStateProvider {
query.where(cb.and(byWorkflow, isExpired));
return em.createQuery(query).getResultStream()
.map(this::toScheduledStep)
.toList();
.map(this::toScheduledStep);
}
@Override
public List<ScheduledStep> getScheduledStepsByWorkflow(String workflowId) {
public Stream<ScheduledStep> getScheduledStepsByWorkflow(String workflowId) {
if (StringUtil.isBlank(workflowId)) {
return List.of();
return Stream.empty();
}
CriteriaBuilder cb = em.getCriteriaBuilder();
@ -114,12 +113,11 @@ public class JpaWorkflowStateProvider implements WorkflowStateProvider {
query.where(byWorkflow);
return em.createQuery(query).getResultStream()
.map(this::toScheduledStep)
.toList();
.map(this::toScheduledStep);
}
@Override
public List<ScheduledStep> getScheduledStepsByResource(String resourceId) {
public Stream<ScheduledStep> getScheduledStepsByResource(String resourceId) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<WorkflowStateEntity> query = cb.createQuery(WorkflowStateEntity.class);
Root<WorkflowStateEntity> stateRoot = query.from(WorkflowStateEntity.class);
@ -128,8 +126,7 @@ public class JpaWorkflowStateProvider implements WorkflowStateProvider {
query.where(byResource);
return em.createQuery(query).getResultStream()
.map(this::toScheduledStep)
.toList();
.map(this::toScheduledStep);
}
@Override
@ -221,6 +218,7 @@ public class JpaWorkflowStateProvider implements WorkflowStateProvider {
}
private ScheduledStep toScheduledStep(WorkflowStateEntity entity) {
return new ScheduledStep(entity.getWorkflowId(), entity.getScheduledStepId(), entity.getResourceId(), entity.getExecutionId());
return new ScheduledStep(entity.getWorkflowId(), entity.getScheduledStepId(), entity.getResourceId(),
entity.getExecutionId(), entity.getScheduledStepTimestamp());
}
}

View File

@ -119,6 +119,26 @@ public class Workflow {
.map(WorkflowStep::new).sorted();
}
/**
* Get steps starting from the specified stepId (inclusive)
*
* @param stepId the step id to start from
* @return the stream of workflow steps
*/
public Stream<WorkflowStep> getSteps(String stepId) {
boolean[] startAdding = {stepId == null};
return getSteps().filter(step -> {
if (startAdding[0]) {
return true;
}
if (step.getId().equals(stepId)) {
startAdding[0] = true;
return true;
}
return false;
});
}
public WorkflowStep getStepById(String id) {
return getSteps().filter(s -> s.getId().equals(id)).findAny().orElse(null);
}

View File

@ -53,6 +53,8 @@ public interface WorkflowProvider extends Provider {
.skip(first).limit(max);
}
Stream<WorkflowRepresentation> getScheduledWorkflowsByResource(String resourceId);
WorkflowRepresentation toRepresentation(Workflow workflow);
void updateWorkflow(Workflow workflow, WorkflowRepresentation rep);

View File

@ -17,7 +17,7 @@
package org.keycloak.models.workflow;
import java.util.List;
import java.util.stream.Stream;
import org.keycloak.provider.Provider;
@ -69,19 +69,19 @@ public interface WorkflowStateProvider extends Provider {
ScheduledStep getScheduledStep(String workflowId, String resourceId);
List<ScheduledStep> getScheduledStepsByResource(String resourceId);
Stream<ScheduledStep> getScheduledStepsByResource(String resourceId);
List<ScheduledStep> getScheduledStepsByWorkflow(String workflowId);
Stream<ScheduledStep> getScheduledStepsByWorkflow(String workflowId);
default List<ScheduledStep> getScheduledStepsByWorkflow(Workflow workflow) {
default Stream<ScheduledStep> getScheduledStepsByWorkflow(Workflow workflow) {
if (workflow == null) {
return List.of();
return Stream.empty();
}
return getScheduledStepsByWorkflow(workflow.getId());
}
List<ScheduledStep> getDueScheduledSteps(Workflow workflow);
Stream<ScheduledStep> getDueScheduledSteps(Workflow workflow);
record ScheduledStep(String workflowId, String stepId, String resourceId, String executionId) {}
record ScheduledStep(String workflowId, String stepId, String resourceId, String executionId, long scheduledAt) {}
}

View File

@ -83,4 +83,14 @@ public class WorkflowsResource {
int max = Optional.ofNullable(maxResults).orElse(10);
return provider.getWorkflows(search, exact, first, max).map(provider::toRepresentation).toList();
}
@Path("scheduled/{resource-id}")
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<WorkflowRepresentation> getScheduledSteps(
@PathParam("resource-id") String resourceId
) {
auth.realm().requireManageRealm();
return provider.getScheduledWorkflowsByResource(resourceId).toList();
}
}

View File

@ -11,7 +11,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.workflow.NotifyUserStepProviderFactory;
import org.keycloak.models.workflow.ResourceType;
import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory;
import org.keycloak.models.workflow.WorkflowStateProvider;
import org.keycloak.models.workflow.WorkflowProvider;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.workflows.WorkflowRepresentation;
@ -244,10 +244,10 @@ public class AdhocWorkflowTest extends AbstractWorkflowTest {
assertThat(user.getAttributes().keySet(), hasItems("workflowOne", "workflowTwo"));
// Verify that the steps are scheduled for the user
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
List<WorkflowStateProvider.ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByResource(user.getId());
assertNotNull(scheduledSteps, "Two steps should have been scheduled for the user " + user.getUsername());
assertThat(scheduledSteps, hasSize(2));
WorkflowProvider provider = session.getProvider(WorkflowProvider.class);
List<WorkflowRepresentation> scheduledWorkflows = provider.getScheduledWorkflowsByResource(user.getId()).toList();
assertNotNull(scheduledWorkflows, "Two workflow steps should have been scheduled for the user " + user.getUsername());
assertThat(scheduledWorkflows, hasSize(2));
});
//deactivate workflow One
@ -255,9 +255,9 @@ public class AdhocWorkflowTest extends AbstractWorkflowTest {
runOnServer.run(session -> {
// Verify that there is single step scheduled for the user
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
List<WorkflowStateProvider.ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByResource(id);
assertThat(scheduledSteps, hasSize(1));
WorkflowProvider provider = session.getProvider(WorkflowProvider.class);
List<WorkflowRepresentation> scheduledWorkflows = provider.getScheduledWorkflowsByResource(id).toList();
assertThat(scheduledWorkflows, hasSize(1));
});
}

View File

@ -31,6 +31,7 @@ import org.keycloak.models.workflow.ResourceOperationType;
import org.keycloak.models.workflow.Workflow;
import org.keycloak.models.workflow.WorkflowProvider;
import org.keycloak.models.workflow.WorkflowStateProvider;
import org.keycloak.models.workflow.WorkflowStateProvider.ScheduledStep;
import org.keycloak.models.workflow.conditions.IdentityProviderWorkflowConditionFactory;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
@ -253,7 +254,7 @@ public class BrokeredUserSessionRefreshTimeWorkflowTest extends AbstractWorkflow
// alice should be associated with the workflow
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
WorkflowStateProvider.ScheduledStep scheduledStep = stateProvider.getScheduledStep(workflow.getId(), alice.getId());
ScheduledStep scheduledStep = stateProvider.getScheduledStep(workflow.getId(), alice.getId());
assertNotNull(scheduledStep, "A step should have been scheduled for the user " + alice.getUsername());
});

View File

@ -209,7 +209,7 @@ public class DeleteUserWorkflowStepTest extends AbstractWorkflowTest {
assertEquals(2, registeredWorkflows.size());
WorkflowStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session);
List<WorkflowStateProvider.ScheduledStep> steps = stateProvider.getScheduledStepsByResource(userId);
List<WorkflowStateProvider.ScheduledStep> steps = stateProvider.getScheduledStepsByResource(userId).toList();
assertThat(steps, hasSize(2));
});
@ -237,7 +237,7 @@ public class DeleteUserWorkflowStepTest extends AbstractWorkflowTest {
}
WorkflowStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session);
List<WorkflowStateProvider.ScheduledStep> steps = stateProvider.getScheduledStepsByResource(userId);
List<WorkflowStateProvider.ScheduledStep> steps = stateProvider.getScheduledStepsByResource(userId).toList();
assertThat(steps, hasSize(0));
});
}

View File

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

View File

@ -18,6 +18,7 @@ import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory;
import org.keycloak.models.workflow.Workflow;
import org.keycloak.models.workflow.WorkflowProvider;
import org.keycloak.models.workflow.WorkflowStateProvider;
import org.keycloak.models.workflow.WorkflowStateProvider.ScheduledStep;
import org.keycloak.models.workflow.conditions.RoleWorkflowConditionFactory;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
@ -112,7 +113,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<WorkflowStateProvider.ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow);
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow).toList();
assertThat(scheduledSteps, hasSize(10));
});
}

View File

@ -17,6 +17,7 @@ import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory;
import org.keycloak.models.workflow.Workflow;
import org.keycloak.models.workflow.WorkflowProvider;
import org.keycloak.models.workflow.WorkflowStateProvider;
import org.keycloak.models.workflow.WorkflowStateProvider.ScheduledStep;
import org.keycloak.models.workflow.conditions.UserAttributeWorkflowConditionFactory;
import org.keycloak.representations.userprofile.config.UPConfig;
import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy;
@ -106,7 +107,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<WorkflowStateProvider.ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow);
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow).toList();
assertThat(scheduledSteps, hasSize(10));
});
}

View File

@ -97,7 +97,7 @@ public class UserSessionRefreshTimeWorkflowTest extends AbstractWorkflowTest {
// 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);
List< WorkflowStateProvider.ScheduledStep> steps = provider.getScheduledStepsByResource(userId).toList();
assertThat(steps, hasSize(1));
return steps.get(0).stepId();
}, String.class);
@ -112,7 +112,7 @@ public class UserSessionRefreshTimeWorkflowTest extends AbstractWorkflowTest {
assertTrue(user.isEnabled());
WorkflowStateProvider provider = session.getProvider(WorkflowStateProvider.class);
List< WorkflowStateProvider.ScheduledStep> steps = provider.getScheduledStepsByResource(userId);
List< WorkflowStateProvider.ScheduledStep> steps = provider.getScheduledStepsByResource(userId).toList();
assertThat(steps, hasSize(1));
return steps.get(0).stepId();
}, String.class);
@ -128,7 +128,7 @@ public class UserSessionRefreshTimeWorkflowTest extends AbstractWorkflowTest {
runOnServer.run(session -> {
WorkflowStateProvider provider = session.getProvider(WorkflowStateProvider.class);
List< WorkflowStateProvider.ScheduledStep> steps = provider.getScheduledStepsByResource(userId);
List< WorkflowStateProvider.ScheduledStep> steps = provider.getScheduledStepsByResource(userId).toList();
// step id must remain the same as before
assertThat(steps, hasSize(1));
assertThat(steps.get(0).stepId(), is(secondStepId));
@ -140,7 +140,7 @@ public class UserSessionRefreshTimeWorkflowTest extends AbstractWorkflowTest {
// 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);
List< WorkflowStateProvider.ScheduledStep> steps = provider.getScheduledStepsByResource(userId).toList();
// step id must be the first one now as the workflow was restarted
assertThat(steps, hasSize(1));
assertThat(steps.get(0).stepId(), is(firstStepId));

View File

@ -85,6 +85,7 @@ import static org.keycloak.models.workflow.ResourceOperationType.USER_ADDED;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
@ -221,7 +222,7 @@ public class WorkflowManagementTest extends AbstractWorkflowTest {
List<Workflow> registeredWorkflows = provider.getWorkflows().toList();
assertEquals(1, registeredWorkflows.size());
WorkflowStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session);
List<ScheduledStep> steps = stateProvider.getScheduledStepsByWorkflow(workflowId);
List<ScheduledStep> steps = stateProvider.getScheduledStepsByWorkflow(workflowId).toList();
assertTrue(steps.isEmpty());
});
}
@ -364,14 +365,8 @@ public class WorkflowManagementTest extends AbstractWorkflowTest {
managedRealm.admin().workflows().workflow(workflowId).activate(ResourceType.USERS.name(), userAlice.getId());
// check step has been scheduled for the user
runOnServer.run((RunOnServer) session -> {
RealmModel realm = session.getContext().getRealm();
UserModel user = session.users().getUserByUsername(realm, "alice");
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByResource(user.getId());
assertThat("A step should have been scheduled for the user " + user.getUsername(), scheduledSteps, hasSize(1));
});
List<WorkflowRepresentation> scheduledSteps = managedRealm.admin().workflows().getScheduledWorkflows(userAlice.getId());
assertThat("A step should have been scheduled for the user " + userAlice.getUsername(), scheduledSteps, hasSize(1));
// now update the workflow to add a condition that will make the user no longer eligible
WorkflowRepresentation workflow = managedRealm.admin().workflows().workflow(workflowId).toRepresentation();
@ -382,15 +377,8 @@ public class WorkflowManagementTest extends AbstractWorkflowTest {
runScheduledSteps(Duration.ofDays(6));
// check the user is still enabled and no scheduled steps exist
runOnServer.run((RunOnServer) session -> {
RealmModel realm = session.getContext().getRealm();
UserModel user = session.users().getUserByUsername(realm, "alice");
assertThat(user.isEnabled(), is(true));
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByResource(user.getId());
assertThat(scheduledSteps, empty());
});
scheduledSteps = managedRealm.admin().workflows().getScheduledWorkflows(userAlice.getId());
assertThat(scheduledSteps, empty());
}
@ -434,6 +422,54 @@ public class WorkflowManagementTest extends AbstractWorkflowTest {
assertThat(representations.get(0).getName(), is("gamma-workflow"));
}
@Test
public void testGetActiveWorkflowsForResource() {
// create a few of simple workflows
String[] workflowNames = {"workflow-one", "workflow-two", "workflow-three", "workflow-four"};
for (String name : workflowNames) {
String workflowId;
try (Response response =
managedRealm.admin().workflows().create(WorkflowRepresentation.withName(name)
.withSteps(
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
.after(Duration.ofDays(5))
.build(),
WorkflowStepRepresentation.create().of(SetUserAttributeStepProviderFactory.ID)
.withConfig("key", "value")
.build(),
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
.after(Duration.ofDays(15))
.build()
).build())) {
workflowId = ApiUtil.getCreatedId(response);
}
// bind all workflows, except the second one, to user alice
if (!name.equals("workflow-two")) {
managedRealm.admin().workflows().workflow(workflowId).activate(ResourceType.USERS.name(), userAlice.getId());
}
}
// use the API to fetch the active workflows for the user
List<WorkflowRepresentation> scheduledWorkflows = managedRealm.admin().workflows().getScheduledWorkflows(userAlice.getId());
assertThat(scheduledWorkflows, hasSize(3));
// assert that "workflow-two" is not among them
assertTrue(scheduledWorkflows.stream().noneMatch(step -> step.getName().equals("workflow-two")));
// assert that all workflows have the scheduledAt set to a positive value, and that the first and second steps are scheduled for the same time
for (WorkflowRepresentation workflow : scheduledWorkflows) {
assertThat(workflow.getSteps(), hasSize(3));
assertThat(workflow.getSteps().get(0).getScheduledAt(), greaterThan(0L));
assertThat(workflow.getSteps().get(1).getScheduledAt(), greaterThan(0L));
assertThat(workflow.getSteps().get(0).getScheduledAt(), equalTo(workflow.getSteps().get(1).getScheduledAt()));
// the third step have a scheduledAt higher than the previous two
assertThat(workflow.getSteps().get(2).getScheduledAt(), greaterThan(workflow.getSteps().get(1).getScheduledAt()));
// it should be precisely 15 days after the second step
long expectedThirdStepScheduledAt = workflow.getSteps().get(1).getScheduledAt() + Duration.ofDays(15).toMillis();
assertThat(workflow.getSteps().get(2).getScheduledAt(), is(expectedThirdStepScheduledAt));
}
}
@Test
public void testWorkflowDoesNotFallThroughStepsInSingleRun() {
@ -558,7 +594,7 @@ public class WorkflowManagementTest 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);
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow).toList();
assertEquals(3, scheduledSteps.size());
scheduledSteps.forEach(scheduledStep -> {
assertEquals(notifyStep.getId(), scheduledStep.stepId());
@ -581,7 +617,7 @@ public class WorkflowManagementTest extends AbstractWorkflowTest {
Workflow workflow = registeredWorkflows.get(0);
WorkflowStep disableStep = workflow.getSteps().toList().get(1);
WorkflowStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session);
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow);
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow).toList();
assertEquals(3, scheduledSteps.size());
scheduledSteps.forEach(scheduledStep -> {
assertEquals(disableStep.getId(), scheduledStep.stepId());
@ -608,7 +644,7 @@ public class WorkflowManagementTest 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);
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow).toList();
assertEquals(13, scheduledSteps.size());
List<WorkflowStep> steps = workflow.getSteps().toList();
@ -693,7 +729,7 @@ public class WorkflowManagementTest extends AbstractWorkflowTest {
List<Workflow> registeredWorkflow = provider.getWorkflows().toList();
assertEquals(1, registeredWorkflow.size());
WorkflowStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session);
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(registeredWorkflow.get(0));
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(registeredWorkflow.get(0)).toList();
// verify that there's only one scheduled step, for the first user
assertEquals(1, scheduledSteps.size());
@ -915,18 +951,17 @@ public class WorkflowManagementTest extends AbstractWorkflowTest {
.build()
).build()).close();
managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser4").name("NoEmail", "").build()).close();
String userId;
try (Response response = managedRealm.admin().users().create(UserConfigBuilder.create().username("testuser4").name("NoEmail", "").build())) {
userId = ApiUtil.getCreatedId(response);
}
runScheduledSteps(Duration.ofDays(5));
runOnServer.run(session -> {
RealmModel realm = session.getContext().getRealm();
// But should still create state record for the workflow flow
UserModel user = session.users().getUserByUsername(realm, "testuser4");
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
var scheduledSteps = stateProvider.getScheduledStepsByResource(user.getId());
assertEquals(1, scheduledSteps.size());
});
List<WorkflowRepresentation> scheduledWorkflows = managedRealm.admin().workflows().getScheduledWorkflows(userId);
assertThat(scheduledWorkflows, hasSize(1));
List<WorkflowStepRepresentation> steps = scheduledWorkflows.get(0).getSteps();
assertThat(steps, hasSize(1));
assertEquals(DisableUserStepProviderFactory.ID, steps.get(0).getUses());
// Should NOT send email to user without email address
MimeMessage testUserMessage = findEmailByRecipientContaining("testuser4");