diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RoleByIdResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RoleByIdResource.java index bb3238f44d..b594dbc475 100755 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RoleByIdResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RoleByIdResource.java @@ -27,6 +27,7 @@ import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import java.util.List; import java.util.Set; @@ -63,6 +64,14 @@ public interface RoleByIdResource { @Produces(MediaType.APPLICATION_JSON) Set getRoleComposites(@PathParam("role-id") String id); + @Path("{role-id}/composites") + @GET + @Produces(MediaType.APPLICATION_JSON) + Set searchRoleComposites(@PathParam("role-id") String id, + @QueryParam("search") String search, + @QueryParam("first") Integer first, + @QueryParam("max") Integer max); + @Path("{role-id}/composites/realm") @GET @Produces(MediaType.APPLICATION_JSON) diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java index 4fb5157618..b6c4b78629 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmCacheSession.java @@ -701,6 +701,11 @@ public class RealmCacheSession implements CacheRealmProvider { return getRoleDelegate().getRealmRolesStream(realm, first, max); } + @Override + public Stream getRolesStream(RealmModel realm, Stream ids, String search, Integer first, Integer max) { + return getRoleDelegate().getRolesStream(realm, ids, search, first, max); + } + @Override public Stream getClientRolesStream(ClientModel client, Integer first, Integer max) { return getRoleDelegate().getClientRolesStream(client, first, max); diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java index 0dfa7ff159..9b4a7e34ef 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RoleAdapter.java @@ -143,6 +143,13 @@ public class RoleAdapter implements RoleModel { return composites.stream(); } + @Override + public Stream getCompositesStream(String search, Integer first, Integer max) { + if (isUpdated()) return updated.getCompositesStream(search, first, max); + + return cacheSession.getRoleDelegate().getRolesStream(realm, cached.getComposites().stream(), search, first, max); + } + @Override public boolean isClientRole() { return cached instanceof CachedClientRole; diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java index eaaea47ed5..cd41257280 100644 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -302,6 +302,26 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc return getRolesStream(query, realm, first, max); } + @Override + public Stream getRolesStream(RealmModel realm, Stream ids, String search, Integer first, Integer max) { + if (ids == null) return Stream.empty(); + + TypedQuery query; + + if (search == null) { + query = em.createNamedQuery("getRoleIdsFromIdList", String.class); + } else { + query = em.createNamedQuery("getRoleIdsByNameContainingFromIdList", String.class) + .setParameter("search", search); + } + + query.setParameter("realm", realm.getId()) + .setParameter("ids", ids.collect(Collectors.toList())); + + return closing(paginateQuery(query, first, max).getResultStream()) + .map(g -> session.roles().getRoleById(realm, g)); + } + @Override public Stream getClientRolesStream(ClientModel client, Integer first, Integer max) { TypedQuery query = em.createNamedQuery("getClientRoles", RoleEntity.class); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java index f97f01d587..c46a038991 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java @@ -113,6 +113,13 @@ public class RoleAdapter implements RoleModel, JpaModel { return composites.filter(Objects::nonNull); } + @Override + public Stream getCompositesStream(String search, Integer first, Integer max) { + return session.roles().getRolesStream(realm, + getEntity().getCompositeRoles().stream().map(RoleEntity::getId), + search, first, max); + } + @Override public boolean hasRole(RoleModel role) { return this.equals(role) || KeycloakModelUtils.searchFor(role, this, new HashSet<>()); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java index c77ae0ca61..68c653236f 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RoleEntity.java @@ -63,6 +63,8 @@ import java.util.Set; @NamedQuery(name="getRealmRoleByName", query="select role from RoleEntity role where role.clientRole = false and role.name = :name and role.realmId = :realm"), @NamedQuery(name="getRealmRoleIdByName", query="select role.id from RoleEntity role where role.clientRole = false and role.name = :name and role.realmId = :realm"), @NamedQuery(name="searchForRealmRoles", query="select role from RoleEntity role where role.clientRole = false and role.realmId = :realm and ( lower(role.name) like :search or lower(role.description) like :search ) order by role.name"), + @NamedQuery(name="getRoleIdsFromIdList", query="select role.id from RoleEntity role where role.realmId = :realm and role.id in :ids order by role.name ASC"), + @NamedQuery(name="getRoleIdsByNameContainingFromIdList", query="select role.id from RoleEntity role where role.realmId = :realm and lower(role.name) like lower(concat('%',:search,'%')) and role.id in :ids order by role.name ASC"), }) public class RoleEntity { diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmProvider.java b/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmProvider.java index 4def40064e..906d4306a5 100644 --- a/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmProvider.java @@ -431,6 +431,11 @@ public class MapRealmProvider implements RealmProvider { return session.roles().getRealmRolesStream(realm, first, max); } + @Override + public Stream getRolesStream(RealmModel realm, Stream ids, String search, Integer first, Integer max) { + return session.roles().getRolesStream(realm, ids, search, first, max); + } + @Override @Deprecated public boolean removeRole(RoleModel role) { diff --git a/model/map/src/main/java/org/keycloak/models/map/role/MapRoleAdapter.java b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleAdapter.java index ab3a64f53a..65d003b2de 100644 --- a/model/map/src/main/java/org/keycloak/models/map/role/MapRoleAdapter.java +++ b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleAdapter.java @@ -76,15 +76,21 @@ public class MapRoleAdapter extends AbstractRoleModel implements .filter(Objects::nonNull); } + @Override + public Stream getCompositesStream(String search, Integer first, Integer max) { + LOG.tracef("%% (%s).getCompositesStream(%s, %d, %d):%d - %s", this, search, first, max, entity.getCompositeRoles().size(), getShortStackTrace()); + return session.roles().getRolesStream(realm, entity.getCompositeRoles().stream(), search, first, max); + } + @Override public void addCompositeRole(RoleModel role) { - LOG.tracef("%s(%s).addCompositeRole(%s(%s))%s", entity.getName(), entity.getId(), role.getName(), role.getId(), getShortStackTrace()); + LOG.tracef("(%s).addCompositeRole(%s(%s))%s", this, role.getName(), role.getId(), getShortStackTrace()); entity.addCompositeRole(role.getId()); } @Override public void removeCompositeRole(RoleModel role) { - LOG.tracef("%s(%s).removeCompositeRole(%s(%s))%s", entity.getName(), entity.getId(), role.getName(), role.getId(), getShortStackTrace()); + LOG.tracef("(%s).removeCompositeRole(%s(%s))%s", this, role.getName(), role.getId(), getShortStackTrace()); entity.removeCompositeRole(role.getId()); } @@ -135,7 +141,7 @@ public class MapRoleAdapter extends AbstractRoleModel implements @Override public String toString() { - return "MapRoleAdapter{" + getId() + '}'; + return String.format("%s@%08x", getName(), System.identityHashCode(this)); } } diff --git a/model/map/src/main/java/org/keycloak/models/map/role/MapRoleProvider.java b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleProvider.java index 4047bda9b4..7e7026afa4 100644 --- a/model/map/src/main/java/org/keycloak/models/map/role/MapRoleProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/role/MapRoleProvider.java @@ -87,6 +87,23 @@ public class MapRoleProvider implements RoleProvider { .map(entityToAdapterFunc(realm)); } + @Override + public Stream getRolesStream(RealmModel realm, Stream ids, String search, Integer first, Integer max) { + LOG.tracef("getRolesStream(%s, %s, %s, %d, %d)%s", realm, ids, search, first, max, getShortStackTrace()); + if (ids == null) return Stream.empty(); + + ModelCriteriaBuilder mcb = roleStore.createCriteriaBuilder() + .compare(RoleModel.SearchableFields.ID, Operator.IN, ids) + .compare(RoleModel.SearchableFields.REALM_ID, Operator.EQ, realm.getId()); + + if (search != null) { + mcb = mcb.compare(RoleModel.SearchableFields.NAME, Operator.ILIKE, "%" + search + "%"); + } + + return tx.read(withCriteria(mcb).pagination(first, max, RoleModel.SearchableFields.NAME)) + .map(entityToAdapterFunc(realm)); + } + @Override public Stream getRealmRolesStream(RealmModel realm) { ModelCriteriaBuilder mcb = roleStore.createCriteriaBuilder() diff --git a/server-spi/src/main/java/org/keycloak/models/RoleModel.java b/server-spi/src/main/java/org/keycloak/models/RoleModel.java index dd282901c6..c802077f97 100755 --- a/server-spi/src/main/java/org/keycloak/models/RoleModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RoleModel.java @@ -69,7 +69,19 @@ public interface RoleModel { * Returns all composite roles as a stream. * @return Stream of {@link RoleModel}. Never returns {@code null}. */ - Stream getCompositesStream(); + default Stream getCompositesStream() { + return getCompositesStream(null, null, null); + } + + /** + * Returns a paginated stream of composite roles of {@code this} role that contain given string in its name. + * + * @param search Case-insensitive search string + * @param first Index of the first result to return. Ignored if negative or {@code null}. + * @param max Maximum number of results to return. Ignored if negative or {@code null}. + * @return A stream of requested roles ordered by the role name + */ + Stream getCompositesStream(String search, Integer first, Integer max); boolean isClientRole(); diff --git a/server-spi/src/main/java/org/keycloak/models/RoleProvider.java b/server-spi/src/main/java/org/keycloak/models/RoleProvider.java index 8797b4a4f6..79ba42da31 100644 --- a/server-spi/src/main/java/org/keycloak/models/RoleProvider.java +++ b/server-spi/src/main/java/org/keycloak/models/RoleProvider.java @@ -81,6 +81,18 @@ public interface RoleProvider extends Provider, RoleLookupProvider { */ Stream getRealmRolesStream(RealmModel realm, Integer first, Integer max); + /** + * Returns a paginated stream of roles with given ids and given search value in role names. + * + * @param realm Realm. Cannot be {@code null}. + * @param ids Stream of ids. Returns empty {@code Stream} when {@code null}. + * @param search Case-insensitive string to search by role's name or description. Ignored if {@code null}. + * @param first Index of the first result to return. Ignored if negative or {@code null}. + * @param max Maximum number of results to return. Ignored if negative or {@code null}. + * @return Stream of desired roles. Never returns {@code null}. + */ + Stream getRolesStream(RealmModel realm, Stream ids, String search, Integer first, Integer max); + /** * Removes given realm role from the given realm. * @param role Role to be removed. diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java index 94315cffb2..2749b79f61 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RoleByIdResource.java @@ -42,6 +42,7 @@ import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -176,12 +177,21 @@ public class RoleByIdResource extends RoleResource { @GET @NoCache @Produces(MediaType.APPLICATION_JSON) - public Stream getRoleComposites(final @PathParam("role-id") String id) { + public Stream getRoleComposites(final @PathParam("role-id") String id, + final @QueryParam("search") String search, + final @QueryParam("first") Integer first, + final @QueryParam("max") Integer max + ) { if (logger.isDebugEnabled()) logger.debug("*** getRoleComposites: '" + id + "'"); RoleModel role = getRoleModel(id); auth.roles().requireView(role); - return role.getCompositesStream().map(ModelToRepresentation::toBriefRepresentation); + + if (search == null && first == null && max == null) { + return role.getCompositesStream().map(ModelToRepresentation::toBriefRepresentation); + } + + return role.getCompositesStream(search, first, max).map(ModelToRepresentation::toBriefRepresentation); } /** diff --git a/services/src/main/java/org/keycloak/storage/RoleStorageManager.java b/services/src/main/java/org/keycloak/storage/RoleStorageManager.java index fc73ffc0c7..95c3aacfc2 100644 --- a/services/src/main/java/org/keycloak/storage/RoleStorageManager.java +++ b/services/src/main/java/org/keycloak/storage/RoleStorageManager.java @@ -150,6 +150,11 @@ public class RoleStorageManager implements RoleProvider { return session.roleLocalStorage().getRealmRolesStream(realm, first, max); } + @Override + public Stream getRolesStream(RealmModel realm, Stream ids, String search, Integer first, Integer max) { + return session.roleLocalStorage().getRolesStream(realm, ids, search, first, max); + } + /** * Obtaining roles from an external role storage is time-bounded. In case the external role storage * isn't available at least roles from a local storage are returned. For this purpose diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProvider.java index 762b7a8b4f..928f6f4975 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedRoleStorageProvider.java @@ -114,7 +114,7 @@ public class HardcodedRoleStorageProvider implements RoleStorageProvider { } @Override - public Stream getCompositesStream() { + public Stream getCompositesStream(String search, Integer first, Integer max) { return Stream.empty(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/RoleByIdResourceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/RoleByIdResourceTest.java index 1b70210dd1..d1f68efcb9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/RoleByIdResourceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/RoleByIdResourceTest.java @@ -39,6 +39,9 @@ import java.util.List; import java.util.Map; import java.util.Set; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -146,6 +149,21 @@ public class RoleByIdResourceTest extends AbstractAdminTest { Set clientComposites = resource.getClientRoleComposites(ids.get("role-a"), clientUuid); Assert.assertNames(clientComposites, "role-c"); + composites = resource.searchRoleComposites(ids.get("role-a"), null, null, null); + Assert.assertNames(composites, "role-b", "role-c"); + + composites = resource.searchRoleComposites(ids.get("role-a"), "b", null, null); + Assert.assertNames(composites, "role-b"); + + composites = resource.searchRoleComposites(ids.get("role-a"), null, 0, 0); + assertThat(composites, is(empty())); + + composites = resource.searchRoleComposites(ids.get("role-a"), null, 0, 1); + Assert.assertNames(composites, "role-b"); + + composites = resource.searchRoleComposites(ids.get("role-a"), null, 1, 1); + Assert.assertNames(composites, "role-c"); + resource.deleteComposites(ids.get("role-a"), l); assertAdminEvents.assertEvent(realmId, OperationType.DELETE, AdminEventPaths.roleByIdResourceCompositesPath(ids.get("role-a")), l, ResourceType.REALM_ROLE); 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 a697db2431..d5a8c1128a 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,6 +21,7 @@ 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.RoleByIdResource; import org.keycloak.admin.client.resource.RoleResource; import org.keycloak.admin.client.resource.RolesResource; import org.keycloak.events.admin.OperationType; @@ -39,8 +40,12 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import javax.ws.rs.ClientErrorException; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -156,6 +161,47 @@ public class ClientRolesTest extends AbstractClientTest { assertEquals(0, rolesRsc.get("role-a").getRoleComposites().size()); } + + @Test + public void testCompositeRolesSearch() { + // Create main-role we will work on + RoleRepresentation mainRole = makeRole("main-role"); + rolesRsc.create(mainRole); + + RoleResource mainRoleRsc = rolesRsc.get("main-role"); + assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId, "main-role"), mainRole, ResourceType.CLIENT_ROLE); + + // Add composites + List createdRoles = IntStream.range(0, 20) + .boxed() + .map(i -> makeRole("role" + i)) + .peek(rolesRsc::create) + .peek(role -> assertAdminEvents.assertEvent(getRealmId(), OperationType.CREATE, AdminEventPaths.clientRoleResourcePath(clientDbId, role.getName()), role, ResourceType.CLIENT_ROLE)) + .map(role -> rolesRsc.get(role.getName()).toRepresentation()) + .collect(Collectors.toList()); + + mainRoleRsc.addComposites(createdRoles); + mainRole = mainRoleRsc.toRepresentation(); + RoleByIdResource roleByIdResource = adminClient.realm(getRealmId()).rolesById(); + + // Search for all composites + Set foundRoles = roleByIdResource.getRoleComposites(mainRole.getId()); + assertThat(foundRoles, hasSize(createdRoles.size())); + + // Search paginated composites + foundRoles = roleByIdResource.searchRoleComposites(mainRole.getId(), null, 0, 10); + assertThat(foundRoles, hasSize(10)); + + // Search for composites by string role1 (should be role1, role10-role19) without pagination + foundRoles = roleByIdResource.searchRoleComposites(mainRole.getId(), "role1", null, null); + assertThat(foundRoles, hasSize(11)); + + // Search for role1 with pagination + foundRoles.forEach(System.out::println); + foundRoles = roleByIdResource.searchRoleComposites(mainRole.getId(), "role1", 5, 5); + assertThat(foundRoles, hasSize(5)); + } + @Test public void usersInRole() { String clientID = clientRsc.toRepresentation().getId(); diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/role/RoleModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/role/RoleModelTest.java new file mode 100644 index 0000000000..7937b60220 --- /dev/null +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/role/RoleModelTest.java @@ -0,0 +1,215 @@ +package org.keycloak.testsuite.model.role; + +import org.hamcrest.Matcher; +import org.junit.Test; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientProvider; +import org.keycloak.models.Constants; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RealmProvider; +import org.keycloak.models.RoleModel; +import org.keycloak.models.RoleProvider; +import org.keycloak.testsuite.model.KeycloakModelTest; +import org.keycloak.testsuite.model.RequireProvider; + +import java.util.Collection; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; + +@RequireProvider(RealmProvider.class) +@RequireProvider(ClientProvider.class) +@RequireProvider(RoleProvider.class) +public class RoleModelTest extends KeycloakModelTest { + + private String realmId; + private String mainRoleId; + private static List rolesSubset; + + @Override + public void createEnvironment(KeycloakSession s) { + RealmModel realm = s.realms().createRealm("realm"); + realm.setDefaultRole(s.roles().addRealmRole(realm, Constants.DEFAULT_ROLES_ROLE_PREFIX + "-" + realm.getName())); + this.realmId = realm.getId(); + + createRoles(s, realm); + } + + @Override + public void cleanEnvironment(KeycloakSession s) { + s.realms().removeRealm(realmId); + } + + @FunctionalInterface + public interface GetResult { + List getResult(String search, Integer first, Integer max); + } + + + private void createRoles(KeycloakSession session, RealmModel realm) { + RoleModel mainRole = session.roles().addRealmRole(realm, "main-role"); + mainRoleId = mainRole.getId(); + + ClientModel clientModel = session.clients().addClient(realm, "client-with-roles"); + + // Create 10 realm roles that are composites of main role + rolesSubset = IntStream.range(0, 10) + .boxed() + .map(i -> session.roles().addRealmRole(realm, "main-role-composite-" + i)) + .peek(mainRole::addCompositeRole) + .map(RoleModel::getId) + .collect(Collectors.toList()); + + // Create 10 client roles that are composites of main role + rolesSubset.addAll(IntStream.range(10, 20) + .boxed() + .map(i -> session.roles().addClientRole(clientModel, "main-role-composite-" + i)) + .peek(mainRole::addCompositeRole) + .map(RoleModel::getId) + .collect(Collectors.toList())); + + // add some additional roles that won't fulfill condition + IntStream.range(0, 20) + .forEach(i -> session.roles().addRealmRole(realm, "non-returned-role-" + i)); + } + + private List getResult(String search, Integer first, Integer max) { + return withRealm(realmId, (session, realm) -> session.roles().getRolesStream(realm, rolesSubset.stream(), search, first, max).collect(Collectors.toList())); + } + + private RoleModel getMainRole() { + return withRealm(realmId, (session, realm) -> session.roles().getRoleById(realm, mainRoleId)); + } + + private List getModelResult(String search, Integer first, Integer max) { + return withRealm(realmId, ((session, realm) -> session.roles().getRoleById(realm, mainRoleId).getCompositesStream(search, first, max).collect(Collectors.toList()))); + } + + @Test + public void testRolesWithIdsQueries() { + // should return all roles from the subset + List result = getResult(null, null, null); + assertThat(result, hasSize(rolesSubset.size())); + assertIndexValues(result, contains(0, 1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9)); + + // test non-existing role ids + result = withRealm(realmId, (session, realm) -> session.roles() + .getRolesStream(realm, IntStream.range(0, 10).boxed() + .map(i -> UUID.randomUUID().toString()), null, null, null) + .collect(Collectors.toList())); + assertThat(result, is(empty())); + + // test mixed non-existing with existing + result = withRealm(realmId, (session, realm) -> session.roles() + .getRolesStream(realm, Stream.concat(rolesSubset.subList(0, 10).stream(), + IntStream.range(0, 10).boxed().map(i -> UUID.randomUUID().toString())), null, null, null) + .collect(Collectors.toList())); + assertThat(result, hasSize(10)); + assertIndexValues(result, contains(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)); + } + + @Test + public void testCompositeRoles() { + List result = getModelResult(null, null, null); + assertThat(result, hasSize(rolesSubset.size())); + assertIndexValues(result, contains(0, 1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9)); + + result = withRealm(realmId, (session, realm) -> session.roles().getRoleById(realm, mainRoleId).getCompositesStream().collect(Collectors.toList())); + assertThat(result, hasSize(rolesSubset.size())); + assertIndexValues(result, containsInAnyOrder(0, 1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9)); + } + + @Test + public void testRolesWithIdsSearchQueries() { + testRolesWithIdsSearchQueries(this::getResult); + } + + @Test + public void testCompositeRolesSearchQueries() { + testRolesWithIdsSearchQueries(this::getModelResult); + } + + public void testRolesWithIdsSearchQueries(GetResult resultProvider) { + // should return all roles from the subset + List result = resultProvider.getResult("", null, null); + assertThat(result, hasSize(rolesSubset.size())); + assertIndexValues(result, contains(0, 1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9)); + + // test string that all contains + result = resultProvider.getResult("role-composite", null, null); + assertThat(result, hasSize(rolesSubset.size())); + assertIndexValues(result, contains(0, 1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9)); + + // test string that some contain + result = resultProvider.getResult("role-composite-1", null, null); + assertThat(result, hasSize(11)); + assertIndexValues(result, contains(1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)); + + // test string none contain + result = resultProvider.getResult("nonsense-string", null, null); + assertThat(result, is(empty())); + } + + @Test + public void testRolesWithIdsPaginationQueries() { + testRolesWithIdsPaginationQueries(this::getResult); + } + + @Test + public void testCompositeRolesPaginationQueries() { + testRolesWithIdsPaginationQueries(this::getResult); + } + + public void testRolesWithIdsPaginationQueries(GetResult resultProvider) { + // should return all roles from the subset + List result = resultProvider.getResult(null, null, rolesSubset.size()); + assertThat(result, hasSize(rolesSubset.size())); + assertIndexValues(result, contains(0, 1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 3, 4, 5, 6, 7, 8, 9)); + + // test max parameter + result = resultProvider.getResult(null, null, 5); + assertThat(result, hasSize(5)); + assertIndexValues(result, contains(0, 1, 10, 11, 12)); + + // test first parameter + result = resultProvider.getResult(null, 10, null); + assertThat(result, hasSize(rolesSubset.size() - 10)); + assertIndexValues(result, contains(18, 19, 2, 3, 4, 5, 6, 7, 8, 9)); + + // test first and max + result = resultProvider.getResult(null, 10, 5); + assertThat(result, hasSize(5)); + assertIndexValues(result, contains(18, 19, 2, 3, 4)); + } + + @Test + public void testRolesWithIdsPaginationSearchQueries() { + testRolesWithIdsPaginationSearchQueries(this::getResult); + } + + @Test + public void testCompositeRolesPaginationSearchQueries() { + testRolesWithIdsPaginationSearchQueries(this::getModelResult); + } + + public void testRolesWithIdsPaginationSearchQueries(GetResult resultProvider) { + // test all parameters together + List result = resultProvider.getResult("1", 4, 3); + assertThat(result, hasSize(3)); + assertIndexValues(result, contains(13, 14, 15)); + } + + private void assertIndexValues(List roles, Matcher> matcher) { + assertThat(roles.stream().map(RoleModel::getName).map(s -> s.substring("main-role-composite-".length())).map(Integer::parseInt).collect(Collectors.toList()), matcher); + } +}