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}}", "groupUpdateError": "Error updating group {{error}}",
"roleMapping": "Role mapping", "roleMapping": "Role mapping",
"noRoles": "No roles for this group", "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 [showDelete, toggleShowDelete] = useToggle();
const [move, setMove] = useState<GroupRepresentation>(); const [move, setMove] = useState<GroupRepresentation>();
const { subGroups, currentGroup, setSubGroups } = useSubGroups(); const { currentGroup } = useSubGroups();
const [key, setKey] = useState(0); const [key, setKey] = useState(0);
const refresh = () => setKey(key + 1); const refresh = () => setKey(key + 1);
@ -212,7 +212,7 @@ export const GroupTable = ({
<Link <Link
key={group.id} key={group.id}
to={`${location.pathname}/${group.id}`} to={`${location.pathname}/${group.id}`}
onClick={() => setSubGroups([...subGroups, group])} onClick={() => navigate(toGroups({ realm, id: group.id }))}
> >
{group.name} {group.name}
</Link> </Link>

View file

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

View file

@ -2,6 +2,7 @@ import { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
AlertVariant,
Checkbox, Checkbox,
Dropdown, Dropdown,
DropdownItem, DropdownItem,
@ -27,6 +28,8 @@ import { fetchAdminUI } from "../../context/auth/admin-ui-endpoint";
import { useRealm } from "../../context/realm-context/RealmContext"; import { useRealm } from "../../context/realm-context/RealmContext";
import { joinPath } from "../../utils/joinPath"; import { joinPath } from "../../utils/joinPath";
import { toGroups } from "../routes/Groups"; import { toGroups } from "../routes/Groups";
import { useAlerts } from "../../components/alert/Alerts";
import { useAccess } from "../../context/access/Access";
import "./group-tree.css"; import "./group-tree.css";
@ -113,6 +116,8 @@ export const GroupTree = ({
const { adminClient } = useAdminClient(); const { adminClient } = useAdminClient();
const { realm } = useRealm(); const { realm } = useRealm();
const navigate = useNavigate(); const navigate = useNavigate();
const { addAlert } = useAlerts();
const { hasAccess } = useAccess();
const [data, setData] = useState<TreeViewDataItem[]>(); const [data, setData] = useState<TreeViewDataItem[]>();
const [groups, setGroups] = useState<GroupRepresentation[]>([]); const [groups, setGroups] = useState<GroupRepresentation[]>([]);
@ -147,7 +152,7 @@ export const GroupTree = ({
group.subGroups && group.subGroups.length > 0 group.subGroups && group.subGroups.length > 0
? group.subGroups.map((g) => mapGroup(g, groups, refresh)) ? group.subGroups.map((g) => mapGroup(g, groups, refresh))
: undefined, : undefined,
action: canViewDetails && ( action: (hasAccess("manage-users") || group.access?.manage) && (
<GroupTreeContextMenu group={group} refresh={refresh} /> <GroupTreeContextMenu group={group} refresh={refresh} />
), ),
defaultExpanded: subGroups.map((g) => g.id).includes(group.id), defaultExpanded: subGroups.map((g) => g.id).includes(group.id),
@ -232,12 +237,16 @@ export const GroupTree = ({
className="keycloak_groups_treeview" className="keycloak_groups_treeview"
onSelect={(_, item) => { onSelect={(_, item) => {
setActiveItem(item); setActiveItem(item);
if (canViewDetails) { const id = item.id?.substring(item.id.lastIndexOf("/") + 1);
const id = item.id?.substring(item.id.lastIndexOf("/") + 1); const subGroups: GroupRepresentation[] = [];
const subGroups: GroupRepresentation[] = []; findGroup(groups, id!, [], subGroups);
findGroup(groups, id!, [], subGroups); setSubGroups(subGroups);
setSubGroups(subGroups);
if (canViewDetails || subGroups.at(-1)?.access?.view) {
navigate(toGroups({ realm, id: item.id })); 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; package org.keycloak.admin.ui.rest;
import java.util.List;
import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation; import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -68,23 +69,35 @@ public class GroupsResource {
private GroupRepresentation toGroupHierarchy(GroupModel group, final String search, boolean exact) { private GroupRepresentation toGroupHierarchy(GroupModel group, final String search, boolean exact) {
GroupRepresentation rep = toRepresentation(group, true); GroupRepresentation rep = toRepresentation(group, true);
rep.setAccess(auth.groups().getAccess(group));
rep.setSubGroups(group.getSubGroupsStream().filter(g -> rep.setSubGroups(group.getSubGroupsStream().filter(g ->
groupMatchesSearchOrIsPathElement( groupMatchesSearchOrIsPathElement(
g, search g, search
) )
).map(subGroup -> { ).map(subGroup ->
final GroupRepresentation subRep = ModelToRepresentation.toGroupHierarchy( ModelToRepresentation.toGroupHierarchy(
subGroup, true, search, exact subGroup, true, search, exact
); )
subRep.setAccess(auth.groups().getAccess(subGroup));
return subRep;
}
).collect(Collectors.toList())); ).collect(Collectors.toList()));
setAccess(group, rep);
return 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) { private static boolean groupMatchesSearchOrIsPathElement(GroupModel group, String search) {
if (StringUtil.isBlank(search)) { if (StringUtil.isBlank(search)) {
return true; return true;

View file

@ -53,7 +53,9 @@ public interface GroupPermissionEvaluator {
boolean canManageMembers(GroupModel group); boolean canManageMembers(GroupModel group);
boolean canManageMembership(GroupModel group); boolean canManageMembership(GroupModel group);
boolean canViewMembers(GroupModel group);
void requireManageMembership(GroupModel group); void requireManageMembership(GroupModel group);
void requireManageMembers(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 @Override
public boolean canManageMembers(GroupModel group) { public boolean canManageMembers(GroupModel group) {
if (root.users().canManage()) return true; if (root.users().canManage()) return true;
@ -367,7 +381,7 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag
return hasPermission(group, MANAGE_MEMBERSHIP_SCOPE); return hasPermission(group, MANAGE_MEMBERSHIP_SCOPE);
} }
@Override @Override
public void requireManageMembership(GroupModel group) { public void requireManageMembership(GroupModel group) {
if (!canManageMembership(group)) { if (!canManageMembership(group)) {
@ -388,6 +402,8 @@ class GroupPermissions implements GroupPermissionEvaluator, GroupPermissionManag
map.put("view", canView(group)); map.put("view", canView(group));
map.put("manage", canManage(group)); map.put("manage", canManage(group));
map.put("manageMembership", canManageMembership(group)); map.put("manageMembership", canManageMembership(group));
map.put("viewMembers", canViewMembers(group));
map.put("manageMembers", canManageMembers(group));
return map; return map;
} }