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:
parent
139b809f72
commit
c595e3430e
7 changed files with 73 additions and 26 deletions
|
@ -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."
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 }));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -54,6 +54,8 @@ public interface GroupPermissionEvaluator {
|
|||
|
||||
boolean canManageMembership(GroupModel group);
|
||||
|
||||
boolean canViewMembers(GroupModel group);
|
||||
|
||||
void requireManageMembership(GroupModel group);
|
||||
|
||||
void requireManageMembers(GroupModel group);
|
||||
|
|
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue