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 e4b3656503..5757ad3c0a 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 @@ -21,15 +21,20 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.RoleResource; import org.keycloak.admin.client.resource.RolesResource; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.util.AdminEventPaths; +import java.util.Collections; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Optional; import java.util.Set; import static org.junit.Assert.assertEquals; @@ -37,7 +42,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** - * * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc. */ public class ClientRolesTest extends AbstractClientTest { @@ -132,4 +136,48 @@ public class ClientRolesTest extends AbstractClientTest { assertFalse(rolesRsc.get("role-a").toRepresentation().isComposite()); assertEquals(0, rolesRsc.get("role-a").getRoleComposites().size()); } + + @Test + public void usersInRole() { + String clientID = clientRsc.toRepresentation().getId(); + + // create test role on client + String roleName = "test-role"; + RoleRepresentation role = makeRole(roleName); + rolesRsc.create(role); + assertTrue(hasRole(rolesRsc, roleName)); + List roleToAdd = Collections.singletonList(rolesRsc.get(roleName).toRepresentation()); + + //create users and assign test role + Set users = new HashSet<>(); + for (int i = 0; i < 10; i++) { + String userName = "user" + i; + UserRepresentation user = new UserRepresentation(); + user.setUsername(userName); + testRealmResource().users().create(user); + user = getFullUserRep(userName); + testRealmResource().users().get(user.getId()).roles().clientLevel(clientID).add(roleToAdd); + users.add(user); + } + + // check if users have test role assigned + RoleResource roleResource = rolesRsc.get(roleName); + Set usersInRole = roleResource.getRoleUserMembers(); + assertEquals(users.size(), usersInRole.size()); + for (UserRepresentation user : users) { + Optional result = usersInRole.stream().filter(u -> user.getUsername().equals(u.getUsername())).findAny(); + assertTrue(result.isPresent()); + } + + // pagination + Set usersInRole1 = roleResource.getRoleUserMembers(0, 5); + assertEquals(5, usersInRole1.size()); + Set usersInRole2 = roleResource.getRoleUserMembers(5, 10); + assertEquals(5, usersInRole2.size()); + for (UserRepresentation user : users) { + Optional result1 = usersInRole1.stream().filter(u -> user.getUsername().equals(u.getUsername())).findAny(); + Optional result2 = usersInRole2.stream().filter(u -> user.getUsername().equals(u.getUsername())).findAny(); + assertTrue((result1.isPresent() || result2.isPresent()) && !(result1.isPresent() && result2.isPresent())); + } + } } 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 2fcf6a95e5..1301f2a51a 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 @@ -976,6 +976,21 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'ClientRoleDetailCtrl' }) + .when('/realms/:realm/clients/:client/roles/:role/users', { + templateUrl : resourceUrl + '/partials/client-role-users.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + client : function(ClientLoader) { + return ClientLoader(); + }, + role : function(ClientRoleLoader) { + return ClientRoleLoader(); + } + }, + controller : 'ClientRoleMembersCtrl' + }) .when('/realms/:realm/clients/:client/mappers', { templateUrl : resourceUrl + '/partials/client-mappers.html', resolve : { diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js index 86d98599ad..d73bbf3405 100755 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js @@ -758,6 +758,51 @@ module.controller('ClientRoleDetailCtrl', function($scope, realm, client, role, }); +module.controller('ClientRoleMembersCtrl', function($scope, realm, client, role, ClientRoleMembership, Dialog, Notifications, $location) { + $scope.realm = realm; + $scope.page = 0; + $scope.role = role; + $scope.client = client; + + $scope.query = { + realm: realm.realm, + role: role.name, + client: client.id, + 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() { + $scope.searchLoaded = false; + + $scope.users = ClientRoleMembership.query($scope.query, function() { + console.log('search loaded'); + $scope.searchLoaded = true; + $scope.lastSearch = $scope.query.search; + }); + }; + + $scope.searchQuery(); +}); + module.controller('ClientImportCtrl', function($scope, $location, $upload, realm, serverInfo, Notifications) { $scope.realm = realm; 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 98e069d6cf..949e5f66de 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 @@ -1913,6 +1913,13 @@ module.factory('RoleMembership', function($resource) { }); }); +module.factory('ClientRoleMembership', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/clients/:client/roles/:role/users', { + realm : '@realm', + client : '@client', + 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/client-role-users.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-role-users.html new file mode 100644 index 0000000000..5349171328 --- /dev/null +++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-role-users.html @@ -0,0 +1,52 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{:: '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}}
+ +
+ + diff --git a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-role.html b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-role.html index 312f3ca624..20f05a9b3f 100755 --- a/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-role.html +++ b/themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client-role.html @@ -10,5 +10,8 @@ {{:: 'authz-permissions' | translate}} {{:: 'manage-permissions-role.tooltip' | translate}} +
  • + {{:: 'authz-users' | translate}} +
  • \ No newline at end of file