From 09928efba3c28822a653d3a6474bd56de8508807 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Thu, 1 Aug 2024 22:24:35 +0800 Subject: [PATCH] feat: added secret folder rbac' --- .../services/permission/project-permission.ts | 5 ++ .../secret-folder/secret-folder-fns.ts | 6 ++ .../secret-folder/secret-folder-service.ts | 76 +++++++++++++++---- .../context/ProjectPermissionContext/types.ts | 1 + .../ProjectRoleModifySection.utils.ts | 3 +- .../RolePermissionSecretFoldersRow.tsx | 71 +++++++++++++++++ .../RolePermissionsSection.tsx | 6 ++ .../components/ActionBar/ActionBar.tsx | 20 ++++- .../FolderListView/FolderListView.tsx | 21 ++++- 9 files changed, 187 insertions(+), 22 deletions(-) create mode 100644 backend/src/services/secret-folder/secret-folder-fns.ts create mode 100644 frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretFoldersRow.tsx diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 7401d8dcd..b942cdd83 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -23,6 +23,7 @@ export enum ProjectPermissionSub { IpAllowList = "ip-allowlist", Project = "workspace", Secrets = "secrets", + SecretFolders = "secret-folders", SecretRollback = "secret-rollback", SecretApproval = "secret-approval", SecretRotation = "secret-rotation", @@ -42,6 +43,10 @@ export type ProjectPermissionSet = ProjectPermissionActions, ProjectPermissionSub.Secrets | (ForcedSubject & SubjectFields) ] + | [ + ProjectPermissionActions, + ProjectPermissionSub.SecretFolders | (ForcedSubject & SubjectFields) + ] | [ProjectPermissionActions, ProjectPermissionSub.Role] | [ProjectPermissionActions, ProjectPermissionSub.Tags] | [ProjectPermissionActions, ProjectPermissionSub.Member] diff --git a/backend/src/services/secret-folder/secret-folder-fns.ts b/backend/src/services/secret-folder/secret-folder-fns.ts new file mode 100644 index 000000000..c8f7d885e --- /dev/null +++ b/backend/src/services/secret-folder/secret-folder-fns.ts @@ -0,0 +1,6 @@ +import { RawRule } from "@casl/ability"; + +import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; + +export const shouldCheckFolderPermission = (rules: RawRule[]) => + rules.some((rule) => (rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders)); diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index 9d6c29454..45b5205b2 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -11,6 +11,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TSecretFolderDALFactory } from "./secret-folder-dal"; +import { shouldCheckFolderPermission } from "./secret-folder-fns"; import { TCreateFolderDTO, TDeleteFolderDTO, @@ -57,10 +58,21 @@ export const secretFolderServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); + + // we do this because we've split Secret and SecretFolder resources + // previously, if one can create/update/read/delete secrets then they can do the same for folders + // for backwards compatibility, we handle authorization only when SecretFolders subject is used + if (shouldCheckFolderPermission(permission.rules)) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) + ); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } const env = await projectEnvDAL.findOne({ projectId, slug: environment }); if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" }); @@ -148,10 +160,20 @@ export const secretFolderServiceFactory = ({ ); folders.forEach(({ environment, path: secretPath }) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); + // we do this because we've split Secret and SecretFolder resources + // previously, if one can create/update/read/delete secrets then they can do the same for folders + // for backwards compatibility, we handle authorization only when SecretFolders subject is used + if (shouldCheckFolderPermission(permission.rules)) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) + ); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } }); const result = await folderDAL.transaction(async (tx) => @@ -243,10 +265,21 @@ export const secretFolderServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); + + // we do this because we've split Secret and SecretFolder resources + // previously, if one can create/update/read/delete secrets then they can do the same for folders + // for backwards compatibility, we handle authorization differently only when SecretFolders subject is used + if (shouldCheckFolderPermission(permission.rules)) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) + ); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" }); @@ -316,10 +349,21 @@ export const secretFolderServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); + + // we do this because we've split Secret and SecretFolder resources + // previously, if one can create/update/read/delete secrets then they can do the same for folders + // for backwards compatibility, we handle authorization differently only when SecretFolders subject is used + if (shouldCheckFolderPermission(permission.rules)) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) + ); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } const env = await projectEnvDAL.findOne({ projectId, slug: environment }); if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" }); diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index fc163da13..557b3cd17 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -21,6 +21,7 @@ export enum ProjectPermissionSub { IpAllowList = "ip-allowlist", Workspace = "workspace", Secrets = "secrets", + SecretFolders = "secret-folders", SecretRollback = "secret-rollback", SecretApproval = "secret-approval", SecretRotation = "secret-rotation", diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts index 0b534347d..7e27bde6b 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts @@ -36,6 +36,7 @@ export const formSchema = z.object({ permissions: z .object({ secrets: z.record(multiEnvPermissionSchema).optional(), + "secret-folders": generalPermissionSchema.optional(), member: generalPermissionSchema, groups: generalPermissionSchema, identity: generalPermissionSchema, @@ -158,7 +159,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => { Object.entries(formVal || {}).forEach(([rule, actions]) => { if (rule === "secrets") { multiEnvForm2Api(permissions, JSON.parse(JSON.stringify(actions || {})), rule); - } else { + } else if (actions) { Object.entries(actions).forEach(([action, isAllowed]) => { if (isAllowed) { permissions.push({ subject: rule, action }); diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretFoldersRow.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretFoldersRow.tsx new file mode 100644 index 000000000..eb0353b73 --- /dev/null +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretFoldersRow.tsx @@ -0,0 +1,71 @@ +import { Control, UseFormSetValue, useWatch } from "react-hook-form"; + +import { Select, SelectItem, Td, Tr } from "@app/components/v2"; +import { ProjectPermissionSub } from "@app/context"; +import { TFormSchema } from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils"; + +type Props = { + isEditable: boolean; + setValue: UseFormSetValue; + control: Control; +}; + +enum Permission { + SameAsSecrets = "same-as-secrets", + ReadOnly = "read-only" +} + +export const RowPermissionSecretFoldersRow = ({ isEditable, setValue, control }: Props) => { + const formName = ProjectPermissionSub.SecretFolders; + const rule = useWatch({ + control, + name: `permissions.${formName}` + }); + + const selectedPermissionCategory = + rule !== undefined ? Permission.ReadOnly : Permission.SameAsSecrets; + + const handlePermissionChange = (val: Permission) => { + if (!val) return; + switch (val) { + case Permission.SameAsSecrets: { + setValue(`permissions.${formName}`, undefined, { shouldDirty: true }); + break; + } + // Read-only + default: + setValue( + `permissions.${formName}`, + { + read: true, + edit: false, + create: false, + delete: false + }, + { + shouldDirty: true + } + ); + break; + } + }; + + return ( + + + Secret Folders + + + + + ); +}; diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx index 11c47a501..e56c3d44e 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -13,6 +13,7 @@ import { } from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils"; import { RolePermissionRow } from "./RolePermissionRow"; +import { RowPermissionSecretFoldersRow } from "./RolePermissionSecretFoldersRow"; import { RowPermissionSecretsRow } from "./RolePermissionSecretsRow"; const SINGLE_PERMISSION_LIST = [ @@ -177,6 +178,11 @@ export const RolePermissionsSection = ({ roleSlug }: Props) => { getValue={getValues} control={control} /> + {SINGLE_PERMISSION_LIST.map((permission) => { return ( + (rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders) + ); + const debouncedOnSearch = debounce(onSearchChange, 500); const handleFolderCreate = async (folderName: string) => { @@ -411,7 +422,12 @@ export const ActionBar = ({
{(isAllowed) => (