mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Allow passing a context to steps
Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
5b5a83b800
commit
fa581c8148
@ -120,19 +120,6 @@ public class JpaWorkflowStateProvider implements WorkflowStateProvider {
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<ScheduledStep> getScheduledStepsByStep(String stepId) {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
CriteriaQuery<WorkflowStateEntity> query = cb.createQuery(WorkflowStateEntity.class);
|
||||
Root<WorkflowStateEntity> stateRoot = query.from(WorkflowStateEntity.class);
|
||||
|
||||
Predicate byStep = cb.equal(stateRoot.get("scheduledStepId"), stepId);
|
||||
query.where(byStep);
|
||||
|
||||
return em.createQuery(query).getResultStream()
|
||||
.map(this::toScheduledStep)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeByResource(String resourceId) {
|
||||
CriteriaBuilder cb = em.getCriteriaBuilder();
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.keycloak.models.workflow.WorkflowStateProvider.ScheduledStep;
|
||||
|
||||
final class DefaultWorkflowExecutionContext implements WorkflowExecutionContext {
|
||||
|
||||
private final String resourceId;
|
||||
private final String executionId;
|
||||
private final Workflow workflow;
|
||||
private WorkflowStep currentStep;
|
||||
|
||||
/**
|
||||
* A new execution context for a workflow event. The execution ID is randomly generated.
|
||||
*
|
||||
* @param workflow the workflow
|
||||
* @param event the event
|
||||
*/
|
||||
public DefaultWorkflowExecutionContext(Workflow workflow, WorkflowEvent event) {
|
||||
this(workflow, null, UUID.randomUUID().toString(), event.getResourceId());
|
||||
}
|
||||
|
||||
/**
|
||||
* A new execution context for a workflow event, resuming a previously scheduled step. The execution ID is taken from the scheduled step
|
||||
* with no current step, indicating that the workflow is being restarted due to an event.
|
||||
*
|
||||
* @param workflow the workflow
|
||||
* @param event the event
|
||||
* @param step the scheduled step
|
||||
*/
|
||||
public DefaultWorkflowExecutionContext(Workflow workflow, WorkflowEvent event, ScheduledStep step) {
|
||||
this(workflow, null, step.executionId(), event.getResourceId());
|
||||
}
|
||||
|
||||
/**
|
||||
* A execution context for a scheduled step, resuming the workflow from that step. The execution ID is taken from the scheduled step.
|
||||
*
|
||||
* @param manager the workflows manager
|
||||
* @param workflow the workflow
|
||||
* @param step the scheduled step
|
||||
*/
|
||||
public DefaultWorkflowExecutionContext(WorkflowsManager manager, Workflow workflow, ScheduledStep step) {
|
||||
this(workflow, manager.getStepById(workflow, step.stepId()), step.executionId(), step.resourceId());
|
||||
}
|
||||
|
||||
private DefaultWorkflowExecutionContext(Workflow workflow, WorkflowStep currentStep, String executionId, String resourceId) {
|
||||
this.workflow = workflow;
|
||||
this.currentStep = currentStep;
|
||||
this.executionId = executionId;
|
||||
this.resourceId = resourceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResourceId() {
|
||||
return resourceId;
|
||||
}
|
||||
|
||||
String getExecutionId() {
|
||||
return this.executionId;
|
||||
}
|
||||
|
||||
Workflow getWorkflow() {
|
||||
return workflow;
|
||||
}
|
||||
|
||||
WorkflowStep getCurrentStep() {
|
||||
return currentStep;
|
||||
}
|
||||
|
||||
void setCurrentStep(WorkflowStep step) {
|
||||
this.currentStep = step;
|
||||
}
|
||||
}
|
||||
@ -1,91 +1,14 @@
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
/**
|
||||
* A contextual object providing information about the workflow execution.
|
||||
*/
|
||||
public interface WorkflowExecutionContext {
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
|
||||
public class WorkflowExecutionContext {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(WorkflowExecutionContext.class);
|
||||
|
||||
private String executionId;
|
||||
private String resourceId;
|
||||
private Workflow workflow;
|
||||
private List<WorkflowStep> steps;
|
||||
|
||||
// variable that keep track of execution steps
|
||||
private int lastExecutedStepIndex = -1;
|
||||
|
||||
public WorkflowExecutionContext(Workflow workflow, List<WorkflowStep> steps, String resourceId) {
|
||||
this.workflow = workflow;
|
||||
this.steps = ofNullable(steps).orElse(List.of());
|
||||
this.resourceId = resourceId;
|
||||
}
|
||||
|
||||
public WorkflowExecutionContext(Workflow workflow, List<WorkflowStep> steps, String resourceId, String stepId, String executionId) {
|
||||
this(workflow, steps, resourceId);
|
||||
this.executionId = executionId;
|
||||
if (stepId != null) {
|
||||
for (int i = 0; i < steps.size(); i++) {
|
||||
if (steps.get(i).getId().equals(stepId)) {
|
||||
this.lastExecutedStepIndex = i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void init() {
|
||||
if (this.executionId == null) {
|
||||
this.executionId = UUID.randomUUID().toString();
|
||||
logger.debugf("Started workflow '%s' for resource %s (execution id: %s)", this.workflow.getName(), this.resourceId, this.executionId);
|
||||
}
|
||||
}
|
||||
|
||||
public void success(WorkflowStep step) {
|
||||
logger.debugf("Step %s completed successfully (execution id: %s)", step.getProviderId(), executionId);
|
||||
}
|
||||
|
||||
public void fail(WorkflowStep step, String errorMessage) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Step %s failed (execution id: %s)");
|
||||
if (errorMessage != null) {
|
||||
sb.append(" - error message: %s");
|
||||
logger.debugf(sb.toString(), step.getProviderId(), executionId, errorMessage);
|
||||
}
|
||||
else {
|
||||
logger.debugf(sb.toString(), step.getProviderId(), executionId);
|
||||
}
|
||||
}
|
||||
|
||||
public void complete() {
|
||||
logger.debugf("Workflow '%s' completed for resource %s (execution id: %s)", workflow.getName(), resourceId, executionId);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
logger.debugf("Workflow '%s' cancelled for resource %s (execution id: %s)", workflow.getName(), resourceId, executionId);
|
||||
}
|
||||
|
||||
public boolean hasNextStep() {
|
||||
return lastExecutedStepIndex + 1 < steps.size();
|
||||
}
|
||||
|
||||
public WorkflowStep getNextStep() {
|
||||
if (lastExecutedStepIndex + 1 < steps.size()) {
|
||||
return steps.get(++lastExecutedStepIndex);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void restart() {
|
||||
logger.debugf("Restarted workflow '%s' for resource %s (execution id: %s)",workflow.getName(), resourceId, executionId);
|
||||
this.lastExecutedStepIndex = -1;
|
||||
}
|
||||
|
||||
public String getExecutionId() {
|
||||
return this.executionId;
|
||||
}
|
||||
/**
|
||||
* Returns the id of the resource bound to the current workflow execution.
|
||||
*
|
||||
* @return the id of the resource
|
||||
*/
|
||||
String getResourceId();
|
||||
}
|
||||
|
||||
@ -57,8 +57,6 @@ public interface WorkflowStateProvider extends Provider {
|
||||
|
||||
List<ScheduledStep> getScheduledStepsByWorkflow(String workflowId);
|
||||
|
||||
List<ScheduledStep> getScheduledStepsByStep(String stepId);
|
||||
|
||||
default List<ScheduledStep> getScheduledStepsByWorkflow(Workflow workflow) {
|
||||
if (workflow == null) {
|
||||
return List.of();
|
||||
|
||||
@ -20,6 +20,8 @@ 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.Objects;
|
||||
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
|
||||
@ -91,4 +93,15 @@ public class WorkflowStep implements Comparable<WorkflowStep> {
|
||||
public int compareTo(WorkflowStep other) {
|
||||
return Integer.compare(this.getPriority(), other.getPriority());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof WorkflowStep that)) return false;
|
||||
return Objects.equals(id, that.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,11 +17,10 @@
|
||||
|
||||
package org.keycloak.models.workflow;
|
||||
|
||||
import java.util.List;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
public interface WorkflowStepProvider extends Provider {
|
||||
|
||||
void run(List<String> resourceIds);
|
||||
void run(WorkflowExecutionContext context);
|
||||
|
||||
}
|
||||
|
||||
@ -41,6 +41,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static org.keycloak.representations.workflows.WorkflowConstants.CONFIG_CONDITIONS;
|
||||
@ -156,6 +158,10 @@ public class WorkflowsManager {
|
||||
return new Workflow(getWorkflowComponent(id));
|
||||
}
|
||||
|
||||
WorkflowStep getStepById(Workflow workflow, String id) {
|
||||
return getSteps(workflow.getId()).filter(s -> s.getId().equals(id)).findAny().orElse(null);
|
||||
}
|
||||
|
||||
private ComponentModel getWorkflowComponent(String id) {
|
||||
ComponentModel component = getRealm().getComponent(id);
|
||||
|
||||
@ -166,16 +172,15 @@ public class WorkflowsManager {
|
||||
return component;
|
||||
}
|
||||
|
||||
public List<Workflow> getWorkflows() {
|
||||
public Stream<Workflow> getWorkflows() {
|
||||
RealmModel realm = getRealm();
|
||||
return realm.getComponentsStream(realm.getId(), WorkflowProvider.class.getName())
|
||||
.map(Workflow::new).toList();
|
||||
return realm.getComponentsStream(realm.getId(), WorkflowProvider.class.getName()).map(Workflow::new);
|
||||
}
|
||||
|
||||
public List<WorkflowStep> getSteps(String workflowId) {
|
||||
public Stream<WorkflowStep> getSteps(String workflowId) {
|
||||
RealmModel realm = getRealm();
|
||||
return realm.getComponentsStream(workflowId, WorkflowStepProvider.class.getName())
|
||||
.map(WorkflowStep::new).sorted().toList();
|
||||
.map(WorkflowStep::new).sorted();
|
||||
}
|
||||
|
||||
/* ================================= Workflows component providers and factories ================================= */
|
||||
@ -232,126 +237,177 @@ public class WorkflowsManager {
|
||||
processEvent(getWorkflows(), event);
|
||||
}
|
||||
|
||||
public void processEvent(List<Workflow> workflows, WorkflowEvent event) {
|
||||
private void processEvent(Stream<Workflow> workflows, WorkflowEvent event) {
|
||||
Map<String, ScheduledStep> scheduledSteps = workflowStateProvider.getScheduledStepsByResource(event.getResourceId())
|
||||
.stream().collect(HashMap::new, (m, v) -> m.put(v.workflowId(), v), HashMap::putAll);
|
||||
|
||||
// iterate through the workflows, and for those not yet assigned to the user check if they can be assigned
|
||||
workflows.stream()
|
||||
.filter(workflow -> workflow.isEnabled() && !getSteps(workflow.getId()).isEmpty())
|
||||
.forEach(workflow -> {
|
||||
WorkflowProvider provider = getWorkflowProvider(workflow);
|
||||
try {
|
||||
// if workflow is not active for the resource, check if the provider allows activating based on the event
|
||||
if (!scheduledSteps.containsKey(workflow.getId())) {
|
||||
if (provider.activateOnEvent(event)) {
|
||||
WorkflowExecutionContext context = buildAndInitContext(workflow, event.getResourceId());
|
||||
// If the workflow has a notBefore set, schedule the first step with it
|
||||
if (context.hasNextStep() && workflow.getNotBefore() != null && workflow.getNotBefore() > 0) {
|
||||
WorkflowStep step = context.getNextStep();
|
||||
log.debugf("Scheduling first step '%s' of workflow '%s' for resource %s based on on event %s with notBefore %d",
|
||||
step.getProviderId(), workflow.getName(), event.getResourceId(), event.getOperation(), workflow.getNotBefore());
|
||||
Long originalAfter = step.getAfter();
|
||||
try {
|
||||
step.setAfter(workflow.getNotBefore());
|
||||
workflowStateProvider.scheduleStep(workflow, step, event.getResourceId(), context.getExecutionId());
|
||||
} finally {
|
||||
// restore the original after value
|
||||
step.setAfter(originalAfter);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// process the workflow steps, scheduling or running them as needed
|
||||
processWorkflow(workflow, context, event.getResourceId());
|
||||
}
|
||||
workflows.forEach(workflow -> {
|
||||
if (!workflow.isEnabled()) {
|
||||
log.debugf("Skipping workflow %s as it is disabled or has no steps", workflow.getName());
|
||||
return;
|
||||
}
|
||||
|
||||
WorkflowProvider provider = getWorkflowProvider(workflow);
|
||||
|
||||
try {
|
||||
ScheduledStep scheduledStep = scheduledSteps.get(workflow.getId());
|
||||
|
||||
// if workflow is not active for the resource, check if the provider allows activating based on the event
|
||||
if (scheduledStep == null) {
|
||||
if (provider.activateOnEvent(event)) {
|
||||
DefaultWorkflowExecutionContext context = new DefaultWorkflowExecutionContext(workflow, event);
|
||||
// If the workflow has a notBefore set, schedule the first step with it
|
||||
if (workflow.getNotBefore() != null && workflow.getNotBefore() > 0) {
|
||||
WorkflowStep firstStep = getSteps(workflow.getId()).findFirst().orElseThrow(() -> new WorkflowInvalidStateException("No steps found for workflow " + workflow.getName()));
|
||||
log.debugf("Scheduling first step '%s' of workflow '%s' for resource %s based on on event %s with notBefore %d",
|
||||
firstStep.getProviderId(), workflow.getName(), event.getResourceId(), event.getOperation(), workflow.getNotBefore());
|
||||
Long originalAfter = firstStep.getAfter();
|
||||
try {
|
||||
firstStep.setAfter(workflow.getNotBefore());
|
||||
workflowStateProvider.scheduleStep(workflow, firstStep, event.getResourceId(), context.getExecutionId());
|
||||
} finally {
|
||||
// restore the original after value
|
||||
firstStep.setAfter(originalAfter);
|
||||
}
|
||||
} else {
|
||||
// workflow is active for the resource, check if the provider wants to reset or deactivate it based on the event
|
||||
WorkflowExecutionContext context = buildFromScheduledStep(scheduledSteps.get(workflow.getId()));
|
||||
if (provider.resetOnEvent(event)) {
|
||||
context.restart();
|
||||
processWorkflow(workflow, context, event.getResourceId());
|
||||
} else if (provider.deactivateOnEvent(event)) {
|
||||
context.cancel();
|
||||
workflowStateProvider.remove(context.getExecutionId());
|
||||
}
|
||||
// process the workflow steps, scheduling or running them as needed
|
||||
runWorkflowNextSteps(context);
|
||||
}
|
||||
} catch (WorkflowInvalidStateException e) {
|
||||
workflow.setEnabled(false);
|
||||
workflow.setError(e.getMessage());
|
||||
updateWorkflowConfig(workflow, workflow.getConfig());
|
||||
log.warnf("Workflow %s was disabled due to: %s", workflow.getId(), e.getMessage());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// workflow is active for the resource, check if the provider wants to reset or deactivate it based on the event
|
||||
String executionId = scheduledStep.executionId();
|
||||
String resourceId = scheduledStep.resourceId();
|
||||
if (provider.resetOnEvent(event)) {
|
||||
restartWorkflow(new DefaultWorkflowExecutionContext(workflow, event, scheduledStep));
|
||||
} else if (provider.deactivateOnEvent(event)) {
|
||||
log.debugf("Workflow '%s' cancelled for resource %s (execution id: %s)", workflow.getName(), resourceId, executionId);
|
||||
workflowStateProvider.remove(executionId);
|
||||
}
|
||||
}
|
||||
} catch (WorkflowInvalidStateException e) {
|
||||
workflow.setEnabled(false);
|
||||
workflow.setError(e.getMessage());
|
||||
updateWorkflowConfig(workflow, workflow.getConfig());
|
||||
log.warnf("Workflow %s was disabled due to: %s", workflow.getId(), e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void runScheduledSteps() {
|
||||
this.getWorkflows().stream().filter(Workflow::isEnabled).forEach(workflow -> {
|
||||
|
||||
getWorkflows().forEach((workflow) -> {
|
||||
if (!workflow.isEnabled()) {
|
||||
log.debugf("Skipping workflow %s as it is disabled", workflow.getName());
|
||||
return;
|
||||
}
|
||||
for (ScheduledStep scheduled : workflowStateProvider.getDueScheduledSteps(workflow)) {
|
||||
WorkflowExecutionContext context = buildFromScheduledStep(scheduled);
|
||||
if (!context.hasNextStep()) {
|
||||
DefaultWorkflowExecutionContext context = new DefaultWorkflowExecutionContext(this, workflow, scheduled);
|
||||
WorkflowStep step = context.getCurrentStep();
|
||||
|
||||
if (step == null) {
|
||||
log.warnf("Could not find step %s in workflow %s for resource %s. Removing the workflow state.",
|
||||
scheduled.stepId(), scheduled.workflowId(), scheduled.resourceId());
|
||||
workflowStateProvider.remove(scheduled.executionId());
|
||||
continue;
|
||||
}
|
||||
|
||||
// run the scheduled step that is due
|
||||
this.runWorkflowStep(context, context.getNextStep(), scheduled.resourceId());
|
||||
runWorkflowStep(context);
|
||||
|
||||
// now process the subsequent steps, scheduling or running them as needed
|
||||
processWorkflow(workflow, context, scheduled.resourceId());
|
||||
runWorkflowNextSteps(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void bind(Workflow workflow, ResourceType type, String resourceId) {
|
||||
processEvent(List.of(workflow), new AdhocWorkflowEvent(type, resourceId));
|
||||
processEvent(Stream.of(workflow), new AdhocWorkflowEvent(type, resourceId));
|
||||
}
|
||||
|
||||
public void bindToAllEligibleResources(Workflow workflow) {
|
||||
if (workflow.isEnabled()) {
|
||||
WorkflowProvider provider = getWorkflowProvider(workflow);
|
||||
provider.getEligibleResourcesForInitialStep()
|
||||
.forEach(resourceId -> processEvent(List.of(workflow), new AdhocWorkflowEvent(ResourceType.USERS, resourceId)));
|
||||
.forEach(resourceId -> processEvent(Stream.of(workflow), new AdhocWorkflowEvent(ResourceType.USERS, resourceId)));
|
||||
}
|
||||
}
|
||||
|
||||
private void processWorkflow(Workflow workflow, WorkflowExecutionContext context, String resourceId) {
|
||||
while (context.hasNextStep()) {
|
||||
WorkflowStep step = context.getNextStep();
|
||||
private void restartWorkflow(DefaultWorkflowExecutionContext context) {
|
||||
Workflow workflow = context.getWorkflow();
|
||||
String resourceId = context.getResourceId();
|
||||
String executionId = context.getExecutionId();
|
||||
context.setCurrentStep(null);
|
||||
log.debugf("Restarted workflow '%s' for resource %s (execution id: %s)", workflow.getName(), resourceId, executionId);
|
||||
runWorkflowNextSteps(context);
|
||||
}
|
||||
|
||||
private void runWorkflowNextSteps(DefaultWorkflowExecutionContext context) {
|
||||
String executionId = context.getExecutionId();
|
||||
Workflow workflow = context.getWorkflow();
|
||||
Stream<WorkflowStep> steps = getSteps(workflow.getId());
|
||||
WorkflowStep currentStep = context.getCurrentStep();
|
||||
|
||||
if (currentStep != null) {
|
||||
steps = steps.skip(currentStep.getPriority());
|
||||
}
|
||||
|
||||
String resourceId = context.getResourceId();
|
||||
AtomicBoolean scheduled = new AtomicBoolean(false);
|
||||
|
||||
steps.forEach(step -> {
|
||||
if (scheduled.get()) {
|
||||
// if we already scheduled a step, we skip processing the other steps of the workflow
|
||||
return;
|
||||
}
|
||||
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 to run in %d ms for resource %s (execution id: %s)",
|
||||
step.getProviderId(), step.getAfter(), resourceId, context.getExecutionId());
|
||||
workflowStateProvider.scheduleStep(workflow, step, resourceId, context.getExecutionId());
|
||||
return;
|
||||
step.getProviderId(), step.getAfter(), resourceId, executionId);
|
||||
workflowStateProvider.scheduleStep(workflow, step, resourceId, executionId);
|
||||
scheduled.set(true);
|
||||
} else {
|
||||
// Otherwise run the step right away
|
||||
runWorkflowStep(context, step, resourceId);
|
||||
// Otherwise, run the step right away
|
||||
context.setCurrentStep(step);
|
||||
runWorkflowStep(context);
|
||||
}
|
||||
});
|
||||
|
||||
if (scheduled.get()) {
|
||||
// if we scheduled a step, we stop processing the workflow here
|
||||
return;
|
||||
}
|
||||
|
||||
// if we've reached the end of the workflow, check if it is recurring or if we can mark it as completed
|
||||
if (workflow.isRecurring()) {
|
||||
// if the workflow is recurring, restart it
|
||||
context.restart();
|
||||
processWorkflow(workflow, context, resourceId);
|
||||
restartWorkflow(context);
|
||||
} else {
|
||||
// not recurring, remove the state record
|
||||
context.complete();
|
||||
workflowStateProvider.remove(context.getExecutionId());
|
||||
log.debugf("Workflow '%s' completed for resource %s (execution id: %s)", workflow.getName(), resourceId, executionId);
|
||||
workflowStateProvider.remove(executionId);
|
||||
}
|
||||
}
|
||||
|
||||
private void runWorkflowStep(WorkflowExecutionContext context, WorkflowStep step, String resourceId) {
|
||||
log.debugf("Running step %s on resource %s (execution id: %s)", step.getProviderId(), resourceId, context.getExecutionId());
|
||||
private void runWorkflowStep(DefaultWorkflowExecutionContext context) {
|
||||
String executionId = context.getExecutionId();
|
||||
WorkflowStep step = context.getCurrentStep();
|
||||
String resourceId = context.getResourceId();
|
||||
log.debugf("Running step %s on resource %s (execution id: %s)", step.getProviderId(), resourceId, executionId);
|
||||
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), session.getContext(), s -> {
|
||||
try {
|
||||
getStepProvider(step).run(List.of(resourceId));
|
||||
context.success(step);
|
||||
getStepProvider(step).run(context);
|
||||
log.debugf("Step %s completed successfully (execution id: %s)", step.getProviderId(), executionId);
|
||||
} catch(WorkflowExecutionException e) {
|
||||
context.fail(step, e.getMessage());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Step %s failed (execution id: %s)");
|
||||
String errorMessage = e.getMessage();
|
||||
if (errorMessage != null) {
|
||||
sb.append(" - error message: %s");
|
||||
log.debugf(sb.toString(), step.getProviderId(), executionId, errorMessage);
|
||||
}
|
||||
else {
|
||||
log.debugf(sb.toString(), step.getProviderId(), executionId);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
@ -361,7 +417,7 @@ public class WorkflowsManager {
|
||||
|
||||
public WorkflowRepresentation toRepresentation(Workflow workflow) {
|
||||
List<WorkflowConditionRepresentation> conditions = toConditionRepresentation(workflow);
|
||||
List<WorkflowStepRepresentation> steps = toRepresentation(getSteps(workflow.getId()));
|
||||
List<WorkflowStepRepresentation> steps = getSteps(workflow.getId()).map(this::toRepresentation).toList();
|
||||
|
||||
return new WorkflowRepresentation(workflow.getId(), workflow.getProviderId(), workflow.getConfig(), conditions, steps);
|
||||
}
|
||||
@ -391,18 +447,8 @@ public class WorkflowsManager {
|
||||
return conditions;
|
||||
}
|
||||
|
||||
private List<WorkflowStepRepresentation> toRepresentation(List<WorkflowStep> existingSteps) {
|
||||
if (existingSteps == null || existingSteps.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<WorkflowStepRepresentation> steps = new ArrayList<>();
|
||||
|
||||
for (WorkflowStep step : existingSteps) {
|
||||
steps.add(new WorkflowStepRepresentation(step.getId(), step.getProviderId(), step.getConfig()));
|
||||
}
|
||||
|
||||
return steps;
|
||||
private WorkflowStepRepresentation toRepresentation(WorkflowStep step) {
|
||||
return new WorkflowStepRepresentation(step.getId(), step.getProviderId(), step.getConfig());
|
||||
}
|
||||
|
||||
public Workflow toModel(WorkflowRepresentation rep) {
|
||||
@ -467,24 +513,6 @@ public class WorkflowsManager {
|
||||
getStepProviderFactory(step);
|
||||
}
|
||||
|
||||
/* ================================== Workflow execution context helper methods ================================== */
|
||||
|
||||
private WorkflowExecutionContext buildAndInitContext(Workflow workflow, String resourceId) {
|
||||
WorkflowExecutionContext context = new WorkflowExecutionContext(workflow, getSteps(workflow.getId()), resourceId);
|
||||
context.init();
|
||||
return context;
|
||||
}
|
||||
|
||||
private WorkflowExecutionContext buildFromScheduledStep(ScheduledStep scheduledStep) {
|
||||
return new WorkflowExecutionContext(
|
||||
getWorkflow(scheduledStep.workflowId()),
|
||||
getSteps(scheduledStep.workflowId()),
|
||||
scheduledStep.resourceId(),
|
||||
scheduledStep.stepId(),
|
||||
scheduledStep.executionId()
|
||||
);
|
||||
}
|
||||
|
||||
/* ============================================ Other utility methods ============================================ */
|
||||
|
||||
public Object resolveResource(ResourceType type, String resourceId) {
|
||||
|
||||
@ -22,20 +22,17 @@ public class AddRequiredActionStepProvider implements WorkflowStepProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(List<String> userIds) {
|
||||
public void run(WorkflowExecutionContext context) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
UserModel user = session.users().getUserById(realm, context.getResourceId());
|
||||
|
||||
for (String id : userIds) {
|
||||
UserModel user = session.users().getUserById(realm, id);
|
||||
|
||||
if (user != null) {
|
||||
try {
|
||||
UserModel.RequiredAction action = UserModel.RequiredAction.valueOf(stepModel.getConfig().getFirst(REQUIRED_ACTION_KEY));
|
||||
log.debugv("Adding required action {0} to user {1})", action, user.getId());
|
||||
user.addRequiredAction(action);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warnv("Invalid required action {0} configured in AddRequiredActionProvider", stepModel.getConfig().getFirst(REQUIRED_ACTION_KEY));
|
||||
}
|
||||
if (user != null) {
|
||||
try {
|
||||
UserModel.RequiredAction action = UserModel.RequiredAction.valueOf(stepModel.getConfig().getFirst(REQUIRED_ACTION_KEY));
|
||||
log.debugv("Adding required action {0} to user {1})", action, user.getId());
|
||||
user.addRequiredAction(action);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warnv("Invalid required action {0} configured in AddRequiredActionProvider", stepModel.getConfig().getFirst(REQUIRED_ACTION_KEY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,18 +39,15 @@ public class DeleteUserStepProvider implements WorkflowStepProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(List<String> ids) {
|
||||
public void run(WorkflowExecutionContext context) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
UserModel user = session.users().getUserById(realm, context.getResourceId());
|
||||
|
||||
for (String id : ids) {
|
||||
UserModel user = session.users().getUserById(realm, id);
|
||||
|
||||
if (user == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log.debugv("Deleting user {0} ({1})", user.getUsername(), user.getId());
|
||||
session.users().removeUser(realm, user);
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.debugv("Deleting user {0} ({1})", user.getUsername(), user.getId());
|
||||
session.users().removeUser(realm, user);
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,16 +39,13 @@ public class DisableUserStepProvider implements WorkflowStepProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(List<String> userIds) {
|
||||
public void run(WorkflowExecutionContext context) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
UserModel user = session.users().getUserById(realm, context.getResourceId());
|
||||
|
||||
for (String id : userIds) {
|
||||
UserModel user = session.users().getUserById(realm, id);
|
||||
|
||||
if (user != null && user.isEnabled()) {
|
||||
log.debugv("Disabling user {0} ({1})", user.getUsername(), user.getId());
|
||||
user.setEnabled(false);
|
||||
}
|
||||
if (user != null && user.isEnabled()) {
|
||||
log.debugv("Disabling user {0} ({1})", user.getUsername(), user.getId());
|
||||
user.setEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,27 +53,24 @@ public class NotifyUserStepProvider implements WorkflowStepProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(List<String> userIds) {
|
||||
public void run(WorkflowExecutionContext context) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
EmailTemplateProvider emailProvider = session.getProvider(EmailTemplateProvider.class).setRealm(realm);
|
||||
|
||||
String subjectKey = getSubjectKey();
|
||||
String bodyTemplate = getBodyTemplate();
|
||||
Map<String, Object> bodyAttributes = getBodyAttributes();
|
||||
UserModel user = session.users().getUserById(realm, context.getResourceId());
|
||||
|
||||
for (String id : userIds) {
|
||||
UserModel user = session.users().getUserById(realm, id);
|
||||
|
||||
if (user != null && user.getEmail() != null) {
|
||||
try {
|
||||
emailProvider.setUser(user).send(subjectKey, bodyTemplate, bodyAttributes);
|
||||
log.debugv("Notification email sent to user {0} ({1})", user.getUsername(), user.getEmail());
|
||||
} catch (EmailException e) {
|
||||
log.errorv(e, "Failed to send notification email to user {0} ({1})", user.getUsername(), user.getEmail());
|
||||
}
|
||||
} else if (user != null && user.getEmail() == null) {
|
||||
log.warnv("User {0} has no email address, skipping notification", user.getUsername());
|
||||
if (user != null && user.getEmail() != null) {
|
||||
try {
|
||||
emailProvider.setUser(user).send(subjectKey, bodyTemplate, bodyAttributes);
|
||||
log.debugv("Notification email sent to user {0} ({1})", user.getUsername(), user.getEmail());
|
||||
} catch (EmailException e) {
|
||||
log.errorv(e, "Failed to send notification email to user {0} ({1})", user.getUsername(), user.getEmail());
|
||||
}
|
||||
} else if (user != null && user.getEmail() == null) {
|
||||
log.warnv("User {0} has no email address, skipping notification", user.getUsername());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -45,20 +45,17 @@ public class SetUserAttributeStepProvider implements WorkflowStepProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(List<String> userIds) {
|
||||
public void run(WorkflowExecutionContext context) {
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
UserModel user = session.users().getUserById(realm, context.getResourceId());
|
||||
|
||||
for (String id : userIds) {
|
||||
UserModel user = session.users().getUserById(realm, id);
|
||||
if (user != null) {
|
||||
for (Entry<String, List<String>> entry : stepModel.getConfig().entrySet()) {
|
||||
String key = entry.getKey();
|
||||
|
||||
if (user != null) {
|
||||
for (Entry<String, List<String>> entry : stepModel.getConfig().entrySet()) {
|
||||
String key = entry.getKey();
|
||||
|
||||
if (!key.startsWith(CONFIG_AFTER) && !key.startsWith(CONFIG_PRIORITY)) {
|
||||
log.debugv("Setting attribute {0} to user {1})", key, user.getId());
|
||||
user.setAttribute(key, entry.getValue());
|
||||
}
|
||||
if (!key.startsWith(CONFIG_AFTER) && !key.startsWith(CONFIG_PRIORITY)) {
|
||||
log.debugv("Setting attribute {0} to user {1})", key, user.getId());
|
||||
user.setAttribute(key, entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
package org.keycloak.workflow.admin.resource;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
@ -19,9 +23,6 @@ import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowSetRepresentation;
|
||||
import org.keycloak.services.ErrorResponse;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class WorkflowsResource {
|
||||
|
||||
private final KeycloakSession session;
|
||||
@ -69,7 +70,7 @@ public class WorkflowsResource {
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public List<WorkflowRepresentation> list() {
|
||||
return manager.getWorkflows().stream().map(manager::toRepresentation).toList();
|
||||
public Stream<WorkflowRepresentation> list() {
|
||||
return manager.getWorkflows().map(manager::toRepresentation);
|
||||
}
|
||||
}
|
||||
|
||||
@ -253,7 +253,7 @@ public class BrokeredUserSessionRefreshTimeWorkflowTest {
|
||||
runOnServer.run(session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
Workflow workflow = manager.getWorkflows().get(0);
|
||||
Workflow workflow = manager.getWorkflows().toList().get(0);
|
||||
UserModel alice = session.users().getUserByUsername(realm, "alice");
|
||||
assertNotNull(alice);
|
||||
|
||||
|
||||
@ -204,7 +204,7 @@ public class WorkflowManagementTest {
|
||||
configureSessionContext(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
List<Workflow> registeredWorkflows = manager.getWorkflows();
|
||||
List<Workflow> registeredWorkflows = manager.getWorkflows().toList();
|
||||
assertEquals(1, registeredWorkflows.size());
|
||||
WorkflowStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session);
|
||||
List<ScheduledStep> steps = stateProvider.getScheduledStepsByWorkflow(id);
|
||||
@ -292,12 +292,13 @@ public class WorkflowManagementTest {
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
UserModel user = session.users().getUserByUsername(realm,"testuser");
|
||||
|
||||
List<Workflow> registeredWorkflows = manager.getWorkflows();
|
||||
List<Workflow> registeredWorkflows = manager.getWorkflows().toList();
|
||||
assertEquals(1, registeredWorkflows.size());
|
||||
|
||||
Workflow workflow = registeredWorkflows.get(0);
|
||||
assertEquals(2, manager.getSteps(workflow.getId()).size());
|
||||
WorkflowStep notifyStep = manager.getSteps(workflow.getId()).get(0);
|
||||
List<WorkflowStep> steps = manager.getSteps(workflow.getId()).toList();
|
||||
assertEquals(2, steps.size());
|
||||
WorkflowStep notifyStep = steps.get(0);
|
||||
|
||||
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
|
||||
ScheduledStep scheduledStep = stateProvider.getScheduledStep(workflow.getId(), user.getId());
|
||||
@ -312,7 +313,7 @@ public class WorkflowManagementTest {
|
||||
user = session.users().getUserById(realm, user.getId());
|
||||
|
||||
// Verify that the next step was scheduled for the user
|
||||
WorkflowStep disableStep = manager.getSteps(workflow.getId()).get(1);
|
||||
WorkflowStep disableStep = manager.getSteps(workflow.getId()).toList().get(1);
|
||||
scheduledStep = stateProvider.getScheduledStep(workflow.getId(), user.getId());
|
||||
assertNotNull(scheduledStep, "A step should have been scheduled for the user " + user.getUsername());
|
||||
assertEquals(disableStep.getId(), scheduledStep.stepId(), "The second step should have been scheduled");
|
||||
@ -378,12 +379,13 @@ public class WorkflowManagementTest {
|
||||
runOnServer.run((RunOnServer) session -> {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
WorkflowsManager workflowsManager = new WorkflowsManager(session);
|
||||
List<Workflow> registeredWorkflows = workflowsManager.getWorkflows();
|
||||
List<Workflow> registeredWorkflows = workflowsManager.getWorkflows().toList();
|
||||
assertEquals(1, registeredWorkflows.size());
|
||||
Workflow workflow = registeredWorkflows.get(0);
|
||||
|
||||
assertEquals(2, workflowsManager.getSteps(workflow.getId()).size());
|
||||
WorkflowStep notifyStep = workflowsManager.getSteps(workflow.getId()).get(0);
|
||||
List<WorkflowStep> steps = workflowsManager.getSteps(workflow.getId()).toList();
|
||||
assertEquals(2, steps.size());
|
||||
WorkflowStep notifyStep = steps.get(0);
|
||||
|
||||
// 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);
|
||||
@ -402,7 +404,7 @@ public class WorkflowManagementTest {
|
||||
workflowsManager.runScheduledSteps();
|
||||
|
||||
// check the same users are now scheduled to run the second step.
|
||||
WorkflowStep disableStep = workflowsManager.getSteps(workflow.getId()).get(1);
|
||||
WorkflowStep disableStep = workflowsManager.getSteps(workflow.getId()).toList().get(1);
|
||||
scheduledSteps = stateProvider.getScheduledStepsByWorkflow(workflow);
|
||||
assertEquals(3, scheduledSteps.size());
|
||||
scheduledSteps.forEach(scheduledStep -> {
|
||||
@ -501,7 +503,7 @@ public class WorkflowManagementTest {
|
||||
RealmModel realm = configureSessionContext(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
List<Workflow> registeredWorkflow = manager.getWorkflows();
|
||||
List<Workflow> registeredWorkflow = manager.getWorkflows().toList();
|
||||
assertEquals(1, registeredWorkflow.size());
|
||||
WorkflowStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session);
|
||||
List<ScheduledStep> scheduledSteps = stateProvider.getScheduledStepsByWorkflow(registeredWorkflow.get(0));
|
||||
@ -584,8 +586,8 @@ public class WorkflowManagementTest {
|
||||
manager.runScheduledSteps();
|
||||
|
||||
UserModel user = session.users().getUserByUsername(realm, "testuser");
|
||||
Workflow workflow = manager.getWorkflows().get(0);
|
||||
WorkflowStep step = manager.getSteps(workflow.getId()).get(0);
|
||||
Workflow workflow = manager.getWorkflows().toList().get(0);
|
||||
WorkflowStep step = manager.getSteps(workflow.getId()).toList().get(0);
|
||||
|
||||
// Verify that the step was scheduled again for the user
|
||||
WorkflowStateProvider stateProvider = session.getProvider(WorkflowStateProvider.class);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user