+
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
17 changes: 17 additions & 0 deletions docs/documentation/upgrading/topics/changes/changes-26_3_0.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,20 @@ This deprecated feature will be removed in a future version.
The `user-profile-commons.ftl` changed to improve support for localization. See https://github.com/keycloak/keycloak/issues/38029.
As a result, and if you are extending this template, pages might start displaying a `locale` field. To avoid that, update
the theme template with the changes aforementioned.

=== Subgroup counts are no longer cached

When returning subgroups of a group, the count of subgroups of each subgroup of a group is no longer cached. With the
introduction of Fine-Grained Admin Permissions, the result set is filtered at the database level based on any permissions
defined to a realm so that the count will change accordingly to these permissions.

Instead of caching the count, a query will be executed every time to obtain the expected number of groups an administrator can access.

Most of the time, this change will not impact clients querying the API to fetch the subgroups of a group. However, if not the case,
a new parameter `subGroupsCount` was introduced to the following endpoints:

* `/realms/{realm}/groups/{id}/children`
* `/realms/{realm}/groups`

With this parameter, clients can decide whether the count should be returned to each individual group returned. To not break existing deployments,
this parameter defaults to `true` so that the count is returned if the parameter is not set.
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,24 @@ public interface GroupResource {
@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.
*
* @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).
* @param subGroupsCount if {@code true}, the count of subgroups is returned for each subgroup. Defaults to true.
*/
@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,
@QueryParam("subGroupsCount") Boolean subGroupsCount);

/**
* Get the paginated list of subgroups belonging to this group, filtered according to the specified parameters.
*
Expand All @@ -124,6 +142,31 @@ List<GroupRepresentation> getSubGroups(
@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. Parameter available since Keycloak server 25. Will be ignored on older Keycloak versions with the default value null.
* @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. Parameter available since Keycloak server 25. Will be ignored on older Keycloak versions with the default value null.
* @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).
* @param subGroupsCount if {@code true}, the count of subgroups is returned for each subgroup. Defaults to true.
*/
@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,
@QueryParam("subGroupsCount") Boolean subGroupsCount);

/**
* Set or create child. This will just set the parent if it exists. Create it and set the parent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,29 @@ List<GroupRepresentation> groups(@QueryParam("search") String search,
@QueryParam("max") Integer max,
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation);

/**
* Get groups by pagination params.
* @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).
* @param subGroupsCount if {@code true}, the count of subgroups is returned for each subgroup. Defaults to true.
* @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,
@QueryParam("subGroupsCount") @DefaultValue("true") Boolean subGroupsCount);

/**
* Get groups by pagination params.
* @param search A {@code String} representing either an exact or partial group name.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ public Stream<GroupRepresentation> getSubGroups(
@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) {
@Parameter(description = "Boolean which defines whether brief groups representations are returned or not (default: false)") @QueryParam("briefRepresentation") @DefaultValue("false") Boolean briefRepresentation,
@Parameter(description = "Boolean which defines whether to return the count of subgroups for each subgroup of this group (default: true") @QueryParam("subGroupsCount") @DefaultValue("true") Boolean subGroupsCount) {
this.auth.groups().requireView(group);

Stream<GroupModel> stream = group.getSubGroupsStream(search, exact, -1, -1);
Expand All @@ -183,7 +184,15 @@ public Stream<GroupRepresentation> getSubGroups(
}

return paginatedStream(stream, first, max)
.map(g -> GroupUtils.populateSubGroupCount(g, GroupUtils.toRepresentation(auth.groups(), g, !briefRepresentation)));
.map(g -> {
GroupRepresentation rep = GroupUtils.toRepresentation(auth.groups(), g, !briefRepresentation);

if (subGroupsCount) {
return GroupUtils.populateSubGroupCount(g, rep);
}

return rep;
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import jakarta.ws.rs.core.Response.Status;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
Expand Down Expand Up @@ -93,7 +94,8 @@ public Stream<GroupRepresentation> getGroups(@QueryParam("search") String search
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults,
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation,
@QueryParam("populateHierarchy") @DefaultValue("true") boolean populateHierarchy) {
@QueryParam("populateHierarchy") @DefaultValue("true") boolean populateHierarchy,
@Parameter(description = "Boolean which defines whether to return the count of subgroups for each group (default: true") @QueryParam("subGroupsCount") @DefaultValue("true") Boolean subGroupsCount) {
GroupPermissionEvaluator groupsEvaluator = auth.groups();
groupsEvaluator.requireList();

Expand All @@ -108,14 +110,22 @@ public Stream<GroupRepresentation> getGroups(@QueryParam("search") String search
}

if (populateHierarchy) {
return GroupUtils.populateGroupHierarchyFromSubGroups(session, realm, stream, !briefRepresentation, groupsEvaluator);
return GroupUtils.populateGroupHierarchyFromSubGroups(session, realm, stream, !briefRepresentation, groupsEvaluator, subGroupsCount);
}

if (!AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(realm)) {
stream = stream.filter(groupsEvaluator::canView);
}

return stream.map(g -> GroupUtils.populateSubGroupCount(g, GroupUtils.toRepresentation(groupsEvaluator, g, !briefRepresentation)));
return stream.map(g -> {
GroupRepresentation rep = GroupUtils.toRepresentation(groupsEvaluator, g, !briefRepresentation);

if (subGroupsCount) {
return GroupUtils.populateSubGroupCount(g, rep);
}

return rep;
});
}

/**
Expand Down
14 changes: 11 additions & 3 deletions services/src/main/java/org/keycloak/utils/GroupUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class GroupUtils {
* @param groups The groups that we want to populate the hierarchy for
* @return A stream of groups that contain all relevant groups from the root down with no extra siblings
*/
public static Stream<GroupRepresentation> populateGroupHierarchyFromSubGroups(KeycloakSession session, RealmModel realm, Stream<GroupModel> groups, boolean full, GroupPermissionEvaluator groupEvaluator) {
public static Stream<GroupRepresentation> populateGroupHierarchyFromSubGroups(KeycloakSession session, RealmModel realm, Stream<GroupModel> groups, boolean full, GroupPermissionEvaluator groupEvaluator, boolean subGroupsCount) {
Map<String, GroupRepresentation> groupIdToGroups = new HashMap<>();
groups.forEach(group -> {

Expand All @@ -36,7 +36,11 @@ public static Stream<GroupRepresentation> populateGroupHierarchyFromSubGroups(Ke
}

GroupRepresentation currGroup = toRepresentation(groupEvaluator, group, full);
populateSubGroupCount(group, currGroup);

if (subGroupsCount) {
populateSubGroupCount(group, currGroup);
}

groupIdToGroups.putIfAbsent(currGroup.getId(), currGroup);

while(currGroup.getParentId() != null) {
Expand All @@ -51,7 +55,11 @@ public static Stream<GroupRepresentation> populateGroupHierarchyFromSubGroups(Ke

GroupRepresentation parent = groupIdToGroups.computeIfAbsent(currGroup.getParentId(),
id -> toRepresentation(groupEvaluator, parentModel, full));
populateSubGroupCount(parentModel, parent);

if (subGroupsCount) {
populateSubGroupCount(parentModel, parent);
}

GroupRepresentation finalCurrGroup = currGroup;

// check the parent for existing subgroups that match the group we're currently operating on and merge them if needed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ public void querySubGroups() {
.attribute(ATTR_QUOTES_NAME, ATTR_QUOTES_VAL)
.build();
addSubGroup(managedRealm, parentGroup, testGroup);

if (i == 2) {
GroupRepresentation subGroup = GroupConfigBuilder.create()
.name("kcsubgroup-" + i)
.build();

addSubGroup(managedRealm, testGroup, subGroup);
}
}
for (int i = 1; i <= 3; i++) {
GroupRepresentation testGroup = GroupConfigBuilder.create()
Expand Down Expand Up @@ -109,11 +117,31 @@ public void querySubGroups() {
subGroups = parentGroupResource.getSubGroups("kcgroup-2", true, 0, 10, false);
assertThat(subGroups, hasSize(1));
assertThat(subGroups.get(0).getName(), is(equalTo("kcgroup-2")));
assertThat(subGroups.get(0).getSubGroupCount(), is(1L));
// 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));

subGroups = parentGroupResource.getSubGroups("kcgroup-2", true, 0, 10, false, false);
assertThat(subGroups, hasSize(1));
assertThat(subGroups.get(0).getName(), is(equalTo("kcgroup-2")));
assertThat(subGroups.get(0).getSubGroupCount(), is(nullValue()));

subGroups = managedRealm.admin().groups().groups("kcgroup-2", true, 0, 1, true);
Assertions.assertEquals(1, subGroups.size());
assertThat(subGroups.get(0).getName(), is(equalTo(parentGroup.getName())));
Assertions.assertEquals(1, subGroups.get(0).getSubGroups().size());
assertThat(subGroups.get(0).getSubGroups().get(0).getName(), is(equalTo("kcgroup-2")));
assertThat(subGroups.get(0).getSubGroups().get(0).getSubGroupCount(), is(1L));

subGroups = managedRealm.admin().groups().groups("kcgroup-2", true, 0, 1, true, false);
Assertions.assertEquals(1, subGroups.size());
assertThat(subGroups.get(0).getName(), is(equalTo(parentGroup.getName())));
Assertions.assertEquals(1, subGroups.get(0).getSubGroups().size());
assertThat(subGroups.get(0).getSubGroups().get(0).getName(), is(equalTo("kcgroup-2")));
assertThat(subGroups.get(0).getSubGroups().get(0).getSubGroupCount(), is(nullValue()));
}

/**
Expand Down
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载