From 6818c8730f0ed59434368386f4983ed4311a5429 Mon Sep 17 00:00:00 2001 From: Alfonso Hernandez Date: Wed, 17 Jul 2024 16:49:54 +0200 Subject: [PATCH 01/21] chore: rename approverUserIds to approvers in registerSecretApprovalPolicy --- .../ee/routes/v1/secret-approval-policy-router.ts | 8 ++++---- .../secret-approval-policy-service.ts | 12 ++++++------ .../secret-approval-policy-types.ts | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/backend/src/ee/routes/v1/secret-approval-policy-router.ts b/backend/src/ee/routes/v1/secret-approval-policy-router.ts index ee10131dd..f25ae42f3 100644 --- a/backend/src/ee/routes/v1/secret-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-policy-router.ts @@ -25,10 +25,10 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi .optional() .nullable() .transform((val) => (val ? removeTrailingSlash(val) : val)), - approverUserIds: z.string().array().min(1), + approvers: z.string().array().min(1), approvals: z.number().min(1).default(1) }) - .refine((data) => data.approvals <= data.approverUserIds.length, { + .refine((data) => data.approvals <= data.approvers.length, { path: ["approvals"], message: "The number of approvals should be lower than the number of approvers." }), @@ -66,7 +66,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi body: z .object({ name: z.string().optional(), - approverUserIds: z.string().array().min(1), + approvers: z.string().array().min(1), approvals: z.number().min(1).default(1), secretPath: z .string() @@ -74,7 +74,7 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi .nullable() .transform((val) => (val ? removeTrailingSlash(val) : val)) }) - .refine((data) => data.approvals <= data.approverUserIds.length, { + .refine((data) => data.approvals <= data.approvers.length, { path: ["approvals"], message: "The number of approvals should be lower than the number of approvers." }), diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index 2db825c88..be6a334e2 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -45,12 +45,12 @@ export const secretApprovalPolicyServiceFactory = ({ actorOrgId, actorAuthMethod, approvals, - approverUserIds, + approvers, projectId, secretPath, environment }: TCreateSapDTO) => { - if (approvals > approverUserIds.length) + if (approvals > approvers.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); const { permission } = await permissionService.getProjectPermission( @@ -78,7 +78,7 @@ export const secretApprovalPolicyServiceFactory = ({ tx ); await secretApprovalPolicyApproverDAL.insertMany( - approverUserIds.map((approverUserId) => ({ + approvers.map((approverUserId) => ({ approverUserId, policyId: doc.id })), @@ -90,7 +90,7 @@ export const secretApprovalPolicyServiceFactory = ({ }; const updateSecretApprovalPolicy = async ({ - approverUserIds, + approvers, secretPath, name, actorId, @@ -122,10 +122,10 @@ export const secretApprovalPolicyServiceFactory = ({ }, tx ); - if (approverUserIds) { + if (approvers) { await secretApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx); await secretApprovalPolicyApproverDAL.insertMany( - approverUserIds.map((approverUserId) => ({ + approvers.map((approverUserId) => ({ approverUserId, policyId: doc.id })), diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts index 1a527289c..2ddd9b51b 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts @@ -4,7 +4,7 @@ export type TCreateSapDTO = { approvals: number; secretPath?: string | null; environment: string; - approverUserIds: string[]; + approvers: string[]; projectId: string; name: string; } & Omit; @@ -13,7 +13,7 @@ export type TUpdateSapDTO = { secretPolicyId: string; approvals?: number; secretPath?: string | null; - approverUserIds: string[]; + approvers: string[]; name?: string; } & Omit; From d19c856e9b4f917dcf37e67c1f8335195bdb0acb Mon Sep 17 00:00:00 2001 From: Alfonso Hernandez Date: Wed, 17 Jul 2024 20:05:34 +0200 Subject: [PATCH 02/21] chore(frontend): rename approverUserIds to approvers in registerSecretApprovalPolicy --- frontend/src/hooks/api/secretApproval/mutation.tsx | 8 ++++---- frontend/src/hooks/api/secretApproval/types.ts | 5 +++-- .../components/SecretApprovalPolicyRow.tsx | 2 +- .../components/SecretPolicyForm.tsx | 8 ++++---- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/frontend/src/hooks/api/secretApproval/mutation.tsx b/frontend/src/hooks/api/secretApproval/mutation.tsx index 991111ef9..e0a8df95d 100644 --- a/frontend/src/hooks/api/secretApproval/mutation.tsx +++ b/frontend/src/hooks/api/secretApproval/mutation.tsx @@ -9,12 +9,12 @@ export const useCreateSecretApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TCreateSecretPolicyDTO>({ - mutationFn: async ({ environment, workspaceId, approvals, approverUserIds, secretPath, name }) => { + mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name }) => { const { data } = await apiRequest.post("/api/v1/secret-approvals", { environment, workspaceId, approvals, - approverUserIds, + approvers, secretPath, name }); @@ -30,10 +30,10 @@ export const useUpdateSecretApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TUpdateSecretPolicyDTO>({ - mutationFn: async ({ id, approverUserIds, approvals, secretPath, name }) => { + mutationFn: async ({ id, approvers, approvals, secretPath, name }) => { const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, { approvals, - approverUserIds, + approvers, secretPath, name }); diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts index f3b8639f7..bc0d57d40 100644 --- a/frontend/src/hooks/api/secretApproval/types.ts +++ b/frontend/src/hooks/api/secretApproval/types.ts @@ -9,6 +9,7 @@ export type TSecretApprovalPolicy = { secretPath?: string; approvals: number; userApprovers: { userId: string }[]; + updatedAt: Date; }; export type TGetSecretApprovalPoliciesDTO = { @@ -26,14 +27,14 @@ export type TCreateSecretPolicyDTO = { name?: string; environment: string; secretPath?: string | null; - approverUserIds?: string[]; + approvers?: string[]; approvals?: number; }; export type TUpdateSecretPolicyDTO = { id: string; name?: string; - approverUserIds?: string[]; + approvers?: string[]; secretPath?: string | null; approvals?: number; // for invalidating list diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx index d3321850a..9f8a08c44 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx @@ -51,7 +51,7 @@ export const SecretApprovalPolicyRow = ({ { workspaceId, id: policy.id, - approverUserIds: selectedApprovers + approvers: selectedApprovers }, { onSettled: () => { diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx index b0db2affd..18b0b87c8 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx @@ -37,9 +37,9 @@ const formSchema = z name: z.string().optional(), secretPath: z.string().optional().nullable(), approvals: z.number().min(1), - approverUserIds: z.string().array().min(1) + approvers: z.string().array().min(1) }) - .refine((data) => data.approvals <= data.approverUserIds.length, { + .refine((data) => data.approvals <= data.approvers.length, { path: ["approvals"], message: "The number of approvals should be lower than the number of approvers." }); @@ -62,7 +62,7 @@ export const SecretPolicyForm = ({ values: editValues ? { ...editValues, - approverUserIds: editValues.userApprovers.map(({ userId }) => userId), + approvers: editValues.userApprovers.map(({ userId }) => userId), environment: editValues.environment.slug } : undefined @@ -183,7 +183,7 @@ export const SecretPolicyForm = ({ /> ( Date: Wed, 17 Jul 2024 20:22:43 +0200 Subject: [PATCH 03/21] feat(frontend): welcome ApprovalPolicyList --- frontend/src/helpers/policies.ts | 12 + .../src/hooks/api/accessApproval/types.ts | 6 + frontend/src/hooks/api/policies/enums.ts | 9 + .../SecretApprovalPage/SecretApprovalPage.tsx | 28 +- .../ApprovalPolicyList/ApprovalPolicyList.tsx | 262 ++++++++++++ .../components/AccessPolicyModal.tsx | 376 ++++++++++++++++++ .../components/ApprovalPolicyRow.tsx | 191 +++++++++ .../components/ApprovalPolicyList/index.tsx | 1 + 8 files changed, 881 insertions(+), 4 deletions(-) create mode 100644 frontend/src/helpers/policies.ts create mode 100644 frontend/src/hooks/api/policies/enums.ts create mode 100644 frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/index.tsx diff --git a/frontend/src/helpers/policies.ts b/frontend/src/helpers/policies.ts new file mode 100644 index 000000000..c6d7c935a --- /dev/null +++ b/frontend/src/helpers/policies.ts @@ -0,0 +1,12 @@ +import { PolicyType } from "@app/hooks/api/policies/enums"; + +export const policyDetails: Record = { + [PolicyType.AccessPolicy]: { + className: "bg-lime-900 text-lime-100", + name: "Access Policy" + }, + [PolicyType.ChangePolicy]: { + className: "bg-indigo-900 text-indigo-100", + name: "Change Policy" + } +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts index 2176b8bc1..a6d09a227 100644 --- a/frontend/src/hooks/api/accessApproval/types.ts +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -1,3 +1,4 @@ +import { EnforcementLevel, PolicyType } from "../policies/enums"; import { TProjectPermission } from "../roles/types"; import { WorkspaceEnv } from "../workspace/types"; @@ -11,6 +12,11 @@ export type TAccessApprovalPolicy = { environment: WorkspaceEnv; projectId: string; approvers: string[]; + policyType: PolicyType; + approversRequired: boolean; + enforcementLevel: EnforcementLevel; + updatedAt: Date; + userApprovers?: { userId: string }[]; }; export type TAccessApprovalRequest = { diff --git a/frontend/src/hooks/api/policies/enums.ts b/frontend/src/hooks/api/policies/enums.ts new file mode 100644 index 000000000..f91bcb98c --- /dev/null +++ b/frontend/src/hooks/api/policies/enums.ts @@ -0,0 +1,9 @@ +export enum EnforcementLevel { + Hard = "hard", + Soft = "soft" +} + +export enum PolicyType { + ChangePolicy = "change", + AccessPolicy = "access" +} diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index 9c273848f..f8d056cce 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -3,11 +3,14 @@ import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { Badge } from "@app/components/v2/Badge"; import { Divider } from "@app/components/v2/Divider"; import { useWorkspace } from "@app/context"; +import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api"; import { AccessApprovalPolicyList } from "./components/AccessApprovalPolicyList"; import { AccessApprovalRequest } from "./components/AccessApprovalRequest"; +import { ApprovalPolicyList } from "./components/ApprovalPolicyList"; import { SecretApprovalPolicyList } from "./components/SecretApprovalPolicyList"; import { SecretApprovalRequest } from "./components/SecretApprovalRequest"; @@ -15,13 +18,19 @@ enum TabSection { SecretApprovalRequests = "approval-requests", SecretPolicies = "approval-rules", ResourcePolicies = "resource-rules", - ResourceApprovalRequests = "resource-requests" + ResourceApprovalRequests = "resource-requests", + Policies = "policies" } export const SecretApprovalPage = () => { const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; const projectSlug = currentWorkspace?.slug || ""; + const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId: projectId }); + const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug }); + const defaultTab = (accessApprovalRequestCount?.pendingCount || 0) > (secretApprovalReqCount?.open || 0) + ? TabSection.ResourceApprovalRequests + : TabSection.SecretApprovalRequests; return (
@@ -45,13 +54,21 @@ export const SecretApprovalPage = () => {
- + - Secret Requests + + Secret Requests + {Boolean(secretApprovalReqCount?.open) && ({secretApprovalReqCount?.open})} + Secret Policies - Access Requests + + Access Requests + {Boolean(accessApprovalRequestCount?.pendingCount) && {accessApprovalRequestCount?.pendingCount}} + Access Request Policies + + Policies @@ -65,6 +82,9 @@ export const SecretApprovalPage = () => { + + + ); diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx new file mode 100644 index 000000000..fb21521c9 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx @@ -0,0 +1,262 @@ +import { useMemo,useState } from "react"; +import { faCheckCircle,faChevronDown, faFileShield, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr, + UpgradePlanModal +} from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + TProjectPermission, + useProjectPermission, + useSubscription, + useWorkspace +} from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { useDeleteAccessApprovalPolicy, useDeleteSecretApprovalPolicy, useGetSecretApprovalPolicies, useGetWorkspaceUsers } from "@app/hooks/api"; +import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/queries"; +import { PolicyType } from "@app/hooks/api/policies/enums"; +import { TAccessApprovalPolicy, Workspace } from "@app/hooks/api/types"; + +import { AccessPolicyForm } from "./components/AccessPolicyModal"; +import { ApprovalPolicyRow } from "./components/ApprovalPolicyRow"; + +interface IProps { + workspaceId: string; +} + +const useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?: Workspace) => { + const { data: accessPolicies, isLoading: isAccessPoliciesLoading } = useGetAccessApprovalPolicies({ + projectSlug: currentWorkspace?.slug as string, + options: { + enabled: + permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) && + !!currentWorkspace?.slug + } + }); + const { data: secretPolicies, isLoading: isSecretPoliciesLoading } = useGetSecretApprovalPolicies({ + workspaceId: currentWorkspace?.id as string, + options: { + enabled: + permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) && + !!currentWorkspace?.id + } + }); + + // merge data sorted by updatedAt + const policies = [ + ...(accessPolicies?.map(policy => ({ ...policy, policyType: PolicyType.AccessPolicy })) || []), + ...(secretPolicies?.map(policy => ({ ...policy, policyType: PolicyType.ChangePolicy })) || []) + ].sort((a, b) => { + return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); + }); + + return { + policies, + isLoading: isAccessPoliciesLoading || isSecretPoliciesLoading + }; +}; + +export const ApprovalPolicyList = ({ workspaceId }: IProps) => { + const { handlePopUpToggle, handlePopUpOpen, handlePopUpClose, popUp } = usePopUp([ + "policyForm", + "deletePolicy", + "upgradePlan" + ] as const); + const { permission } = useProjectPermission(); + const { subscription } = useSubscription(); + const { currentWorkspace } = useWorkspace(); + + const { data: members } = useGetWorkspaceUsers(workspaceId); + const { policies, isLoading: isPoliciesLoading } = useApprovalPolicies(permission, currentWorkspace); + + const [filterType, setFilterType] = useState(null); + + const filteredPolicies = useMemo(() => { + return filterType + ? policies.filter(policy => policy.policyType === filterType) + : policies; + }, [policies, filterType]); + + const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy(); + const { mutateAsync: deleteAccessApprovalPolicy } = useDeleteAccessApprovalPolicy(); + + const handleDeletePolicy = async () => { + const { id, policyType } = popUp.deletePolicy.data as TAccessApprovalPolicy; + if (!currentWorkspace?.slug) return; + + try { + if (policyType === PolicyType.ChangePolicy) { + await deleteSecretApprovalPolicy({ + workspaceId, + id + }); + } else { + await deleteAccessApprovalPolicy({ + projectSlug: currentWorkspace?.slug, + id + }); + } + createNotification({ + type: "success", + text: "Successfully deleted policy" + }); + handlePopUpClose("deletePolicy"); + } catch (err) { + console.log(err); + createNotification({ + type: "error", + text: "Failed to delete policy" + }); + } + }; + + return ( +
+
+
+ Policies +
+ Implement granular policies for access requests and secrets management. +
+
+
+ + {(isAllowed) => ( + + )} + +
+
+ + + + + + + + + + + + + + {isPoliciesLoading && ( + + )} + {!isPoliciesLoading && !filteredPolicies?.length && ( + + + + )} + {!!currentWorkspace && + filteredPolicies?.map((policy) => ( + handlePopUpOpen("policyForm", policy)} + onDelete={() => handlePopUpOpen("deletePolicy", policy)} + /> + ))} + +
NameEnvironmentSecret PathEligible ApproversApproval Required + + + + + + Select a type + setFilterType(null)} + icon={!filterType && } + iconPos="right" + > + All + + setFilterType(PolicyType.AccessPolicy)} + icon={filterType === PolicyType.AccessPolicy && } + iconPos="right" + > + Access Policy + + setFilterType(PolicyType.ChangePolicy)} + icon={filterType === PolicyType.ChangePolicy && } + iconPos="right" + > + Change Policy + + + + +
+ +
+
+ handlePopUpToggle("policyForm", isOpen)} + members={members} + editValues={popUp.policyForm.data as TAccessApprovalPolicy} + /> + handlePopUpToggle("deletePolicy", isOpen)} + onDeleteApproved={handleDeletePolicy} + /> + handlePopUpToggle("upgradePlan", isOpen)} + text="You can add secret approval policy if you switch to Infisical's Enterprise plan." + /> +
+ ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx new file mode 100644 index 000000000..9ebd36661 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx @@ -0,0 +1,376 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { faCheckCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Alert, + AlertDescription, + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { policyDetails } from "@app/helpers/policies"; +import { useCreateSecretApprovalPolicy, useUpdateSecretApprovalPolicy } from "@app/hooks/api"; +import { + useCreateAccessApprovalPolicy, + useUpdateAccessApprovalPolicy +} from "@app/hooks/api/accessApproval"; +import { TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types"; +import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums"; +import { TWorkspaceUser } from "@app/hooks/api/users/types"; + +type Props = { + isOpen?: boolean; + onToggle: (isOpen: boolean) => void; + members?: TWorkspaceUser[]; + projectSlug: string; + editValues?: TAccessApprovalPolicy; +}; + +const formSchema = z +.object({ + environment: z.string(), + name: z.string().optional(), + secretPath: z.string().optional(), + approvals: z.number().min(1), + approvers: z.string().array().min(1), + policyType: z.nativeEnum(PolicyType), + enforcementLevel: z.nativeEnum(EnforcementLevel) +}) +.refine((data) => data.approvals <= data.approvers.length, { + path: ["approvals"], + message: "The number of approvals should be lower than the number of approvers." +}); + +type TFormSchema = z.infer; + +export const AccessPolicyForm = ({ + isOpen, + onToggle, + members = [], + projectSlug, + editValues +}: Props) => { + const { + control, + handleSubmit, + reset, + watch, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema), + values: editValues ? { + ...editValues, + environment: editValues.environment.slug, + approvers: editValues?.userApprovers?.map((user) => user.userId) || editValues?.approvers + } : undefined + }); + const { currentWorkspace } = useWorkspace(); + + const environments = currentWorkspace?.environments || []; + const isEditMode = Boolean(editValues); + + useEffect(() => { + if (!isOpen || !isEditMode) reset({}); + }, [isOpen, isEditMode]); + + const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy(); + const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy(); + + const { mutateAsync: createSecretApprovalPolicy } = useCreateSecretApprovalPolicy(); + const { mutateAsync: updateSecretApprovalPolicy } = useUpdateSecretApprovalPolicy(); + + const enforcementLevel = watch("enforcementLevel"); + const policyName = policyDetails[watch("policyType")]?.name || "Policy"; + + const handleCreatePolicy = async (data: TFormSchema) => { + if (!projectSlug) return; + + try { + if (data.policyType === PolicyType.ChangePolicy) { + await createSecretApprovalPolicy({ + ...data, + workspaceId: currentWorkspace?.id || "" + }); + } else { + await createAccessApprovalPolicy({ + ...data, + projectSlug + }); + } + createNotification({ + type: "success", + text: "Successfully created policy" + }); + onToggle(false); + } catch (err) { + console.log(err); + createNotification({ + type: "error", + text: "Failed to create policy" + }); + } + }; + + const handleUpdatePolicy = async (data: TFormSchema) => { + if (!projectSlug) return; + if (!editValues?.id) return; + + try { + if (data.policyType === PolicyType.ChangePolicy) { + await updateSecretApprovalPolicy({ + id: editValues?.id, + ...data, + workspaceId: currentWorkspace?.id || "" + }); + } else { + await updateAccessApprovalPolicy({ + id: editValues?.id, + ...data, + projectSlug + }); + createNotification({ + type: "success", + text: "Successfully updated policy" + }); + onToggle(false); + } + } catch (err) { + console.log(err); + createNotification({ + type: "error", + text: "failed to update policy" + }); + } + }; + + const handleFormSubmit = async (data: TFormSchema) => { + if (isEditMode) { + await handleUpdatePolicy(data); + } else { + await handleCreatePolicy(data); + } + }; + + const formatEnforcementLevel = (level: EnforcementLevel) => { + if (level === EnforcementLevel.Hard) return "Hard"; + if (level === EnforcementLevel.Soft) return "Soft"; + return level; + }; + + return ( + + +
+
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + + + + + Select members that are allowed to approve requests + + {members.map(({ id, user }) => { + const userId = watch("policyType") === PolicyType.ChangePolicy ? user.id : id; + const isChecked = value?.includes(userId); + return ( + { + evt.preventDefault(); + onChange( + isChecked ? value?.filter((el: string) => el !== userId) : [...(value || []), userId] + ); + }} + key={`create-policy-members-${userId}`} + iconPos="right" + icon={isChecked && } + > + {user.username} + + ); + })} + + + + )} + /> + ( + + field.onChange(parseInt(el.target.value, 10))} + /> + + )} + /> + ( + + + + )} + /> + {enforcementLevel === EnforcementLevel.Soft && ( + + + Soft enforcement allows requesters to bypass approval, which may reduce system security and stability. + + + )} +
+ + +
+ +
+
+
+ ); +}; + diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx new file mode 100644 index 000000000..e251f88ca --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx @@ -0,0 +1,191 @@ +import { useState } from "react"; +import { faCheckCircle, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + IconButton, + Input, + Td, + Tr +} from "@app/components/v2"; +import { Badge } from "@app/components/v2/Badge"; +import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; +import { policyDetails } from "@app/helpers/policies"; +import { useUpdateAccessApprovalPolicy, useUpdateSecretApprovalPolicy } from "@app/hooks/api"; +import { PolicyType } from "@app/hooks/api/policies/enums"; +import { WorkspaceEnv } from "@app/hooks/api/types"; +import { TWorkspaceUser } from "@app/hooks/api/users/types"; + +interface IPolicy { + id: string; + name: string; + environment: WorkspaceEnv; + projectId?: string; + secretPath?: string; + approvals: number; + approvers?: string[]; + userApprovers?: { userId: string }[]; + updatedAt: Date; + policyType: PolicyType; +}; + +type Props = { + policy: IPolicy; + members?: TWorkspaceUser[]; + projectSlug: string; + workspaceId: string; + onEdit: () => void; + onDelete: () => void; +}; + +export const ApprovalPolicyRow = ({ + policy, + members = [], + projectSlug, + workspaceId, + onEdit, + onDelete +}: Props) => { + const [selectedApprovers, setSelectedApprovers] = useState(policy.userApprovers?.map(({ userId }) => userId) || policy.approvers || []); + const { mutate: updateAccessApprovalPolicy, isLoading: isAccessApprovalPolicyLoading } = useUpdateAccessApprovalPolicy(); + const { mutate: updateSecretApprovalPolicy, isLoading: isSecretApprovalPolicyLoading } = useUpdateSecretApprovalPolicy(); + const isLoading = isAccessApprovalPolicyLoading || isSecretApprovalPolicyLoading; + + const { permission } = useProjectPermission(); + + return ( + + {policy.name} + {policy.environment.slug} + {policy.secretPath || "*"} + + { + if (!isOpen) { + if (policy.policyType === PolicyType.AccessPolicy) { + updateAccessApprovalPolicy( + { + projectSlug, + id: policy.id, + approvers: selectedApprovers + }, + { + onSettled: () => { + // No changes needed here + } + } + ); + } else { + updateSecretApprovalPolicy( + { + workspaceId, + id: policy.id, + approvers: selectedApprovers + }, + { + onSettled: () => { + // No changes needed here + } + } + ); + } + } else { + setSelectedApprovers(policy.policyType === PolicyType.ChangePolicy + ? policy?.userApprovers?.map(({ userId }) => userId) || [] + : policy?.approvers || [] + ); + } + }} + > + + + + + + Select members that are allowed to approve changes + + {members?.map(({ id, user }) => { + const userId = policy.policyType === PolicyType.ChangePolicy ? user.id : id; + const isChecked = selectedApprovers.includes(userId); + return ( + { + evt.preventDefault(); + setSelectedApprovers((state) => + isChecked ? state.filter((el) => el !== userId) : [...state, userId] + ); + }} + key={`create-policy-members-${userId}`} + iconPos="right" + icon={isChecked && } + > + {user.username} + + ); + })} + + + + {policy.approvals} + + + {policyDetails[policy.policyType].name} + + + +
+ + {(isAllowed) => ( + + + + )} + + + {(isAllowed) => ( + + + + )} + +
+ + + ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/index.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/index.tsx new file mode 100644 index 000000000..2817ec627 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/index.tsx @@ -0,0 +1 @@ +export { ApprovalPolicyList } from "./ApprovalPolicyList"; From 7b3e116bf8f14bd2996b9881977a47025723c68f Mon Sep 17 00:00:00 2001 From: Alfonso Hernandez Date: Wed, 17 Jul 2024 20:26:27 +0200 Subject: [PATCH 04/21] feat(frontend): remove AccessApprovalPolicyList --- .../SecretApprovalPage/SecretApprovalPage.tsx | 6 - .../AccessApprovalPolicyList.tsx | 174 ------------ .../components/AccessApprovalPolicyRow.tsx | 146 ---------- .../components/AccessPolicyModal.tsx | 266 ------------------ .../AccessApprovalPolicyList/index.tsx | 1 - 5 files changed, 593 deletions(-) delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/index.tsx diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index f8d056cce..df0ba55f9 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -8,7 +8,6 @@ import { Divider } from "@app/components/v2/Divider"; import { useWorkspace } from "@app/context"; import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api"; -import { AccessApprovalPolicyList } from "./components/AccessApprovalPolicyList"; import { AccessApprovalRequest } from "./components/AccessApprovalRequest"; import { ApprovalPolicyList } from "./components/ApprovalPolicyList"; import { SecretApprovalPolicyList } from "./components/SecretApprovalPolicyList"; @@ -66,8 +65,6 @@ export const SecretApprovalPage = () => { Access Requests {Boolean(accessApprovalRequestCount?.pendingCount) && {accessApprovalRequestCount?.pendingCount}} - Access Request Policies - Policies @@ -79,9 +76,6 @@ export const SecretApprovalPage = () => { - - - diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx deleted file mode 100644 index aa47cce80..000000000 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { faFileShield, faPlus } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { createNotification } from "@app/components/notifications"; -import { ProjectPermissionCan } from "@app/components/permissions"; -import { - Button, - DeleteActionModal, - EmptyState, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - Th, - THead, - Tr, - UpgradePlanModal -} from "@app/components/v2"; -import { - ProjectPermissionActions, - ProjectPermissionSub, - useProjectPermission, - useSubscription, - useWorkspace -} from "@app/context"; -import { usePopUp } from "@app/hooks"; -import { useDeleteAccessApprovalPolicy, useGetWorkspaceUsers } from "@app/hooks/api"; -import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/queries"; -import { TAccessApprovalPolicy } from "@app/hooks/api/types"; - -import { AccessApprovalPolicyRow } from "./components/AccessApprovalPolicyRow"; -import { AccessPolicyForm } from "./components/AccessPolicyModal"; - -interface IProps { - workspaceId: string; -} - -export const AccessApprovalPolicyList = ({ workspaceId }: IProps) => { - const { handlePopUpToggle, handlePopUpOpen, handlePopUpClose, popUp } = usePopUp([ - "secretPolicyForm", - "deletePolicy", - "upgradePlan" - ] as const); - const { permission } = useProjectPermission(); - const { subscription } = useSubscription(); - const { currentWorkspace } = useWorkspace(); - - const { data: members } = useGetWorkspaceUsers(workspaceId); - const { data: policies, isLoading: isPoliciesLoading } = useGetAccessApprovalPolicies({ - projectSlug: currentWorkspace?.slug as string, - options: { - enabled: - permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) && - !!currentWorkspace?.slug - } - }); - - const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteAccessApprovalPolicy(); - - const handleDeletePolicy = async () => { - const { id } = popUp.deletePolicy.data as TAccessApprovalPolicy; - if (!currentWorkspace?.slug) return; - - try { - await deleteSecretApprovalPolicy({ - projectSlug: currentWorkspace?.slug, - id - }); - createNotification({ - type: "success", - text: "Successfully deleted policy" - }); - handlePopUpClose("deletePolicy"); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to delete policy" - }); - } - }; - - return ( -
-
-
- Access Request Policies -
- Implement secret request policies for specific secrets and environments. -
-
-
- - {(isAllowed) => ( - - )} - -
-
- - - - - - - - - - - - - {isPoliciesLoading && ( - - )} - {!isPoliciesLoading && !policies?.length && ( - - - - )} - {!!currentWorkspace && - policies?.map((policy) => ( - handlePopUpOpen("secretPolicyForm", policy)} - onDelete={() => handlePopUpOpen("deletePolicy", policy)} - /> - ))} - -
NameEnvironmentSecret PathEligible ApproversApproval Required -
- -
-
- handlePopUpToggle("secretPolicyForm", isOpen)} - members={members} - editValues={popUp.secretPolicyForm.data as TAccessApprovalPolicy} - /> - handlePopUpToggle("deletePolicy", isOpen)} - onDeleteApproved={handleDeletePolicy} - /> - handlePopUpToggle("upgradePlan", isOpen)} - text="You can add secret approval policy if you switch to Infisical's Enterprise plan." - /> -
- ); -}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx deleted file mode 100644 index 8476bac8d..000000000 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { useState } from "react"; -import { faCheckCircle, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { ProjectPermissionCan } from "@app/components/permissions"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, - IconButton, - Input, - Td, - Tr -} from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; -import { useUpdateAccessApprovalPolicy } from "@app/hooks/api"; -import { TAccessApprovalPolicy } from "@app/hooks/api/types"; -import { TWorkspaceUser } from "@app/hooks/api/users/types"; - -type Props = { - policy: TAccessApprovalPolicy; - members?: TWorkspaceUser[]; - projectSlug: string; - onEdit: () => void; - onDelete: () => void; -}; - -export const AccessApprovalPolicyRow = ({ - policy, - members = [], - projectSlug, - onEdit, - onDelete -}: Props) => { - const [selectedApprovers, setSelectedApprovers] = useState([]); - const { mutate: updateAccessApprovalPolicy, isLoading } = useUpdateAccessApprovalPolicy(); - const { permission } = useProjectPermission(); - - return ( - - {policy.name} - {policy.environment.slug} - {policy.secretPath || "*"} - - { - if (!isOpen) { - updateAccessApprovalPolicy( - { - projectSlug, - id: policy.id, - approvers: selectedApprovers - }, - { - onSettled: () => { - setSelectedApprovers([]); - } - } - ); - } else { - setSelectedApprovers(policy.approvers); - } - }} - > - - - - - - Select members that are allowed to approve changes - - {members?.map(({ id, user }) => { - const isChecked = selectedApprovers.includes(id); - return ( - { - evt.preventDefault(); - setSelectedApprovers((state) => - isChecked ? state.filter((el) => el !== id) : [...state, id] - ); - }} - key={`create-policy-members-${id}`} - iconPos="right" - icon={isChecked && } - > - {user.username} - - ); - })} - - - - {policy.approvals} - -
- - {(isAllowed) => ( - - - - )} - - - {(isAllowed) => ( - - - - )} - -
- - - ); -}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx deleted file mode 100644 index 6c0ee3fb6..000000000 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx +++ /dev/null @@ -1,266 +0,0 @@ -import { useEffect } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { faCheckCircle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; - -import { createNotification } from "@app/components/notifications"; -import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, - FormControl, - Input, - Modal, - ModalContent, - Select, - SelectItem -} from "@app/components/v2"; -import { useWorkspace } from "@app/context"; -import { - useCreateAccessApprovalPolicy, - useUpdateAccessApprovalPolicy -} from "@app/hooks/api/accessApproval"; -import { TAccessApprovalPolicy } from "@app/hooks/api/accessApproval/types"; -import { TWorkspaceUser } from "@app/hooks/api/users/types"; - -type Props = { - isOpen?: boolean; - onToggle: (isOpen: boolean) => void; - members?: TWorkspaceUser[]; - projectSlug: string; - editValues?: TAccessApprovalPolicy; -}; - -const formSchema = z - .object({ - environment: z.string(), - name: z.string().optional(), - secretPath: z.string().optional(), - approvals: z.number().min(1), - approvers: z.string().array().min(1) - }) - .refine((data) => data.approvals <= data.approvers.length, { - path: ["approvals"], - message: "The number of approvals should be lower than the number of approvers." - }); - -type TFormSchema = z.infer; - -export const AccessPolicyForm = ({ - isOpen, - onToggle, - members = [], - projectSlug, - editValues -}: Props) => { - const { - control, - handleSubmit, - reset, - formState: { isSubmitting } - } = useForm({ - resolver: zodResolver(formSchema), - values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined - }); - const { currentWorkspace } = useWorkspace(); - - const environments = currentWorkspace?.environments || []; - useEffect(() => { - if (!isOpen) reset({}); - }, [isOpen]); - - const isEditMode = Boolean(editValues); - - const { mutateAsync: createAccessApprovalPolicy } = useCreateAccessApprovalPolicy(); - const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy(); - - const handleCreatePolicy = async (data: TFormSchema) => { - if (!projectSlug) return; - - try { - await createAccessApprovalPolicy({ - ...data, - projectSlug - }); - createNotification({ - type: "success", - text: "Successfully created policy" - }); - onToggle(false); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to create policy" - }); - } - }; - - const handleUpdatePolicy = async (data: TFormSchema) => { - if (!projectSlug) return; - if (!editValues?.id) return; - - try { - await updateAccessApprovalPolicy({ - id: editValues?.id, - ...data, - projectSlug - }); - createNotification({ - type: "success", - text: "Successfully updated policy" - }); - onToggle(false); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "failed to update policy" - }); - } - }; - - const handleFormSubmit = async (data: TFormSchema) => { - if (isEditMode) { - await handleUpdatePolicy(data); - } else { - await handleCreatePolicy(data); - } - }; - - return ( - - -
- ( - - - - )} - /> - ( - - - - )} - /> - - ( - - - - )} - /> - - ( - - - - - - - - Select members that are allowed to approve changes - - {members.map(({ id, user }) => { - const isChecked = value?.includes(id); - return ( - { - evt.preventDefault(); - onChange( - isChecked ? value?.filter((el) => el !== id) : [...(value || []), id] - ); - }} - key={`create-policy-members-${id}`} - iconPos="right" - icon={isChecked && } - > - {user.username} - - ); - })} - - - - )} - /> - ( - - field.onChange(parseInt(el.target.value, 10))} - /> - - )} - /> -
- - -
- -
-
- ); -}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/index.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/index.tsx deleted file mode 100644 index f6db07c94..000000000 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { AccessApprovalPolicyList } from "./AccessApprovalPolicyList"; From 869fcd6541698cd965a52858b691f8bc18830bc9 Mon Sep 17 00:00:00 2001 From: Alfonso Hernandez Date: Wed, 17 Jul 2024 21:30:55 +0200 Subject: [PATCH 05/21] feat(frontend): remove SecretApprovalPolicyList --- .../SecretApprovalPage/SecretApprovalPage.tsx | 7 - .../SecretApprovalPolicyList.tsx | 178 ------------ .../components/SecretApprovalPolicyRow.tsx | 148 ---------- .../components/SecretPolicyForm.tsx | 262 ------------------ .../SecretApprovalPolicyList/index.tsx | 1 - 5 files changed, 596 deletions(-) delete mode 100644 frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx delete mode 100644 frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx delete mode 100644 frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx delete mode 100644 frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/index.tsx diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index df0ba55f9..8d7282130 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -4,13 +4,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { Badge } from "@app/components/v2/Badge"; -import { Divider } from "@app/components/v2/Divider"; import { useWorkspace } from "@app/context"; import { useGetAccessRequestsCount, useGetSecretApprovalRequestCount } from "@app/hooks/api"; import { AccessApprovalRequest } from "./components/AccessApprovalRequest"; import { ApprovalPolicyList } from "./components/ApprovalPolicyList"; -import { SecretApprovalPolicyList } from "./components/SecretApprovalPolicyList"; import { SecretApprovalRequest } from "./components/SecretApprovalRequest"; enum TabSection { @@ -59,17 +57,12 @@ export const SecretApprovalPage = () => { Secret Requests {Boolean(secretApprovalReqCount?.open) && ({secretApprovalReqCount?.open})} - Secret Policies - Access Requests {Boolean(accessApprovalRequestCount?.pendingCount) && {accessApprovalRequestCount?.pendingCount}} Policies - - - diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx deleted file mode 100644 index f377f44e9..000000000 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { faFileShield, faPlus } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { createNotification } from "@app/components/notifications"; -import { ProjectPermissionCan } from "@app/components/permissions"; -import { - Button, - DeleteActionModal, - EmptyState, - Modal, - ModalContent, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - Th, - THead, - Tr, - UpgradePlanModal -} from "@app/components/v2"; -import { - ProjectPermissionActions, - ProjectPermissionSub, - useProjectPermission, - useSubscription -} from "@app/context"; -import { usePopUp } from "@app/hooks"; -import { - useDeleteSecretApprovalPolicy, - useGetSecretApprovalPolicies, - useGetWorkspaceUsers -} from "@app/hooks/api"; -import { TSecretApprovalPolicy } from "@app/hooks/api/types"; - -import { SecretApprovalPolicyRow } from "./components/SecretApprovalPolicyRow"; -import { SecretPolicyForm } from "./components/SecretPolicyForm"; - -type Props = { - workspaceId: string; -}; - -export const SecretApprovalPolicyList = ({ workspaceId }: Props) => { - const { handlePopUpToggle, handlePopUpOpen, handlePopUpClose, popUp } = usePopUp([ - "secretPolicyForm", - "deletePolicy", - "upgradePlan" - ] as const); - const { permission } = useProjectPermission(); - const { subscription } = useSubscription(); - - const { data: members } = useGetWorkspaceUsers(workspaceId); - const { data: policies, isLoading: isPoliciesLoading } = useGetSecretApprovalPolicies({ - workspaceId, - options: { - enabled: permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) - } - }); - - const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy(); - - const handleDeletePolicy = async () => { - const { id } = popUp.deletePolicy.data as TSecretApprovalPolicy; - try { - await deleteSecretApprovalPolicy({ - workspaceId, - id - }); - createNotification({ - type: "success", - text: "Successfully deleted policy" - }); - handlePopUpClose("deletePolicy"); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to delete policy" - }); - } - }; - - return ( -
-
-
- Approval Policies -
- Implement policies to prevent unauthorized secret changes. -
-
-
- - {(isAllowed) => ( - - )} - -
-
- - - - - - - - - - - - - {isPoliciesLoading && ( - - )} - {!isPoliciesLoading && !policies?.length && ( - - - - )} - {policies?.map((policy) => ( - handlePopUpOpen("secretPolicyForm", policy)} - onDelete={() => handlePopUpOpen("deletePolicy", policy)} - /> - ))} - -
NameEnvironmentSecret PathEligible ApproversApproval Required
- -
-
- handlePopUpToggle("secretPolicyForm", isOpen)} - > - - handlePopUpToggle("secretPolicyForm", isOpen)} - members={members} - editValues={popUp.secretPolicyForm.data as TSecretApprovalPolicy} - /> - - - handlePopUpToggle("deletePolicy", isOpen)} - onDeleteApproved={handleDeletePolicy} - /> - handlePopUpToggle("upgradePlan", isOpen)} - text="You can add secret approval policy if you switch to Infisical's Enterprise plan." - /> -
- ); -}; diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx deleted file mode 100644 index 9f8a08c44..000000000 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { useState } from "react"; -import { faCheckCircle, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { ProjectPermissionCan } from "@app/components/permissions"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, - IconButton, - Input, - Td, - Tr -} from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; -import { useUpdateSecretApprovalPolicy } from "@app/hooks/api"; -import { TSecretApprovalPolicy } from "@app/hooks/api/types"; -import { TWorkspaceUser } from "@app/hooks/api/users/types"; - -type Props = { - policy: TSecretApprovalPolicy; - members?: TWorkspaceUser[]; - workspaceId: string; - onEdit: () => void; - onDelete: () => void; -}; - -export const SecretApprovalPolicyRow = ({ - policy, - members = [], - workspaceId, - onEdit, - onDelete -}: Props) => { - const [selectedApprovers, setSelectedApprovers] = useState([]); - const { mutate: updateSecretApprovalPolicy, isLoading } = useUpdateSecretApprovalPolicy(); - const { permission } = useProjectPermission(); - - return ( - - {policy.name} - {policy.environment.slug} - {policy.secretPath || "*"} - - { - if (!isOpen) { - updateSecretApprovalPolicy( - { - workspaceId, - id: policy.id, - approvers: selectedApprovers - }, - { - onSettled: () => { - setSelectedApprovers([]); - } - } - ); - } else { - setSelectedApprovers(policy.userApprovers.map(({ userId }) => userId)); - } - }} - > - - - - - - Select members that are allowed to approve changes - - {members?.map(({ user }) => { - const isChecked = selectedApprovers.includes(user.id); - return ( - { - evt.preventDefault(); - setSelectedApprovers((state) => - isChecked ? state.filter((el) => el !== user.id) : [...state, user.id] - ); - }} - key={`create-policy-members-${user.id}`} - iconPos="right" - icon={isChecked && } - > - {user.username} - - ); - })} - - - - {policy.approvals} - -
- - {(isAllowed) => ( - - - - )} - - - {(isAllowed) => ( - - - - )} - -
- - - ); -}; diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx deleted file mode 100644 index 18b0b87c8..000000000 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import { Controller, useForm } from "react-hook-form"; -import { faCheckCircle } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; - -import { createNotification } from "@app/components/notifications"; -import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuTrigger, - FormControl, - Input, - Select, - SelectItem -} from "@app/components/v2"; -import { SecretPathInput } from "@app/components/v2/SecretPathInput"; -import { useWorkspace } from "@app/context"; -import { useCreateSecretApprovalPolicy, useUpdateSecretApprovalPolicy } from "@app/hooks/api"; -import { TSecretApprovalPolicy } from "@app/hooks/api/types"; -import { TWorkspaceUser } from "@app/hooks/api/users/types"; - -type Props = { - isOpen?: boolean; - onToggle: (isOpen: boolean) => void; - members?: TWorkspaceUser[]; - workspaceId: string; - editValues?: TSecretApprovalPolicy; -}; - -const formSchema = z - .object({ - environment: z.string(), - name: z.string().optional(), - secretPath: z.string().optional().nullable(), - approvals: z.number().min(1), - approvers: z.string().array().min(1) - }) - .refine((data) => data.approvals <= data.approvers.length, { - path: ["approvals"], - message: "The number of approvals should be lower than the number of approvers." - }); - -type TFormSchema = z.infer; - -export const SecretPolicyForm = ({ - onToggle, - members = [], - workspaceId, - editValues -}: Props) => { - const { - control, - handleSubmit, - watch, - formState: { isSubmitting } - } = useForm({ - resolver: zodResolver(formSchema), - values: editValues - ? { - ...editValues, - approvers: editValues.userApprovers.map(({ userId }) => userId), - environment: editValues.environment.slug - } - : undefined - }); - const { currentWorkspace } = useWorkspace(); - const selectedEnvironment = watch("environment"); - - const environments = currentWorkspace?.environments || []; - - const isEditMode = Boolean(editValues); - - const { mutateAsync: createSecretApprovalPolicy } = useCreateSecretApprovalPolicy(); - const { mutateAsync: updateSecretApprovalPolicy } = useUpdateSecretApprovalPolicy(); - - const handleCreatePolicy = async (data: TFormSchema) => { - try { - await createSecretApprovalPolicy({ - ...data, - workspaceId - }); - createNotification({ - type: "success", - text: "Successfully created policy" - }); - onToggle(false); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "Failed to create policy" - }); - } - }; - - const handleUpdatePolicy = async (data: TFormSchema) => { - if (!editValues?.id) return; - try { - await updateSecretApprovalPolicy({ - id: editValues?.id, - ...data, - secretPath: data.secretPath || null, - workspaceId - }); - createNotification({ - type: "success", - text: "Successfully updated policy" - }); - onToggle(false); - } catch (err) { - console.log(err); - createNotification({ - type: "error", - text: "failed to update policy" - }); - } - }; - - const handleFormSubmit = async (data: TFormSchema) => { - if (isEditMode) { - await handleUpdatePolicy(data); - } else { - await handleCreatePolicy(data); - } - }; - - return ( -
- ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - )} - /> - ( - - - - - - - - Select members that are allowed to approve changes - - {members.map(({ user }) => { - const isChecked = value?.includes(user.id); - return ( - { - evt.preventDefault(); - onChange( - isChecked - ? value?.filter((el) => el !== user.id) - : [...(value || []), user.id] - ); - }} - key={`create-policy-members-${user.id}`} - iconPos="right" - icon={isChecked && } - > - {user.username} - - ); - })} - - - - )} - /> - ( - - field.onChange(parseInt(el.target.value, 10))} - /> - - )} - /> -
- - -
- - - ); -}; diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/index.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/index.tsx deleted file mode 100644 index f204264b4..000000000 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { SecretApprovalPolicyList } from "./SecretApprovalPolicyList"; From 2c57bd94fb00f27a316626b292834b4030e00751 Mon Sep 17 00:00:00 2001 From: Alfonso Hernandez Date: Wed, 17 Jul 2024 21:39:33 +0200 Subject: [PATCH 06/21] feat(backend): add enforcementLevel into secret_approval_policies --- ...4929_add-enforcement-level-secrets-policies.ts | 15 +++++++++++++++ .../src/db/schemas/secret-approval-policies.ts | 3 +++ .../ee/routes/v1/secret-approval-policy-router.ts | 10 +++++++--- .../secret-approval-policy-service.ts | 12 ++++++++---- .../secret-approval-policy-types.ts | 4 +++- backend/src/lib/types/index.ts | 5 +++++ 6 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 backend/src/db/migrations/20240717184929_add-enforcement-level-secrets-policies.ts diff --git a/backend/src/db/migrations/20240717184929_add-enforcement-level-secrets-policies.ts b/backend/src/db/migrations/20240717184929_add-enforcement-level-secrets-policies.ts new file mode 100644 index 000000000..4a1df888a --- /dev/null +++ b/backend/src/db/migrations/20240717184929_add-enforcement-level-secrets-policies.ts @@ -0,0 +1,15 @@ +import { Knex } from "knex"; + +import { EnforcementLevel } from "@app/lib/types"; + +export async function up(knex: Knex): Promise { + await knex.schema.table("secret_approval_policies", (table) => { + table.specificType("enforcementLevel", "VARCHAR(10)").notNullable().defaultTo(EnforcementLevel.Hard); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.table("secret_approval_policies", (table) => { + table.dropColumn("enforcementLevel"); + }); +} diff --git a/backend/src/db/schemas/secret-approval-policies.ts b/backend/src/db/schemas/secret-approval-policies.ts index d907ef1e0..bac9c4b2e 100644 --- a/backend/src/db/schemas/secret-approval-policies.ts +++ b/backend/src/db/schemas/secret-approval-policies.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { EnforcementLevel } from "@app/lib/types"; + import { TImmutableDBKeys } from "./models"; export const SecretApprovalPoliciesSchema = z.object({ @@ -12,6 +14,7 @@ export const SecretApprovalPoliciesSchema = z.object({ name: z.string(), secretPath: z.string().nullable().optional(), approvals: z.number().default(1), + enforcementLevel: z.nativeEnum(EnforcementLevel), envId: z.string().uuid(), createdAt: z.date(), updatedAt: z.date() diff --git a/backend/src/ee/routes/v1/secret-approval-policy-router.ts b/backend/src/ee/routes/v1/secret-approval-policy-router.ts index f25ae42f3..305c588a6 100644 --- a/backend/src/ee/routes/v1/secret-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-policy-router.ts @@ -2,6 +2,7 @@ import { nanoid } from "nanoid"; import { z } from "zod"; import { removeTrailingSlash } from "@app/lib/fn"; +import { EnforcementLevel } from "@app/lib/types"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { sapPubSchema } from "@app/server/routes/sanitizedSchemas"; @@ -26,7 +27,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi .nullable() .transform((val) => (val ? removeTrailingSlash(val) : val)), approvers: z.string().array().min(1), - approvals: z.number().min(1).default(1) + approvals: z.number().min(1).default(1), + enforcementLevel: z.nativeEnum(EnforcementLevel) }) .refine((data) => data.approvals <= data.approvers.length, { path: ["approvals"], @@ -47,7 +49,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi actorOrgId: req.permission.orgId, projectId: req.body.workspaceId, ...req.body, - name: req.body.name ?? `${req.body.environment}-${nanoid(3)}` + name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`, + enforcementLevel: req.body.enforcementLevel ?? EnforcementLevel.Hard }); return { approval }; } @@ -72,7 +75,8 @@ export const registerSecretApprovalPolicyRouter = async (server: FastifyZodProvi .string() .optional() .nullable() - .transform((val) => (val ? removeTrailingSlash(val) : val)) + .transform((val) => (val ? removeTrailingSlash(val) : val)), + enforcementLevel: z.nativeEnum(EnforcementLevel) }) .refine((data) => data.approvals <= data.approvers.length, { path: ["approvals"], diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts index be6a334e2..366ad69ea 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-service.ts @@ -48,7 +48,8 @@ export const secretApprovalPolicyServiceFactory = ({ approvers, projectId, secretPath, - environment + environment, + enforcementLevel }: TCreateSapDTO) => { if (approvals > approvers.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); @@ -73,7 +74,8 @@ export const secretApprovalPolicyServiceFactory = ({ envId: env.id, approvals, secretPath, - name + name, + enforcementLevel }, tx ); @@ -98,7 +100,8 @@ export const secretApprovalPolicyServiceFactory = ({ actorOrgId, actorAuthMethod, approvals, - secretPolicyId + secretPolicyId, + enforcementLevel }: TUpdateSapDTO) => { const secretApprovalPolicy = await secretApprovalPolicyDAL.findById(secretPolicyId); if (!secretApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); @@ -118,7 +121,8 @@ export const secretApprovalPolicyServiceFactory = ({ { approvals, secretPath, - name + name, + enforcementLevel }, tx ); diff --git a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts index 2ddd9b51b..8e7099c98 100644 --- a/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts +++ b/backend/src/ee/services/secret-approval-policy/secret-approval-policy-types.ts @@ -1,4 +1,4 @@ -import { TProjectPermission } from "@app/lib/types"; +import { EnforcementLevel, TProjectPermission } from "@app/lib/types"; export type TCreateSapDTO = { approvals: number; @@ -7,6 +7,7 @@ export type TCreateSapDTO = { approvers: string[]; projectId: string; name: string; + enforcementLevel: EnforcementLevel; } & Omit; export type TUpdateSapDTO = { @@ -15,6 +16,7 @@ export type TUpdateSapDTO = { secretPath?: string | null; approvers: string[]; name?: string; + enforcementLevel?: EnforcementLevel; } & Omit; export type TDeleteSapDTO = { diff --git a/backend/src/lib/types/index.ts b/backend/src/lib/types/index.ts index 2c41f4d23..382762aaa 100644 --- a/backend/src/lib/types/index.ts +++ b/backend/src/lib/types/index.ts @@ -42,3 +42,8 @@ export type RequiredKeys = { }[keyof T]; export type PickRequired = Pick>; + +export enum EnforcementLevel { + Hard = "hard", + Soft = "soft" +} From f3f87cfd84460e3aea5309c55fa35b32f681728f Mon Sep 17 00:00:00 2001 From: Alfonso Hernandez Date: Wed, 17 Jul 2024 21:43:53 +0200 Subject: [PATCH 07/21] feat(frontend): add enforcementLevel into SecretPolicy --- .../src/hooks/api/secretApproval/mutation.tsx | 10 ++++++---- frontend/src/hooks/api/secretApproval/types.ts | 3 +++ .../components/AccessPolicyModal.tsx | 10 +++++----- .../components/ApprovalPolicyRow.tsx | 15 ++++----------- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/frontend/src/hooks/api/secretApproval/mutation.tsx b/frontend/src/hooks/api/secretApproval/mutation.tsx index e0a8df95d..2ad79932b 100644 --- a/frontend/src/hooks/api/secretApproval/mutation.tsx +++ b/frontend/src/hooks/api/secretApproval/mutation.tsx @@ -9,14 +9,15 @@ export const useCreateSecretApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TCreateSecretPolicyDTO>({ - mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name }) => { + mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name, enforcementLevel }) => { const { data } = await apiRequest.post("/api/v1/secret-approvals", { environment, workspaceId, approvals, approvers, secretPath, - name + name, + enforcementLevel }); return data; }, @@ -30,12 +31,13 @@ export const useUpdateSecretApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TUpdateSecretPolicyDTO>({ - mutationFn: async ({ id, approvers, approvals, secretPath, name }) => { + mutationFn: async ({ id, approvers, approvals, secretPath, name, enforcementLevel }) => { const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, { approvals, approvers, secretPath, - name + name, + enforcementLevel }); return data; }, diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts index bc0d57d40..2a64ccdc9 100644 --- a/frontend/src/hooks/api/secretApproval/types.ts +++ b/frontend/src/hooks/api/secretApproval/types.ts @@ -1,3 +1,4 @@ +import { EnforcementLevel } from "../policies/enums"; import { WorkspaceEnv } from "../workspace/types"; export type TSecretApprovalPolicy = { @@ -29,6 +30,7 @@ export type TCreateSecretPolicyDTO = { secretPath?: string | null; approvers?: string[]; approvals?: number; + enforcementLevel: EnforcementLevel; }; export type TUpdateSecretPolicyDTO = { @@ -37,6 +39,7 @@ export type TUpdateSecretPolicyDTO = { approvers?: string[]; secretPath?: string | null; approvals?: number; + enforcementLevel?: EnforcementLevel; // for invalidating list workspaceId: string; }; diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx index 9ebd36661..54d88ed08 100644 --- a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx +++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/AccessPolicyModal.tsx @@ -143,12 +143,12 @@ export const AccessPolicyForm = ({ ...data, projectSlug }); - createNotification({ - type: "success", - text: "Successfully updated policy" - }); - onToggle(false); } + createNotification({ + type: "success", + text: "Successfully updated policy" + }); + onToggle(false); } catch (err) { console.log(err); createNotification({ diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx index e251f88ca..3aa5ce486 100644 --- a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx +++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/components/ApprovalPolicyRow.tsx @@ -18,7 +18,7 @@ import { Badge } from "@app/components/v2/Badge"; import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; import { policyDetails } from "@app/helpers/policies"; import { useUpdateAccessApprovalPolicy, useUpdateSecretApprovalPolicy } from "@app/hooks/api"; -import { PolicyType } from "@app/hooks/api/policies/enums"; +import { EnforcementLevel, PolicyType } from "@app/hooks/api/policies/enums"; import { WorkspaceEnv } from "@app/hooks/api/types"; import { TWorkspaceUser } from "@app/hooks/api/users/types"; @@ -33,6 +33,7 @@ interface IPolicy { userApprovers?: { userId: string }[]; updatedAt: Date; policyType: PolicyType; + enforcementLevel: EnforcementLevel; }; type Props = { @@ -75,11 +76,7 @@ export const ApprovalPolicyRow = ({ id: policy.id, approvers: selectedApprovers }, - { - onSettled: () => { - // No changes needed here - } - } + { onSettled: () => {} } ); } else { updateSecretApprovalPolicy( @@ -88,11 +85,7 @@ export const ApprovalPolicyRow = ({ id: policy.id, approvers: selectedApprovers }, - { - onSettled: () => { - // No changes needed here - } - } + { onSettled: () => {} } ); } } else { From a538e37a62174cee0d22f69038ce0aad4e1d9716 Mon Sep 17 00:00:00 2001 From: Alfonso Hernandez Date: Wed, 17 Jul 2024 21:48:57 +0200 Subject: [PATCH 08/21] chore(backend): schema secret_approval_policies --- backend/src/db/schemas/secret-approval-policies.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/backend/src/db/schemas/secret-approval-policies.ts b/backend/src/db/schemas/secret-approval-policies.ts index bac9c4b2e..94aeba050 100644 --- a/backend/src/db/schemas/secret-approval-policies.ts +++ b/backend/src/db/schemas/secret-approval-policies.ts @@ -5,8 +5,6 @@ import { z } from "zod"; -import { EnforcementLevel } from "@app/lib/types"; - import { TImmutableDBKeys } from "./models"; export const SecretApprovalPoliciesSchema = z.object({ @@ -14,10 +12,10 @@ export const SecretApprovalPoliciesSchema = z.object({ name: z.string(), secretPath: z.string().nullable().optional(), approvals: z.number().default(1), - enforcementLevel: z.nativeEnum(EnforcementLevel), envId: z.string().uuid(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + enforcementLevel: z.string().default("hard") }); export type TSecretApprovalPolicies = z.infer; From d4dd684f320e4c3b2bd287ddd231244be461bf9f Mon Sep 17 00:00:00 2001 From: Alfonso Hernandez Date: Wed, 17 Jul 2024 22:30:55 +0200 Subject: [PATCH 09/21] feat(backend): add enforcementLevel into access_approval_policies --- ...94958_add-enforcement-level-access-policies.ts | 15 +++++++++++++++ .../src/db/schemas/access-approval-policies.ts | 5 ++++- .../ee/routes/v1/access-approval-policy-router.ts | 10 +++++++--- .../access-approval-policy-service.ts | 12 ++++++++---- .../access-approval-policy-types.ts | 4 +++- 5 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 backend/src/db/migrations/20240717194958_add-enforcement-level-access-policies.ts diff --git a/backend/src/db/migrations/20240717194958_add-enforcement-level-access-policies.ts b/backend/src/db/migrations/20240717194958_add-enforcement-level-access-policies.ts new file mode 100644 index 000000000..9aeb8d51e --- /dev/null +++ b/backend/src/db/migrations/20240717194958_add-enforcement-level-access-policies.ts @@ -0,0 +1,15 @@ +import { Knex } from "knex"; + +import { EnforcementLevel } from "@app/lib/types"; + +export async function up(knex: Knex): Promise { + await knex.schema.table("access_approval_policies", (table) => { + table.specificType("enforcementLevel", "VARCHAR(10)").notNullable().defaultTo(EnforcementLevel.Hard); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.table("access_approval_policies", (table) => { + table.dropColumn("enforcementLevel"); + }); +} diff --git a/backend/src/db/schemas/access-approval-policies.ts b/backend/src/db/schemas/access-approval-policies.ts index 69068d23b..c05f22b31 100644 --- a/backend/src/db/schemas/access-approval-policies.ts +++ b/backend/src/db/schemas/access-approval-policies.ts @@ -5,6 +5,8 @@ import { z } from "zod"; +import { EnforcementLevel } from "@app/lib/types"; + import { TImmutableDBKeys } from "./models"; export const AccessApprovalPoliciesSchema = z.object({ @@ -14,7 +16,8 @@ export const AccessApprovalPoliciesSchema = z.object({ secretPath: z.string().nullable().optional(), envId: z.string().uuid(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + enforcementLevel: z.nativeEnum(EnforcementLevel).default(EnforcementLevel.Hard) }); export type TAccessApprovalPolicies = z.infer; diff --git a/backend/src/ee/routes/v1/access-approval-policy-router.ts b/backend/src/ee/routes/v1/access-approval-policy-router.ts index 3b8949d3b..fe3a16f2b 100644 --- a/backend/src/ee/routes/v1/access-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/access-approval-policy-router.ts @@ -1,6 +1,7 @@ import { nanoid } from "nanoid"; import { z } from "zod"; +import { EnforcementLevel } from "@app/lib/types"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { sapPubSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -17,7 +18,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi secretPath: z.string().trim().default("/"), environment: z.string(), approvers: z.string().array().min(1), - approvals: z.number().min(1).default(1) + approvals: z.number().min(1).default(1), + enforcementLevel: z.nativeEnum(EnforcementLevel) }) .refine((data) => data.approvals <= data.approvers.length, { path: ["approvals"], @@ -38,7 +40,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi actorOrgId: req.permission.orgId, ...req.body, projectSlug: req.body.projectSlug, - name: req.body.name ?? `${req.body.environment}-${nanoid(3)}` + name: req.body.name ?? `${req.body.environment}-${nanoid(3)}`, + enforcementLevel: req.body.enforcementLevel }); return { approval }; } @@ -115,7 +118,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi .optional() .transform((val) => (val === "" ? "/" : val)), approvers: z.string().array().min(1), - approvals: z.number().min(1).default(1) + approvals: z.number().min(1).default(1), + enforcementLevel: z.nativeEnum(EnforcementLevel) }) .refine((data) => data.approvals <= data.approvers.length, { path: ["approvals"], diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts index 51a51abb5..c91fbf970 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -47,7 +47,8 @@ export const accessApprovalPolicyServiceFactory = ({ approvals, approvers, projectSlug, - environment + environment, + enforcementLevel }: TCreateAccessApprovalPolicy) => { const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new BadRequestError({ message: "Project not found" }); @@ -94,7 +95,8 @@ export const accessApprovalPolicyServiceFactory = ({ envId: env.id, approvals, secretPath, - name + name, + enforcementLevel }, tx ); @@ -143,7 +145,8 @@ export const accessApprovalPolicyServiceFactory = ({ actor, actorOrgId, actorAuthMethod, - approvals + approvals, + enforcementLevel }: TUpdateAccessApprovalPolicy) => { const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId); if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); @@ -163,7 +166,8 @@ export const accessApprovalPolicyServiceFactory = ({ { approvals, secretPath, - name + name, + enforcementLevel }, tx ); diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts index 601561b68..fdb6fc8bb 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts @@ -1,4 +1,4 @@ -import { TProjectPermission } from "@app/lib/types"; +import { EnforcementLevel, TProjectPermission } from "@app/lib/types"; import { ActorAuthMethod } from "@app/services/auth/auth-type"; import { TPermissionServiceFactory } from "../permission/permission-service"; @@ -20,6 +20,7 @@ export type TCreateAccessApprovalPolicy = { approvers: string[]; projectSlug: string; name: string; + enforcementLevel: EnforcementLevel; } & Omit; export type TUpdateAccessApprovalPolicy = { @@ -28,6 +29,7 @@ export type TUpdateAccessApprovalPolicy = { approvers?: string[]; secretPath?: string; name?: string; + enforcementLevel?: EnforcementLevel; } & Omit; export type TDeleteAccessApprovalPolicy = { From c331af5345ea8871551f3634780ff16daee42258 Mon Sep 17 00:00:00 2001 From: Alfonso Hernandez Date: Wed, 17 Jul 2024 22:32:30 +0200 Subject: [PATCH 10/21] feat(frontend): add enforcementLevel into AccessPolicy --- frontend/src/hooks/api/accessApproval/mutation.tsx | 10 ++++++---- frontend/src/hooks/api/accessApproval/types.ts | 2 ++ frontend/src/hooks/api/secretApproval/types.ts | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx index 5f595c8a2..251e1c626 100644 --- a/frontend/src/hooks/api/accessApproval/mutation.tsx +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -16,14 +16,15 @@ export const useCreateAccessApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TCreateAccessPolicyDTO>({ - mutationFn: async ({ environment, projectSlug, approvals, approvers, name, secretPath }) => { + mutationFn: async ({ environment, projectSlug, approvals, approvers, name, secretPath, enforcementLevel }) => { const { data } = await apiRequest.post("/api/v1/access-approvals/policies", { environment, projectSlug, approvals, approvers, secretPath, - name + name, + enforcementLevel }); return data; }, @@ -37,12 +38,13 @@ export const useUpdateAccessApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TUpdateAccessPolicyDTO>({ - mutationFn: async ({ id, approvers, approvals, name, secretPath }) => { + mutationFn: async ({ id, approvers, approvals, name, secretPath, enforcementLevel }) => { const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, { approvals, approvers, secretPath, - name + name, + enforcementLevel }); return data; }, diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts index a6d09a227..f023b3555 100644 --- a/frontend/src/hooks/api/accessApproval/types.ts +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -125,6 +125,7 @@ export type TCreateAccessPolicyDTO = { approvers?: string[]; approvals?: number; secretPath?: string; + enforcementLevel?: EnforcementLevel; }; export type TUpdateAccessPolicyDTO = { @@ -134,6 +135,7 @@ export type TUpdateAccessPolicyDTO = { secretPath?: string; environment?: string; approvals?: number; + enforcementLevel?: EnforcementLevel; // for invalidating list projectSlug: string; }; diff --git a/frontend/src/hooks/api/secretApproval/types.ts b/frontend/src/hooks/api/secretApproval/types.ts index 2a64ccdc9..06ffad432 100644 --- a/frontend/src/hooks/api/secretApproval/types.ts +++ b/frontend/src/hooks/api/secretApproval/types.ts @@ -11,6 +11,7 @@ export type TSecretApprovalPolicy = { approvals: number; userApprovers: { userId: string }[]; updatedAt: Date; + enforcementLevel: EnforcementLevel; }; export type TGetSecretApprovalPoliciesDTO = { From aed310b9ee1efadb144efaf579c6659300136bc6 Mon Sep 17 00:00:00 2001 From: Alfonso Hernandez Date: Thu, 18 Jul 2024 01:02:57 +0200 Subject: [PATCH 11/21] feat(backemd): accept soft approvals on secret requests --- .../src/ee/routes/v1/secret-approval-request-router.ts | 6 ++++-- .../secret-approval-request-dal.ts | 8 ++++++-- .../secret-approval-request-service.ts | 6 +++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts index 8e72597bd..8e0b8a682 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -49,7 +49,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv name: z.string(), approvals: z.number(), approvers: z.string().array(), - secretPath: z.string().optional().nullable() + secretPath: z.string().optional().nullable(), + enforcementLevel: z.string() }), committerUser: approvalRequestUser, commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(), @@ -248,7 +249,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv name: z.string(), approvals: z.number(), approvers: approvalRequestUser.array(), - secretPath: z.string().optional().nullable() + secretPath: z.string().optional().nullable(), + enforcementLevel: z.string() }), environment: z.string(), statusChangedByUser: approvalRequestUser.optional(), diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts index 06c48ac8b..4ce174d26 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-dal.ts @@ -94,6 +94,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { tx.ref("projectId").withSchema(TableName.Environment), tx.ref("slug").withSchema(TableName.Environment).as("environment"), tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), + tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals") ); @@ -128,7 +129,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { id: el.policyId, name: el.policyName, approvals: el.policyApprovals, - secretPath: el.policySecretPath + secretPath: el.policySecretPath, + enforcementLevel: el.policyEnforcementLevel } }), childrenMapper: [ @@ -282,6 +284,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { `DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank` ), db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), + db.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"), db.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover), db.ref("email").withSchema("committerUser").as("committerUserEmail"), @@ -308,7 +311,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => { id: el.policyId, name: el.policyName, approvals: el.policyApprovals, - secretPath: el.policySecretPath + secretPath: el.policySecretPath, + enforcementLevel: el.policyEnforcementLevel }, committerUser: { userId: el.committerUserId, diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index a519af4fd..ae9e54b46 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -11,6 +11,7 @@ import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { groupBy, pick, unique } from "@app/lib/fn"; import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { EnforcementLevel } from "@app/lib/types"; import { ActorType } from "@app/services/auth/auth-type"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; @@ -289,7 +290,10 @@ export const secretApprovalRequestServiceFactory = ({ ({ userId: approverId }) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED ).length; - if (!hasMinApproval) throw new BadRequestError({ message: "Doesn't have minimum approvals needed" }); + const isSoftPolicy = secretApprovalRequest.policy.enforcementLevel === EnforcementLevel.Soft; + + if (!hasMinApproval && !isSoftPolicy) + throw new BadRequestError({ message: "Doesn't have minimum approvals needed" }); const secretApprovalSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id); if (!secretApprovalSecrets) throw new BadRequestError({ message: "No secrets found" }); From 8a5a295a0100faf37b66af3d16d39f1093287a03 Mon Sep 17 00:00:00 2001 From: Alfonso Hernandez Date: Thu, 18 Jul 2024 01:30:40 +0200 Subject: [PATCH 12/21] feat(frontend): accept soft approvals on secret requests --- .../ApprovalPolicyList/ApprovalPolicyList.tsx | 2 +- .../SecretApprovalRequestAction.tsx | 39 +++++++++++++++---- .../SecretApprovalRequestChanges.tsx | 1 + 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx index fb21521c9..2639f2351 100644 --- a/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx +++ b/frontend/src/views/SecretApprovalPage/components/ApprovalPolicyList/ApprovalPolicyList.tsx @@ -177,7 +177,7 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => { @@ -151,6 +162,21 @@ export const ReviewAccessRequestModal = ({ Reject Request + {isSoftEnforcement && request.isRequestedByCurrentUser && !request.isApprover && ( +
+ setByPassApproval(checked === true)} + isChecked={byPassApproval} + id="byPassApproval" + checkIndicatorBg="text-white" + className={byPassApproval ? "bg-red hover:bg-red-600 border-red" : ""} + > + + Approve without waiting for requirements to be met (by pass secrets protection) + + +
+ )} From d301f74feba71e4919664e0712cc7ea226e75978 Mon Sep 17 00:00:00 2001 From: Alfonso Hernandez Date: Thu, 18 Jul 2024 02:46:50 +0200 Subject: [PATCH 15/21] fix(frontend): interactions SecretApprovalRequestAction --- .../components/SecretApprovalRequestAction.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx index 05db75a9d..f56695764 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx @@ -134,8 +134,11 @@ export const SecretApprovalRequestAction = ({ Close request