8000 28818 - Reintroduce search by name for subgroups by sguilhen · Pull Request #29011 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -86,18 +86,45 @@ public interface GroupResource {
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 max
* @param briefRepresentation
* @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 (include role mappings and attributes).
*/
@GET
@Path("children")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
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
* if the group doesn't exist.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ public interface GroupsResource {

/**
* Get groups by pagination params.
* @param first index of the first element
* @param max max number of occurrences
* @param first index of the first element (pagination offset).
* @param max the maximum number of results.
* @return A list containing the slice of all groups.
*/
@GET
Expand All @@ -52,9 +52,9 @@ public interface GroupsResource {

/**
* Get groups by pagination params.
* @param search max number of occurrences
* @param first index of the first element
* @param max max number of occurrences
* @param search A {@code String} representing either an exact or partial group name.
* @param first index of the first element (pagination offset).
* @param max the maximum number of results.
* @return A list containing the slice of all groups.
*/
@GET
Expand All @@ -66,10 +66,12 @@ List<GroupRepresentation> groups(@QueryParam("search") String search,

/**
* Get groups by pagination params.
* @param search max number of occurrences
* @param first index of the first element
* @param max max number of occurrences
* @param briefRepresentation if false, return groups with their attributes
* @param search A {@code String} representing either an exact or partial group name.
* @param first index of the first element (pagination offset).
* @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.
*/
@GET
Expand All @@ -82,11 +84,14 @@ List<GroupRepresentation> groups(@QueryParam("search") String search,

/**
* 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
* @param search A {@code String} representing either an exact or partial group name.
* @param exact if {@code true}, the groups will be searched using exact match for the {@code search} param. If false,
* * the method returns all groups that partially match the specified name.
* @param first index of the first element (pagination offset).
* @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.
*/
@GET
Expand Down
1 change: 1 addition & 0 deletions js/apps/admin-ui/src/groups/GroupTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const GroupTable = ({ refresh: viewRefresh }: GroupTableProps) => {
let groupsData = undefined;
if (id) {
const args: SubGroupQuery = {
search: search || "",
first: first,
max: max,
parentId: id,
Expand Down
5 changes: 3 additions & 2 deletions js/libs/keycloak-admin-client/src/resources/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ interface SummarizedQuery {
}

export type GroupQuery = Query & PaginatedQuery & SummarizedQuery;
export type SubGroupQuery = PaginatedQuery &
export type SubGroupQuery = Query &
PaginatedQuery &
SummarizedQuery & {
parentId: string;
};
Expand Down Expand Up @@ -142,7 +143,7 @@ export class Groups extends Resource<{ realm?: string }> {
method: "GET",
path: "/{parentId}/children",
urlParamKeys: ["parentId"],
queryParamKeys: ["first", "max", "briefRepresentation"],
queryParamKeys: ["search", "first", "max", "briefRepresentation"],
catchNotFound: true,
},
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,15 +159,18 @@ public void deleteGroup() {
@Produces(MediaType.APPLICATION_JSON)
@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")
public Stream<GroupRepresentation> getSubGroups(@QueryParam("first") @DefaultValue("0") Integer first,
@QueryParam("max") @DefaultValue("10") Integer max,
@QueryParam("briefRepresentation") @DefaultValue("false") Boolean briefRepresentation) {
public Stream<GroupRepresentation> getSubGroups(
@Parameter(description = "A String representing either an exact group name or a partial name") @QueryParam("search") String search,
@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,
8BD0 @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);
boolean canViewGlobal = auth.groups().canView();
return paginatedStream(group.getSubGroupsStream(-1, -1)
.filter(g -> canViewGlobal || auth.groups().canView(g))
.map(g -> GroupUtils.populateSubGroupCount(g, GroupUtils.toRepresentation(auth.groups(), g, !briefRepresentation)))
, first, max);
return paginatedStream(
group.getSubGroupsStream(search, exact, -1, -1)
.filter(g -> canViewGlobal || auth.groups().canView(g)), first, max)
.map(g -> GroupUtils.populateSubGroupCount(g, GroupUtils.toRepresentation(auth.groups(), g, !briefRepresentation)));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package org.keycloak.testsuite.admin.group;

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.Matchers.anEmptyMap;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
Expand All @@ -11,6 +14,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.jboss.arquillian.container.test.api.ContainerController;
Expand Down Expand Up @@ -214,6 +218,61 @@ public void queryFullRepresentation() throws Exception {
}
}

@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) {
GroupsResource search = testRealmResource().groups();
List<String> found = search.query(searchQuery).stream()
Expand Down
0