Fine-grained permissions for groups (#2579)

This commit is contained in:
Stan Silvert 2022-05-09 06:44:37 -04:00 committed by GitHub
parent 93088c5380
commit 5427eaf6ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 135 additions and 99 deletions

View file

@ -64,6 +64,7 @@ export const GroupAttributes = () => {
<AttributesForm
form={form}
save={save}
fineGrainedAccess={currentGroup()?.access?.manage}
reset={() =>
form.reset({
attributes: convertAttributes(),

View file

@ -37,7 +37,7 @@ export const GroupTable = () => {
const [selectedRows, setSelectedRows] = useState<GroupRepresentation[]>([]);
const [move, setMove] = useState<GroupRepresentation>();
const { subGroups, setSubGroups } = useSubGroups();
const { subGroups, currentGroup, setSubGroups } = useSubGroups();
const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime());
@ -47,7 +47,10 @@ export const GroupTable = () => {
const id = getLastId(location.pathname);
const { hasAccess } = useAccess();
const isManager = hasAccess("manage-users", "query-clients");
const isManager = hasAccess("manage-users") || currentGroup()?.access?.manage;
const canView =
hasAccess("query-groups", "view-users") ||
hasAccess("manage-users", "query-groups");
const loader = async () => {
let groupsData = undefined;
@ -90,14 +93,17 @@ export const GroupTable = () => {
};
const GroupNameCell = (group: GroupRepresentation) => {
if (!isManager) return <span>{group.name}</span>;
if (!canView) return <span>{group.name}</span>;
return (
<Link
key={group.id}
to={`${location.pathname}/${group.id}`}
onClick={() => {
setSubGroups([...subGroups, group]);
onClick={async () => {
const loadedGroup = await adminClient.groups.findOne({
id: group.id!,
});
setSubGroups([...subGroups, loadedGroup!]);
}}
>
{group.name}

View file

@ -28,6 +28,7 @@ import { toGroupsSearch } from "./routes/GroupsSearch";
import { GroupRoleMapping } from "./GroupRoleMapping";
import helpUrls from "../help-urls";
import { PermissionsTab } from "../components/permission-tab/PermissionTab";
import { useAccess } from "../context/access/Access";
import "./GroupsSection.css";
@ -46,6 +47,16 @@ export default function GroupsSection() {
const location = useLocation();
const id = getLastId(location.pathname);
const { hasAccess } = useAccess();
const canViewPermissions = hasAccess(
"manage-authorization",
"manage-users",
"manage-clients"
);
const canManageGroup =
hasAccess("manage-users") || currentGroup()?.access?.manage;
const canManageRoles = hasAccess("manage-users");
const deleteGroup = async (group: GroupRepresentation) => {
try {
await adminClient.groups.del({
@ -112,7 +123,7 @@ export default function GroupsSection() {
helpUrl={!id ? helpUrls.groupsUrl : ""}
divider={!id}
dropdownItems={
id
id && canManageGroup
? [
SearchDropdown,
<DropdownItem
@ -170,20 +181,24 @@ export default function GroupsSection() {
>
<GroupAttributes />
</Tab>
<Tab
eventKey={3}
data-testid="role-mapping-tab"
title={<TabTitleText>{t("roleMapping")}</TabTitleText>}
>
<GroupRoleMapping id={id!} name={currentGroup()?.name!} />
</Tab>
<Tab
eventKey={4}
data-testid="permissionsTab"
title={<TabTitleText>{t("common:permissions")}</TabTitleText>}
>
<PermissionsTab id={id} type="groups" />
</Tab>
{canManageRoles && (
<Tab
eventKey={3}
data-testid="role-mapping-tab"
title={<TabTitleText>{t("roleMapping")}</TabTitleText>}
>
<GroupRoleMapping id={id!} name={currentGroup()?.name!} />
</Tab>
)}
{canViewPermissions && (
<Tab
eventKey={4}
data-testid="permissionsTab"
title={<TabTitleText>{t("common:permissions")}</TabTitleText>}
>
<PermissionsTab id={id} type="groups" />
</Tab>
)}
</Tabs>
)}
{subGroups.length === 0 && <GroupTable />}

View file

@ -26,6 +26,7 @@ import { MemberModal } from "./MembersModal";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { GroupPath } from "../components/group/GroupPath";
import { toUser } from "../user/routes/User";
import { useAccess } from "../context/access/Access";
type MembersOf = UserRepresentation & {
membership: GroupRepresentation[];
@ -43,6 +44,10 @@ export const Members = () => {
const [addMembers, setAddMembers] = useState(false);
const [isKebabOpen, setIsKebabOpen] = useState(false);
const [selectedRows, setSelectedRows] = useState<UserRepresentation[]>([]);
const { hasAccess } = useAccess();
const isManager =
hasAccess("manage-users") || currentGroup()!.access!.manageMembership;
const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime());
@ -123,86 +128,95 @@ export const Members = () => {
canSelectAll
onSelect={(rows) => setSelectedRows([...rows])}
toolbarItem={
<>
<ToolbarItem>
<Button
data-testid="addMember"
variant="primary"
onClick={() => setAddMembers(true)}
>
{t("addMember")}
</Button>
</ToolbarItem>
<ToolbarItem>
<Checkbox
data-testid="includeSubGroupsCheck"
label={t("includeSubGroups")}
id="kc-include-sub-groups"
isChecked={includeSubGroup}
onChange={() => setIncludeSubGroup(!includeSubGroup)}
/>
</ToolbarItem>
<ToolbarItem>
<Dropdown
toggle={
<KebabToggle
onToggle={() => setIsKebabOpen(!isKebabOpen)}
isDisabled={selectedRows.length === 0}
/>
}
isOpen={isKebabOpen}
isPlain
dropdownItems={[
<DropdownItem
key="action"
component="button"
onClick={async () => {
try {
await Promise.all(
selectedRows.map((user) =>
adminClient.users.delFromGroup({
id: user.id!,
groupId: id!,
})
)
);
setIsKebabOpen(false);
addAlert(
t("usersLeft", { count: selectedRows.length }),
AlertVariant.success
);
} catch (error) {
addError("groups:usersLeftError", error);
}
isManager && (
<>
<ToolbarItem>
<Button
data-testid="addMember"
variant="primary"
onClick={() => setAddMembers(true)}
>
{t("addMember")}
</Button>
</ToolbarItem>
<ToolbarItem>
<Checkbox
data-testid="includeSubGroupsCheck"
label={t("includeSubGroups")}
id="kc-include-sub-groups"
isChecked={includeSubGroup}
onChange={() => setIncludeSubGroup(!includeSubGroup)}
/>
</ToolbarItem>
<ToolbarItem>
<Dropdown
toggle={
<KebabToggle
onToggle={() => setIsKebabOpen(!isKebabOpen)}
isDisabled={selectedRows.length === 0}
/>
}
isOpen={isKebabOpen}
isPlain
dropdownItems={[
<DropdownItem
key="action"
component="button"
onClick={async () => {
try {
await Promise.all(
selectedRows.map((user) =>
adminClient.users.delFromGroup({
id: user.id!,
groupId: id!,
})
)
);
setIsKebabOpen(false);
addAlert(
t("usersLeft", { count: selectedRows.length }),
AlertVariant.success
);
} catch (error) {
addError("groups:usersLeftError", error);
}
refresh();
}}
>
{t("leave")}
</DropdownItem>,
]}
/>
</ToolbarItem>
</>
refresh();
}}
>
{t("leave")}
</DropdownItem>,
]}
/>
</ToolbarItem>
</>
)
}
actions={[
{
title: t("leave"),
onRowClick: async (user) => {
try {
await adminClient.users.delFromGroup({
id: user.id!,
groupId: id!,
});
addAlert(t("usersLeft", { count: 1 }), AlertVariant.success);
} catch (error) {
addError("groups:usersLeftError", error);
}
actions={
isManager
? [
{
title: t("leave"),
onRowClick: async (user) => {
try {
await adminClient.users.delFromGroup({
id: user.id!,
groupId: id!,
});
addAlert(
t("usersLeft", { count: 1 }),
AlertVariant.success
);
} catch (error) {
addError("groups:usersLeftError", error);
}
return true;
},
},
]}
return true;
},
},
]
: []
}
columns={[
{
name: "username",
@ -233,8 +247,8 @@ export const Members = () => {
emptyState={
<ListEmptyState
message={t("users:noUsersFound")}
instructions={t("users:emptyInstructions")}
primaryActionText={t("addMember")}
instructions={isManager ? t("users:emptyInstructions") : undefined}
primaryActionText={isManager ? t("addMember") : undefined}
onPrimaryAction={() => setAddMembers(true)}
/>
}