8000 fix: using inputstream to represent patches by shawkins · Pull Request #47953 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion js/libs/keycloak-admin-client/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,9 @@ paths:
- Clients (v2)
requestBody:
content:
application/merge-patch+json: {}
application/merge-patch+json:
schema:
type: object
required: true
responses:
"200":
Expand Down
7 changes: 1 addition & 6 deletions js/libs/keycloak-admin-client/test/clientsV2.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,10 @@ describe("Clients V2 API", () => {
it("should patch a client", async () => {
const patchedDisplayName = "Patched Display Name";

// Note: Kiota's patch expects an ArrayBuffer for merge-patch+json
const patchBody = JSON.stringify({ displayName: patchedDisplayName });
const encoder = new TextEncoder();
const patchBuffer = encoder.encode(patchBody).buffer;

const patchedClient = await kcAdminClient.clients
.v2()
.byId(currentClientId)
.patch(patchBuffer);
.patch({ additionalData: { displayName: patchedDisplayName } });

expect((patchedClient as OIDCClientRepresentation).displayName).to.equal(
patchedDisplayName,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.keycloak.services.client;

import java.io.InputStream;
import java.util.Optional;
import java.util.stream.Stream;

Expand All @@ -9,8 +10,6 @@
import org.keycloak.services.Service;
import org.keycloak.services.ServiceException;

import com.fasterxml.jackson.databind.JsonNode;

public interface ClientService extends Service {

class ClientSearchOptions {
Expand Down Expand Up @@ -50,5 +49,5 @@ default Stream<BaseClientRepresentation> getClients(RealmModel realm) {

BaseClientRepresentation createClient(RealmModel realm, BaseClientRepresentation client) throws ServiceException;

BaseClientRepresentation patchClient(RealmModel realm, String clientId, PatchType patchType, JsonNode patch) throws ServiceException;
BaseClientRepresentation patchClient(RealmModel realm, String clientId, PatchType patchType, InputStream patch) throws ServiceException;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.keycloak.services.client;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -49,9 +50,10 @@
import org.keycloak.validation.jakarta.JakartaValidatorProvider;
import org.keycloak.validation.jakarta.ValidationContext;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import org.apache.http.HttpEntity;
Expand Down Expand Up @@ -224,20 +226,23 @@ protected void fireAdminEvent(OperationType operationType, BaseClientRepresentat
}

@Override
public BaseClientRepresentation patchClient(RealmModel realm, String clientId, PatchType patchType, JsonNode patch) throws ServiceException {
public BaseClientRepresentation patchClient(RealmModel realm, String clientId, PatchType patchType, InputStream patch) throws ServiceException {
Supplier<BaseClientRepresentation> getOriginalClient = () -> getClient(realm, clientId)
.orElseThrow(() -> new ServiceException("Cannot find the specified client", Response.Status.NOT_FOUND));

BaseClientRepresentation updated;
switch (patchType) {
case JSON_MERGE -> {
try {
if (patch == null) {
// based on the RFC 7396 JSON Merge Patch should replace the whole entity if the patch is not an object - we can't do it
throw new ServiceException("Cannot replace client resource with null", Response.Status.BAD_REQUEST);
}
try (JsonParser parser = MAPPER.getFactory().createParser(patch)) {
final ObjectReader objectReader = MAPPER.readerForUpdating(getOriginalClient.get());
updated = objectReader.readValue(patch);
JsonToken nextToken = parser.nextToken();
if (nextToken != JsonToken.START_OBJECT) {
throw new ServiceException("Cannot replace client resource with non-object", Response.Status.BAD_REQUEST);
}
updated = objectReader.readValue(parser);
if (parser.nextToken() != null) {
throw new ServiceException("Patch contains additional content", Response.Status.BAD_REQUEST);
}
} catch (JsonMappingException e) {
var invalidFields = e.getPath().stream().map(JsonMappingException.Reference::getFieldName).collect(Collectors.joining(", "));
throw new ServiceException("Invalid values for these fields: %s".formatted((invalidFields)));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.keycloak.admin.api.client;

import java.io.InputStream;

import jakarta.validation.Valid;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
Expand All @@ -13,10 +15,11 @@
import org.keycloak.representations.admin.v2.BaseClientRepresentation;
import org.keycloak.services.PatchTypeNames;

import com.fasterxml.jackson.databind.JsonNode;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;

Expand Down Expand Up @@ -49,7 +52,8 @@ public interface ClientApi {
@APIResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = BaseClientRepresentation.class))),
@APIResponse(responseCode = "404", description = "Not Found")
})
BaseClientRepresentation patchClient(JsonNode patch);
@RequestBody(required = true, content = @Content(schema = @Schema(type = SchemaType.OBJECT)))
BaseClientRepresentation patchClient(InputStream patch);

@DELETE
@Operation(summary = "Delete a client", description = "Deletes a client from the realm")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.keycloak.admin.api.client;

import java.io.InputStream;

import jakarta.annotation.Nonnull;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
Expand All @@ -20,7 +22,6 @@
import org.keycloak.services.resources.admin.RealmAdminResource;
import org.keycloak.services.resources.admin.fgap.AdminPermissionEvaluator;

import com.fasterxml.jackson.databind.JsonNode;

public class DefaultClientApi implements ClientApi {
private final KeycloakSession session;
Expand Down Expand Up @@ -60,7 +61,7 @@ public Response createOrUpdateClient(BaseClientRepresentation client) {

@PATCH
@Override
public BaseClientRepresentation patchClient(JsonNode patch) {
public BaseClientRepresentation patchClient(InputStream patch) {
String contentType = session.getContext().getHttpRequest().getHttpHeaders().getHeaderString(HttpHeaders.CONTENT_TYPE);
PatchType patchType = PatchType.getByMediaType(contentType)
.orElseThrow(() -> new WebApplicationException("Unsupported media type", Response.Status.UNSUPPORTED_MEDIA_TYPE));
Expand Down
F63D
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.keycloak.tests.admin.client.v2;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.List;
import java.util.Set;
Expand Down Expand Up @@ -119,10 +120,10 @@ public void jsonPatchClient() throws Exception {
}

@Test
public void jsonMergePatchClient() {
public void jsonMergePatchClient() throws JsonProcessingException {
OIDCClientRepresentation patch = new OIDCClientRepresentation();
patch.setDescription("I'm also a description");
BaseClientRepresentation baseRep = clients(masterRealm.getName()).v2().client(testClient.getClientId()).patchClient(mapper.valueToTree(patch));
BaseClientRepresentation baseRep = clients(masterRealm.getName()).v2().client(testClient.getClientId()).patchClient(new ByteArrayInputStream(mapper.writeValueAsBytes(patch)));
assertEquals("I'm also a description", baseRep.getDescription());
}

Expand All @@ -132,6 +133,12 @@ public void jsonMergePatchClientInvalid() throws Exception {
setAuthHeader(request);
request.setHeader(HttpHeaders.CONTENT_TYPE, PatchTypeNames.JSON_MERGE);

request.setEntity(null);
try (var response = client.execute(request)) {
assertEquals(400, response.getStatusLine().getStatusCode());
assertThat(EntityUtils.toString(response.getEntity()), Matchers.containsString("Cannot replace client resource with non-object"));
}

request.setEntity(new StringEntity("patch client invalid"));
try (var response = client.execute(request)) {
assertThat(response.getStatusLine().getStatusCode(),is(400));
Expand All @@ -148,11 +155,24 @@ public void jsonMergePatchClientInvalid() throws Exception {
assertEquals(200, response.getStatusLine().getStatusCode());
}

request.setEntity(new StringEntity("\"a\""));
try (var response = client.execute(request)) {
assertThat(response.getStatusLine().getStatusCode(),is(400));
assertThat(EntityUtils.toString(response.getEntity()), Matchers.containsString("Cannot replace client resource with non-object"));
}

request.setEntity(new StringEntity(""));
try (var response = client.execute(request)) {
assertThat(response.getStatusLine().getStatusCode(),is(400));
assertThat(EntityUtils.toString(response.getEntity()), Matchers.containsString("Cannot replace client resource with null"));
assertThat(EntityUtils.toString(response.getEntity()), Matchers.containsString("Cannot replace client resource with non-object"));
}

request.setEntity(new StringEntity("{} {}"));
try (var response = client.execute(request)) {
assertThat(response.getStatusLine().getStatusCode(),is(400));
assertThat(EntityUtils.toString(response.getEntity()), Matchers.containsString("Patch contains additional content"));
}

}

@Test
Expand Down Expand Up @@ -884,9 +904,9 @@ private OIDCClientRepresentation.Auth getResultingAuthConfigPut(OIDCClientRepres
}
}

private OIDCClientRepresentation.Auth getResultingAuthConfigPatch(OIDCClientRepresentation.Auth auth, String clientId, String... additionalFields) throws IllegalArgumentException {
private OIDCClientRepresentation.Auth getResultingAuthConfigPatch(OIDCClientRepresentation.Auth auth, String clientId, String... additionalFields) throws IllegalArgumentException, JsonProcessingException {
var rep = getResultingClientRep(auth, clientId, additionalFields);
OIDCClientRepresentation createdClient = (OIDCClientRepresentation) clients(testRealm.getName()).v2().client(clientId).patchClient(mapper.valueToTree(rep));
OIDCClientRepresentation createdClient = (OIDCClientRepresentation) clients(testRealm.getName()).v2().client(clientId).patchClient(new ByteArrayInputStream(mapper.writeValueAsBytes(rep)));
return assertClientEnabledIdDescriptionAndAuth(rep, createdClient);
}

Expand Down Expand Up @@ -1009,4 +1029,4 @@ public RealmConfigBuilder configure(RealmConfigBuilder realm) {
return realm;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public void patchClientWithNullEntityFails(String protocol) throws Exception {
assertThat(response.getStatusLine().getStatusCode(), is(400));

String responseBody = EntityUtils.toString(response.getEntity());
assertThat(responseBody, containsString("Cannot replace client resource with null"));
assertThat(responseBody, containsString("Cannot replace client resource with non-object"));
}
}

Expand Down
Loading
0