keycloak-aplcache

Keycloak 2035 This PR adds: * an endpoint to Role that lists

9/20/2017 1:05:33 PM

Changes

Details

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 97b1ea0..dc8a898 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
@@ -300,6 +300,16 @@ public class EjbExampleUserStorageProvider implements UserStorageProvider,
     }
 
     @Override
+    public List<UserModel> getRoleMembers(RealmModel realm, RoleModel role, int firstResult, int maxResults) {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    public List<UserModel> getRoleMembers(RealmModel realm, RoleModel role) {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
     public List<UserModel> 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 1b256c9..54755ee 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<UserModel> getRoleMembers(RealmModel realm, RoleModel role, int firstResult, int maxResults) {
+        // Not supported in federated storage
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    public List<UserModel> getRoleMembers(RealmModel realm, RoleModel role) {
+        // Not supported in federated storage
+        return Collections.EMPTY_LIST;
+    }
+
 
     @Override
     public List<UserModel> 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 9ac2cd9..7ef7b89 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<RoleRepresentation> rolesToRemove);
+    
+    @GET
+    @Path("users")
+    @Produces(MediaType.APPLICATION_JSON)
+    Set<UserRepresentation> 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 390c25c..b0e731f 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
@@ -512,6 +512,17 @@ public class UserCacheSession implements UserCache {
     }
 
     @Override
+    public List<UserModel> getRoleMembers(RealmModel realm, RoleModel role, int firstResult, int maxResults) {
+        return getDelegate().getRoleMembers(realm, role, firstResult, maxResults);
+    }
+
+    @Override
+    public List<UserModel> 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
         UserModel user = findServiceAccount(client);
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 7f5db60..884ba3d 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/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index b543d6b..d192a7d 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<UserModel> getRoleMembers(RealmModel realm, RoleModel role) {
+        TypedQuery<UserEntity> query = em.createNamedQuery("usersInRole", UserEntity.class);
+        query.setParameter("roleId", role.getId());
+        List<UserEntity> results = query.getResultList();
+
+        List<UserModel> users = new ArrayList<UserModel>();
+        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<UserModel> getRoleMembers(RealmModel realm, RoleModel role, int firstResult, int maxResults) {
+        TypedQuery<UserEntity> query = em.createNamedQuery("usersInRole", UserEntity.class);
+        query.setParameter("roleId", role.getId());
+        if (firstResult != -1) {
+            query.setFirstResult(firstResult);
+        }
+        if (maxResults != -1) {
+            query.setMaxResults(maxResults);
+        }
+        List<UserEntity> results = query.getResultList();
+
+        List<UserModel> users = new LinkedList<>();
+        for (UserEntity user : results) {
+            users.add(new UserAdapter(session, realm, em, user));
+        }
+        return users;
+    }
 
     @Override
     public List<UserModel> searchForUser(String search, RealmModel realm) {
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 8143dc4..2cbae11 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;
@@ -118,6 +119,35 @@ public interface UserQueryProvider {
     List<UserModel> getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults);
 
     /**
+     * Get users that belong to a specific role.
+     * 
+     *
+     *
+     * @param realm
+     * @param role
+     * @return
+     */
+    default List<UserModel> 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<UserModel> 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 7ad9d22..c0914f3 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<UserRepresentation> 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<UserRepresentation> results = new ArrayList<UserRepresentation>();
+        List<UserModel> 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 8c5b633..ef63b88 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<UserModel> getGroupMembers(RealmModel realm, GroupModel group) {
         return getGroupMembers(realm, group, -1, -1);
     }
-
+    
+    @Override
+    public List<UserModel> 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<UserModel> getRoleMembers(final RealmModel realm, final RoleModel role, int firstResult, int maxResults) {
+        List<UserModel> 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 3716aff..7d793d7 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<UserModel> 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 2aa5933..e4b3656 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 4fa09df..8d00bb2 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -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.<String>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<UserRepresentation> 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<RoleRepresentation> 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<UserRepresentation> 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<RoleRepresentation> 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 0000000..c68366c
--- /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 <a href="mailto:antonio.ferreira@fiercely.pt">Antonio Ferreira</a>
+ *
+ */
+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.<String>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<UserRepresentation> 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<RoleRepresentation> 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/theme/base/admin/index.ftl b/themes/src/main/resources/theme/base/admin/index.ftl
index aebc488..397c4b0 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 @@
     <script src="${resourceUrl}/js/controllers/clients.js" type="text/javascript"></script>
     <script src="${resourceUrl}/js/controllers/users.js" type="text/javascript"></script>
     <script src="${resourceUrl}/js/controllers/groups.js" type="text/javascript"></script>
+    <script src="${resourceUrl}/js/controllers/roles.js" type="text/javascript"></script>
     <script src="${resourceUrl}/js/loaders.js" type="text/javascript"></script>
     <script src="${resourceUrl}/js/services.js" type="text/javascript"></script>
 
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 8848371..1620996 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 c650d00..4e6de53 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 0000000..185913a
--- /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 b084a4f..a9935f4 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 0000000..11cbbc6
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-role-users.html
@@ -0,0 +1,50 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+    <ol class="breadcrumb">
+        <li><a href="#/realms/{{realm.realm}}/roles">{{:: 'roles' | translate}}</a></li>
+        <li>{{role.name}}</li>
+    </ol>
+
+    <kc-tabs-role></kc-tabs-role>
+
+    <table class="table table-striped table-bordered">
+        <caption data-ng-show="users" class="hidden">{{:: 'table-of-role-members' | translate}}</caption>
+        <thead>
+         <tr>
+        <tr data-ng-show="searchLoaded && users.length > 0">
+            <th>{{:: 'username' | translate}}</th>
+            <th>{{:: 'last-name' | translate}}</th>
+            <th>{{:: 'first-name' | translate}}</th>
+            <th>{{:: 'email' | translate}}</th>
+            <th></th>
+        </tr>
+        </tr>
+        </thead>
+        <tfoot data-ng-show="users && (users.length >= query.max || query.first > 0)">
+        <tr>
+            <td colspan="7">
+                <div class="table-nav">
+                    <button data-ng-click="firstPage()" class="first" ng-disabled="query.first == 0">{{:: 'first-page' | translate}}</button>
+                    <button data-ng-click="previousPage()" class="prev" ng-disabled="query.first == 0">{{:: 'previous-page' | translate}}</button>
+                    <button data-ng-click="nextPage()" class="next" ng-disabled="users.length < query.max">{{:: 'next-page' | translate}}</button>
+                </div>
+            </td>
+        </tr>
+        </tfoot>
+        <tbody>
+        <tr ng-repeat="user in users">
+            <td><a href="#/realms/{{realm.realm}}/users/{{user.id}}">{{user.username}}</a></td>
+            <td>{{user.lastName}}</td>
+            <td>{{user.firstName}}</td>
+            <td>{{user.email}}</td>
+            <td class="kc-action-cell" kc-open="/realms/{{realm.realm}}/users/{{user.id}}">{{:: 'edit' | translate}}</td>
+        </tr>
+        <tr data-ng-show="!users || users.length == 0">
+            <td class="text-muted" data-ng-show="searchLoaded && users.length == 0 && lastSearch != null">{{:: 'no-role-members' | translate}}</td>
+            <td class="text-muted" data-ng-show="searchLoaded && users.length == 0 && lastSearch == null">{{:: 'no-role-members' | translate}}</td>
+        </tr>
+        </tbody>
+    </table>
+
+</div>
+
+<kc-menu></kc-menu>
\ 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 785c7e9..9c3c4a7 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 @@
             <a href="#/realms/{{realm.realm}}/roles/{{role.id}}/permissions">{{:: 'authz-permissions' | translate}}</a>
             <kc-tooltip>{{:: 'manage-permissions-role.tooltip' | translate}}</kc-tooltip>
         </li>
+        <li ng-class="{active: path[4] == 'users'}" data-ng-show="access.manageRealm && access.manageAuthorization">
+        <a href="#/realms/{{realm.realm}}/roles/{{role.id}}/users">{{:: 'authz-users' | translate}}</a></li>
     </ul>
 </div>
\ No newline at end of file
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 6476cf2..96b64d8 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