mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
[RLM] Allow adding and removing actions to existing policies
Closes #42384 Signed-off-by: Martin Kanis <mkanis@redhat.com>
This commit is contained in:
parent
44b4235b50
commit
7ae9ebb467
@ -36,4 +36,7 @@ public interface WorkflowResource {
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
void bind(@PathParam("type") String type, @PathParam("resourceId") String resourceId, Long milliseconds);
|
||||
|
||||
@Path("steps")
|
||||
WorkflowStepsResource steps();
|
||||
}
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
package org.keycloak.admin.client.resource;
|
||||
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface WorkflowStepsResource {
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
List<WorkflowStepRepresentation> list();
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
Response create(WorkflowStepRepresentation stepRep, @QueryParam("position") Integer position);
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
Response create(WorkflowStepRepresentation stepRep);
|
||||
|
||||
@Path("{stepId}")
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
WorkflowStepRepresentation get(@PathParam("stepId") String stepId);
|
||||
|
||||
@Path("{stepId}")
|
||||
@DELETE
|
||||
Response delete(@PathParam("stepId") String stepId);
|
||||
}
|
||||
@ -433,4 +433,111 @@ public class WorkflowsManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public WorkflowStep addStepToWorkflow(String workflowId, WorkflowStep step, Integer position) {
|
||||
Objects.requireNonNull(workflowId, "workflowId cannot be null");
|
||||
Objects.requireNonNull(step, "step cannot be null");
|
||||
|
||||
List<WorkflowStep> existingSteps = getSteps(workflowId);
|
||||
|
||||
int targetPosition = position != null ? position : existingSteps.size();
|
||||
if (targetPosition < 0 || targetPosition > existingSteps.size()) {
|
||||
throw new BadRequestException("Invalid position: " + targetPosition + ". Must be between 0 and " + existingSteps.size());
|
||||
}
|
||||
|
||||
// First, shift existing steps at and after the target position to make room
|
||||
shiftStepsForInsertion(targetPosition, existingSteps);
|
||||
|
||||
step.setPriority(targetPosition + 1);
|
||||
WorkflowStep addedStep = addStep(workflowId, step);
|
||||
|
||||
updateScheduledStepsAfterStepChange(workflowId);
|
||||
|
||||
log.debugf("Added step %s to workflow %s at position %d", addedStep.getId(), workflowId, targetPosition);
|
||||
return addedStep;
|
||||
}
|
||||
|
||||
public void removeStepFromWorkflow(String workflowId, String stepId) {
|
||||
Objects.requireNonNull(workflowId, "workflowId 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)) {
|
||||
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);
|
||||
|
||||
log.debugf("Removed step %s from workflow %s", stepId, workflowId);
|
||||
}
|
||||
|
||||
private void shiftStepsForInsertion(int insertPosition, List<WorkflowStep> existingSteps) {
|
||||
RealmModel realm = getRealm();
|
||||
|
||||
// Shift all steps at and after the insertion position by +1 priority
|
||||
for (int i = insertPosition; i < existingSteps.size(); i++) {
|
||||
WorkflowStep step = existingSteps.get(i);
|
||||
step.setPriority(step.getPriority() + 1);
|
||||
updateStepComponent(realm, step);
|
||||
}
|
||||
}
|
||||
|
||||
private void reorderAllSteps(String workflowId) {
|
||||
List<WorkflowStep> steps = getSteps(workflowId);
|
||||
RealmModel realm = getRealm();
|
||||
|
||||
for (int i = 0; i < steps.size(); i++) {
|
||||
WorkflowStep step = steps.get(i);
|
||||
step.setPriority(i + 1);
|
||||
updateStepComponent(realm, step);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStepComponent(RealmModel realm, WorkflowStep step) {
|
||||
ComponentModel component = realm.getComponent(step.getId());
|
||||
component.setConfig(step.getConfig());
|
||||
realm.updateComponent(component);
|
||||
}
|
||||
|
||||
private void updateScheduledStepsAfterStepChange(String workflowId) {
|
||||
List<WorkflowStep> steps = getSteps(workflowId);
|
||||
|
||||
if (steps.isEmpty()) {
|
||||
workflowStateProvider.remove(workflowId);
|
||||
return;
|
||||
}
|
||||
|
||||
for (ScheduledStep scheduled : workflowStateProvider.getScheduledStepsByWorkflow(workflowId)) {
|
||||
boolean stepStillExists = steps.stream()
|
||||
.anyMatch(step -> step.getId().equals(scheduled.stepId()));
|
||||
|
||||
if (!stepStillExists) {
|
||||
Workflow workflow = getWorkflow(workflowId);
|
||||
workflowStateProvider.scheduleStep(workflow, steps.get(0), scheduled.resourceId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public WorkflowStepRepresentation toStepRepresentation(WorkflowStep step) {
|
||||
List<WorkflowStepRepresentation> steps = step.getSteps().stream()
|
||||
.map(this::toStepRepresentation)
|
||||
.toList();
|
||||
return new WorkflowStepRepresentation(step.getId(), step.getProviderId(), step.getConfig(), steps);
|
||||
}
|
||||
|
||||
public WorkflowStep toStepModel(WorkflowStepRepresentation rep) {
|
||||
List<WorkflowStep> subSteps = new ArrayList<>();
|
||||
|
||||
for (WorkflowStepRepresentation subStep : ofNullable(rep.getSteps()).orElse(List.of())) {
|
||||
subSteps.add(toStepModel(subStep));
|
||||
}
|
||||
|
||||
return new WorkflowStep(rep.getProviderId(), rep.getConfig(), subSteps);
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,4 +82,9 @@ public class WorkflowResource {
|
||||
|
||||
manager.bind(workflow, type, resourceId);
|
||||
}
|
||||
|
||||
@Path("steps")
|
||||
public WorkflowStepsResource steps() {
|
||||
return new WorkflowStepsResource(manager, workflow);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.workflow.admin.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.ws.rs.BadRequestException;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Content;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
|
||||
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
|
||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
|
||||
import org.keycloak.models.workflow.WorkflowStep;
|
||||
import org.keycloak.models.workflow.Workflow;
|
||||
import org.keycloak.models.workflow.WorkflowsManager;
|
||||
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
|
||||
/**
|
||||
* Resource for managing steps within a workflow.
|
||||
*
|
||||
*/
|
||||
@Tag(name = "Workflow Steps", description = "Manage steps within workflows")
|
||||
public class WorkflowStepsResource {
|
||||
|
||||
private final WorkflowsManager workflowsManager;
|
||||
private final Workflow workflow;
|
||||
|
||||
public WorkflowStepsResource(WorkflowsManager workflowsManager, Workflow workflow) {
|
||||
this.workflowsManager = workflowsManager;
|
||||
this.workflow = workflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all steps for this workflow.
|
||||
*
|
||||
* @return list of steps
|
||||
*/
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Get all steps for this workflow")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Success",
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(type = SchemaType.ARRAY,
|
||||
implementation = WorkflowStepRepresentation.class)))
|
||||
})
|
||||
public List<WorkflowStepRepresentation> getSteps() {
|
||||
return workflowsManager.getSteps(workflow.getId()).stream()
|
||||
.map(workflowsManager::toStepRepresentation)
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new step to this workflow.
|
||||
*
|
||||
* @param stepRep step representation
|
||||
* @param position optional position to insert the step at (0-based index)
|
||||
* @return the created step
|
||||
*/
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Add a new step to this workflow")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Step created successfully",
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(implementation = WorkflowStepRepresentation.class))),
|
||||
@APIResponse(responseCode = "400", description = "Invalid step representation or position")
|
||||
})
|
||||
public Response addStep(
|
||||
@RequestBody(description = "Step to add", required = true,
|
||||
content = @Content(schema = @Schema(implementation = WorkflowStepRepresentation.class)))
|
||||
WorkflowStepRepresentation stepRep,
|
||||
@Parameter(description = "Position to insert the step at (0-based index). If not specified, step is added at the end.")
|
||||
@QueryParam("position") Integer position) {
|
||||
if (stepRep == null) {
|
||||
throw new BadRequestException("Step representation cannot be null");
|
||||
}
|
||||
|
||||
WorkflowStep step = workflowsManager.toStepModel(stepRep);
|
||||
WorkflowStep addedStep = workflowsManager.addStepToWorkflow(workflow.getId(), step, position);
|
||||
|
||||
WorkflowStepRepresentation result = workflowsManager.toStepRepresentation(addedStep);
|
||||
return Response.ok(result).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a step from this workflow.
|
||||
*
|
||||
* @param stepId ID of the step to remove
|
||||
* @return no content response on success
|
||||
*/
|
||||
@Path("{stepId}")
|
||||
@DELETE
|
||||
@Operation(summary = "Remove a step from this workflow")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "204", description = "Step removed successfully"),
|
||||
@APIResponse(responseCode = "400", description = "Invalid step ID"),
|
||||
@APIResponse(responseCode = "404", description = "Step not found")
|
||||
})
|
||||
public Response removeStep(
|
||||
@Parameter(description = "ID of the step to remove", required = true)
|
||||
@PathParam("stepId") String stepId) {
|
||||
if (stepId == null || stepId.trim().isEmpty()) {
|
||||
throw new BadRequestException("Step ID cannot be null or empty");
|
||||
}
|
||||
|
||||
workflowsManager.removeStepFromWorkflow(workflow.getId(), stepId);
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific step by its ID.
|
||||
*
|
||||
* @param stepId ID of the step to retrieve
|
||||
* @return the step representation
|
||||
*/
|
||||
@Path("{stepId}")
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(summary = "Get a specific step by its ID")
|
||||
@APIResponses({
|
||||
@APIResponse(responseCode = "200", description = "Step found",
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(implementation = WorkflowStepRepresentation.class))),
|
||||
@APIResponse(responseCode = "400", description = "Invalid step ID"),
|
||||
@APIResponse(responseCode = "404", description = "Step not found")
|
||||
})
|
||||
public WorkflowStepRepresentation getStep(
|
||||
@Parameter(description = "ID of the step to retrieve", required = true)
|
||||
@PathParam("stepId") String stepId) {
|
||||
if (stepId == null || stepId.trim().isEmpty()) {
|
||||
throw new BadRequestException("Step ID cannot be null or empty");
|
||||
}
|
||||
|
||||
WorkflowStep step = workflowsManager.getStepById(stepId);
|
||||
if (step == null) {
|
||||
throw new BadRequestException("Step not found: " + stepId);
|
||||
}
|
||||
|
||||
return workflowsManager.toStepRepresentation(step);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,296 @@
|
||||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.tests.admin.model.workflow;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.keycloak.admin.client.resource.WorkflowResource;
|
||||
import org.keycloak.admin.client.resource.WorkflowStepsResource;
|
||||
import org.keycloak.admin.client.resource.WorkflowsResource;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.workflow.DisableUserStepProviderFactory;
|
||||
import org.keycloak.models.workflow.NotifyUserStepProviderFactory;
|
||||
import org.keycloak.models.workflow.UserCreationTimeWorkflowProviderFactory;
|
||||
import org.keycloak.models.workflow.WorkflowStep;
|
||||
import org.keycloak.models.workflow.Workflow;
|
||||
import org.keycloak.models.workflow.WorkflowsManager;
|
||||
import org.keycloak.models.workflow.WorkflowStateProvider;
|
||||
import org.keycloak.models.workflow.ResourceType;
|
||||
import org.keycloak.models.workflow.ResourceOperationType;
|
||||
import org.keycloak.representations.workflows.WorkflowRepresentation;
|
||||
import org.keycloak.representations.workflows.WorkflowStepRepresentation;
|
||||
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;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@KeycloakIntegrationTest(config = WorkflowsServerConfig.class)
|
||||
public class WorkflowStepManagementTest {
|
||||
|
||||
@InjectRealm(lifecycle = LifeCycle.METHOD)
|
||||
ManagedRealm managedRealm;
|
||||
|
||||
@InjectRunOnServer(permittedPackages = "org.keycloak.tests")
|
||||
RunOnServerClient runOnServer;
|
||||
|
||||
private WorkflowsResource workflowsResource;
|
||||
private String workflowId;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
workflowsResource = managedRealm.admin().workflows();
|
||||
|
||||
// Create a workflow for testing (need at least one step for persistence)
|
||||
List<WorkflowRepresentation> workflows = WorkflowRepresentation.create()
|
||||
.of(UserCreationTimeWorkflowProviderFactory.ID)
|
||||
.onEvent(ResourceOperationType.CREATE.toString())
|
||||
.name("Test Workflow")
|
||||
.withSteps(
|
||||
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
|
||||
.after(Duration.ofDays(1))
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
|
||||
try (Response response = workflowsResource.create(workflows)) {
|
||||
if (response.getStatus() != 201) {
|
||||
String responseBody = response.readEntity(String.class);
|
||||
System.err.println("Workflow creation failed with status: " + response.getStatus());
|
||||
System.err.println("Response body: " + responseBody);
|
||||
}
|
||||
assertEquals(201, response.getStatus());
|
||||
|
||||
// Since we created a list of workflows, get the first one from the list
|
||||
List<WorkflowRepresentation> createdWorkflows = workflowsResource.list();
|
||||
assertNotNull(createdWorkflows);
|
||||
assertEquals(1, createdWorkflows.size());
|
||||
workflowId = createdWorkflows.get(0).getId();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddStepToWorkflow() {
|
||||
WorkflowResource workflow = workflowsResource.workflow(workflowId);
|
||||
WorkflowStepsResource steps = workflow.steps();
|
||||
|
||||
WorkflowStepRepresentation stepRep = new WorkflowStepRepresentation();
|
||||
stepRep.setProviderId(DisableUserStepProviderFactory.ID);
|
||||
stepRep.setConfig("name", "Test Step");
|
||||
stepRep.setConfig("after", String.valueOf(Duration.ofDays(30).toMillis()));
|
||||
|
||||
try (Response response = steps.create(stepRep)) {
|
||||
assertEquals(200, response.getStatus());
|
||||
WorkflowStepRepresentation addedStep = response.readEntity(WorkflowStepRepresentation.class);
|
||||
|
||||
assertNotNull(addedStep);
|
||||
assertNotNull(addedStep.getId());
|
||||
assertEquals(DisableUserStepProviderFactory.ID, addedStep.getProviderId());
|
||||
}
|
||||
|
||||
// Verify step is in workflow (should be 2 total: setup step + our added step)
|
||||
List<WorkflowStepRepresentation> allSteps = steps.list();
|
||||
assertEquals(2, allSteps.size());
|
||||
|
||||
// Verify our added step is present
|
||||
boolean foundOurStep = allSteps.stream()
|
||||
.anyMatch(step -> DisableUserStepProviderFactory.ID.equals(step.getProviderId()) &&
|
||||
"Test Step".equals(step.getConfig().getFirst("name")));
|
||||
assertTrue(foundOurStep, "Our added step should be present in the workflow");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveStepFromWorkflow() {
|
||||
WorkflowResource workflow = workflowsResource.workflow(workflowId);
|
||||
WorkflowStepsResource steps = workflow.steps();
|
||||
|
||||
// Add one more step
|
||||
WorkflowStepRepresentation step1 = new WorkflowStepRepresentation();
|
||||
step1.setProviderId(DisableUserStepProviderFactory.ID);
|
||||
step1.setConfig("after", String.valueOf(Duration.ofDays(30).toMillis()));
|
||||
|
||||
String step1Id;
|
||||
try (Response response = steps.create(step1)) {
|
||||
assertEquals(200, response.getStatus());
|
||||
step1Id = response.readEntity(WorkflowStepRepresentation.class).getId();
|
||||
}
|
||||
|
||||
// Verify both steps exist
|
||||
List<WorkflowStepRepresentation> allSteps = steps.list();
|
||||
assertEquals(2, allSteps.size());
|
||||
|
||||
// Remove the step we added
|
||||
try (Response response = steps.delete(step1Id)) {
|
||||
assertEquals(204, response.getStatus());
|
||||
}
|
||||
|
||||
// Verify only the original setup step remains
|
||||
allSteps = steps.list();
|
||||
assertEquals(1, allSteps.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddStepAtSpecificPosition() {
|
||||
WorkflowResource workflow = workflowsResource.workflow(workflowId);
|
||||
WorkflowStepsResource steps = workflow.steps();
|
||||
|
||||
// Add first step at position 0
|
||||
WorkflowStepRepresentation step1 = new WorkflowStepRepresentation();
|
||||
step1.setProviderId(NotifyUserStepProviderFactory.ID);
|
||||
step1.setConfig("name", "Step 1");
|
||||
step1.setConfig("after", String.valueOf(Duration.ofDays(30).toMillis()));
|
||||
|
||||
String step1Id;
|
||||
try (Response response = steps.create(step1, 0)) {
|
||||
assertEquals(200, response.getStatus());
|
||||
step1Id = response.readEntity(WorkflowStepRepresentation.class).getId();
|
||||
}
|
||||
|
||||
// Verify step1 is at position 0
|
||||
List<WorkflowStepRepresentation> allSteps = steps.list();
|
||||
assertEquals(step1Id, allSteps.get(0).getId());
|
||||
|
||||
// Add second step at position 1
|
||||
WorkflowStepRepresentation step2 = new WorkflowStepRepresentation();
|
||||
step2.setProviderId(DisableUserStepProviderFactory.ID);
|
||||
step2.setConfig("name", "Step 2");
|
||||
step2.setConfig("after", String.valueOf(Duration.ofDays(60).toMillis()));
|
||||
|
||||
String step2Id;
|
||||
try (Response response = steps.create(step2, 1)) {
|
||||
assertEquals(200, response.getStatus());
|
||||
step2Id = response.readEntity(WorkflowStepRepresentation.class).getId();
|
||||
}
|
||||
|
||||
// Verify step2 is at position 1
|
||||
allSteps = steps.list();
|
||||
assertEquals(step2Id, allSteps.get(1).getId());
|
||||
|
||||
// Add third step at position 1 (middle)
|
||||
WorkflowStepRepresentation step3 = new WorkflowStepRepresentation();
|
||||
step3.setProviderId(NotifyUserStepProviderFactory.ID);
|
||||
step3.setConfig("name", "Step 3");
|
||||
step3.setConfig("after", String.valueOf(Duration.ofDays(45).toMillis())); // Between 30 and 60 days
|
||||
|
||||
String step3Id;
|
||||
try (Response response = steps.create(step3, 1)) {
|
||||
assertEquals(200, response.getStatus());
|
||||
step3Id = response.readEntity(WorkflowStepRepresentation.class).getId();
|
||||
}
|
||||
|
||||
// Verify step3 is at position 1 (inserted between step1 and step2)
|
||||
allSteps = steps.list();
|
||||
assertEquals(step1Id, allSteps.get(0).getId());
|
||||
assertEquals(step3Id, allSteps.get(1).getId());
|
||||
assertEquals(step2Id, allSteps.get(2).getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSpecificStep() {
|
||||
WorkflowResource workflow = workflowsResource.workflow(workflowId);
|
||||
WorkflowStepsResource steps = workflow.steps();
|
||||
|
||||
WorkflowStepRepresentation stepRep = new WorkflowStepRepresentation();
|
||||
stepRep.setProviderId(NotifyUserStepProviderFactory.ID);
|
||||
stepRep.setConfig("name", "Test Step");
|
||||
stepRep.setConfig("after", String.valueOf(Duration.ofDays(15).toMillis()));
|
||||
|
||||
String stepId;
|
||||
try (Response response = steps.create(stepRep)) {
|
||||
assertEquals(200, response.getStatus());
|
||||
stepId = response.readEntity(WorkflowStepRepresentation.class).getId();
|
||||
}
|
||||
|
||||
// Get the specific step
|
||||
WorkflowStepRepresentation retrievedStep = steps.get(stepId);
|
||||
assertNotNull(retrievedStep);
|
||||
assertEquals(stepId, retrievedStep.getId());
|
||||
assertEquals(NotifyUserStepProviderFactory.ID, retrievedStep.getProviderId());
|
||||
assertEquals("Test Step", retrievedStep.getConfig().getFirst("name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testScheduledStepTableUpdatesAfterStepManagement() {
|
||||
runOnServer.run(session -> {
|
||||
configureSessionContext(session);
|
||||
WorkflowsManager manager = new WorkflowsManager(session);
|
||||
|
||||
Workflow workflow = manager.addWorkflow(UserCreationTimeWorkflowProviderFactory.ID, Map.of());
|
||||
|
||||
WorkflowStep step1 = new WorkflowStep(NotifyUserStepProviderFactory.ID, null, List.of());
|
||||
step1.setAfter(Duration.ofDays(30).toMillis());
|
||||
WorkflowStep step2 = new WorkflowStep(DisableUserStepProviderFactory.ID, null, List.of());
|
||||
step2.setAfter(Duration.ofDays(60).toMillis());
|
||||
|
||||
WorkflowStep addedStep1 = manager.addStepToWorkflow(workflow.getId(), step1, null);
|
||||
WorkflowStep addedStep2 = manager.addStepToWorkflow(workflow.getId(), step2, null);
|
||||
|
||||
// Simulate scheduled steps by binding workflow to a test resource
|
||||
String testResourceId = "test-user-123";
|
||||
manager.bind(workflow, ResourceType.USERS, testResourceId);
|
||||
|
||||
// Get scheduled steps for the workflow
|
||||
WorkflowStateProvider stateProvider = session.getKeycloakSessionFactory().getProviderFactory(WorkflowStateProvider.class).create(session);
|
||||
|
||||
var scheduledStepsBeforeRemoval = stateProvider.getScheduledStepsByWorkflow(workflow.getId());
|
||||
assertNotNull(scheduledStepsBeforeRemoval);
|
||||
|
||||
// Remove the first step
|
||||
manager.removeStepFromWorkflow(workflow.getId(), addedStep1.getId());
|
||||
|
||||
// Verify scheduled steps are updated
|
||||
var scheduledStepsAfterRemoval = stateProvider.getScheduledStepsByWorkflow(workflow.getId());
|
||||
assertNotNull(scheduledStepsAfterRemoval);
|
||||
|
||||
// Verify remaining steps are still properly ordered
|
||||
List<WorkflowStep> remainingSteps = manager.getSteps(workflow.getId());
|
||||
assertEquals(1, remainingSteps.size());
|
||||
assertEquals(addedStep2.getId(), remainingSteps.get(0).getId());
|
||||
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());
|
||||
step3.setAfter(Duration.ofDays(15).toMillis());
|
||||
manager.addStepToWorkflow(workflow.getId(), step3, 0); // Insert at beginning
|
||||
|
||||
// Verify final state
|
||||
List<WorkflowStep> finalSteps = manager.getSteps(workflow.getId());
|
||||
assertEquals(2, finalSteps.size());
|
||||
assertEquals(step3.getProviderId(), finalSteps.get(0).getProviderId());
|
||||
assertEquals(1, finalSteps.get(0).getPriority());
|
||||
assertEquals(addedStep2.getId(), finalSteps.get(1).getId());
|
||||
assertEquals(2, finalSteps.get(1).getPriority());
|
||||
});
|
||||
}
|
||||
|
||||
private static void configureSessionContext(KeycloakSession session) {
|
||||
RealmModel realm = session.realms().getRealmByName("default");
|
||||
session.getContext().setRealm(realm);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user