mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
Allow defining steps in a workflow that can run immediate or scheduled
Closes #42888 Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
parent
27796c5dce
commit
80453bdbfb
@ -7,7 +7,6 @@ import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_NA
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_ON_EVENT;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_RECURRING;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_RESET_ON;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_SCHEDULED;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_STATE;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_STEPS;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_WITH;
|
||||
@ -28,7 +27,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
|
||||
@JsonPropertyOrder({"id", CONFIG_NAME, CONFIG_USES, CONFIG_ENABLED, CONFIG_ON_EVENT, CONFIG_RESET_ON, CONFIG_SCHEDULED, CONFIG_RECURRING, CONFIG_IF, CONFIG_STEPS, CONFIG_STATE})
|
||||
@JsonPropertyOrder({"id", CONFIG_NAME, CONFIG_USES, CONFIG_ENABLED, CONFIG_ON_EVENT, CONFIG_RESET_ON, CONFIG_RECURRING, CONFIG_IF, CONFIG_STEPS, CONFIG_STATE})
|
||||
@JsonIgnoreProperties(CONFIG_WITH)
|
||||
public final class WorkflowRepresentation extends AbstractWorkflowComponentRepresentation {
|
||||
|
||||
@ -107,14 +106,6 @@ public final class WorkflowRepresentation extends AbstractWorkflowComponentRepre
|
||||
setConfigValue(CONFIG_RECURRING, recurring);
|
||||
}
|
||||
|
||||
public Boolean getScheduled() {
|
||||
return getConfigValue(CONFIG_SCHEDULED, Boolean.class);
|
||||
}
|
||||
|
||||
public void setScheduled(Boolean scheduled) {
|
||||
setConfigValue(CONFIG_SCHEDULED, scheduled);
|
||||
}
|
||||
|
||||
public Boolean getEnabled() {
|
||||
return getConfigValue(CONFIG_ENABLED, Boolean.class);
|
||||
}
|
||||
@ -207,11 +198,6 @@ public final class WorkflowRepresentation extends AbstractWorkflowComponentRepre
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder immediate() {
|
||||
representation.setScheduled(false);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder recurring() {
|
||||
representation.setRecurring(true);
|
||||
return this;
|
||||
|
||||
@ -8,7 +8,6 @@ import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_WI
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
@ -22,26 +21,16 @@ public final class WorkflowStepRepresentation extends AbstractWorkflowComponentR
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
private List<WorkflowStepRepresentation> steps;
|
||||
|
||||
public WorkflowStepRepresentation() {
|
||||
super(null, null, null);
|
||||
this(null, null, null);
|
||||
}
|
||||
|
||||
public WorkflowStepRepresentation(String step) {
|
||||
this(step, null);
|
||||
public WorkflowStepRepresentation(String uses) {
|
||||
this(null, uses, null);
|
||||
}
|
||||
|
||||
public WorkflowStepRepresentation(String step, MultivaluedHashMap<String, String> config) {
|
||||
this(null, step, config, null);
|
||||
}
|
||||
|
||||
public WorkflowStepRepresentation(String id, String step, MultivaluedHashMap<String, String> config, List<WorkflowStepRepresentation> steps) {
|
||||
super(id, step, config);
|
||||
|
||||
if (steps != null && !steps.isEmpty()) {
|
||||
this.steps = steps;
|
||||
}
|
||||
public WorkflowStepRepresentation(String id, String uses, MultivaluedHashMap<String, String> config) {
|
||||
super(id, uses, config);
|
||||
}
|
||||
|
||||
@JsonSerialize(using = MultivaluedHashMapValueSerializer.class)
|
||||
@ -66,14 +55,6 @@ public final class WorkflowStepRepresentation extends AbstractWorkflowComponentR
|
||||
setConfig(CONFIG_PRIORITY, String.valueOf(ms));
|
||||
}
|
||||
|
||||
public List<WorkflowStepRepresentation> getSteps() {
|
||||
return steps;
|
||||
}
|
||||
|
||||
public void setSteps(List<WorkflowStepRepresentation> steps) {
|
||||
this.steps = steps;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private WorkflowStepRepresentation step;
|
||||
@ -112,11 +93,6 @@ public final class WorkflowStepRepresentation extends AbstractWorkflowComponentR
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withSteps(WorkflowStepRepresentation... steps) {
|
||||
step.setSteps(Arrays.asList(steps));
|
||||
return this;
|
||||
}
|
||||
|
||||
public WorkflowStepRepresentation build() {
|
||||
return step;
|
||||
}
|
||||
|
||||
@ -8,13 +8,11 @@ import static org.junit.Assert.assertTrue;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
public class WorkflowDefinitionTest {
|
||||
@ -31,7 +29,6 @@ public class WorkflowDefinitionTest {
|
||||
expected.setSteps(null);
|
||||
expected.setConditions(null);
|
||||
expected.setRecurring(true);
|
||||
expected.setScheduled(true);
|
||||
expected.setEnabled(true);
|
||||
|
||||
expected.setConditions(Arrays.asList(
|
||||
@ -77,10 +74,9 @@ public class WorkflowDefinitionTest {
|
||||
assertEquals(expected.getUses(), actual.getUses());
|
||||
assertTrue(actual.getOn() instanceof String);
|
||||
assertEquals(expected.getOn(), (String) actual.getOn());
|
||||
assertArrayEquals(((List) expected.getOnEventReset()).toArray(), ((List) actual.getOnEventReset()).toArray());
|
||||
assertArrayEquals(((List<?>) expected.getOnEventReset()).toArray(), ((List<?>) actual.getOnEventReset()).toArray());
|
||||
assertEquals(expected.getName(), actual.getName());
|
||||
assertEquals(expected.getRecurring(), actual.getRecurring());
|
||||
assertEquals(expected.getScheduled(), actual.getScheduled());
|
||||
assertEquals(expected.getEnabled(), actual.getEnabled());
|
||||
|
||||
List<WorkflowConditionRepresentation> actualConditions = actual.getConditions();
|
||||
@ -140,33 +136,4 @@ public class WorkflowDefinitionTest {
|
||||
System.out.println(json);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAggregatedAction() throws IOException {
|
||||
WorkflowRepresentation expected = new WorkflowRepresentation();
|
||||
MultivaluedHashMap<String, String> config = new MultivaluedHashMap<>();
|
||||
config.put("k1", Collections.singletonList("v1"));
|
||||
WorkflowStepRepresentation aggregated = new WorkflowStepRepresentation("step-1", config);
|
||||
|
||||
config = new MultivaluedHashMap<>();
|
||||
config.put("k1", Collections.singletonList("v1"));
|
||||
aggregated.setSteps(Arrays.asList(new WorkflowStepRepresentation("sub-step-1", new MultivaluedHashMap<>(config)), new WorkflowStepRepresentation("sub-step-2", new MultivaluedHashMap<>(config))));
|
||||
|
||||
expected.setSteps(Collections.singletonList(aggregated));
|
||||
|
||||
String json = JsonSerialization.writeValueAsPrettyString(expected);
|
||||
WorkflowRepresentation actual = JsonSerialization.readValue(json, WorkflowRepresentation.class);
|
||||
|
||||
List<WorkflowStepRepresentation> actualSteps = actual.getSteps();
|
||||
assertNotNull(actualSteps);
|
||||
actualSteps = actualSteps.stream().sorted(Comparator.comparing(WorkflowStepRepresentation::getUses)).collect(Collectors.toList());
|
||||
List<WorkflowStepRepresentation> expectedSteps = expected.getSteps().stream().sorted(Comparator.comparing(WorkflowStepRepresentation::getUses)).collect(Collectors.toList());
|
||||
|
||||
assertEquals(expectedSteps.size(), actualSteps.size());
|
||||
assertEquals(expectedSteps.get(0).getUses(), actualSteps.get(0).getUses());
|
||||
assertEquals(expectedSteps.get(0).getConfig().get("k1"), actualSteps.get(0).getConfig().get("k1"));
|
||||
assertEquals(expectedSteps.get(0).getSteps().size(), actualSteps.get(0).getSteps().size());
|
||||
assertEquals(expectedSteps.get(0).getSteps().get(0).getConfig().get("k1"), actualSteps.get(0).getSteps().get(0).getConfig().get("k1"));
|
||||
|
||||
System.out.println(json);
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,6 @@ package org.keycloak.models.workflow;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_ENABLED;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_ERROR;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_RECURRING;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_SCHEDULED;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -31,20 +30,10 @@ import org.keycloak.component.ComponentModel;
|
||||
public class Workflow {
|
||||
|
||||
private MultivaluedHashMap<String, String> config;
|
||||
private String providerId;
|
||||
private final String providerId;
|
||||
private String id;
|
||||
private Long notBefore;
|
||||
|
||||
public Workflow() {
|
||||
// reflection
|
||||
}
|
||||
|
||||
public Workflow(String providerId) {
|
||||
this.providerId = providerId;
|
||||
this.id = null;
|
||||
this.config = null;
|
||||
}
|
||||
|
||||
public Workflow(ComponentModel c) {
|
||||
this.id = c.getId();
|
||||
this.providerId = c.getProviderId();
|
||||
@ -78,10 +67,6 @@ public class Workflow {
|
||||
return config != null && Boolean.parseBoolean(config.getFirst(CONFIG_RECURRING));
|
||||
}
|
||||
|
||||
public boolean isScheduled() {
|
||||
return config != null && Boolean.parseBoolean(config.getFirstOrDefault(CONFIG_SCHEDULED, "true"));
|
||||
}
|
||||
|
||||
public Long getNotBefore() {
|
||||
return notBefore;
|
||||
}
|
||||
|
||||
@ -20,26 +20,18 @@ package org.keycloak.models.workflow;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_AFTER;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_PRIORITY;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
|
||||
public class WorkflowStep implements Comparable<WorkflowStep> {
|
||||
|
||||
private String id;
|
||||
private String providerId;
|
||||
private final String providerId;
|
||||
private MultivaluedHashMap<String, String> config;
|
||||
private List<WorkflowStep> steps = List.of();
|
||||
|
||||
public WorkflowStep() {
|
||||
// reflection
|
||||
}
|
||||
|
||||
public WorkflowStep(String providerId, MultivaluedHashMap<String, String> config, List<WorkflowStep> steps) {
|
||||
public WorkflowStep(String providerId, MultivaluedHashMap<String, String> config) {
|
||||
this.providerId = providerId;
|
||||
this.config = config;
|
||||
this.steps = steps;
|
||||
}
|
||||
|
||||
public WorkflowStep(ComponentModel model) {
|
||||
@ -95,17 +87,6 @@ public class WorkflowStep implements Comparable<WorkflowStep> {
|
||||
return Long.valueOf(getConfig().getFirstOrDefault(CONFIG_AFTER, "0"));
|
||||
}
|
||||
|
||||
public List<WorkflowStep> getSteps() {
|
||||
if (steps == null) {
|
||||
return List.of();
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
|
||||
public void setSteps(List<WorkflowStep> steps) {
|
||||
this.steps = steps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(WorkflowStep other) {
|
||||
return Integer.compare(this.getPriority(), other.getPriority());
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
||||
public class AggregatedStepProvider implements WorkflowStepProvider {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final ComponentModel model;
|
||||
private final Logger log = Logger.getLogger(AggregatedStepProvider.class);
|
||||
|
||||
public AggregatedStepProvider(KeycloakSession session, ComponentModel model) {
|
||||
this.session = session;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(List<String> userIds) {
|
||||
List<WorkflowStepProvider> steps = getSteps();
|
||||
|
||||
for (String userId : userIds) {
|
||||
for (WorkflowStepProvider step : steps) {
|
||||
try {
|
||||
step.run(List.of(userId));
|
||||
} catch (Exception e) {
|
||||
log.errorf(e, "Failed to execute step %s for user %s", model.getProviderId(), userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<WorkflowStepProvider> getSteps() {
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
return manager.getStepById(model.getId())
|
||||
.getSteps().stream()
|
||||
.map(manager::getStepProvider)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
public class AggregatedStepProviderFactory implements WorkflowStepProviderFactory<AggregatedStepProvider> {
|
||||
|
||||
public static final String ID = "aggregated";
|
||||
|
||||
@Override
|
||||
public AggregatedStepProvider create(KeycloakSession session, ComponentModel model) {
|
||||
return new AggregatedStepProvider(session, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceType getType() {
|
||||
return ResourceType.USERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
@ -44,10 +44,7 @@ import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_AFTER;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_CONDITIONS;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_RECURRING;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_SCHEDULED;
|
||||
|
||||
public class WorkflowsManager {
|
||||
|
||||
@ -87,7 +84,7 @@ public class WorkflowsManager {
|
||||
}
|
||||
|
||||
// This method takes an ordered list of steps. First step in the list has the highest priority, last step has the lowest priority
|
||||
private void addSteps(Workflow workflow, String parentId, List<WorkflowStep> steps) {
|
||||
private void addSteps(Workflow workflow, List<WorkflowStep> steps) {
|
||||
for (int i = 0; i < steps.size(); i++) {
|
||||
WorkflowStep step = steps.get(i);
|
||||
|
||||
@ -95,33 +92,27 @@ public class WorkflowsManager {
|
||||
step.setPriority(i + 1);
|
||||
|
||||
// persist the new step component.
|
||||
step = addStep(parentId, step);
|
||||
|
||||
addSteps(workflow, step.getId(), step.getSteps());
|
||||
addStep(workflow, step);
|
||||
}
|
||||
}
|
||||
|
||||
private WorkflowStep addStep(String parentId, WorkflowStep step) {
|
||||
private WorkflowStep addStep(Workflow workflow, WorkflowStep step) {
|
||||
RealmModel realm = getRealm();
|
||||
ComponentModel parentModel = realm.getComponent(parentId);
|
||||
ComponentModel workflowModel = realm.getComponent(workflow.getId());
|
||||
|
||||
if (parentModel == null) {
|
||||
throw new ModelValidationException("Parent component not found: " + parentId);
|
||||
if (workflowModel == null) {
|
||||
throw new ModelValidationException("Workflow with id '%s' not found.".formatted(workflow.getId()));
|
||||
}
|
||||
|
||||
ComponentModel stepModel = new ComponentModel();
|
||||
|
||||
stepModel.setId(step.getId());//need to keep stable UUIDs not to break a link in state table
|
||||
stepModel.setParentId(parentModel.getId());
|
||||
stepModel.setParentId(workflowModel.getId());
|
||||
stepModel.setProviderId(step.getProviderId());
|
||||
stepModel.setProviderType(WorkflowStepProvider.class.getName());
|
||||
stepModel.setConfig(step.getConfig());
|
||||
|
||||
WorkflowStep persisted = new WorkflowStep(realm.addComponentModel(stepModel));
|
||||
|
||||
persisted.setSteps(step.getSteps());
|
||||
|
||||
return persisted;
|
||||
return new WorkflowStep(realm.addComponentModel(stepModel));
|
||||
}
|
||||
|
||||
public List<Workflow> getWorkflows() {
|
||||
@ -141,11 +132,7 @@ public class WorkflowsManager {
|
||||
}
|
||||
|
||||
private WorkflowStep toStep(ComponentModel model) {
|
||||
WorkflowStep step = new WorkflowStep(model);
|
||||
|
||||
step.setSteps(getSteps(step.getId()));
|
||||
|
||||
return step;
|
||||
return new WorkflowStep(model);
|
||||
}
|
||||
|
||||
public WorkflowStep getStepById(String id) {
|
||||
@ -160,14 +147,7 @@ public class WorkflowsManager {
|
||||
}
|
||||
|
||||
private WorkflowStep getFirstStep(Workflow workflow) {
|
||||
WorkflowStep step = getSteps(workflow.getId()).get(0);
|
||||
Long notBefore = workflow.getNotBefore();
|
||||
|
||||
if (notBefore != null) {
|
||||
step.setAfter(notBefore);
|
||||
}
|
||||
|
||||
return step;
|
||||
return getSteps(workflow.getId()).get(0);
|
||||
}
|
||||
|
||||
private WorkflowProvider getWorkflowProvider(Workflow workflow) {
|
||||
@ -228,18 +208,36 @@ public class WorkflowsManager {
|
||||
if (!currentlyAssignedWorkflows.contains(workflow.getId())) {
|
||||
// if workflow is not active for the resource, check if the provider allows activating based on the event
|
||||
if (provider.activateOnEvent(event)) {
|
||||
if (workflow.isScheduled()) {
|
||||
// workflow is scheduled, so we schedule the first step
|
||||
log.debugf("Scheduling first step of workflow %s for resource %s based on event %s",
|
||||
workflow.getId(), event.getResourceId(), event.getOperation());
|
||||
workflowStateProvider.scheduleStep(workflow, getFirstStep(workflow), event.getResourceId());
|
||||
} else {
|
||||
// workflow is not scheduled, so we run all steps immediately
|
||||
log.debugf("Running all steps of workflow %s for resource %s based on event %s",
|
||||
workflow.getId(), event.getResourceId(), event.getOperation());
|
||||
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), session.getContext(), s ->
|
||||
getSteps(workflow.getId()).forEach(step -> getStepProvider(step).run(List.of(event.getResourceId())))
|
||||
);
|
||||
WorkflowStep firstStep = getFirstStep(workflow);
|
||||
for (WorkflowStep step : getSteps(workflow.getId())) {
|
||||
// If the workflow has a notBefore set, schedule the first step with it
|
||||
if (step.getId().equals(firstStep.getId()) && workflow.getNotBefore() != null && workflow.getNotBefore() > 0) {
|
||||
log.debugf("Scheduling first step %s of workflow %s for resource %s based on on event %s with notBefore %d",
|
||||
step.getId(), workflow.getId(), event.getResourceId(), event.getOperation(), workflow.getNotBefore());
|
||||
Long originalAfter = step.getAfter();
|
||||
try {
|
||||
step.setAfter(workflow.getNotBefore());
|
||||
workflowStateProvider.scheduleStep(workflow, step, event.getResourceId());
|
||||
continue;
|
||||
} finally {
|
||||
// restore the original after value
|
||||
step.setAfter(originalAfter);
|
||||
}
|
||||
}
|
||||
if (step.getAfter() > 0) {
|
||||
// If a step has a time defined, schedule it and stop processing the other steps of workflow
|
||||
log.debugf("Scheduling step %s of workflow %s for resource %s based on event %s",
|
||||
step.getId(), workflow.getId(), event.getResourceId(), event.getOperation());
|
||||
workflowStateProvider.scheduleStep(workflow, step, event.getResourceId());
|
||||
break;
|
||||
} else {
|
||||
// Otherwise run the step right away
|
||||
log.debugf("Running step %s of workflow %s for resource %s based on event %s",
|
||||
step.getId(), workflow.getId(), event.getResourceId(), event.getOperation());
|
||||
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), session.getContext(), s ->
|
||||
getStepProvider(step).run(List.of(event.getResourceId()))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -261,33 +259,42 @@ public class WorkflowsManager {
|
||||
public void runScheduledSteps() {
|
||||
this.getWorkflows().stream().filter(Workflow::isEnabled).forEach(workflow -> {
|
||||
|
||||
for (ScheduledStep scheduled : workflowStateProvider.getDueScheduledSteps(workflow)) {
|
||||
List<WorkflowStep> steps = getSteps(workflow.getId());
|
||||
for (ScheduledStep scheduled : workflowStateProvider.getDueScheduledSteps(workflow)) {
|
||||
List<WorkflowStep> steps = getSteps(workflow.getId());
|
||||
|
||||
for (int i = 0; i < steps.size(); i++) {
|
||||
WorkflowStep currentStep = steps.get(i);
|
||||
for (int i = 0; i < steps.size(); i++) {
|
||||
WorkflowStep currentStep = steps.get(i);
|
||||
|
||||
if (currentStep.getId().equals(scheduled.stepId())) {
|
||||
getStepProvider(currentStep).run(List.of(scheduled.resourceId()));
|
||||
if (currentStep.getId().equals(scheduled.stepId())) {
|
||||
getStepProvider(currentStep).run(List.of(scheduled.resourceId()));
|
||||
|
||||
if (steps.size() > i + 1) {
|
||||
// schedule the next step using the time offset difference between the steps.
|
||||
WorkflowStep nextStep = steps.get(i + 1);
|
||||
workflowStateProvider.scheduleStep(workflow, nextStep, scheduled.resourceId());
|
||||
} else {
|
||||
// this was the last step, check if the workflow is recurring - i.e. if we need to schedule the first step again
|
||||
if (workflow.isRecurring()) {
|
||||
WorkflowStep firstStep = getFirstStep(workflow);
|
||||
workflowStateProvider.scheduleStep(workflow, firstStep, scheduled.resourceId());
|
||||
} else {
|
||||
// not recurring, remove the state record
|
||||
workflowStateProvider.remove(workflow.getId(), scheduled.resourceId());
|
||||
int nextIndex = i + 1;
|
||||
// Process subsequent steps: run immediately if no time condition, schedule if time condition
|
||||
while (nextIndex < steps.size()) {
|
||||
WorkflowStep nextStep = steps.get(nextIndex);
|
||||
if (nextStep.getAfter() > 0) {
|
||||
workflowStateProvider.scheduleStep(workflow, nextStep, scheduled.resourceId());
|
||||
break;
|
||||
} else {
|
||||
getStepProvider(nextStep).run(List.of(scheduled.resourceId()));
|
||||
nextIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextIndex == steps.size()) {
|
||||
// this was the last step, check if the workflow is recurring - i.e. if we need to schedule the first step again
|
||||
if (workflow.isRecurring()) {
|
||||
WorkflowStep firstStep = getFirstStep(workflow);
|
||||
workflowStateProvider.scheduleStep(workflow, firstStep, scheduled.resourceId());
|
||||
} else {
|
||||
// not recurring, remove the state record
|
||||
workflowStateProvider.remove(workflow.getId(), scheduled.resourceId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void removeWorkflow(String id) {
|
||||
@ -306,7 +313,7 @@ public class WorkflowsManager {
|
||||
}
|
||||
|
||||
public void updateWorkflow(Workflow workflow, MultivaluedHashMap<String, String> config) {
|
||||
validateWorkflow(toRepresentation(workflow), config);
|
||||
validateWorkflow(toRepresentation(workflow));
|
||||
ComponentModel component = getWorkflowComponent(workflow.getId());
|
||||
component.setConfig(config);
|
||||
getRealm().updateComponent(component);
|
||||
@ -370,16 +377,15 @@ public class WorkflowsManager {
|
||||
}
|
||||
|
||||
public WorkflowStepRepresentation toRepresentation(WorkflowStep step) {
|
||||
List<WorkflowStepRepresentation> steps = step.getSteps().stream().map(this::toRepresentation).toList();
|
||||
return new WorkflowStepRepresentation(step.getId(), step.getProviderId(), step.getConfig(), steps);
|
||||
return new WorkflowStepRepresentation(step.getId(), step.getProviderId(), step.getConfig());
|
||||
}
|
||||
|
||||
public Workflow toModel(WorkflowRepresentation rep) {
|
||||
validateWorkflow(rep);
|
||||
|
||||
MultivaluedHashMap<String, String> config = ofNullable(rep.getConfig()).orElse(new MultivaluedHashMap<>());
|
||||
List<WorkflowConditionRepresentation> conditions = ofNullable(rep.getConditions()).orElse(List.of());
|
||||
|
||||
validateWorkflow(rep, config);
|
||||
|
||||
for (WorkflowConditionRepresentation condition : conditions) {
|
||||
String conditionProviderId = condition.getUses();
|
||||
getConditionProviderFactory(conditionProviderId);
|
||||
@ -391,42 +397,19 @@ public class WorkflowsManager {
|
||||
}
|
||||
|
||||
Workflow workflow = addWorkflow(rep.getUses(), config);
|
||||
List<WorkflowStep> steps = new ArrayList<>();
|
||||
|
||||
for (WorkflowStepRepresentation stepRep : rep.getSteps()) {
|
||||
steps.add(toModel(workflow, stepRep));
|
||||
}
|
||||
List<WorkflowStep> steps = rep.getSteps().stream().map(this::toModel).toList();
|
||||
|
||||
addSteps(workflow, workflow.getId(), steps);
|
||||
addSteps(workflow, steps);
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
private void validateWorkflow(WorkflowRepresentation rep, MultivaluedHashMap<String, String> config) {
|
||||
// Validations:
|
||||
// workflow cannot be both immediate and recurring
|
||||
// immediate workflow cannot have time conditions
|
||||
// all steps of scheduled workflow must have time condition
|
||||
|
||||
boolean isImmediate = config.containsKey(CONFIG_SCHEDULED) && !Boolean.parseBoolean(config.getFirst(CONFIG_SCHEDULED));
|
||||
boolean isRecurring = config.containsKey(CONFIG_RECURRING) && Boolean.parseBoolean(config.getFirst(CONFIG_RECURRING));
|
||||
boolean hasTimeCondition = rep.getSteps().stream().allMatch(step -> step.getConfig() != null
|
||||
&& step.getConfig().containsKey(CONFIG_AFTER));
|
||||
if (isImmediate && isRecurring) {
|
||||
throw new WorkflowInvalidStateException("Workflow cannot be both immediate and recurring.");
|
||||
}
|
||||
if (isImmediate && hasTimeCondition) {
|
||||
throw new WorkflowInvalidStateException("Immediate workflow cannot have steps with time conditions.");
|
||||
}
|
||||
if (!isImmediate && !hasTimeCondition) {
|
||||
throw new WorkflowInvalidStateException("Scheduled workflow cannot have steps without time conditions.");
|
||||
}
|
||||
|
||||
private void validateWorkflow(WorkflowRepresentation rep) {
|
||||
validateEvents(rep.getOnValues());
|
||||
validateEvents(rep.getOnEventsReset());
|
||||
}
|
||||
|
||||
|
||||
private static void validateEvents(List<String> events) {
|
||||
for (String event : ofNullable(events).orElse(List.of())) {
|
||||
try {
|
||||
@ -447,52 +430,19 @@ public class WorkflowsManager {
|
||||
return type.resolveResource(session, resourceId);
|
||||
}
|
||||
|
||||
private void validateStep(Workflow workflow, WorkflowStep step, boolean topLevel) throws ModelValidationException {
|
||||
private void validateStep(WorkflowStep step) throws ModelValidationException {
|
||||
if (step.getAfter() < 0) {
|
||||
throw new ModelValidationException("Step 'after' time condition cannot be negative.");
|
||||
}
|
||||
|
||||
boolean isAggregatedStep = !step.getSteps().isEmpty();
|
||||
boolean isScheduledWorkflow = workflow.isScheduled();
|
||||
|
||||
// verify the step does have valid provider
|
||||
getStepProviderFactory(step);
|
||||
|
||||
if (isAggregatedStep) {
|
||||
if (!step.getProviderId().equals(AggregatedStepProviderFactory.ID)) {
|
||||
// for now, only AggregatedStepProvider supports having sub-steps, but we might want to support
|
||||
// in the future more steps from having sub-steps by querying the capability from the provider or via
|
||||
// a marker interface
|
||||
throw new ModelValidationException("Step provider " + step.getProviderId() + " does not support aggregated steps");
|
||||
}
|
||||
|
||||
List<WorkflowStep> subSteps = step.getSteps();
|
||||
// for each sub-step (in case it's not aggregated step on its own) check all it's sub-steps do not have
|
||||
// time conditions, all its sub-steps are meant to be run at once
|
||||
if (subSteps.stream().anyMatch(subStep ->
|
||||
subStep.getConfig().getFirst(CONFIG_AFTER) != null &&
|
||||
!subStep.getProviderId().equals(AggregatedStepProviderFactory.ID))) {
|
||||
throw new ModelValidationException("Sub-steps of aggregated step cannot have time conditions.");
|
||||
}
|
||||
} else {
|
||||
if (isScheduledWorkflow && topLevel) {
|
||||
if (step.getConfig().getFirst(CONFIG_AFTER) == null) {
|
||||
throw new ModelValidationException("All steps of scheduled workflow must have a valid 'after' time condition.");
|
||||
}
|
||||
} else { // immediate workflow | sub-step of aggregated step
|
||||
if (step.getConfig().getFirst(CONFIG_AFTER) != null) {
|
||||
throw new ModelValidationException(topLevel ?
|
||||
"Immediate workflow step cannot have a time condition." :
|
||||
"Sub-step of aggregated step cannot have a time conditions.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public WorkflowStep addStepToWorkflow(String workflowId, WorkflowStep step, Integer position) {
|
||||
Objects.requireNonNull(workflowId, "workflowId cannot be null");
|
||||
public WorkflowStep addStepToWorkflow(Workflow workflow, WorkflowStep step, Integer position) {
|
||||
Objects.requireNonNull(workflow, "workflow cannot be null");
|
||||
Objects.requireNonNull(step, "step cannot be null");
|
||||
|
||||
List<WorkflowStep> existingSteps = getSteps(workflowId);
|
||||
List<WorkflowStep> existingSteps = getSteps(workflow.getId());
|
||||
|
||||
int targetPosition = position != null ? position : existingSteps.size();
|
||||
if (targetPosition < 0 || targetPosition > existingSteps.size()) {
|
||||
@ -503,32 +453,32 @@ public class WorkflowsManager {
|
||||
shiftStepsForInsertion(targetPosition, existingSteps);
|
||||
|
||||
step.setPriority(targetPosition + 1);
|
||||
WorkflowStep addedStep = addStep(workflowId, step);
|
||||
WorkflowStep addedStep = addStep(workflow, step);
|
||||
|
||||
updateScheduledStepsAfterStepChange(workflowId);
|
||||
updateScheduledStepsAfterStepChange(workflow.getId());
|
||||
|
||||
log.debugf("Added step %s to workflow %s at position %d", addedStep.getId(), workflowId, targetPosition);
|
||||
log.debugf("Added step %s to workflow %s at position %d", addedStep.getId(), workflow.getId(), targetPosition);
|
||||
return addedStep;
|
||||
}
|
||||
|
||||
public void removeStepFromWorkflow(String workflowId, String stepId) {
|
||||
Objects.requireNonNull(workflowId, "workflowId cannot be null");
|
||||
public void removeStepFromWorkflow(Workflow workflow, String stepId) {
|
||||
Objects.requireNonNull(workflow, "workflow cannot be null");
|
||||
Objects.requireNonNull(stepId, "stepId cannot be null");
|
||||
|
||||
RealmModel realm = getRealm();
|
||||
ComponentModel stepComponent = realm.getComponent(stepId);
|
||||
|
||||
if (stepComponent == null || !stepComponent.getParentId().equals(workflowId)) {
|
||||
if (stepComponent == null || !stepComponent.getParentId().equals(workflow.getId())) {
|
||||
throw new BadRequestException("Step not found or not part of workflow: " + stepId);
|
||||
}
|
||||
|
||||
realm.removeComponent(stepComponent);
|
||||
|
||||
// Reorder remaining steps and update state
|
||||
reorderAllSteps(workflowId);
|
||||
updateScheduledStepsAfterStepChange(workflowId);
|
||||
reorderAllSteps(workflow.getId());
|
||||
updateScheduledStepsAfterStepChange(workflow.getId());
|
||||
|
||||
log.debugf("Removed step %s from workflow %s", stepId, workflowId);
|
||||
log.debugf("Removed step %s from workflow %s", stepId, workflow.getId());
|
||||
}
|
||||
|
||||
private void shiftStepsForInsertion(int insertPosition, List<WorkflowStep> existingSteps) {
|
||||
@ -578,20 +528,9 @@ public class WorkflowsManager {
|
||||
}
|
||||
}
|
||||
|
||||
public WorkflowStep toModel(Workflow workflow, WorkflowStepRepresentation rep) {
|
||||
return toModel(workflow, rep, true);
|
||||
}
|
||||
|
||||
private WorkflowStep toModel(Workflow workflow, WorkflowStepRepresentation rep, boolean topLevel) {
|
||||
List<WorkflowStep> subSteps = new ArrayList<>();
|
||||
|
||||
for (WorkflowStepRepresentation subStep : ofNullable(rep.getSteps()).orElse(List.of())) {
|
||||
subSteps.add(toModel(workflow, subStep, false));
|
||||
}
|
||||
|
||||
WorkflowStep step = new WorkflowStep(rep.getUses(), rep.getConfig(), subSteps);
|
||||
validateStep(workflow, step, topLevel);
|
||||
|
||||
public WorkflowStep toModel(WorkflowStepRepresentation rep) {
|
||||
WorkflowStep step = new WorkflowStep(rep.getUses(), rep.getConfig());
|
||||
validateStep(step);
|
||||
return step;
|
||||
}
|
||||
|
||||
|
||||
@ -64,7 +64,6 @@ public class WorkflowResource {
|
||||
* @param resourceId the resource id
|
||||
* @param notBefore optional notBefore time in milliseconds to schedule the first workflow step,
|
||||
* it overrides the first workflow step time configuration (after).
|
||||
* if set and the workflow is not scheduled (immediate) a Bad Request response will be returned
|
||||
*/
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@ -77,9 +76,6 @@ public class WorkflowResource {
|
||||
}
|
||||
|
||||
if (notBefore != null) {
|
||||
if (!workflow.isScheduled()) {
|
||||
throw ErrorResponse.error("Immediate workflows does not support binding with provided time.", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
workflow.setNotBefore(notBefore);
|
||||
}
|
||||
|
||||
|
||||
@ -110,8 +110,8 @@ public class WorkflowStepsResource {
|
||||
throw ErrorResponse.error("Step representation cannot be null", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
try {
|
||||
WorkflowStep step = workflowsManager.toModel(workflow, stepRep);
|
||||
WorkflowStep addedStep = workflowsManager.addStepToWorkflow(workflow.getId(), step, position);
|
||||
WorkflowStep step = workflowsManager.toModel(stepRep);
|
||||
WorkflowStep addedStep = workflowsManager.addStepToWorkflow(workflow, step, position);
|
||||
|
||||
return Response.ok(workflowsManager.toRepresentation(addedStep)).build();
|
||||
} catch (ModelException e) {
|
||||
@ -140,7 +140,7 @@ public class WorkflowStepsResource {
|
||||
throw new BadRequestException("Step ID cannot be null or empty");
|
||||
}
|
||||
|
||||
workflowsManager.removeStepFromWorkflow(workflow.getId(), stepId);
|
||||
workflowsManager.removeStepFromWorkflow(workflow, stepId);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
|
||||
@ -19,5 +19,4 @@ org.keycloak.models.workflow.DisableUserStepProviderFactory
|
||||
org.keycloak.models.workflow.NotifyUserStepProviderFactory
|
||||
org.keycloak.models.workflow.DeleteUserStepProviderFactory
|
||||
org.keycloak.models.workflow.SetUserAttributeStepProviderFactory
|
||||
org.keycloak.models.workflow.AggregatedStepProviderFactory
|
||||
org.keycloak.models.workflow.AddRequiredActionStepProviderFactory
|
||||
|
||||
@ -23,8 +23,6 @@ import static org.hamcrest.Matchers.is;
|
||||
@KeycloakIntegrationTest(config = WorkflowsServerConfig.class)
|
||||
public class AddRequiredActionTest {
|
||||
|
||||
private static final String REALM_NAME = "default";
|
||||
|
||||
@InjectRealm(lifecycle = LifeCycle.METHOD)
|
||||
ManagedRealm managedRealm;
|
||||
|
||||
@ -32,7 +30,6 @@ public class AddRequiredActionTest {
|
||||
public void testStepRun() {
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.immediate()
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create()
|
||||
.of(AddRequiredActionStepProviderFactory.ID)
|
||||
|
||||
@ -67,7 +67,6 @@ public class AdhocWorkflowTest {
|
||||
public void testBindAdHocScheduledWithImmediateWorkflow() {
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(EventBasedWorkflowProviderFactory.ID)
|
||||
.immediate()
|
||||
.withSteps(WorkflowStepRepresentation.create()
|
||||
.of(SetUserAttributeStepProviderFactory.ID)
|
||||
.withConfig("message", "message")
|
||||
@ -113,7 +112,6 @@ public class AdhocWorkflowTest {
|
||||
public void testRunAdHocImmediateWorkflow() {
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(EventBasedWorkflowProviderFactory.ID)
|
||||
.immediate()
|
||||
.withSteps(WorkflowStepRepresentation.create()
|
||||
.of(SetUserAttributeStepProviderFactory.ID)
|
||||
.withConfig("message", "message")
|
||||
@ -140,12 +138,11 @@ public class AdhocWorkflowTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRunAdHocTimedScheduledWorkflow() {
|
||||
public void testRunAdHocTimedWorkflow() {
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(EventBasedWorkflowProviderFactory.ID)
|
||||
.withSteps(WorkflowStepRepresentation.create()
|
||||
.of(SetUserAttributeStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.withConfig("message", "message")
|
||||
.build())
|
||||
.build()).close();
|
||||
|
||||
@ -1,231 +0,0 @@
|
||||
package org.keycloak.tests.admin.model.workflow;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasEntry;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.core.Response.Status;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.workflow.AggregatedStepProviderFactory;
|
||||
import org.keycloak.models.workflow.DisableUserStepProviderFactory;
|
||||
import org.keycloak.models.workflow.WorkflowsManager;
|
||||
import org.keycloak.models.workflow.SetUserAttributeStepProviderFactory;
|
||||
import org.keycloak.models.workflow.UserCreationTimeWorkflowProviderFactory;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
|
||||
|
||||
@KeycloakIntegrationTest(config = WorkflowsServerConfig.class)
|
||||
public class AggregatedStepTest {
|
||||
|
||||
private static final String REALM_NAME = "default";
|
||||
|
||||
@InjectRunOnServer(permittedPackages = "org.keycloak.tests")
|
||||
RunOnServerClient runOnServer;
|
||||
|
||||
@InjectRealm(lifecycle = LifeCycle.METHOD)
|
||||
ManagedRealm managedRealm;
|
||||
|
||||
@Test
|
||||
public void testCreate() {
|
||||
try (Response response = managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(AggregatedStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.withSteps(WorkflowStepRepresentation.create()
|
||||
.of(SetUserAttributeStepProviderFactory.ID)
|
||||
.withConfig("message", "message")
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create()
|
||||
.of(DisableUserStepProviderFactory.ID)
|
||||
.build()
|
||||
).build())
|
||||
.build())) {
|
||||
assertThat(response.getStatus(), is(Status.CREATED.getStatusCode()));
|
||||
List<WorkflowRepresentation> workflows = managedRealm.admin().workflows().list();
|
||||
assertThat(workflows, hasSize(1));
|
||||
WorkflowRepresentation workflow = workflows.get(0);
|
||||
assertThat(workflow.getSteps(), hasSize(1));
|
||||
WorkflowStepRepresentation aggregatedStep = workflow.getSteps().get(0);
|
||||
assertThat(aggregatedStep.getUses(), is(AggregatedStepProviderFactory.ID));
|
||||
List<WorkflowStepRepresentation> steps = aggregatedStep.getSteps();
|
||||
assertThat(steps, hasSize(2));
|
||||
assertStep(steps, SetUserAttributeStepProviderFactory.ID, a -> {
|
||||
assertNotNull(a.getConfig());
|
||||
assertThat(a.getConfig().isEmpty(), is(false));
|
||||
assertThat(a.getConfig(), hasEntry("priority", List.of("1")));
|
||||
assertThat(a.getConfig(), hasEntry("message", List.of("message")));
|
||||
});
|
||||
assertStep(steps, DisableUserStepProviderFactory.ID, a -> {
|
||||
assertNotNull(a.getConfig());
|
||||
assertThat(a.getConfig().isEmpty(), is(false));
|
||||
assertThat(a.getConfig(), hasEntry("priority", List.of("2")));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateAggregatedStepAsSubStep() {
|
||||
try (Response response = managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(AggregatedStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.withSteps(WorkflowStepRepresentation.create()
|
||||
.of(AggregatedStepProviderFactory.ID)
|
||||
.withConfig("message", "message")
|
||||
.after(Duration.ofDays(5))
|
||||
.withSteps(WorkflowStepRepresentation.create()
|
||||
.of(SetUserAttributeStepProviderFactory.ID)
|
||||
.withConfig("message", "message")
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create()
|
||||
.of(DisableUserStepProviderFactory.ID)
|
||||
.build()
|
||||
)
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create()
|
||||
.of(DisableUserStepProviderFactory.ID)
|
||||
.build()
|
||||
).build())
|
||||
.build())) {
|
||||
assertThat(response.getStatus(), is(Status.CREATED.getStatusCode()));
|
||||
List<WorkflowRepresentation> workflows = managedRealm.admin().workflows().list();
|
||||
assertThat(workflows, hasSize(1));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailCreateIfSettingStepsToRegularSteps() {
|
||||
try (Response response = managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(SetUserAttributeStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.withConfig("key", "value")
|
||||
.withSteps(WorkflowStepRepresentation.create()
|
||||
.of(SetUserAttributeStepProviderFactory.ID)
|
||||
.withConfig("message", "message")
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create()
|
||||
.of(DisableUserStepProviderFactory.ID)
|
||||
.build()
|
||||
).build())
|
||||
.build())) {
|
||||
assertThat(response.getStatus(), is(Status.BAD_REQUEST.getStatusCode()));
|
||||
assertThat(response.readEntity(ErrorRepresentation.class).getErrorMessage(), equalTo("Step provider " + SetUserAttributeStepProviderFactory.ID + " does not support aggregated steps"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailCreateIfSubStepHasTimeCondition() {
|
||||
try (Response response = managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(SetUserAttributeStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.withConfig("key", "value")
|
||||
.withSteps(WorkflowStepRepresentation.create()
|
||||
.of(SetUserAttributeStepProviderFactory.ID)
|
||||
.withConfig("message", "message")
|
||||
.after(Duration.ofDays(1))
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create()
|
||||
.of(DisableUserStepProviderFactory.ID)
|
||||
.build()
|
||||
).build())
|
||||
.build())) {
|
||||
assertThat(response.getStatus(), is(Status.BAD_REQUEST.getStatusCode()));
|
||||
assertThat(response.readEntity(ErrorRepresentation.class).getErrorMessage(), equalTo("Sub-step of aggregated step cannot have a time conditions."));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStepRun() {
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(AggregatedStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(5))
|
||||
.withSteps(WorkflowStepRepresentation.create()
|
||||
.of(SetUserAttributeStepProviderFactory.ID)
|
||||
.withConfig("message", "message")
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create()
|
||||
.of(DisableUserStepProviderFactory.ID)
|
||||
.build()
|
||||
).build())
|
||||
.build()).close();
|
||||
|
||||
managedRealm.admin().users().create(getUserRepresentation("alice", "Alice", "Wonderland", "alice@wornderland.org")).close();
|
||||
|
||||
runOnServer.run((session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
try {
|
||||
Time.setOffset(Math.toIntExact(Duration.ofDays(6).toSeconds()));
|
||||
manager.runScheduledSteps();
|
||||
UserModel user = session.users().getUserByUsername(realm, "alice");
|
||||
assertNotNull(user.getAttributes().get("message"));
|
||||
assertFalse(user.isEnabled());
|
||||
} finally {
|
||||
Time.setOffset(0);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private static RealmModel configureSessionContext(KeycloakSession session) {
|
||||
RealmModel realm = session.realms().getRealmByName(REALM_NAME);
|
||||
session.getContext().setRealm(realm);
|
||||
return realm;
|
||||
}
|
||||
|
||||
private UserRepresentation getUserRepresentation(String username, String firstName, String lastName, String email) {
|
||||
UserRepresentation representation = new UserRepresentation();
|
||||
representation.setUsername(username);
|
||||
representation.setFirstName(firstName);
|
||||
representation.setLastName(lastName);
|
||||
representation.setEmail(email);
|
||||
representation.setEnabled(true);
|
||||
CredentialRepresentation credential = new CredentialRepresentation();
|
||||
credential.setType(CredentialRepresentation.PASSWORD);
|
||||
credential.setValue(username);
|
||||
representation.setCredentials(List.of(credential));
|
||||
return representation;
|
||||
}
|
||||
|
||||
private void assertStep(List<WorkflowStepRepresentation> steps, String expectedProviderId, Consumer<WorkflowStepRepresentation> assertions) {
|
||||
assertTrue(steps.stream()
|
||||
.anyMatch(a -> {
|
||||
if (a.getUses().equals(expectedProviderId)) {
|
||||
assertions.accept(a);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}));
|
||||
}
|
||||
}
|
||||
@ -18,7 +18,6 @@
|
||||
package org.keycloak.tests.admin.model.workflow;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
@ -28,7 +27,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_SCHEDULED;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
@ -585,7 +583,6 @@ public class WorkflowManagementTest {
|
||||
// create a test workflow with no time conditions - should run immediately when scheduled
|
||||
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.immediate()
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(SetUserAttributeStepProviderFactory.ID)
|
||||
.withConfig("message", "message")
|
||||
@ -609,126 +606,6 @@ public class WorkflowManagementTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateImmediateWorkflowWithTimeConditions() {
|
||||
WorkflowSetRepresentation workflows = WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.immediate()
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(3))
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(7))
|
||||
.build()
|
||||
).build();
|
||||
|
||||
try (Response response = managedRealm.admin().workflows().create(workflows)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST.getStatusCode()));
|
||||
String error = response.readEntity(String.class);
|
||||
assertThat(error, containsString("Immediate workflow cannot have steps with time conditions"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateWorkflowFromImmediateToScheduled() {
|
||||
// Create an immediate workflow
|
||||
WorkflowSetRepresentation workflows = WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.immediate()
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.build()
|
||||
).build();
|
||||
|
||||
WorkflowsResource workflowsResource = managedRealm.admin().workflows();
|
||||
try (Response response = workflowsResource.create(workflows)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
|
||||
}
|
||||
|
||||
WorkflowRepresentation workflow = workflowsResource.list().get(0);
|
||||
// Attempt to update the workflow to scheduled
|
||||
workflow.getConfig().putSingle(CONFIG_SCHEDULED, "true");
|
||||
|
||||
try (Response response = workflowsResource.workflow(workflow.getId()).update(workflow)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST.getStatusCode()));
|
||||
String error = response.readEntity(String.class);
|
||||
assertThat(error, containsString("Scheduled workflow cannot have steps without time conditions."));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateWorkflowFromScheduledToImmediate() {
|
||||
// Create a scheduled workflow
|
||||
WorkflowSetRepresentation workflows = WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(1))
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(1))
|
||||
.build()
|
||||
).build();
|
||||
|
||||
WorkflowsResource workflowsResource = managedRealm.admin().workflows();
|
||||
try (Response response = workflowsResource.create(workflows)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.CREATED.getStatusCode()));
|
||||
}
|
||||
|
||||
WorkflowRepresentation workflow = workflowsResource.list().get(0);
|
||||
// Attempt to update the workflow to immediate
|
||||
workflow.setScheduled(false);
|
||||
|
||||
try (Response response = workflowsResource.workflow(workflow.getId()).update(workflow)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST.getStatusCode()));
|
||||
String error = response.readEntity(String.class);
|
||||
assertThat(error, containsString("Immediate workflow cannot have steps with time conditions."));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateScheduledWorkflowWithoutTimeConditions() {
|
||||
WorkflowSetRepresentation workflows = WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.build()
|
||||
).build();
|
||||
|
||||
try (Response response = managedRealm.admin().workflows().create(workflows)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST.getStatusCode()));
|
||||
String error = response.readEntity(String.class);
|
||||
assertThat(error, containsString("Scheduled workflow cannot have steps without time conditions"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateWorkflowMarkedAsBothImmediateAndRecurring() {
|
||||
WorkflowSetRepresentation workflows = WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.immediate()
|
||||
.recurring()
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(3))
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create().of(DisableUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(7))
|
||||
.build()
|
||||
).build();
|
||||
|
||||
try (Response response = managedRealm.admin().workflows().create(workflows)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST.getStatusCode()));
|
||||
String error = response.readEntity(String.class);
|
||||
assertThat(error, containsString("Workflow cannot be both immediate and recurring."));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailCreateWorkflowWithNegativeTime() {
|
||||
WorkflowSetRepresentation workflows = WorkflowRepresentation.create()
|
||||
@ -737,14 +614,7 @@ public class WorkflowManagementTest {
|
||||
WorkflowStepRepresentation.create().of(SetUserAttributeStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(-5))
|
||||
.withConfig("key", "value")
|
||||
.withSteps(WorkflowStepRepresentation.create()
|
||||
.of(SetUserAttributeStepProviderFactory.ID)
|
||||
.withConfig("message", "message")
|
||||
.build(),
|
||||
WorkflowStepRepresentation.create()
|
||||
.of(DisableUserStepProviderFactory.ID)
|
||||
.build()
|
||||
).build())
|
||||
.build())
|
||||
.build();
|
||||
try (Response response = managedRealm.admin().workflows().create(workflows)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST.getStatusCode()));
|
||||
|
||||
@ -44,23 +44,14 @@ import org.keycloak.testframework.remote.runonserver.InjectRunOnServer;
|
||||
import org.keycloak.testframework.remote.runonserver.RunOnServerClient;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.keycloak.testframework.util.ApiUtil;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_SCHEDULED;
|
||||
|
||||
@KeycloakIntegrationTest(config = WorkflowsServerConfig.class)
|
||||
public class WorkflowStepManagementTest {
|
||||
@ -136,64 +127,6 @@ public class WorkflowStepManagementTest {
|
||||
assertTrue(foundOurStep, "Our added step should be present in the workflow");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddStepWithTimeToImmediateWorkflow() {
|
||||
workflowsResource.create(WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.USER_ADD.toString())
|
||||
.name("immediate-workflow")
|
||||
.immediate()
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID).build()
|
||||
).build()).close();
|
||||
|
||||
String immediateWorkflowId = workflowsResource.list().stream()
|
||||
.filter(wf -> "immediate-workflow".equals(wf.getName()))
|
||||
.findFirst()
|
||||
.map(WorkflowRepresentation::getId)
|
||||
.orElse(null);
|
||||
assertThat(immediateWorkflowId, notNullValue());
|
||||
|
||||
WorkflowRepresentation representation = workflowsResource.workflow(immediateWorkflowId).toRepresentation();
|
||||
assertThat(representation.getConfig().getFirst(CONFIG_SCHEDULED), equalTo("false"));
|
||||
|
||||
// Attempt to add a step with 'after' time (should be rejected)
|
||||
WorkflowStepRepresentation stepRep = new WorkflowStepRepresentation();
|
||||
stepRep.setUses(DisableUserStepProviderFactory.ID);
|
||||
stepRep.setConfig("name", "Invalid Step");
|
||||
stepRep.setConfig("after", String.valueOf(Duration.ofDays(30).toMillis()));
|
||||
|
||||
try (Response response = workflowsResource.workflow(immediateWorkflowId).steps().create(stepRep)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST.getStatusCode()));
|
||||
String errorMessage = response.readEntity(String.class);
|
||||
assertThat(errorMessage, containsString("Immediate workflow step cannot have a time condition."));
|
||||
}
|
||||
// Verify step was not added (should still be only the original setup step)
|
||||
List<WorkflowStepRepresentation> allSteps = workflowsResource.workflow(immediateWorkflowId).steps().list();
|
||||
assertThat(allSteps, hasSize(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddStepWithoutTimeToScheduledWorkflow() {
|
||||
WorkflowRepresentation representation = workflowsResource.workflow(workflowId).toRepresentation();
|
||||
assertThat(representation.getConfig().getFirst(CONFIG_SCHEDULED), nullValue());//immediate
|
||||
|
||||
// Attempt to add a step without 'after' time (should be rejected)
|
||||
WorkflowStepRepresentation stepRep = WorkflowStepRepresentation.create()
|
||||
.of(NotifyUserStepProviderFactory.ID)
|
||||
.build();
|
||||
|
||||
try (Response response = workflowsResource.workflow(workflowId).steps().create(stepRep)) {
|
||||
assertThat(response.getStatus(), is(Response.Status.BAD_REQUEST.getStatusCode()));
|
||||
String errorMessage = response.readEntity(String.class);
|
||||
assertThat(errorMessage, containsString("All steps of scheduled workflow must have a valid 'after' time condition."));
|
||||
}
|
||||
|
||||
// Verify step was not added (should still be only the original setup step)
|
||||
List<WorkflowStepRepresentation> allSteps = workflowsResource.workflow(workflowId).steps().list();
|
||||
assertThat(allSteps, hasSize(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveStepFromWorkflow() {
|
||||
WorkflowResource workflow = workflowsResource.workflow(workflowId);
|
||||
@ -312,13 +245,13 @@ public class WorkflowStepManagementTest {
|
||||
|
||||
Workflow workflow = manager.addWorkflow(UserCreationTimeWorkflowProviderFactory.ID, Map.of());
|
||||
|
||||
WorkflowStep step1 = new WorkflowStep(NotifyUserStepProviderFactory.ID, null, List.of());
|
||||
WorkflowStep step1 = new WorkflowStep(NotifyUserStepProviderFactory.ID, null);
|
||||
step1.setAfter(Duration.ofDays(30).toMillis());
|
||||
WorkflowStep step2 = new WorkflowStep(DisableUserStepProviderFactory.ID, null, List.of());
|
||||
WorkflowStep step2 = new WorkflowStep(DisableUserStepProviderFactory.ID, null);
|
||||
step2.setAfter(Duration.ofDays(60).toMillis());
|
||||
|
||||
WorkflowStep addedStep1 = manager.addStepToWorkflow(workflow.getId(), step1, null);
|
||||
WorkflowStep addedStep2 = manager.addStepToWorkflow(workflow.getId(), step2, null);
|
||||
WorkflowStep addedStep1 = manager.addStepToWorkflow(workflow, step1, null);
|
||||
WorkflowStep addedStep2 = manager.addStepToWorkflow(workflow, step2, null);
|
||||
|
||||
// Simulate scheduled steps by binding workflow to a test resource
|
||||
String testResourceId = "test-user-123";
|
||||
@ -331,7 +264,7 @@ public class WorkflowStepManagementTest {
|
||||
assertNotNull(scheduledStepsBeforeRemoval);
|
||||
|
||||
// Remove the first step
|
||||
manager.removeStepFromWorkflow(workflow.getId(), addedStep1.getId());
|
||||
manager.removeStepFromWorkflow(workflow, addedStep1.getId());
|
||||
|
||||
// Verify scheduled steps are updated
|
||||
var scheduledStepsAfterRemoval = stateProvider.getScheduledStepsByWorkflow(workflow.getId());
|
||||
@ -344,9 +277,9 @@ public class WorkflowStepManagementTest {
|
||||
assertEquals(1, remainingSteps.get(0).getPriority()); // Should be reordered to priority 1
|
||||
|
||||
// Add a new step and verify scheduled steps are updated
|
||||
WorkflowStep step3 = new WorkflowStep(NotifyUserStepProviderFactory.ID, null, List.of());
|
||||
WorkflowStep step3 = new WorkflowStep(NotifyUserStepProviderFactory.ID, null);
|
||||
step3.setAfter(Duration.ofDays(15).toMillis());
|
||||
manager.addStepToWorkflow(workflow.getId(), step3, 0); // Insert at beginning
|
||||
manager.addStepToWorkflow(workflow, step3, 0); // Insert at beginning
|
||||
|
||||
// Verify final state
|
||||
List<WorkflowStep> finalSteps = manager.getSteps(workflow.getId());
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user