From 7acf2ceccb7be9d62b6bd99f2156af99103fa46b Mon Sep 17 00:00:00 2001 From: Stefan Guilhen Date: Mon, 10 Nov 2025 10:54:31 -0300 Subject: [PATCH] Add pagination and search by name capabilities to WorkflowsResource Closes #44164 Signed-off-by: Stefan Guilhen --- .../client/resource/WorkflowsResource.java | 10 +++++ .../models/workflow/WorkflowProvider.java | 13 ++++++ .../admin/resource/WorkflowsResource.java | 14 ++++++- .../workflow/WorkflowManagementTest.java | 41 +++++++++++++++++++ 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/WorkflowsResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/WorkflowsResource.java index 5eaba3a1320..bae9e6867a4 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/WorkflowsResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/WorkflowsResource.java @@ -9,6 +9,7 @@ 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.WorkflowRepresentation; @@ -35,6 +36,15 @@ public interface WorkflowsResource { @Produces(MediaType.APPLICATION_JSON) List list(); + @GET + @Produces(MediaType.APPLICATION_JSON) + List list( + @QueryParam("search") String search, + @QueryParam("exact") Boolean exact, + @QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults + ); + @Path("{id}") WorkflowResource workflow(@PathParam("id") String id); } diff --git a/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowProvider.java b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowProvider.java index 0001fd1f1e2..39d475ebfc9 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/models/workflow/WorkflowProvider.java @@ -17,10 +17,12 @@ package org.keycloak.models.workflow; +import java.util.Comparator; import java.util.stream.Stream; import org.keycloak.provider.Provider; import org.keycloak.representations.workflows.WorkflowRepresentation; +import org.keycloak.utils.StringUtil; public interface WorkflowProvider extends Provider { @@ -40,6 +42,17 @@ public interface WorkflowProvider extends Provider { Stream getWorkflows(); + default Stream getWorkflows(String search, Boolean exact, Integer first, Integer max) { + return getWorkflows().sorted(Comparator.comparing(Workflow::getName)) + .filter(workflow -> { + if (StringUtil.isBlank(search)) { + return true; + } + return Boolean.TRUE.equals(exact) ? workflow.getName().equals(search) : workflow.getName().toLowerCase().contains(search.toLowerCase()); + }) + .skip(first).limit(max); + } + WorkflowRepresentation toRepresentation(Workflow workflow); void updateWorkflow(Workflow workflow, WorkflowRepresentation rep); diff --git a/services/src/main/java/org/keycloak/workflow/admin/resource/WorkflowsResource.java b/services/src/main/java/org/keycloak/workflow/admin/resource/WorkflowsResource.java index d49b2e3aead..63467b832e7 100644 --- a/services/src/main/java/org/keycloak/workflow/admin/resource/WorkflowsResource.java +++ b/services/src/main/java/org/keycloak/workflow/admin/resource/WorkflowsResource.java @@ -5,14 +5,17 @@ import java.util.Optional; import com.fasterxml.jackson.jakarta.rs.yaml.YAMLMediaTypes; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.NotFoundException; 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.parameters.Parameter; import org.keycloak.common.Profile; import org.keycloak.common.Profile.Feature; import org.keycloak.models.KeycloakSession; @@ -79,9 +82,16 @@ public class WorkflowsResource { @GET @Produces({MediaType.APPLICATION_JSON, YAMLMediaTypes.APPLICATION_JACKSON_YAML}) - public List list() { + public List list( + @Parameter(description = "A String representing the workflow name - either partial or exact") @QueryParam("search") String search, + @Parameter(description = "Boolean which defines whether the param 'search' must match exactly or not") @QueryParam("exact") Boolean exact, + @Parameter(description = "The position of the first result to be processed (pagination offset)") @QueryParam("first") @DefaultValue("0") Integer firstResult, + @Parameter(description = "The maximum number of results to be returned - defaults to 10") @QueryParam("max") @DefaultValue("10") Integer maxResults + ) { auth.realm().requireManageRealm(); - return provider.getWorkflows().map(provider::toRepresentation).toList(); + int first = Optional.ofNullable(firstResult).orElse(0); + int max = Optional.ofNullable(maxResults).orElse(10); + return provider.getWorkflows(search, exact, first, max).map(provider::toRepresentation).toList(); } } diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowManagementTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowManagementTest.java index 88e4e2a4439..894f26e29bc 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowManagementTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/model/workflow/WorkflowManagementTest.java @@ -264,6 +264,47 @@ public class WorkflowManagementTest extends AbstractWorkflowTest { } + @Test + public void testSearch() { + // create a few workflows with different names + String[] workflowNames = {"alpha-workflow", "beta-workflow", "gamma-workflow", "delta-workflow"}; + for (String name : workflowNames) { + managedRealm.admin().workflows().create(WorkflowRepresentation.withName(name) + .onEvent(ResourceOperationType.USER_ADDED.toString()) + .withSteps( + WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID) + .after(Duration.ofDays(5)) + .build() + ).build()).close(); + } + + // use the API to search for workflows by name, both partial and exact matches + WorkflowsResource workflows = managedRealm.admin().workflows(); + List representations = workflows.list("alpha", false, null, null); + assertThat(representations, Matchers.hasSize(1)); + assertThat(representations.get(0).getName(), is("alpha-workflow")); + + representations = workflows.list("workflow", false, null, null); + assertThat(representations, Matchers.hasSize(4)); + representations = workflows.list("beta-workflow", true, null, null); + assertThat(representations, Matchers.hasSize(1)); + assertThat(representations.get(0).getName(), is("beta-workflow")); + representations = workflows.list("nonexistent", false, null, null); + assertThat(representations, Matchers.hasSize(0)); + + // test pagination parameters + representations = workflows.list(null, null, 1, 2); + assertThat(representations, Matchers.hasSize(2)); + // returned workflows should be ordered by name + assertThat(representations.get(0).getName(), is("beta-workflow")); + assertThat(representations.get(1).getName(), is("delta-workflow")); + + representations = workflows.list("gamma", false, 0, 10); + assertThat(representations, Matchers.hasSize(1)); + assertThat(representations.get(0).getName(), is("gamma-workflow")); + } + + @Test public void testWorkflowDoesNotFallThroughStepsInSingleRun() { managedRealm.admin().workflows().create(WorkflowRepresentation.withName("myworkflow")