From e78bf5f87614f089ae6a130605f0734004d1d085 Mon Sep 17 00:00:00 2001 From: howcroft Date: Wed, 20 Sep 2017 17:05:33 +0100 Subject: [PATCH] Keycloak 2035 This PR adds: * an endpoint to Role that lists users with the Role * a tab "Users in Role" in Admin console Role page * it is applicable to Realm and Client Roles * Extends UserQueryProvider with default methods (throwing Runtime Exception if not overriden) * Testing in base testsuite and Console --- .../user/EjbExampleUserStorageProvider.java | 10 ++ .../PropertyFileUserStorageProvider.java | 14 +++ .../admin/client/resource/RoleResource.java | 6 + .../cache/infinispan/UserCacheSession.java | 11 ++ .../keycloak/models/jpa/JpaUserProvider.java | 33 +++++ .../jpa/entities/UserRoleMappingEntity.java | 1 + .../storage/user/UserQueryProvider.java | 30 +++++ .../admin/RoleContainerResource.java | 35 ++++++ .../keycloak/storage/UserStorageManager.java | 18 ++- .../federation/UserPropertyFileStorage.java | 2 +- .../admin/client/ClientRolesTest.java | 1 - .../testsuite/admin/realm/RealmRolesTest.java | 95 ++++++++++++++ .../console/roles/UsersInRoleTest.java | 118 ++++++++++++++++++ .../messages/admin-messages_pt_BR.properties | 3 + .../main/resources/theme/base/admin/index.ftl | 1 + .../messages/admin-messages_en.properties | 3 + .../theme/base/admin/resources/js/app.js | 12 ++ .../admin/resources/js/controllers/roles.js | 45 +++++++ .../theme/base/admin/resources/js/services.js | 7 ++ .../resources/partials/realm-role-users.html | 50 ++++++++ .../resources/templates/kc-tabs-role.html | 2 + 21 files changed, 494 insertions(+), 3 deletions(-) create mode 100644 testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/roles/UsersInRoleTest.java create mode 100644 themes/src/main/resources/theme/base/admin/resources/js/controllers/roles.js create mode 100644 themes/src/main/resources/theme/base/admin/resources/partials/realm-role-users.html diff --git a/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/EjbExampleUserStorageProvider.java b/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/EjbExampleUserStorageProvider.java index 97b1ea0b49..dc8a8985df 100644 --- a/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/EjbExampleUserStorageProvider.java +++ b/examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/EjbExampleUserStorageProvider.java @@ -299,6 +299,16 @@ public class EjbExampleUserStorageProvider implements UserStorageProvider, return Collections.EMPTY_LIST; } + @Override + public List getRoleMembers(RealmModel realm, RoleModel role, int firstResult, int maxResults) { + return Collections.EMPTY_LIST; + } + + @Override + public List getRoleMembers(RealmModel realm, RoleModel role) { + return Collections.EMPTY_LIST; + } + @Override public List searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) { return Collections.EMPTY_LIST; diff --git a/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/writeable/PropertyFileUserStorageProvider.java b/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/writeable/PropertyFileUserStorageProvider.java index 1b256c9b53..54755ee3bb 100755 --- a/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/writeable/PropertyFileUserStorageProvider.java +++ b/examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/writeable/PropertyFileUserStorageProvider.java @@ -26,6 +26,7 @@ import org.keycloak.credential.CredentialModel; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.storage.StorageId; @@ -190,6 +191,19 @@ public class PropertyFileUserStorageProvider implements // runtime automatically handles querying UserFederatedStorage return Collections.EMPTY_LIST; } + + @Override + public List getRoleMembers(RealmModel realm, RoleModel role, int firstResult, int maxResults) { + // Not supported in federated storage + return Collections.EMPTY_LIST; + } + + @Override + public List getRoleMembers(RealmModel realm, RoleModel role) { + // Not supported in federated storage + return Collections.EMPTY_LIST; + } + @Override public List searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) { 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 9ac2cd91bb..7ef7b89807 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 @@ -18,6 +18,7 @@ package org.keycloak.admin.client.resource; import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -71,5 +72,10 @@ public interface RoleResource { @Path("composites") @Consumes(MediaType.APPLICATION_JSON) void deleteComposites(List rolesToRemove); + + @GET + @Path("users") + @Produces(MediaType.APPLICATION_JSON) + Set getRoleUserMembers(); } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java index 390c25c158..b0e731fcff 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java @@ -511,6 +511,17 @@ public class UserCacheSession implements UserCache { return getDelegate().getGroupMembers(realm, group); } + @Override + public List getRoleMembers(RealmModel realm, RoleModel role, int firstResult, int maxResults) { + return getDelegate().getRoleMembers(realm, role, firstResult, maxResults); + } + + @Override + public List getRoleMembers(RealmModel realm, RoleModel role) { + return getDelegate().getRoleMembers(realm, role); + } + + @Override public UserModel getServiceAccount(ClientModel client) { // Just an attempt to find the user from cache by default serviceAccount username diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index b543d6b2f4..d192a7d742 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -496,6 +496,20 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { } return users; } + + @Override + public List getRoleMembers(RealmModel realm, RoleModel role) { + TypedQuery query = em.createNamedQuery("usersInRole", UserEntity.class); + query.setParameter("roleId", role.getId()); + List results = query.getResultList(); + + List users = new ArrayList(); + for (UserEntity user : results) { + users.add(new UserAdapter(session, realm, em, user)); + } + return users; + } + @Override public void preRemove(RealmModel realm, GroupModel group) { @@ -635,6 +649,25 @@ public class JpaUserProvider implements UserProvider, UserCredentialStore { } return users; } + + @Override + public List getRoleMembers(RealmModel realm, RoleModel role, int firstResult, int maxResults) { + TypedQuery query = em.createNamedQuery("usersInRole", UserEntity.class); + query.setParameter("roleId", role.getId()); + if (firstResult != -1) { + query.setFirstResult(firstResult); + } + if (maxResults != -1) { + query.setMaxResults(maxResults); + } + List results = query.getResultList(); + + List users = new LinkedList<>(); + for (UserEntity user : results) { + users.add(new UserAdapter(session, realm, em, user)); + } + return users; + } @Override public List searchForUser(String search, RealmModel realm) { diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserRoleMappingEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserRoleMappingEntity.java index 7f5db60110..884ba3d8f3 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserRoleMappingEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/UserRoleMappingEntity.java @@ -34,6 +34,7 @@ import java.io.Serializable; * @version $Revision: 1 $ */ @NamedQueries({ + @NamedQuery(name="usersInRole", query="select u from UserRoleMappingEntity m, UserEntity u where m.roleId=:roleId and u.id=m.user"), @NamedQuery(name="userHasRole", query="select m from UserRoleMappingEntity m where m.user = :user and m.roleId = :roleId"), @NamedQuery(name="userRoleMappings", query="select m from UserRoleMappingEntity m where m.user = :user"), @NamedQuery(name="userRoleMappingIds", query="select m.roleId from UserRoleMappingEntity m where m.user = :user"), diff --git a/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java b/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java index 8143dc413c..2cbae119f7 100644 --- a/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java +++ b/server-spi/src/main/java/org/keycloak/storage/user/UserQueryProvider.java @@ -18,6 +18,7 @@ package org.keycloak.storage.user; import org.keycloak.models.GroupModel; import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; import java.util.List; @@ -117,6 +118,35 @@ public interface UserQueryProvider { */ List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults); + /** + * Get users that belong to a specific role. + * + * + * + * @param realm + * @param role + * @return + */ + default List getRoleMembers(RealmModel realm, RoleModel role) + { + throw new UnsupportedOperationException(); + } + + /** + * Search for users that have a specific role with a specific roleId. + * + * + * + * @param firstResult + * @param maxResults + * @param role + * @return + */ + default List getRoleMembers(RealmModel realm, RoleModel role, int firstResult, int maxResults) + { + throw new UnsupportedOperationException(); + } + /** * Get users that belong to a specific group. Implementations do not have to search in UserFederatedStorageProvider * as this is done automatically. 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 7ad9d2233f..c0914f3051 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 @@ -19,20 +19,24 @@ package org.keycloak.services.resources.admin; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.NotFoundException; +import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement; import org.keycloak.services.resources.admin.permissions.AdminPermissions; 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.KeycloakSession; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; 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.ManagementPermissionReference; import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.ErrorResponse; import javax.ws.rs.BadRequestException; @@ -373,4 +377,35 @@ public class RoleContainerResource extends RoleResource { } } + /** + * Return List of Users that have the specified role name + * + * + * @param roleName + * @param firstResult + * @param maxResults + * @return initialized manage permissions reference + */ + @Path("{role-name}/users") + @GET + @Produces(MediaType.APPLICATION_JSON) + @NoCache + public List getUsersInRole(final @PathParam("role-name") String roleName, + @QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults) { + + auth.roles().requireView(roleContainer); + firstResult = firstResult != null ? firstResult : 0; + maxResults = maxResults != null ? maxResults : Constants.DEFAULT_MAX_RESULTS; + + RoleModel role = roleContainer.getRole(roleName); + List results = new ArrayList(); + List userModels = session.users().getRoleMembers(realm, role, firstResult, maxResults); + + for (UserModel user : userModels) { + results.add(ModelToRepresentation.toRepresentation(session, realm, user)); + } + return results; + + } } diff --git a/services/src/main/java/org/keycloak/storage/UserStorageManager.java b/services/src/main/java/org/keycloak/storage/UserStorageManager.java index 8c5b633f89..ef63b8815b 100755 --- a/services/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/services/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -328,7 +328,12 @@ public class UserStorageManager implements UserProvider, OnUserCache, OnCreateCo public List getGroupMembers(RealmModel realm, GroupModel group) { return getGroupMembers(realm, group, -1, -1); } - + + @Override + public List getRoleMembers(RealmModel realm, RoleModel role) { + return getRoleMembers(realm, role, -1, -1); + } + @Override public UserModel getUserByUsername(String username, RealmModel realm) { UserModel user = localStorage().getUserByUsername(username, realm); @@ -577,6 +582,17 @@ public class UserStorageManager implements UserProvider, OnUserCache, OnCreateCo return importValidation(realm, results); } + @Override + public List getRoleMembers(final RealmModel realm, final RoleModel role, int firstResult, int maxResults) { + List results = query((provider, first, max) -> { + if (provider instanceof UserQueryProvider) { + return ((UserQueryProvider)provider).getRoleMembers(realm, role, first, max); + } + return Collections.EMPTY_LIST; + }, realm, firstResult, maxResults); + return importValidation(realm, results); + } + @Override public void preRemove(RealmModel realm) { diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/UserPropertyFileStorage.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/UserPropertyFileStorage.java index 3716afffff..7d793d71fe 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/UserPropertyFileStorage.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/UserPropertyFileStorage.java @@ -137,7 +137,6 @@ public class UserPropertyFileStorage implements UserLookupProvider, UserStorageP } } - @Override public int getUsersCount(RealmModel realm) { return userPasswords.size(); @@ -207,6 +206,7 @@ public class UserPropertyFileStorage implements UserLookupProvider, UserStorageP return Collections.EMPTY_LIST; } + @Override public List searchForUser(String search, RealmModel realm) { return getUsers(realm, 0, Integer.MAX_VALUE - 1); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientRolesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientRolesTest.java index 2aa5933712..e4b3656503 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientRolesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientRolesTest.java @@ -132,5 +132,4 @@ public class ClientRolesTest extends AbstractClientTest { assertFalse(rolesRsc.get("role-a").toRepresentation().isComposite()); assertEquals(0, rolesRsc.get("role-a").getRoleComposites().size()); } - } 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 4fa09df4b3..8d00bb26e8 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 @@ -19,11 +19,15 @@ package org.keycloak.testsuite.admin.realm; import org.junit.Before; import org.junit.Test; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.RoleResource; import org.keycloak.admin.client.resource.RolesResource; +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.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.admin.AbstractAdminTest; import org.keycloak.testsuite.admin.ApiUtil; @@ -33,6 +37,8 @@ import org.keycloak.testsuite.util.RoleBuilder; import javax.ws.rs.NotFoundException; import javax.ws.rs.core.Response; + +import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -44,6 +50,8 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.keycloak.testsuite.Assert.assertNames; +import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; /** * @author Stian Thorgersen @@ -59,9 +67,15 @@ public class RealmRolesTest extends AbstractAdminTest { public void before() { RoleRepresentation roleA = RoleBuilder.create().name("role-a").description("Role A").build(); RoleRepresentation roleB = RoleBuilder.create().name("role-b").description("Role B").build(); + //KEYCLOAK-2035 + RoleRepresentation roleWithUsers = RoleBuilder.create().name("role-with-users").description("Role with users").build(); + RoleRepresentation roleWithoutUsers = RoleBuilder.create().name("role-without-users").description("role-without-users").build(); adminClient.realm(REALM_NAME).roles().create(roleA); adminClient.realm(REALM_NAME).roles().create(roleB); + adminClient.realm(REALM_NAME).roles().create(roleWithUsers); + adminClient.realm(REALM_NAME).roles().create(roleWithoutUsers); + ClientRepresentation clientRep = ClientBuilder.create().clientId("client-a").build(); Response response = adminClient.realm(REALM_NAME).clients().create(clientRep); clientUuid = ApiUtil.getCreatedId(response); @@ -78,18 +92,35 @@ public class RealmRolesTest extends AbstractAdminTest { for (RoleRepresentation r : adminClient.realm(REALM_NAME).clients().get(clientUuid).roles().list()) { ids.put(r.getName(), r.getId()); } + + UserRepresentation userRep = new UserRepresentation(); + userRep.setUsername("test-role-member"); + userRep.setEmail("test-role-member@test-role-member.com"); + userRep.setRequiredActions(Collections.emptyList()); + userRep.setEnabled(true); + adminClient.realm(REALM_NAME).users().create(userRep); getCleanup().addRoleId(ids.get("role-a")); getCleanup().addRoleId(ids.get("role-b")); getCleanup().addRoleId(ids.get("role-c")); + getCleanup().addRoleId(ids.get("role-with-users")); + getCleanup().addRoleId(ids.get("role-without-users")); + getCleanup().addUserId(adminClient.realm(REALM_NAME).users().search(userRep.getUsername()).get(0).getId()); + resource = adminClient.realm(REALM_NAME).roles(); assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.roleResourcePath("role-a"), roleA, ResourceType.REALM_ROLE); assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.roleResourcePath("role-b"), roleB, ResourceType.REALM_ROLE); + assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.roleResourcePath("role-with-users"), roleWithUsers, ResourceType.REALM_ROLE); + assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.roleResourcePath("role-without-users"), roleWithoutUsers, ResourceType.REALM_ROLE); assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.clientResourcePath(clientUuid), clientRep, ResourceType.CLIENT); 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); + + } @Test @@ -163,4 +194,68 @@ public class RealmRolesTest extends AbstractAdminTest { assertEquals(0, resource.get("role-a").getRoleComposites().size()); } + /** + * KEYCLOAK-2035 Verifies that Users assigned to Role are being properly retrieved as members in API endpoint for role membership + */ + @Test + public void testUsersInRole() { + RoleResource role = resource.get("role-with-users"); + + List users = adminClient.realm(REALM_NAME).users().search("test-role-member", null, null, null, null, null); + assertEquals(1, users.size()); + UserResource user = adminClient.realm(REALM_NAME).users().get(users.get(0).getId()); + UserRepresentation userRep = user.toRepresentation(); + + RoleResource roleResource = adminClient.realm(REALM_NAME).roles().get(role.toRepresentation().getName()); + List rolesToAdd = new LinkedList<>(); + rolesToAdd.add(roleResource.toRepresentation()); + adminClient.realm(REALM_NAME).users().get(userRep.getId()).roles().realmLevel().add(rolesToAdd); + + roleResource = adminClient.realm(REALM_NAME).roles().get(role.toRepresentation().getName()); + roleResource.getRoleUserMembers(); + //roleResource.getRoleUserMembers().stream().forEach((member) -> log.infof("Found user {}", member.getUsername())); + assertEquals(1, roleResource.getRoleUserMembers().size()); + + } + + /** + * KEYCLOAK-2035 Verifies that Role with no users assigned is being properly retrieved without members in API endpoint for role membership + */ + @Test + public void testUsersNotInRole() { + RoleResource role = resource.get("role-without-users"); + + role = adminClient.realm(REALM_NAME).roles().get(role.toRepresentation().getName()); + role.getRoleUserMembers(); + assertEquals(0, role.getRoleUserMembers().size()); + + } + + /** + * KEYCLOAK-2035 Verifies that Role Membership is ok after user removal + */ + @Test + public void roleMembershipAfterUserRemoval() { + RoleResource role = resource.get("role-with-users"); + + List users = adminClient.realm(REALM_NAME).users().search("test-role-member", null, null, null, null, null); + assertEquals(1, users.size()); + UserResource user = adminClient.realm(REALM_NAME).users().get(users.get(0).getId()); + UserRepresentation userRep = user.toRepresentation(); + + RoleResource roleResource = adminClient.realm(REALM_NAME).roles().get(role.toRepresentation().getName()); + List rolesToAdd = new LinkedList<>(); + rolesToAdd.add(roleResource.toRepresentation()); + adminClient.realm(REALM_NAME).users().get(userRep.getId()).roles().realmLevel().add(rolesToAdd); + + roleResource = adminClient.realm(REALM_NAME).roles().get(role.toRepresentation().getName()); + roleResource.getRoleUserMembers(); + assertEquals(1, roleResource.getRoleUserMembers().size()); + + adminClient.realm(REALM_NAME).users().delete(userRep.getId()); + roleResource.getRoleUserMembers(); + assertEquals(0, roleResource.getRoleUserMembers().size()); + + } + } diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/roles/UsersInRoleTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/roles/UsersInRoleTest.java new file mode 100644 index 0000000000..c68366c294 --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/roles/UsersInRoleTest.java @@ -0,0 +1,118 @@ +/** + * + */ +package org.keycloak.testsuite.console.roles; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import org.jboss.arquillian.graphene.page.Page; +import org.junit.Before; +import org.junit.Test; +import org.keycloak.admin.client.resource.RoleResource; +import org.keycloak.admin.client.resource.RolesResource; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.console.page.roles.DefaultRoles; +import org.keycloak.testsuite.console.page.roles.RealmRoles; +import org.keycloak.testsuite.console.page.roles.Role; +import org.keycloak.testsuite.console.page.roles.Roles; +import org.keycloak.testsuite.console.page.users.UserRoleMappings; +import org.keycloak.testsuite.console.page.users.Users; + +/** + * @author Antonio Ferreira + * + */ +public class UsersInRoleTest extends AbstractRolesTest { + + + @Page + private DefaultRoles defaultRolesPage; + + @Page + private UserRoleMappings userRolesPage; + + @Page + private Users usersPage; + + @Page + private Roles rolesPage; + + @Page + private Role rolePage; + + @Page + private RealmRoles realmRolesPage; + + private RoleRepresentation testRoleRep; + private UserRepresentation newUser; + + + + @Before + public void beforeDefaultRolesTest() { + // create a role via admin client + testRoleRep = new RoleRepresentation("test-role", "", false); + rolesResource().create(testRoleRep); + + newUser = new UserRepresentation(); + newUser.setUsername("test_user"); + newUser.setEnabled(true); + newUser.setEmail("test-role-member@test-role-member.com"); + newUser.setRequiredActions(Collections.emptyList()); + //testRealmResource().users().create(newUser); + createUserWithAdminClient(testRealmResource(), newUser); + rolesResource().create(testRoleRep); + rolesPage.navigateTo(); + } + + + public RolesResource rolesResource() { + return testRealmResource().roles(); + } + + //Added for KEYCLOAK-2035 + @Test + public void usersInRoleTabIsPresent() { + + rolesPage.navigateTo(); + rolesPage.tabs().realmRoles(); + realmRolesPage.table().search(testRoleRep.getName()); + realmRolesPage.table().clickRole(testRoleRep.getName()); + //assert no users in list + //Role Page class missing a getUsers() method + + List users = testRealmResource().users().search("test_user", null, null, null, null, null); + assertEquals(1, users.size()); + UserResource user = testRealmResource().users().get(users.get(0).getId()); + UserRepresentation userRep = user.toRepresentation(); + + usersPage.navigateTo(); + usersPage.table().search(userRep.getUsername()); + usersPage.table().clickUser(userRep.getUsername()); + + assertFalse(userRolesPage.form().isAssignedRole(testRoleRep.getName())); + + RoleResource roleResource = testRealmResource().roles().get(testRoleRep.getName()); + List rolesToAdd = new LinkedList<>(); + rolesToAdd.add(roleResource.toRepresentation()); + testRealmResource().users().get(userRep.getId()).roles().realmLevel().add(rolesToAdd); + + rolesPage.navigateTo(); + rolesPage.tabs().realmRoles(); + realmRolesPage.table().search(testRoleRep.getName()); + realmRolesPage.table().clickRole(testRoleRep.getName()); + + assertTrue(userRolesPage.form().isAssignedRole(testRoleRep.getName())); + } + + +} diff --git a/themes/src/main/resources-community/theme/base/admin/messages/admin-messages_pt_BR.properties b/themes/src/main/resources-community/theme/base/admin/messages/admin-messages_pt_BR.properties index 6476cf2946..96b64d86d3 100644 --- a/themes/src/main/resources-community/theme/base/admin/messages/admin-messages_pt_BR.properties +++ b/themes/src/main/resources-community/theme/base/admin/messages/admin-messages_pt_BR.properties @@ -573,6 +573,7 @@ select-a-type.placeholder=selecione um tipo available-groups=Grupos disponíveis value=Valor table-of-group-members=Tabela de membros do grupo +table-of-role-members=Tabela de membros do role last-name=Sobrenome first-name=Primeiro nome email=E-mail @@ -672,6 +673,7 @@ download-keys-and-cert=Download chave e certificado no-value-assigned.placeholder=Nenhum valor associado remove=Remover no-group-members=Nenhum membro +no-role-members=Nenhum membro no role temporary=Temporária join=Participar event-type=Tipo de evento @@ -697,6 +699,7 @@ authz-scope=Escopo authz-authz-scopes=Autorização de escopos authz-policies=Políticas authz-permissions=Permissões +authz-users=Usuários no role authz-evaluate=Avaliar authz-icon-uri=URI do ícone authz-select-scope=Selecione um escopo diff --git a/themes/src/main/resources/theme/base/admin/index.ftl b/themes/src/main/resources/theme/base/admin/index.ftl index aebc488493..397c4b04c5 100755 --- a/themes/src/main/resources/theme/base/admin/index.ftl +++ b/themes/src/main/resources/theme/base/admin/index.ftl @@ -66,6 +66,7 @@ + diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 88483717d8..1620996305 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -938,6 +938,7 @@ available-groups=Available Groups available-groups.tooltip=Select a group you want to add as a default. value=Value table-of-group-members=Table of group members +table-of-role-members=Table of role members last-name=Last Name first-name=First Name email=Email @@ -1063,6 +1064,7 @@ download-keys-and-cert=Download keys and cert no-value-assigned.placeholder=No value assigned remove=Remove no-group-members=No group members +no-role-members=No role members temporary=Temporary join=Join event-type=Event Type @@ -1088,6 +1090,7 @@ authz-scope=Scope authz-authz-scopes=Authorization Scopes authz-policies=Policies authz-permissions=Permissions +authz-users=Users in Role authz-evaluate=Evaluate authz-icon-uri=Icon URI authz-icon-uri.tooltip=An URI pointing to an icon. diff --git a/themes/src/main/resources/theme/base/admin/resources/js/app.js b/themes/src/main/resources/theme/base/admin/resources/js/app.js index c650d00681..4e6de53a92 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -770,6 +770,18 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'RoleDetailCtrl' }) + .when('/realms/:realm/roles/:role/users', { + templateUrl : resourceUrl + '/partials/realm-role-users.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + role : function(RoleLoader) { + return RoleLoader(); + } + }, + controller : 'RoleMembersCtrl' + }) .when('/realms/:realm/roles', { templateUrl : resourceUrl + '/partials/role-list.html', resolve : { diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/roles.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/roles.js new file mode 100644 index 0000000000..185913abb7 --- /dev/null +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/roles.js @@ -0,0 +1,45 @@ +module.controller('RoleMembersCtrl', function($scope, realm, role, RoleMembership) { + $scope.realm = realm; + $scope.page = 0; + $scope.role = role; + + $scope.query = { + realm: realm.realm, + role: role.name, + max : 5, + first : 0 + } + + + $scope.firstPage = function() { + $scope.query.first = 0; + $scope.searchQuery(); + } + + $scope.previousPage = function() { + $scope.query.first -= parseInt($scope.query.max); + if ($scope.query.first < 0) { + $scope.query.first = 0; + } + $scope.searchQuery(); + } + + $scope.nextPage = function() { + $scope.query.first += parseInt($scope.query.max); + $scope.searchQuery(); + } + + $scope.searchQuery = function() { + console.log("query.search: " + $scope.query.search); + $scope.searchLoaded = false; + + $scope.users = RoleMembership.query($scope.query, function() { + console.log('search loaded'); + $scope.searchLoaded = true; + $scope.lastSearch = $scope.query.search; + }); + }; + + $scope.searchQuery(); + +}); diff --git a/themes/src/main/resources/theme/base/admin/resources/js/services.js b/themes/src/main/resources/theme/base/admin/resources/js/services.js index b084a4fc08..a9935f4507 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -1684,6 +1684,13 @@ module.factory('GroupMembership', function($resource) { }); }); +module.factory('RoleMembership', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/roles/:role/users', { + realm : '@realm', + role : '@role' + }); +}); + module.factory('UserGroupMembership', function($resource) { return $resource(authUrl + '/admin/realms/:realm/users/:userId/groups', { diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-role-users.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-role-users.html new file mode 100644 index 0000000000..11cbbc6840 --- /dev/null +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-role-users.html @@ -0,0 +1,50 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{:: 'username' | translate}}{{:: 'last-name' | translate}}{{:: 'first-name' | translate}}{{:: 'email' | translate}}
+
+ + + +
+
{{user.username}}{{user.lastName}}{{user.firstName}}{{user.email}}{{:: 'edit' | translate}}
{{:: 'no-role-members' | translate}}{{:: 'no-role-members' | translate}}
+ +
+ + \ No newline at end of file diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-role.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-role.html index 785c7e9260..9c3c4a7beb 100755 --- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-role.html +++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-role.html @@ -9,5 +9,7 @@ {{:: 'authz-permissions' | translate}} {{:: 'manage-permissions-role.tooltip' | translate}} +
  • + {{:: 'authz-users' | translate}}
  • \ No newline at end of file