From 522e8d288762f2a892d1431ea8772e916123fcf0 Mon Sep 17 00:00:00 2001 From: rmartinc Date: Tue, 5 Dec 2023 08:47:12 +0100 Subject: [PATCH] Workaround to allow percent chars in getGroupByPath via PathSegment Closes #25111 Signed-off-by: rmartinc --- .../models/utils/KeycloakModelUtils.java | 17 +++++++++ .../resources/admin/RealmAdminResource.java | 4 +- .../testsuite/admin/group/GroupTest.java | 38 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java index 579b8d57f3..0930d612cb 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -716,6 +716,23 @@ public final class KeycloakModelUtils { return getGroupModel(session.groups(), realm, null, split, 0); } + /** + * Finds group by path. Variant when you have the path already separated by + * group names. + * + * @param session Keycloak session + * @param realm The realm + * @param path Path The path hierarchy of groups + * + * @return {@code GroupModel} corresponding to the given {@code path} or {@code null} if no group was found + */ + public static GroupModel findGroupByPath(KeycloakSession session, RealmModel realm, String[] path) { + if (path == null || path.length == 0) { + return null; + } + return getGroupModel(session.groups(), realm, null, path, 0); + } + private static GroupModel getGroupModel(GroupProvider groupProvider, RealmModel realm, GroupModel parent, String[] split, int index) { StringBuilder nameBuilder = new StringBuilder(); for (int i = index; i < split.length; i++) { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 9ad57db70f..a5a2401a7e 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -49,6 +49,7 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.PathSegment; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.StreamingOutput; @@ -1080,7 +1081,8 @@ public class RealmAdminResource { @Produces(MediaType.APPLICATION_JSON) @Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN) @Operation() - public GroupRepresentation getGroupByPath(@PathParam("path") String path) { + public GroupRepresentation getGroupByPath(@PathParam("path") List pathSegments) { + String[] path = pathSegments.stream().map(PathSegment::getPath).toArray(String[]::new); GroupModel found = KeycloakModelUtils.findGroupByPath(session, realm, path); if (found == null) { throw new NotFoundException("Group path does not exist"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java index 143f13a5ac..5fc6ecd0e7 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java @@ -1299,6 +1299,44 @@ public class GroupTest extends AbstractGroupTest { assertTrue(searchResultGroups.isEmpty()); } + @Test + public void testGroupsWithSpaces() { + RealmResource realm = adminClient.realms().realm("test"); + GroupRepresentation parentGroup = new GroupRepresentation(); + parentGroup.setName("parent space"); + parentGroup = createGroup(realm, parentGroup); + GroupRepresentation childGroup = new GroupRepresentation(); + childGroup.setName("child space"); + try (Response response = realm.groups().group(parentGroup.getId()).subGroup(childGroup)) { + assertEquals(201, response.getStatus()); // created status + childGroup.setId(ApiUtil.getCreatedId(response)); + } + assertAdminEvents.assertEvent(testRealmId, OperationType.CREATE, + AdminEventPaths.groupSubgroupsPath(parentGroup.getId()), childGroup, ResourceType.GROUP); + + List groupsFound = realm.groups().groups("parent space", true, 0, 1, true); + Assert.assertEquals(1, groupsFound.size()); + Assert.assertEquals(parentGroup.getId(), groupsFound.iterator().next().getId()); + Assert.assertEquals(0, groupsFound.iterator().next().getSubGroups().size()); + groupsFound = realm.groups().groups("child space", true, 0, 1, true); + Assert.assertEquals(1, groupsFound.size()); + Assert.assertEquals(parentGroup.getId(), groupsFound.iterator().next().getId()); + Assert.assertEquals(1, groupsFound.iterator().next().getSubGroups().size()); + Assert.assertEquals(childGroup.getId(), groupsFound.iterator().next().getSubGroups().iterator().next().getId()); + + GroupRepresentation groupFound = realm.getGroupByPath(parentGroup.getName()); + Assert.assertNotNull(groupFound); + Assert.assertEquals(parentGroup.getId(), groupFound.getId()); + groupFound = realm.getGroupByPath("/" + parentGroup.getName() + "/" + childGroup.getName()); + Assert.assertNotNull(groupFound); + Assert.assertEquals(childGroup.getId(), groupFound.getId()); + + realm.groups().group(childGroup.getId()).remove(); + assertAdminEvents.assertEvent(testRealmId, OperationType.DELETE, AdminEventPaths.groupPath(childGroup.getId()), ResourceType.GROUP); + realm.groups().group(parentGroup.getId()).remove(); + assertAdminEvents.assertEvent(testRealmId, OperationType.DELETE, AdminEventPaths.groupPath(parentGroup.getId()), ResourceType.GROUP); + } + /** * Assert that when you create/move/update a group name, the response is not Http 409 Conflict and the message does not * correspond to the returned user-friendly message in such cases