Allow passing a context to steps

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2025-10-10 11:25:25 -03:00
parent 5b5a83b800
commit fa581c8148
15 changed files with 291 additions and 281 deletions

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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) {

View File

@ -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));
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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);