Fine-grained permissions for groups (#2579)
This commit is contained in:
parent
93088c5380
commit
5427eaf6ff
4 changed files with 135 additions and 99 deletions
|
@ -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(),
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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 />}
|
||||||
|
|
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue