From d0590bc9b9ff9f41a427d180fe5113e76cec36bf Mon Sep 17 00:00:00 2001 From: Stefan Guilhen Date: Thu, 21 May 2026 18:02:30 -0300 Subject: [PATCH] Fix location of SCIM resources so IDs don't appear twice in the URL Closes #49176 Signed-off-by: Stefan Guilhen --- .../services/ScimResourceTypeResource.java | 5 ++- .../keycloak/tests/scim/tck/GroupTest.java | 36 +++++++++++++++++++ .../org/keycloak/tests/scim/tck/UserTest.java | 35 ++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/scim/services/src/main/java/org/keycloak/scim/services/ScimResourceTypeResource.java b/scim/services/src/main/java/org/keycloak/scim/services/ScimResourceTypeResource.java index 8a90afb9887..aa6c74c7789 100644 --- a/scim/services/src/main/java/org/keycloak/scim/services/ScimResourceTypeResource.java +++ b/scim/services/src/main/java/org/keycloak/scim/services/ScimResourceTypeResource.java @@ -255,7 +255,10 @@ public class ScimResourceTypeResource { } UriBuilder location = session.getContext().getUri().getAbsolutePathBuilder(); if (resource.getId() != null) { - location.path(resource.getId()); + String path = session.getContext().getUri().getAbsolutePath().getPath(); + if (!path.endsWith("/" + resource.getId())) { + location.path(resource.getId()); + } } meta.setLocation(location.build().toString()); resource.setMeta(meta); diff --git a/scim/tests/base/src/test/java/org/keycloak/tests/scim/tck/GroupTest.java b/scim/tests/base/src/test/java/org/keycloak/tests/scim/tck/GroupTest.java index 419eca8a92f..fda8f17e7e2 100644 --- a/scim/tests/base/src/test/java/org/keycloak/tests/scim/tck/GroupTest.java +++ b/scim/tests/base/src/test/java/org/keycloak/tests/scim/tck/GroupTest.java @@ -129,6 +129,42 @@ public class GroupTest extends AbstractScimTest { assertEquals(expected.getExternalId(), actual.getExternalId()); } + @Test + public void testMetaLocationUrl() { + Group group = new Group(); + group.setDisplayName(KeycloakModelUtils.generateId()); + group = client.groups().create(group); + String id = group.getId(); + + // location from create response should end with /Groups/{id} + assertNotNull(group.getMeta()); + assertTrue(group.getMeta().getLocation().endsWith("/Groups/" + id), + "Create location should end with /Groups/" + id + " but was: " + group.getMeta().getLocation()); + assertFalse(group.getMeta().getLocation().contains(id + "/" + id), + "Create location should not contain duplicated ID: " + group.getMeta().getLocation()); + + // location from single-resource GET should also be correct + Group fetched = client.groups().get(id); + assertNotNull(fetched.getMeta()); + assertTrue(fetched.getMeta().getLocation().endsWith("/Groups/" + id), + "GET location should end with /Groups/" + id + " but was: " + fetched.getMeta().getLocation()); + assertFalse(fetched.getMeta().getLocation().contains(id + "/" + id), + "GET location should not contain duplicated ID: " + fetched.getMeta().getLocation()); + + // location from list response should match + String filter = ResourceFilter.filter().eq("displayName", group.getDisplayName()).build(); + ListResponse response = client.groups().getAll(filter); + assertFalse(response.getResources().isEmpty()); + Group listed = response.getResources().get(0); + assertNotNull(listed.getMeta()); + assertTrue(listed.getMeta().getLocation().endsWith("/Groups/" + id), + "List location should end with /Groups/" + id + " but was: " + listed.getMeta().getLocation()); + + // all three locations should be equal + assertEquals(group.getMeta().getLocation(), fetched.getMeta().getLocation()); + assertEquals(group.getMeta().getLocation(), listed.getMeta().getLocation()); + } + @Test public void testMetaTimestamps() { Group group = new Group(); diff --git a/scim/tests/base/src/test/java/org/keycloak/tests/scim/tck/UserTest.java b/scim/tests/base/src/test/java/org/keycloak/tests/scim/tck/UserTest.java index acb861a9c91..6987911459a 100644 --- a/scim/tests/base/src/test/java/org/keycloak/tests/scim/tck/UserTest.java +++ b/scim/tests/base/src/test/java/org/keycloak/tests/scim/tck/UserTest.java @@ -60,6 +60,7 @@ import static org.keycloak.scim.resource.Scim.getCoreSchema; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -1252,6 +1253,40 @@ public class UserTest extends AbstractScimTest { return subGroup; } + @Test + public void testMetaLocationUrl() { + User user = client.users().create(createUser()); + String id = user.getId(); + + // location from create response should end with /Users/{id} + assertNotNull(user.getMeta()); + assertTrue(user.getMeta().getLocation().endsWith("/Users/" + id), + "Create location should end with /Users/" + id + " but was: " + user.getMeta().getLocation()); + assertFalse(user.getMeta().getLocation().contains(id + "/" + id), + "Create location should not contain duplicated ID: " + user.getMeta().getLocation()); + + // location from single-resource GET should also be correct + User fetched = client.users().get(id); + assertNotNull(fetched.getMeta()); + assertTrue(fetched.getMeta().getLocation().endsWith("/Users/" + id), + "GET location should end with /Users/" + id + " but was: " + fetched.getMeta().getLocation()); + assertFalse(fetched.getMeta().getLocation().contains(id + "/" + id), + "GET location should not contain duplicated ID: " + fetched.getMeta().getLocation()); + + // location from list response should match + String filter = ResourceFilter.filter().eq("userName", user.getUserName()).build(); + ListResponse response = client.users().getAll(filter); + assertFalse(response.getResources().isEmpty()); + User listed = response.getResources().get(0); + assertNotNull(listed.getMeta()); + assertTrue(listed.getMeta().getLocation().endsWith("/Users/" + id), + "List location should end with /Users/" + id + " but was: " + listed.getMeta().getLocation()); + + // all three locations should be equal + assertEquals(user.getMeta().getLocation(), fetched.getMeta().getLocation()); + assertEquals(user.getMeta().getLocation(), listed.getMeta().getLocation()); + } + @Test public void testMetaTimestamps() { User user = client.users().create(createUser());