* fix: closes #21095

* Added overloaded version of GroupUtils.toGroupHierarchy with additional full parameter.
This commit is contained in:
Daniele Martinoli 2023-07-10 12:13:26 +02:00 committed by GitHub
parent 561bd65b4a
commit 817f129484
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 96 additions and 60 deletions

View file

@ -1,8 +1,6 @@
package org.keycloak.admin.ui.rest; package org.keycloak.admin.ui.rest;
import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.DefaultValue;
@ -15,15 +13,13 @@ import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Content; import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.keycloak.common.Profile;
import org.keycloak.models.GroupModel; import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.services.resources.admin.permissions.GroupPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.GroupPermissionEvaluator;
import org.keycloak.utils.StringUtil; import org.keycloak.utils.GroupUtils;
public class GroupsResource { public class GroupsResource {
private final KeycloakSession session; private final KeycloakSession session;
@ -68,49 +64,6 @@ public class GroupsResource {
boolean canViewGlobal = groupsEvaluator.canView(); boolean canViewGlobal = groupsEvaluator.canView();
return stream.filter(group -> canViewGlobal || groupsEvaluator.canView(group)) return stream.filter(group -> canViewGlobal || groupsEvaluator.canView(group))
.map(group -> toGroupHierarchy(group, search, exact)); .map(group -> GroupUtils.toGroupHierarchy(groupsEvaluator, group, search, exact));
}
private GroupRepresentation toGroupHierarchy(GroupModel group, final String search, boolean exact) {
GroupRepresentation rep = toRepresentation(group, true);
rep.setSubGroups(group.getSubGroupsStream().filter(g ->
groupMatchesSearchOrIsPathElement(
g, search
)
).map(subGroup ->
ModelToRepresentation.toGroupHierarchy(
subGroup, true, search, exact
)
).collect(Collectors.toList()));
if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) {
setAccess(group, rep);
}
return rep;
}
// set fine-grained access for each group in the tree
private void setAccess(GroupModel groupTree, GroupRepresentation rootGroup) {
if (rootGroup == null) return;
rootGroup.setAccess(auth.groups().getAccess(groupTree));
rootGroup.getSubGroups().stream().forEach(subGroup -> {
GroupModel foundGroupModel = groupTree.getSubGroupsStream().filter(g -> g.getId().equals(subGroup.getId())).findFirst().get();
setAccess(foundGroupModel, subGroup);
});
}
private static boolean groupMatchesSearchOrIsPathElement(GroupModel group, String search) {
if (StringUtil.isBlank(search)) {
return true;
}
if (group.getName().contains(search)) {
return true;
}
return group.getSubGroupsStream().findAny().isPresent();
} }
} }

View file

@ -151,6 +151,13 @@ public class ModelToRepresentation {
} }
public static Stream<GroupRepresentation> searchGroupsByAttributes(KeycloakSession session, RealmModel realm, boolean full, boolean populateHierarchy, Map<String,String> attributes, Integer first, Integer max) { public static Stream<GroupRepresentation> searchGroupsByAttributes(KeycloakSession session, RealmModel realm, boolean full, boolean populateHierarchy, Map<String,String> attributes, Integer first, Integer max) {
Stream<GroupModel> groups = searchGroupModelsByAttributes(session, realm, full, populateHierarchy, attributes, first, max);
// and then turn the result into GroupRepresentations creating whole hierarchy of child groups for each root group
return groups.map(g -> toGroupHierarchy(g, full, attributes));
}
public static Stream<GroupModel> searchGroupModelsByAttributes(KeycloakSession session, RealmModel realm, boolean full, boolean populateHierarchy, Map<String,String> attributes, Integer first, Integer max) {
Stream<GroupModel> groups = session.groups().searchGroupsByAttributes(realm, attributes, first, max); Stream<GroupModel> groups = session.groups().searchGroupsByAttributes(realm, attributes, first, max);
if(populateHierarchy) { if(populateHierarchy) {
groups = groups groups = groups
@ -166,13 +173,16 @@ public class ModelToRepresentation {
// More child groups of one root can fulfill the search, so we need to filter duplicates // More child groups of one root can fulfill the search, so we need to filter duplicates
.filter(StreamsUtil.distinctByKey(GroupModel::getId)); .filter(StreamsUtil.distinctByKey(GroupModel::getId));
} }
// and then turn the result into GroupRepresentations creating whole hierarchy of child groups for each root group return groups;
return groups.map(g -> toGroupHierarchy(g, full, attributes));
} }
public static Stream<GroupRepresentation> searchForGroupByName(KeycloakSession session, RealmModel realm, boolean full, String search, Boolean exact, Integer first, Integer max) { 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) return searchForGroupModelByName(session, realm, full, search, exact, first, max)
.map(g -> toGroupHierarchy(g, full, search, exact)); .map(g -> toGroupHierarchy(g, full, search, exact));
}
public static Stream<GroupModel> searchForGroupModelByName(KeycloakSession session, RealmModel realm, boolean full, String search, Boolean exact, Integer first, Integer max) {
return session.groups().searchForGroupByNameStream(realm, search, exact, first, max);
} }
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) {
@ -181,8 +191,12 @@ public class ModelToRepresentation {
} }
public static Stream<GroupRepresentation> toGroupHierarchy(RealmModel realm, boolean full, Integer first, Integer max) { public static Stream<GroupRepresentation> toGroupHierarchy(RealmModel realm, boolean full, Integer first, Integer max) {
return realm.getTopLevelGroupsStream(first, max) return toGroupModelHierarchy(realm, full, first, max)
.map(g -> toGroupHierarchy(g, full)); .map(g -> toGroupHierarchy(g, full));
}
public static Stream<GroupModel> toGroupModelHierarchy(RealmModel realm, boolean full, Integer first, Integer max) {
return realm.getTopLevelGroupsStream(first, max);
} }
public static Stream<GroupRepresentation> toGroupHierarchy(UserModel user, boolean full, Integer first, Integer max) { public static Stream<GroupRepresentation> toGroupHierarchy(UserModel user, boolean full, Integer first, Integer max) {

View file

@ -21,6 +21,7 @@ import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.annotations.cache.NoCache;
import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.NotFoundException;
import org.keycloak.common.util.ObjectUtil; import org.keycloak.common.util.ObjectUtil;
import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType; import org.keycloak.events.admin.ResourceType;
@ -33,6 +34,8 @@ import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.services.ErrorResponse; import org.keycloak.services.ErrorResponse;
import org.keycloak.services.resources.KeycloakOpenAPI; import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.services.resources.admin.permissions.GroupPermissionEvaluator;
import org.keycloak.utils.GroupUtils;
import org.keycloak.utils.SearchQueryUtils; import org.keycloak.utils.SearchQueryUtils;
import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Consumes;
@ -88,18 +91,24 @@ public class GroupsResource {
@QueryParam("max") Integer maxResults, @QueryParam("max") Integer maxResults,
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation, @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation,
@QueryParam("populateHierarchy") @DefaultValue("true") boolean populateHierarchy) { @QueryParam("populateHierarchy") @DefaultValue("true") boolean populateHierarchy) {
auth.groups().requireList(); GroupPermissionEvaluator groupsEvaluator = auth.groups();
groupsEvaluator.requireList();
Stream<GroupModel> stream = null;
if (Objects.nonNull(searchQuery)) { if (Objects.nonNull(searchQuery)) {
Map<String, String> attributes = SearchQueryUtils.getFields(searchQuery); Map<String, String> attributes = SearchQueryUtils.getFields(searchQuery);
return ModelToRepresentation.searchGroupsByAttributes(session, realm, !briefRepresentation, populateHierarchy, attributes, firstResult, maxResults); stream = ModelToRepresentation.searchGroupModelsByAttributes(session, realm, !briefRepresentation, populateHierarchy, attributes, firstResult, maxResults);
} else if (Objects.nonNull(search)) { } else if (Objects.nonNull(search)) {
return ModelToRepresentation.searchForGroupByName(session, realm, !briefRepresentation, search.trim(), exact, firstResult, maxResults); stream = ModelToRepresentation.searchForGroupModelByName(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); stream = ModelToRepresentation.toGroupModelHierarchy(realm, !briefRepresentation, firstResult, maxResults);
} else { } else {
return ModelToRepresentation.toGroupHierarchy(realm, !briefRepresentation); stream = realm.getTopLevelGroupsStream();
} }
boolean canViewGlobal = groupsEvaluator.canView();
return stream.filter(group -> canViewGlobal || groupsEvaluator.canView(group))
.map(group -> GroupUtils.toGroupHierarchy(groupsEvaluator, group, search, exact, !briefRepresentation));
} }
/** /**

View file

@ -0,0 +1,60 @@
package org.keycloak.utils;
import java.util.stream.Collectors;
import org.keycloak.common.Profile;
import org.keycloak.models.GroupModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.services.resources.admin.permissions.GroupPermissionEvaluator;
public class GroupUtils {
// Moved out from org.keycloak.admin.ui.rest.GroupsResource
public static GroupRepresentation toGroupHierarchy(GroupPermissionEvaluator groupsEvaluator, GroupModel group, final String search, boolean exact) {
return toGroupHierarchy(groupsEvaluator, group, search, exact, true);
}
public static GroupRepresentation toGroupHierarchy(GroupPermissionEvaluator groupsEvaluator, GroupModel group, final String search, boolean exact, boolean full) {
GroupRepresentation rep = ModelToRepresentation.toRepresentation(group, full);
rep.setSubGroups(group.getSubGroupsStream().filter(g ->
groupMatchesSearchOrIsPathElement(
g, search
)
).map(subGroup ->
ModelToRepresentation.toGroupHierarchy(
subGroup, full, search, exact
)
).collect(Collectors.toList()));
if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) {
setAccess(groupsEvaluator, group, rep);
}
return rep;
}
//From org.keycloak.admin.ui.rest.GroupsResource
// set fine-grained access for each group in the tree
private static void setAccess(GroupPermissionEvaluator groupsEvaluator, GroupModel groupTree, GroupRepresentation rootGroup) {
if (rootGroup == null) return;
rootGroup.setAccess(groupsEvaluator.getAccess(groupTree));
rootGroup.getSubGroups().stream().forEach(subGroup -> {
GroupModel foundGroupModel = groupTree.getSubGroupsStream().filter(g -> g.getId().equals(subGroup.getId())).findFirst().get();
setAccess(groupsEvaluator, foundGroupModel, subGroup);
});
}
private static boolean groupMatchesSearchOrIsPathElement(GroupModel group, String search) {
if (StringUtil.isBlank(search)) {
return true;
}
if (group.getName().contains(search)) {
return true;
}
return group.getSubGroupsStream().findAny().isPresent();
}
}