feat(rbac): made changes from testing with maidul

This commit is contained in:
Akhil Mohan
2023-09-08 21:22:36 +05:30
parent 252042fb20
commit 98893a40f1
19 changed files with 612 additions and 529 deletions
@@ -16,37 +16,37 @@ export const formSchema = z.object({
name: z.string().trim(),
description: z.string().trim().optional(),
slug: z.string().trim(),
permissions: z.object({
workspace: z
.object({
read: z.boolean().optional(),
create: z.boolean().optional()
})
.optional(),
member: generalPermissionSchema,
role: generalPermissionSchema,
settings: generalPermissionSchema,
"service-account": generalPermissionSchema,
"incident-contact": generalPermissionSchema,
"secret-scanning": generalPermissionSchema,
sso: generalPermissionSchema,
billing: generalPermissionSchema
})
permissions: z
.object({
workspace: z
.object({
read: z.boolean().optional(),
create: z.boolean().optional()
})
.optional(),
member: generalPermissionSchema,
role: generalPermissionSchema,
settings: generalPermissionSchema,
"service-account": generalPermissionSchema,
"incident-contact": generalPermissionSchema,
"secret-scanning": generalPermissionSchema,
sso: generalPermissionSchema,
billing: generalPermissionSchema
})
.optional()
});
export type TFormSchema = z.infer<typeof formSchema>;
// convert role permission to form compatiable data structure
export const rolePermission2Form = (permissions: TPermission[] = []) => {
const formVal: Partial<TFormSchema["permissions"]> = {};
// any because if it set it as form type due to the discriminated union type of ts
// i would have to write a if loop with both conditions same
const formVal: Record<string, any> = {};
permissions.forEach((permission) => {
const { subject, action } = permission;
if (!formVal?.[subject]) formVal[subject] = {};
// akhilmhdh: this is typecast as something other than workspace key else i would need an if loop with same condition on both side
const key = subject as keyof TFormSchema["permissions"];
(formVal[key] as Exclude<TFormSchema["permissions"]["member"], undefined>)[action] = true;
formVal[subject][action] = true;
});
return formVal;
@@ -54,19 +54,13 @@ export const rolePermission2Form = (permissions: TPermission[] = []) => {
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
const permissions: TPermission[] = [];
(Object.keys(formVal) as Array<keyof typeof formVal>).forEach((rule) => {
// all these type annotations are due to Object.keys of ts cannot infer and put it just a string[]
// quite annoying i know
const actions = Object.keys(formVal[rule] || {}) as Array<
keyof z.infer<typeof generalPermissionSchema>
>;
actions.forEach((action) => {
// akhilmhdh: set it as any due to the union type bug i would end up writing an if else with same condition on both side
if (formVal?.[rule]?.[action as keyof typeof formVal.workspace]) {
permissions.push({ subject: rule, action } as any);
Object.entries(formVal || {}).forEach(([rule, actions]) => {
Object.entries(actions).forEach(([action, isAllowed]) => {
if (isAllowed) {
permissions.push({ subject: rule, action });
}
});
});
return permissions;
};
@@ -11,7 +11,7 @@ import { useToggle } from "@app/hooks";
import { TFormSchema } from "./OrgRoleModifySection.utils";
type Props = {
formName: keyof Omit<TFormSchema["permissions"], "workspace">;
formName: keyof Omit<Exclude<TFormSchema["permissions"], undefined>, "workspace">;
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
@@ -34,6 +34,31 @@ const PERMISSIONS = [
{ action: "delete", label: "Remove" }
] as const;
const SECRET_SCANNING_PERMISSIONS = [
{ action: "read", label: "View risks" },
{ action: "create", label: "Add integrations" },
{ action: "edit", label: "Edit risk status" },
{ action: "delete", label: "Remove integrations" }
] as const;
const BILLING_PERMISSIONS = [
{ action: "read", label: "View bills" },
{ action: "create", label: "Add payment methods" },
{ action: "edit", label: "Edit payments" },
{ action: "delete", label: "Remove payments" }
] as const;
const getPermissionList = (option: Props["formName"]) => {
switch (option) {
case "secret-scanning":
return SECRET_SCANNING_PERMISSIONS;
case "billing":
return BILLING_PERMISSIONS;
default:
return PERMISSIONS;
}
};
export const SimpleLevelPermissionOption = ({
isNonEditable,
setValue,
@@ -138,7 +163,7 @@ export const SimpleLevelPermissionOption = ({
className="overflow-hidden grid gap-8 grid-flow-col auto-cols-min"
>
{isCustom &&
PERMISSIONS.map(({ action, label }) => (
getPermissionList(formName).map(({ action, label }) => (
<Controller
name={`permissions.${formName}.${action}`}
key={`permissions.${formName}.${action}`}
@@ -86,7 +86,9 @@ export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props)
</div>
<div className="flex-grow flex flex-col">
<div className="font-medium mb-1 text-lg">Project</div>
<div className="text-xs font-light">Project management control</div>
<div className="text-xs font-light">
More fine granined project access control can be defined with project level roles
</div>
</div>
<div>
<Select
@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
@@ -16,10 +17,9 @@ import {
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteRole } from "@app/hooks/api";
import { TRole } from "@app/hooks/api/roles/types";
@@ -65,9 +65,17 @@ export const OrgRoleTable = ({ isRolesLoading, roles = [], onSelectRole }: Props
placeholder="Search roles..."
/>
</div>
<Button leftIcon={<FontAwesomeIcon icon={faPlus} />} onClick={() => onSelectRole()}>
Add Role
</Button>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => onSelectRole()}
>
Add Role
</Button>
)}
</OrgPermissionCan>
</div>
<div>
<TableContainer>
@@ -95,27 +103,42 @@ export const OrgRoleTable = ({ isRolesLoading, roles = [], onSelectRole }: Props
</Td>
<Td>
<div className="flex space-x-2 items-center">
<Tooltip content="Edit">
<IconButton
ariaLabel="edit"
onClick={() => onSelectRole(role)}
variant="plain"
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
</Tooltip>
<Tooltip
content={isNonMutatable ? "Reserved roles are non-removable" : "Delete"}
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Role}
renderTooltip
allowedLabel="Edit"
>
<IconButton
ariaLabel="delete"
onClick={() => handlePopUpOpen("deleteRole", role)}
variant="plain"
isDisabled={isNonMutatable}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
{(isAllowed) => (
<IconButton
isDisabled={!isAllowed}
ariaLabel="edit"
onClick={() => onSelectRole(role)}
variant="plain"
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Role}
renderTooltip
allowedLabel={
isNonMutatable ? "Reserved roles are non-removable" : "Delete"
}
>
{(isAllowed) => (
<IconButton
ariaLabel="delete"
onClick={() => handlePopUpOpen("deleteRole", role)}
variant="plain"
isDisabled={isNonMutatable || !isAllowed}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
)}
</OrgPermissionCan>
</div>
</Td>
</Tr>