Add access to full group tree. Fix access for members tab. Add missing (#19423)

props to Access object.
Fixes #17589
This commit is contained in:
Stan Silvert 2023-03-31 15:11:13 -04:00 committed by GitHub
parent 139b809f72
commit c595e3430e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 73 additions and 26 deletions

View File

@ -66,5 +66,6 @@
"groupUpdateError": "Error updating group {{error}}",
"roleMapping": "Role mapping",
"noRoles": "No roles for this group",
"noRolesInstructions": "You haven't created any roles for this group. Create a role to get started."
"noRolesInstructions": "You haven't created any roles for this group. Create a role to get started.",
"noViewRights": "You do not have rights to view this group."
}

View File

@ -39,7 +39,7 @@ export const GroupTable = ({
const [showDelete, toggleShowDelete] = useToggle();
const [move, setMove] = useState<GroupRepresentation>();
const { subGroups, currentGroup, setSubGroups } = useSubGroups();
const { currentGroup } = useSubGroups();
const [key, setKey] = useState(0);
const refresh = () => setKey(key + 1);
@ -212,7 +212,7 @@ export const GroupTable = ({
<Link
key={group.id}
to={`${location.pathname}/${group.id}`}
onClick={() => setSubGroups([...subGroups, group])}
onClick={() => navigate(toGroups({ realm, id: group.id }))}
>
{group.name}
</Link>

View File

@ -69,6 +69,10 @@ export default function GroupsSection() {
const canViewDetails =
hasAccess("query-groups", "view-users") ||
hasAccess("manage-users", "query-groups");
const canViewMembers =
hasAccess("view-users") ||
currentGroup()?.access?.viewMembers ||
currentGroup()?.access?.manageMembers;
useFetch(
async () => {
@ -177,13 +181,15 @@ export default function GroupsSection() {
canViewDetails={canViewDetails}
/>
</Tab>
<Tab
data-testid="members"
eventKey={1}
title={<TabTitleText>{t("members")}</TabTitleText>}
>
<Members />
</Tab>
{canViewMembers && (
<Tab
data-testid="members"
eventKey={1}
title={<TabTitleText>{t("members")}</TabTitleText>}
>
<Members />
</Tab>
)}
<Tab
data-testid="attributes"
eventKey={2}

View File

@ -2,6 +2,7 @@ import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
AlertVariant,
Checkbox,
Dropdown,
DropdownItem,
@ -27,6 +28,8 @@ import { fetchAdminUI } from "../../context/auth/admin-ui-endpoint";
import { useRealm } from "../../context/realm-context/RealmContext";
import { joinPath } from "../../utils/joinPath";
import { toGroups } from "../routes/Groups";
import { useAlerts } from "../../components/alert/Alerts";
import { useAccess } from "../../context/access/Access";
import "./group-tree.css";
@ -113,6 +116,8 @@ export const GroupTree = ({
const { adminClient } = useAdminClient();
const { realm } = useRealm();
const navigate = useNavigate();
const { addAlert } = useAlerts();
const { hasAccess } = useAccess();
const [data, setData] = useState<TreeViewDataItem[]>();
const [groups, setGroups] = useState<GroupRepresentation[]>([]);
@ -147,7 +152,7 @@ export const GroupTree = ({
group.subGroups && group.subGroups.length > 0
? group.subGroups.map((g) => mapGroup(g, groups, refresh))
: undefined,
action: canViewDetails && (
action: (hasAccess("manage-users") || group.access?.manage) && (
<GroupTreeContextMenu group={group} refresh={refresh} />
),
defaultExpanded: subGroups.map((g) => g.id).includes(group.id),
@ -232,12 +237,16 @@ export const GroupTree = ({
className="keycloak_groups_treeview"
onSelect={(_, item) => {
setActiveItem(item);
if (canViewDetails) {
const id = item.id?.substring(item.id.lastIndexOf("/") + 1);
const subGroups: GroupRepresentation[] = [];
findGroup(groups, id!, [], subGroups);
setSubGroups(subGroups);
const id = item.id?.substring(item.id.lastIndexOf("/") + 1);
const subGroups: GroupRepresentation[] = [];
findGroup(groups, id!, [], subGroups);
setSubGroups(subGroups);
if (canViewDetails || subGroups.at(-1)?.access?.view) {
navigate(toGroups({ realm, id: item.id }));
} else {
addAlert(t("noViewRights"), AlertVariant.warning);
navigate(toGroups({ realm }));
}
}}
/>

View File

@ -1,5 +1,6 @@
package org.keycloak.admin.ui.rest;
import java.util.List;
import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
import java.util.stream.Collectors;
@ -68,23 +69,35 @@ public class GroupsResource {
private GroupRepresentation toGroupHierarchy(GroupModel group, final String search, boolean exact) {
GroupRepresentation rep = toRepresentation(group, true);
rep.setAccess(auth.groups().getAccess(group));
rep.setSubGroups(group.getSubGroupsStream().filter(g ->
groupMatchesSearchOrIsPathElement(
g, search
)
).map(subGroup -> {
final GroupRepresentation subRep = ModelToRepresentation.toGroupHierarchy(
subGroup, true, search, exact
);
subRep.setAccess(auth.groups().getAccess(subGroup));
return subRep;
}
).map(subGroup ->
ModelToRepresentation.toGroupHierarchy(
subGroup, true, search, exact
)
).collect(Collectors.toList()));
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;

View File

@ -53,7 +53,9 @@ public interface GroupPermissionEvaluator {
boolean canManageMembers(GroupModel group);
boolean canManageMembership(GroupModel group);
boolean canViewMembers(GroupModel group);
void requireManageMembership(GroupModel group);
void requireManageMembers(GroupModel group);

View File

@ -343,6 +343,20 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag
}
}
@Override
public boolean canViewMembers(GroupModel group) {
if (root.users().canView()) return true;
if (!root.isAdminSameRealm()) {
return false;
}
ResourceServer server = root.realmResourceServer();
if (server == null) return false;
return hasPermission(group, VIEW_MEMBERS_SCOPE);
}
@Override
public boolean canManageMembers(GroupModel group) {
if (root.users().canManage()) return true;
@ -367,7 +381,7 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag
return hasPermission(group, MANAGE_MEMBERSHIP_SCOPE);
}
@Override
public void requireManageMembership(GroupModel group) {
if (!canManageMembership(group)) {
@ -388,6 +402,8 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag
map.put("view", canView(group));
map.put("manage", canManage(group));
map.put("manageMembership", canManageMembership(group));
map.put("viewMembers", canViewMembers(group));
map.put("manageMembers", canManageMembers(group));
return map;
}