diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java index 04583b4c4ec..03b7c996663 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java @@ -218,12 +218,12 @@ public class Keycloak implements AutoCloseable { } /** - * Create a secure proxy with endpoints targetting the server + * Create a secure proxy with endpoints targeting the server */ public T proxy(Class proxyClass) { return CLIENT_PROVIDER.targetProxy(target, proxyClass); } - + /** * Closes the underlying client. After calling this method, this Keycloak instance cannot be reused. */ diff --git a/js/libs/keycloak-admin-client/openapi.json b/js/libs/keycloak-admin-client/openapi.json index 662caeeb64b..5dc3f752be9 100644 --- a/js/libs/keycloak-admin-client/openapi.json +++ b/js/libs/keycloak-admin-client/openapi.json @@ -236,15 +236,16 @@ "tags" : [ "Clients (v2)" ], "parameters" : [ { "description" : "Set of fields to include in the response. Must be top-level fields. If omitted or empty, all fields will be populated.", - "name" : "fields", - "in" : "query", + "explode" : false, "schema" : { "type" : "array", "uniqueItems" : true, "items" : { "type" : "string" } - } + }, + "name" : "fields", + "in" : "query" }, { "description" : "Filter expression using SCIM-like syntax, e.g. clientId eq \"my-app\" and enabled eq true", "name" : "q", diff --git a/rest/admin-v2/api/pom.xml b/rest/admin-v2/api/pom.xml index b993d225137..9c1a2a75242 100644 --- a/rest/admin-v2/api/pom.xml +++ b/rest/admin-v2/api/pom.xml @@ -45,6 +45,11 @@ hibernate-validator provided + + org.junit.jupiter + junit-jupiter + test + 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 index 8ce23f1897b..896ea919765 100644 --- 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 @@ -1,16 +1,23 @@ package org.keycloak.admin.api; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import jakarta.ws.rs.QueryParam; +import org.eclipse.microprofile.openapi.annotations.enums.Explode; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; 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.") + @Parameter(description = "Set of fields to include in the response. Must be top-level fields. If omitted or empty, all fields will be populated.", + explode = Explode.FALSE, schema = @Schema(type = SchemaType.ARRAY, uniqueItems = true, implementation = String.class)) @QueryParam("fields") - protected Set fields; + protected String fields; @Parameter(description = "Filter expression using SCIM-like syntax, e.g. clientId eq \"my-app\" and enabled eq true") @QueryParam("q") @@ -27,11 +34,17 @@ public class ListOptions { } public Set getFields() { - return fields; + if (fields == null) { + return null; + } + if (fields.isEmpty()) { + return Set.of(); + } + return new LinkedHashSet<>(List.of(fields.split(","))); } public void setFields(Set fields) { - this.fields = fields; + this.fields = fields == null ? null : fields.stream().collect(Collectors.joining(",")); } public String getQuery() { @@ -41,5 +54,5 @@ public class ListOptions { public void setQuery(String query) { this.query = query; } - + } diff --git a/rest/admin-v2/api/src/test/java/org/keycloak/admin/api/ListOptionsTest.java b/rest/admin-v2/api/src/test/java/org/keycloak/admin/api/ListOptionsTest.java new file mode 100644 index 00000000000..85fa38c03cd --- /dev/null +++ b/rest/admin-v2/api/src/test/java/org/keycloak/admin/api/ListOptionsTest.java @@ -0,0 +1,26 @@ +package org.keycloak.admin.api; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ListOptionsTest { + + @Test + void testGetFieldsEmpty() { + ListOptions options = new ListOptions(); + options.setFields(Set.of()); + assertEquals("", options.fields); + assertEquals(Set.of(), options.getFields()); + } + + @Test + void testGetFields() { + ListOptions options = new ListOptions(); + options.fields = "a,b"; + assertEquals(Set.of("a", "b"), options.getFields()); + } + +} 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 d49456c2040..b9ce1b059b2 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 @@ -20,6 +20,7 @@ package org.keycloak.tests.admin.client.v2; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Set; @@ -365,6 +366,10 @@ public class ClientApiV2Test extends AbstractClientApiV2Test{ public void invalidFieldProjection() { 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)); + + // ensure that multiple fields are interpreted correctly + e = assertThrows(BadRequestException.class, () -> getClientsApi().getClients(new ListOptions().fields(new LinkedHashSet<>(List.of("clientId","unknown!"))))); + assertEquals("{\"error\":\"unknown! is an unknown field\"}", e.getResponse().readEntity(String.class)); } @Test