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}}",
|
"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."
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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 }));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue