diff --git a/frontend/src/components/permissions/ProjectPermissionCan.tsx b/frontend/src/components/permissions/ProjectPermissionCan.tsx index f1af141f2..4fc5acde9 100644 --- a/frontend/src/components/permissions/ProjectPermissionCan.tsx +++ b/frontend/src/components/permissions/ProjectPermissionCan.tsx @@ -1,23 +1,25 @@ import { FunctionComponent, ReactNode } from "react"; -import { BoundCanProps, Can } from "@casl/react"; +import { AbilityTuple, MongoAbility } from "@casl/ability"; +import { Can } from "@casl/react"; -import { TProjectPermission, useProjectPermission } from "@app/context/ProjectPermissionContext"; +import { ProjectPermissionSet, useProjectPermission } from "@app/context/ProjectPermissionContext"; -import { Tooltip } from "../v2"; +import { Tooltip } from "../v2/Tooltip"; -type Props = { +type Props = { label?: ReactNode; // this prop is used when there exist already a tooltip as helper text for users // so when permission is allowed same tooltip will be reused to show helpertext renderTooltip?: boolean; allowedLabel?: string; - // BUG(akhilmhdh): As a workaround for now i put any but this should be TProjectPermission - // For some reason when i put TProjectPermission in a wrapper component it just wont work causes a weird ts error - // tried a lot combinations - // REF: https://github.com/stalniy/casl/blob/ac081a34f56366a7eaaed05d21689d27041ef005/packages/casl-react/src/factory.ts#L15 -} & BoundCanProps; + children: ReactNode | ((isAllowed: boolean, ability: T) => ReactNode); + passThrough?: boolean; + I: T[0]; + a: T[1]; + ability?: MongoAbility; +}; -export const ProjectPermissionCan: FunctionComponent = ({ +export const ProjectPermissionCan: FunctionComponent> = ({ label = "Access restricted", children, passThrough = true, @@ -31,9 +33,7 @@ export const ProjectPermissionCan: FunctionComponent = ({ {(isAllowed, ability) => { // akhilmhdh: This is set as type due to error in casl react type. const finalChild = - typeof children === "function" - ? children(isAllowed, ability as TProjectPermission) - : children; + typeof children === "function" ? children(isAllowed, ability as any) : children; if (!isAllowed && passThrough) { return {finalChild}; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index d47a3d01b..c52745f24 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -52,9 +52,11 @@ export enum ProjectPermissionSub { Tags = "tags", AuditLogs = "audit-logs", IpAllowList = "ip-allowlist", - Workspace = "workspace", + Project = "workspace", Secrets = "secrets", SecretFolders = "secret-folders", + SecretImports = "secret-imports", + DynamicSecrets = "dynamic-secrets", SecretRollback = "secret-rollback", SecretApproval = "secret-approval", SecretRotation = "secret-rotation", @@ -68,7 +70,24 @@ export enum ProjectPermissionSub { Cmek = "cmek" } -type SubjectFields = { +export type SecretSubjectFields = { + environment: string; + secretPath: string; + secretName: string; + secretTags: string[]; +}; + +export type SecretFolderSubjectFields = { + environment: string; + secretPath: string; +}; + +export type DynamicSecretSubjectFields = { + environment: string; + secretPath: string; +}; + +export type SecretImportSubjectFields = { environment: string; secretPath: string; }; @@ -76,13 +95,30 @@ type SubjectFields = { export type ProjectPermissionSet = | [ ProjectPermissionActions, - ProjectPermissionSub.Secrets | (ForcedSubject & SubjectFields) + ( + | ProjectPermissionSub.Secrets + | (ForcedSubject & SecretSubjectFields) + ) ] | [ ProjectPermissionActions, ( | ProjectPermissionSub.SecretFolders - | (ForcedSubject & SubjectFields) + | (ForcedSubject & SecretFolderSubjectFields) + ) + ] + | [ + ProjectPermissionActions, + ( + | ProjectPermissionSub.DynamicSecrets + | (ForcedSubject & DynamicSecretSubjectFields) + ) + ] + | [ + ProjectPermissionActions, + ( + | ProjectPermissionSub.SecretImports + | (ForcedSubject & SecretImportSubjectFields) ) ] | [ProjectPermissionActions, ProjectPermissionSub.Role] @@ -95,19 +131,19 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.Environments] | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] | [ProjectPermissionActions, ProjectPermissionSub.Settings] - | [ProjectPermissionActions, ProjectPermissionSub.Identity] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] + | [ProjectPermissionActions, ProjectPermissionSub.Identity] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.Certificates] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] - | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] - | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] + | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] + | [ProjectPermissionActions.Edit, ProjectPermissionSub.Project] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback] - | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]; - + | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek] + | [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]; export type TProjectPermission = MongoAbility; diff --git a/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx b/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx index 103ff61b7..22c91ab52 100644 --- a/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx +++ b/frontend/src/hoc/withProjectPermission/withProjectPermission.tsx @@ -1,31 +1,29 @@ import { ComponentType } from "react"; -import { Abilities, AbilityTuple, Generics, SubjectType } from "@casl/ability"; +import { AbilityTuple } from "@casl/ability"; import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -import { TProjectPermission, useProjectPermission } from "@app/context"; +import { useProjectPermission } from "@app/context"; +import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; -type Props = (T extends AbilityTuple - ? { - action: T[0]; - subject: Extract; - } - : { - action: string; - subject: string; - }) & { className?: string; containerClassName?: string }; +type Props = { + className?: string; + containerClassName?: string; + action: T[0]; + subject: T[1]; +}; -export const withProjectPermission = ( - Component: ComponentType, - { action, subject, className, containerClassName }: Props["abilities"]> +export const withProjectPermission = ( + Component: ComponentType, "action" | "subject"> & T>, + { action, subject, className, containerClassName }: Props ) => { - const HOC = (hocProps: T) => { + const HOC = (hocProps: Omit, "action" | "subject"> & T) => { const { permission } = useProjectPermission(); // akhilmhdh: Set as any due to casl/react ts type bug // REASON: casl due to its type checking can't seem to union even if union intersection is applied - if (permission.cannot(action as any, subject)) { + if (permission.cannot(action as any, subject as any)) { return (
{ mutationFn: async ({ projectSlug, ...dto }: TCreateProjectRoleDTO) => { const { data: { role } - } = await apiRequest.post(`/api/v1/workspace/${projectSlug}/roles`, dto); + } = await apiRequest.post(`/api/v2/workspace/${projectSlug}/roles`, dto); return role; }, onSuccess: (_, { projectSlug }) => { @@ -38,7 +38,7 @@ export const useUpdateProjectRole = () => { mutationFn: async ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) => { const { data: { role } - } = await apiRequest.patch(`/api/v1/workspace/${projectSlug}/roles/${id}`, dto); + } = await apiRequest.patch(`/api/v2/workspace/${projectSlug}/roles/${id}`, dto); return role; }, onSuccess: (_, { projectSlug }) => { @@ -53,7 +53,7 @@ export const useDeleteProjectRole = () => { mutationFn: async ({ projectSlug, id }: TDeleteProjectRoleDTO) => { const { data: { role } - } = await apiRequest.delete(`/api/v1/workspace/${projectSlug}/roles/${id}`); + } = await apiRequest.delete(`/api/v2/workspace/${projectSlug}/roles/${id}`); return role; }, onSuccess: (_, { projectSlug }) => { diff --git a/frontend/src/hooks/api/roles/queries.tsx b/frontend/src/hooks/api/roles/queries.tsx index 77e82e4fa..288fdf396 100644 --- a/frontend/src/hooks/api/roles/queries.tsx +++ b/frontend/src/hooks/api/roles/queries.tsx @@ -7,6 +7,7 @@ import picomatch from "picomatch"; import { apiRequest } from "@app/config/request"; import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types"; import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext/types"; +import { omit } from "@app/lib/fn/object"; import { OrgUser, TProjectMembership } from "../users/types"; import { @@ -49,7 +50,7 @@ export const roleQueryKeys = { export const getProjectRoles = async (projectId: string) => { const { data } = await apiRequest.get<{ roles: Array> }>( - `/api/v1/workspace/${projectId}/roles` + `/api/v2/workspace/${projectId}/roles` ); return data.roles; }; @@ -66,7 +67,7 @@ export const useGetProjectRoleBySlug = (projectSlug: string, roleSlug: string) = queryKey: roleQueryKeys.getProjectRoleBySlug(projectSlug, roleSlug), queryFn: async () => { const { data } = await apiRequest.get<{ role: TProjectRole }>( - `/api/v1/workspace/${projectSlug}/roles/slug/${roleSlug}` + `/api/v2/workspace/${projectSlug}/roles/slug/${roleSlug}` ); return data.role; }, @@ -134,7 +135,7 @@ const getUserProjectPermissions = async ({ workspaceId }: TGetUserProjectPermiss permissions: PackRule>>[]; membership: Omit & { roles: { role: string }[] }; }; - }>(`/api/v1/workspace/${workspaceId}/permissions`, {}); + }>(`/api/v2/workspace/${workspaceId}/permissions`, {}); return data.data; }; @@ -146,7 +147,19 @@ export const useGetUserProjectPermissions = ({ workspaceId }: TGetUserProjectPer enabled: Boolean(workspaceId), select: (data) => { const rule = unpackRules>>(data.permissions); - const ability = createMongoAbility(rule, { conditionsMatcher }); + const ability = createMongoAbility(rule, { + // this allows in frontend to skip some rules using * + conditionsMatcher: (rules) => { + return (entity) => { + const rulesStrippedOfWildcard = omit( + rules, + Object.keys(entity).filter((el) => entity[el].includes("*")) + ); + const baseMatcher = conditionsMatcher(rulesStrippedOfWildcard); + return baseMatcher(entity); + }; + } + }); const membership = { ...data.membership, diff --git a/frontend/src/lib/fn/object.ts b/frontend/src/lib/fn/object.ts new file mode 100644 index 000000000..0af1ead2d --- /dev/null +++ b/frontend/src/lib/fn/object.ts @@ -0,0 +1,20 @@ +/** + * Omit a list of properties from an object + * returning a new object with the properties + * that remain + */ +export const omit = (obj: T, keys: TKeys[]): Omit => { + if (!obj) return {} as Omit; + if (!keys || keys.length === 0) return obj as Omit; + return keys.reduce( + (acc, key) => { + // Gross, I know, it's mutating the object, but we + // are allowing it in this very limited scope due + // to the performance implications of an omit func. + // Not a pattern or practice to use elsewhere. + delete acc[key]; + return acc; + }, + { ...obj } + ); +}; 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 7dd981691..d800ddf6b 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts @@ -28,10 +28,6 @@ const CmekPolicyActionSchema = z.object({ decrypt: z.boolean().optional() }); -const SecretFolderPolicyActionSchema = z.object({ - read: z.boolean().optional() -}); - const SecretRollbackPolicyActionSchema = z.object({ read: z.boolean().optional(), create: z.boolean().optional() @@ -42,11 +38,29 @@ const WorkspacePolicyActionSchema = z.object({ delete: z.boolean().optional() }); -const ConditionSchema = z.object({ - operator: z.string(), - lhs: z.string(), - rhs: z.string().min(1) -}); +const ConditionSchema = z + .object({ + operator: z.string(), + lhs: z.string(), + rhs: z.string().min(1) + }) + .array() + .optional() + .default([]) + .refine( + (el) => { + const lhsOperatorSet = new Set(); + for (let i = 0; i < el.length; i += 1) { + const { lhs, operator } = el[i]; + if (lhsOperatorSet.has(`${lhs}-${operator}`)) { + return false; + } + lhsOperatorSet.add(`${lhs}-${operator}`); + } + return true; + }, + { message: "Duplicate operator found for a condition" } + ); export const formSchema = z.object({ name: z.string().trim(), @@ -59,27 +73,25 @@ export const formSchema = z.object({ permissions: z .object({ [ProjectPermissionSub.Secrets]: GeneralPolicyActionSchema.extend({ - conditions: ConditionSchema.array() - .optional() - .default([]) - .refine( - (el) => { - const lhsOperatorSet = new Set(); - for (let i = 0; i < el.length; i += 1) { - const { lhs, operator } = el[i]; - if (lhsOperatorSet.has(`${lhs}-${operator}`)) { - return false; - } - lhsOperatorSet.add(`${lhs}-${operator}`); - } - return true; - }, - { message: "Duplicate operator found for a condition" } - ) + conditions: ConditionSchema + }) + .array() + .default([]), + [ProjectPermissionSub.SecretFolders]: GeneralPolicyActionSchema.extend({ + conditions: ConditionSchema + }) + .array() + .default([]), + [ProjectPermissionSub.SecretImports]: GeneralPolicyActionSchema.extend({ + conditions: ConditionSchema + }) + .array() + .default([]), + [ProjectPermissionSub.DynamicSecrets]: GeneralPolicyActionSchema.extend({ + conditions: ConditionSchema }) .array() .default([]), - [ProjectPermissionSub.SecretFolders]: SecretFolderPolicyActionSchema.array().default([]), [ProjectPermissionSub.Member]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Groups]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Identity]: GeneralPolicyActionSchema.array().default([]), @@ -98,7 +110,7 @@ export const formSchema = z.object({ [ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]), - [ProjectPermissionSub.Workspace]: WorkspacePolicyActionSchema.array().default([]), + [ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]), [ProjectPermissionSub.Tags]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretRotation]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.Kms]: GeneralPolicyActionSchema.array().default([]), @@ -110,8 +122,22 @@ export const formSchema = z.object({ export type TFormSchema = z.infer; +type TConditionalFields = + | ProjectPermissionSub.Secrets + | ProjectPermissionSub.SecretFolders + | ProjectPermissionSub.SecretImports + | ProjectPermissionSub.DynamicSecrets; + +export const isConditionalSubjects = ( + subject: ProjectPermissionSub +): subject is TConditionalFields => + subject === (ProjectPermissionSub.Secrets as const) || + subject === ProjectPermissionSub.DynamicSecrets || + subject === ProjectPermissionSub.SecretImports || + subject === ProjectPermissionSub.SecretFolders; + const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => { - const formConditions: z.infer[] = []; + const formConditions: z.infer = []; Object.entries(caslConditions).forEach(([type, condition]) => { if (typeof condition === "string") { formConditions.push({ @@ -144,6 +170,9 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { if ( [ ProjectPermissionSub.Secrets, + ProjectPermissionSub.SecretFolders, + ProjectPermissionSub.SecretImports, + ProjectPermissionSub.DynamicSecrets, ProjectPermissionSub.Member, ProjectPermissionSub.Groups, ProjectPermissionSub.Identity, @@ -172,7 +201,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { const canCreate = action.includes(ProjectPermissionActions.Create); // from above statement we are sure it won't be undefined - if (subject === ProjectPermissionSub.Secrets) { + if (isConditionalSubjects(subject)) { if (!formVal[subject]) formVal[subject] = []; formVal[subject]!.push({ read: canRead, @@ -190,13 +219,13 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true; if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true; } - } else if (subject === ProjectPermissionSub.Workspace) { + } else if (subject === ProjectPermissionSub.Project) { const canEdit = action.includes(ProjectPermissionActions.Edit); const canDelete = action.includes(ProjectPermissionActions.Delete); if (!formVal[subject]) formVal[subject] = [{}]; // from above statement we are sure it won't be undefined - if (canEdit) formVal[subject as ProjectPermissionSub.Workspace]![0].edit = true; + if (canEdit) formVal[subject as ProjectPermissionSub.Project]![0].edit = true; if (canDelete) formVal[subject as ProjectPermissionSub.Member]![0].delete = true; } else if (subject === ProjectPermissionSub.SecretRollback) { const canRead = action.includes(ProjectPermissionActions.Read); @@ -206,12 +235,6 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => { // from above statement we are sure it won't be undefined if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true; if (canCreate) formVal[subject as ProjectPermissionSub.Member]![0].create = true; - } else if (subject === ProjectPermissionSub.SecretFolders) { - const canRead = action.includes(ProjectPermissionActions.Read); - if (!formVal[subject]) formVal[subject] = [{}]; - - // from above statement we are sure it won't be undefined - if (canRead) formVal[subject as ProjectPermissionSub.Member]![0].read = true; } else if (subject === ProjectPermissionSub.Cmek) { const canRead = action.includes(ProjectPermissionCmekActions.Read); const canEdit = action.includes(ProjectPermissionCmekActions.Edit); @@ -306,7 +329,30 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { }, [ProjectPermissionSub.SecretFolders]: { title: "Secret Folders", - actions: [{ label: "Read Only", value: "read" }] + actions: [ + { label: "Read", value: "read" }, + { label: "Create", value: "create" }, + { label: "Modify", value: "edit" }, + { label: "Remove", value: "delete" } + ] + }, + [ProjectPermissionSub.SecretImports]: { + title: "Secret Folders", + actions: [ + { label: "Read", value: "read" }, + { label: "Create", value: "create" }, + { label: "Modify", value: "edit" }, + { label: "Remove", value: "delete" } + ] + }, + [ProjectPermissionSub.DynamicSecrets]: { + title: "Secret Folders", + actions: [ + { label: "Read", value: "read" }, + { label: "Create", value: "create" }, + { label: "Modify", value: "edit" }, + { label: "Remove", value: "delete" } + ] }, [ProjectPermissionSub.Cmek]: { title: "KMS", @@ -332,7 +378,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { { label: "Remove", value: "delete" } ] }, - [ProjectPermissionSub.Workspace]: { + [ProjectPermissionSub.Project]: { title: "Project", actions: [ { label: "Update project details", value: "edit" }, diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx index fb3567897..a7ac06010 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -10,13 +10,15 @@ import { ProjectPermissionSub, useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api"; -import { GeneralPermissionOptions } from "./components/GeneralPermissionOptions"; +import { GeneralPermissionConditions } from "./components/GeneralPermissionConditions"; +import { GeneralPermissionPolicies } from "./components/GeneralPermissionPolicies"; import { NewPermissionRule } from "./components/NewPermissionRule"; import { SecretPermissionConditions } from "./components/SecretPermissionConditions"; import { PermissionEmptyState } from "./PermissionEmptyState"; import { formRolePermission2API, formSchema, + isConditionalSubjects, PROJECT_PERMISSION_OBJECT, rolePermission2Form, TFormSchema @@ -27,6 +29,17 @@ type Props = { isDisabled?: boolean; }; +const renderConditionalComponents = (subject: ProjectPermissionSub, isDisabled?: boolean) => { + if (subject === ProjectPermissionSub.Secrets) + return ; + + if (isConditionalSubjects(subject)) { + return ; + } + + return undefined; +}; + export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => { const { currentWorkspace } = useWorkspace(); const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const); @@ -130,17 +143,15 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
{!isLoading && } {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => ( - - {subject === ProjectPermissionSub.Secrets ? ( - - ) : undefined} - + {renderConditionalComponents(subject, isDisabled)} + ))}
diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionConditions.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionConditions.tsx new file mode 100644 index 000000000..da4dd72eb --- /dev/null +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionConditions.tsx @@ -0,0 +1,155 @@ +import { Controller, useFieldArray, useFormContext } from "react-hook-form"; +import { faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2"; +import { + PermissionConditionOperators, + ProjectPermissionSub +} from "@app/context/ProjectPermissionContext/types"; + +import { TFormSchema } from "../ProjectRoleModifySection.utils"; + +type Props = { + position?: number; + isDisabled?: boolean; + type: + | ProjectPermissionSub.DynamicSecrets + | ProjectPermissionSub.SecretFolders + | ProjectPermissionSub.SecretImports; +}; + +const getValueLabel = (type: string) => { + if (type === "environment") return "Environment slug"; + if (type === "secretPath") return "Folder path"; + return ""; +}; + +export const GeneralPermissionConditions = ({ position = 0, isDisabled, type }: Props) => { + const { + control, + watch, + formState: { errors } + } = useFormContext(); + const items = useFieldArray({ + control, + name: `permissions.${type}.${position}.conditions` + }); + + return ( +
+
+ {items.fields.map((el, index) => { + const lhs = watch(`permissions.${type}.${position}.conditions.${index}.lhs`); + return ( +
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> +
+
+ ( + + + + )} + /> +
+
+ items.remove(index)} + > + + +
+
+ ); + })} +
+ {errors?.permissions?.[type]?.[position]?.conditions?.message && ( +
+ + {errors?.permissions?.[type]?.[position]?.conditions?.message} +
+ )} +
{}
+
+ +
+
+ ); +}; diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionOptions.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies.tsx similarity index 93% rename from frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionOptions.tsx rename to frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies.tsx index cdf0f5151..a85015508 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionOptions.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies.tsx @@ -8,7 +8,11 @@ import { Button, Checkbox, Tag } from "@app/components/v2"; import { ProjectPermissionSub } from "@app/context"; import { useToggle } from "@app/hooks"; -import { TFormSchema, TProjectPermissionObject } from "../ProjectRoleModifySection.utils"; +import { + isConditionalSubjects, + TFormSchema, + TProjectPermissionObject +} from "../ProjectRoleModifySection.utils"; type Props = { title: string; @@ -18,7 +22,7 @@ type Props = { isDisabled?: boolean; }; -export const GeneralPermissionOptions = >({ +export const GeneralPermissionPolicies = >({ subject, actions, children, @@ -98,10 +102,10 @@ export const GeneralPermissionOptions = - {!isDisabled && subject === ProjectPermissionSub.Secrets && ( + {!isDisabled && isConditionalSubjects(subject) && (
)} - {canReadSecret && imports?.length && ( + {canReadSecretImports && imports?.length && ( { onNavigateToFolder={handleResetFilter} /> )} - {canReadSecret && dynamicSecrets?.length && ( + {canReadDynamicSecret && dynamicSecrets?.length && ( { isProtectedBranch={isProtectedBranch} /> )} - {!canReadSecret && folders?.length === 0 && } + {!canReadSecret && + !canReadDynamicSecret && + !canReadSecretImports && + folders?.length === 0 && } {!isDetailsLoading && totalCount > 0 && ( diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx index 333f5f0c6..8da0b9167 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx @@ -45,12 +45,7 @@ import { Tooltip, UpgradePlanModal } from "@app/components/v2"; -import { - ProjectPermissionActions, - ProjectPermissionSub, - useProjectPermission, - useSubscription -} from "@app/context"; +import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api"; import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries"; @@ -125,12 +120,6 @@ export const ActionBar = ({ const { reset: resetSelectedSecret } = useSelectedSecretActions(); const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length); - const { permission } = useProjectPermission(); - - const shouldCheckFolderPermission = permission.rules.some((rule) => - (rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders) - ); - const handleFolderCreate = async (folderName: string) => { try { await createFolder({ @@ -438,7 +427,12 @@ export const ActionBar = ({
{(isAllowed) => (
- + {(isAllowed) => ( { )}
- + {(isAllowed) => (