enhance existing group search functionality allow exact name search keycloak/keycloak#13973

Co-authored-by: Abhijeet Gandhewar <agandhew@redhat.com>
This commit is contained in:
Alice Wood 2022-09-29 15:19:07 -05:00 committed by Hynek Mlnařík
parent a20d6e2f1f
commit 1eb7e95b97
16 changed files with 238 additions and 80 deletions

View file

@ -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<GroupRepresentation> 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.

View file

@ -1478,8 +1478,9 @@ public class RealmAdapter implements CachedRealmModel {
}
@Override
@Deprecated
public Stream<GroupModel> searchForGroupByNameStream(String search, Integer first, Integer max) {
return cacheSession.searchForGroupByNameStream(this, search, first, max);
return cacheSession.searchForGroupByNameStream( this, search, false, first, max);
}
@Override

View file

@ -1025,7 +1025,12 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override
public Stream<GroupModel> 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<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Boolean exact, Integer firstResult, Integer maxResults) {
return getGroupDelegate().searchForGroupByNameStream(realm, search, exact, firstResult, maxResults);
}
@Override

View file

@ -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<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer first, Integer max) {
TypedQuery<String> query = em.createNamedQuery("getGroupIdsByNameContaining", String.class)
.setParameter("realm", realm.getId())
.setParameter("search", search);
return searchForGroupByNameStream(realm, search, false, first, max);
}
@Override
public Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Boolean exact, Integer first, Integer max) {
TypedQuery<String> 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<String> 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<GroupModel> searchGroupsByAttributes(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) {
Map<String, String> filteredAttributes = groupSearchableAttributes == null || groupSearchableAttributes.isEmpty()

View file

@ -1938,8 +1938,9 @@ public class RealmAdapter implements LegacyRealmModel, JpaModel<RealmEntity> {
}
@Override
@Deprecated
public Stream<GroupModel> 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

View file

@ -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"),

View file

@ -64,12 +64,9 @@ public class GroupStorageManager extends AbstractStorageManager<GroupStorageProv
*
*/
@Override
@Deprecated
public Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
Stream<GroupModel> local = localStorage().searchForGroupByNameStream(realm, search, firstResult, maxResults);
Stream<GroupModel> 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<GroupStorageProv
return Stream.concat(local, ext);
}
/**
* Obtaining groups from an external client storage is time-bounded. In case the external group storage
* isn't available at least groups from a local storage are returned. For this purpose
* the {@link org.keycloak.services.DefaultKeycloakSessionFactory#getClientStorageProviderTimeout()} property is used.
* Default value is 3000 milliseconds and it's configurable.
* See {@link org.keycloak.services.DefaultKeycloakSessionFactory} for details.
*
*/
@Override
public Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Boolean exact, Integer firstResult, Integer maxResults) {
Stream<GroupModel> local = localStorage().searchForGroupByNameStream(realm, search, exact, firstResult, maxResults);
Stream<GroupModel> 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

View file

@ -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<GroupModel> 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<GroupModel> 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<GroupModel> 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

View file

@ -1425,8 +1425,9 @@ public class MapRealmAdapter extends AbstractRealmModel<MapRealmEntity> implemen
}
@Override
@Deprecated
public Stream<GroupModel> 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

View file

@ -393,8 +393,8 @@ public class MapRealmProvider implements RealmProvider {
@Override
@Deprecated
public Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
return session.groups().searchForGroupByNameStream(realm, search, firstResult, maxResults);
public Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Boolean exact, Integer firstResult, Integer maxResults) {
return session.groups().searchForGroupByNameStream(realm, search, exact, firstResult, maxResults);
}
@Override

View file

@ -177,9 +177,15 @@ public class ModelToRepresentation {
.map(g -> toGroupHierarchy(g, full, attributes));
}
@Deprecated
public static Stream<GroupRepresentation> 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<GroupRepresentation> 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<GroupRepresentation> 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<GroupRepresentation> 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());

View file

@ -846,7 +846,7 @@ public interface RealmModel extends RoleContainerModel {
Stream<GroupModel> 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<GroupModel> 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<GroupModel> searchForGroupByNameStream(String search, Integer first, Integer max);
boolean removeGroup(GroupModel group);

View file

@ -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<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults);
@Deprecated
default Stream<GroupModel> 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<GroupModel> searchGroupsByAttributes(RealmModel realm, Map<String, String> 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<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Boolean exact, Integer firstResult, Integer maxResults);
}

View file

@ -77,6 +77,7 @@ public class GroupsResource {
@Produces(MediaType.APPLICATION_JSON)
public Stream<GroupRepresentation> 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<String, String> 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 {

View file

@ -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<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
return searchForGroupByNameStream(realm, search, false, firstResult, maxResults);
}
@Override
public Stream<GroupModel> 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();

View file

@ -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<String> 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<String, List<String>> attributes = new HashMap<String, List<String>>();
attributes.put("attribute1", Arrays.asList("attribute1","attribute2"));
group.setAttributes(attributes);
group = createGroup(realm, group);
List<GroupRepresentation> 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<GroupResource> g = Creator.create(realm, GroupBuilder.create().name("group-name-1").build());
Creator<GroupResource> g1 = Creator.create(realm, GroupBuilder.create().name("group-name-2").build())) {
GroupsResource groupsResource = adminClient.realms().realm("test").groups();
List<GroupRepresentation> 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<GroupResource> g = Creator.create(realm, GroupBuilder.create().name("group-name-1").build());
Creator<GroupResource> g1 = Creator.create(realm, GroupBuilder.create().name("group-name-2").build())) {
GroupsResource groupsResource = adminClient.realms().realm("test").groups();
List<GroupRepresentation> 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<GroupResource> g = Creator.create(realm, GroupBuilder.create().name("group-name-1").build());
Creator<GroupResource> g1 = Creator.create(realm, GroupBuilder.create().name("group-name-2").build())) {
GroupsResource groupsResource = adminClient.realms().realm("test").groups();
List<GroupRepresentation> 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<String, List<String>> attributes = new HashMap<String, List<String>>();
attributes.put("attribute1", Arrays.asList("attribute1","attribute2"));
group.setAttributes(attributes);
group = createGroup(realm, group);
List<GroupRepresentation> 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<GroupRepresentation> 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<GroupRepresentation> 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());
}
}