From 265c27e08d73cd766a5a3d0ed78a1e1d07355ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Barto=C5=A1?= Date: Tue, 2 Dec 2025 10:39:03 +0100 Subject: [PATCH] [admin-api-v2] Create client does not return 201 status code (#44541) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #44540 Signed-off-by: Martin Bartoš --- .../keycloak/admin/api/client/ClientApi.java | 6 +++- .../keycloak/admin/api/client/ClientsApi.java | 6 +++- .../admin/api/client/DefaultClientApi.java | 16 ++++------ .../admin/api/client/DefaultClientsApi.java | 12 ++++---- .../admin/client/v2/ClientApiV2Test.java | 30 +++++++++++++++++-- 5 files changed, 48 insertions(+), 22 deletions(-) diff --git a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/ClientApi.java b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/ClientApi.java index a863b502e0f..d1705bb5c7d 100644 --- a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/ClientApi.java +++ b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/ClientApi.java @@ -8,6 +8,7 @@ import jakarta.ws.rs.PATCH; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import org.keycloak.representations.admin.v2.ClientRepresentation; @@ -22,10 +23,13 @@ public interface ClientApi { @Produces(MediaType.APPLICATION_JSON) ClientRepresentation getClient(); + /** + * @return {@link ClientRepresentation} of created/updated client + */ @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - ClientRepresentation createOrUpdateClient(@Valid ClientRepresentation client); + Response createOrUpdateClient(@Valid ClientRepresentation client); @PATCH @Consumes(CONTENT_TYPE_MERGE_PATCH) diff --git a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/ClientsApi.java b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/ClientsApi.java index 82450a462e8..049250f7895 100644 --- a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/ClientsApi.java +++ b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/ClientsApi.java @@ -10,6 +10,7 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import org.keycloak.representations.admin.v2.ClientRepresentation; import org.keycloak.services.resources.KeycloakOpenAPI; @@ -28,11 +29,14 @@ public interface ClientsApi { @Operation(summary = "Get all clients", description = "Returns a list of all clients in the realm") Stream getClients(); + /** + * @return {@link ClientRepresentation} of created client + */ @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Create a new client", description = "Creates a new client in the realm") - ClientRepresentation createClient(@Valid ClientRepresentation client); + Response createClient(@Valid ClientRepresentation client); @Path("{id}") ClientApi client(@PathParam("id") String id); diff --git a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/DefaultClientApi.java b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/DefaultClientApi.java index ef3583bb541..1df39b06135 100644 --- a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/DefaultClientApi.java +++ b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/DefaultClientApi.java @@ -9,7 +9,6 @@ import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; -import org.keycloak.http.HttpResponse; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -33,7 +32,6 @@ public class DefaultClientApi implements ClientApi { private final RealmModel realm; private final ClientModel client; private final ClientService clientService; - private HttpResponse response; private final ClientResource clientResource; private final ClientsResource clientsResource; @@ -47,7 +45,6 @@ public class DefaultClientApi implements ClientApi { this.realm = Objects.requireNonNull(session.getContext().getRealm()); this.client = Objects.requireNonNull(session.getContext().getClient()); this.clientService = new DefaultClientService(session); - this.response = session.getContext().getHttpResponse(); this.clientsResource = clientsResource; this.clientResource = clientResource; this.clientId = clientId; @@ -61,17 +58,14 @@ public class DefaultClientApi implements ClientApi { } @Override - public ClientRepresentation createOrUpdateClient(ClientRepresentation client) { + public Response createOrUpdateClient(ClientRepresentation client) { try { if (!Objects.equals(clientId, client.getClientId())) { throw new WebApplicationException("cliendId in payload does not match the clientId in the path", Response.Status.BAD_REQUEST); } - validateUnknownFields(client, response); + validateUnknownFields(client); var result = clientService.createOrUpdate(clientsResource, clientResource, realm, client, true); - if (result.created()) { - response.setStatus(Response.Status.CREATED.getStatusCode()); - } - return result.representation(); + return Response.status(result.created() ? Response.Status.CREATED : Response.Status.OK).entity(result.representation()).build(); } catch (ServiceException e) { throw new WebApplicationException(e.getMessage(), e.getSuggestedResponseStatus().orElse(Response.Status.BAD_REQUEST)); } @@ -91,7 +85,7 @@ public class DefaultClientApi implements ClientApi { final ObjectReader objectReader = objectMapper.readerForUpdating(client); ClientRepresentation updated = objectReader.readValue(patch); - validateUnknownFields(updated, response); + validateUnknownFields(updated); return clientService.createOrUpdate(clientsResource, clientResource, realm, updated, true).representation(); } catch (IllegalArgumentException e) { throw new WebApplicationException("Unsupported media type", Response.Status.UNSUPPORTED_MEDIA_TYPE); @@ -110,7 +104,7 @@ public class DefaultClientApi implements ClientApi { clientResource.deleteClient(); } - static void validateUnknownFields(ClientRepresentation rep, HttpResponse response) { + static void validateUnknownFields(ClientRepresentation rep) { if (!rep.getAdditionalFields().isEmpty()) { throw new WebApplicationException("Payload contains unknown fields: " + rep.getAdditionalFields().keySet(), Response.Status.BAD_REQUEST); } diff --git a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/DefaultClientsApi.java b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/DefaultClientsApi.java index 50f03fa6dc1..5a8e9f8bb07 100644 --- a/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/DefaultClientsApi.java +++ b/rest/admin-v2/rest/src/main/java/org/keycloak/admin/api/client/DefaultClientsApi.java @@ -9,7 +9,6 @@ import jakarta.ws.rs.PathParam; import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Response; -import org.keycloak.http.HttpResponse; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.representations.admin.v2.ClientRepresentation; @@ -24,7 +23,6 @@ import org.keycloak.validation.jakarta.JakartaValidatorProvider; public class DefaultClientsApi implements ClientsApi { private final KeycloakSession session; private final RealmModel realm; - private final HttpResponse response; private final ClientService clientService; private final JakartaValidatorProvider validator; private final ClientsResource clientsResource; @@ -33,7 +31,6 @@ public class DefaultClientsApi implements ClientsApi { this.session = session; this.realm = Objects.requireNonNull(session.getContext().getRealm()); this.clientService = new DefaultClientService(session); - this.response = session.getContext().getHttpResponse(); this.validator = new HibernateValidatorProvider(); this.clientsResource = clientsResource; } @@ -44,12 +41,13 @@ public class DefaultClientsApi implements ClientsApi { } @Override - public ClientRepresentation createClient(@Valid ClientRepresentation client) { + public Response createClient(@Valid ClientRepresentation client) { try { - DefaultClientApi.validateUnknownFields(client, response); + DefaultClientApi.validateUnknownFields(client); validator.validate(client, CreateClientDefault.class); - response.setStatus(Response.Status.CREATED.getStatusCode()); - return clientService.createOrUpdate(clientsResource, null, realm, client, false).representation(); + return Response.status(Response.Status.CREATED) + .entity(clientService.createOrUpdate(clientsResource, null, realm, client, false).representation()) + .build(); } catch (ServiceException e) { throw new WebApplicationException(e.getMessage(), e.getSuggestedResponseStatus().orElse(Response.Status.BAD_REQUEST)); } 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 cea74e63d2f..3e1c574c4d3 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 @@ -147,7 +147,7 @@ public class ClientApiV2Test { request.setEntity(new StringEntity(mapper.writeValueAsString(rep))); try (var response = client.execute(request)) { - assertEquals(200, response.getStatusLine().getStatusCode()); + assertEquals(201, response.getStatusLine().getStatusCode()); ClientRepresentation client = mapper.createParser(response.getEntity().getContent()).readValueAs(ClientRepresentation.class); assertEquals("I'm new", client.getDescription()); } @@ -162,6 +162,32 @@ public class ClientApiV2Test { } } + @Test + public void createClient() throws Exception { + HttpPost request = new HttpPost(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients"); + setAuthHeader(request); + request.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); + + ClientRepresentation rep = new ClientRepresentation(); + rep.setEnabled(true); + rep.setClientId("client-123"); + rep.setDescription("I'm new"); + + request.setEntity(new StringEntity(mapper.writeValueAsString(rep))); + + try (var response = client.execute(request)) { + assertThat(response.getStatusLine().getStatusCode(),is(201)); + ClientRepresentation client = mapper.createParser(response.getEntity().getContent()).readValueAs(ClientRepresentation.class); + assertThat(client.getEnabled(),is(true)); + assertThat(client.getClientId(),is("client-123")); + assertThat(client.getDescription(),is("I'm new")); + } + + try (var response = client.execute(request)) { + assertThat(response.getStatusLine().getStatusCode(),is(409)); + } + } + @Test public void deleteClient() throws Exception { HttpPut createRequest = new HttpPut(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/to-delete"); @@ -175,7 +201,7 @@ public class ClientApiV2Test { createRequest.setEntity(new StringEntity(mapper.writeValueAsString(rep))); try (var response = client.execute(createRequest)) { - assertEquals(200, response.getStatusLine().getStatusCode()); + assertEquals(201, response.getStatusLine().getStatusCode()); } HttpGet getRequest = new HttpGet(HOSTNAME_LOCAL_ADMIN + "/realms/master/clients/to-delete");