From 26ef6d1b085466c4a7d3b51a45692e9f30a8fef5 Mon Sep 17 00:00:00 2001 From: Steven Hawkins Date: Fri, 22 May 2026 13:54:20 -0400 Subject: [PATCH] task: using a beanparam for client listing options (#49074) * task: using a beanparam for client listing options closes: #48650 Signed-off-by: Steve Hawkins * just adding fluent methods Signed-off-by: Steve Hawkins --------- Signed-off-by: Steve Hawkins --- js/libs/keycloak-admin-client/openapi.yaml | 3 +- .../org/keycloak/admin/api/ListOptions.java | 28 +++++++++++++++++++ .../keycloak/admin/api/client/ClientsApi.java | 9 +++--- .../admin/api/client/DefaultClientsApi.java | 6 ++-- .../admin/client/v2/ClientApiV2Test.java | 5 ++-- 5 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 rest/admin-v2/api/src/main/java/org/keycloak/admin/api/ListOptions.java diff --git a/js/libs/keycloak-admin-client/openapi.yaml b/js/libs/keycloak-admin-client/openapi.yaml index 1b599075664..39ea5bf8eb4 100644 --- a/js/libs/keycloak-admin-client/openapi.yaml +++ b/js/libs/keycloak-admin-client/openapi.yaml @@ -181,8 +181,7 @@ paths: - Clients (v2) parameters: - description: "Set of fields to include in the response. Must be top-level\ - \ fields on one of the client types. If omitted or empty, all fields will\ - \ be populated." + \ fields. If omitted or empty, all fields will be populated." name: fields in: query schema: diff --git a/rest/admin-v2/api/src/main/java/org/keycloak/admin/api/ListOptions.java b/rest/admin-v2/api/src/main/java/org/keycloak/admin/api/ListOptions.java new file mode 100644 index 00000000000..e3a83104524 --- /dev/null +++ b/rest/admin-v2/api/src/main/java/org/keycloak/admin/api/ListOptions.java @@ -0,0 +1,28 @@ +package org.keycloak.admin.api; + +import java.util.Set; + +import jakarta.ws.rs.QueryParam; + +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; + +public class ListOptions { + + @Parameter(description = "Set of fields to include in the response. Must be top-level fields. If omitted or empty, all fields will be populated.") + @QueryParam("fields") + protected Set fields; + + public ListOptions fields(Set fields) { + this.setFields(fields); + return this; + } + + public Set getFields() { + return fields; + } + + public void setFields(Set fields) { + this.fields = fields; + } + +} diff --git a/rest/admin-v2/api/src/main/java/org/keycloak/admin/api/client/ClientsApi.java b/rest/admin-v2/api/src/main/java/org/keycloak/admin/api/client/ClientsApi.java index 93f882af3ee..43d91a22f99 100644 --- a/rest/admin-v2/api/src/main/java/org/keycloak/admin/api/client/ClientsApi.java +++ b/rest/admin-v2/api/src/main/java/org/keycloak/admin/api/client/ClientsApi.java @@ -1,19 +1,19 @@ package org.keycloak.admin.api.client; -import java.util.Set; import java.util.stream.Stream; import jakarta.validation.Valid; +import jakarta.ws.rs.BeanParam; import jakarta.ws.rs.Consumes; 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.admin.api.ListOptions; import org.keycloak.common.constants.KeycloakOpenAPI; import org.keycloak.representations.admin.v2.BaseClientRepresentation; @@ -22,7 +22,6 @@ import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.extensions.Extension; 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.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import org.eclipse.microprofile.openapi.annotations.tags.Tag; @@ -38,10 +37,10 @@ public interface ClientsApi { @APIResponses(value = { @APIResponse(responseCode = "200", content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = BaseClientRepresentation.class))) }) - Stream getClients(@Parameter(description = "Set of fields to include in the response. Must be top-level fields on one of the client types. If omitted or empty, all fields will be populated.") @QueryParam("fields") Set fields); + Stream getClients(@BeanParam ListOptions params); default Stream getClients() { - return getClients(Set.of()); + return getClients(new ListOptions()); } @POST diff --git a/rest/admin-v2/services/src/main/java/org/keycloak/rest/admin/api/client/DefaultClientsApi.java b/rest/admin-v2/services/src/main/java/org/keycloak/rest/admin/api/client/DefaultClientsApi.java index af993176941..a03ff64f04f 100644 --- a/rest/admin-v2/services/src/main/java/org/keycloak/rest/admin/api/client/DefaultClientsApi.java +++ b/rest/admin-v2/services/src/main/java/org/keycloak/rest/admin/api/client/DefaultClientsApi.java @@ -1,6 +1,5 @@ package org.keycloak.rest.admin.api.client; -import java.util.Set; import java.util.stream.Stream; import jakarta.annotation.Nonnull; @@ -11,6 +10,7 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.core.Response; +import org.keycloak.admin.api.ListOptions; import org.keycloak.admin.api.client.ClientApi; import org.keycloak.admin.api.client.ClientsApi; import org.keycloak.models.KeycloakSession; @@ -38,8 +38,8 @@ public class DefaultClientsApi implements ClientsApi { } @Override - public Stream getClients(Set fields) { - return clientService.getClients(realm, new ClientProjectionOptions(fields), null, null); + public Stream getClients(ListOptions params) { + return clientService.getClients(realm, new ClientProjectionOptions(params.getFields()), null, null); } @POST diff --git a/rest/admin-v2/tests/src/test/java/org/keycloak/tests/admin/client/v2/ClientApiV2Test.java b/rest/admin-v2/tests/src/test/java/org/keycloak/tests/admin/client/v2/ClientApiV2Test.java index 7a7a7e6f7ed..fc0682c87bd 100644 --- a/rest/admin-v2/tests/src/test/java/org/keycloak/tests/admin/client/v2/ClientApiV2Test.java +++ b/rest/admin-v2/tests/src/test/java/org/keycloak/tests/admin/client/v2/ClientApiV2Test.java @@ -30,6 +30,7 @@ import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; +import org.keycloak.admin.api.ListOptions; import org.keycloak.admin.api.PatchTypeNames; import org.keycloak.admin.client.Keycloak; import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator; @@ -347,7 +348,7 @@ public class ClientApiV2Test extends AbstractClientApiV2Test{ assertThat(samlClient.getFrontChannelLogout(), is(false)); // test projecting only id and protocol - try (Stream baseClientRepresentationStream = getClientsApi().getClients(Set.of("clientId", "protocol"))) { + try (Stream baseClientRepresentationStream = getClientsApi().getClients(new ListOptions().fields(Set.of("clientId", "protocol")))) { List clients = baseClientRepresentationStream.toList(); for (BaseClientRepresentation client : clients) { BaseClientRepresentation toCompare = null; @@ -364,7 +365,7 @@ public class ClientApiV2Test extends AbstractClientApiV2Test{ @Test public void invalidFieldProjection() { - BadRequestException e = assertThrows(BadRequestException.class, () -> getClientsApi().getClients(Set.of("unknown!"))); + BadRequestException e = assertThrows(BadRequestException.class, () -> getClientsApi().getClients(new ListOptions().fields(Set.of("unknown!")))); assertEquals("{\"error\":\"unknown! is an unknown field\"}", e.getResponse().readEntity(String.class)); }