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("first") Integer first,
@QueryParam("max") Integer max, @QueryParam("max") Integer max,
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation); @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. * Counts all groups.
* @return A map containing key "count" with number of groups as value. * @return A map containing key "count" with number of groups as value.

View file

@ -1478,8 +1478,9 @@ public class RealmAdapter implements CachedRealmModel {
} }
@Override @Override
@Deprecated
public Stream<GroupModel> searchForGroupByNameStream(String search, Integer first, Integer max) { 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 @Override

View file

@ -1025,7 +1025,12 @@ public class RealmCacheSession implements CacheRealmProvider {
@Override @Override
public Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer first, Integer max) { 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 @Override

View file

@ -40,6 +40,7 @@ import javax.persistence.criteria.Join;
import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root; import javax.persistence.criteria.Root;
import org.apache.commons.lang.BooleanUtils;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.common.util.Time; import org.keycloak.common.util.Time;
import org.keycloak.connections.jpa.util.JpaUtils; import org.keycloak.connections.jpa.util.JpaUtils;
@ -937,10 +938,19 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
@Override @Override
public Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer first, Integer max) { public Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer first, Integer max) {
TypedQuery<String> query = em.createNamedQuery("getGroupIdsByNameContaining", String.class) return searchForGroupByNameStream(realm, search, false, first, max);
.setParameter("realm", realm.getId()) }
.setParameter("search", search);
@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(); Stream<String> groups = paginateQuery(query, first, max).getResultStream();
return closing(groups.map(id -> { return closing(groups.map(id -> {
@ -951,7 +961,6 @@ public class JpaRealmProvider implements RealmProvider, ClientProvider, ClientSc
return groupById; return groupById;
}).sorted(GroupModel.COMPARE_BY_NAME).distinct()); }).sorted(GroupModel.COMPARE_BY_NAME).distinct());
} }
@Override @Override
public Stream<GroupModel> searchGroupsByAttributes(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) { public Stream<GroupModel> searchGroupsByAttributes(RealmModel realm, Map<String, String> attributes, Integer firstResult, Integer maxResults) {
Map<String, String> filteredAttributes = groupSearchableAttributes == null || groupSearchableAttributes.isEmpty() Map<String, String> filteredAttributes = groupSearchableAttributes == null || groupSearchableAttributes.isEmpty()

View file

@ -1938,8 +1938,9 @@ public class RealmAdapter implements LegacyRealmModel, JpaModel<RealmEntity> {
} }
@Override @Override
@Deprecated
public Stream<GroupModel> searchForGroupByNameStream(String search, Integer first, Integer max) { 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 @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="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="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="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="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="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"), @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 @Override
@Deprecated
public Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) { public Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) {
Stream<GroupModel> local = localStorage().searchForGroupByNameStream(realm, search, firstResult, maxResults); return searchForGroupByNameStream(realm, search, false, firstResult, maxResults);
Stream<GroupModel> ext = flatMapEnabledStorageProvidersWithTimeout(realm, GroupLookupProvider.class,
p -> p.searchForGroupByNameStream(realm, search, firstResult, maxResults));
return Stream.concat(local, ext);
} }
@Override @Override
@ -81,6 +78,22 @@ public class GroupStorageManager extends AbstractStorageManager<GroupStorageProv
return Stream.concat(local, ext); 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) */ /* GROUP PROVIDER METHODS - provided only by local storage (e.g. not supported by storage providers) */
@Override @Override

View file

@ -17,6 +17,7 @@
package org.keycloak.models.map.group; package org.keycloak.models.map.group;
import java.security.Key;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;
import org.keycloak.models.GroupModel.SearchableFields; import org.keycloak.models.GroupModel.SearchableFields;
@ -167,24 +168,34 @@ public class MapGroupProvider implements GroupProvider {
} }
@Override @Override
@Deprecated
public Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) { 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(); DefaultModelCriteria<GroupModel> mcb = criteria();
mcb = mcb.compare(SearchableFields.REALM_ID, Operator.EQ, realm.getId()) if (exact != null && exact.equals(Boolean.TRUE)) {
.compare(SearchableFields.NAME, Operator.ILIKE, "%" + search + "%"); 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)) return tx.read(withCriteria(mcb).pagination(firstResult, maxResults, SearchableFields.NAME))
.map(MapGroupEntity::getId) .map(MapGroupEntity::getId)
.map(id -> { .map(id -> {
GroupModel groupById = session.groups().getGroupById(realm, id); GroupModel groupById = session.groups().getGroupById(realm, id);
while (Objects.nonNull(groupById.getParentId())) { while (Objects.nonNull(groupById.getParentId())) {
groupById = session.groups().getGroupById(realm, groupById.getParentId()); groupById = session.groups().getGroupById(realm, groupById.getParentId());
} }
return groupById; return groupById;
}).sorted(GroupModel.COMPARE_BY_NAME).distinct(); }).sorted(GroupModel.COMPARE_BY_NAME).distinct();
} }
@Override @Override

View file

@ -1425,8 +1425,9 @@ public class MapRealmAdapter extends AbstractRealmModel<MapRealmEntity> implemen
} }
@Override @Override
@Deprecated
public Stream<GroupModel> searchForGroupByNameStream(String search, Integer first, Integer max) { 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 @Override

View file

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

View file

@ -177,9 +177,15 @@ public class ModelToRepresentation {
.map(g -> toGroupHierarchy(g, full, attributes)); .map(g -> toGroupHierarchy(g, full, attributes));
} }
@Deprecated
public static Stream<GroupRepresentation> searchForGroupByName(RealmModel realm, boolean full, String search, Integer first, Integer max) { public static Stream<GroupRepresentation> searchForGroupByName(RealmModel realm, boolean full, String search, Integer first, Integer max) {
return realm.searchForGroupByNameStream(search, first, 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) { 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); return toGroupHierarchy(group, full, (String) null);
} }
@Deprecated
public static GroupRepresentation toGroupHierarchy(GroupModel group, boolean full, String search) { 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); GroupRepresentation rep = toRepresentation(group, full);
List<GroupRepresentation> subGroups = group.getSubGroupsStream() List<GroupRepresentation> subGroups = group.getSubGroupsStream()
.filter(g -> groupMatchesSearchOrIsPathElement(g, search)) .filter(g -> groupMatchesSearchOrIsPathElement(g, search, exact))
.map(subGroup -> toGroupHierarchy(subGroup, full, search)).collect(Collectors.toList()); .map(subGroup -> toGroupHierarchy(subGroup, full, search, exact)).collect(Collectors.toList());
rep.setSubGroups(subGroups); rep.setSubGroups(subGroups);
return rep; return rep;
} }
@ -228,16 +239,24 @@ public class ModelToRepresentation {
return rep; return rep;
} }
private static boolean groupMatchesSearchOrIsPathElement(GroupModel group, String search) { private static boolean groupMatchesSearchOrIsPathElement(GroupModel group, String search, Boolean exact) {
if (StringUtil.isBlank(search)) { if (StringUtil.isBlank(search)) {
return true; return true;
} }
if (group.getName().contains(search)) { if(exact !=null && exact.equals(true)){
return true; if (group.getName().equals(search)){
return true;
}
} else {
if (group.getName().contains(search)) {
return true;
}
} }
return group.getSubGroupsStream().findAny().isPresent(); return group.getSubGroupsStream().findAny().isPresent();
} }
public static UserRepresentation toRepresentation(KeycloakSession session, RealmModel realm, UserModel user) { public static UserRepresentation toRepresentation(KeycloakSession session, RealmModel realm, UserModel user) {
UserRepresentation rep = new UserRepresentation(); UserRepresentation rep = new UserRepresentation();
rep.setId(user.getId()); rep.setId(user.getId());

View file

@ -846,7 +846,7 @@ public interface RealmModel extends RoleContainerModel {
Stream<GroupModel> getTopLevelGroupsStream(Integer first, Integer max); 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 @Deprecated
default List<GroupModel> searchForGroupByName(String search, Integer first, Integer max) { 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 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}. * @param max {@code Integer} Maximum number of returned groups. Ignored if negative or {@code null}.
* @return Stream of {@link GroupModel}. Never returns {@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); Stream<GroupModel> searchForGroupByNameStream(String search, Integer first, Integer max);
boolean removeGroup(GroupModel group); 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}. * @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. * @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}. * 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. * 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); 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) @Produces(MediaType.APPLICATION_JSON)
public Stream<GroupRepresentation> getGroups(@QueryParam("search") String search, public Stream<GroupRepresentation> getGroups(@QueryParam("search") String search,
@QueryParam("q") String searchQuery, @QueryParam("q") String searchQuery,
@QueryParam("exact") @DefaultValue("false") Boolean exact,
@QueryParam("first") Integer firstResult, @QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults, @QueryParam("max") Integer maxResults,
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) { @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) {
@ -86,7 +87,7 @@ public class GroupsResource {
Map<String, String> attributes = SearchQueryUtils.getFields(searchQuery); Map<String, String> attributes = SearchQueryUtils.getFields(searchQuery);
return ModelToRepresentation.searchGroupsByAttributes(session, realm, !briefRepresentation, attributes, firstResult, maxResults); return ModelToRepresentation.searchGroupsByAttributes(session, realm, !briefRepresentation, attributes, firstResult, maxResults);
} else if (Objects.nonNull(search)) { } 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)) { } else if(Objects.nonNull(firstResult) && Objects.nonNull(maxResults)) {
return ModelToRepresentation.toGroupHierarchy(realm, !briefRepresentation, firstResult, maxResults); return ModelToRepresentation.toGroupHierarchy(realm, !briefRepresentation, firstResult, maxResults);
} else { } else {

View file

@ -16,6 +16,7 @@
*/ */
package org.keycloak.testsuite.federation; package org.keycloak.testsuite.federation;
import org.apache.commons.lang.BooleanUtils;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;
@ -53,15 +54,27 @@ public class HardcodedGroupStorageProvider implements GroupStorageProvider {
} }
@Override @Override
@Deprecated
public Stream<GroupModel> searchForGroupByNameStream(RealmModel realm, String search, Integer firstResult, Integer maxResults) { 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 { if (Boolean.parseBoolean(component.getConfig().getFirst(HardcodedGroupStorageProviderFactory.DELAYED_SEARCH))) try {
Thread.sleep(5000l); Thread.sleep(5000l);
} catch (InterruptedException ex) { } catch (InterruptedException ex) {
Logger.getLogger(HardcodedGroupStorageProvider.class).warn(ex.getCause()); Logger.getLogger(HardcodedGroupStorageProvider.class).warn(ex.getCause());
return Stream.empty(); return Stream.empty();
} }
if (search != null && this.groupName.toLowerCase().contains(search.toLowerCase())) { if(BooleanUtils.isTrue(exact)){
return Stream.of(new HardcodedGroupAdapter(realm)); 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(); return Stream.empty();

View file

@ -40,6 +40,7 @@ import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.updaters.Creator;
import org.keycloak.testsuite.util.AdminEventPaths; import org.keycloak.testsuite.util.AdminEventPaths;
import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.RoleBuilder; import org.keycloak.testsuite.util.RoleBuilder;
@ -67,14 +68,22 @@ import javax.ws.rs.ClientErrorException;
import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.Status;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import org.junit.Rule; import org.junit.Rule;
import org.junit.rules.ExpectedException; 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.admin.client.Keycloak;
import org.keycloak.models.AdminRoles; import org.keycloak.models.AdminRoles;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.keycloak.testsuite.Assert.assertNames; import static org.keycloak.testsuite.Assert.assertNames;
import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot; import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot;
@ -182,7 +191,7 @@ public class GroupTest extends AbstractGroupTest {
response.close(); response.close();
assertEquals(409, response.getStatus()); // conflict status 409 - same name not allowed assertEquals(409, response.getStatus()); // conflict status 409 - same name not allowed
} }
@Test @Test
// KEYCLOAK-11412 Unintended Groups with same names // KEYCLOAK-11412 Unintended Groups with same names
public void doNotAllowSameGroupNameAtSameLevelWhenUpdatingName() throws Exception { public void doNotAllowSameGroupNameAtSameLevelWhenUpdatingName() throws Exception {
@ -195,9 +204,9 @@ public class GroupTest extends AbstractGroupTest {
GroupRepresentation anotherTopGroup = new GroupRepresentation(); GroupRepresentation anotherTopGroup = new GroupRepresentation();
anotherTopGroup.setName("top2"); anotherTopGroup.setName("top2");
anotherTopGroup = createGroup(realm, anotherTopGroup); anotherTopGroup = createGroup(realm, anotherTopGroup);
anotherTopGroup.setName("top1"); anotherTopGroup.setName("top1");
try { try {
realm.groups().group(anotherTopGroup.getId()).update(anotherTopGroup); realm.groups().group(anotherTopGroup.getId()).update(anotherTopGroup);
Assert.fail("Expected ClientErrorException"); Assert.fail("Expected ClientErrorException");
@ -215,14 +224,14 @@ public class GroupTest extends AbstractGroupTest {
addSubGroup(realm, topGroup, anotherlevel2Group); addSubGroup(realm, topGroup, anotherlevel2Group);
anotherlevel2Group.setName("level2-1"); anotherlevel2Group.setName("level2-1");
try { try {
realm.groups().group(anotherlevel2Group.getId()).update(anotherlevel2Group); realm.groups().group(anotherlevel2Group.getId()).update(anotherlevel2Group);
Assert.fail("Expected ClientErrorException"); Assert.fail("Expected ClientErrorException");
} catch (ClientErrorException e) { } catch (ClientErrorException e) {
// conflict status 409 - same name not allowed // conflict status 409 - same name not allowed
assertEquals("HTTP 409 Conflict", e.getMessage()); assertEquals("HTTP 409 Conflict", e.getMessage());
} }
} }
@Test @Test
@ -295,7 +304,7 @@ public class GroupTest extends AbstractGroupTest {
Assert.fail("Creating a group with empty name should fail"); Assert.fail("Creating a group with empty name should fail");
} }
} catch (Exception expected) { } catch (Exception expected) {
Assert.assertNotNull(expected); assertNotNull(expected);
} }
group.setName(null); group.setName(null);
@ -304,7 +313,7 @@ public class GroupTest extends AbstractGroupTest {
Assert.fail("Creating a group with null name should fail"); Assert.fail("Creating a group with null name should fail");
} }
} catch (Exception expected) { } catch (Exception expected) {
Assert.assertNotNull(expected); assertNotNull(expected);
} }
} }
@ -327,7 +336,7 @@ public class GroupTest extends AbstractGroupTest {
realm.groups().group(groupId).update(group); realm.groups().group(groupId).update(group);
Assert.fail("Updating a group with empty name should fail"); Assert.fail("Updating a group with empty name should fail");
} catch(Exception expected) { } catch(Exception expected) {
Assert.assertNotNull(expected); assertNotNull(expected);
} }
try { try {
@ -335,7 +344,7 @@ public class GroupTest extends AbstractGroupTest {
realm.groups().group(groupId).update(group); realm.groups().group(groupId).update(group);
Assert.fail("Updating a group with null name should fail"); Assert.fail("Updating a group with null name should fail");
} catch(Exception expected) { } catch(Exception expected) {
Assert.assertNotNull(expected); assertNotNull(expected);
} }
} }
@ -379,7 +388,7 @@ public class GroupTest extends AbstractGroupTest {
}); });
level2Group = realm.getGroupByPath("/top/level2"); level2Group = realm.getGroupByPath("/top/level2");
Assert.assertNotNull(level2Group); assertNotNull(level2Group);
roles.clear(); roles.clear();
roles.add(level2Role); roles.add(level2Role);
realm.groups().group(level2Group.getId()).roles().realmLevel().add(roles); 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); assertAdminEvents.assertEvent(testRealmId, OperationType.CREATE, AdminEventPaths.groupSubgroupsPath(level2Group.getId()), level3Group, ResourceType.GROUP);
level3Group = realm.getGroupByPath("/top/level2/level3"); level3Group = realm.getGroupByPath("/top/level2/level3");
Assert.assertNotNull(level3Group); assertNotNull(level3Group);
roles.clear(); roles.clear();
roles.add(level3Role); roles.add(level3Role);
realm.groups().group(level3Group.getId()).roles().realmLevel().add(roles); realm.groups().group(level3Group.getId()).roles().realmLevel().add(roles);
@ -473,7 +482,7 @@ public class GroupTest extends AbstractGroupTest {
} }
catch (NotFoundException e) {} 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 @Test
@ -489,7 +498,7 @@ public class GroupTest extends AbstractGroupTest {
createGroup(realm, group); createGroup(realm, group);
group = realm.getGroupByPath("/" + groupName); group = realm.getGroupByPath("/" + groupName);
Assert.assertNotNull(group); assertNotNull(group);
assertThat(group.getName(), is(groupName)); assertThat(group.getName(), is(groupName));
assertThat(group.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2")); assertThat(group.getAttributes().keySet(), containsInAnyOrder("attr1", "attr2"));
assertThat(group.getAttributes(), hasEntry(is("attr1"), contains("attrval1"))); 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 // Move "mygroup2" as child of "mygroup1" . Assert it was moved
Response response = realm.groups().group(group1.getId()).subGroup(group2); Response response = realm.groups().group(group1.getId()).subGroup(group2);
Assert.assertEquals(204, response.getStatus()); assertEquals(204, response.getStatus());
response.close(); response.close();
// Assert "mygroup2" was moved // Assert "mygroup2" was moved
group1 = realm.groups().group(group1.getId()).toRepresentation(); group1 = realm.groups().group(group1.getId()).toRepresentation();
group2 = realm.groups().group(group2.getId()).toRepresentation(); group2 = realm.groups().group(group2.getId()).toRepresentation();
assertNames(group1.getSubGroups(), "mygroup2"); assertNames(group1.getSubGroups(), "mygroup2");
Assert.assertEquals("/mygroup1/mygroup2", group2.getPath()); assertEquals("/mygroup1/mygroup2", group2.getPath());
assertAdminEvents.clear(); 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 // 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" // of "mygroup1" with name "mygroup2"
response = realm.groups().group(group1.getId()).subGroup(group3); response = realm.groups().group(group1.getId()).subGroup(group3);
Assert.assertEquals(409, response.getStatus()); assertEquals(409, response.getStatus());
realm.groups().group(group3.getId()).remove(); realm.groups().group(group3.getId()).remove();
// Move "mygroup2" back under parent // Move "mygroup2" back under parent
response = realm.groups().add(group2); response = realm.groups().add(group2);
Assert.assertEquals(204, response.getStatus()); assertEquals(204, response.getStatus());
response.close(); response.close();
// Assert "mygroup2" was moved // Assert "mygroup2" was moved
group1 = realm.groups().group(group1.getId()).toRepresentation(); group1 = realm.groups().group(group1.getId()).toRepresentation();
group2 = realm.groups().group(group2.getId()).toRepresentation(); group2 = realm.groups().group(group2.getId()).toRepresentation();
assertTrue(group1.getSubGroups().isEmpty()); assertTrue(group1.getSubGroups().isEmpty());
Assert.assertEquals("/mygroup2", group2.getPath()); assertEquals("/mygroup2", group2.getPath());
} }
@Test @Test
@ -601,7 +610,7 @@ public class GroupTest extends AbstractGroupTest {
assertNames(members, "user-b"); assertNames(members, "user-b");
} }
@Test @Test
//KEYCLOAK-6300 //KEYCLOAK-6300
public void groupMembershipUsersOrder() { public void groupMembershipUsersOrder() {
@ -615,20 +624,20 @@ public class GroupTest extends AbstractGroupTest {
for (int i = 0; i < 9; i++) { for (int i = 0; i < 9; i++) {
UserRepresentation user = UserBuilder.create().username("user" + i).build(); UserRepresentation user = UserBuilder.create().username("user" + i).build();
usernames.add(user.getUsername()); usernames.add(user.getUsername());
try (Response create = realm.users().create(user)) { try (Response create = realm.users().create(user)) {
assertEquals(Status.CREATED, create.getStatusInfo()); assertEquals(Status.CREATED, create.getStatusInfo());
String userAId = ApiUtil.getCreatedId(create); String userAId = ApiUtil.getCreatedId(create);
realm.users().get(userAId).joinGroup(groupId); realm.users().get(userAId).joinGroup(groupId);
} }
} }
List<String> memberUsernames = new ArrayList<>(); List<String> memberUsernames = new ArrayList<>();
for (UserRepresentation member : realm.groups().group(groupId).members(0, 10)) { for (UserRepresentation member : realm.groups().group(groupId).members(0, 10)) {
memberUsernames.add(member.getUsername()); memberUsernames.add(member.getUsername());
} }
assertArrayEquals("Expected: " + usernames + ", was: " + memberUsernames, assertArrayEquals("Expected: " + usernames + ", was: " + memberUsernames,
usernames.toArray(), memberUsernames.toArray()); usernames.toArray(), memberUsernames.toArray());
} }
@ -865,7 +874,7 @@ public class GroupTest extends AbstractGroupTest {
GroupRepresentation group = GroupBuilder.create().name(groupName).build(); GroupRepresentation group = GroupBuilder.create().name(groupName).build();
try (Response response = realm.groups().add(group)) { try (Response response = realm.groups().add(group)) {
String groupId = ApiUtil.getCreatedId(response); String groupId = ApiUtil.getCreatedId(response);
RoleMappingResource mappings = realm.groups().group(groupId).roles(); RoleMappingResource mappings = realm.groups().group(groupId).roles();
mappings.realmLevel().add(Collections.singletonList(adminRole)); mappings.realmLevel().add(Collections.singletonList(adminRole));
@ -996,41 +1005,74 @@ public class GroupTest extends AbstractGroupTest {
assertEquals(110, group.members(-1, -2).size()); assertEquals(110, group.members(-1, -2).size());
} }
} }
@Test @Test
public void getGroupsWithFullRepresentation() { public void getGroupsWithFullRepresentation() {
RealmResource realm = adminClient.realms().realm("test"); RealmResource realm = adminClient.realms().realm("test");
GroupsResource groupsResource = adminClient.realms().realm("test").groups(); GroupsResource groupsResource = adminClient.realms().realm("test").groups();
GroupRepresentation group = new GroupRepresentation(); GroupRepresentation group = new GroupRepresentation();
group.setName("groupWithAttribute"); group.setName("groupWithAttribute");
Map<String, List<String>> attributes = new HashMap<String, List<String>>(); Map<String, List<String>> attributes = new HashMap<String, List<String>>();
attributes.put("attribute1", Arrays.asList("attribute1","attribute2")); attributes.put("attribute1", Arrays.asList("attribute1","attribute2"));
group.setAttributes(attributes); group.setAttributes(attributes);
group = createGroup(realm, group); group = createGroup(realm, group);
List<GroupRepresentation> groups = groupsResource.groups("groupWithAttribute", 0, 20, false); List<GroupRepresentation> groups = groupsResource.groups("groupWithAttribute", 0, 20, false);
assertFalse(groups.isEmpty()); assertFalse(groups.isEmpty());
assertTrue(groups.get(0).getAttributes().containsKey("attribute1")); 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 @Test
public void getGroupsWithBriefRepresentation() { public void getGroupsWithBriefRepresentation() {
RealmResource realm = adminClient.realms().realm("test"); RealmResource realm = adminClient.realms().realm("test");
GroupsResource groupsResource = adminClient.realms().realm("test").groups(); GroupsResource groupsResource = adminClient.realms().realm("test").groups();
GroupRepresentation group = new GroupRepresentation(); GroupRepresentation group = new GroupRepresentation();
group.setName("groupWithAttribute"); group.setName("groupWithAttribute");
Map<String, List<String>> attributes = new HashMap<String, List<String>>(); Map<String, List<String>> attributes = new HashMap<String, List<String>>();
attributes.put("attribute1", Arrays.asList("attribute1","attribute2")); attributes.put("attribute1", Arrays.asList("attribute1","attribute2"));
group.setAttributes(attributes); group.setAttributes(attributes);
group = createGroup(realm, group); group = createGroup(realm, group);
List<GroupRepresentation> groups = groupsResource.groups("groupWithAttribute", 0, 20); List<GroupRepresentation> groups = groupsResource.groups("groupWithAttribute", 0, 20);
assertFalse(groups.isEmpty()); assertFalse(groups.isEmpty());
assertNull(groups.get(0).getAttributes()); assertNull(groups.get(0).getAttributes());
} }
@ -1145,7 +1187,7 @@ public class GroupTest extends AbstractGroupTest {
assertTrue(Comparators.isInStrictOrder(secondPage, compareByName)); assertTrue(Comparators.isInStrictOrder(secondPage, compareByName));
// Check that the ordering of groups across multiple pages is correct // 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 // every group from the first page to the first group of the second page
GroupRepresentation firstGroupOnSecondPage = secondPage.get(0); GroupRepresentation firstGroupOnSecondPage = secondPage.get(0);
for (GroupRepresentation firstPageGroup : firstPage) { for (GroupRepresentation firstPageGroup : firstPage) {
@ -1213,17 +1255,17 @@ public class GroupTest extends AbstractGroupTest {
final List<GroupRepresentation> searchResultGroups = realm.groups().groups(searchFor, 0, 10); final List<GroupRepresentation> searchResultGroups = realm.groups().groups(searchFor, 0, 10);
Assert.assertFalse(searchResultGroups.isEmpty()); assertFalse(searchResultGroups.isEmpty());
Assert.assertEquals(expectedRootGroup.getId(), searchResultGroups.get(0).getId()); assertEquals(expectedRootGroup.getId(), searchResultGroups.get(0).getId());
Assert.assertEquals(expectedRootGroup.getName(), searchResultGroups.get(0).getName()); assertEquals(expectedRootGroup.getName(), searchResultGroups.get(0).getName());
List<GroupRepresentation> searchResultSubGroups = searchResultGroups.get(0).getSubGroups(); List<GroupRepresentation> searchResultSubGroups = searchResultGroups.get(0).getSubGroups();
Assert.assertEquals(expectedChildGroup.getId(), searchResultSubGroups.get(0).getId()); assertEquals(expectedChildGroup.getId(), searchResultSubGroups.get(0).getId());
Assert.assertEquals(expectedChildGroup.getName(), searchResultSubGroups.get(0).getName()); assertEquals(expectedChildGroup.getName(), searchResultSubGroups.get(0).getName());
searchResultSubGroups.remove(0); searchResultSubGroups.remove(0);
Assert.assertTrue(searchResultSubGroups.isEmpty()); assertTrue(searchResultSubGroups.isEmpty());
searchResultGroups.remove(0); searchResultGroups.remove(0);
Assert.assertTrue(searchResultGroups.isEmpty()); assertTrue(searchResultGroups.isEmpty());
} }
} }