Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.io.InputStream;
import java.time.Instant;
import java.util.List;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.stream.Stream;

Expand Down Expand Up @@ -223,7 +224,8 @@ public Response patch(@PathParam("id") String id, PatchRequest request) {
return resourceNotFound(id);
}

if (!request.getSchemas().contains(Scim.PATCH_OP_CORE_SCHEMA)) {
Set<String> schemas = request.getSchemas();
if (schemas == null || !schemas.contains(Scim.PATCH_OP_CORE_SCHEMA)) {
return invalidSyntax("No PATCH op schema provided in request");
}
Comment on lines +227 to 230

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.userprofile.config.UPAttribute;
import org.keycloak.representations.userprofile.config.UPAttributePermissions;
import org.keycloak.OAuth2Constants;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.userprofile.config.UPAttributeRequired;
import org.keycloak.representations.userprofile.config.UPConfig;
import org.keycloak.scim.client.ResourceFilter;
Expand All @@ -36,6 +38,7 @@
import org.keycloak.scim.resource.user.GroupMembership;
import org.keycloak.scim.resource.user.Name;
import org.keycloak.scim.resource.user.User;
import org.keycloak.testframework.annotations.InjectHttpClient;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.events.AdminEventAssertion;
import org.keycloak.testframework.realm.ClientBuilder;
Expand All @@ -45,6 +48,11 @@
import org.keycloak.testframework.util.ApiUtil;
import org.keycloak.userprofile.config.UPConfigUtils;

import org.apache.http.HttpHeaders;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPatch;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
Expand Down Expand Up @@ -76,6 +84,9 @@ public class UserTest extends AbstractScimTest {
@InjectScimClient(clientId = "noaccess-scim-client", clientSecret = "secret", attachTo = "noaccess-scim-client")
ScimClient noAccessClient;

@InjectHttpClient
HttpClient httpClient;

@BeforeEach
public void onBefore() {
UPConfig upConfig = realm.admin().users().userProfile().getConfiguration();
Expand Down Expand Up @@ -605,6 +616,38 @@ public void testPatchAdd() {
assertEquals("Amanda", actual.getFirstName());
}

@Test
public void testPatchWithNullSchemas() throws Exception {
User expected = client.users().create(createUser());

// PatchRequest carries @JsonInclude(NON_NULL), so setSchemas(null) is silently dropped
// during serialization and the server would retain its default PATCH schema. To actually
// exercise the null-schemas path we send a raw PATCH body with an explicit "schemas": null.
String tokenUrl = keycloakUrls.getToken(realm.getName());
String tokenBody = OAuth2Constants.GRANT_TYPE + "=" + OAuth2Constants.CLIENT_CREDENTIALS
+ "&" + OAuth2Constants.CLIENT_ID + "=scim-client"
+ "&" + OAuth2Constants.CLIENT_SECRET + "=secret";
Comment on lines +627 to +629
org.apache.http.client.methods.HttpPost tokenRequest = new org.apache.http.client.methods.HttpPost(tokenUrl);
tokenRequest.setHeader(HttpHeaders.CONTENT_TYPE, org.apache.http.entity.ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
tokenRequest.setEntity(new StringEntity(tokenBody, org.apache.http.entity.ContentType.APPLICATION_FORM_URLENCODED));

String accessToken;
org.apache.http.HttpResponse tokenResponse = httpClient.execute(tokenRequest);
AccessTokenResponse atr = org.keycloak.util.JsonSerialization.readValue(tokenResponse.getEntity().getContent(), AccessTokenResponse.class);
accessToken = atr.getToken();
Comment on lines +630 to +637

String url = keycloakUrls.getBase() + "/realms/" + realm.getName() + "/scim/v2/Users/" + expected.getId();
HttpPatch patch = new HttpPatch(url);
patch.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
patch.setHeader(HttpHeaders.CONTENT_TYPE, "application/scim+json");
patch.setEntity(new StringEntity(
"{\"schemas\": null, \"Operations\": [{\"op\": \"replace\", \"path\": \"active\", \"value\": false}]}",
ContentType.create("application/scim+json")));

org.apache.http.HttpResponse response = httpClient.execute(patch);
assertEquals(400, response.getStatusLine().getStatusCode());
Comment on lines +647 to +648
}

@Test
public void testPatchReplace() {
User expected = client.users().create(createUser());
Expand Down