From e18fb563896cd5ec7ed7b79d4f462c87f6019eb7 Mon Sep 17 00:00:00 2001 From: Axel Messinese Date: Fri, 14 Dec 2018 00:47:27 +0100 Subject: [PATCH] KEYCLOAK-4978 Add endpoint to get groups by role --- .../admin/client/resource/RoleResource.java | 27 +++++++++++ .../cache/infinispan/RealmCacheSession.java | 5 ++ .../keycloak/models/jpa/JpaRealmProvider.java | 19 ++++++++ .../jpa/entities/GroupRoleMappingEntity.java | 1 + .../org/keycloak/models/RealmProvider.java | 2 + .../admin/RoleContainerResource.java | 35 ++++++++++++++ .../testsuite/admin/realm/RealmRolesTest.java | 46 ++++++++++++++++++- 7 files changed, 133 insertions(+), 2 deletions(-) diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RoleResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RoleResource.java index 5607f9136d..b83930f369 100755 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RoleResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RoleResource.java @@ -17,6 +17,7 @@ package org.keycloak.admin.client.resource; +import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.ManagementPermissionReference; import org.keycloak.representations.idm.ManagementPermissionRepresentation; import org.keycloak.representations.idm.RoleRepresentation; @@ -126,5 +127,31 @@ public interface RoleResource { @Produces(MediaType.APPLICATION_JSON) Set getRoleUserMembers(@QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults); + + /** + * Get role groups + *

+ * Returns groups that have the given role + * + * @return a list of groups with the given role + */ + @GET + @Path("groups") + @Produces(MediaType.APPLICATION_JSON) + Set getRoleGroupMembers(); + /** + * Get role groups + *

+ * Returns groups that have the given role, paginated according to the query parameters + * + * @param firstResult Pagination offset + * @param maxResults Pagination size + * @return a list of groups with the given role + */ + @GET + @Path("groups") + @Produces(MediaType.APPLICATION_JSON) + Set getRoleGroupMembers(@QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index e781c6f09c..68c3ea0484 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -878,6 +878,11 @@ public class RealmCacheSession implements CacheRealmProvider { public Long getGroupsCountByNameContaining(RealmModel realm, String search) { return getRealmDelegate().getGroupsCountByNameContaining(realm, search); } + + @Override + public List getGroupsByRole(RealmModel realm, RoleModel role, int firstResult, int maxResults) { + return getRealmDelegate().getGroupsByRole(realm, role, firstResult, maxResults); + } @Override public List getTopLevelGroups(RealmModel realm) { diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java index 90b4bc57e7..75a95c42b1 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -385,6 +385,25 @@ public class JpaRealmProvider implements RealmProvider { public Long getGroupsCountByNameContaining(RealmModel realm, String search) { return (long) searchForGroupByName(realm, search, null, null).size(); } + + @Override + public List getGroupsByRole(RealmModel realm, RoleModel role, int firstResult, int maxResults) { + TypedQuery query = em.createNamedQuery("groupsInRole", GroupEntity.class); + query.setParameter("roleId", role.getId()); + if (firstResult != -1) { + query.setFirstResult(firstResult); + } + if (maxResults != -1) { + query.setMaxResults(maxResults); + } + List results = query.getResultList(); + + return results.stream() + .map(g -> new GroupAdapter(realm, em, g)) + .sorted(Comparator.comparing(GroupModel::getName)) + .collect(Collectors.collectingAndThen( + Collectors.toList(), Collections::unmodifiableList)); + } @Override public List getTopLevelGroups(RealmModel realm) { diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupRoleMappingEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupRoleMappingEntity.java index 796d29db8a..8ba16dc7db 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupRoleMappingEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupRoleMappingEntity.java @@ -34,6 +34,7 @@ import java.io.Serializable; * @version $Revision: 1 $ */ @NamedQueries({ + @NamedQuery(name="groupsInRole", query="select g from GroupRoleMappingEntity m, GroupEntity g where m.roleId=:roleId and g.id=m.group"), @NamedQuery(name="groupHasRole", query="select m from GroupRoleMappingEntity m where m.group = :group and m.roleId = :roleId"), @NamedQuery(name="groupRoleMappings", query="select m from GroupRoleMappingEntity m where m.group = :group"), @NamedQuery(name="groupRoleMappingIds", query="select m.roleId from GroupRoleMappingEntity m where m.group = :group"), diff --git a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java index 3423b51527..96c41c45e3 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmProvider.java @@ -43,6 +43,8 @@ public interface RealmProvider extends Provider, ClientProvider { Long getGroupsCount(RealmModel realm, Boolean onlyTopGroups); Long getGroupsCountByNameContaining(RealmModel realm, String search); + + List getGroupsByRole(RealmModel realm, RoleModel role, int firstResult, int maxResults); List getTopLevelGroups(RealmModel realm); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java index cebd168216..45e0e7f5db 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleContainerResource.java @@ -23,6 +23,7 @@ import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; +import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; @@ -30,6 +31,7 @@ import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.ManagementPermissionReference; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; @@ -41,6 +43,7 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissions; import javax.ws.rs.BadRequestException; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; @@ -54,6 +57,7 @@ import javax.ws.rs.core.UriInfo; import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; /** * @resource Roles @@ -402,4 +406,35 @@ public class RoleContainerResource extends RoleResource { return results; } + + /** + * Return List of Groups that have the specified role name + * + * + * @param roleName + * @param firstResult + * @param maxResults + * @param fullRepresentation if true, return a full representation of the GroupRepresentation objects + * @return + */ + @Path("{role-name}/groups") + @GET + @Produces(MediaType.APPLICATION_JSON) + @NoCache + public List getGroupsInRole(final @PathParam("role-name") String roleName, + @QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults, + @QueryParam("full") @DefaultValue("false") boolean fullRepresentation) { + + auth.roles().requireView(roleContainer); + firstResult = firstResult != null ? firstResult : 0; + maxResults = maxResults != null ? maxResults : Constants.DEFAULT_MAX_RESULTS; + + RoleModel role = roleContainer.getRole(roleName); + List groupsModel = session.realms().getGroupsByRole(realm, role, firstResult, maxResults); + + return groupsModel.stream() + .map(g -> ModelToRepresentation.toRepresentation(g, fullRepresentation)) + .collect(Collectors.toList()); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmRolesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmRolesTest.java index 45c41cbd66..5228dbfd78 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmRolesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmRolesTest.java @@ -25,6 +25,7 @@ import org.keycloak.admin.client.resource.UserResource; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.Assert; @@ -110,7 +111,12 @@ public class RealmRolesTest extends AbstractAdminTest { getCleanup().addRoleId(ids.get("role-without-users")); getCleanup().addUserId(adminClient.realm(REALM_NAME).users().search(userRep.getUsername()).get(0).getId()); - + GroupRepresentation groupRep = new GroupRepresentation(); + groupRep.setName("test-role-group"); + groupRep.setPath("/test-role-group"); + adminClient.realm(REALM_NAME).groups().add(groupRep); + getCleanup().addGroupId(adminClient.realm(REALM_NAME).groups().groups().get(0).getId()); + resource = adminClient.realm(REALM_NAME).roles(); assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.roleResourcePath("role-a"), roleA, ResourceType.REALM_ROLE); @@ -122,7 +128,7 @@ public class RealmRolesTest extends AbstractAdminTest { assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientUuid, "role-c"), roleC, ResourceType.CLIENT_ROLE); assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.userResourcePath(adminClient.realm(REALM_NAME).users().search(userRep.getUsername()).get(0).getId()), userRep, ResourceType.USER); - + assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.groupPath(adminClient.realm(REALM_NAME).groups().groups().get(0).getId()), groupRep, ResourceType.GROUP); } @@ -221,6 +227,7 @@ public class RealmRolesTest extends AbstractAdminTest { } + /** * KEYCLOAK-2035 Verifies that Role with no users assigned is being properly retrieved without members in API endpoint for role membership */ @@ -234,6 +241,41 @@ public class RealmRolesTest extends AbstractAdminTest { } + + /** + * KEYCLOAK-4978 Verifies that Groups assigned to Role are being properly retrieved as members in API endpoint for role membership + */ + @Test + public void testGroupsInRole() { + RoleResource role = resource.get("role-with-users"); + + List groups = adminClient.realm(REALM_NAME).groups().groups(); + GroupRepresentation groupRep = groups.stream().filter(g -> g.getPath().equals("/test-role-group")).findFirst().get(); + + RoleResource roleResource = adminClient.realm(REALM_NAME).roles().get(role.toRepresentation().getName()); + List rolesToAdd = new LinkedList<>(); + rolesToAdd.add(roleResource.toRepresentation()); + adminClient.realm(REALM_NAME).groups().group(groupRep.getId()).roles().realmLevel().add(rolesToAdd); + + roleResource = adminClient.realm(REALM_NAME).roles().get(role.toRepresentation().getName()); + + Set groupsInRole = roleResource.getRoleGroupMembers(); + assertTrue(groupsInRole.stream().filter(g -> g.getPath().equals("/test-role-group")).findFirst().isPresent()); + } + + /** + * KEYCLOAK-4978 Verifies that Role with no users assigned is being properly retrieved without groups in API endpoint for role membership + */ + @Test + public void testGroupsNotInRole() { + RoleResource role = resource.get("role-without-users"); + + role = adminClient.realm(REALM_NAME).roles().get(role.toRepresentation().getName()); + + Set groupsInRole = role.getRoleGroupMembers(); + assertTrue(groupsInRole.isEmpty()); + } + /** * KEYCLOAK-2035 Verifies that Role Membership is ok after user removal */