diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/GroupsResource.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/GroupsResource.java index 1633ecf9905b..af542542cb6d 100644 --- a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/GroupsResource.java +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/GroupsResource.java @@ -1,8 +1,6 @@ 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 jakarta.ws.rs.Consumes; import jakarta.ws.rs.DefaultValue; @@ -15,15 +13,13 @@ import org.eclipse.microprofile.openapi.annotations.media.Content; import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import org.keycloak.common.Profile; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; -import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator; import org.keycloak.services.resources.admin.permissions.GroupPermissionEvaluator; -import org.keycloak.utils.StringUtil; +import org.keycloak.utils.GroupUtils; public class GroupsResource { private final KeycloakSession session; @@ -68,49 +64,6 @@ public final Stream listGroups(@QueryParam("search") @Defau boolean canViewGlobal = groupsEvaluator.canView(); return stream.filter(group -> canViewGlobal || groupsEvaluator.canView(group)) - .map(group -> toGroupHierarchy(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(); + .map(group -> GroupUtils.toGroupHierarchy(groupsEvaluator, group, search, exact)); } } diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 40bcaa6007c3..df514a1cb39d 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -151,6 +151,13 @@ public static GroupRepresentation toRepresentation(GroupModel group, boolean ful } public static Stream searchGroupsByAttributes(KeycloakSession session, RealmModel realm, boolean full, boolean populateHierarchy, Map attributes, Integer first, Integer max) { + Stream 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 searchGroupModelsByAttributes(KeycloakSession session, RealmModel realm, boolean full, boolean populateHierarchy, Map attributes, Integer first, Integer max) { Stream groups = session.groups().searchGroupsByAttributes(realm, attributes, first, max); if(populateHierarchy) { groups = groups @@ -166,13 +173,16 @@ public static Stream searchGroupsByAttributes(KeycloakSessi // More child groups of one root can fulfill the search, so we need to filter duplicates .filter(StreamsUtil.distinctByKey(GroupModel::getId)); } - // 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)); + return groups; } public static Stream 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)); + return searchForGroupModelByName(session, realm, full, search, exact, first, max) + .map(g -> toGroupHierarchy(g, full, search, exact)); + } + + public static Stream 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 searchForGroupByName(UserModel user, boolean full, String search, Integer first, Integer max) { @@ -181,8 +191,12 @@ public static Stream searchForGroupByName(UserModel user, b } public static Stream toGroupHierarchy(RealmModel realm, boolean full, Integer first, Integer max) { - return realm.getTopLevelGroupsStream(first, max) - .map(g -> toGroupHierarchy(g, full)); + return toGroupModelHierarchy(realm, full, first, max) + .map(g -> toGroupHierarchy(g, full)); + } + + public static Stream toGroupModelHierarchy(RealmModel realm, boolean full, Integer first, Integer max) { + return realm.getTopLevelGroupsStream(first, max); } public static Stream toGroupHierarchy(UserModel user, boolean full, Integer first, Integer max) { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java index 34e513306668..beb5b62b6f0e 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java @@ -21,6 +21,7 @@ import org.eclipse.microprofile.openapi.annotations.tags.Tag; import org.jboss.resteasy.annotations.cache.NoCache; import jakarta.ws.rs.NotFoundException; + import org.keycloak.common.util.ObjectUtil; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; @@ -33,6 +34,8 @@ import org.keycloak.services.ErrorResponse; import org.keycloak.services.resources.KeycloakOpenAPI; 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 jakarta.ws.rs.Consumes; @@ -88,18 +91,24 @@ public Stream getGroups(@QueryParam("search") String search @QueryParam("max") Integer maxResults, @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation, @QueryParam("populateHierarchy") @DefaultValue("true") boolean populateHierarchy) { - auth.groups().requireList(); + GroupPermissionEvaluator groupsEvaluator = auth.groups(); + groupsEvaluator.requireList(); + Stream stream = null; if (Objects.nonNull(searchQuery)) { Map 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)) { - 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)) { - return ModelToRepresentation.toGroupHierarchy(realm, !briefRepresentation, firstResult, maxResults); + stream = ModelToRepresentation.toGroupModelHierarchy(realm, !briefRepresentation, firstResult, maxResults); } 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)); } /** diff --git a/services/src/main/java/org/keycloak/utils/GroupUtils.java b/services/src/main/java/org/keycloak/utils/GroupUtils.java new file mode 100644 index 000000000000..be46a79d0cd6 --- /dev/null +++ b/services/src/main/java/org/keycloak/utils/GroupUtils.java @@ -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(); + } +}