diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java index 793e6ccf58..422709c66f 100755 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java @@ -79,6 +79,25 @@ public interface GroupsResource { @QueryParam("first") Integer first, @QueryParam("max") Integer max, @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation); + + /** + * Get groups by pagination params. + * @param search search string for group + * @param exact exact match for search + * @param first index of the first element + * @param max max number of occurrences + * @param briefRepresentation if false, return groups with their attributes + * @return A list containing the slice of all groups. + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + List groups(@QueryParam("search") String search, + @QueryParam("exact") Boolean exact, + @QueryParam("first") Integer first, + @QueryParam("max") Integer max, + @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation); + /** * Counts all groups. * @return A map containing key "count" with number of groups as value. diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java index 311023c029..96c17aba3a 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java @@ -1478,8 +1478,9 @@ public class RealmAdapter implements CachedRealmModel { } @Override + @Deprecated public Stream searchForGroupByNameStream(String search, Integer first, Integer max) { - return cacheSession.searchForGroupByNameStream(this, search, first, max); + return cacheSession.searchForGroupByNameStream( this, search, false, first, max); } @Override 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 4579e414e1..32145c2db1 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 @@ -1025,7 +1025,12 @@ public class RealmCacheSession implements CacheRealmProvider { @Override public Stream searchForGroupByNameStream(RealmModel realm, String search, Integer first, Integer max) { - return getGroupDelegate().searchForGroupByNameStream(realm, search, first, max); + return getGroupDelegate().searchForGroupByNameStream(realm, search, false, first, max); + } + + @Override + public Stream searchForGroupByNameStream(RealmModel realm, String search, Boolean exact, Integer firstResult, Integer maxResults) { + return getGroupDelegate().searchForGroupByNameStream(realm, search, exact, firstResult, maxResults); } @Override 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 6276c92588..ec01cdff35 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 @@ -40,6 +40,7 @@ import javax.persistence.criteria.Join; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; +import org.apache.commons.lang.BooleanUtils; import org.jboss.logging.Logger; import org.keycloak.common.util.Time; import org.keycloak.connections.jpa.util.JpaUtils; @@ -937,10 +938,19 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc @Override public Stream searchForGroupByNameStream(RealmModel realm, String search, Integer first, Integer max) { - TypedQuery query = em.createNamedQuery("getGroupIdsByNameContaining", String.class) - .setParameter("realm", realm.getId()) - .setParameter("search", search); + return searchForGroupByNameStream(realm, search, false, first, max); + } + @Override + public Stream searchForGroupByNameStream(RealmModel realm, String search, Boolean exact, Integer first, Integer max) { + TypedQuery query; + if (Boolean.TRUE.equals(exact)) { + query = em.createNamedQuery("getGroupIdsByName", String.class); + } else { + query = em.createNamedQuery("getGroupIdsByNameContaining", String.class); + } + query.setParameter("realm", realm.getId()) + .setParameter("search", search); Stream groups = paginateQuery(query, first, max).getResultStream(); return closing(groups.map(id -> { @@ -951,7 +961,6 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc return groupById; }).sorted(GroupModel.COMPARE_BY_NAME).distinct()); } - @Override public Stream searchGroupsByAttributes(RealmModel realm, Map attributes, Integer firstResult, Integer maxResults) { Map filteredAttributes = groupSearchableAttributes == null || groupSearchableAttributes.isEmpty() diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 7eb9afeece..ecf207a9fc 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -1938,8 +1938,9 @@ public class RealmAdapter implements LegacyRealmModel, JpaModel { } @Override + @Deprecated public Stream searchForGroupByNameStream(String search, Integer first, Integer max) { - return session.groups().searchForGroupByNameStream(this, search, first, max); + return session.groups().searchForGroupByNameStream(this, search, false, first, max); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java index adf0164040..e1ca4abb67 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/GroupEntity.java @@ -32,6 +32,7 @@ import java.util.LinkedList; @NamedQuery(name="getGroupIdsByRealm", query="select u.id from GroupEntity u where u.realm = :realm order by u.name ASC"), @NamedQuery(name="getGroupIdsByNameContaining", query="select u.id from GroupEntity u where u.realm = :realm and u.name like concat('%',:search,'%') order by u.name ASC"), @NamedQuery(name="getGroupIdsByNameContainingFromIdList", query="select u.id from GroupEntity u where u.realm = :realm and lower(u.name) like lower(concat('%',:search,'%')) and u.id in :ids order by u.name ASC"), + @NamedQuery(name="getGroupIdsByName", query="select u.id from GroupEntity u where u.realm = :realm and u.name = :search order by u.name ASC"), @NamedQuery(name="getGroupIdsFromIdList", query="select u.id from GroupEntity u where u.realm = :realm and u.id in :ids order by u.name ASC"), @NamedQuery(name="getGroupCountByNameContainingFromIdList", query="select count(u) from GroupEntity u where u.realm = :realm and lower(u.name) like lower(concat('%',:search,'%')) and u.id in :ids"), @NamedQuery(name="getTopLevelGroupIds", query="select u.id from GroupEntity u where u.parentId = :parent and u.realm = :realm order by u.name ASC"), diff --git a/model/legacy-private/src/main/java/org/keycloak/storage/GroupStorageManager.java b/model/legacy-private/src/main/java/org/keycloak/storage/GroupStorageManager.java index dd4e3a35c5..62e4ecc38f 100644 --- a/model/legacy-private/src/main/java/org/keycloak/storage/GroupStorageManager.java +++ b/model/legacy-private/src/main/java/org/keycloak/storage/GroupStorageManager.java @@ -64,12 +64,9 @@ public class GroupStorageManager extends AbstractStorageManager searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) { - Stream local = localStorage().searchForGroupByNameStream(realm, search, firstResult, maxResults); - Stream ext = flatMapEnabledStorageProvidersWithTimeout(realm, GroupLookupProvider.class, - p -> p.searchForGroupByNameStream(realm, search, firstResult, maxResults)); - - return Stream.concat(local, ext); + return searchForGroupByNameStream(realm, search, false, firstResult, maxResults); } @Override @@ -81,6 +78,22 @@ public class GroupStorageManager extends AbstractStorageManager searchForGroupByNameStream(RealmModel realm, String search, Boolean exact, Integer firstResult, Integer maxResults) { + Stream local = localStorage().searchForGroupByNameStream(realm, search, exact, firstResult, maxResults); + Stream ext = flatMapEnabledStorageProvidersWithTimeout(realm, GroupLookupProvider.class, + p -> p.searchForGroupByNameStream(realm, search, exact, firstResult, maxResults)); + + return Stream.concat(local, ext); + } /* GROUP PROVIDER METHODS - provided only by local storage (e.g. not supported by storage providers) */ @Override diff --git a/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProvider.java b/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProvider.java index 3127015622..1eaa56b766 100644 --- a/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/group/MapGroupProvider.java @@ -17,6 +17,7 @@ package org.keycloak.models.map.group; +import java.security.Key; import org.jboss.logging.Logger; import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel.SearchableFields; @@ -167,24 +168,34 @@ public class MapGroupProvider implements GroupProvider { } @Override + @Deprecated public Stream searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) { - LOG.tracef("searchForGroupByNameStream(%s, %s, %d, %d)%s", realm, search, firstResult, maxResults, getShortStackTrace()); + return searchForGroupByNameStream(realm, search, false, firstResult, maxResults); + } + + public Stream searchForGroupByNameStream(RealmModel realm, String search, Boolean exact, Integer firstResult, Integer maxResults) { + LOG.tracef("searchForGroupByNameStream(%s, %s, %s, %b, %d, %d)%s", realm, session, search, exact, firstResult, maxResults, getShortStackTrace()); DefaultModelCriteria mcb = criteria(); - mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) - .compare(SearchableFields.NAME, Operator.ILIKE, "%" + search + "%"); + if (exact != null && exact.equals(Boolean.TRUE)) { + mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) + .compare(SearchableFields.NAME, Operator.EQ, search); + } else { + mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) + .compare(SearchableFields.NAME, Operator.ILIKE, "%" + search + "%"); + } return tx.read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.NAME)) - .map(MapGroupEntity::getId) - .map(id -> { - GroupModel groupById = session.groups().getGroupById(realm, id); - while (Objects.nonNull(groupById.getParentId())) { - groupById = session.groups().getGroupById(realm, groupById.getParentId()); - } - return groupById; - }).sorted(GroupModel.COMPARE_BY_NAME).distinct(); + .map(MapGroupEntity::getId) + .map(id -> { + GroupModel groupById = session.groups().getGroupById(realm, id); + while (Objects.nonNull(groupById.getParentId())) { + groupById = session.groups().getGroupById(realm, groupById.getParentId()); + } + return groupById; + }).sorted(GroupModel.COMPARE_BY_NAME).distinct(); } @Override diff --git a/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmAdapter.java b/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmAdapter.java index c1ea867a23..d6a8f6085a 100644 --- a/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmAdapter.java +++ b/model/map/src/main/java/org/keycloak/models/map/realm/MapRealmAdapter.java @@ -1425,8 +1425,9 @@ public class MapRealmAdapter extends AbstractRealmModel implemen } @Override + @Deprecated public Stream searchForGroupByNameStream(String search, Integer first, Integer max) { - return session.groups().searchForGroupByNameStream(this, search, first, max); + return session.groups().searchForGroupByNameStream(this, search, false, first, max); } @Override 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 040011e7c1..c98f95f21a 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 @@ -393,8 +393,8 @@ public class MapRealmProvider implements RealmProvider { @Override @Deprecated - public Stream searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) { - return session.groups().searchForGroupByNameStream(realm, search, firstResult, maxResults); + public Stream searchForGroupByNameStream(RealmModel realm, String search, Boolean exact, Integer firstResult, Integer maxResults) { + return session.groups().searchForGroupByNameStream(realm, search, exact, firstResult, maxResults); } @Override diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 079ba5ddab..9419830321 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -177,9 +177,15 @@ public class ModelToRepresentation { .map(g -> toGroupHierarchy(g, full, attributes)); } + @Deprecated public static Stream searchForGroupByName(RealmModel realm, boolean full, String search, Integer first, Integer max) { return realm.searchForGroupByNameStream(search, first, max) - .map(g -> toGroupHierarchy(g, full, search)); + .map(g -> toGroupHierarchy(g, full, search)); + } + + public static Stream searchForGroupByName(KeycloakSession session, RealmModel realm, boolean full, String search, Boolean exact, Integer first, Integer max) { + return session.groups().searchForGroupByNameStream(realm, search, exact, first, max) + .map(g -> toGroupHierarchy(g, full, search, exact)); } public static Stream searchForGroupByName(UserModel user, boolean full, String search, Integer first, Integer max) { @@ -211,11 +217,16 @@ public class ModelToRepresentation { return toGroupHierarchy(group, full, (String) null); } + @Deprecated public static GroupRepresentation toGroupHierarchy(GroupModel group, boolean full, String search) { + return toGroupHierarchy(group, full, search, false); + } + + public static GroupRepresentation toGroupHierarchy(GroupModel group, boolean full, String search, Boolean exact) { GroupRepresentation rep = toRepresentation(group, full); List subGroups = group.getSubGroupsStream() - .filter(g -> groupMatchesSearchOrIsPathElement(g, search)) - .map(subGroup -> toGroupHierarchy(subGroup, full, search)).collect(Collectors.toList()); + .filter(g -> groupMatchesSearchOrIsPathElement(g, search, exact)) + .map(subGroup -> toGroupHierarchy(subGroup, full, search, exact)).collect(Collectors.toList()); rep.setSubGroups(subGroups); return rep; } @@ -228,16 +239,24 @@ public class ModelToRepresentation { return rep; } - private static boolean groupMatchesSearchOrIsPathElement(GroupModel group, String search) { + private static boolean groupMatchesSearchOrIsPathElement(GroupModel group, String search, Boolean exact) { if (StringUtil.isBlank(search)) { return true; } - if (group.getName().contains(search)) { - return true; + if(exact !=null && exact.equals(true)){ + if (group.getName().equals(search)){ + return true; + } + } else { + if (group.getName().contains(search)) { + return true; + } } + return group.getSubGroupsStream().findAny().isPresent(); } + public static UserRepresentation toRepresentation(KeycloakSession session, RealmModel realm, UserModel user) { UserRepresentation rep = new UserRepresentation(); rep.setId(user.getId()); diff --git a/server-spi/src/main/java/org/keycloak/models/RealmModel.java b/server-spi/src/main/java/org/keycloak/models/RealmModel.java index 41fb744e24..d426dc7dd8 100755 --- a/server-spi/src/main/java/org/keycloak/models/RealmModel.java +++ b/server-spi/src/main/java/org/keycloak/models/RealmModel.java @@ -846,7 +846,7 @@ public interface RealmModel extends RoleContainerModel { Stream getTopLevelGroupsStream(Integer first, Integer max); /** - * @deprecated Use {@link #searchForGroupByNameStream(String, Integer, Integer) searchForGroupByName} instead. + * @deprecated Use {@link GroupProvider#searchForGroupByNameStream(RealmModel, String, Boolean, Integer, Integer)} instead. */ @Deprecated default List searchForGroupByName(String search, Integer first, Integer max) { @@ -859,7 +859,9 @@ public interface RealmModel extends RoleContainerModel { * @param first {@code Integer} Index of the first desired group. Ignored if negative or {@code null}. * @param max {@code Integer} Maximum number of returned groups. Ignored if negative or {@code null}. * @return Stream of {@link GroupModel}. Never returns {@code null}. + * @deprecated Use {@link GroupProvider#searchForGroupByNameStream(RealmModel, String, Boolean, Integer, Integer)} instead. */ + @Deprecated Stream searchForGroupByNameStream(String search, Integer first, Integer max); boolean removeGroup(GroupModel group); diff --git a/server-spi/src/main/java/org/keycloak/storage/group/GroupLookupProvider.java b/server-spi/src/main/java/org/keycloak/storage/group/GroupLookupProvider.java index fcd544a518..2101c6c117 100644 --- a/server-spi/src/main/java/org/keycloak/storage/group/GroupLookupProvider.java +++ b/server-spi/src/main/java/org/keycloak/storage/group/GroupLookupProvider.java @@ -62,8 +62,12 @@ public interface GroupLookupProvider { * @param maxResults Maximum number of results to return. Ignored if negative or {@code null}. * @return Stream of root groups that have the given string in their name themself or a group in their child-collection has. * The returned hierarchy contains siblings that do not necessarily have a matching name. Never returns {@code null}. + * @deprecated Use {@link #searchForGroupByNameStream(RealmModel, String, Boolean, Integer, Integer)} instead. */ - Stream searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults); + @Deprecated + default Stream searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) { + return searchForGroupByNameStream(realm, search, false, firstResult, maxResults); + } /** * Returns the groups filtered by attribute names and attribute values for the given realm. @@ -76,4 +80,20 @@ public interface GroupLookupProvider { */ Stream searchGroupsByAttributes(RealmModel realm, Map attributes, Integer firstResult, Integer maxResults); + /** + * Returns the group hierarchy with the given string in name for the given realm. + * + * For a matching group node the parent group is fetched by id (with all children) and added to the result stream. + * This is done until the group node does not have a parent (root group) + * + * @param realm Realm. + * @param search Case sensitive searched string. + * @param exact Boolean which defines wheather search param should be matched exactly. + * @param firstResult First result to return. Ignored if negative or {@code null}. + * @param maxResults Maximum number of results to return. Ignored if negative or {@code null}. + * @return Stream of root groups that have the given string in their name themself or a group in their child-collection has. + * The returned hierarchy contains siblings that do not necessarily have a matching name. Never returns {@code null}. + */ + Stream searchForGroupByNameStream(RealmModel realm, String search, Boolean exact, Integer firstResult, Integer maxResults); + } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java index d3fce9ccf5..a34fdb181c 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java @@ -77,6 +77,7 @@ public class GroupsResource { @Produces(MediaType.APPLICATION_JSON) public Stream getGroups(@QueryParam("search") String search, @QueryParam("q") String searchQuery, + @QueryParam("exact") @DefaultValue("false") Boolean exact, @QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults, @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) { @@ -86,7 +87,7 @@ public class GroupsResource { Map attributes = SearchQueryUtils.getFields(searchQuery); return ModelToRepresentation.searchGroupsByAttributes(session, realm, !briefRepresentation, attributes, firstResult, maxResults); } else if (Objects.nonNull(search)) { - return ModelToRepresentation.searchForGroupByName(realm, !briefRepresentation, search.trim(), firstResult, maxResults); + return ModelToRepresentation.searchForGroupByName(session, realm, !briefRepresentation, search.trim(), exact, firstResult, maxResults); } else if(Objects.nonNull(firstResult) && Objects.nonNull(maxResults)) { return ModelToRepresentation.toGroupHierarchy(realm, !briefRepresentation, firstResult, maxResults); } else { diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedGroupStorageProvider.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedGroupStorageProvider.java index 35fe0b7788..440eece045 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedGroupStorageProvider.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/federation/HardcodedGroupStorageProvider.java @@ -16,6 +16,7 @@ */ package org.keycloak.testsuite.federation; +import org.apache.commons.lang.BooleanUtils; import org.jboss.logging.Logger; import org.keycloak.models.ClientModel; import org.keycloak.models.GroupModel; @@ -53,15 +54,27 @@ public class HardcodedGroupStorageProvider implements GroupStorageProvider { } @Override + @Deprecated public Stream searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) { + return searchForGroupByNameStream(realm, search, false, firstResult, maxResults); + } + + @Override + public Stream searchForGroupByNameStream(RealmModel realm, String search, Boolean exact, Integer firstResult, Integer maxResults) { if (Boolean.parseBoolean(component.getConfig().getFirst(HardcodedGroupStorageProviderFactory.DELAYED_SEARCH))) try { Thread.sleep(5000l); } catch (InterruptedException ex) { Logger.getLogger(HardcodedGroupStorageProvider.class).warn(ex.getCause()); return Stream.empty(); } - if (search != null && this.groupName.toLowerCase().contains(search.toLowerCase())) { - return Stream.of(new HardcodedGroupAdapter(realm)); + if(BooleanUtils.isTrue(exact)){ + if (search != null && this.groupName.equals(search)) { + return Stream.of(new HardcodedGroupAdapter(realm)); + } + }else { + if (search != null && this.groupName.toLowerCase().contains(search.toLowerCase())) { + return Stream.of(new HardcodedGroupAdapter(realm)); + } } return Stream.empty(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java index c4e27eede0..8eb5f8dc1c 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupTest.java @@ -40,6 +40,7 @@ import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; +import org.keycloak.testsuite.updaters.Creator; import org.keycloak.testsuite.util.AdminEventPaths; import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.RoleBuilder; @@ -67,14 +68,22 @@ import javax.ws.rs.ClientErrorException; import javax.ws.rs.core.Response.Status; import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; import org.junit.Rule; import org.junit.rules.ExpectedException; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + import org.keycloak.admin.client.Keycloak; import org.keycloak.models.AdminRoles; import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.RealmModel; import org.keycloak.models.utils.KeycloakModelUtils; + +import static org.hamcrest.MatcherAssert.assertThat; import static org.keycloak.testsuite.Assert.assertNames; import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot; @@ -182,7 +191,7 @@ public class GroupTest extends AbstractGroupTest { response.close(); assertEquals(409, response.getStatus()); // conflict status 409 - same name not allowed } - + @Test // KEYCLOAK-11412 Unintended Groups with same names public void doNotAllowSameGroupNameAtSameLevelWhenUpdatingName() throws Exception { @@ -195,9 +204,9 @@ public class GroupTest extends AbstractGroupTest { GroupRepresentation anotherTopGroup = new GroupRepresentation(); anotherTopGroup.setName("top2"); anotherTopGroup = createGroup(realm, anotherTopGroup); - + anotherTopGroup.setName("top1"); - + try { realm.groups().group(anotherTopGroup.getId()).update(anotherTopGroup); Assert.fail("Expected ClientErrorException"); @@ -215,14 +224,14 @@ public class GroupTest extends AbstractGroupTest { addSubGroup(realm, topGroup, anotherlevel2Group); anotherlevel2Group.setName("level2-1"); - + try { realm.groups().group(anotherlevel2Group.getId()).update(anotherlevel2Group); Assert.fail("Expected ClientErrorException"); } catch (ClientErrorException e) { // conflict status 409 - same name not allowed assertEquals("HTTP 409 Conflict", e.getMessage()); - } + } } @Test @@ -295,7 +304,7 @@ public class GroupTest extends AbstractGroupTest { Assert.fail("Creating a group with empty name should fail"); } } catch (Exception expected) { - Assert.assertNotNull(expected); + assertNotNull(expected); } group.setName(null); @@ -304,7 +313,7 @@ public class GroupTest extends AbstractGroupTest { Assert.fail("Creating a group with null name should fail"); } } catch (Exception expected) { - Assert.assertNotNull(expected); + assertNotNull(expected); } } @@ -327,7 +336,7 @@ public class GroupTest extends AbstractGroupTest { realm.groups().group(groupId).update(group); Assert.fail("Updating a group with empty name should fail"); } catch(Exception expected) { - Assert.assertNotNull(expected); + assertNotNull(expected); } try { @@ -335,7 +344,7 @@ public class GroupTest extends AbstractGroupTest { realm.groups().group(groupId).update(group); Assert.fail("Updating a group with null name should fail"); } catch(Exception expected) { - Assert.assertNotNull(expected); + assertNotNull(expected); } } @@ -379,7 +388,7 @@ public class GroupTest extends AbstractGroupTest { }); level2Group = realm.getGroupByPath("/top/level2"); - Assert.assertNotNull(level2Group); + assertNotNull(level2Group); roles.clear(); roles.add(level2Role); realm.groups().group(level2Group.getId()).roles().realmLevel().add(roles); @@ -392,7 +401,7 @@ public class GroupTest extends AbstractGroupTest { assertAdminEvents.assertEvent(testRealmId, OperationType.CREATE, AdminEventPaths.groupSubgroupsPath(level2Group.getId()), level3Group, ResourceType.GROUP); level3Group = realm.getGroupByPath("/top/level2/level3"); - Assert.assertNotNull(level3Group); + assertNotNull(level3Group); roles.clear(); roles.add(level3Role); realm.groups().group(level3Group.getId()).roles().realmLevel().add(roles); @@ -473,7 +482,7 @@ public class GroupTest extends AbstractGroupTest { } catch (NotFoundException e) {} - Assert.assertNull(login("direct-login", "resource-owner", "secret", user.getId()).getRealmAccess()); + assertNull(login("direct-login", "resource-owner", "secret", user.getId()).getRealmAccess()); } @Test @@ -489,7 +498,7 @@ public class GroupTest extends AbstractGroupTest { createGroup(realm, group); group = realm.getGroupByPath("/" + groupName); - Assert.assertNotNull(group); + assertNotNull(group); assertThat(group.getName(), is(groupName)); assertThat(group.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2")); assertThat(group.getAttributes(), hasEntry(is("attr1"), contains("attrval1"))); @@ -530,14 +539,14 @@ public class GroupTest extends AbstractGroupTest { // Move "mygroup2" as child of "mygroup1" . Assert it was moved Response response = realm.groups().group(group1.getId()).subGroup(group2); - Assert.assertEquals(204, response.getStatus()); + assertEquals(204, response.getStatus()); response.close(); // Assert "mygroup2" was moved group1 = realm.groups().group(group1.getId()).toRepresentation(); group2 = realm.groups().group(group2.getId()).toRepresentation(); assertNames(group1.getSubGroups(), "mygroup2"); - Assert.assertEquals("/mygroup1/mygroup2", group2.getPath()); + assertEquals("/mygroup1/mygroup2", group2.getPath()); assertAdminEvents.clear(); @@ -549,19 +558,19 @@ public class GroupTest extends AbstractGroupTest { // Try to move top level "mygroup2" as child of "mygroup1". It should fail as there is already a child group // of "mygroup1" with name "mygroup2" response = realm.groups().group(group1.getId()).subGroup(group3); - Assert.assertEquals(409, response.getStatus()); + assertEquals(409, response.getStatus()); realm.groups().group(group3.getId()).remove(); // Move "mygroup2" back under parent response = realm.groups().add(group2); - Assert.assertEquals(204, response.getStatus()); + assertEquals(204, response.getStatus()); response.close(); // Assert "mygroup2" was moved group1 = realm.groups().group(group1.getId()).toRepresentation(); group2 = realm.groups().group(group2.getId()).toRepresentation(); assertTrue(group1.getSubGroups().isEmpty()); - Assert.assertEquals("/mygroup2", group2.getPath()); + assertEquals("/mygroup2", group2.getPath()); } @Test @@ -601,7 +610,7 @@ public class GroupTest extends AbstractGroupTest { assertNames(members, "user-b"); } - + @Test //KEYCLOAK-6300 public void groupMembershipUsersOrder() { @@ -615,20 +624,20 @@ public class GroupTest extends AbstractGroupTest { for (int i = 0; i < 9; i++) { UserRepresentation user = UserBuilder.create().username("user" + i).build(); usernames.add(user.getUsername()); - + try (Response create = realm.users().create(user)) { assertEquals(Status.CREATED, create.getStatusInfo()); - + String userAId = ApiUtil.getCreatedId(create); realm.users().get(userAId).joinGroup(groupId); } } - + List memberUsernames = new ArrayList<>(); for (UserRepresentation member : realm.groups().group(groupId).members(0, 10)) { memberUsernames.add(member.getUsername()); } - assertArrayEquals("Expected: " + usernames + ", was: " + memberUsernames, + assertArrayEquals("Expected: " + usernames + ", was: " + memberUsernames, usernames.toArray(), memberUsernames.toArray()); } @@ -865,7 +874,7 @@ public class GroupTest extends AbstractGroupTest { GroupRepresentation group = GroupBuilder.create().name(groupName).build(); try (Response response = realm.groups().add(group)) { String groupId = ApiUtil.getCreatedId(response); - + RoleMappingResource mappings = realm.groups().group(groupId).roles(); mappings.realmLevel().add(Collections.singletonList(adminRole)); @@ -996,41 +1005,74 @@ public class GroupTest extends AbstractGroupTest { assertEquals(110, group.members(-1, -2).size()); } } - + @Test public void getGroupsWithFullRepresentation() { RealmResource realm = adminClient.realms().realm("test"); GroupsResource groupsResource = adminClient.realms().realm("test").groups(); - + GroupRepresentation group = new GroupRepresentation(); group.setName("groupWithAttribute"); - + Map> attributes = new HashMap>(); attributes.put("attribute1", Arrays.asList("attribute1","attribute2")); group.setAttributes(attributes); group = createGroup(realm, group); - + List groups = groupsResource.groups("groupWithAttribute", 0, 20, false); - + assertFalse(groups.isEmpty()); assertTrue(groups.get(0).getAttributes().containsKey("attribute1")); } - + + @Test + public void searchGroupsByNameContaining() { + RealmResource realm = adminClient.realms().realm("test"); + try(Creator g = Creator.create(realm, GroupBuilder.create().name("group-name-1").build()); + Creator g1 = Creator.create(realm, GroupBuilder.create().name("group-name-2").build())) { + GroupsResource groupsResource = adminClient.realms().realm("test").groups(); + List groups = groupsResource.groups("group-name", false, 0, 20, false); + assertThat(groups, hasSize(2)); + } + } + + @Test + public void searchGroupsByNameExactSuccess() { + RealmResource realm = adminClient.realms().realm("test"); + try(Creator g = Creator.create(realm, GroupBuilder.create().name("group-name-1").build()); + Creator g1 = Creator.create(realm, GroupBuilder.create().name("group-name-2").build())) { + GroupsResource groupsResource = adminClient.realms().realm("test").groups(); + List groups = groupsResource.groups("group-name-1", true, 0, 20, false); + assertThat(groups, hasSize(1)); + } + } + + @Test + public void searchGroupsByNameExactFailure() { + RealmResource realm = adminClient.realms().realm("test"); + try(Creator g = Creator.create(realm, GroupBuilder.create().name("group-name-1").build()); + Creator g1 = Creator.create(realm, GroupBuilder.create().name("group-name-2").build())) { + GroupsResource groupsResource = adminClient.realms().realm("test").groups(); + List groups = groupsResource.groups("group-name", true, 0, 20, false); + assertThat(groups, empty()); + } + } + @Test public void getGroupsWithBriefRepresentation() { RealmResource realm = adminClient.realms().realm("test"); GroupsResource groupsResource = adminClient.realms().realm("test").groups(); - + GroupRepresentation group = new GroupRepresentation(); group.setName("groupWithAttribute"); - + Map> attributes = new HashMap>(); attributes.put("attribute1", Arrays.asList("attribute1","attribute2")); group.setAttributes(attributes); group = createGroup(realm, group); - + List groups = groupsResource.groups("groupWithAttribute", 0, 20); - + assertFalse(groups.isEmpty()); assertNull(groups.get(0).getAttributes()); } @@ -1145,7 +1187,7 @@ public class GroupTest extends AbstractGroupTest { assertTrue(Comparators.isInStrictOrder(secondPage, compareByName)); // Check that the ordering of groups across multiple pages is correct - // Since the individual pages are ordered it is sufficient to compare + // Since the individual pages are ordered it is sufficient to compare // every group from the first page to the first group of the second page GroupRepresentation firstGroupOnSecondPage = secondPage.get(0); for (GroupRepresentation firstPageGroup : firstPage) { @@ -1213,17 +1255,17 @@ public class GroupTest extends AbstractGroupTest { final List searchResultGroups = realm.groups().groups(searchFor, 0, 10); - Assert.assertFalse(searchResultGroups.isEmpty()); - Assert.assertEquals(expectedRootGroup.getId(), searchResultGroups.get(0).getId()); - Assert.assertEquals(expectedRootGroup.getName(), searchResultGroups.get(0).getName()); + assertFalse(searchResultGroups.isEmpty()); + assertEquals(expectedRootGroup.getId(), searchResultGroups.get(0).getId()); + assertEquals(expectedRootGroup.getName(), searchResultGroups.get(0).getName()); List searchResultSubGroups = searchResultGroups.get(0).getSubGroups(); - Assert.assertEquals(expectedChildGroup.getId(), searchResultSubGroups.get(0).getId()); - Assert.assertEquals(expectedChildGroup.getName(), searchResultSubGroups.get(0).getName()); + assertEquals(expectedChildGroup.getId(), searchResultSubGroups.get(0).getId()); + assertEquals(expectedChildGroup.getName(), searchResultSubGroups.get(0).getName()); searchResultSubGroups.remove(0); - Assert.assertTrue(searchResultSubGroups.isEmpty()); + assertTrue(searchResultSubGroups.isEmpty()); searchResultGroups.remove(0); - Assert.assertTrue(searchResultGroups.isEmpty()); + assertTrue(searchResultGroups.isEmpty()); } }