From d380b7f788f150ee0c3ddaeb9db9fc228c604d2a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:49:00 -0700 Subject: [PATCH] Fix: Added support for request access --- .infisicalignore | 1 + .../SpecificPrivilegeSection.tsx | 270 +++++++++++++++--- 2 files changed, 235 insertions(+), 36 deletions(-) diff --git a/.infisicalignore b/.infisicalignore index d5cc9f15d..855047fe4 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -4,3 +4,4 @@ frontend/src/views/Project/MembersPage/components/IdentityTab/components/Identit frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx:generic-api-key:206 frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292 docs/self-hosting/configuration/envars.mdx:generic-api-key:106 +frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:451 diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx index 228683726..89058b08d 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -1,9 +1,11 @@ +import { useMemo } from "react"; import { Controller, useForm } from "react-hook-form"; import { faArrowRotateLeft, faCaretDown, faCheck, faClock, + faLockOpen, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; @@ -44,11 +46,13 @@ import { import { usePopUp } from "@app/hooks"; import { TProjectUserPrivilege, + useCreateAccessRequest, useCreateProjectUserAdditionalPrivilege, useDeleteProjectUserAdditionalPrivilege, useListProjectUserPrivileges, useUpdateProjectUserAdditionalPrivilege } from "@app/hooks/api"; +import { TAccessApprovalPolicy } from "@app/hooks/api/types"; const secretPermissionSchema = z.object({ secretPath: z.string().optional(), @@ -70,51 +74,107 @@ const secretPermissionSchema = z.object({ ]) }); type TSecretPermissionForm = z.infer; -const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPrivilege }) => { +export const SpecificPrivilegeSecretForm = ({ + privilege, + policies, + onClose +}: { + privilege?: TProjectUserPrivilege; + policies?: TAccessApprovalPolicy[]; + onClose?: () => void; +}) => { const { currentWorkspace } = useWorkspace(); + const { membership: projectMembership } = useProjectPermission(); + const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ - "deletePrivilege" + "deletePrivilege", + "requestAccess" ] as const); const { permission } = useProjectPermission(); - const isMemberEditDisabled = permission.cannot( - ProjectPermissionActions.Edit, - ProjectPermissionSub.Member - ); + const isMemberEditDisabled = + permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Member) && !!privilege; const updateUserPrivilege = useUpdateProjectUserAdditionalPrivilege(); const deleteUserPrivilege = useDeleteProjectUserAdditionalPrivilege(); + const requestAccess = useCreateAccessRequest(); const privilegeForm = useForm({ resolver: zodResolver(secretPermissionSchema), values: { - environmentSlug: privilege.permissions?.[0]?.conditions?.environment, - // secret path will be inside $glob operator - secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob || "", - read: privilege.permissions?.some(({ action }) => - action.includes(ProjectPermissionActions.Read) - ), - edit: privilege.permissions?.some(({ action }) => - action.includes(ProjectPermissionActions.Edit) - ), - create: privilege.permissions?.some(({ action }) => - action.includes(ProjectPermissionActions.Create) - ), - delete: privilege.permissions?.some(({ action }) => - action.includes(ProjectPermissionActions.Delete) - ), - // zod will pick it - temporaryAccess: privilege + ...(privilege + ? { + environmentSlug: privilege.permissions?.[0]?.conditions?.environment, + // secret path will be inside $glob operator + secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob || "", + read: privilege.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Read) + ), + edit: privilege.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Edit) + ), + create: privilege.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Create) + ), + delete: privilege.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Delete) + ), + // zod will pick it + temporaryAccess: privilege + } + : { + environmentSlug: currentWorkspace?.environments?.[0].slug!, + read: false, + edit: false, + create: false, + delete: false, + temporaryAccess: { + isTemporary: false + } + }) } }); const temporaryAccessField = privilegeForm.watch("temporaryAccess"); const selectedEnvironmentSlug = privilegeForm.watch("environmentSlug"); + const selectedEnvironment = privilegeForm.watch("environmentSlug"); + const secretPath = privilegeForm.watch("secretPath"); + + const readAccess = privilegeForm.watch("read"); + const createAccess = privilegeForm.watch("create"); + const editAccess = privilegeForm.watch("edit"); + const deleteAccess = privilegeForm.watch("delete"); + + const accessSelected = readAccess || createAccess || editAccess || deleteAccess; + + const selectablePaths = useMemo(() => { + if (!policies) return []; + const environmentPolicies = policies.filter( + (policy) => policy.environment.slug === selectedEnvironment + ); + + privilegeForm.setValue("secretPath", "", { + shouldValidate: true + }); + + return [...environmentPolicies.map((policy) => policy.secretPath)]; + }, [policies, selectedEnvironment]); + const isTemporary = temporaryAccessField?.isTemporary; const isExpired = temporaryAccessField.isTemporary && new Date() > new Date(temporaryAccessField.temporaryAccessEndTime || ""); const handleUpdatePrivilege = async (data: TSecretPermissionForm) => { + if (!privilege) { + createNotification({ + type: "error", + text: "No privilege to update found.", + title: "Error" + }); + + return; + } + if (updateUserPrivilege.isLoading) return; try { const actions = [ @@ -152,6 +212,15 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri }; const handleDeletePrivilege = async () => { + if (!privilege) { + createNotification({ + type: "error", + text: "No privilege to delete found.", + title: "Error" + }); + return; + } + if (deleteUserPrivilege.isLoading) return; try { await deleteUserPrivilege.mutateAsync({ @@ -170,35 +239,122 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri } }; + const getAccessLabel = (exactTime = false) => { if (isExpired) return "Access expired"; if (!temporaryAccessField?.isTemporary) return "Permanent"; + if (exactTime) return `Until ${format( new Date(temporaryAccessField.temporaryAccessEndTime || ""), "yyyy-MM-dd HH:mm:ss" )}`; return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); + }; + + }; + + // This is used for requesting access additional privileges, not directly creating a privilege! + const handleRequestAccess = async (data: TSecretPermissionForm) => { + if (!policies) return; + if (!currentWorkspace) { + createNotification({ + type: "error", + text: "No workspace found.", + title: "Error" + }); + return; + } + + if (!data.secretPath) { + createNotification({ + type: "error", + text: "Please select a secret path", + title: "Error" + }); + return; + } + + const actions = [ + { action: ProjectPermissionActions.Read, allowed: data.read }, + { action: ProjectPermissionActions.Create, allowed: data.create }, + { action: ProjectPermissionActions.Delete, allowed: data.delete }, + { action: ProjectPermissionActions.Edit, allowed: data.edit } + ]; + const conditions: Record = { environment: data.environmentSlug }; + if (data.secretPath) { + conditions.secretPath = { $glob: data.secretPath }; + } + await requestAccess.mutateAsync({ + ...data, + ...(data.temporaryAccess.isTemporary && { + temporaryAccessStartTime: data.temporaryAccess.temporaryAccessStartTime, + temporaryAccessEndTime: data.temporaryAccess.temporaryAccessEndTime, + temporaryRange: data.temporaryAccess.temporaryRange, + temporaryMode: "relative" + }), + envSlug: data.environmentSlug, + secretPath: data.secretPath, + projectSlug: currentWorkspace.slug, + projectMembershipId: projectMembership.id, + isTemporary: data.temporaryAccess.isTemporary, + permissions: actions + .filter(({ allowed }) => allowed) + .map(({ action }) => ({ + action, + subject: [ProjectPermissionSub.Secrets], + conditions + })) + }); + + createNotification({ + type: "success", + text: "Successfully requested access" + }); + privilegeForm.reset(); + if (onClose) onClose(); + }; + + const handleSubmit = async (data: TSecretPermissionForm) => { + if (privilege) { + handleUpdatePrivilege(data); + } else { + handleRequestAccess(data); + } + }; + + const getAccessLabel = (exactTime = false) => { + if (isExpired) return "Access expired"; + if (!temporaryAccessField?.isTemporary) return "Permanent"; + + if (exactTime) + return `Until ${format( + new Date(temporaryAccessField.temporaryAccessEndTime || ""), + "yyyy-MM-dd HH:mm:ss" + )}`; + return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); + }; + }; return ( -
-
-
+
+ +
( - + @@ -208,8 +364,28 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri ( - + render={({ field }) => { + console.log(policies); + if (policies) { + return ( + + + + ); + } + return ( + - )} + ); + }} />
-
+
@@ -301,7 +478,7 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri isExpired && "text-red-600" )} > - {getAccessLabel()} + {getAccessLabel(false)}
@@ -382,7 +559,8 @@ const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPri
- {privilegeForm.formState.isDirty ? ( + {/* eslint-disable-next-line no-nested-ternary */} + {privilegeForm.formState.isDirty && privilege ? ( <> - ) : ( + ) : // eslint-disable-next-line no-nested-ternary + privilege ? ( + ) : ( +
)}
+ {!!policies && ( + + )}