[admin-api-v2] Create client does not return 201 status code (#44541)

Closes #44540

Signed-off-by: Martin Bartoš <mabartos@redhat.com>
This commit is contained in:
Martin Bartoš
2025-12-02 10:39:03 +01:00
committed by GitHub
parent cab11cf811
commit 265c27e08d
5 changed files with 48 additions and 22 deletions

View File

@@ -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)

View File

@@ -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<ClientRepresentation> 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);

View File

@@ -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);
}

View File

@@ -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));
}

View File

@@ -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");