From 26028e73124c0c0f14df70cea68a872a5a3785a6 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Tue, 5 Sep 2023 12:25:06 +0530 Subject: [PATCH] feat(rbac): fixed broken view when clicking editing predefined role --- backend/src/controllers/v1/roleController.ts | 31 ++-- backend/src/services/ProjectRoleService.ts | 2 +- .../OrgRoleModifySection.utils.ts | 20 +-- .../ProjectRoleModifySection.tsx | 18 ++- .../ProjectRoleModifySection.utils.ts | 44 +++--- .../SecretRollbackPermission.tsx | 146 ++++++++++++++++++ .../WsProjectPermission.tsx | 2 - 7 files changed, 202 insertions(+), 61 deletions(-) create mode 100644 frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SecretRollbackPermission.tsx diff --git a/backend/src/controllers/v1/roleController.ts b/backend/src/controllers/v1/roleController.ts index e290c6a6e..4eb304e6c 100644 --- a/backend/src/controllers/v1/roleController.ts +++ b/backend/src/controllers/v1/roleController.ts @@ -1,14 +1,4 @@ import { Request, Response } from "express"; -import { BadRequestError } from "../../utils/errors"; -import Role from "../../models/role"; -import { - OrgPermissionActions, - OrgPermissionSubjects, - adminPermissions, - getUserOrgPermissions, - memberPermissions -} from "../../services/RoleService"; -import { validateRequest } from "../../helpers/validation"; import { CreateRoleSchema, DeleteRoleSchema, @@ -17,12 +7,23 @@ import { GetUserProjectPermission, UpdateRoleSchema } from "../../validation"; -import { packRules } from "@casl/ability/extra"; import { adminProjectPermissions, getUserProjectPermissions, + memberProjectPermissions, viewerProjectPermission } from "../../services/ProjectRoleService"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + adminPermissions, + getUserOrgPermissions, + memberPermissions +} from "../../services/RoleService"; +import { BadRequestError } from "../../utils/errors"; +import Role from "../../models/role"; +import { validateRequest } from "../../helpers/validation"; +import { packRules } from "@casl/ability/extra"; export const createRole = async (req: Request, res: Response) => { const { @@ -137,7 +138,9 @@ export const getRoles = async (req: Request, res: Response) => { } const customRoles = await Role.find({ organization: orgId, isOrgRole, workspace: workspaceId }); + // as this is shared between org and workspace switch the rule set based on it const roles = [ + // owner is only in org level role ...(isOrgRole ? [ { @@ -161,8 +164,9 @@ export const getRoles = async (req: Request, res: Response) => { name: "Member", slug: "member", description: "Non-administrative role in an organization", - permissions: isOrgRole ? memberPermissions.rules : adminProjectPermissions.rules + permissions: isOrgRole ? memberPermissions.rules : memberProjectPermissions.rules }, + // viewer role only for project level ...(isOrgRole ? [] : [ @@ -171,7 +175,7 @@ export const getRoles = async (req: Request, res: Response) => { name: "Viewer", slug: "viewer", description: "Non-administrative role in an organization", - permissions: isOrgRole ? viewerProjectPermission.rules : viewerProjectPermission.rules + permissions: viewerProjectPermission.rules } ]), ...customRoles @@ -203,6 +207,7 @@ export const getUserWorkspacePermissions = async (req: Request, res: Response) = params: { workspaceId } } = await validateRequest(GetUserProjectPermission, req); const { permission } = await getUserProjectPermissions(req.user.id, workspaceId); + res.status(200).json({ data: { permissions: packRules(permission.rules) diff --git a/backend/src/services/ProjectRoleService.ts b/backend/src/services/ProjectRoleService.ts index 9a86c048e..1c16518f7 100644 --- a/backend/src/services/ProjectRoleService.ts +++ b/backend/src/services/ProjectRoleService.ts @@ -142,7 +142,7 @@ const buildAdminPermission = () => { can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace); - can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList); + can(ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace); return build(); }; diff --git a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts index d9e109e79..05e29e96d 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts +++ b/frontend/src/views/Org/MembersPage/components/OrgRoleTabSection/OrgRoleModifySection/OrgRoleModifySection.utils.ts @@ -38,23 +38,15 @@ export type TFormSchema = z.infer; // convert role permission to form compatiable data structure export const rolePermission2Form = (permissions: TPermission[] = []) => { - const formVal: TFormSchema["permissions"] = { - workspace: {}, - billing: {}, - settings: {}, - role: {}, - sso: {}, - member: {}, - "service-account": {}, - "incident-contact": {}, - "secret-scanning": {} - }; + const formVal: Partial = {}; 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 = permission.subject as keyof TFormSchema["permissions"]; - (formVal[key] as Exclude)[permission.action] = - true; + const key = subject as keyof TFormSchema["permissions"]; + (formVal[key] as Exclude)[action] = true; }); return formVal; diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx index 26b6c4104..b5f1d7254 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx @@ -33,6 +33,7 @@ import { rolePermission2Form, TFormSchema } from "./ProjectRoleModifySection.utils"; +import { SecretRollbackPermission } from "./SecretRollbackPermission"; import { SingleProjectPermission } from "./SingleProjectPermission"; import { WsProjectPermission } from "./WsProjectPermission"; @@ -226,7 +227,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => { /> -
+
{ formName="secrets" />
-
+
{ formName="folders" />
-
+
{ formName="secret-imports" />
+
+ +
{SINGLE_PERMISSION_LIST.map(({ title, subtitle, icon, formName }) => (
{ />
))} -
- + { - const formVal: TFormSchema["permissions"] = { - secrets: {}, - folders: {}, - integrations: {}, - settings: {}, - role: {}, - member: {}, - "service-tokens": {}, - workspace: {}, - environments: {}, - tags: {}, - webhooks: {}, - "audit-logs": {}, - "ip-allowlist": {}, - "secret-imports": {} - }; + const formVal: Partial = {}; permissions.forEach((permission) => { - if (["secrets", "folders", "secret-imports"].includes(permission.subject)) { - multiEnvApi2Form( - formVal[permission.subject] as TFormSchema["permissions"]["secrets"], - permission - ); + const { subject, action } = permission; + if (!formVal?.[subject]) formVal[subject] = {}; + + if (["secrets", "folders", "secret-imports"].includes(subject)) { + multiEnvApi2Form(formVal[subject] as TFormSchema["permissions"]["secrets"], permission); } else { // everything else follows same pattern // formVal[settings][read | write] = true - const key = permission.subject as keyof Omit< - TFormSchema["permissions"], - "secrets" | "workspace" - >; - formVal[key]![permission.action] = true; + formVal[ + subject as keyof Omit< + TFormSchema["permissions"], + "secrets" | "workspace" | "secret-rollback" + > + ]![action] = true; } }); diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SecretRollbackPermission.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SecretRollbackPermission.tsx new file mode 100644 index 000000000..9d36b58da --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SecretRollbackPermission.tsx @@ -0,0 +1,146 @@ +import { useEffect, useMemo } from "react"; +import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; +import { faPuzzlePiece } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { motion } from "framer-motion"; +import { twMerge } from "tailwind-merge"; + +import { Checkbox, Select, SelectItem } from "@app/components/v2"; +import { useToggle } from "@app/hooks"; + +import { TFormSchema } from "./ProjectRoleModifySection.utils"; + +type Props = { + isNonEditable?: boolean; + setValue: UseFormSetValue; + control: Control; +}; + +enum Permission { + NoAccess = "no-access", + ReadOnly = "read-only", + FullAccess = "full-acess", + Custom = "custom" +} + +const PERMISSIONS = [ + { action: "create", label: "Perform Rollback" }, + { action: "read", label: "View" } +] as const; + +export const SecretRollbackPermission = ({ isNonEditable, setValue, control }: Props) => { + const rule = useWatch({ + control, + name: "permissions.secret-rollback" + }); + const [isCustom, setIsCustom] = useToggle(); + + const selectedPermissionCategory = useMemo(() => { + const actions = Object.keys(rule || {}) as Array; + const totalActions = PERMISSIONS.length; + const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number); + + if (isCustom) return Permission.Custom; + if (score === 0) return Permission.NoAccess; + if (score === totalActions) return Permission.FullAccess; + + return Permission.Custom; + }, [rule, isCustom]); + + useEffect(() => { + if (selectedPermissionCategory === Permission.Custom) setIsCustom.on(); + else setIsCustom.off(); + }, [selectedPermissionCategory]); + + const handlePermissionChange = (val: Permission) => { + if (val === Permission.Custom) setIsCustom.on(); + else setIsCustom.off(); + + switch (val) { + case Permission.NoAccess: + setValue( + "permissions.secret-rollback", + { read: false, create: false }, + { shouldDirty: true } + ); + break; + case Permission.FullAccess: + setValue( + "permissions.secret-rollback", + { read: true, create: true }, + { shouldDirty: true } + ); + break; + case Permission.ReadOnly: + setValue( + "permissions.secret-rollback", + { read: true, create: false }, + { shouldDirty: true } + ); + break; + default: + setValue( + "permissions.secret-rollback", + { read: false, create: false }, + { shouldDirty: true } + ); + break; + } + }; + + return ( +
+
+
+ +
+
+
Secret Rollback
+
Secret rollback control actions
+
+
+ +
+
+ + {isCustom && + PERMISSIONS.map(({ action, label }) => ( + ( + + {label} + + )} + /> + ))} + +
+ ); +}; diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/WsProjectPermission.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/WsProjectPermission.tsx index 08ce12099..b52848ca4 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/WsProjectPermission.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/WsProjectPermission.tsx @@ -18,7 +18,6 @@ type Props = { enum Permission { NoAccess = "no-access", - ReadOnly = "read-only", FullAccess = "full-acess", Custom = "custom" } @@ -92,7 +91,6 @@ export const WsProjectPermission = ({ isNonEditable, setValue, control }: Props) onValueChange={handlePermissionChange} > No Access - Read Only Full Access Custom