Keycloak-11526 search and pagination for roles

This commit is contained in:
Axel Messinese 2019-09-23 15:55:27 +02:00 committed by Stian Thorgersen
parent 73eaa38357
commit b73553e305
22 changed files with 676 additions and 70 deletions

View file

@ -21,11 +21,13 @@ import org.keycloak.representations.idm.RoleRepresentation;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE; import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import java.util.List; import java.util.List;
@ -38,6 +40,78 @@ public interface RolesResource {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
List<RoleRepresentation> list(); List<RoleRepresentation> list();
/**
* @param briefRepresentation if false, return roles with their attributes
* @return A list containing all roles.
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
List<RoleRepresentation> list(@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation);
/**
* Get roles by pagination params.
* @param search max number of occurrences
* @param first index of the first element
* @param max max number of occurrences
* @return A list containing the slice of all roles.
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
List<RoleRepresentation> list(@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults);
/**
* Get roles by pagination params.
* @param first index of the first element
* @param max max number of occurrences
* @param briefRepresentation if false, return roles with their attributes
* @return A list containing the slice of all roles.
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
List<RoleRepresentation> list(@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults,
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation);
/**
* Get roles by pagination params.
* @param search max number of occurrences
* @param briefRepresentation if false, return roles with their attributes
* @return A list containing the slice of all roles.
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
List<RoleRepresentation> list(@QueryParam("search") @DefaultValue("") String search,
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation);
/**
* Get roles by pagination params.
* @param search max number of occurrences
* @param first index of the first element
* @param max max number of occurrences
* @return A list containing the slice of all roles.
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
List<RoleRepresentation> list(@QueryParam("search") @DefaultValue("") String search,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults);
/**
* Get roles by pagination params.
* @param search max number of occurrences
* @param first index of the first element
* @param max max number of occurrences
* @param briefRepresentation if false, return roles with their attributes
* @return A list containing the slice of all roles.
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
List<RoleRepresentation> list(@QueryParam("search") @DefaultValue("") String search,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults,
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation);
@POST @POST
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
void create(RoleRepresentation roleRepresentation); void create(RoleRepresentation roleRepresentation);

View file

@ -612,6 +612,16 @@ public class ClientAdapter implements ClientModel, CachedObject {
return cacheSession.getClientRoles(cachedRealm, this); return cacheSession.getClientRoles(cachedRealm, this);
} }
@Override
public Set<RoleModel> getRoles(Integer first, Integer max) {
return cacheSession.getClientRoles(cachedRealm, this, first, max);
}
@Override
public Set<RoleModel> searchForRoles(String search, Integer first, Integer max) {
return cacheSession.searchForClientRoles(cachedRealm, this, search, first, max);
}
@Override @Override
public int getNodeReRegistrationTimeout() { public int getNodeReRegistrationTimeout() {
if (isUpdated()) return updated.getNodeReRegistrationTimeout(); if (isUpdated()) return updated.getNodeReRegistrationTimeout();
@ -675,5 +685,4 @@ public class ClientAdapter implements ClientModel, CachedObject {
public int hashCode() { public int hashCode() {
return getId().hashCode(); return getId().hashCode();
} }
} }

View file

@ -974,6 +974,15 @@ public class RealmAdapter implements CachedRealmModel {
return cacheSession.getRealmRoles(this); return cacheSession.getRealmRoles(this);
} }
@Override
public Set<RoleModel> getRoles(Integer first, Integer max) {
return cacheSession.getRealmRoles(this, first, max);
}
@Override
public Set<RoleModel> searchForRoles(String search, Integer first, Integer max) {
return cacheSession.searchForRoles(this, search, first, max);
}
@Override @Override
public RoleModel addRole(String name) { public RoleModel addRole(String name) {

View file

@ -669,6 +669,27 @@ public class RealmCacheSession implements CacheRealmProvider {
return list; return list;
} }
@Override
public Set<RoleModel> getRealmRoles(RealmModel realm, Integer first, Integer max) {
return getRealmDelegate().getRealmRoles(realm, first, max);
}
@Override
public Set<RoleModel> getClientRoles(RealmModel realm, ClientModel client, Integer first, Integer max) {
return getRealmDelegate().getClientRoles(realm, client, first, max);
}
@Override
public Set<RoleModel> searchForClientRoles(RealmModel realm, ClientModel client, String search, Integer first,
Integer max) {
return getRealmDelegate().searchForClientRoles(realm, client, search, first, max);
}
@Override
public Set<RoleModel> searchForRoles(RealmModel realm, String search, Integer first, Integer max) {
return getRealmDelegate().searchForRoles(realm, search, first, max);
}
@Override @Override
public RoleModel addClientRole(RealmModel realm, ClientModel client, String name) { public RoleModel addClientRole(RealmModel realm, ClientModel client, String name) {
return addClientRole(realm, client, KeycloakModelUtils.generateId(), name); return addClientRole(realm, client, KeycloakModelUtils.generateId(), name);
@ -1196,4 +1217,5 @@ public class RealmCacheSession implements CacheRealmProvider {
public void decreaseRemainingCount(RealmModel realm, ClientInitialAccessModel clientInitialAccess) { public void decreaseRemainingCount(RealmModel realm, ClientInitialAccessModel clientInitialAccess) {
getRealmDelegate().decreaseRemainingCount(realm, clientInitialAccess); getRealmDelegate().decreaseRemainingCount(realm, clientInitialAccess);
} }
} }

View file

@ -664,6 +664,16 @@ public class ClientAdapter implements ClientModel, JpaModel<ClientEntity> {
return session.realms().getClientRoles(realm, this); return session.realms().getClientRoles(realm, this);
} }
@Override
public Set<RoleModel> getRoles(Integer first, Integer max) {
return session.realms().getClientRoles(realm, this, first, max);
}
@Override
public Set<RoleModel> searchForRoles(String search, Integer first, Integer max) {
return session.realms().searchForClientRoles(realm, this, search, first, max);
}
@Override @Override
public boolean hasScope(RoleModel role) { public boolean hasScope(RoleModel role) {
if (isFullScopeAllowed()) return true; if (isFullScopeAllowed()) return true;

View file

@ -46,6 +46,7 @@ import javax.persistence.TypedQuery;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
@ -294,6 +295,67 @@ public class JpaRealmProvider implements RealmProvider {
return list; return list;
} }
@Override
public Set<RoleModel> getRealmRoles(RealmModel realm, Integer first, Integer max) {
TypedQuery<RoleEntity> query = em.createNamedQuery("getRealmRoles", RoleEntity.class);
query.setParameter("realm", realm.getId());
return getRoles(query, realm, first, max);
}
@Override
public Set<RoleModel> getClientRoles(RealmModel realm, ClientModel client, Integer first, Integer max) {
TypedQuery<RoleEntity> query = em.createNamedQuery("getClientRoles", RoleEntity.class);
query.setParameter("client", client.getId());
return getRoles(query, realm, first, max);
}
protected Set<RoleModel> getRoles(TypedQuery<RoleEntity> query, RealmModel realm, Integer first, Integer max) {
if(Objects.nonNull(first) && Objects.nonNull(max)
&& first >= 0 && max >= 0) {
query= query.setFirstResult(first).setMaxResults(max);
}
List<RoleEntity> results = query.getResultList();
return results.stream()
.map(role -> new RoleAdapter(session, realm, em, role))
.collect(Collectors.collectingAndThen(
Collectors.toCollection(LinkedHashSet::new), Collections::unmodifiableSet));
}
@Override
public Set<RoleModel> searchForClientRoles(RealmModel realm, ClientModel client, String search, Integer first, Integer max) {
TypedQuery<RoleEntity> query = em.createNamedQuery("searchForClientRoles", RoleEntity.class);
query.setParameter("client", client.getId());
return searchForRoles(query, realm, search, first, max);
}
@Override
public Set<RoleModel> searchForRoles(RealmModel realm, String search, Integer first, Integer max) {
TypedQuery<RoleEntity> query = em.createNamedQuery("searchForRealmRoles", RoleEntity.class);
query.setParameter("realm", realm.getId());
return searchForRoles(query, realm, search, first, max);
}
protected Set<RoleModel> searchForRoles(TypedQuery<RoleEntity> query, RealmModel realm, String search, Integer first, Integer max) {
query.setParameter("search", "%" + search.trim().toLowerCase() + "%");
if(Objects.nonNull(first) && Objects.nonNull(max)
&& first >= 0 && max >= 0) {
query= query.setFirstResult(first).setMaxResults(max);
}
List<RoleEntity> results = query.getResultList();
return results.stream()
.map(role -> new RoleAdapter(session, realm, em, role))
.collect(Collectors.collectingAndThen(
Collectors.toSet(), Collections::unmodifiableSet));
}
@Override @Override
public boolean removeRole(RealmModel realm, RoleModel role) { public boolean removeRole(RealmModel realm, RoleModel role) {
session.users().preRemove(realm, role); session.users().preRemove(realm, role);
@ -778,4 +840,5 @@ public class JpaRealmProvider implements RealmProvider {
model.setTimestamp(entity.getTimestamp()); model.setTimestamp(entity.getTimestamp());
return model; return model;
} }
} }

View file

@ -896,6 +896,16 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
return session.realms().getRealmRoles(this); return session.realms().getRealmRoles(this);
} }
@Override
public Set<RoleModel> getRoles(Integer first, Integer max) {
return session.realms().getRealmRoles(this, first, max);
}
@Override
public Set<RoleModel> searchForRoles(String search, Integer first, Integer max) {
return session.realms().searchForRoles(this, search, first, max);
}
@Override @Override
public RoleModel getRoleById(String id) { public RoleModel getRoleById(String id) {
return session.realms().getRoleById(id, this); return session.realms().getRoleById(id, this);
@ -2259,5 +2269,4 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
if (c == null) return null; if (c == null) return null;
return entityToModel(c); return entityToModel(c);
} }
} }

View file

@ -55,14 +55,16 @@ import java.util.Set;
@UniqueConstraint(columnNames = { "NAME", "CLIENT_REALM_CONSTRAINT" }) @UniqueConstraint(columnNames = { "NAME", "CLIENT_REALM_CONSTRAINT" })
}) })
@NamedQueries({ @NamedQueries({
@NamedQuery(name="getClientRoles", query="select role from RoleEntity role where role.client = :client"), @NamedQuery(name="getClientRoles", query="select role from RoleEntity role where role.client.id = :client order by role.name"),
@NamedQuery(name="getClientRoleIds", query="select role.id from RoleEntity role where role.client.id = :client"), @NamedQuery(name="getClientRoleIds", query="select role.id from RoleEntity role where role.client.id = :client"),
@NamedQuery(name="getClientRoleByName", query="select role from RoleEntity role where role.name = :name and role.client = :client"), @NamedQuery(name="getClientRoleByName", query="select role from RoleEntity role where role.name = :name and role.client = :client"),
@NamedQuery(name="getClientRoleIdByName", query="select role.id from RoleEntity role where role.name = :name and role.client.id = :client"), @NamedQuery(name="getClientRoleIdByName", query="select role.id from RoleEntity role where role.name = :name and role.client.id = :client"),
@NamedQuery(name="getRealmRoles", query="select role from RoleEntity role where role.clientRole = false and role.realm = :realm"), @NamedQuery(name="searchForClientRoles", query="select role from RoleEntity role where role.client.id = :client and ( lower(role.name) like :search or lower(role.description) like :search ) order by role.name"),
@NamedQuery(name="getRealmRoles", query="select role from RoleEntity role where role.clientRole = false and role.realm.id = :realm order by role.name"),
@NamedQuery(name="getRealmRoleIds", query="select role.id from RoleEntity role where role.clientRole = false and role.realm.id = :realm"), @NamedQuery(name="getRealmRoleIds", query="select role.id from RoleEntity role where role.clientRole = false and role.realm.id = :realm"),
@NamedQuery(name="getRealmRoleByName", query="select role from RoleEntity role where role.clientRole = false and role.name = :name and role.realm = :realm"), @NamedQuery(name="getRealmRoleByName", query="select role from RoleEntity role where role.clientRole = false and role.name = :name and role.realm = :realm"),
@NamedQuery(name="getRealmRoleIdByName", query="select role.id from RoleEntity role where role.clientRole = false and role.name = :name and role.realm.id = :realm") @NamedQuery(name="getRealmRoleIdByName", query="select role.id from RoleEntity role where role.clientRole = false and role.name = :name and role.realm.id = :realm"),
@NamedQuery(name="searchForRealmRoles", query="select role from RoleEntity role where role.clientRole = false and role.realm.id = :realm and ( lower(role.name) like :search or lower(role.description) like :search ) order by role.name"),
}) })
public class RoleEntity { public class RoleEntity {

View file

@ -56,6 +56,16 @@ public abstract class UnsupportedOperationsClientStorageAdapter implements Clien
return Collections.EMPTY_SET; return Collections.EMPTY_SET;
} }
@Override
public final Set<RoleModel> getRoles(Integer first, Integer max) {
return Collections.EMPTY_SET;
}
@Override
public final Set<RoleModel> searchForRoles(String search, Integer first, Integer max) {
return Collections.EMPTY_SET;
}
@Override @Override
public final List<String> getDefaultRoles() { public final List<String> getDefaultRoles() {
return Collections.EMPTY_LIST; return Collections.EMPTY_LIST;

View file

@ -62,7 +62,6 @@ public interface RealmProvider extends Provider, ClientProvider {
void addTopLevelGroup(RealmModel realm, GroupModel subGroup); void addTopLevelGroup(RealmModel realm, GroupModel subGroup);
RoleModel addRealmRole(RealmModel realm, String name); RoleModel addRealmRole(RealmModel realm, String name);
RoleModel addRealmRole(RealmModel realm, String id, String name); RoleModel addRealmRole(RealmModel realm, String id, String name);
@ -71,6 +70,15 @@ public interface RealmProvider extends Provider, ClientProvider {
Set<RoleModel> getRealmRoles(RealmModel realm); Set<RoleModel> getRealmRoles(RealmModel realm);
Set<RoleModel> getRealmRoles(RealmModel realm, Integer first, Integer max);
Set<RoleModel> getClientRoles(RealmModel realm, ClientModel client, Integer first, Integer max);
Set<RoleModel> searchForClientRoles(RealmModel realm, ClientModel client, String search, Integer first,
Integer max);
Set<RoleModel> searchForRoles(RealmModel realm, String search, Integer first, Integer max);
boolean removeRole(RealmModel realm, RoleModel role); boolean removeRole(RealmModel realm, RoleModel role);
RoleModel getRoleById(String id, RealmModel realm); RoleModel getRoleById(String id, RealmModel realm);
@ -91,4 +99,5 @@ public interface RealmProvider extends Provider, ClientProvider {
List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm); List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm);
void removeExpiredClientInitialAccess(); void removeExpiredClientInitialAccess();
void decreaseRemainingCount(RealmModel realm, ClientInitialAccessModel clientInitialAccess); // Separate provider method to ensure we decrease remainingCount atomically instead of doing classic update void decreaseRemainingCount(RealmModel realm, ClientInitialAccessModel clientInitialAccess); // Separate provider method to ensure we decrease remainingCount atomically instead of doing classic update
} }

View file

@ -45,6 +45,10 @@ public interface RoleContainerModel {
Set<RoleModel> getRoles(); Set<RoleModel> getRoles();
Set<RoleModel> getRoles(Integer firstResult, Integer maxResults);
Set<RoleModel> searchForRoles(String search, Integer first, Integer max);
List<String> getDefaultRoles(); List<String> getDefaultRoles();
void addDefaultRole(String name); void addDefaultRole(String name);
@ -52,4 +56,5 @@ public interface RoleContainerModel {
void updateDefaultRoles(String... defaultRoles); void updateDefaultRoles(String... defaultRoles);
void removeDefaultRoles(String... defaultRoles); void removeDefaultRoles(String... defaultRoles);
} }

View file

@ -17,6 +17,7 @@
package org.keycloak.services.resources.admin; package org.keycloak.services.resources.admin;
import org.apache.commons.lang.StringUtils;
import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.annotations.cache.NoCache;
import javax.ws.rs.NotFoundException; import javax.ws.rs.NotFoundException;
import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.OperationType;
@ -55,7 +56,9 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -92,13 +95,29 @@ public class RoleContainerResource extends RoleResource {
@GET @GET
@NoCache @NoCache
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public List<RoleRepresentation> getRoles() { public List<RoleRepresentation> getRoles(@QueryParam("search") @DefaultValue("") String search,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults,
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) {
auth.roles().requireList(roleContainer); auth.roles().requireList(roleContainer);
Set<RoleModel> roleModels = roleContainer.getRoles(); Set<RoleModel> roleModels = new HashSet<RoleModel>();
if(search != null && search.trim().length() > 0) {
roleModels = roleContainer.searchForRoles(search, firstResult, maxResults);
} else if (!Objects.isNull(firstResult) && !Objects.isNull(maxResults)) {
roleModels = roleContainer.getRoles(firstResult, maxResults);
} else {
roleModels = roleContainer.getRoles();
}
List<RoleRepresentation> roles = new ArrayList<RoleRepresentation>(); List<RoleRepresentation> roles = new ArrayList<RoleRepresentation>();
for (RoleModel roleModel : roleModels) { for (RoleModel roleModel : roleModels) {
if(briefRepresentation) {
roles.add(ModelToRepresentation.toBriefRepresentation(roleModel)); roles.add(ModelToRepresentation.toBriefRepresentation(roleModel));
} else {
roles.add(ModelToRepresentation.toRepresentation(roleModel));
}
} }
return roles; return roles;
} }

View file

@ -30,15 +30,19 @@ import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.util.AdminEventPaths; import org.keycloak.testsuite.util.AdminEventPaths;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
/** /**
@ -180,4 +184,129 @@ public class ClientRolesTest extends AbstractClientTest {
assertTrue((result1.isPresent() || result2.isPresent()) && !(result1.isPresent() && result2.isPresent())); assertTrue((result1.isPresent() || result2.isPresent()) && !(result1.isPresent() && result2.isPresent()));
} }
} }
@Test
public void testSearchForRoles() {
for(int i = 0; i<15; i++) {
String roleName = "role"+i;
RoleRepresentation role = makeRole(roleName);
rolesRsc.create(role);
assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleName), role, ResourceType.CLIENT_ROLE);
}
String roleNameA = "abcdef";
RoleRepresentation roleA = makeRole(roleNameA);
rolesRsc.create(roleA);
assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleNameA), roleA, ResourceType.CLIENT_ROLE);
String roleNameB = "defghi";
RoleRepresentation roleB = makeRole(roleNameB);
rolesRsc.create(roleB);
assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleNameB), roleB, ResourceType.CLIENT_ROLE);
List<RoleRepresentation> resultSearch = rolesRsc.list("def", -1, -1);
assertEquals(2,resultSearch.size());
List<RoleRepresentation> resultSearch2 = rolesRsc.list("role", -1, -1);
assertEquals(15,resultSearch2.size());
List<RoleRepresentation> resultSearchPagination = rolesRsc.list("role", 1, 5);
assertEquals(5,resultSearchPagination.size());
}
@Test
public void testPaginationRoles() {
for(int i = 0; i<15; i++) {
String roleName = "role"+i;
RoleRepresentation role = makeRole(roleName);
rolesRsc.create(role);
assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleName), role, ResourceType.CLIENT_ROLE);
}
List<RoleRepresentation> resultSearchWithoutPagination = rolesRsc.list();
assertEquals(15,resultSearchWithoutPagination.size());
List<RoleRepresentation> resultSearchPagination = rolesRsc.list(1, 5);
assertEquals(5,resultSearchPagination.size());
List<RoleRepresentation> resultSearchPaginationIncoherentParams = rolesRsc.list(1, null);
assertTrue(resultSearchPaginationIncoherentParams.size() >= 15);
}
@Test
public void testPaginationRolesCache() {
for(int i = 0; i<5; i++) {
String roleName = "paginaterole"+i;
RoleRepresentation role = makeRole(roleName);
rolesRsc.create(role);
assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleName), role, ResourceType.CLIENT_ROLE);
}
List<RoleRepresentation> resultBeforeAddingRoleToTestCache = rolesRsc.list(1, 1000);
// after a first call which init the cache, we add a new role to see if the result change
RoleRepresentation role = makeRole("anewrole");
rolesRsc.create(role);
assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,"anewrole"), role, ResourceType.CLIENT_ROLE);
List<RoleRepresentation> resultafterAddingRoleToTestCache = rolesRsc.list(1, 1000);
assertEquals(resultBeforeAddingRoleToTestCache.size()+1, resultafterAddingRoleToTestCache.size());
}
@Test
public void getRolesWithFullRepresentation() {
for(int i = 0; i<5; i++) {
String roleName = "attributesrole"+i;
RoleRepresentation role = makeRole(roleName);
Map<String, List<String>> attributes = new HashMap<String, List<String>>();
attributes.put("attribute1", Arrays.asList("value1","value2"));
role.setAttributes(attributes);
rolesRsc.create(role);
assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleName), role, ResourceType.CLIENT_ROLE);
// we have to update the role to set the attributes because
// the add role endpoint only care about name and description
RoleResource roleToUpdate = rolesRsc.get(roleName);
role.setId(roleToUpdate.toRepresentation().getId());
roleToUpdate.update(role);
assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleName), role, ResourceType.CLIENT_ROLE);
}
List<RoleRepresentation> roles = rolesRsc.list(false);
assertTrue(roles.get(0).getAttributes().containsKey("attribute1"));
}
@Test
public void getRolesWithBriefRepresentation() {
for(int i = 0; i<5; i++) {
String roleName = "attributesrole"+i;
RoleRepresentation role = makeRole(roleName);
Map<String, List<String>> attributes = new HashMap<String, List<String>>();
attributes.put("attribute1", Arrays.asList("value1","value2"));
role.setAttributes(attributes);
rolesRsc.create(role);
assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleName), role, ResourceType.CLIENT_ROLE);
// we have to update the role to set the attributes because
// the add role endpoint only care about name and description
RoleResource roleToUpdate = rolesRsc.get(roleName);
role.setId(roleToUpdate.toRepresentation().getId());
roleToUpdate.update(role);
assertAdminEvents.assertEvent(getRealmId(), OperationType.UPDATE, AdminEventPaths.clientRoleResourcePath(clientDbId,roleName), role, ResourceType.CLIENT_ROLE);
}
List<RoleRepresentation> roles = rolesRsc.list();
assertNull(roles.get(0).getAttributes());
}
} }

View file

@ -19,6 +19,7 @@ package org.keycloak.testsuite.admin.realm;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.RoleResource; import org.keycloak.admin.client.resource.RoleResource;
import org.keycloak.admin.client.resource.RolesResource; import org.keycloak.admin.client.resource.RolesResource;
import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UserResource;
@ -38,6 +39,7 @@ import org.keycloak.testsuite.util.RoleBuilder;
import javax.ws.rs.NotFoundException; import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@ -53,7 +55,11 @@ import static org.hamcrest.Matchers.is;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
@ -132,6 +138,12 @@ public class RealmRolesTest extends AbstractAdminTest {
} }
private RoleRepresentation makeRole(String name) {
RoleRepresentation role = new RoleRepresentation();
role.setName(name);
return role;
}
@Test @Test
public void getRole() { public void getRole() {
RoleRepresentation role = resource.get("role-a").toRepresentation(); RoleRepresentation role = resource.get("role-a").toRepresentation();
@ -344,4 +356,131 @@ public class RealmRolesTest extends AbstractAdminTest {
assertThat(expectedMembers, containsInAnyOrder("test-role-member", "test-role-member2")); assertThat(expectedMembers, containsInAnyOrder("test-role-member", "test-role-member2"));
} }
@Test
public void testSearchForRoles() {
for(int i = 0; i<15; i++) {
String roleName = "testrole"+i;
RoleRepresentation role = makeRole(roleName);
resource.create(role);
assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.roleResourcePath(roleName), role, ResourceType.REALM_ROLE);
}
String roleNameA = "abcdef";
RoleRepresentation roleA = makeRole(roleNameA);
resource.create(roleA);
assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.roleResourcePath(roleNameA), roleA, ResourceType.REALM_ROLE);
String roleNameB = "defghi";
RoleRepresentation roleB = makeRole(roleNameB);
resource.create(roleB);
assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.roleResourcePath(roleNameB), roleB, ResourceType.REALM_ROLE);
List<RoleRepresentation> resultSearch = resource.list("def", -1, -1);
assertEquals(2,resultSearch.size());
List<RoleRepresentation> resultSearch2 = resource.list("testrole", -1, -1);
assertEquals(15,resultSearch2.size());
List<RoleRepresentation> resultSearchPagination = resource.list("testrole", 1, 5);
assertEquals(5,resultSearchPagination.size());
}
@Test
public void testPaginationRoles() {
for(int i = 0; i<15; i++) {
String roleName = "role"+i;
RoleRepresentation role = makeRole(roleName);
resource.create(role);
assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.roleResourcePath(roleName), role, ResourceType.REALM_ROLE);
}
List<RoleRepresentation> resultSearchPagination = resource.list(1, 5);
assertEquals(5,resultSearchPagination.size());
List<RoleRepresentation> resultSearchPagination2 = resource.list(5, 5);
assertEquals(5,resultSearchPagination2.size());
List<RoleRepresentation> resultSearchPagination3 = resource.list(1, 5);
assertEquals(5,resultSearchPagination3.size());
List<RoleRepresentation> resultSearchPaginationIncoherentParams = resource.list(1, null);
assertTrue(resultSearchPaginationIncoherentParams.size() > 15);
}
@Test
public void testPaginationRolesCache() {
for(int i = 0; i<5; i++) {
String roleName = "paginaterole"+i;
RoleRepresentation role = makeRole(roleName);
resource.create(role);
assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.roleResourcePath(roleName), role, ResourceType.REALM_ROLE);
}
List<RoleRepresentation> resultBeforeAddingRoleToTestCache = resource.list(1, 1000);
// after a first call which init the cache, we add a new role to see if the result change
RoleRepresentation role = makeRole("anewrole");
resource.create(role);
assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.roleResourcePath("anewrole"), role, ResourceType.REALM_ROLE);
List<RoleRepresentation> resultafterAddingRoleToTestCache = resource.list(1, 1000);
assertEquals(resultBeforeAddingRoleToTestCache.size()+1, resultafterAddingRoleToTestCache.size());
}
@Test
public void getRolesWithFullRepresentation() {
for(int i = 0; i<5; i++) {
String roleName = "attributesrole"+i;
RoleRepresentation role = makeRole(roleName);
Map<String, List<String>> attributes = new HashMap<String, List<String>>();
attributes.put("attribute1", Arrays.asList("value1","value2"));
role.setAttributes(attributes);
resource.create(role);
assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.roleResourcePath(roleName), role, ResourceType.REALM_ROLE);
// we have to update the role to set the attributes because
// the add role endpoint only care about name and description
RoleResource roleToUpdate = resource.get(roleName);
role.setId(roleToUpdate.toRepresentation().getId());
roleToUpdate.update(role);
assertAdminEvents.assertEvent(realmId, OperationType.UPDATE, AdminEventPaths.roleResourcePath(roleName), role, ResourceType.REALM_ROLE);
}
List<RoleRepresentation> roles = resource.list("attributesrole", false);
assertTrue(roles.get(0).getAttributes().containsKey("attribute1"));
}
@Test
public void getRolesWithBriefRepresentation() {
for(int i = 0; i<5; i++) {
String roleName = "attributesrolebrief"+i;
RoleRepresentation role = makeRole(roleName);
Map<String, List<String>> attributes = new HashMap<String, List<String>>();
attributes.put("attribute1", Arrays.asList("value1","value2"));
role.setAttributes(attributes);
resource.create(role);
assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.roleResourcePath(roleName), role, ResourceType.REALM_ROLE);
// we have to update the role to set the attributes because
// the add role endpoint only care about name and description
RoleResource roleToUpdate = resource.get(roleName);
role.setId(roleToUpdate.toRepresentation().getId());
roleToUpdate.update(role);
assertAdminEvents.assertEvent(realmId, OperationType.UPDATE, AdminEventPaths.roleResourcePath(roleName), role, ResourceType.REALM_ROLE);
}
List<RoleRepresentation> roles = resource.list("attributesrolebrief", true);
assertNull(roles.get(0).getAttributes());
}
} }

View file

@ -1255,6 +1255,7 @@ membership.available-groups.tooltip=Groups a user can join. Select a group and c
table-of-realm-users=Table of Realm Users table-of-realm-users=Table of Realm Users
view-all-users=View all users view-all-users=View all users
view-all-groups=View all groups view-all-groups=View all groups
view-all-roles=View all roles
unlock-users=Unlock users unlock-users=Unlock users
no-users-available=No users available no-users-available=No users available
users.instruction=Please enter a search, or click on view all users users.instruction=Please enter a search, or click on view all users

View file

@ -817,9 +817,6 @@ module.config([ '$routeProvider', function($routeProvider) {
resolve : { resolve : {
realm : function(RealmLoader) { realm : function(RealmLoader) {
return RealmLoader(); return RealmLoader();
},
roles : function(RoleListLoader) {
return RoleListLoader();
} }
}, },
controller : 'RoleListCtrl' controller : 'RoleListCtrl'
@ -1355,9 +1352,6 @@ module.config([ '$routeProvider', function($routeProvider) {
}, },
client : function(ClientLoader) { client : function(ClientLoader) {
return ClientLoader(); return ClientLoader();
},
roles : function(ClientRoleListLoader) {
return ClientRoleListLoader();
} }
}, },
controller : 'ClientRoleListCtrl' controller : 'ClientRoleListCtrl'

View file

@ -18,11 +18,54 @@ module.controller('ClientTabCtrl', function(Dialog, $scope, Current, Notificatio
}; };
}); });
module.controller('ClientRoleListCtrl', function($scope, $location, realm, client, roles, $route, RoleById, Notifications, Dialog) { module.controller('ClientRoleListCtrl', function($scope, $route, realm, client, ClientRoleList, RoleById, Notifications, Dialog) {
$scope.realm = realm; $scope.realm = realm;
$scope.roles = roles; $scope.roles = [];
$scope.client = client; $scope.client = client;
$scope.query = {
realm: realm.realm,
client: $scope.client.id,
search : null,
max : 20,
first : 0
}
$scope.$watch('query.search', function (newVal, oldVal) {
if($scope.query.search && $scope.query.search.length >= 3) {
$scope.firstPage();
}
}, true);
$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.roles = ClientRoleList.query($scope.query, function() {
$scope.searchLoaded = true;
$scope.lastSearch = $scope.query.search;
});
};
$scope.searchQuery();
$scope.removeRole = function(role) { $scope.removeRole = function(role) {
Dialog.confirmDelete(role.name, 'role', function() { Dialog.confirmDelete(role.name, 'role', function() {
RoleById.remove({ RoleById.remove({
@ -34,12 +77,6 @@ module.controller('ClientRoleListCtrl', function($scope, $location, realm, clien
}); });
}); });
}; };
$scope.$watch(function() {
return $location.path();
}, function() {
$scope.path = $location.path().substring(1).split("/");
});
}); });
module.controller('ClientCredentialsCtrl', function($scope, $location, realm, client, clientAuthenticatorProviders, clientConfigProperties, Client, ClientRegistrationAccessToken, Notifications) { module.controller('ClientCredentialsCtrl', function($scope, $location, realm, client, clientAuthenticatorProviders, clientConfigProperties, Client, ClientRegistrationAccessToken, Notifications) {

View file

@ -1464,22 +1464,52 @@ module.controller('RoleTabCtrl', function(Dialog, $scope, Current, Notifications
}); });
module.controller('RoleListCtrl', function($scope, $route, Dialog, Notifications, realm, roles, RoleById, filterFilter) { module.controller('RoleListCtrl', function($scope, $route, Dialog, Notifications, realm, RoleList, RoleById, filterFilter) {
$scope.realm = realm; $scope.realm = realm;
$scope.roles = roles; $scope.roles = [];
$scope.currentPage = 1;
$scope.currentPageInput = 1;
$scope.pageSize = 20;
$scope.numberOfPages = Math.ceil($scope.roles.length/$scope.pageSize);
$scope.$watch('searchQuery', function (newVal, oldVal) { $scope.query = {
$scope.filtered = filterFilter($scope.roles, {name: newVal}); realm: realm.realm,
$scope.totalItems = $scope.filtered.length; search : null,
$scope.numberOfPages = Math.ceil($scope.totalItems/$scope.pageSize); max : 20,
$scope.currentPage = 1; first : 0
$scope.currentPageInput = 1; }
$scope.$watch('query.search', function (newVal, oldVal) {
if($scope.query.search && $scope.query.search.length >= 3) {
$scope.firstPage();
}
}, true); }, true);
$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.roles = RoleList.query($scope.query, function() {
$scope.searchLoaded = true;
$scope.lastSearch = $scope.query.search;
});
};
$scope.searchQuery();
$scope.removeRole = function (role) { $scope.removeRole = function (role) {
Dialog.confirmDelete(role.name, 'role', function () { Dialog.confirmDelete(role.name, 'role', function () {
RoleById.remove({ RoleById.remove({

View file

@ -304,17 +304,6 @@ module.factory('ClientOptionalClientScopesLoader', function(Loader, ClientOption
}); });
}); });
module.factory('ClientRoleListLoader', function(Loader, ClientRole, $route, $q) {
return Loader.query(ClientRole, function() {
return {
realm : $route.current.params.realm,
client : $route.current.params.client
}
});
});
module.factory('ClientLoader', function(Loader, Client, $route, $q) { module.factory('ClientLoader', function(Loader, Client, $route, $q) {
return Loader.get(Client, function() { return Loader.get(Client, function() {
return { return {

View file

@ -2002,6 +2002,12 @@ module.factory('GroupMembership', function($resource) {
}); });
}); });
module.factory('RoleList', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/roles', {
realm : '@realm'
});
});
module.factory('RoleMembership', function($resource) { module.factory('RoleMembership', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/roles/:role/users', { return $resource(authUrl + '/admin/realms/:realm/roles/:role/users', {
realm : '@realm', realm : '@realm',
@ -2009,6 +2015,13 @@ module.factory('RoleMembership', function($resource) {
}); });
}); });
module.factory('ClientRoleList', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/clients/:client/roles', {
realm : '@realm',
client : '@client'
});
});
module.factory('ClientRoleMembership', function($resource) { module.factory('ClientRoleMembership', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/clients/:client/roles/:role/users', { return $resource(authUrl + '/admin/realms/:realm/clients/:client/roles/:role/users', {
realm : '@realm', realm : '@realm',

View file

@ -10,10 +10,21 @@
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
<th class="kc-table-actions" colspan="5" data-ng-show="client.access.configure"> <th class="kc-table-actions" colspan="5">
<div class="pull-right"> <div class="form-inline">
<div class="form-group">
<div class="input-group">
<input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="query.search" ng-model-options="{debounce: 500}" class="form-control search">
<div class="input-group-addon">
<i class="fa fa-search" type="submit" data-ng-click="firstPage()"></i>
</div>
</div>
</div>
<button id="viewAllRoles" class="btn btn-default" ng-click="query.search = null; firstPage()">{{:: 'view-all-roles' | translate}}</button>
<div class="pull-right" data-ng-show="access.manageRealm">
<a class="btn btn-default" href="#/create/role/{{realm.realm}}/clients/{{client.id}}">{{:: 'add-role' | translate}}</a> <a class="btn btn-default" href="#/create/role/{{realm.realm}}/clients/{{client.id}}">{{:: 'add-role' | translate}}</a>
</div> </div>
</div>
</th> </th>
</tr> </tr>
<tr data-ng-hide="!roles || roles.length == 0"> <tr data-ng-hide="!roles || roles.length == 0">
@ -31,10 +42,22 @@
<td class="kc-action-cell" kc-open="/realms/{{realm.realm}}/clients/{{client.id}}/roles/{{role.id}}">{{:: 'edit' | translate}}</td> <td class="kc-action-cell" kc-open="/realms/{{realm.realm}}/clients/{{client.id}}/roles/{{role.id}}">{{:: 'edit' | translate}}</td>
<td class="kc-action-cell" data-ng-show="client.access.configure" data-ng-click="removeRole(role)">{{:: 'delete' | translate}}</td> <td class="kc-action-cell" data-ng-show="client.access.configure" data-ng-click="removeRole(role)">{{:: 'delete' | translate}}</td>
</tr> </tr>
<tr data-ng-show="!roles || roles.length == 0"> <tr data-ng-show="(roles | filter:{name: query.search}).length == 0">
<td>{{:: 'no-client-roles-available' | translate}}</td> <td class="text-muted" colspan="4" data-ng-show="searchLoaded && roles.length == 0 && lastSearch != null">{{:: 'no-results' | translate}}</td>
<td class="text-muted" colspan="4" data-ng-show="searchLoaded && roles.length == 0 && lastSearch == null">{{:: 'no-client-roles-available' | translate}}</td>
</tr> </tr>
</tbody> </tbody>
<tfoot data-ng-show="roles && (roles.length >= query.max || query.first > 0)">
<tr>
<td colspan="5">
<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="roles.length < query.max">{{:: 'next-page' | translate}}</button>
</div>
</td>
</tr>
</tfoot>
</table> </table>
</div> </div>

View file

@ -6,20 +6,20 @@
<li><a href="#/realms/{{realm.realm}}/default-roles">{{:: 'default-roles' | translate}}</a></li> <li><a href="#/realms/{{realm.realm}}/default-roles">{{:: 'default-roles' | translate}}</a></li>
</ul> </ul>
<table class="datatable table table-striped table-bordered dataTable no-footer"> <table class="table table-striped table-bordered">
<thead> <thead>
<tr> <tr>
<th class="kc-table-actions" colspan="5"> <th class="kc-table-actions" colspan="5">
<div class="form-inline"> <div class="form-inline">
<div class="form-group"> <div class="form-group">
<div class="input-group"> <div class="input-group">
<input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="searchQuery" class="form-control search" onkeyup="if (event.keyCode === 13){$(this).next('I').click(); }"> <input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="query.search" ng-model-options="{debounce: 500}" class="form-control search">
<div class="input-group-addon"> <div class="input-group-addon">
<i class="fa fa-search" type="submit"></i> <i class="fa fa-search" type="submit" data-ng-click="firstPage()"></i>
</div> </div>
</div> </div>
</div> </div>
<button id="viewAllRoles" class="btn btn-default" ng-click="query.search = null; firstPage()">{{:: 'view-all-roles' | translate}}</button>
<div class="pull-right" data-ng-show="access.manageRealm"> <div class="pull-right" data-ng-show="access.manageRealm">
<a id="createRole" class="btn btn-default" href="#/create/role/{{realm.realm}}">{{:: 'add-role' | translate}}</a> <a id="createRole" class="btn btn-default" href="#/create/role/{{realm.realm}}">{{:: 'add-role' | translate}}</a>
</div> </div>
@ -34,20 +34,30 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="role in roles| filter:{name: searchQuery} | orderBy:'name'| startFrom:(currentPage - 1) * pageSize | limitTo:pageSize"> <tr ng-repeat="role in roles">
<td><a href="#/realms/{{realm.realm}}/roles/{{role.id}}">{{role.name}}</a></td> <td><a href="#/realms/{{realm.realm}}/roles/{{role.id}}">{{role.name}}</a></td>
<td translate="{{role.composite}}"></td> <td translate="{{role.composite}}"></td>
<td>{{role.description}}</td> <td>{{role.description}}</td>
<td class="kc-action-cell" kc-open="/realms/{{realm.realm}}/roles/{{role.id}}">{{:: 'edit' | translate}}</td> <td class="kc-action-cell" kc-open="/realms/{{realm.realm}}/roles/{{role.id}}">{{:: 'edit' | translate}}</td>
<td class="kc-action-cell" data-ng-click="removeRole(role)">{{:: 'delete' | translate}}</td> <td class="kc-action-cell" data-ng-click="removeRole(role)">{{:: 'delete' | translate}}</td>
</tr> </tr>
<tr data-ng-show="(roles | filter:{name: searchQuery}).length == 0"> <tr data-ng-show="(roles | filter:{name: query.search}).length == 0">
<td class="text-muted" colspan="4" data-ng-show="searchQuery">{{:: 'no-results' | translate}}</td> <td class="text-muted" colspan="4" data-ng-show="searchLoaded && roles.length == 0 && lastSearch != null">{{:: 'no-results' | translate}}</td>
<td class="text-muted" colspan="4" data-ng-hide="searchQuery">{{:: 'no-realm-roles-available' | translate}}</td> <td class="text-muted" colspan="4" data-ng-show="searchLoaded && roles.length == 0 && lastSearch == null">{{:: 'no-realm-roles-available' | translate}}</td>
</tr> </tr>
</tbody> </tbody>
<tfoot data-ng-show="roles && (roles.length >= query.max || query.first > 0)">
<tr>
<td colspan="5">
<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="roles.length < query.max">{{:: 'next-page' | translate}}</button>
</div>
</td>
</tr>
</tfoot>
</table> </table>
<kc-paging current-page='currentPage' number-of-pages='numberOfPages' current-page-input='currentPageInput'></kc-paging>
</div> </div>
<kc-menu></kc-menu> <kc-menu></kc-menu>