28818 - Reintroduce search by name for subgroups
Closes #28818 Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
parent
a3d67a2b64
commit
8fa2890f68
6 changed files with 123 additions and 27 deletions
|
@ -86,11 +86,13 @@ public interface GroupResource {
|
||||||
void remove();
|
void remove();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the paginated list of subgroups belonging to this group
|
* Get the paginated list of subgroups belonging to this group.
|
||||||
*
|
*
|
||||||
* @param first
|
* @param first the position of the first result to be returned.
|
||||||
* @param max
|
* @param max the maximum number of results that are to be returned.
|
||||||
* @param briefRepresentation
|
* @param briefRepresentation if {@code true}, each returned subgroup representation will only contain basic information
|
||||||
|
* (id, name, path, and parentId). If {@code false}, the complete representations of the subgroups
|
||||||
|
* are returned (include role mappings and attributes).
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
@Path("children")
|
@Path("children")
|
||||||
|
@ -98,6 +100,31 @@ public interface GroupResource {
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
List<GroupRepresentation> getSubGroups(@QueryParam("first") Integer first, @QueryParam("max") Integer max, @QueryParam("briefRepresentation") Boolean briefRepresentation);
|
List<GroupRepresentation> getSubGroups(@QueryParam("first") Integer first, @QueryParam("max") Integer max, @QueryParam("briefRepresentation") Boolean briefRepresentation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the paginated list of subgroups belonging to this group, filtered according to the specified parameters.
|
||||||
|
*
|
||||||
|
* @param search a {@code String} representing either an exact group name or a partial name. If empty or {@code null}
|
||||||
|
* then all subgroups of this group are returned.
|
||||||
|
* @param exact if {@code true}, the subgroups will be searched using exact match for the {@code search} param. If false
|
||||||
|
* or {@code null}, the method returns all subgroups that partially match the specified name.
|
||||||
|
* @param first the position of the first result to be returned.
|
||||||
|
* @param max the maximum number of results that are to be returned.
|
||||||
|
* @param briefRepresentation if {@code true}, each returned subgroup representation will only contain basic information
|
||||||
|
* (id, name, path, and parentId). If {@code false}, the complete representations of the subgroups
|
||||||
|
* are returned (including role mappings and attributes).
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("children")
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
List<GroupRepresentation> getSubGroups(
|
||||||
|
@QueryParam("search") String search,
|
||||||
|
@QueryParam("exact") Boolean exact,
|
||||||
|
@QueryParam("first") Integer first,
|
||||||
|
@QueryParam("max") Integer max,
|
||||||
|
@QueryParam("briefRepresentation") Boolean briefRepresentation);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set or create child. This will just set the parent if it exists. Create it and set the parent
|
* Set or create child. This will just set the parent if it exists. Create it and set the parent
|
||||||
* if the group doesn't exist.
|
* if the group doesn't exist.
|
||||||
|
|
|
@ -41,8 +41,8 @@ public interface GroupsResource {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get groups by pagination params.
|
* Get groups by pagination params.
|
||||||
* @param first index of the first element
|
* @param first index of the first element (pagination offset).
|
||||||
* @param max max number of occurrences
|
* @param max the maximum number of results.
|
||||||
* @return A list containing the slice of all groups.
|
* @return A list containing the slice of all groups.
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
|
@ -52,9 +52,9 @@ public interface GroupsResource {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get groups by pagination params.
|
* Get groups by pagination params.
|
||||||
* @param search max number of occurrences
|
* @param search A {@code String} representing either an exact or partial group name.
|
||||||
* @param first index of the first element
|
* @param first index of the first element (pagination offset).
|
||||||
* @param max max number of occurrences
|
* @param max the maximum number of results.
|
||||||
* @return A list containing the slice of all groups.
|
* @return A list containing the slice of all groups.
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
|
@ -66,10 +66,12 @@ public interface GroupsResource {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get groups by pagination params.
|
* Get groups by pagination params.
|
||||||
* @param search max number of occurrences
|
* @param search A {@code String} representing either an exact or partial group name.
|
||||||
* @param first index of the first element
|
* @param first index of the first element (pagination offset).
|
||||||
* @param max max number of occurrences
|
* @param max the maximum number of results.
|
||||||
* @param briefRepresentation if false, return groups with their attributes
|
* @param briefRepresentation if {@code true}, each returned group representation will only contain basic information
|
||||||
|
* (id, name, path, and parentId). If {@code false}, the complete representations of the groups
|
||||||
|
* are returned (including role mappings and attributes).
|
||||||
* @return A list containing the slice of all groups.
|
* @return A list containing the slice of all groups.
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
|
@ -82,11 +84,14 @@ public interface GroupsResource {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get groups by pagination params.
|
* Get groups by pagination params.
|
||||||
* @param search search string for group
|
* @param search A {@code String} representing either an exact or partial group name.
|
||||||
* @param exact exact match for search
|
* @param exact if {@code true}, the groups will be searched using exact match for the {@code search} param. If false,
|
||||||
* @param first index of the first element
|
* * the method returns all groups that partially match the specified name.
|
||||||
* @param max max number of occurrences
|
* @param first index of the first element (pagination offset).
|
||||||
* @param briefRepresentation if false, return groups with their attributes
|
* @param max the maximum number of results.
|
||||||
|
* @param briefRepresentation if {@code true}, each returned group representation will only contain basic information
|
||||||
|
* (id, name, path, and parentId). If {@code false}, the complete representations of the groups
|
||||||
|
* are returned (including role mappings and attributes).
|
||||||
* @return A list containing the slice of all groups.
|
* @return A list containing the slice of all groups.
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
|
|
|
@ -50,6 +50,7 @@ export const GroupTable = ({ refresh: viewRefresh }: GroupTableProps) => {
|
||||||
let groupsData = undefined;
|
let groupsData = undefined;
|
||||||
if (id) {
|
if (id) {
|
||||||
const args: SubGroupQuery = {
|
const args: SubGroupQuery = {
|
||||||
|
search: search || "",
|
||||||
first: first,
|
first: first,
|
||||||
max: max,
|
max: max,
|
||||||
parentId: id,
|
parentId: id,
|
||||||
|
|
|
@ -23,7 +23,8 @@ interface SummarizedQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GroupQuery = Query & PaginatedQuery & SummarizedQuery;
|
export type GroupQuery = Query & PaginatedQuery & SummarizedQuery;
|
||||||
export type SubGroupQuery = PaginatedQuery &
|
export type SubGroupQuery = Query &
|
||||||
|
PaginatedQuery &
|
||||||
SummarizedQuery & {
|
SummarizedQuery & {
|
||||||
parentId: string;
|
parentId: string;
|
||||||
};
|
};
|
||||||
|
@ -142,7 +143,7 @@ export class Groups extends Resource<{ realm?: string }> {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
path: "/{parentId}/children",
|
path: "/{parentId}/children",
|
||||||
urlParamKeys: ["parentId"],
|
urlParamKeys: ["parentId"],
|
||||||
queryParamKeys: ["first", "max", "briefRepresentation"],
|
queryParamKeys: ["search", "first", "max", "briefRepresentation"],
|
||||||
catchNotFound: true,
|
catchNotFound: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -159,15 +159,18 @@ public class GroupResource {
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@Tag(name = KeycloakOpenAPI.Admin.Tags.GROUPS)
|
@Tag(name = KeycloakOpenAPI.Admin.Tags.GROUPS)
|
||||||
@Operation( summary = "Return a paginated list of subgroups that have a parent group corresponding to the group on the URL")
|
@Operation( summary = "Return a paginated list of subgroups that have a parent group corresponding to the group on the URL")
|
||||||
public Stream<GroupRepresentation> getSubGroups(@QueryParam("first") @DefaultValue("0") Integer first,
|
public Stream<GroupRepresentation> getSubGroups(
|
||||||
@QueryParam("max") @DefaultValue("10") Integer max,
|
@Parameter(description = "A String representing either an exact group name or a partial name") @QueryParam("search") String search,
|
||||||
@QueryParam("briefRepresentation") @DefaultValue("false") Boolean briefRepresentation) {
|
@Parameter(description = "Boolean which defines whether the params \"search\" must match exactly or not") @QueryParam("exact") Boolean exact,
|
||||||
|
@Parameter(description = "The position of the first result to be returned (pagination offset).") @QueryParam("first") @DefaultValue("0") Integer first,
|
||||||
|
@Parameter(description = "The maximum number of results that are to be returned. Defaults to 10") @QueryParam("max") @DefaultValue("10") Integer max,
|
||||||
|
@Parameter(description = "Boolean which defines whether brief groups representations are returned or not (default: false)") @QueryParam("briefRepresentation") @DefaultValue("false") Boolean briefRepresentation) {
|
||||||
this.auth.groups().requireView(group);
|
this.auth.groups().requireView(group);
|
||||||
boolean canViewGlobal = auth.groups().canView();
|
boolean canViewGlobal = auth.groups().canView();
|
||||||
return paginatedStream(group.getSubGroupsStream(-1, -1)
|
return paginatedStream(
|
||||||
.filter(g -> canViewGlobal || auth.groups().canView(g))
|
group.getSubGroupsStream(search, exact, -1, -1)
|
||||||
.map(g -> GroupUtils.populateSubGroupCount(g, GroupUtils.toRepresentation(auth.groups(), g, !briefRepresentation)))
|
.filter(g -> canViewGlobal || auth.groups().canView(g)), first, max)
|
||||||
, first, max);
|
.map(g -> GroupUtils.populateSubGroupCount(g, GroupUtils.toRepresentation(auth.groups(), g, !briefRepresentation)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package org.keycloak.testsuite.admin.group;
|
package org.keycloak.testsuite.admin.group;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
|
import static org.hamcrest.CoreMatchers.not;
|
||||||
|
import static org.hamcrest.CoreMatchers.nullValue;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.anEmptyMap;
|
||||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
import static org.hamcrest.Matchers.empty;
|
import static org.hamcrest.Matchers.empty;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
@ -11,6 +14,7 @@ import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.jboss.arquillian.container.test.api.ContainerController;
|
import org.jboss.arquillian.container.test.api.ContainerController;
|
||||||
|
@ -214,6 +218,61 @@ public class GroupSearchTest extends AbstractGroupTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void querySubGroups() {
|
||||||
|
// create a parent group with a few subgroups.
|
||||||
|
try (Creator<GroupResource> parentGroupCreator = Creator.create(testRealmResource(), parentGroup)) {
|
||||||
|
for (int i = 1; i <= 5; i++) {
|
||||||
|
GroupRepresentation testGroup = new GroupRepresentation();
|
||||||
|
testGroup.setName("kcgroup-" + i);
|
||||||
|
testGroup.setAttributes(new HashMap<>() {{
|
||||||
|
put(ATTR_ORG_NAME, Collections.singletonList("keycloak org"));
|
||||||
|
put(ATTR_QUOTES_NAME, Collections.singletonList(ATTR_QUOTES_VAL));
|
||||||
|
}});
|
||||||
|
parentGroupCreator.resource().subGroup(testGroup);
|
||||||
|
}
|
||||||
|
for (int i = 1; i <= 3; i++) {
|
||||||
|
GroupRepresentation testGroup = new GroupRepresentation();
|
||||||
|
testGroup.setName("testgroup-" + i);
|
||||||
|
parentGroupCreator.resource().subGroup(testGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
// search for subgroups filtering by name - all groups with 'kc' in the name.
|
||||||
|
List<GroupRepresentation> subGroups = parentGroupCreator.resource().getSubGroups("kc", false, 0, 10, true);
|
||||||
|
assertThat(subGroups, hasSize(5));
|
||||||
|
for (int i = 1; i <= 5; i++) {
|
||||||
|
// subgroups should be ordered by name.
|
||||||
|
assertThat(subGroups.get(i-1).getName(), is(equalTo("kcgroup-" + i)));
|
||||||
|
assertThat(subGroups.get(i-1).getAttributes(), is(nullValue())); // brief rep - no attributes should be returned in subgroups.
|
||||||
|
}
|
||||||
|
|
||||||
|
// search for subgroups filtering by name - all groups with 'test' in the name.
|
||||||
|
subGroups = parentGroupCreator.resource().getSubGroups("test", false, 0, 10, true);
|
||||||
|
assertThat(subGroups, hasSize(3));
|
||||||
|
for (int i = 1; i <= 3; i++) {
|
||||||
|
assertThat(subGroups.get(i-1).getName(), is(equalTo("testgroup-" + i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// search for subgroups filtering by name - all groups with 'gro' in the name.
|
||||||
|
subGroups = parentGroupCreator.resource().getSubGroups("gro", false, 0, 10, true);
|
||||||
|
assertThat(subGroups, hasSize(8));
|
||||||
|
|
||||||
|
// search using a string that matches none of the subgroups.
|
||||||
|
subGroups = parentGroupCreator.resource().getSubGroups("nonexistent", false, 0, 10, false);
|
||||||
|
assertThat(subGroups, is(empty()));
|
||||||
|
|
||||||
|
// exact search with full representation - only one subgroup should be returned.
|
||||||
|
subGroups = parentGroupCreator.resource().getSubGroups("kcgroup-2", true, 0, 10, false);
|
||||||
|
assertThat(subGroups, hasSize(1));
|
||||||
|
assertThat(subGroups.get(0).getName(), is(equalTo("kcgroup-2")));
|
||||||
|
// attributes should be present in the returned subgroup.
|
||||||
|
Map<String, List<String>> attributes = subGroups.get(0).getAttributes();
|
||||||
|
assertThat(attributes, not(anEmptyMap()));
|
||||||
|
assertThat(attributes.keySet(), hasSize(2));
|
||||||
|
assertThat(attributes.keySet(), containsInAnyOrder(ATTR_ORG_NAME, ATTR_QUOTES_NAME));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void search(String searchQuery, String... expectedGroupIds) {
|
private void search(String searchQuery, String... expectedGroupIds) {
|
||||||
GroupsResource search = testRealmResource().groups();
|
GroupsResource search = testRealmResource().groups();
|
||||||
List<String> found = search.query(searchQuery).stream()
|
List<String> found = search.query(searchQuery).stream()
|
||||||
|
|
Loading…
Reference in a new issue