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 <AttributesForm
form={form} form={form}
save={save} save={save}
fineGrainedAccess={currentGroup()?.access?.manage}
reset={() => reset={() =>
form.reset({ form.reset({
attributes: convertAttributes(), attributes: convertAttributes(),

View file

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

View file

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

View file

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