Add validation for Workflwow, Condition and Steps fields

Closes #43559

Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
vramik 2025-10-17 10:36:00 +02:00 committed by Pedro Igor
parent c88e56707b
commit b1c0c15ad5
12 changed files with 52 additions and 7 deletions

View File

@ -34,6 +34,7 @@ import org.keycloak.representations.workflows.WorkflowConditionRepresentation;
import org.keycloak.representations.workflows.WorkflowConstants;
import org.keycloak.representations.workflows.WorkflowRepresentation;
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
import org.keycloak.utils.StringUtil;
import java.util.ArrayList;
import java.util.HashMap;
@ -84,9 +85,10 @@ 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, List<WorkflowStep> steps) {
private void addSteps(Workflow workflow, List<WorkflowStepRepresentation> steps) {
steps = ofNullable(steps).orElse(List.of());
for (int i = 0; i < steps.size(); i++) {
WorkflowStep step = steps.get(i);
WorkflowStep step = toModel(steps.get(i));
// assign priority based on index.
step.setPriority(i + 1);
@ -441,6 +443,7 @@ public class WorkflowsManager {
List<WorkflowConditionRepresentation> conditions = ofNullable(rep.getConditions()).orElse(List.of());
for (WorkflowConditionRepresentation condition : conditions) {
validateField(condition, "uses", condition.getUses());
String conditionProviderId = condition.getUses();
getConditionProviderFactory(conditionProviderId);
config.computeIfAbsent(CONFIG_CONDITIONS, key -> new ArrayList<>()).add(conditionProviderId);
@ -456,9 +459,7 @@ public class WorkflowsManager {
Workflow workflow = addWorkflow(new Workflow(rep.getUses(), config));
List<WorkflowStep> steps = rep.getSteps().stream().map(this::toModel).toList();
addSteps(workflow, steps);
addSteps(workflow, rep.getSteps());
return workflow;
}
@ -470,10 +471,13 @@ public class WorkflowsManager {
}
private void validateWorkflow(WorkflowRepresentation rep) {
validateField(rep, "name", rep.getName());
validateEvents(rep.getOnValues());
// if a workflow has a restart step, at least one of the previous steps must be scheduled to prevent an infinite loop of immediate executions
List<WorkflowStepRepresentation> steps = ofNullable(rep.getSteps()).orElse(List.of());
steps.forEach(step -> validateField(step, "uses", step.getUses()));
List<WorkflowStepRepresentation> restartSteps = steps.stream()
.filter(step -> Objects.equals("restart", step.getUses()))
.toList();
@ -494,6 +498,12 @@ public class WorkflowsManager {
}
}
private void validateField(Object obj, String fieldName, String value) {
if (StringUtil.isBlank(value)) {
throw new ModelValidationException("%s field '%s' cannot be null or empty.".formatted(obj.getClass().getCanonicalName(), fieldName));
}
}
private static void validateEvents(List<String> events) {
for (String event : ofNullable(events).orElse(List.of())) {
try {

View File

@ -30,6 +30,7 @@ public class AddRequiredActionTest {
public void testStepRun() {
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(UserCreationTimeWorkflowProviderFactory.ID)
.name(UserCreationTimeWorkflowProviderFactory.ID)
.withSteps(
WorkflowStepRepresentation.create()
.of(AddRequiredActionStepProviderFactory.ID)

View File

@ -48,6 +48,7 @@ public class AdhocWorkflowTest {
public void testCreate() {
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(EventBasedWorkflowProviderFactory.ID)
.name(EventBasedWorkflowProviderFactory.ID)
.withSteps(WorkflowStepRepresentation.create()
.of(SetUserAttributeStepProviderFactory.ID)
.withConfig("message", "message")
@ -67,6 +68,7 @@ public class AdhocWorkflowTest {
public void testBindAdHocScheduledWithImmediateWorkflow() {
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(EventBasedWorkflowProviderFactory.ID)
.name(EventBasedWorkflowProviderFactory.ID)
.withSteps(WorkflowStepRepresentation.create()
.of(SetUserAttributeStepProviderFactory.ID)
.withConfig("message", "message")
@ -91,6 +93,7 @@ public class AdhocWorkflowTest {
public void testRunAdHocScheduledWorkflow() {
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(EventBasedWorkflowProviderFactory.ID)
.name(EventBasedWorkflowProviderFactory.ID)
.withSteps(WorkflowStepRepresentation.create()
.of(SetUserAttributeStepProviderFactory.ID)
.after(Duration.ofDays(5))
@ -112,6 +115,7 @@ public class AdhocWorkflowTest {
public void testRunAdHocImmediateWorkflow() {
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(EventBasedWorkflowProviderFactory.ID)
.name(EventBasedWorkflowProviderFactory.ID)
.withSteps(WorkflowStepRepresentation.create()
.of(SetUserAttributeStepProviderFactory.ID)
.withConfig("message", "message")
@ -141,6 +145,7 @@ public class AdhocWorkflowTest {
public void testRunAdHocTimedWorkflow() {
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(EventBasedWorkflowProviderFactory.ID)
.name(EventBasedWorkflowProviderFactory.ID)
.withSteps(WorkflowStepRepresentation.create()
.of(SetUserAttributeStepProviderFactory.ID)
.withConfig("message", "message")

View File

@ -129,6 +129,7 @@ public class BrokeredUserSessionRefreshTimeWorkflowTest {
public void testInvalidateWorkflowOnIdentityProviderRemoval() {
consumerRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(UserSessionRefreshTimeWorkflowProviderFactory.ID)
.name(UserSessionRefreshTimeWorkflowProviderFactory.ID)
.onEvent(ResourceOperationType.USER_LOGIN.toString())
.onConditions(WorkflowConditionRepresentation.create()
.of(IdentityProviderWorkflowConditionFactory.ID)
@ -165,6 +166,7 @@ public class BrokeredUserSessionRefreshTimeWorkflowTest {
public void tesRunStepOnFederatedUser() {
consumerRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(UserSessionRefreshTimeWorkflowProviderFactory.ID)
.name(UserSessionRefreshTimeWorkflowProviderFactory.ID)
.onEvent(ResourceOperationType.USER_LOGIN.toString())
.onConditions(WorkflowConditionRepresentation.create()
.of(IdentityProviderWorkflowConditionFactory.ID)

View File

@ -224,6 +224,7 @@ public class ExpressionConditionWorkflowTest {
private String createWorkflow(String expression) {
WorkflowSetRepresentation expectedWorkflows = WorkflowRepresentation.create()
.of(EventBasedWorkflowProviderFactory.ID)
.name(EventBasedWorkflowProviderFactory.ID)
.onEvent(ResourceOperationType.USER_LOGIN.name())
.onConditions(WorkflowConditionRepresentation.create()
.of(ExpressionWorkflowConditionFactory.ID)

View File

@ -70,6 +70,7 @@ public class GroupMembershipJoinWorkflowTest {
WorkflowSetRepresentation expectedWorkflows = WorkflowRepresentation.create()
.of(EventBasedWorkflowProviderFactory.ID)
.name(EventBasedWorkflowProviderFactory.ID)
.onEvent(ResourceOperationType.USER_GROUP_MEMBERSHIP_ADD.name())
.onConditions(WorkflowConditionRepresentation.create()
.of(GroupMembershipWorkflowConditionFactory.ID)
@ -128,6 +129,7 @@ public class GroupMembershipJoinWorkflowTest {
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(UserSessionRefreshTimeWorkflowProviderFactory.ID)
.name(UserSessionRefreshTimeWorkflowProviderFactory.ID)
.onEvent(ResourceOperationType.USER_LOGIN.toString())
.onConditions(WorkflowConditionRepresentation.create()
.of(GroupMembershipWorkflowConditionFactory.ID)

View File

@ -140,6 +140,7 @@ public class RoleWorkflowConditionTest {
WorkflowSetRepresentation expectedWorkflows = WorkflowRepresentation.create()
.of(EventBasedWorkflowProviderFactory.ID)
.name(EventBasedWorkflowProviderFactory.ID)
.onEvent(ResourceOperationType.USER_ROLE_ADD.name())
.onConditions(WorkflowConditionRepresentation.create()
.of(RoleWorkflowConditionFactory.ID)

View File

@ -77,6 +77,7 @@ public class StepRunnerScheduledTaskTest {
realm.workflows().create(WorkflowRepresentation.create()
.of(UserCreationTimeWorkflowProviderFactory.ID)
.name(UserCreationTimeWorkflowProviderFactory.ID)
.withSteps(
WorkflowStepRepresentation.create().of(SetUserAttributeStepProviderFactory.ID)
.after(Duration.ofDays(5))

View File

@ -135,6 +135,7 @@ public class UserAttributeWorkflowConditionTest {
private void createWorkflow(Map<String, List<String>> attributes) {
WorkflowSetRepresentation expectedWorkflows = WorkflowRepresentation.create()
.of(EventBasedWorkflowProviderFactory.ID)
.name(EventBasedWorkflowProviderFactory.ID)
.onEvent(ResourceOperationType.USER_ADD.name())
.onConditions(WorkflowConditionRepresentation.create()
.of(UserAttributeWorkflowConditionFactory.ID)

View File

@ -64,6 +64,7 @@ public class UserCreationTimeWorkflowTest {
public void testDisableUserBasedOnCreationDate() {
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(UserCreationTimeWorkflowProviderFactory.ID)
.name(UserCreationTimeWorkflowProviderFactory.ID)
.withSteps(
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
.after(Duration.ofDays(5))

View File

@ -91,6 +91,7 @@ public class UserSessionRefreshTimeWorkflowTest {
public void testDisabledUserAfterInactivityPeriod() {
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(UserSessionRefreshTimeWorkflowProviderFactory.ID)
.name(UserSessionRefreshTimeWorkflowProviderFactory.ID)
.onEvent(ResourceOperationType.USER_LOGIN.toString())
.concurrency().cancelIfRunning() // this setting enables restarting the workflow
.withSteps(
@ -174,6 +175,7 @@ public class UserSessionRefreshTimeWorkflowTest {
public void testMultipleWorkflows() {
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(UserSessionRefreshTimeWorkflowProviderFactory.ID)
.name(UserSessionRefreshTimeWorkflowProviderFactory.ID + "_1")
.onEvent(ResourceOperationType.USER_LOGIN.toString())
.withSteps(
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
@ -181,7 +183,9 @@ public class UserSessionRefreshTimeWorkflowTest {
.withConfig("custom_subject_key", "notifier1_subject")
.withConfig("custom_message", "notifier1_message")
.build()
).of(UserSessionRefreshTimeWorkflowProviderFactory.ID)
)
.of(UserSessionRefreshTimeWorkflowProviderFactory.ID)
.name(UserSessionRefreshTimeWorkflowProviderFactory.ID + "_2")
.onEvent(ResourceOperationType.USER_LOGIN.toString())
.withSteps(
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)

View File

@ -99,6 +99,7 @@ public class WorkflowManagementTest {
public void testCreate() {
WorkflowSetRepresentation expectedWorkflows = WorkflowRepresentation.create()
.of(UserCreationTimeWorkflowProviderFactory.ID)
.name(UserCreationTimeWorkflowProviderFactory.ID)
.withSteps(
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
.after(Duration.ofDays(5))
@ -128,6 +129,7 @@ public class WorkflowManagementTest {
public void testCreateWithNoConditions() {
WorkflowSetRepresentation expectedWorkflows = WorkflowRepresentation.create()
.of(EventBasedWorkflowProviderFactory.ID)
.name(EventBasedWorkflowProviderFactory.ID)
.withSteps(
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
.after(Duration.ofDays(5))
@ -148,6 +150,7 @@ public class WorkflowManagementTest {
public void testCreateWithNoWorkflowSetDefaultWorkflow() {
WorkflowSetRepresentation expectedWorkflows = WorkflowRepresentation.create()
.of(null)
.name("default-workflow")
.withSteps(
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
.after(Duration.ofDays(5))
@ -173,6 +176,7 @@ public class WorkflowManagementTest {
workflows.create(WorkflowRepresentation.create()
.of(UserCreationTimeWorkflowProviderFactory.ID)
.name(UserCreationTimeWorkflowProviderFactory.ID)
.onEvent(ResourceOperationType.USER_ADD.toString())
.withSteps(
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
@ -180,7 +184,9 @@ public class WorkflowManagementTest {
.build(),
WorkflowStepRepresentation.create().of(RestartWorkflowStepProviderFactory.ID)
.build()
).of(EventBasedWorkflowProviderFactory.ID)
)
.of(EventBasedWorkflowProviderFactory.ID)
.name(EventBasedWorkflowProviderFactory.ID)
.onEvent(ResourceOperationType.USER_LOGIN.toString())
.withSteps(
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
@ -277,6 +283,7 @@ public class WorkflowManagementTest {
public void testWorkflowDoesNotFallThroughStepsInSingleRun() {
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(UserCreationTimeWorkflowProviderFactory.ID)
.name(UserCreationTimeWorkflowProviderFactory.ID)
.onEvent(ResourceOperationType.USER_ADD.toString())
.withSteps(
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
@ -353,6 +360,7 @@ public class WorkflowManagementTest {
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(UserCreationTimeWorkflowProviderFactory.ID)
.name(UserCreationTimeWorkflowProviderFactory.ID)
.onEvent(ResourceOperationType.USER_FEDERATED_IDENTITY_ADD.name())
.onConditions(WorkflowConditionRepresentation.create()
.of(IdentityProviderWorkflowConditionFactory.ID)
@ -569,6 +577,7 @@ public class WorkflowManagementTest {
public void testRecurringWorkflow() {
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(UserCreationTimeWorkflowProviderFactory.ID)
.name(UserCreationTimeWorkflowProviderFactory.ID)
.onEvent(ResourceOperationType.USER_ADD.toString())
.withSteps(
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
@ -616,6 +625,7 @@ 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)
.name(UserCreationTimeWorkflowProviderFactory.ID)
.withSteps(
WorkflowStepRepresentation.create().of(SetUserAttributeStepProviderFactory.ID)
.withConfig("message", "message")
@ -643,6 +653,7 @@ public class WorkflowManagementTest {
public void testFailCreateWorkflowWithNegativeTime() {
WorkflowSetRepresentation workflows = WorkflowRepresentation.create()
.of(UserCreationTimeWorkflowProviderFactory.ID)
.name(UserCreationTimeWorkflowProviderFactory.ID)
.withSteps(
WorkflowStepRepresentation.create().of(SetUserAttributeStepProviderFactory.ID)
.after(Duration.ofDays(-5))
@ -660,6 +671,7 @@ public class WorkflowManagementTest {
// Create workflow: disable at 10 days, notify 3 days before (at day 7)
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(UserCreationTimeWorkflowProviderFactory.ID)
.name(UserCreationTimeWorkflowProviderFactory.ID)
.withSteps(
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
.after(Duration.ofDays(7))
@ -698,6 +710,7 @@ public class WorkflowManagementTest {
// Create workflow: delete at 30 days, notify 15 days before (at day 15)
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(UserCreationTimeWorkflowProviderFactory.ID)
.name(UserCreationTimeWorkflowProviderFactory.ID)
.withSteps(
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
.after(Duration.ofDays(15))
@ -736,6 +749,7 @@ public class WorkflowManagementTest {
// Create workflow: disable at 7 days, notify 2 days before (at day 5) with custom message
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(UserCreationTimeWorkflowProviderFactory.ID)
.name(UserCreationTimeWorkflowProviderFactory.ID)
.withSteps(
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
.after(Duration.ofDays(5))
@ -774,6 +788,7 @@ public class WorkflowManagementTest {
public void testNotifyUserStepSkipsUsersWithoutEmailButLogsWarning() {
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(UserCreationTimeWorkflowProviderFactory.ID)
.name(UserCreationTimeWorkflowProviderFactory.ID)
.withSteps(
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
.after(Duration.ofDays(5))
@ -813,6 +828,7 @@ public class WorkflowManagementTest {
// Create workflow: just disable at 30 days with one notification before
managedRealm.admin().workflows().create(WorkflowRepresentation.create()
.of(UserCreationTimeWorkflowProviderFactory.ID)
.name(UserCreationTimeWorkflowProviderFactory.ID)
.withSteps(
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
.after(Duration.ofDays(15))