keycloak-aplcache
Changes
examples/providers/user-storage-jpa/src/main/java/org/keycloak/examples/storage/user/EjbExampleUserStorageProvider.java 10(+10 -0)
examples/providers/user-storage-simple/src/main/java/org/keycloak/examples/userstorage/writeable/PropertyFileUserStorageProvider.java 14(+14 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RoleResource.java 6(+6 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserCacheSession.java 11(+11 -0)
testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/UserPropertyFileStorage.java 1(+0 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/ClientRolesTest.java 1(+0 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmRolesTest.java 95(+95 -0)
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..b1dab0b 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,8 +18,10 @@ 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.Collections;
import java.util.List;
import java.util.Map;
@@ -118,6 +120,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)
+ {
+ return Collections.EMPTY_LIST;
+ }
+
+ /**
+ * 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)
+ {
+ return Collections.EMPTY_LIST;
+ }
+
+ /**
* 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..0d01c41 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();
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