From c88923e0c6cab7b04513db878f3a5756e3914df8 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Mon, 22 Apr 2024 17:59:21 +0000 Subject: [PATCH 001/188] fix: backend/package.json to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-MYSQL2-6670046 --- backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index 0f7b5a5907..06f6241bca 100644 --- a/backend/package.json +++ b/backend/package.json @@ -109,7 +109,7 @@ "libsodium-wrappers": "^0.7.13", "lodash.isequal": "^4.5.0", "ms": "^2.1.3", - "mysql2": "^3.9.4", + "mysql2": "^3.9.7", "nanoid": "^5.0.4", "nodemailer": "^6.9.9", "ora": "^7.0.1", From 9c002ad64512d67fd4acdc1adafaea5ef5b99ca3 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 2 May 2024 22:42:02 -0700 Subject: [PATCH 002/188] Finish preliminary AWS IAM Auth method --- .../src/hooks/api/identities/constants.tsx | 3 +- frontend/src/hooks/api/identities/enums.tsx | 3 +- frontend/src/hooks/api/identities/index.tsx | 10 +- .../src/hooks/api/identities/mutations.tsx | 77 +++- frontend/src/hooks/api/identities/queries.tsx | 28 +- frontend/src/hooks/api/identities/types.ts | 63 +++- .../IdentityAuthMethodModal.tsx | 61 ++- .../IdentityAwsIamAuthForm.tsx | 348 ++++++++++++++++++ .../IdentitySection/IdentityModal.tsx | 45 +-- .../IdentitySection/IdentityTable.tsx | 3 - .../IdentityUniversalAuthForm.tsx | 3 +- 11 files changed, 575 insertions(+), 69 deletions(-) create mode 100644 frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsIamAuthForm.tsx diff --git a/frontend/src/hooks/api/identities/constants.tsx b/frontend/src/hooks/api/identities/constants.tsx index 53fceb7d5b..5e8ae069f6 100644 --- a/frontend/src/hooks/api/identities/constants.tsx +++ b/frontend/src/hooks/api/identities/constants.tsx @@ -1,5 +1,6 @@ import { IdentityAuthMethod } from "./enums"; export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = { - [IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth" + [IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth", + [IdentityAuthMethod.AWS_IAM_AUTH]: "AWS IAM Auth" }; diff --git a/frontend/src/hooks/api/identities/enums.tsx b/frontend/src/hooks/api/identities/enums.tsx index 524c3a20c1..614549da32 100644 --- a/frontend/src/hooks/api/identities/enums.tsx +++ b/frontend/src/hooks/api/identities/enums.tsx @@ -1,3 +1,4 @@ export enum IdentityAuthMethod { - UNIVERSAL_AUTH = "universal-auth" + UNIVERSAL_AUTH = "universal-auth", + AWS_IAM_AUTH = "aws-iam-auth" } diff --git a/frontend/src/hooks/api/identities/index.tsx b/frontend/src/hooks/api/identities/index.tsx index 684a00b6fb..16fc567821 100644 --- a/frontend/src/hooks/api/identities/index.tsx +++ b/frontend/src/hooks/api/identities/index.tsx @@ -1,12 +1,16 @@ export { identityAuthToNameMap } from "./constants"; export { IdentityAuthMethod } from "./enums"; export { + useAddIdentityAwsIamAuth, useAddIdentityUniversalAuth, useCreateIdentity, useCreateIdentityUniversalAuthClientSecret, useDeleteIdentity, useRevokeIdentityUniversalAuthClientSecret, useUpdateIdentity, - useUpdateIdentityUniversalAuth -} from "./mutations"; -export { useGetIdentityUniversalAuth, useGetIdentityUniversalAuthClientSecrets } from "./queries"; + useUpdateIdentityAwsIamAuth, + useUpdateIdentityUniversalAuth} from "./mutations"; +export { + useGetIdentityAwsIamAuth, + useGetIdentityUniversalAuth, + useGetIdentityUniversalAuthClientSecrets} from "./queries"; diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index ec418659dd..fa956af137 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -5,6 +5,7 @@ import { apiRequest } from "@app/config/request"; import { organizationKeys } from "../organization/queries"; import { identitiesKeys } from "./queries"; import { + AddIdentityAwsIamAuthDTO, AddIdentityUniversalAuthDTO, ClientSecretData, CreateIdentityDTO, @@ -13,10 +14,11 @@ import { DeleteIdentityDTO, DeleteIdentityUniversalAuthClientSecretDTO, Identity, + IdentityAwsIamAuth, IdentityUniversalAuth, + UpdateIdentityAwsIamAuthDTO, UpdateIdentityDTO, - UpdateIdentityUniversalAuthDTO -} from "./types"; + UpdateIdentityUniversalAuthDTO} from "./types"; export const useCreateIdentity = () => { const queryClient = useQueryClient(); @@ -169,3 +171,74 @@ export const useRevokeIdentityUniversalAuthClientSecret = () => { } }); }; + +export const useAddIdentityAwsIamAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityAwsIamAuth } + } = await apiRequest.post<{ identityAwsIamAuth: IdentityAwsIamAuth }>( + `/api/v1/auth/aws-iam-auth/identities/${identityId}`, + { + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + + return identityAwsIamAuth; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + } + }); +}; + +export const useUpdateIdentityAwsIamAuth = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + identityId, + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }) => { + const { + data: { identityAwsIamAuth } + } = await apiRequest.patch<{ identityAwsIamAuth: IdentityAwsIamAuth }>( + `/api/v1/auth/aws-iam-auth/identities/${identityId}`, + { + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + } + ); + return identityAwsIamAuth; + }, + onSuccess: (_, { organizationId }) => { + queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId)); + } + }); +}; diff --git a/frontend/src/hooks/api/identities/queries.tsx b/frontend/src/hooks/api/identities/queries.tsx index 92d2a432f0..5d4a4aeef5 100644 --- a/frontend/src/hooks/api/identities/queries.tsx +++ b/frontend/src/hooks/api/identities/queries.tsx @@ -2,27 +2,26 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { ClientSecretData, IdentityUniversalAuth } from "./types"; +import { ClientSecretData, IdentityAwsIamAuth,IdentityUniversalAuth } from "./types"; export const identitiesKeys = { getIdentityUniversalAuth: (identityId: string) => [{ identityId }, "identity-universal-auth"] as const, getIdentityUniversalAuthClientSecrets: (identityId: string) => - [{ identityId }, "identity-universal-auth-client-secrets"] as const + [{ identityId }, "identity-universal-auth-client-secrets"] as const, + getIdentityAwsIamAuth: (identityId: string) => [{ identityId }, "identity-aws-iam-auth"] as const }; export const useGetIdentityUniversalAuth = (identityId: string) => { return useQuery({ + enabled: Boolean(identityId), queryKey: identitiesKeys.getIdentityUniversalAuth(identityId), queryFn: async () => { - if (identityId === "") throw new Error("Identity ID is required"); - const { data: { identityUniversalAuth } } = await apiRequest.get<{ identityUniversalAuth: IdentityUniversalAuth }>( `/api/v1/auth/universal-auth/identities/${identityId}` ); - return identityUniversalAuth; } }); @@ -30,17 +29,30 @@ export const useGetIdentityUniversalAuth = (identityId: string) => { export const useGetIdentityUniversalAuthClientSecrets = (identityId: string) => { return useQuery({ + enabled: Boolean(identityId), queryKey: identitiesKeys.getIdentityUniversalAuthClientSecrets(identityId), queryFn: async () => { - if (identityId === "") return []; - const { data: { clientSecretData } } = await apiRequest.get<{ clientSecretData: ClientSecretData[] }>( `/api/v1/auth/universal-auth/identities/${identityId}/client-secrets` ); - return clientSecretData; } }); }; + +export const useGetIdentityAwsIamAuth = (identityId: string) => { + return useQuery({ + enabled: Boolean(identityId), + queryKey: identitiesKeys.getIdentityAwsIamAuth(identityId), + queryFn: async () => { + const { + data: { identityAwsIamAuth } + } = await apiRequest.get<{ identityAwsIamAuth: IdentityAwsIamAuth }>( + `/api/v1/auth/aws-iam-auth/identities/${identityId}` + ); + return identityAwsIamAuth; + } + }); +}; diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index 4ac19c3511..02b0e9f53c 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -38,19 +38,19 @@ export type IdentityMembership = { customRoleSlug: string; } & ( | { - isTemporary: false; - temporaryRange: null; - temporaryMode: null; - temporaryAccessEndTime: null; - temporaryAccessStartTime: null; - } + isTemporary: false; + temporaryRange: null; + temporaryMode: null; + temporaryAccessEndTime: null; + temporaryAccessStartTime: null; + } | { - isTemporary: true; - temporaryRange: string; - temporaryMode: string; - temporaryAccessEndTime: string; - temporaryAccessStartTime: string; - } + isTemporary: true; + temporaryRange: string; + temporaryMode: string; + temporaryAccessEndTime: string; + temporaryAccessStartTime: string; + } ) >; createdAt: string; @@ -113,6 +113,45 @@ export type UpdateIdentityUniversalAuthDTO = { }[]; }; +export type IdentityAwsIamAuth = { + identityId: string; + stsEndpoint: string; + allowedPrincipalArns: string; + allowedAccountIds: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: IdentityTrustedIp[]; +}; + +export type AddIdentityAwsIamAuthDTO = { + organizationId: string; + identityId: string; + stsEndpoint: string; + allowedPrincipalArns: string; + allowedAccountIds: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { + ipAddress: string; + }[]; +}; + +export type UpdateIdentityAwsIamAuthDTO = { + organizationId: string; + identityId: string; + stsEndpoint?: string; + allowedPrincipalArns?: string; + allowedAccountIds?: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { + ipAddress: string; + }[]; +}; + export type CreateIdentityUniversalAuthClientSecretDTO = { identityId: string; description?: string; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx index 7ec17dd312..2cd4cc3342 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal.tsx @@ -1,3 +1,4 @@ +import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; @@ -13,6 +14,7 @@ import { import { IdentityAuthMethod } from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; +import { IdentityAwsIamAuthForm } from "./IdentityAwsIamAuthForm"; import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm"; type Props = { @@ -24,22 +26,25 @@ type Props = { ) => void; }; -const identityAuthMethods = [{ label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH }]; +const identityAuthMethods = [ + { label: "Universal Auth", value: IdentityAuthMethod.UNIVERSAL_AUTH }, + { label: "AWS IAM Auth", value: IdentityAuthMethod.AWS_IAM_AUTH } +]; const schema = yup .object({ - authMethod: yup.string().required("Auth method is required") // TODO: better enforcement here + authMethod: yup.string().required("Auth method is required") }) .required(); export type FormData = yup.InferType; export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => { - const { - control - // watch, - } = useForm({ - resolver: yupResolver(schema) + const { control, watch, setValue } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + authMethod: IdentityAuthMethod.UNIVERSAL_AUTH + } }); const identityAuthMethodData = popUp?.identityAuthMethod?.data as { @@ -48,16 +53,41 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog authMethod?: IdentityAuthMethod; }; - // const authMethod = watch("authMethod"); + useEffect(() => { + if (identityAuthMethodData?.authMethod) { + setValue("authMethod", identityAuthMethodData.authMethod); + return; + } + + setValue("authMethod", IdentityAuthMethod.UNIVERSAL_AUTH); + }, [identityAuthMethodData?.authMethod]); + + const authMethod = watch("authMethod"); const renderIdentityAuthForm = () => { - return ( - - ); + switch (identityAuthMethodData?.authMethod ?? authMethod) { + case IdentityAuthMethod.AWS_IAM_AUTH: { + return ( + + ); + } + case IdentityAuthMethod.UNIVERSAL_AUTH: { + return ( + + ); + } + default: { + return
; + } + } }; return ( @@ -83,6 +113,7 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog {...field} onValueChange={(e) => onChange(e)} className="w-full" + isDisabled={!!identityAuthMethodData?.authMethod} > {identityAuthMethods.map(({ label, value }) => ( diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsIamAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsIamAuthForm.tsx new file mode 100644 index 0000000000..416d70482f --- /dev/null +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsIamAuthForm.tsx @@ -0,0 +1,348 @@ +import { useEffect } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, IconButton, Input } from "@app/components/v2"; +import { useOrganization, useSubscription } from "@app/context"; +import { + useAddIdentityAwsIamAuth, + useGetIdentityAwsIamAuth, + useUpdateIdentityAwsIamAuth +} from "@app/hooks/api"; +import { IdentityAuthMethod } from "@app/hooks/api/identities"; +import { IdentityTrustedIp } from "@app/hooks/api/identities/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = yup + .object({ + stsEndpoint: yup.string(), + allowedPrincipalArns: yup.string(), + allowedAccountIds: yup.string(), + accessTokenTTL: yup.string().required("Access Token TTL is required"), + accessTokenMaxTTL: yup.string().required("Access Max Token TTL is required"), + accessTokenNumUsesLimit: yup.string().required("Access Token Max Number of Uses is required"), + accessTokenTrustedIps: yup + .array( + yup.object({ + ipAddress: yup.string().max(50).required().label("IP Address") + }) + ) + .min(1) + .required() + .label("Access Token Trusted IP") + }) + .required(); + +export type FormData = yup.InferType; + +type Props = { + handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["identityAuthMethod"]>, + state?: boolean + ) => void; + identityAuthMethodData: { + identityId: string; + name: string; + authMethod?: IdentityAuthMethod; + }; +}; + +export const IdentityAwsIamAuthForm = ({ + handlePopUpOpen, + handlePopUpToggle, + identityAuthMethodData +}: Props) => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { subscription } = useSubscription(); + + const { mutateAsync: addMutateAsync } = useAddIdentityAwsIamAuth(); + const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsIamAuth(); + + const { data } = useGetIdentityAwsIamAuth(identityAuthMethodData?.identityId ?? ""); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + stsEndpoint: "https://sts.amazonaws.com/", + allowedPrincipalArns: "", + allowedAccountIds: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + } + }); + + const { + fields: accessTokenTrustedIpsFields, + append: appendAccessTokenTrustedIp, + remove: removeAccessTokenTrustedIp + } = useFieldArray({ control, name: "accessTokenTrustedIps" }); + + useEffect(() => { + if (data) { + reset({ + stsEndpoint: data.stsEndpoint, + allowedPrincipalArns: data.allowedPrincipalArns, + allowedAccountIds: data.allowedAccountIds, + accessTokenTTL: String(data.accessTokenTTL), + accessTokenMaxTTL: String(data.accessTokenMaxTTL), + accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit), + accessTokenTrustedIps: data.accessTokenTrustedIps.map( + ({ ipAddress, prefix }: IdentityTrustedIp) => { + return { + ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}` + }; + } + ) + }); + } else { + reset({ + stsEndpoint: "https://sts.amazonaws.com/", + allowedPrincipalArns: "", + allowedAccountIds: "", + accessTokenTTL: "2592000", + accessTokenMaxTTL: "2592000", + accessTokenNumUsesLimit: "0", + accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }] + }); + } + }, [data]); + + const onFormSubmit = async ({ + allowedPrincipalArns, + allowedAccountIds, + stsEndpoint, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps + }: FormData) => { + try { + if (!identityAuthMethodData) return; + + if (data) { + await updateMutateAsync({ + organizationId: orgId, + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + identityId: identityAuthMethodData.identityId, + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } else { + await addMutateAsync({ + organizationId: orgId, + identityId: identityAuthMethodData.identityId, + stsEndpoint: stsEndpoint || "", + allowedPrincipalArns: allowedPrincipalArns || "", + allowedAccountIds: allowedAccountIds || "", + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), + accessTokenTrustedIps + }); + } + + handlePopUpToggle("identityAuthMethod", false); + + createNotification({ + text: `Successfully ${ + identityAuthMethodData?.authMethod ? "updated" : "configured" + } auth method`, + type: "success" + }); + + reset(); + } catch (err) { + createNotification({ + text: `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`, + type: "error" + }); + } + }; + + return ( +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + {accessTokenTrustedIpsFields.map(({ id }, index) => ( +
+ { + return ( + + { + if (subscription?.ipAllowlisting) { + field.onChange(e); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + placeholder="123.456.789.0" + /> + + ); + }} + /> + { + if (subscription?.ipAllowlisting) { + removeAccessTokenTrustedIp(index); + return; + } + + handlePopUpOpen("upgradePlan"); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="p-3" + > + + +
+ ))} +
+ +
+
+ + +
+ + ); +}; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx index 4ab44e947f..e8b16cb5db 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal.tsx @@ -15,7 +15,10 @@ import { } from "@app/components/v2"; import { useOrganization } from "@app/context"; import { useCreateIdentity, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api"; -import { IdentityAuthMethod, useAddIdentityUniversalAuth } from "@app/hooks/api/identities"; +import { + IdentityAuthMethod + // useAddIdentityUniversalAuth +} from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = yup @@ -40,9 +43,7 @@ type Props = { handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void; }; -export const IdentityModal = ({ popUp, /* handlePopUpOpen, */ handlePopUpToggle }: Props) => { - - +export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => { const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; @@ -50,7 +51,7 @@ export const IdentityModal = ({ popUp, /* handlePopUpOpen, */ handlePopUpToggle const { mutateAsync: createMutateAsync } = useCreateIdentity(); const { mutateAsync: updateMutateAsync } = useUpdateIdentity(); - const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth(); + // const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth(); const { control, @@ -113,31 +114,31 @@ export const IdentityModal = ({ popUp, /* handlePopUpOpen, */ handlePopUpToggle // create const { - id: createdId - // name: createdName, - // authMethod + id: createdId, + name: createdName, + authMethod } = await createMutateAsync({ name, role: role || undefined, organizationId: orgId }); - await addMutateAsync({ - organizationId: orgId, - identityId: createdId, - clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], - accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], - accessTokenTTL: 2592000, - accessTokenMaxTTL: 2592000, - accessTokenNumUsesLimit: 0 - }); + // await addMutateAsync({ + // organizationId: orgId, + // identityId: createdId, + // clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], + // accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }], + // accessTokenTTL: 2592000, + // accessTokenMaxTTL: 2592000, + // accessTokenNumUsesLimit: 0 + // }); handlePopUpToggle("identity", false); - // handlePopUpOpen("identityAuthMethod", { - // identityId: createdId, - // name: createdName, - // authMethod - // }); + handlePopUpOpen("identityAuthMethod", { + identityId: createdId, + name: createdName, + authMethod + }); } createNotification({ diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx index 804c2d0542..0ed9cd9a45 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityTable.tsx @@ -23,8 +23,6 @@ import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities"; import { UsePopUpState } from "@app/hooks/usePopUp"; -// TODO: some kind of map - type Props = { handlePopUpOpen: ( popUpName: keyof UsePopUpState< @@ -44,7 +42,6 @@ type Props = { }; export const IdentityTable = ({ handlePopUpOpen }: Props) => { - const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx index d8b4ac0429..af82082a51 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm.tsx @@ -63,7 +63,6 @@ export const IdentityUniversalAuthForm = ({ handlePopUpToggle, identityAuthMethodData }: Props) => { - const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; const { subscription } = useSubscription(); @@ -384,7 +383,7 @@ export const IdentityUniversalAuthForm = ({ variant="plain" onClick={() => handlePopUpToggle("identityAuthMethod", false)} > - Cancel + {identityAuthMethodData?.authMethod ? "Cancel" : "Skip"}
From d5c74d558afb284bbf95392a88a2d47582edaa1d Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Thu, 2 May 2024 22:52:37 -0700 Subject: [PATCH 003/188] Start docs for AWS IAM auth --- backend/package-lock.json | 664 +++++++++++------- backend/src/@types/fastify.d.ts | 2 + backend/src/@types/knex.d.ts | 8 + .../20240502044531_identity-aws-iam-auth.ts | 29 + .../src/db/schemas/identity-aws-iam-auths.ts | 26 + backend/src/db/schemas/index.ts | 1 + backend/src/db/schemas/models.ts | 4 +- .../ee/services/audit-log/audit-log-types.ts | 52 ++ backend/src/lib/api-docs/constants.ts | 12 + backend/src/server/routes/index.ts | 12 + .../routes/v1/identity-aws-iam-auth-router.ts | 269 +++++++ backend/src/server/routes/v1/index.ts | 2 + .../identity-aws-iam-auth-dal.ts | 11 + .../identity-aws-iam-auth-service.ts | 315 +++++++++ .../identity-aws-iam-auth-types.ts | 54 ++ .../identity-aws-iam-auth-validators.ts | 58 ++ .../identity-aws-iam-auth.fns.ts | 67 ++ .../platform/identities/aws-iam-auth.mdx | 12 + .../platform/identities/universal-auth.mdx | 25 +- docs/mint.json | 5 +- 20 files changed, 1355 insertions(+), 273 deletions(-) create mode 100644 backend/src/db/migrations/20240502044531_identity-aws-iam-auth.ts create mode 100644 backend/src/db/schemas/identity-aws-iam-auths.ts create mode 100644 backend/src/server/routes/v1/identity-aws-iam-auth-router.ts create mode 100644 backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-dal.ts create mode 100644 backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-service.ts create mode 100644 backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-types.ts create mode 100644 backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-validators.ts create mode 100644 backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth.fns.ts create mode 100644 docs/documentation/platform/identities/aws-iam-auth.mdx diff --git a/backend/package-lock.json b/backend/package-lock.json index b51573688e..5928f64a5d 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1207,6 +1207,58 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/client-sts": { + "version": "3.504.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.504.0.tgz", + "integrity": "sha512-IESs8FkL7B/uY+ml4wgoRkrr6xYo4PizcNw6JX17eveq1gRBCPKeGMjE6HTDOcIYZZ8rqz/UeuH3JD4UhrMOnA==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.496.0", + "@aws-sdk/middleware-host-header": "3.502.0", + "@aws-sdk/middleware-logger": "3.502.0", + "@aws-sdk/middleware-recursion-detection": "3.502.0", + "@aws-sdk/middleware-user-agent": "3.502.0", + "@aws-sdk/region-config-resolver": "3.502.0", + "@aws-sdk/types": "3.502.0", + "@aws-sdk/util-endpoints": "3.502.0", + "@aws-sdk/util-user-agent-browser": "3.502.0", + "@aws-sdk/util-user-agent-node": "3.502.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.1", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.1.1", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": "^3.504.0" + } + }, "node_modules/@aws-sdk/client-secrets-manager/node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -1314,7 +1366,7 @@ "@aws-sdk/credential-provider-node": "^3.504.0" } }, - "node_modules/@aws-sdk/client-sts": { + "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/client-sts": { "version": "3.504.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.504.0.tgz", "integrity": "sha512-IESs8FkL7B/uY+ml4wgoRkrr6xYo4PizcNw6JX17eveq1gRBCPKeGMjE6HTDOcIYZZ8rqz/UeuH3JD4UhrMOnA==", @@ -1436,6 +1488,58 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/credential-provider-ini/node_modules/@aws-sdk/client-sts": { + "version": "3.504.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.504.0.tgz", + "integrity": "sha512-IESs8FkL7B/uY+ml4wgoRkrr6xYo4PizcNw6JX17eveq1gRBCPKeGMjE6HTDOcIYZZ8rqz/UeuH3JD4UhrMOnA==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.496.0", + "@aws-sdk/middleware-host-header": "3.502.0", + "@aws-sdk/middleware-logger": "3.502.0", + "@aws-sdk/middleware-recursion-detection": "3.502.0", + "@aws-sdk/middleware-user-agent": "3.502.0", + "@aws-sdk/region-config-resolver": "3.502.0", + "@aws-sdk/types": "3.502.0", + "@aws-sdk/util-endpoints": "3.502.0", + "@aws-sdk/util-user-agent-browser": "3.502.0", + "@aws-sdk/util-user-agent-node": "3.502.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.1", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.1.1", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": "^3.504.0" + } + }, "node_modules/@aws-sdk/credential-provider-node": { "version": "3.504.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.504.0.tgz", @@ -1505,6 +1609,58 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/@aws-sdk/client-sts": { + "version": "3.504.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.504.0.tgz", + "integrity": "sha512-IESs8FkL7B/uY+ml4wgoRkrr6xYo4PizcNw6JX17eveq1gRBCPKeGMjE6HTDOcIYZZ8rqz/UeuH3JD4UhrMOnA==", + "dependencies": { + "@aws-crypto/sha256-browser": "3.0.0", + "@aws-crypto/sha256-js": "3.0.0", + "@aws-sdk/core": "3.496.0", + "@aws-sdk/middleware-host-header": "3.502.0", + "@aws-sdk/middleware-logger": "3.502.0", + "@aws-sdk/middleware-recursion-detection": "3.502.0", + "@aws-sdk/middleware-user-agent": "3.502.0", + "@aws-sdk/region-config-resolver": "3.502.0", + "@aws-sdk/types": "3.502.0", + "@aws-sdk/util-endpoints": "3.502.0", + "@aws-sdk/util-user-agent-browser": "3.502.0", + "@aws-sdk/util-user-agent-node": "3.502.0", + "@smithy/config-resolver": "^2.1.1", + "@smithy/core": "^1.3.1", + "@smithy/fetch-http-handler": "^2.4.1", + "@smithy/hash-node": "^2.1.1", + "@smithy/invalid-dependency": "^2.1.1", + "@smithy/middleware-content-length": "^2.1.1", + "@smithy/middleware-endpoint": "^2.4.1", + "@smithy/middleware-retry": "^2.1.1", + "@smithy/middleware-serde": "^2.1.1", + "@smithy/middleware-stack": "^2.1.1", + "@smithy/node-config-provider": "^2.2.1", + "@smithy/node-http-handler": "^2.3.1", + "@smithy/protocol-http": "^3.1.1", + "@smithy/smithy-client": "^2.3.1", + "@smithy/types": "^2.9.1", + "@smithy/url-parser": "^2.1.1", + "@smithy/util-base64": "^2.1.1", + "@smithy/util-body-length-browser": "^2.1.1", + "@smithy/util-body-length-node": "^2.2.1", + "@smithy/util-defaults-mode-browser": "^2.1.1", + "@smithy/util-defaults-mode-node": "^2.1.1", + "@smithy/util-endpoints": "^1.1.1", + "@smithy/util-middleware": "^2.1.1", + "@smithy/util-retry": "^2.1.1", + "@smithy/util-utf8": "^2.1.1", + "fast-xml-parser": "4.2.5", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": "^3.504.0" + } + }, "node_modules/@aws-sdk/middleware-host-header": { "version": "3.502.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.502.0.tgz", @@ -3657,60 +3813,60 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.1.3.tgz", - "integrity": "sha512-c2aYH2Wu1RVE3rLlVgg2kQOBJGM0WbjReQi5DnPTm2Zb7F0gk7J2aeQeaX2u/lQZoHl6gv8Oac7mt9alU3+f4A==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-wRlta7GuLWpTqtFfGo+nZyOO1vEvewdNR1R4rTxpC8XU6vG/NDyrFBhwLZsqg1NUoR1noVaXJPC/7ZK47QCySw==", "dependencies": { - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/config-resolver": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.1.4.tgz", - "integrity": "sha512-AW2WUZmBAzgO3V3ovKtsUbI3aBNMeQKFDumoqkNxaVDWF/xfnxAWqBKDr/NuG7c06N2Rm4xeZLPiJH/d+na0HA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.2.0.tgz", + "integrity": "sha512-fsiMgd8toyUba6n1WRmr+qACzXltpdDkPTAaDqc8QqPBUzO+/JKwL6bUBseHVi8tu9l+3JOK+tSf7cay+4B3LA==", "dependencies": { - "@smithy/node-config-provider": "^2.2.4", - "@smithy/types": "^2.10.1", - "@smithy/util-config-provider": "^2.2.1", - "@smithy/util-middleware": "^2.1.3", - "tslib": "^2.5.0" + "@smithy/node-config-provider": "^2.3.0", + "@smithy/types": "^2.12.0", + "@smithy/util-config-provider": "^2.3.0", + "@smithy/util-middleware": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/core": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.3.5.tgz", - "integrity": "sha512-Rrc+e2Jj6Gu7Xbn0jvrzZlSiP2CZocIOfZ9aNUA82+1sa6GBnxqL9+iZ9EKHeD9aqD1nU8EK4+oN2EiFpSv7Yw==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.4.2.tgz", + "integrity": "sha512-2fek3I0KZHWJlRLvRTqxTEri+qV0GRHrJIoLFuBMZB4EMg4WgeBGfF0X6abnrNYpq55KJ6R4D6x4f0vLnhzinA==", "dependencies": { - "@smithy/middleware-endpoint": "^2.4.4", - "@smithy/middleware-retry": "^2.1.4", - "@smithy/middleware-serde": "^2.1.3", - "@smithy/protocol-http": "^3.2.1", - "@smithy/smithy-client": "^2.4.2", - "@smithy/types": "^2.10.1", - "@smithy/util-middleware": "^2.1.3", - "tslib": "^2.5.0" + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-retry": "^2.3.1", + "@smithy/middleware-serde": "^2.3.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "@smithy/util-middleware": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/credential-provider-imds": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.2.4.tgz", - "integrity": "sha512-DdatjmBZQnhGe1FhI8gO98f7NmvQFSDiZTwC3WMvLTCKQUY+Y1SVkhJqIuLu50Eb7pTheoXQmK+hKYUgpUWsNA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.3.0.tgz", + "integrity": "sha512-BWB9mIukO1wjEOo1Ojgl6LrG4avcaC7T/ZP6ptmAaW4xluhSIPZhY+/PI5YKzlk+jsm+4sQZB45Bt1OfMeQa3w==", "dependencies": { - "@smithy/node-config-provider": "^2.2.4", - "@smithy/property-provider": "^2.1.3", - "@smithy/types": "^2.10.1", - "@smithy/url-parser": "^2.1.3", - "tslib": "^2.5.0" + "@smithy/node-config-provider": "^2.3.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" @@ -3779,459 +3935,451 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.4.3.tgz", - "integrity": "sha512-Fn/KYJFo6L5I4YPG8WQb2hOmExgRmNpVH5IK2zU3JKrY5FKW7y9ar5e0BexiIC9DhSKqKX+HeWq/Y18fq7Dkpw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.5.0.tgz", + "integrity": "sha512-BOWEBeppWhLn/no/JxUL/ghTfANTjT7kg3Ww2rPqTUY9R4yHPXxJ9JhMe3Z03LN3aPwiwlpDIUcVw1xDyHqEhw==", "dependencies": { - "@smithy/protocol-http": "^3.2.1", - "@smithy/querystring-builder": "^2.1.3", - "@smithy/types": "^2.10.1", - "@smithy/util-base64": "^2.1.1", - "tslib": "^2.5.0" + "@smithy/protocol-http": "^3.3.0", + "@smithy/querystring-builder": "^2.2.0", + "@smithy/types": "^2.12.0", + "@smithy/util-base64": "^2.3.0", + "tslib": "^2.6.2" } }, "node_modules/@smithy/hash-node": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.1.3.tgz", - "integrity": "sha512-FsAPCUj7VNJIdHbSxMd5uiZiF20G2zdSDgrgrDrHqIs/VMxK85Vqk5kMVNNDMCZmMezp6UKnac0B4nAyx7HJ9g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.2.0.tgz", + "integrity": "sha512-zLWaC/5aWpMrHKpoDF6nqpNtBhlAYKF/7+9yMN7GpdR8CzohnWfGtMznPybnwSS8saaXBMxIGwJqR4HmRp6b3g==", "dependencies": { - "@smithy/types": "^2.10.1", - "@smithy/util-buffer-from": "^2.1.1", - "@smithy/util-utf8": "^2.1.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/invalid-dependency": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.1.3.tgz", - "integrity": "sha512-wkra7d/G4CbngV4xsjYyAYOvdAhahQje/WymuQdVEnXFExJopEu7fbL5AEAlBPgWHXwu94VnCSG00gVzRfExyg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.2.0.tgz", + "integrity": "sha512-nEDASdbKFKPXN2O6lOlTgrEEOO9NHIeO+HVvZnkqc8h5U9g3BIhWsvzFo+UcUbliMHvKNPD/zVxDrkP1Sbgp8Q==", "dependencies": { - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" } }, "node_modules/@smithy/is-array-buffer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.1.1.tgz", - "integrity": "sha512-xozSQrcUinPpNPNPds4S7z/FakDTh1MZWtRP/2vQtYB/u3HYrX2UXuZs+VhaKBd6Vc7g2XPr2ZtwGBNDN6fNKQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "dependencies": { - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/middleware-content-length": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.1.3.tgz", - "integrity": "sha512-aJduhkC+dcXxdnv5ZpM3uMmtGmVFKx412R1gbeykS5HXDmRU6oSsyy2SoHENCkfOGKAQOjVE2WVqDJibC0d21g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.2.0.tgz", + "integrity": "sha512-5bl2LG1Ah/7E5cMSC+q+h3IpVHMeOkG0yLRyQT1p2aMJkSrZG7RlXHPuAgb7EyaFeidKEnnd/fNaLLaKlHGzDQ==", "dependencies": { - "@smithy/protocol-http": "^3.2.1", - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/middleware-endpoint": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.4.4.tgz", - "integrity": "sha512-4yjHyHK2Jul4JUDBo2sTsWY9UshYUnXeb/TAK/MTaPEb8XQvDmpwSFnfIRDU45RY1a6iC9LCnmJNg/yHyfxqkw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.5.1.tgz", + "integrity": "sha512-1/8kFp6Fl4OsSIVTWHnNjLnTL8IqpIb/D3sTSczrKFnrE9VMNWxnrRKNvpUHOJ6zpGD5f62TPm7+17ilTJpiCQ==", "dependencies": { - "@smithy/middleware-serde": "^2.1.3", - "@smithy/node-config-provider": "^2.2.4", - "@smithy/shared-ini-file-loader": "^2.3.4", - "@smithy/types": "^2.10.1", - "@smithy/url-parser": "^2.1.3", - "@smithy/util-middleware": "^2.1.3", - "tslib": "^2.5.0" + "@smithy/middleware-serde": "^2.3.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "@smithy/url-parser": "^2.2.0", + "@smithy/util-middleware": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/middleware-retry": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.1.4.tgz", - "integrity": "sha512-Cyolv9YckZTPli1EkkaS39UklonxMd08VskiuMhURDjC0HHa/AD6aK/YoD21CHv9s0QLg0WMLvk9YeLTKkXaFQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.3.1.tgz", + "integrity": "sha512-P2bGufFpFdYcWvqpyqqmalRtwFUNUA8vHjJR5iGqbfR6mp65qKOLcUd6lTr4S9Gn/enynSrSf3p3FVgVAf6bXA==", "dependencies": { - "@smithy/node-config-provider": "^2.2.4", - "@smithy/protocol-http": "^3.2.1", - "@smithy/service-error-classification": "^2.1.3", - "@smithy/smithy-client": "^2.4.2", - "@smithy/types": "^2.10.1", - "@smithy/util-middleware": "^2.1.3", - "@smithy/util-retry": "^2.1.3", - "tslib": "^2.5.0", - "uuid": "^8.3.2" + "@smithy/node-config-provider": "^2.3.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/service-error-classification": "^2.1.5", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-retry": "^2.2.0", + "tslib": "^2.6.2", + "uuid": "^9.0.1" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/@smithy/middleware-retry/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@smithy/middleware-serde": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.1.3.tgz", - "integrity": "sha512-s76LId+TwASrHhUa9QS4k/zeXDUAuNuddKklQzRgumbzge5BftVXHXIqL4wQxKGLocPwfgAOXWx+HdWhQk9hTg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.3.0.tgz", + "integrity": "sha512-sIADe7ojwqTyvEQBe1nc/GXB9wdHhi9UwyX0lTyttmUWDJLP655ZYE1WngnNyXREme8I27KCaUhyhZWRXL0q7Q==", "dependencies": { - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/middleware-stack": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.1.3.tgz", - "integrity": "sha512-opMFufVQgvBSld/b7mD7OOEBxF6STyraVr1xel1j0abVILM8ALJvRoFbqSWHGmaDlRGIiV9Q5cGbWi0sdiEaLQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.2.0.tgz", + "integrity": "sha512-Qntc3jrtwwrsAC+X8wms8zhrTr0sFXnyEGhZd9sLtsJ/6gGQKFzNB+wWbOcpJd7BR8ThNCoKt76BuQahfMvpeA==", "dependencies": { - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/node-config-provider": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.2.4.tgz", - "integrity": "sha512-nqazHCp8r4KHSFhRQ+T0VEkeqvA0U+RhehBSr1gunUuNW3X7j0uDrWBxB2gE9eutzy6kE3Y7L+Dov/UXT871vg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.3.0.tgz", + "integrity": "sha512-0elK5/03a1JPWMDPaS726Iw6LpQg80gFut1tNpPfxFuChEEklo2yL823V94SpTZTxmKlXFtFgsP55uh3dErnIg==", "dependencies": { - "@smithy/property-provider": "^2.1.3", - "@smithy/shared-ini-file-loader": "^2.3.4", - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/property-provider": "^2.2.0", + "@smithy/shared-ini-file-loader": "^2.4.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/node-http-handler": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.4.1.tgz", - "integrity": "sha512-HCkb94soYhJMxPCa61wGKgmeKpJ3Gftx1XD6bcWEB2wMV1L9/SkQu/6/ysKBnbOzWRE01FGzwrTxucHypZ8rdg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.5.0.tgz", + "integrity": "sha512-mVGyPBzkkGQsPoxQUbxlEfRjrj6FPyA3u3u2VXGr9hT8wilsoQdZdvKpMBFMB8Crfhv5dNkKHIW0Yyuc7eABqA==", "dependencies": { - "@smithy/abort-controller": "^2.1.3", - "@smithy/protocol-http": "^3.2.1", - "@smithy/querystring-builder": "^2.1.3", - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/abort-controller": "^2.2.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/querystring-builder": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/property-provider": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.1.3.tgz", - "integrity": "sha512-bMz3se+ySKWNrgm7eIiQMa2HO/0fl2D0HvLAdg9pTMcpgp4SqOAh6bz7Ik6y7uQqSrk4rLjIKgbQ6yzYgGehCQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.2.0.tgz", + "integrity": "sha512-+xiil2lFhtTRzXkx8F053AV46QnIw6e7MV8od5Mi68E1ICOjCeCHw2XfLnDEUHnT9WGUIkwcqavXjfwuJbGlpg==", "dependencies": { - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/protocol-http": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.2.1.tgz", - "integrity": "sha512-KLrQkEw4yJCeAmAH7hctE8g9KwA7+H2nSJwxgwIxchbp/L0B5exTdOQi9D5HinPLlothoervGmhpYKelZ6AxIA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.3.0.tgz", + "integrity": "sha512-Xy5XK1AFWW2nlY/biWZXu6/krgbaf2dg0q492D8M5qthsnU2H+UgFeZLbM76FnH7s6RO/xhQRkj+T6KBO3JzgQ==", "dependencies": { - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/querystring-builder": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.1.3.tgz", - "integrity": "sha512-kFD3PnNqKELe6m9GRHQw/ftFFSZpnSeQD4qvgDB6BQN6hREHELSosVFUMPN4M3MDKN2jAwk35vXHLoDrNfKu0A==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.2.0.tgz", + "integrity": "sha512-L1kSeviUWL+emq3CUVSgdogoM/D9QMFaqxL/dd0X7PCNWmPXqt+ExtrBjqT0V7HLN03Vs9SuiLrG3zy3JGnE5A==", "dependencies": { - "@smithy/types": "^2.10.1", - "@smithy/util-uri-escape": "^2.1.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "@smithy/util-uri-escape": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/querystring-parser": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.1.3.tgz", - "integrity": "sha512-3+CWJoAqcBMR+yvz6D+Fc5VdoGFtfenW6wqSWATWajrRMGVwJGPT3Vy2eb2bnMktJc4HU4bpjeovFa566P3knQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.2.0.tgz", + "integrity": "sha512-BvHCDrKfbG5Yhbpj4vsbuPV2GgcpHiAkLeIlcA1LtfpMz3jrqizP1+OguSNSj1MwBHEiN+jwNisXLGdajGDQJA==", "dependencies": { - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/service-error-classification": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.3.tgz", - "integrity": "sha512-iUrpSsem97bbXHHT/v3s7vaq8IIeMo6P6cXdeYHrx0wOJpMeBGQF7CB0mbJSiTm3//iq3L55JiEm8rA7CTVI8A==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.5.tgz", + "integrity": "sha512-uBDTIBBEdAQryvHdc5W8sS5YX7RQzF683XrHePVdFmAgKiMofU15FLSM0/HU03hKTnazdNRFa0YHS7+ArwoUSQ==", "dependencies": { - "@smithy/types": "^2.10.1" + "@smithy/types": "^2.12.0" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.3.4.tgz", - "integrity": "sha512-CiZmPg9GeDKbKmJGEFvJBsJcFnh0AQRzOtQAzj1XEa8N/0/uSN/v1LYzgO7ry8hhO8+9KB7+DhSW0weqBra4Aw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.4.0.tgz", + "integrity": "sha512-WyujUJL8e1B6Z4PBfAqC/aGY1+C7T0w20Gih3yrvJSk97gpiVfB+y7c46T4Nunk+ZngLq0rOIdeVeIklk0R3OA==", "dependencies": { - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/signature-v4": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.1.3.tgz", - "integrity": "sha512-Jq4iPPdCmJojZTsPePn4r1ULShh6ONkokLuxp1Lnk4Sq7r7rJp4HlA1LbPBq4bD64TIzQezIpr1X+eh5NYkNxw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.3.0.tgz", + "integrity": "sha512-ui/NlpILU+6HAQBfJX8BBsDXuKSNrjTSuOYArRblcrErwKFutjrCNb/OExfVRyj9+26F9J+ZmfWT+fKWuDrH3Q==", "dependencies": { - "@smithy/eventstream-codec": "^2.1.3", - "@smithy/is-array-buffer": "^2.1.1", - "@smithy/types": "^2.10.1", - "@smithy/util-hex-encoding": "^2.1.1", - "@smithy/util-middleware": "^2.1.3", - "@smithy/util-uri-escape": "^2.1.1", - "@smithy/util-utf8": "^2.1.1", - "tslib": "^2.5.0" + "@smithy/is-array-buffer": "^2.2.0", + "@smithy/types": "^2.12.0", + "@smithy/util-hex-encoding": "^2.2.0", + "@smithy/util-middleware": "^2.2.0", + "@smithy/util-uri-escape": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/smithy-client": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.4.2.tgz", - "integrity": "sha512-ntAFYN51zu3N3mCd95YFcFi/8rmvm//uX+HnK24CRbI6k5Rjackn0JhgKz5zOx/tbNvOpgQIwhSX+1EvEsBLbA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.5.1.tgz", + "integrity": "sha512-jrbSQrYCho0yDaaf92qWgd+7nAeap5LtHTI51KXqmpIFCceKU3K9+vIVTUH72bOJngBMqa4kyu1VJhRcSrk/CQ==", "dependencies": { - "@smithy/middleware-endpoint": "^2.4.4", - "@smithy/middleware-stack": "^2.1.3", - "@smithy/protocol-http": "^3.2.1", - "@smithy/types": "^2.10.1", - "@smithy/util-stream": "^2.1.3", - "tslib": "^2.5.0" + "@smithy/middleware-endpoint": "^2.5.1", + "@smithy/middleware-stack": "^2.2.0", + "@smithy/protocol-http": "^3.3.0", + "@smithy/types": "^2.12.0", + "@smithy/util-stream": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/types": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.10.1.tgz", - "integrity": "sha512-hjQO+4ru4cQ58FluQvKKiyMsFg0A6iRpGm2kqdH8fniyNd2WyanoOsYJfMX/IFLuLxEoW6gnRkNZy1y6fUUhtA==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", "dependencies": { - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/url-parser": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.1.3.tgz", - "integrity": "sha512-X1NRA4WzK/ihgyzTpeGvI9Wn45y8HmqF4AZ/FazwAv8V203Ex+4lXqcYI70naX9ETqbqKVzFk88W6WJJzCggTQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.2.0.tgz", + "integrity": "sha512-hoA4zm61q1mNTpksiSWp2nEl1dt3j726HdRhiNgVJQMj7mLp7dprtF57mOB6JvEk/x9d2bsuL5hlqZbBuHQylQ==", "dependencies": { - "@smithy/querystring-parser": "^2.1.3", - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/querystring-parser": "^2.2.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" } }, "node_modules/@smithy/util-base64": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.1.1.tgz", - "integrity": "sha512-UfHVpY7qfF/MrgndI5PexSKVTxSZIdz9InghTFa49QOvuu9I52zLPLUHXvHpNuMb1iD2vmc6R+zbv/bdMipR/g==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.3.0.tgz", + "integrity": "sha512-s3+eVwNeJuXUwuMbusncZNViuhv2LjVJ1nMwTqSA0XAC7gjKhqqxRdJPhR8+YrkoZ9IiIbFk/yK6ACe/xlF+hw==", "dependencies": { - "@smithy/util-buffer-from": "^2.1.1", - "tslib": "^2.5.0" + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/util-body-length-browser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.1.1.tgz", - "integrity": "sha512-ekOGBLvs1VS2d1zM2ER4JEeBWAvIOUKeaFch29UjjJsxmZ/f0L3K3x0dEETgh3Q9bkZNHgT+rkdl/J/VUqSRag==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.2.0.tgz", + "integrity": "sha512-dtpw9uQP7W+n3vOtx0CfBD5EWd7EPdIdsQnWTDoFf77e3VUf05uA7R7TGipIo8e4WL2kuPdnsr3hMQn9ziYj5w==", "dependencies": { - "tslib": "^2.5.0" + "tslib": "^2.6.2" } }, "node_modules/@smithy/util-body-length-node": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.2.1.tgz", - "integrity": "sha512-/ggJG+ta3IDtpNVq4ktmEUtOkH1LW64RHB5B0hcr5ZaWBmo96UX2cIOVbjCqqDickTXqBWZ4ZO0APuaPrD7Abg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.3.0.tgz", + "integrity": "sha512-ITWT1Wqjubf2CJthb0BuT9+bpzBfXeMokH/AAa5EJQgbv9aPMVfnM76iFIZVFf50hYXGbtiV71BHAthNWd6+dw==", "dependencies": { - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/util-buffer-from": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.1.1.tgz", - "integrity": "sha512-clhNjbyfqIv9Md2Mg6FffGVrJxw7bgK7s3Iax36xnfVj6cg0fUG7I4RH0XgXJF8bxi+saY5HR21g2UPKSxVCXg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "dependencies": { - "@smithy/is-array-buffer": "^2.1.1", - "tslib": "^2.5.0" + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/util-config-provider": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.2.1.tgz", - "integrity": "sha512-50VL/tx9oYYcjJn/qKqNy7sCtpD0+s8XEBamIFo4mFFTclKMNp+rsnymD796uybjiIquB7VCB/DeafduL0y2kw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.3.0.tgz", + "integrity": "sha512-HZkzrRcuFN1k70RLqlNK4FnPXKOpkik1+4JaBoHNJn+RnJGYqaa3c5/+XtLOXhlKzlRgNvyaLieHTW2VwGN0VQ==", "dependencies": { - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.1.4.tgz", - "integrity": "sha512-J6XAVY+/g7jf03QMnvqPyU+8jqGrrtXoKWFVOS+n1sz0Lg8HjHJ1ANqaDN+KTTKZRZlvG8nU5ZrJOUL6VdwgcQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.2.1.tgz", + "integrity": "sha512-RtKW+8j8skk17SYowucwRUjeh4mCtnm5odCL0Lm2NtHQBsYKrNW0od9Rhopu9wF1gHMfHeWF7i90NwBz/U22Kw==", "dependencies": { - "@smithy/property-provider": "^2.1.3", - "@smithy/smithy-client": "^2.4.2", - "@smithy/types": "^2.10.1", + "@smithy/property-provider": "^2.2.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", "bowser": "^2.11.0", - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { "node": ">= 10.0.0" } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.2.3.tgz", - "integrity": "sha512-ttUISrv1uVOjTlDa3nznX33f0pthoUlP+4grhTvOzcLhzArx8qHB94/untGACOG3nlf8vU20nI2iWImfzoLkYA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.3.1.tgz", + "integrity": "sha512-vkMXHQ0BcLFysBMWgSBLSk3+leMpFSyyFj8zQtv5ZyUBx8/owVh1/pPEkzmW/DR/Gy/5c8vjLDD9gZjXNKbrpA==", "dependencies": { - "@smithy/config-resolver": "^2.1.4", - "@smithy/credential-provider-imds": "^2.2.4", - "@smithy/node-config-provider": "^2.2.4", - "@smithy/property-provider": "^2.1.3", - "@smithy/smithy-client": "^2.4.2", - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/config-resolver": "^2.2.0", + "@smithy/credential-provider-imds": "^2.3.0", + "@smithy/node-config-provider": "^2.3.0", + "@smithy/property-provider": "^2.2.0", + "@smithy/smithy-client": "^2.5.1", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">= 10.0.0" } }, "node_modules/@smithy/util-endpoints": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.1.4.tgz", - "integrity": "sha512-/qAeHmK5l4yQ4/bCIJ9p49wDe9rwWtOzhPHblu386fwPNT3pxmodgcs9jDCV52yK9b4rB8o9Sj31P/7Vzka1cg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.2.0.tgz", + "integrity": "sha512-BuDHv8zRjsE5zXd3PxFXFknzBG3owCpjq8G3FcsXW3CykYXuEqM3nTSsmLzw5q+T12ZYuDlVUZKBdpNbhVtlrQ==", "dependencies": { - "@smithy/node-config-provider": "^2.2.4", - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/node-config-provider": "^2.3.0", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@smithy/util-hex-encoding": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.1.1.tgz", - "integrity": "sha512-3UNdP2pkYUUBGEXzQI9ODTDK+Tcu1BlCyDBaRHwyxhA+8xLP8agEKQq4MGmpjqb4VQAjq9TwlCQX0kP6XDKYLg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.2.0.tgz", + "integrity": "sha512-7iKXR+/4TpLK194pVjKiasIyqMtTYJsgKgM242Y9uzt5dhHnUDvMNb+3xIhRJ9QhvqGii/5cRUt4fJn3dtXNHQ==", "dependencies": { - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/util-middleware": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.1.3.tgz", - "integrity": "sha512-/+2fm7AZ2ozl5h8wM++ZP0ovE9/tiUUAHIbCfGfb3Zd3+Dyk17WODPKXBeJ/TnK5U+x743QmA0xHzlSm8I/qhw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.2.0.tgz", + "integrity": "sha512-L1qpleXf9QD6LwLCJ5jddGkgWyuSvWBkJwWAZ6kFkdifdso+sk3L3O1HdmPvCdnCK3IS4qWyPxev01QMnfHSBw==", "dependencies": { - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/util-retry": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.1.3.tgz", - "integrity": "sha512-Kbvd+GEMuozbNUU3B89mb99tbufwREcyx2BOX0X2+qHjq6Gvsah8xSDDgxISDwcOHoDqUWO425F0Uc/QIRhYkg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.2.0.tgz", + "integrity": "sha512-q9+pAFPTfftHXRytmZ7GzLFFrEGavqapFc06XxzZFcSIGERXMerXxCitjOG1prVDR9QdjqotF40SWvbqcCpf8g==", "dependencies": { - "@smithy/service-error-classification": "^2.1.3", - "@smithy/types": "^2.10.1", - "tslib": "^2.5.0" + "@smithy/service-error-classification": "^2.1.5", + "@smithy/types": "^2.12.0", + "tslib": "^2.6.2" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@smithy/util-stream": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.1.3.tgz", - "integrity": "sha512-HvpEQbP8raTy9n86ZfXiAkf3ezp1c3qeeO//zGqwZdrfaoOpGKQgF2Sv1IqZp7wjhna7pvczWaGUHjcOPuQwKw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.2.0.tgz", + "integrity": "sha512-17faEXbYWIRst1aU9SvPZyMdWmqIrduZjVOqCPMIsWFNxs5yQQgFrJL6b2SdiCzyW9mJoDjFtgi53xx7EH+BXA==", "dependencies": { - "@smithy/fetch-http-handler": "^2.4.3", - "@smithy/node-http-handler": "^2.4.1", - "@smithy/types": "^2.10.1", - "@smithy/util-base64": "^2.1.1", - "@smithy/util-buffer-from": "^2.1.1", - "@smithy/util-hex-encoding": "^2.1.1", - "@smithy/util-utf8": "^2.1.1", - "tslib": "^2.5.0" + "@smithy/fetch-http-handler": "^2.5.0", + "@smithy/node-http-handler": "^2.5.0", + "@smithy/types": "^2.12.0", + "@smithy/util-base64": "^2.3.0", + "@smithy/util-buffer-from": "^2.2.0", + "@smithy/util-hex-encoding": "^2.2.0", + "@smithy/util-utf8": "^2.3.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/util-uri-escape": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.1.1.tgz", - "integrity": "sha512-saVzI1h6iRBUVSqtnlOnc9ssU09ypo7n+shdQ8hBTZno/9rZ3AuRYvoHInV57VF7Qn7B+pFJG7qTzFiHxWlWBw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.2.0.tgz", + "integrity": "sha512-jtmJMyt1xMD/d8OtbVJ2gFZOSKc+ueYJZPW20ULW1GOp/q/YIM0wNh+u8ZFao9UaIGz4WoPW8hC64qlWLIfoDA==", "dependencies": { - "tslib": "^2.5.0" + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@smithy/util-utf8": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.1.1.tgz", - "integrity": "sha512-BqTpzYEcUMDwAKr7/mVRUtHDhs6ZoXDi9NypMvMfOr/+u1NW7JgqodPDECiiLboEm6bobcPcECxzjtQh865e9A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "dependencies": { - "@smithy/util-buffer-from": "^2.1.1", - "tslib": "^2.5.0" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=14.0.0" diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index a4c3eea7bc..c218e952df 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -29,6 +29,7 @@ import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-se import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service"; import { TIdentityServiceFactory } from "@app/services/identity/identity-service"; import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; +import { TIdentityAwsIamAuthServiceFactory } from "@app/services/identity-aws-iam-auth/identity-aws-iam-auth-service"; import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service"; import { TIntegrationServiceFactory } from "@app/services/integration/integration-service"; @@ -112,6 +113,7 @@ declare module "fastify" { identityAccessToken: TIdentityAccessTokenServiceFactory; identityProject: TIdentityProjectServiceFactory; identityUa: TIdentityUaServiceFactory; + identityAwsIamAuth: TIdentityAwsIamAuthServiceFactory; secretApprovalPolicy: TSecretApprovalPolicyServiceFactory; secretApprovalRequest: TSecretApprovalRequestServiceFactory; secretRotation: TSecretRotationServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 8845c1d016..448a1110ff 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -44,6 +44,9 @@ import { TIdentityAccessTokens, TIdentityAccessTokensInsert, TIdentityAccessTokensUpdate, + TIdentityAwsIamAuths, + TIdentityAwsIamAuthsInsert, + TIdentityAwsIamAuthsUpdate, TIdentityOrgMemberships, TIdentityOrgMembershipsInsert, TIdentityOrgMembershipsUpdate, @@ -311,6 +314,11 @@ declare module "knex/types/tables" { TIdentityUniversalAuthsInsert, TIdentityUniversalAuthsUpdate >; + [TableName.IdentityAwsIamAuth]: Knex.CompositeTableType< + TIdentityAwsIamAuths, + TIdentityAwsIamAuthsInsert, + TIdentityAwsIamAuthsUpdate + >; [TableName.IdentityUaClientSecret]: Knex.CompositeTableType< TIdentityUaClientSecrets, TIdentityUaClientSecretsInsert, diff --git a/backend/src/db/migrations/20240502044531_identity-aws-iam-auth.ts b/backend/src/db/migrations/20240502044531_identity-aws-iam-auth.ts new file mode 100644 index 0000000000..0728d4f286 --- /dev/null +++ b/backend/src/db/migrations/20240502044531_identity-aws-iam-auth.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.IdentityAwsIamAuth))) { + await knex.schema.createTable(TableName.IdentityAwsIamAuth, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable(); + t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable(); + t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable(); + t.jsonb("accessTokenTrustedIps").notNullable(); + t.timestamps(true, true, true); + t.uuid("identityId").notNullable().unique(); + t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + t.string("stsEndpoint").notNullable(); + t.string("allowedPrincipalArns").notNullable(); + t.string("allowedAccountIds").notNullable(); + }); + } + + await createOnUpdateTrigger(knex, TableName.IdentityAwsIamAuth); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.IdentityAwsIamAuth); + await dropOnUpdateTrigger(knex, TableName.IdentityAwsIamAuth); +} diff --git a/backend/src/db/schemas/identity-aws-iam-auths.ts b/backend/src/db/schemas/identity-aws-iam-auths.ts new file mode 100644 index 0000000000..8912c71b39 --- /dev/null +++ b/backend/src/db/schemas/identity-aws-iam-auths.ts @@ -0,0 +1,26 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const IdentityAwsIamAuthsSchema = z.object({ + id: z.string().uuid(), + accessTokenTTL: z.coerce.number().default(7200), + accessTokenMaxTTL: z.coerce.number().default(7200), + accessTokenNumUsesLimit: z.coerce.number().default(0), + accessTokenTrustedIps: z.unknown(), + createdAt: z.date(), + updatedAt: z.date(), + identityId: z.string().uuid(), + stsEndpoint: z.string(), + allowedPrincipalArns: z.string(), + allowedAccountIds: z.string() +}); + +export type TIdentityAwsIamAuths = z.infer; +export type TIdentityAwsIamAuthsInsert = Omit, TImmutableDBKeys>; +export type TIdentityAwsIamAuthsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 30d6208b8d..83b1afb71f 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -12,6 +12,7 @@ export * from "./group-project-memberships"; export * from "./groups"; export * from "./identities"; export * from "./identity-access-tokens"; +export * from "./identity-aws-iam-auths"; export * from "./identity-org-memberships"; export * from "./identity-project-additional-privilege"; export * from "./identity-project-membership-role"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index ea70dccdb1..74c2799f54 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -45,6 +45,7 @@ export enum TableName { IdentityAccessToken = "identity_access_tokens", IdentityUniversalAuth = "identity_universal_auths", IdentityUaClientSecret = "identity_ua_client_secrets", + IdentityAwsIamAuth = "identity_aws_iam_auths", IdentityOrgMembership = "identity_org_memberships", IdentityProjectMembership = "identity_project_memberships", IdentityProjectMembershipRole = "identity_project_membership_role", @@ -137,5 +138,6 @@ export enum ProjectUpgradeStatus { } export enum IdentityAuthMethod { - Univeral = "universal-auth" + Univeral = "universal-auth", + AWS_IAM_AUTH = "aws-iam-auth" } diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 220c250027..1e79984e48 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -66,6 +66,10 @@ export enum EventType { CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret", REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret", GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret", + LOGIN_IDENTITY_AWS_IAM_AUTH = "login-identity-aws-iam-auth", + ADD_IDENTITY_AWS_IAM_AUTH = "add-identity-aws-iam-auth", + UPDATE_IDENTITY_AWS_IAM_AUTH = "update-identity-aws-iam-auth", + GET_IDENTITY_AWS_IAM_AUTH = "get-identity-aws-iam-auth", CREATE_ENVIRONMENT = "create-environment", UPDATE_ENVIRONMENT = "update-environment", DELETE_ENVIRONMENT = "delete-environment", @@ -406,6 +410,50 @@ interface RevokeIdentityUniversalAuthClientSecretEvent { }; } +interface LoginIdentityAwsIamAuthEvent { + type: EventType.LOGIN_IDENTITY_AWS_IAM_AUTH; + metadata: { + identityId: string; + identityAwsIamAuthId: string; + identityAccessTokenId: string; + }; +} + +interface AddIdentityAwsIamAuthEvent { + type: EventType.ADD_IDENTITY_AWS_IAM_AUTH; + metadata: { + identityId: string; + stsEndpoint: string; + allowedPrincipalArns: string; + allowedAccountIds: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: Array; + }; +} + +interface UpdateIdentityAwsIamAuthEvent { + type: EventType.UPDATE_IDENTITY_AWS_IAM_AUTH; + metadata: { + identityId: string; + stsEndpoint?: string; + allowedPrincipalArns?: string; + allowedAccountIds?: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: Array; + }; +} + +interface GetIdentityAwsIamAuthEvent { + type: EventType.GET_IDENTITY_AWS_IAM_AUTH; + metadata: { + identityId: string; + }; +} + interface CreateEnvironmentEvent { type: EventType.CREATE_ENVIRONMENT; metadata: { @@ -660,6 +708,10 @@ export type Event = | CreateIdentityUniversalAuthClientSecretEvent | GetIdentityUniversalAuthClientSecretsEvent | RevokeIdentityUniversalAuthClientSecretEvent + | LoginIdentityAwsIamAuthEvent + | AddIdentityAwsIamAuthEvent + | UpdateIdentityAwsIamAuthEvent + | GetIdentityAwsIamAuthEvent | CreateEnvironmentEvent | UpdateEnvironmentEvent | DeleteEnvironmentEvent diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 04b7509cad..8fa20f9202 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -92,6 +92,18 @@ export const UNIVERSAL_AUTH = { } } as const; +export const AWS_IAM_AUTH = { + LOGIN: { + identityId: "The ID of the identity to login.", + iamHttpRequestMethod: "The HTTP request method used in the signed request.", + iamRequestUrl: + "The base64-encoded HTTP URL used in the signed request. Most likely, the base64-encoding of https://sts.amazonaws.com/", + iamRequestBody: + "The base64-encoded body of the signed request. Most likely, the base64-encoding of Action=GetCallerIdentity&Version=2011-06-15.", + iamRequestHeaders: "The base64-encoded headers of the sts:GetCallerIdentity signed request." + } +} as const; + export const ORGANIZATIONS = { LIST_USER_MEMBERSHIPS: { organizationId: "The ID of the organization to get memberships from." diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 4cb56a2226..417bb05f1a 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -70,6 +70,8 @@ import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal"; import { identityServiceFactory } from "@app/services/identity/identity-service"; import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal"; import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; +import { identityAwsIamAuthDALFactory } from "@app/services/identity-aws-iam-auth/identity-aws-iam-auth-dal"; +import { identityAwsIamAuthServiceFactory } from "@app/services/identity-aws-iam-auth/identity-aws-iam-auth-service"; import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal"; import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service"; @@ -191,6 +193,7 @@ export const registerRoutes = async ( const identityUaDAL = identityUaDALFactory(db); const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db); + const identityAwsIamAuthDAL = identityAwsIamAuthDALFactory(db); const auditLogDAL = auditLogDALFactory(db); const trustedIpDAL = trustedIpDALFactory(db); @@ -637,6 +640,14 @@ export const registerRoutes = async ( identityUaDAL, licenseService }); + const identityAWSIAMAuthService = identityAwsIamAuthServiceFactory({ + identityAccessTokenDAL, + identityAwsIamAuthDAL, + identityOrgMembershipDAL, + identityDAL, + licenseService, + permissionService + }); const dynamicSecretProviders = buildDynamicSecretProviders(); const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({ @@ -706,6 +717,7 @@ export const registerRoutes = async ( identityAccessToken: identityAccessTokenService, identityProject: identityProjectService, identityUa: identityUaService, + identityAwsIamAuth: identityAWSIAMAuthService, secretApprovalPolicy: sapService, secretApprovalRequest: sarService, secretRotation: secretRotationService, diff --git a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts new file mode 100644 index 0000000000..0245b7607f --- /dev/null +++ b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts @@ -0,0 +1,269 @@ +import { z } from "zod"; + +import { IdentityAwsIamAuthsSchema } from "@app/db/schemas"; +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { AWS_IAM_AUTH } from "@app/lib/api-docs"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; +import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; +import { + validateAccountIds, + validatePrincipalArns +} from "@app/services/identity-aws-iam-auth/identity-aws-iam-auth-validators"; + +export const registerIdentityAwsIamAuthRouter = async (server: FastifyZodProvider) => { + server.route({ + method: "POST", + url: "/aws-iam-auth/login", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Login with Universal Auth", + body: z.object({ + identityId: z.string().describe(AWS_IAM_AUTH.LOGIN.identityId), + iamHttpRequestMethod: z.string().default("POST").describe(AWS_IAM_AUTH.LOGIN.iamHttpRequestMethod), + iamRequestBody: z.string().describe(AWS_IAM_AUTH.LOGIN.iamRequestBody), + iamRequestHeaders: z.string().describe(AWS_IAM_AUTH.LOGIN.iamRequestHeaders) + }), + response: { + 200: z.object({ + accessToken: z.string(), + expiresIn: z.coerce.number(), + accessTokenMaxTTL: z.coerce.number(), + tokenType: z.literal("Bearer") + }) + } + }, + handler: async (req) => { + const { identityAwsIamAuth, accessToken, identityAccessToken, identityMembershipOrg } = + await server.services.identityAwsIamAuth.login(req.body); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityMembershipOrg?.orgId, + event: { + type: EventType.LOGIN_IDENTITY_AWS_IAM_AUTH, + metadata: { + identityId: identityAwsIamAuth.identityId, + identityAccessTokenId: identityAccessToken.id, + identityAwsIamAuthId: identityAwsIamAuth.id + } + } + }); + + return { + accessToken, + tokenType: "Bearer" as const, + expiresIn: identityAwsIamAuth.accessTokenTTL, + accessTokenMaxTTL: identityAwsIamAuth.accessTokenMaxTTL + }; + } + }); + + server.route({ + method: "POST", + url: "/aws-iam-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Attach AWS IAM Auth configuration onto identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string().trim() + }), + body: z.object({ + stsEndpoint: z.string().trim().min(1).default("https://sts.amazonaws.com/"), + allowedPrincipalArns: validatePrincipalArns, + allowedAccountIds: validateAccountIds, + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]), + accessTokenTTL: z + .number() + .int() + .min(1) + .refine((value) => value !== 0, { + message: "accessTokenTTL must have a non zero number" + }) + .default(2592000), + accessTokenMaxTTL: z + .number() + .int() + .refine((value) => value !== 0, { + message: "accessTokenMaxTTL must have a non zero number" + }) + .default(2592000), + accessTokenNumUsesLimit: z.number().int().min(0).default(0) + }), + response: { + 200: z.object({ + identityAwsIamAuth: IdentityAwsIamAuthsSchema + }) + } + }, + handler: async (req) => { + const identityAwsIamAuth = await server.services.identityAwsIamAuth.attachAwsIamAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityAwsIamAuth.orgId, + event: { + type: EventType.ADD_IDENTITY_AWS_IAM_AUTH, + metadata: { + identityId: identityAwsIamAuth.identityId, + stsEndpoint: identityAwsIamAuth.stsEndpoint, + allowedPrincipalArns: identityAwsIamAuth.allowedPrincipalArns, + allowedAccountIds: identityAwsIamAuth.allowedAccountIds, + accessTokenTTL: identityAwsIamAuth.accessTokenTTL, + accessTokenMaxTTL: identityAwsIamAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityAwsIamAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityAwsIamAuth.accessTokenNumUsesLimit + } + } + }); + + return { identityAwsIamAuth }; + } + }); + + server.route({ + method: "PATCH", + url: "/aws-iam-auth/identities/:identityId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Update AWS IAM Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string() + }), + body: z.object({ + stsEndpoint: z.string().trim().min(1).optional(), + allowedPrincipalArns: validatePrincipalArns, + allowedAccountIds: validateAccountIds, + accessTokenTrustedIps: z + .object({ + ipAddress: z.string().trim() + }) + .array() + .min(1) + .optional(), + accessTokenTTL: z.number().int().min(0).optional(), + accessTokenNumUsesLimit: z.number().int().min(0).optional(), + accessTokenMaxTTL: z + .number() + .int() + .refine((value) => value !== 0, { + message: "accessTokenMaxTTL must have a non zero number" + }) + .optional() + }), + response: { + 200: z.object({ + identityAwsIamAuth: IdentityAwsIamAuthsSchema + }) + } + }, + handler: async (req) => { + const identityAwsIamAuth = await server.services.identityAwsIamAuth.updateAwsIamAuth({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body, + identityId: req.params.identityId + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityAwsIamAuth.orgId, + event: { + type: EventType.UPDATE_IDENTITY_AWS_IAM_AUTH, + metadata: { + identityId: identityAwsIamAuth.identityId, + stsEndpoint: identityAwsIamAuth.stsEndpoint, + allowedPrincipalArns: identityAwsIamAuth.allowedPrincipalArns, + allowedAccountIds: identityAwsIamAuth.allowedAccountIds, + accessTokenTTL: identityAwsIamAuth.accessTokenTTL, + accessTokenMaxTTL: identityAwsIamAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityAwsIamAuth.accessTokenTrustedIps as TIdentityTrustedIp[], + accessTokenNumUsesLimit: identityAwsIamAuth.accessTokenNumUsesLimit + } + } + }); + + return { identityAwsIamAuth }; + } + }); + + server.route({ + method: "GET", + url: "/aws-iam-auth/identities/:identityId", + config: { + rateLimit: readLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Retrieve AWS IAM Auth configuration on identity", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + identityId: z.string() + }), + response: { + 200: z.object({ + identityAwsIamAuth: IdentityAwsIamAuthsSchema + }) + } + }, + handler: async (req) => { + const identityAwsIamAuth = await server.services.identityAwsIamAuth.getAwsIamAuth({ + identityId: req.params.identityId, + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod + }); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: identityAwsIamAuth.orgId, + event: { + type: EventType.GET_IDENTITY_AWS_IAM_AUTH, + metadata: { + identityId: identityAwsIamAuth.identityId + } + } + }); + return { identityAwsIamAuth }; + } + }); +}; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index fbc68d974a..4fbaabdfce 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -2,6 +2,7 @@ import { registerAdminRouter } from "./admin-router"; import { registerAuthRoutes } from "./auth-router"; import { registerProjectBotRouter } from "./bot-router"; import { registerIdentityAccessTokenRouter } from "./identity-access-token-router"; +import { registerIdentityAwsIamAuthRouter } from "./identity-aws-iam-auth-router"; import { registerIdentityRouter } from "./identity-router"; import { registerIdentityUaRouter } from "./identity-ua"; import { registerIntegrationAuthRouter } from "./integration-auth-router"; @@ -28,6 +29,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await authRouter.register(registerAuthRoutes); await authRouter.register(registerIdentityUaRouter); await authRouter.register(registerIdentityAccessTokenRouter); + await authRouter.register(registerIdentityAwsIamAuthRouter); }, { prefix: "/auth" } ); diff --git a/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-dal.ts b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-dal.ts new file mode 100644 index 0000000000..584ac775fb --- /dev/null +++ b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TIdentityAwsIamAuthDALFactory = ReturnType; + +export const identityAwsIamAuthDALFactory = (db: TDbClient) => { + const awsIamAuthOrm = ormify(db, TableName.IdentityAwsIamAuth); + + return awsIamAuthOrm; +}; diff --git a/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-service.ts b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-service.ts new file mode 100644 index 0000000000..66dc2c21a0 --- /dev/null +++ b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-service.ts @@ -0,0 +1,315 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { ForbiddenError } from "@casl/ability"; +import axios from "axios"; +import jwt from "jsonwebtoken"; + +import { IdentityAuthMethod } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; +import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { getConfig } from "@app/lib/config/env"; +import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; + +import { AuthTokenType } from "../auth/auth-type"; +import { TIdentityDALFactory } from "../identity/identity-dal"; +import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; +import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; +import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; +import { extractPrincipalArn } from "./identity-aws-iam-auth.fns"; +import { TIdentityAwsIamAuthDALFactory } from "./identity-aws-iam-auth-dal"; +import { + TAttachAWSIAMAuthDTO, + TAWSGetCallerIdentityHeaders, + TGetAWSIAMAuthDTO, + TGetCallerIdentityResponse, + TLoginAWSIAMAuthDTO, + TUpdateAWSIAMAuthDTO +} from "./identity-aws-iam-auth-types"; + +type TIdentityAwsIamAuthServiceFactoryDep = { + identityAccessTokenDAL: Pick; + identityAwsIamAuthDAL: Pick; + identityOrgMembershipDAL: Pick; + identityDAL: Pick; + licenseService: Pick; + permissionService: Pick; +}; + +export type TIdentityAwsIamAuthServiceFactory = ReturnType; + +export const identityAwsIamAuthServiceFactory = ({ + identityAccessTokenDAL, + identityAwsIamAuthDAL, + identityOrgMembershipDAL, + identityDAL, + licenseService, + permissionService +}: TIdentityAwsIamAuthServiceFactoryDep) => { + const login = async ({ + identityId, + iamHttpRequestMethod, + iamRequestBody, + iamRequestHeaders + }: TLoginAWSIAMAuthDTO) => { + const identityAwsIamAuth = await identityAwsIamAuthDAL.findOne({ identityId }); + if (!identityAwsIamAuth) throw new UnauthorizedError(); + + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityAwsIamAuth.identityId }); + + const headers: TAWSGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString()); + const body: string = Buffer.from(iamRequestBody, "base64").toString(); + + const { + data: { + GetCallerIdentityResponse: { + GetCallerIdentityResult: { Account, Arn } + } + } + }: { data: TGetCallerIdentityResponse } = await axios({ + method: iamHttpRequestMethod, + url: identityAwsIamAuth.stsEndpoint, + headers, + data: body + }); + + if (identityAwsIamAuth.allowedAccountIds) { + // validate if Account is in the list of allowed Account IDs + + const isAccountAllowed = identityAwsIamAuth.allowedAccountIds + .split(",") + .map((accountId) => accountId.trim()) + .some((accountId) => accountId === Account); + + if (!isAccountAllowed) throw new UnauthorizedError(); + } + + if (identityAwsIamAuth.allowedPrincipalArns) { + // validate if Arn is in the list of allowed Principal ARNs + + const isArnAllowed = identityAwsIamAuth.allowedPrincipalArns + .split(",") + .map((principalArn) => principalArn.trim()) + .some((principalArn) => { + // convert wildcard ARN to a regular expression: "arn:aws:iam::123456789012:*" -> "^arn:aws:iam::123456789012:.*$" + // considers exact matches + wildcard matches + const regex = new RegExp(`^${principalArn.replace(/\*/g, ".*")}$`); + return regex.test(extractPrincipalArn(Arn)); + }); + + if (!isArnAllowed) throw new UnauthorizedError(); + } + + const identityAccessToken = await identityAwsIamAuthDAL.transaction(async (tx) => { + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityAwsIamAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityAwsIamAuth.accessTokenTTL, + accessTokenMaxTTL: identityAwsIamAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityAwsIamAuth.accessTokenNumUsesLimit + }, + tx + ); + return newToken; + }); + + const appCfg = getConfig(); + const accessToken = jwt.sign( + { + identityId: identityAwsIamAuth.identityId, + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + { + expiresIn: + Number(identityAccessToken.accessTokenMaxTTL) === 0 + ? undefined + : Number(identityAccessToken.accessTokenMaxTTL) + } + ); + + return { accessToken, identityAwsIamAuth, identityAccessToken, identityMembershipOrg }; + }; + + const attachAwsIamAuth = async ({ + identityId, + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TAttachAWSIAMAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity.authMethod) + throw new BadRequestError({ + message: "Failed to add AWS IAM Auth to already configured identity" + }); + + if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { + throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity); + + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { + if ( + !plan.ipAllowlisting && + accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" && + accessTokenTrustedIp.ipAddress !== "::/0" + ) + throw new BadRequestError({ + message: + "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range." + }); + if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress)) + throw new BadRequestError({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + + const identityAwsIamAuth = await identityAwsIamAuthDAL.transaction(async (tx) => { + const doc = await identityAwsIamAuthDAL.create( + { + identityId: identityMembershipOrg.identityId, + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps) + }, + tx + ); + await identityDAL.updateById( + identityMembershipOrg.identityId, + { + authMethod: IdentityAuthMethod.AWS_IAM_AUTH + }, + tx + ); + return doc; + }); + return { ...identityAwsIamAuth, orgId: identityMembershipOrg.orgId }; + }; + + const updateAwsIamAuth = async ({ + identityId, + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TUpdateAWSIAMAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AWS_IAM_AUTH) + throw new BadRequestError({ + message: "Failed to update AWS IAM Auth" + }); + + const identityAwsIamAuth = await identityAwsIamAuthDAL.findOne({ identityId }); + + if ( + (accessTokenMaxTTL || identityAwsIamAuth.accessTokenMaxTTL) > 0 && + (accessTokenTTL || identityAwsIamAuth.accessTokenMaxTTL) > + (accessTokenMaxTTL || identityAwsIamAuth.accessTokenMaxTTL) + ) { + throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }); + } + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity); + + const plan = await licenseService.getPlan(identityMembershipOrg.orgId); + const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => { + if ( + !plan.ipAllowlisting && + accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" && + accessTokenTrustedIp.ipAddress !== "::/0" + ) + throw new BadRequestError({ + message: + "Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range." + }); + if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress)) + throw new BadRequestError({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + + const updatedAwsIamAuth = await identityAwsIamAuthDAL.updateById(identityAwsIamAuth.id, { + stsEndpoint, + allowedPrincipalArns, + allowedAccountIds, + accessTokenMaxTTL, + accessTokenTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps + ? JSON.stringify(reformattedAccessTokenTrustedIps) + : undefined + }); + + return { ...updatedAwsIamAuth, orgId: identityMembershipOrg.orgId }; + }; + + const getAwsIamAuth = async ({ identityId, actorId, actor, actorAuthMethod, actorOrgId }: TGetAWSIAMAuthDTO) => { + const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId }); + if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" }); + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.AWS_IAM_AUTH) + throw new BadRequestError({ + message: "The identity does not have AWS IAM Auth attached" + }); + + const awsIamIdentityAuth = await identityAwsIamAuthDAL.findOne({ identityId }); + + const { permission } = await permissionService.getOrgPermission( + actor, + actorId, + identityMembershipOrg.orgId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); + return { ...awsIamIdentityAuth, orgId: identityMembershipOrg.orgId }; + }; + + return { + login, + attachAwsIamAuth, + updateAwsIamAuth, + getAwsIamAuth + }; +}; diff --git a/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-types.ts b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-types.ts new file mode 100644 index 0000000000..19f27f4302 --- /dev/null +++ b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-types.ts @@ -0,0 +1,54 @@ +import { TProjectPermission } from "@app/lib/types"; + +export type TLoginAWSIAMAuthDTO = { + identityId: string; + iamHttpRequestMethod: string; + iamRequestBody: string; + iamRequestHeaders: string; +}; + +export type TAttachAWSIAMAuthDTO = { + identityId: string; + stsEndpoint: string; + allowedPrincipalArns: string; + allowedAccountIds: string; + accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; + accessTokenTrustedIps: { ipAddress: string }[]; +} & Omit; + +export type TUpdateAWSIAMAuthDTO = { + identityId: string; + stsEndpoint?: string; + allowedPrincipalArns?: string; + allowedAccountIds?: string; + accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; + accessTokenTrustedIps?: { ipAddress: string }[]; +} & Omit; + +export type TGetAWSIAMAuthDTO = { + identityId: string; +} & Omit; + +export type TAWSGetCallerIdentityHeaders = { + "Content-Type": string; + Host: string; + "X-Amz-Date": string; + "Content-Length": number; + "x-amz-security-token": string; + Authorization: string; +}; + +export type TGetCallerIdentityResponse = { + GetCallerIdentityResponse: { + GetCallerIdentityResult: { + Account: string; + Arn: string; + UserId: string; + }; + ResponseMetadata: { RequestId: string }; + }; +}; diff --git a/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-validators.ts b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-validators.ts new file mode 100644 index 0000000000..35fb566749 --- /dev/null +++ b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-validators.ts @@ -0,0 +1,58 @@ +import { z } from "zod"; + +const twelveDigitRegex = /^\d{12}$/; +const arnRegex = /^arn:aws:iam::\d{12}:(user\/[A-Za-z0-9]+|role\/[A-Za-z0-9]+|\*)$/; + +export const validateAccountIds = z + .string() + .trim() + .default("") + // Custom validation to ensure each part is a 12-digit number + .refine( + (data) => { + if (data === "") return true; + // Split the string by commas to check each supposed number + const accountIds = data.split(",").map((id) => id.trim()); + // Return true only if every item matches the 12-digit requirement + return accountIds.every((id) => twelveDigitRegex.test(id)); + }, + { + message: "Each account ID must be a 12-digit number." + } + ) + // Transform the string to normalize space after commas + .transform((data) => { + if (data === "") return ""; + // Trim each ID and join with ', ' to ensure formatting + return data + .split(",") + .map((id) => id.trim()) + .join(", "); + }); + +export const validatePrincipalArns = z + .string() + .trim() + .default("") + // Custom validation for ARN format + .refine( + (data) => { + // Skip validation if the string is empty + if (data === "") return true; + // Split the string by commas to check each supposed ARN + const arns = data.split(","); + // Return true only if every item matches one of the allowed ARN formats + return arns.every((arn) => arnRegex.test(arn.trim())); + }, + { + message: + "Each ARN must be in the format of 'arn:aws:iam::123456789012:user/UserName', 'arn:aws:iam::123456789012:role/RoleName', or 'arn:aws:iam::123456789012:*'." + } + ) + // Transform to normalize the spaces around commas + .transform((data) => + data + .split(",") + .map((arn) => arn.trim()) + .join(", ") + ); diff --git a/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth.fns.ts b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth.fns.ts new file mode 100644 index 0000000000..517e9f6131 --- /dev/null +++ b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth.fns.ts @@ -0,0 +1,67 @@ +/** + * Extracts the identity ARN from the GetCallerIdentity response to one of the following formats: + * - arn:aws:iam::123456789012:user/MyUserName + * - arn:aws:iam::123456789012:role/MyRoleName + */ +export const extractPrincipalArn = (arn: string) => { + // split the ARN into parts using ":" as the delimiter + const fullParts = arn.split(":"); + if (fullParts.length !== 6) { + throw new Error(`Unrecognized ARN: contains ${fullParts.length} colon-separated parts, expected 6`); + } + const [prefix, partition, service, , accountNumber, resource] = fullParts; + if (prefix !== "arn") { + throw new Error('Unrecognized ARN: does not begin with "arn:"'); + } + + // structure to hold the parsed data + const entity = { + Partition: partition, + Service: service, + AccountNumber: accountNumber, + Type: "", + Path: "", + FriendlyName: "", + SessionInfo: "" + }; + + // validate the service is either 'iam' or 'sts' + if (entity.Service !== "iam" && entity.Service !== "sts") { + throw new Error(`Unrecognized service: ${entity.Service}, not one of iam or sts`); + } + + // parse the last part of the ARN which describes the resource + const parts = resource.split("/"); + if (parts.length < 2) { + throw new Error(`Unrecognized ARN: "${resource}" contains fewer than 2 slash-separated parts`); + } + + const [type, ...rest] = parts; + entity.Type = type; + entity.FriendlyName = parts[parts.length - 1]; + + // handle different types of resources + switch (entity.Type) { + case "assumed-role": { + if (rest.length < 2) { + throw new Error(`Unrecognized ARN: "${resource}" contains fewer than 3 slash-separated parts`); + } + // assumed roles use a special format where the friendly name is the role name + const [roleName, sessionId] = rest; + entity.Type = "role"; // treat assumed role case as role + entity.FriendlyName = roleName; + entity.SessionInfo = sessionId; + break; + } + case "user": + case "role": + case "instance-profile": + // standard cases: just join back the path if there's any + entity.Path = rest.slice(0, -1).join("/"); + break; + default: + throw new Error(`Unrecognized principal type: "${entity.Type}"`); + } + + return `arn:aws:iam::${entity.AccountNumber}:${entity.Type}/${entity.FriendlyName}`; +}; diff --git a/docs/documentation/platform/identities/aws-iam-auth.mdx b/docs/documentation/platform/identities/aws-iam-auth.mdx new file mode 100644 index 0000000000..1213b82d01 --- /dev/null +++ b/docs/documentation/platform/identities/aws-iam-auth.mdx @@ -0,0 +1,12 @@ +--- +title: AWS IAM Auth +description: "Learn how to authenticate with Infisical from AWS." +--- + +**AWS IAM Auth** is an AWS-native authentication method that can be configured for a [machine identity](/documentation/platform/identities/machine-identities) to access Infisical from AWS. + +In this method, each identity is configured to represent one or more IAM principals in AWS with shared privileges. + +## Workflow + +TODO diff --git a/docs/documentation/platform/identities/universal-auth.mdx b/docs/documentation/platform/identities/universal-auth.mdx index a9f4dffae3..474d24476b 100644 --- a/docs/documentation/platform/identities/universal-auth.mdx +++ b/docs/documentation/platform/identities/universal-auth.mdx @@ -27,18 +27,18 @@ using the Universal Auth authentication method. ![identities organization](/images/platform/identities/identities-org.png) When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles. - + ![identities organization create](/images/platform/identities/identities-org-create.png) Now input a few details for your new identity. Here's some guidance for each field: - Name (required): A friendly name for the identity. - Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to. - + Once you've created an identity, you'll be prompted to configure the **Universal Auth** authentication method for it. - + ![identities organization create auth method](/images/platform/identities/identities-org-create-auth-method.png) - + Here's some more guidance on each field: - Access Token TTL (default is `2592000` equivalent to 30 days): The lifetime for an acccess token in seconds. This value will be referenced at renewal time. @@ -78,8 +78,9 @@ using the Universal Auth authentication method. Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to. ![identities project](/images/platform/identities/identities-project.png) - + ![identities project create](/images/platform/identities/identities-project-create.png) + To access the Infisical API as the identity, you should first perform a login operation @@ -94,9 +95,9 @@ using the Universal Auth authentication method. --data-urlencode 'clientSecret=...' \ --data-urlencode 'clientId=...' ``` - + #### Sample response - + ``` { "accessToken": "...", @@ -107,7 +108,7 @@ using the Universal Auth authentication method. ``` Next, you can use the access token to authenticate with the [Infisical API](/api-reference/overview/introduction) - + Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation; the default TTL is `7200` seconds which can be adjusted. @@ -115,6 +116,7 @@ using the Universal Auth authentication method. If an identity access token expires, it can no longer authenticate with the Infisical API. In this case, a new access token should be obtained by performing another login operation. + @@ -134,7 +136,8 @@ using the Universal Auth authentication method. In certain cases, you may want to extend the lifespan of an access token; to do so, you must set a max TTL parameter. - A token can be renewed any number of time and each call to renew it will extend the toke life by increments of access token TTL. - Regardless of how frequently an access token is renewed, its lifespan remains bound to the maximum TTL determined at its creation +A token can be renewed any number of time and each call to renew it will extend the toke life by increments of access token TTL. +Regardless of how frequently an access token is renewed, its lifespan remains bound to the maximum TTL determined at its creation + - \ No newline at end of file + diff --git a/docs/mint.json b/docs/mint.json index 26bf3ac5b0..8ca1709894 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -152,6 +152,7 @@ "documentation/platform/auth-methods/email-password", "documentation/platform/token", "documentation/platform/identities/universal-auth", + "documentation/platform/identities/aws-iam-auth", "documentation/platform/mfa", { "group": "SSO", @@ -210,9 +211,7 @@ }, { "group": "Reference architectures", - "pages": [ - "self-hosting/reference-architectures/aws-ecs" - ] + "pages": ["self-hosting/reference-architectures/aws-ecs"] }, "self-hosting/ee", "self-hosting/faq" From cbf8e041e9ddcce60c24b2935b39a9bf0d2ebb48 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 3 May 2024 17:20:44 -0700 Subject: [PATCH 004/188] Finish docs for AWS IAM Auth, update ARN regex --- .../identity-aws-iam-auth-validators.ts | 2 +- .../getting-started/introduction.mdx | 78 ++--- .../platform/identities/aws-iam-auth.mdx | 274 +++++++++++++++++- .../identities/machine-identities.mdx | 7 +- .../platform/identities/universal-auth.mdx | 21 +- ...ntities-org-create-aws-iam-auth-method.png | Bin 0 -> 550802 bytes .../IdentityAwsIamAuthForm.tsx | 6 +- 7 files changed, 334 insertions(+), 54 deletions(-) create mode 100644 docs/images/platform/identities/identities-org-create-aws-iam-auth-method.png diff --git a/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-validators.ts b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-validators.ts index 35fb566749..2cb7b4ea44 100644 --- a/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-validators.ts +++ b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-validators.ts @@ -1,7 +1,7 @@ import { z } from "zod"; const twelveDigitRegex = /^\d{12}$/; -const arnRegex = /^arn:aws:iam::\d{12}:(user\/[A-Za-z0-9]+|role\/[A-Za-z0-9]+|\*)$/; +const arnRegex = /^arn:aws:iam::\d{12}:(user\/[\w-]+|role\/[\w-]+|\*)$/; export const validateAccountIds = z .string() diff --git a/docs/documentation/getting-started/introduction.mdx b/docs/documentation/getting-started/introduction.mdx index 0f414c62a9..144691ac44 100644 --- a/docs/documentation/getting-started/introduction.mdx +++ b/docs/documentation/getting-started/introduction.mdx @@ -4,59 +4,66 @@ sidebarTitle: "What is Infisical?" description: "An Introduction to the Infisical secret management platform." --- -Infisical is an [open-source](https://github.com/infisical/infisical) secret management platform for developers. -It provides capabilities for storing, managing, and syncing application configuration and secrets like API keys, database -credentials, and certificates across infrastructure. In addition, Infisical prevents secrets leaks to git and enables secure +Infisical is an [open-source](https://github.com/infisical/infisical) secret management platform for developers. +It provides capabilities for storing, managing, and syncing application configuration and secrets like API keys, database +credentials, and certificates across infrastructure. In addition, Infisical prevents secrets leaks to git and enables secure sharing of secrets among engineers. Start managing secrets securely with [Infisical Cloud](https://app.infisical.com) or learn how to [host Infisical](/self-hosting/overview) yourself. - - Get started with Infisical Cloud in just a few minutes. - - - Self-host Infisical on your own infrastructure. - + + Get started with Infisical Cloud in just a few minutes. + + + Self-host Infisical on your own infrastructure. + -## Why Infisical? +## Why Infisical? + +Infisical helps developers achieve secure centralized secret management and provides all the tools to easily manage secrets in various environments and infrastructure components. In particular, here are some of the most common points that developers mention after adopting Infisical: -Infisical helps developers achieve secure centralized secret management and provides all the tools to easily manage secrets in various environments and infrastructure components. In particular, here are some of the most common points that developers mention after adopting Infisical: - Streamlined **local development** processes (switching .env files to [Infisical CLI](/cli/commands/run) and removing secrets from developer machines). -- **Best-in-class developer experience** with an easy-to-use [Web Dashboard](/documentation/platform/project). -- Simple secret management inside **[CI/CD pipelines](/integrations/cicd/githubactions)** and staging environments. -- Secure and compliant secret management practices in **[production environments](/sdks/overview)**. +- **Best-in-class developer experience** with an easy-to-use [Web Dashboard](/documentation/platform/project). +- Simple secret management inside **[CI/CD pipelines](/integrations/cicd/githubactions)** and staging environments. +- Secure and compliant secret management practices in **[production environments](/sdks/overview)**. - **Facilitated workflows** around [secret change management](/documentation/platform/pr-workflows), [access requests](/documentation/platform/access-controls/access-requests), [temporary access provisioning](/documentation/platform/access-controls/temporary-access), and more. - **Improved security posture** thanks to [secret scanning](/cli/scanning-overview), [granular access control policies](/documentation/platform/access-controls/overview), [automated secret rotation](https://infisical.com/docs/documentation/platform/secret-rotation/overview), and [dynamic secrets](/documentation/platform/dynamic-secrets/overview) capabilities. -## How does Infisical work? +## How does Infisical work? -To make secret management effortless and secure, Infisical follows a certain structure for enabling secret management workflows as defined below. +To make secret management effortless and secure, Infisical follows a certain structure for enabling secret management workflows as defined below. -**Identities** in Infisical are users or machine which have a certain set of roles and permissions assigned to them. Such identities are able to manage secrets in various **Clients** throughout the entire infrastructure. To do that, identities have to verify themselves through one of the available **Authentication Methods**. +**Identities** in Infisical are users or machine which have a certain set of roles and permissions assigned to them. Such identities are able to manage secrets in various **Clients** throughout the entire infrastructure. To do that, identities have to verify themselves through one of the available **Authentication Methods**. -As a result, the 3 main concepts that are important to understand are: -- **[Identities](/documentation/platform/identities/overview)**: users or machines with a set permissions assigned to them. +As a result, the 3 main concepts that are important to understand are: + +- **[Identities](/documentation/platform/identities/overview)**: users or machines with a set permissions assigned to them. - **[Clients](/integrations/platforms/kubernetes)**: Infisical-developed tools for managing secrets in various infrastructure components (e.g., [Kubernetes Operator](/integrations/platforms/kubernetes), [Infisical Agent](/integrations/platforms/infisical-agent), [CLI](/cli/usage), [SDKs](/sdks/overview), [API](/api-reference/overview/introduction), [Web Dashboard](/documentation/platform/organization)). -- **[Authentication Methods](/documentation/platform/identities/universal-auth)**: ways for Identities to authenticate inside different clients (e.g., SAML SSO for Web Dashboard, Universal Auth for Infisical Agent, etc.). +- **[Authentication Methods](/documentation/platform/identities/universal-auth)**: ways for Identities to authenticate inside different clients (e.g., SAML SSO for Web Dashboard, Universal Auth for Infisical Agent, AWS IAM Auth etc.). -## How to get started with Infisical? +## How to get started with Infisical? Depending on your use case, it might be helpful to look into some of the resources and guides provided below. - + Inject secrets into any application process/environment. Fetch secrets with any programming language on demand. - + Inject secrets into Docker containers. +We recommend using one of Infisical's clients like SDKs or the Infisical Agent +to authenticate with Infisical using AWS IAM Auth as they handle the +authentication process including the signed `GetCallerIdentity` query +construction for you. + +Also, note that Infisical needs network-level access to send requests to the AWS STS API +as part of the AWS IAM Auth workflow. + + ## Workflow -TODO +In the following steps, we explore how to create and use identities for your workloads and applications on AWS to +access the Infisical API using the AWS IAM authentication method. + + + + To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + + ![identities organization](/images/platform/identities/identities-org.png) + + When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles. + + ![identities organization create](/images/platform/identities/identities-org-create.png) + + Now input a few details for your new identity. Here's some guidance for each field: + + - Name (required): A friendly name for the identity. + - Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to. + + Once you've created an identity, you'll be prompted to configure the authentication method for it. Here, select **AWS IAM Auth**. + + ![identities create iam auth method](/images/platform/identities/identities-org-create-aws-iam-auth-method.png) + + Here's some more guidance on each field: + + - Allowed Principal ARNs: A comma-separated list of trusted IAM principal ARNs that are allowed to authenticate with Infisical. The values should take one of three forms: `arn:aws:iam::123456789012:user/MyUserName`, `arn:aws:iam::123456789012:role/MyRoleName`, or `arn:aws:iam::123456789012:*`. Using a wildcard in this case allows any IAM principal in the account `123456789012` to authenticate with Infisical under the identity. + - Allowed Account IDs: A comma-separated list of trusted AWS account IDs that are allowed to authenticate with Infisical. + - STS Endpoint (default is `https://sts.amazonaws.com/`): The endpoint URL for the AWS STS API. This is useful for AWS GovCloud or other AWS regions that have different STS endpoints. + - Access Token TTL (default is `2592000` equivalent to 30 days): The lifetime for an acccess token in seconds. This value will be referenced at renewal time. + - Access Token Max TTL (default is `2592000` equivalent to 30 days): The maximum lifetime for an acccess token in seconds. This value will be referenced at renewal time. + - Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses. + - Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address. + + + To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project. + + To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**. + + Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to. + + ![identities project](/images/platform/identities/identities-project.png) + + ![identities project create](/images/platform/identities/identities-project-create.png) + + + To access the Infisical API as the identity, you need to construct a signed `GetCallerIdentity` query using the [AWS Signature v4 algorithm](https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html) and make a request to the `/api/v1/auth/aws-iam-auth/login` endpoint containing the query data + in exchange for an access token. + + We provide a few code examples below of how you can authenticate with Infisical from inside a Lambda function, EC2 instance, etc. and obtain an access token to access the [Infisical API](/api-reference/overview/introduction). + + + + The following query construction is an example of how you can authenticate with Infisical from inside a Lambda function. + + The shown example uses Node.js but you can use other languages supported by AWS Lambda. + + ```javascript + import AWS from "aws-sdk"; + import axios from "axios"; + + export const handler = async (event, context) => { + try { + const region = process.env.AWS_REGION; + AWS.config.update({ region }); + + const iamRequestURL = `https://sts.${region}.amazonaws.com/`; + const iamRequestBody = "Action=GetCallerIdentity&Version=2011-06-15"; + const iamRequestHeaders = { + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", + Host: `sts.${region}.amazonaws.com`, + }; + + // Create the request + const request = new AWS.HttpRequest(iamRequestURL, region); + request.method = "POST"; + request.headers = iamRequestHeaders; + request.headers["X-Amz-Date"] = AWS.util.date + .iso8601(new Date()) + .replace(/[:-]|\.\d{3}/g, ""); + request.body = iamRequestBody; + request.headers["Content-Length"] = + Buffer.byteLength(iamRequestBody).toString(); + + // Sign the request + const signer = new AWS.Signers.V4(request, "sts"); + signer.addAuthorization(AWS.config.credentials, new Date()); + + const infisicalUrl = "https://app.infisical.com"; // or your self-hosted Infisical URL + const identityId = ""; + + const { data } = await axios.post( + `${infisicalUrl}/api/v1/auth/aws-iam-auth/login`, + { + identityId, + iamHttpRequestMethod: "POST", + iamRequestUrl: Buffer.from(iamRequestURL).toString("base64"), + iamRequestBody: Buffer.from(iamRequestBody).toString("base64"), + iamRequestHeaders: Buffer.from( + JSON.stringify(iamRequestHeaders) + ).toString("base64"), + } + ); + + console.log("result data: ", data); // access token here + } catch (err) { + console.error(err); + } + }; + ```` + + + The following query construction is an example of how you can authenticate with Infisical from inside a EC2 instance. + + The shown example uses Node.js but you can use other language you wish. + + ```javascript + import AWS from "aws-sdk"; + import axios from "axios"; + + const main = async () => { + try { + // obtain region from EC2 instance metadata + const tokenResponse = await axios.put("http://169.254.169.254/latest/api/token", null, { + headers: { + "X-aws-ec2-metadata-token-ttl-seconds": "21600" + } + }); + + const url = "http://169.254.169.254/latest/dynamic/instance-identity/document"; + const response = await axios.get(url, { + headers: { + "X-aws-ec2-metadata-token": tokenResponse.data + } + }); + + const region = response.data.region; + + AWS.config.update({ + region + }); + + const iamRequestURL = `https://sts.${region}.amazonaws.com/`; + const iamRequestBody = "Action=GetCallerIdentity&Version=2011-06-15"; + const iamRequestHeaders = { + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", + Host: `sts.${region}.amazonaws.com` + }; + + const request = new AWS.HttpRequest(new AWS.Endpoint(iamRequestURL), AWS.config.region); + request.method = "POST"; + request.headers = iamRequestHeaders; + request.headers["X-Amz-Date"] = AWS.util.date.iso8601(new Date()).replace(/[:-]|\.\d{3}/g, ""); + request.body = iamRequestBody; + request.headers["Content-Length"] = Buffer.byteLength(iamRequestBody); + + const signer = new AWS.Signers.V4(request, "sts"); + signer.addAuthorization(AWS.config.credentials, new Date()); + + const infisicalUrl = "https://app.infisical.com"; // or your self-hosted Infisical URL + const identityId = ""; + + const { data } = await axios.post(`${infisicalUrl}/api/v1/auth/aws-iam-auth/login`, { + identityId, + iamHttpRequestMethod: "POST", + iamRequestUrl: Buffer.from(iamRequestURL).toString("base64"), + iamRequestBody: Buffer.from(iamRequestBody).toString("base64"), + iamRequestHeaders: Buffer.from(JSON.stringify(iamRequestHeaders)).toString("base64") + }); + + console.log("result data: ", data); // access token here + } catch (err) { + console.error(err); + } + } + + main(); + ```` + + + The following query construction provides a generic example of how you can construct a signed `GetCallerIdentity` query and obtain the required payload components. + + The shown example uses Node.js but you can use any language you wish. + + ```javascript + const AWS = require("aws-sdk"); + + const region = ""; + const infisicalUrl = "https://app.infisical.com"; // or your self-hosted Infisical URL + + const iamRequestURL = `https://sts.${region}.amazonaws.com/`; + const iamRequestBody = "Action=GetCallerIdentity&Version=2011-06-15"; + const iamRequestHeaders = { + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", + Host: `sts.${region}.amazonaws.com` + }; + + const request = new AWS.HttpRequest(new AWS.Endpoint(iamRequestURL), region); + request.method = "POST"; + request.headers = iamRequestHeaders; + request.headers["X-Amz-Date"] = AWS.util.date.iso8601(new Date()).replace(/[:-]|\.\d{3}/g, ""); + request.body = iamRequestBody; + request.headers["Content-Length"] = Buffer.byteLength(iamRequestBody); + ```` + + #### Sample request + + ```bash Request + curl --location --request POST 'https://app.infisical.com/api/v1/auth/aws-iam-auth/login' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'identityId=...' \ + --data-urlencode 'iamHttpRequestMethod=...' \ + --data-urlencode 'iamRequestBody=...' \ + --data-urlencode 'iamRequestHeaders=...' + ``` + + #### Sample response + + ```bash Response + { + "accessToken": "...", + "expiresIn": 7200, + "accessTokenMaxTTL": 43244 + "tokenType": "Bearer" + } + ``` + + Next, you can use the access token to access the [Infisical API](/api-reference/overview/introduction) + + + + + We recommend using one of Infisical's clients like SDKs or the Infisical Agent to authenticate with Infisical using AWS IAM Auth as they handle the authentication process including the signed `GetCallerIdentity` query construction for you. + + + + Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation; + the default TTL is `7200` seconds which can be adjusted. + + If an identity access token expires, it can no longer authenticate with the Infisical API. In this case, + a new access token should be obtained by performing another login operation. + + + + diff --git a/docs/documentation/platform/identities/machine-identities.mdx b/docs/documentation/platform/identities/machine-identities.mdx index daa5db7ede..9168cb230d 100644 --- a/docs/documentation/platform/identities/machine-identities.mdx +++ b/docs/documentation/platform/identities/machine-identities.mdx @@ -7,7 +7,7 @@ description: "Learn how to use Machine Identities to programmatically interact w An Infisical machine identity is an entity that represents a workload or application that require access to various resources in Infisical. This is conceptually similar to an IAM user in AWS or service account in Google Cloud Platform (GCP). -Each identity must authenticate with the API using a supported authentication method like [Universal Auth](/documentation/platform/identities/universal-auth) to get back a short-lived access token to be used in subsequent requests. +Each identity must authenticate using a supported authentication method like [Universal Auth](/documentation/platform/identities/universal-auth) or [AWS IAM Auth](/documentation/platform/identities/aws-iam-auth) to get back a short-lived access token to be used in subsequent requests. ![organization identities](/images/platform/organization/organization-machine-identities.png) @@ -21,7 +21,7 @@ Key Features: A typical workflow for using identities consists of four steps: 1. Creating the identity with a name and [role](/documentation/platform/role-based-access-controls) in Organization Access Control > Machine Identities. - This step also involves configuring an authentication method for it such as [Universal Auth](/documentation/platform/identities/universal-auth). + This step also involves configuring an authentication method for it such as [Universal Auth](/documentation/platform/identities/universal-auth) or [AWS IAM Auth](/documentation/platform/identities/aws-iam-auth). 2. Adding the identity to the project(s) you want it to have access to. 3. Authenticating the identity with the Infisical API based on the configured authentication method on it and receiving a short-lived access token back. 4. Authenticating subsequent requests with the Infisical API using the short-lived access token. @@ -37,7 +37,8 @@ Machine Identity support for the rest of the clients is planned to be released i To interact with various resources in Infisical, Machine Identities are able to authenticate using: -- [Universal Auth](/documentation/platform/identities/universal-auth): the most versatile authentication method that can be configured on an identity from any platform/environment to access Infisical. +- [Universal Auth](/documentation/platform/identities/universal-auth): A platform-agnostic authentication method that can be configured on an identity suitable to authenticate from any platform/environment. +- [AWS IAM Auth](/documentation/platform/identities/aws-iam-auth): An AWS-native authentication method for IAM principals like EC2 instances or Lambda functions to authenticate with Infisical. ## FAQ diff --git a/docs/documentation/platform/identities/universal-auth.mdx b/docs/documentation/platform/identities/universal-auth.mdx index 474d24476b..cd40a9e641 100644 --- a/docs/documentation/platform/identities/universal-auth.mdx +++ b/docs/documentation/platform/identities/universal-auth.mdx @@ -3,17 +3,14 @@ title: Universal Auth description: "Learn how to authenticate to Infisical from any platform or environment." --- -**Universal Auth** is the most versatile authentication method that can be configured for a [machine identity](/documentation/platform/identities/machine-identities) to access Infisical from any platform or environment. +**Universal Auth** is a platform-agnostic authentication method that can be configured for a [machine identity](/documentation/platform/identities/machine-identities) suitable to authenticate from any platform/environment. -In this method, each identity is given a **Client ID** for which you can generate one or more **Client Secret(s)**. Together, a **Client ID** and **Client Secret** can be exchanged for an access token to authenticate with the Infisical API. +## Concept -## Properties +In this method, Infisical authenticates an identity by verifying the credentials issued for it at the `/api/v1/auth/universal-auth/login` endpoint. If successful, +then Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API. -Universal Auth supports many settings that can be beneficial for tightening your workflow security configuration: - -- Support for restrictions on the number of times that the **Client Secret(s)** and access token(s) can be used. -- Support for expiration, so, if specified, the **Client Secret** of the identity will automatically be defunct after a period of time. -- Support for IP allowlisting; this means you can restrict the usage of **Client Secret(s)** and access token to a specific IP or CIDR range. +In Universal Auth, an identity is given a **Client ID** and one or more **Client Secret(s)**. Together, a **Client ID** and **Client Secret** can be exchanged for a short-lived access token to authenticate with the Infisical API. ## Workflow @@ -89,16 +86,16 @@ using the Universal Auth authentication method. #### Sample request - ``` + ```bash Request curl --location --request POST 'https://app.infisical.com/api/v1/auth/universal-auth/login' \ --header 'Content-Type: application/x-www-form-urlencoded' \ - --data-urlencode 'clientSecret=...' \ - --data-urlencode 'clientId=...' + --data-urlencode 'clientId=...' \ + --data-urlencode 'clientSecret=...' ``` #### Sample response - ``` + ```bash Response { "accessToken": "...", "expiresIn": 7200, diff --git a/docs/images/platform/identities/identities-org-create-aws-iam-auth-method.png b/docs/images/platform/identities/identities-org-create-aws-iam-auth-method.png new file mode 100644 index 0000000000000000000000000000000000000000..4b902c048a08b4078f66e53a66f2638724de2f86 GIT binary patch literal 550802 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%0DnUs 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

4@AM2#F?>zF{P4vMAUW)SNTLb_RZ( ztvRpYl)gj^=_wvdmC&$>`PFuA*;Fo8Nq;Khh4}t;!WgR`@;BCogf}_~^_tH#BA_w@ zdKaej*YNh;ri*V15uNc54I(({uSwQi6XkFEcfgm1tH$jWXnV3G>RKw@REiW?Xr8Bu zJmui9ks(@5ou)w4;~fHT!YxrwVRxL=FPi}cP~67RQME^Nm3$y;80=aEuBY!|&D1f% z-9@Ydj(#l0!+%~^N`C^ge>7VcAvc$(%#^b9&1!da4+l5j;Wz9@c!Nl=Ig-iq4*_PD z?460DuE>-RT9#s@Q!?mZ;UY>ir>B%%Ce_&2F7MuTIJrZot`h$Zf%k!U>Q6(4mbNz8 z&pNZ6FLByir42p{WP+@og{`pCvcIxhHWv5y8TWN;*Z4w$a!-GO}{!N;T9+ zxmY-2sTD7P_JS+rmPK~w08hebJwGU)J|`S|=%U=D*iDvF$AmCqXXB^8o|nE+PAy>< z;I9Ncg%0DX0u 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�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 @9&E!dOnPQ=pi_N zv+J_J%ju8CbF@}@BpB!W!01{iUtuqzCyGA90;m(qN=+#xIo^gl6%-Umm{?GKwj2~N zjdGcPUq{XUaQD-vO8O{d>?3eqzZINgJm}kdUeh$NFzq=TejxtSNE>OQ%e|Oiakuli zvu;+=Gx>zA{oTW1nfEWqbJ3pmUe5kKo zt~VC1Azo9syD2KYmAG*B+1**WunpQFS>;2gd$)b-`o4M_IuuK zDKW(?njyOIZr!lZP@>T*af7zD`9^spZQMF(A3hA?Kr|h-SqN|I0mA&Z{z0D7&o}=2f&UcNATJ8I|SKo&ryqAJ%I6BSlHP;8tjxa1ipD~ z+~$fDaR(V)$K)(le$t>Yu?tZo`j2V(sw@+N;#&gR_)Mb z{TlGelzrEwaJ(vRxSmtW%p7Fj*DgWq|4rgsk#ggO4?FAuyEl$Bq_d)8W96o_oP_Jo z_PX`YPBQ#mf{zxhW4b<5ePBs(^B{SS-I-TS`^G(X-{GGRFnl!m8AMt*ZSh3YXZ=$y%XD=RAudI^85 z0lft?7*ws`ygbGGQod~zeY73hOHv}lJl7`@%`dEC1iuTo7lCZ_Zgm;Ako}7n17+Mr zp-5IrGX4eiaq4_57QL-c3=vxk_XZX=dLbLs&aFB{O^=k1fTxjqLIn2gOr>3k+bDQ$myX#{WzTV<5KMF?2ul>rf*e;>Rmlxc zGi73#V&^V6XnG!P8A)-mHMLPJu8x&jcypxUJ1Lx&^KlKHfhv+)J@2KWNZ=h)SaU-N z@hCBw1l79NdnLmcq^hvvrFclhzGkU(=-pc2&+H?3U7F0@ot@S?=X2Nlc0VQlhe{9w zm|6WaqqFyoZRI&8{{}06sy4ky-I&#A)n^ulq4)^_V$G&6M* z*&^d|j-9V|6Bw*Cj>6yS%<$a{KV+Xkk6UMat*IiM?6v;~Hxihp20&iguUR|czmwd* zv)v64Y*lcV>|~5xn!ypPgcOQ6#~~h~=Sf!t3AfR{+GS1K$<-Uo0}!J<`Ha6H-A%p# z$5p^&dgk|YW-mT9e%0=-3HHWK9A+D4d+RyLyHZY%^+RFL-IjL2l5gbj{d1kCER|b6 zHS53+?v2#sG-rCAvadx&3Ao2Bewd$WlRQ5gX}Uk^{cvQ+!hm%(N`u;kDbZznk>u%e z!atUBsu(~&D#EZU(plHq(sI{JEi}{uge^74_a8{^1vFKuWRIyf#rlHdcM3QjF7UYI z7&|teBqo|=`@f4mv~pp8z(O4B;90>(r&wOoUQg1iEEK9?Cl_7@dzzlBbv!|B7tX!ub{ z(}SOhMz5<(CyqQ(z?Hξ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

b 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 G8~+E)IYSHp literal 0 HcmV?d00001 diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsIamAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsIamAuthForm.tsx index 416d70482f..8528a5c30d 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsIamAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsIamAuthForm.tsx @@ -183,7 +183,11 @@ export const IdentityAwsIamAuthForm = ({ defaultValue="2592000" name="allowedPrincipalArns" render={({ field, fieldState: { error } }) => ( - + Date: Mon, 6 May 2024 18:35:34 -0700 Subject: [PATCH 005/188] Update aws iam auth fns filename --- ...dentity-aws-iam-auth.fns.ts => identity-aws-iam-auth-fns.ts} | 0 .../identity-aws-iam-auth/identity-aws-iam-auth-service.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename backend/src/services/identity-aws-iam-auth/{identity-aws-iam-auth.fns.ts => identity-aws-iam-auth-fns.ts} (100%) diff --git a/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth.fns.ts b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-fns.ts similarity index 100% rename from backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth.fns.ts rename to backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-fns.ts diff --git a/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-service.ts b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-service.ts index 66dc2c21a0..95deba2d8c 100644 --- a/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-service.ts +++ b/backend/src/services/identity-aws-iam-auth/identity-aws-iam-auth-service.ts @@ -16,8 +16,8 @@ import { TIdentityDALFactory } from "../identity/identity-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types"; -import { extractPrincipalArn } from "./identity-aws-iam-auth.fns"; import { TIdentityAwsIamAuthDALFactory } from "./identity-aws-iam-auth-dal"; +import { extractPrincipalArn } from "./identity-aws-iam-auth-fns"; import { TAttachAWSIAMAuthDTO, TAWSGetCallerIdentityHeaders, From 308e605b6c8c791a262207ce855cb599e04acb49 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 7 May 2024 03:28:10 +0000 Subject: [PATCH 006/188] chore: renamed new migration files to latest timestamp (gh-action) --- ...-ldap-emails.ts => 20240507032811_trusted-saml-ldap-emails.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backend/src/db/migrations/{20240506163405_trusted-saml-ldap-emails.ts => 20240507032811_trusted-saml-ldap-emails.ts} (100%) diff --git a/backend/src/db/migrations/20240506163405_trusted-saml-ldap-emails.ts b/backend/src/db/migrations/20240507032811_trusted-saml-ldap-emails.ts similarity index 100% rename from backend/src/db/migrations/20240506163405_trusted-saml-ldap-emails.ts rename to backend/src/db/migrations/20240507032811_trusted-saml-ldap-emails.ts From a3552d00d1e11d93918ad151fb3310a78c294083 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 7 May 2024 13:52:42 +0800 Subject: [PATCH 007/188] feat: add multi-select in secret overview --- .../pages/project/[id]/secrets/overview.tsx | 5 +- .../SecretOverviewPage.store.tsx | 78 +++++++++++++++++++ .../SecretOverviewPage/SecretOverviewPage.tsx | 10 +++ .../SecretOverviewFolderRow.tsx | 23 +++++- .../SecretOverviewTableRow.tsx | 24 +++++- .../SelectionPanel/SelectionPanel.tsx | 78 +++++++++++++++++++ 6 files changed, 211 insertions(+), 7 deletions(-) create mode 100644 frontend/src/views/SecretOverviewPage/SecretOverviewPage.store.tsx create mode 100644 frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx diff --git a/frontend/src/pages/project/[id]/secrets/overview.tsx b/frontend/src/pages/project/[id]/secrets/overview.tsx index 69e2bc1968..eaf915120b 100644 --- a/frontend/src/pages/project/[id]/secrets/overview.tsx +++ b/frontend/src/pages/project/[id]/secrets/overview.tsx @@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next"; import Head from "next/head"; import { SecretOverviewPage } from "@app/views/SecretOverviewPage"; +import { StoreProvider } from "@app/views/SecretOverviewPage/SecretOverviewPage.store"; const Dashboard = () => { const { t } = useTranslation(); @@ -16,7 +17,9 @@ const Dashboard = () => {

); diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.store.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.store.tsx new file mode 100644 index 0000000000..df239d6ae1 --- /dev/null +++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.store.tsx @@ -0,0 +1,78 @@ +import { createContext, ReactNode, useContext, useEffect, useRef } from "react"; +import { useRouter } from "next/router"; +import { createStore, StateCreator, StoreApi, useStore } from "zustand"; + +export enum EntryType { + FOLDER = "folder", + SECRET = "secret" +} + +type SelectedEntriesState = { + selectedEntries: { + [EntryType.FOLDER]: Record; + [EntryType.SECRET]: Record; + }; + action: { + toggle: (type: EntryType, key: string) => void; + reset: () => void; + }; +}; + +const createSelectedSecretStore: StateCreator = (set) => ({ + selectedEntries: { + [EntryType.FOLDER]: {}, + [EntryType.SECRET]: {} + }, + action: { + toggle: (type: EntryType, key: string) => + set((state) => { + const isChecked = Boolean(state.selectedEntries[type]?.[key]); + const newChecks = { ...state.selectedEntries }; + // remove selection if its present else add it + if (isChecked) delete newChecks[type][key]; + else newChecks[type][key] = true; + return { selectedEntries: newChecks }; + }), + reset: () => + set({ + selectedEntries: { + [EntryType.FOLDER]: {}, + [EntryType.SECRET]: {} + } + }) + } +}); + +const StoreContext = createContext | null>(null); +export const StoreProvider = ({ children }: { children: ReactNode }) => { + const storeRef = useRef>(); + const router = useRouter(); + if (!storeRef.current) { + storeRef.current = createStore((...a) => ({ + ...createSelectedSecretStore(...a) + })); + } + + useEffect(() => { + const onRouteChangeStart = () => { + const state = storeRef.current?.getState(); + state?.action.reset(); + }; + + router.events.on("routeChangeStart", onRouteChangeStart); + return () => { + router.events.off("routeChangeStart", onRouteChangeStart); + }; + }, []); + + return {children}; +}; + +const useStoreContext = (selector: (state: SelectedEntriesState) => T): T => { + const ctx = useContext(StoreContext); + if (!ctx) throw new Error("Missing "); + return useStore(ctx, selector); +}; + +export const useSelectedEntries = () => useStoreContext((state) => state.selectedEntries); +export const useSelectedEntryActions = () => useStoreContext((state) => state.action); diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx index 692bdcbe35..c6d97361aa 100644 --- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx +++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx @@ -70,6 +70,8 @@ import { ProjectIndexSecretsSection } from "./components/ProjectIndexSecretsSect import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynamicSecretRow"; import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow"; import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow"; +import { SelectionPanel } from "./components/SelectionPanel/SelectionPanel"; +import { EntryType, useSelectedEntries, useSelectedEntryActions } from "./SecretOverviewPage.store"; export const SecretOverviewPage = () => { const { t } = useTranslation(); @@ -105,6 +107,9 @@ export const SecretOverviewPage = () => { const [searchFilter, setSearchFilter] = useState(""); const secretPath = (router.query?.secretPath as string) || "/"; + const selectedEntries = useSelectedEntries(); + const { toggle: toggleSelectedEntry } = useSelectedEntryActions(); + useEffect(() => { if (!isWorkspaceLoading && !workspaceId && router.isReady) { router.push(`/org/${currentOrg?.id}/overview`); @@ -543,6 +548,7 @@ export const SecretOverviewPage = () => { +
@@ -666,6 +672,8 @@ export const SecretOverviewPage = () => { toggleSelectedEntry(EntryType.FOLDER, folderName)} environments={visibleEnvs} key={`overview-${folderName}-${index + 1}`} onClick={handleFolderClick} @@ -684,6 +692,8 @@ export const SecretOverviewPage = () => { visibleEnvs?.length > 0 && filteredSecretNames.map((key, index) => ( toggleSelectedEntry(EntryType.SECRET, key)} secretPath={secretPath} isImportedSecretPresentInEnv={isImportedSecretPresentInEnv} onSecretCreate={handleSecretCreate} diff --git a/frontend/src/views/SecretOverviewPage/components/SecretOverviewFolderRow/SecretOverviewFolderRow.tsx b/frontend/src/views/SecretOverviewPage/components/SecretOverviewFolderRow/SecretOverviewFolderRow.tsx index 597541fe79..c60aff912d 100644 --- a/frontend/src/views/SecretOverviewPage/components/SecretOverviewFolderRow/SecretOverviewFolderRow.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SecretOverviewFolderRow/SecretOverviewFolderRow.tsx @@ -2,20 +2,23 @@ import { faCheck, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -import { Td, Tr } from "@app/components/v2"; +import { Checkbox, Td, Tr } from "@app/components/v2"; type Props = { folderName: string; environments: { name: string; slug: string }[]; isFolderPresentInEnv: (name: string, env: string) => boolean; onClick: (path: string) => void; + isSelected: boolean; + onToggleFolderSelect: (folderName: string) => void; }; export const SecretOverviewFolderRow = ({ folderName, environments = [], isFolderPresentInEnv, - + isSelected, + onToggleFolderSelect, onClick }: Props) => { return ( @@ -23,7 +26,21 @@ export const SecretOverviewFolderRow = ({ + + + + + + + ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx new file mode 100644 index 0000000000..56a4bdef5f --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -0,0 +1,250 @@ +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[]; + workspaceId: string; + editValues?: TAccessApprovalPolicy; +}; + +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 AccessPolicyForm = ({ + isOpen, + onToggle, + members = [], + workspaceId, + 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) => { + try { + await createAccessApprovalPolicy({ + ...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 updateAccessApprovalPolicy({ + id: editValues?.id, + ...data, + 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(({ 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.email} + + ); + })} + + + + )} + /> + ( + + 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 new file mode 100644 index 0000000000..f6db07c943 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/index.tsx @@ -0,0 +1 @@ +export { AccessApprovalPolicyList } from "./AccessApprovalPolicyList"; From d33711880326a5698305a5e5b51b88ab0b11283e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 2 Apr 2024 22:25:58 -0700 Subject: [PATCH 019/188] Feat: Request access --- backend/src/@types/fastify.d.ts | 2 ++ backend/src/@types/knex.d.ts | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 12c07486f2..71a7c887cd 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -2,6 +2,7 @@ import "fastify"; import { TUsers } from "@app/db/schemas"; import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service"; +import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service"; import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service"; @@ -115,6 +116,7 @@ declare module "fastify" { identityProject: TIdentityProjectServiceFactory; identityUa: TIdentityUaServiceFactory; accessApprovalPolicy: TAccessApprovalPolicyServiceFactory; + accessApprovalRequest: TAccessApprovalRequestServiceFactory; secretApprovalPolicy: TSecretApprovalPolicyServiceFactory; secretApprovalRequest: TSecretApprovalRequestServiceFactory; secretRotation: TSecretRotationServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index b6e8744edf..14ebd42954 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -8,6 +8,12 @@ import { TAccessApprovalPoliciesApproversUpdate, TAccessApprovalPoliciesInsert, TAccessApprovalPoliciesUpdate, + TAccessApprovalRequests, + TAccessApprovalRequestsInsert, + TAccessApprovalRequestsReviewers, + TAccessApprovalRequestsReviewersInsert, + TAccessApprovalRequestsReviewersUpdate, + TAccessApprovalRequestsUpdate, TApiKeys, TApiKeysInsert, TApiKeysUpdate, @@ -363,6 +369,18 @@ declare module "knex/types/tables" { TAccessApprovalPoliciesApproversUpdate >; + [TableName.AccessApprovalRequest]: Knex.CompositeTableType< + TAccessApprovalRequests, + TAccessApprovalRequestsInsert, + TAccessApprovalRequestsUpdate + >; + + [TableName.AccessApprovalRequestReviewer]: Knex.CompositeTableType< + TAccessApprovalRequestsReviewers, + TAccessApprovalRequestsReviewersInsert, + TAccessApprovalRequestsReviewersUpdate + >; + [TableName.ScimToken]: Knex.CompositeTableType; [TableName.SecretApprovalPolicy]: Knex.CompositeTableType< TSecretApprovalPolicies, From a528d011c0c3f8999dfc6bdfc2009ea46a08d0f4 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:35:51 -0700 Subject: [PATCH 020/188] Feat: Request Access (migrations) --- .../20240330075122_access-approval-policy.ts | 3 +- ...20240401173320_access_approval_requests.ts | 51 +++++++++++++++++++ .../db/schemas/access-approval-policies.ts | 1 + .../access-approval-requests-reviewers.ts | 26 ++++++++++ .../db/schemas/access-approval-requests.ts | 26 ++++++++++ backend/src/db/schemas/index.ts | 2 + 6 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 backend/src/db/migrations/20240401173320_access_approval_requests.ts create mode 100644 backend/src/db/schemas/access-approval-requests-reviewers.ts create mode 100644 backend/src/db/schemas/access-approval-requests.ts diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts index 0595d5c6e0..8203fb3334 100644 --- a/backend/src/db/migrations/20240330075122_access-approval-policy.ts +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -10,6 +10,7 @@ export async function up(knex: Knex): Promise { t.string("name").notNullable(); t.integer("approvals").defaultTo(1).notNullable(); t.uuid("envId").notNullable(); + t.string("secretPath"); t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); t.timestamps(true, true, true); }); @@ -31,8 +32,8 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { - await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy); await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover); + await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy); await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); } diff --git a/backend/src/db/migrations/20240401173320_access_approval_requests.ts b/backend/src/db/migrations/20240401173320_access_approval_requests.ts new file mode 100644 index 0000000000..901be9a78e --- /dev/null +++ b/backend/src/db/migrations/20240401173320_access_approval_requests.ts @@ -0,0 +1,51 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.AccessApprovalRequest))) { + await knex.schema.createTable(TableName.AccessApprovalRequest, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.uuid("policyId").notNullable(); + t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE"); + + t.uuid("privilegeId").nullable(); + t.foreign("privilegeId").references("id").inTable(TableName.ProjectUserAdditionalPrivilege).onDelete("CASCADE"); + + t.uuid("requestedBy").notNullable(); + t.foreign("requestedBy").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE"); + + // We use these values to create the actual privilege at a later point in time. + t.boolean("isTemporary").notNullable(); + t.string("temporaryRange").nullable(); + + t.jsonb("permissions").notNullable(); + + t.timestamps(true, true, true); + }); + } + await createOnUpdateTrigger(knex, TableName.AccessApprovalRequest); + + if (!(await knex.schema.hasTable(TableName.AccessApprovalRequestReviewer))) { + await knex.schema.createTable(TableName.AccessApprovalRequestReviewer, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("member").notNullable(); + t.foreign("member").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE"); + t.string("status").notNullable(); + t.uuid("requestId").notNullable(); + t.foreign("requestId").references("id").inTable(TableName.AccessApprovalRequest).onDelete("CASCADE"); + t.timestamps(true, true, true); + }); + } + await createOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.AccessApprovalRequestReviewer); + await knex.schema.dropTableIfExists(TableName.AccessApprovalRequest); + + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest); +} diff --git a/backend/src/db/schemas/access-approval-policies.ts b/backend/src/db/schemas/access-approval-policies.ts index 5500497022..bf7e74ff2c 100644 --- a/backend/src/db/schemas/access-approval-policies.ts +++ b/backend/src/db/schemas/access-approval-policies.ts @@ -12,6 +12,7 @@ export const AccessApprovalPoliciesSchema = z.object({ name: z.string(), approvals: z.number().default(1), envId: z.string().uuid(), + secretPath: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date() }); diff --git a/backend/src/db/schemas/access-approval-requests-reviewers.ts b/backend/src/db/schemas/access-approval-requests-reviewers.ts new file mode 100644 index 0000000000..509fd74259 --- /dev/null +++ b/backend/src/db/schemas/access-approval-requests-reviewers.ts @@ -0,0 +1,26 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const AccessApprovalRequestsReviewersSchema = z.object({ + id: z.string().uuid(), + member: z.string().uuid(), + status: z.string(), + requestId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TAccessApprovalRequestsReviewers = z.infer; +export type TAccessApprovalRequestsReviewersInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TAccessApprovalRequestsReviewersUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/access-approval-requests.ts b/backend/src/db/schemas/access-approval-requests.ts new file mode 100644 index 0000000000..bd598bac6e --- /dev/null +++ b/backend/src/db/schemas/access-approval-requests.ts @@ -0,0 +1,26 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const AccessApprovalRequestsSchema = z.object({ + id: z.string().uuid(), + policyId: z.string().uuid(), + privilegeId: z.string().uuid().nullable().optional(), + requestedBy: z.string().uuid(), + isTemporary: z.boolean(), + temporaryRange: z.string().nullable().optional(), + permissions: z.unknown(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TAccessApprovalRequests = z.infer; +export type TAccessApprovalRequestsInsert = Omit, TImmutableDBKeys>; +export type TAccessApprovalRequestsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index f02188577c..0fd7c52714 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -1,5 +1,7 @@ export * from "./access-approval-policies"; export * from "./access-approval-policies-approvers"; +export * from "./access-approval-requests"; +export * from "./access-approval-requests-reviewers"; export * from "./api-keys"; export * from "./audit-log-streams"; export * from "./audit-logs"; From f981c59b5c259b47cf6ef97dc4e38e585049c82b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:36:11 -0700 Subject: [PATCH 021/188] Feat: Request access (models) --- backend/src/db/schemas/models.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 8e66cabe4b..f401e822c8 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -54,6 +54,8 @@ export enum TableName { // New tables so far AccessApprovalPolicy = "access_approval_policies", AccessApprovalPolicyApprover = "access_approval_policies_approvers", + AccessApprovalRequest = "access_approval_requests", + AccessApprovalRequestReviewer = "access_approval_requests_reviewers", SecretApprovalPolicy = "secret_approval_policies", SecretApprovalPolicyApprover = "secret_approval_policies_approvers", From 99f5ed1f4bec5bff9e972286ad24d33940ca640c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:36:47 -0700 Subject: [PATCH 022/188] Fix: Move to project slug --- .../v1/access-approval-policy-router.ts | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) 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 ec5331a891..8a80900426 100644 --- a/backend/src/ee/routes/v1/access-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/access-approval-policy-router.ts @@ -12,8 +12,9 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi schema: { body: z .object({ - workspaceId: z.string(), + projectSlug: z.string().trim(), name: z.string().optional(), + secretPath: z.string().trim().default("/"), environment: z.string(), approvers: z.string().array().min(1), approvals: z.number().min(1).default(1) @@ -35,8 +36,8 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.body.workspaceId, ...req.body, + projectSlug: req.body.projectSlug, name: req.body.name ?? `${req.body.environment}-${nanoid(3)}` }); return { approval }; @@ -48,11 +49,11 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi method: "GET", schema: { querystring: z.object({ - workspaceId: z.string().trim() + projectSlug: z.string().trim() }), response: { 200: z.object({ - approvals: sapPubSchema.merge(z.object({ approvers: z.string().array() })).array() + approvals: sapPubSchema.extend({ approvers: z.string().array(), secretPath: z.string().optional() }).array() }) } }, @@ -63,12 +64,41 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: req.query.workspaceId + projectSlug: req.query.projectSlug }); return { approvals }; } }); + server.route({ + url: "/policy-count", + method: "GET", + schema: { + querystring: z.object({ + projectSlug: z.string(), + envSlug: z.string() + }), + response: { + 200: z.object({ + policyCount: z.number() + }) + } + }, + + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { policyCount } = await server.services.accessApprovalPolicy.getAccessPolicyCountByEnvSlug({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + projectSlug: req.query.projectSlug, + actorOrgId: req.permission.orgId, + envSlug: req.query.envSlug + }); + return { policyCount }; + } + }); + server.route({ url: "/:policyId", method: "PATCH", @@ -79,6 +109,11 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi body: z .object({ name: z.string().optional(), + secretPath: z + .string() + .trim() + .optional() + .transform((val) => (val === "" ? "/" : val)), approvers: z.string().array().min(1), approvals: z.number().min(1).default(1) }) From f458e34c3734f797f50317447d21bfa979bd3e38 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:37:49 -0700 Subject: [PATCH 023/188] Feat: Request access (new routes) --- .../v1/access-approval-request-router.ts | 176 ++++++++++++++++++ backend/src/ee/routes/v1/index.ts | 2 + 2 files changed, 178 insertions(+) create mode 100644 backend/src/ee/routes/v1/access-approval-request-router.ts diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts new file mode 100644 index 0000000000..696c8b76fa --- /dev/null +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -0,0 +1,176 @@ +import slugify from "@sindresorhus/slugify"; +import { z } from "zod"; + +import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas"; +import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerAccessApprovalRequestRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/", + method: "POST", + schema: { + body: z.object({ + slug: z + .string() + .min(1) + .max(60) + .trim() + .default(`requested-privilege-${slugify(alphaNumericNanoId(12))}`) + .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }), + permissions: z.any().array(), + isTemporary: z.boolean(), + temporaryRange: z.string().optional() + }), + querystring: z.object({ + projectSlug: z.string().trim(), + secretPath: z.string().trim(), + envSlug: z.string().trim() + }), + response: { + 200: z.object({ + approval: AccessApprovalRequestsSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { request } = await server.services.accessApprovalRequest.createAccessApprovalRequest({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + permissions: req.body.permissions, + envSlug: req.query.envSlug, + actorOrgId: req.permission.orgId, + secretPath: req.query.secretPath, + projectSlug: req.query.projectSlug, + temporaryRange: req.body.temporaryRange, + isTemporary: req.body.isTemporary + }); + return { approval: request }; + } + }); + + server.route({ + url: "/count", + method: "GET", + schema: { + querystring: z.object({ + projectSlug: z.string().trim() + }), + response: { + 200: z.object({ + pendingCount: z.number(), + finalizedCount: z.number() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { count } = await server.services.accessApprovalRequest.getCount({ + projectSlug: req.query.projectSlug, + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod + }); + + return { ...count }; + } + }); + + server.route({ + url: "/", + method: "GET", + schema: { + querystring: z.object({ + projectSlug: z.string().trim(), + authorProjectMembershipId: z.string().trim().optional(), + envSlug: z.string().trim().optional() + }), + response: { + 200: z.object({ + requests: AccessApprovalRequestsSchema.extend({ + environmentName: z.string(), + isApproved: z.boolean(), + privilege: z + .object({ + membershipId: z.string(), + isTemporary: z.boolean(), + temporaryMode: z.string().nullish(), + temporaryRange: z.string().nullish(), + temporaryAccessStartTime: z.date().nullish(), + temporaryAccessEndTime: z.date().nullish(), + permissions: z.unknown() + }) + .nullable(), + policy: z.object({ + id: z.string(), + name: z.string(), + approvals: z.number(), + approvers: z.string().array(), + secretPath: z.string().nullish(), + envId: z.string() + }), + reviewers: z + .object({ + member: z.string(), + status: z.string() + }) + .array() + }).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { requests } = await server.services.accessApprovalRequest.listApprovalRequests({ + projectSlug: req.query.projectSlug, + authorProjectMembershipId: req.query.authorProjectMembershipId, + envSlug: req.query.envSlug, + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod + }); + + return { requests }; + } + }); + + server.route({ + url: "/:requestId/review", + method: "POST", + schema: { + params: z.object({ + requestId: z.string().trim() + }), + body: z.object({ + status: z.enum([ApprovalStatus.APPROVED, ApprovalStatus.REJECTED]) + }), + response: { + 200: z.object({ + review: AccessApprovalRequestsReviewersSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const review = await server.services.accessApprovalRequest.reviewAccessRequest({ + actor: req.permission.type, + actorId: req.permission.id, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + requestId: req.params.requestId, + status: req.body.status + }); + + return { review }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 086a1b3d64..c73ed24c5c 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,5 +1,6 @@ import { registerAuditLogStreamRouter } from "./audit-log-stream-router"; import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router"; +import { registerAccessApprovalRequestRouter } from "./access-approval-request-router"; import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerGroupRouter } from "./group-router"; @@ -43,6 +44,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { }); await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals" }); + await server.register(registerAccessApprovalRequestRouter, { prefix: "/access-approval-requests" }); await server.register( async (dynamicSecretRouter) => { From 7e29a6a6561825c8c4ff401a5f998767130c18c2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:38:36 -0700 Subject: [PATCH 024/188] Fix: Access Approval Policy DAL bugs --- .../access-approval-policy/access-approval-policy-dal.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts index 792a851c37..88e2888329 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts @@ -10,7 +10,7 @@ export type TAccessApprovalPolicyDALFactory = ReturnType { const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy); - const sapFindQuery = async (tx: Knex, filter: TFindFilter) => { + const accessApprovalPolicyFindQuery = async (tx: Knex, filter: TFindFilter) => { const result = await tx(TableName.AccessApprovalPolicy) // eslint-disable-next-line .where(buildFindFilter(filter)) @@ -32,7 +32,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const doc = await sapFindQuery(tx || db, { + const doc = await accessApprovalPolicyFindQuery(tx || db, { [`${TableName.AccessApprovalPolicy}.id` as "id"]: id }); const formatedDoc = mergeOneToManyRelation( @@ -54,7 +54,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => { const find = async (filter: TFindFilter, tx?: Knex) => { try { - const docs = await sapFindQuery(tx || db, filter); + const docs = await accessApprovalPolicyFindQuery(tx || db, filter); const formatedDoc = mergeOneToManyRelation( docs, "id", @@ -66,7 +66,7 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => { ({ approverId }) => approverId, "approvers" ); - return formatedDoc; + return formatedDoc.map((policy) => ({ ...policy, secretPath: policy.secretPath || undefined })); } catch (error) { throw new DatabaseError({ error, name: "Find" }); } From eca36f19934eec05945f25fa6c24eced1e29c0c3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:38:52 -0700 Subject: [PATCH 025/188] Feat: Request access --- .../access-approval-policy-fns.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts new file mode 100644 index 0000000000..b570c32fa1 --- /dev/null +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts @@ -0,0 +1,37 @@ +import { ForbiddenError, subject } from "@casl/ability"; + +import { BadRequestError } from "@app/lib/errors"; +import { ActorType } from "@app/services/auth/auth-type"; + +import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { TVerifyApprovers } from "./access-approval-policy-types"; + +export const verifyApprovers = async ({ + approverProjectMemberships, + projectId, + orgId, + envSlug, + actorAuthMethod, + secretPath, + permissionService +}: TVerifyApprovers) => { + for (const approver of approverProjectMemberships) { + try { + // eslint-disable-next-line no-await-in-loop + const { permission: approverPermission } = await permissionService.getProjectPermission( + ActorType.USER, + approver.userId, + projectId, + actorAuthMethod, + orgId + ); + + ForbiddenError.from(approverPermission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { environment: envSlug, secretPath }) + ); + } catch (err) { + throw new BadRequestError({ message: "One or more approvers doesn't have access to be specified secret path" }); + } + } +}; From 5255c4075ae6f54e95cdb864bf7ab5a93c7304b3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:39:15 -0700 Subject: [PATCH 026/188] Fix: Validate approvers access --- .../access-approval-policy-service.ts | 87 ++++++++++++++++--- 1 file changed, 75 insertions(+), 12 deletions(-) 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 d1f0ce1e72..30f5f10fe4 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 @@ -3,22 +3,26 @@ import { ForbiddenError } from "@casl/ability"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError } from "@app/lib/errors"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal"; import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal"; +import { verifyApprovers } from "./access-approval-policy-fns"; import { TCreateAccessApprovalPolicy, TDeleteAccessApprovalPolicy, + TGetAccessPolicyCountByEnvironmentDTO, TListAccessApprovalPoliciesDTO, TUpdateAccessApprovalPolicy } from "./access-approval-policy-types"; type TSecretApprovalPolicyServiceFactoryDep = { + projectDAL: TProjectDALFactory; permissionService: Pick; accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory; - projectEnvDAL: Pick; + projectEnvDAL: Pick; accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory; projectMembershipDAL: Pick; }; @@ -30,6 +34,7 @@ export const accessApprovalPolicyServiceFactory = ({ accessApprovalPolicyApproverDAL, permissionService, projectEnvDAL, + projectDAL, projectMembershipDAL }: TSecretApprovalPolicyServiceFactoryDep) => { const createAccessApprovalPolicy = async ({ @@ -37,19 +42,24 @@ export const accessApprovalPolicyServiceFactory = ({ actor, actorId, actorOrgId, + secretPath, actorAuthMethod, approvals, approvers, - projectId, + projectSlug, environment }: TCreateAccessApprovalPolicy) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + if (approvals > approvers.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); + if (!secretPath) throw new BadRequestError({ message: "Secret path is required" }); const { permission } = await permissionService.getProjectPermission( actor, actorId, - projectId, + project.id, actorAuthMethod, actorOrgId ); @@ -57,13 +67,24 @@ export const accessApprovalPolicyServiceFactory = ({ ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval ); - const env = await projectEnvDAL.findOne({ slug: environment, projectId }); + const env = await projectEnvDAL.findOne({ slug: environment, projectId: project.id }); if (!env) throw new BadRequestError({ message: "Environment not found" }); const secretApprovers = await projectMembershipDAL.find({ - projectId, + projectId: project.id, $in: { id: approvers } }); + + await verifyApprovers({ + projectId: project.id, + orgId: actorOrgId, + envSlug: environment, + secretPath, + actorAuthMethod, + permissionService, + approverProjectMemberships: secretApprovers + }); + if (secretApprovers.length !== approvers.length) { throw new BadRequestError({ message: "Approver not found in project" }); } @@ -73,6 +94,7 @@ export const accessApprovalPolicyServiceFactory = ({ { envId: env.id, approvals, + secretPath, name }, tx @@ -86,7 +108,7 @@ export const accessApprovalPolicyServiceFactory = ({ ); return doc; }); - return { ...accessApproval, environment: env, projectId }; + return { ...accessApproval, environment: env, projectId: project.id }; }; const getAccessApprovalPolicyByProjectId = async ({ @@ -94,24 +116,29 @@ export const accessApprovalPolicyServiceFactory = ({ actor, actorOrgId, actorAuthMethod, - projectId + projectSlug }: TListAccessApprovalPoliciesDTO) => { - const { permission } = await permissionService.getProjectPermission( + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + + // Anyone in the project should be able to get the policies. + /* const { permission } = */ await permissionService.getProjectPermission( actor, actorId, - projectId, + project.id, actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + // ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); - const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId }); + const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id }); return accessApprovalPolicies; }; const updateAccessApprovalPolicy = async ({ policyId, approvers, + secretPath, name, actorId, actor, @@ -121,7 +148,6 @@ export const accessApprovalPolicyServiceFactory = ({ }: TUpdateAccessApprovalPolicy) => { const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId); if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); - const { permission } = await permissionService.getProjectPermission( actor, actorId, @@ -129,6 +155,7 @@ export const accessApprovalPolicyServiceFactory = ({ actorAuthMethod, actorOrgId ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { @@ -136,6 +163,7 @@ export const accessApprovalPolicyServiceFactory = ({ accessApprovalPolicy.id, { approvals, + secretPath, name }, tx @@ -149,6 +177,17 @@ export const accessApprovalPolicyServiceFactory = ({ }, { tx } ); + + await verifyApprovers({ + projectId: accessApprovalPolicy.projectId, + orgId: actorOrgId, + envSlug: accessApprovalPolicy.environment.slug, + secretPath: doc.secretPath!, + actorAuthMethod, + permissionService, + approverProjectMemberships: secretApprovers + }); + if (secretApprovers.length !== approvers.length) throw new BadRequestError({ message: "Approver not found in project" }); if (doc.approvals > secretApprovers.length) @@ -197,7 +236,31 @@ export const accessApprovalPolicyServiceFactory = ({ return policy; }; + const getAccessPolicyCountByEnvSlug = async ({ + actor, + actorOrgId, + actorAuthMethod, + projectSlug, + actorId, + envSlug + }: TGetAccessPolicyCountByEnvironmentDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + + if (!project) throw new BadRequestError({ message: "Project not found" }); + + await permissionService.getProjectPermission(actor, actorId, project.id, actorAuthMethod, actorOrgId); + + const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug }); + if (!environment) throw new BadRequestError({ message: "Environment not found" }); + + const policies = await accessApprovalPolicyDAL.find({ envId: environment.id, projectId: project.id }); + if (!policies) throw new BadRequestError({ message: "No policies found" }); + + return { policyCount: policies.length }; + }; + return { + getAccessPolicyCountByEnvSlug, createAccessApprovalPolicy, deleteAccessApprovalPolicy, updateAccessApprovalPolicy, From 317956a03844a91d06b23a82f9b3f95170a10477 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:39:48 -0700 Subject: [PATCH 027/188] Fix: Types mismatch --- .../access-approval-policy-types.ts | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) 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 034132d04d..c7b452771f 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,10 +1,25 @@ +import { TProjectMemberships } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; +import { ActorAuthMethod } from "@app/services/auth/auth-type"; + +import { TPermissionServiceFactory } from "../permission/permission-service"; + +export type TVerifyApprovers = { + approverProjectMemberships: TProjectMemberships[]; + permissionService: Pick; + envSlug: string; + actorAuthMethod: ActorAuthMethod; + secretPath: string; + projectId: string; + orgId: string; +}; export type TCreateAccessApprovalPolicy = { approvals: number; + secretPath?: string | null; environment: string; approvers: string[]; - projectId: string; + projectSlug: string; name: string; } & Omit; @@ -12,6 +27,7 @@ export type TUpdateAccessApprovalPolicy = { policyId: string; approvals?: number; approvers: string[]; + secretPath?: string; name?: string; } & Omit; @@ -19,7 +35,14 @@ export type TDeleteAccessApprovalPolicy = { policyId: string; } & Omit; -export type TListAccessApprovalPoliciesDTO = TProjectPermission; +export type TGetAccessPolicyCountByEnvironmentDTO = { + envSlug: string; + projectSlug: string; +} & Omit; + +export type TListAccessApprovalPoliciesDTO = { + projectSlug: string; +} & Omit; export type TGetBoardAccessApprovalPolicy = { projectId: string; From 992a82015aac946f6882e4313c37d6874b650182 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:40:05 -0700 Subject: [PATCH 028/188] Feat: Request access --- .../access-approval-request-dal.ts | 265 ++++++++++++++++++ .../access-approval-request-reviewer-dal.ts | 10 + .../access-approval-request-secret-dal.ts | 230 +++++++++++++++ 3 files changed, 505 insertions(+) create mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-dal.ts create mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts create mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts new file mode 100644 index 0000000000..78fef7c8ab --- /dev/null +++ b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts @@ -0,0 +1,265 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { AccessApprovalRequestsSchema, TableName, TAccessApprovalRequests } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols, sqlNestRelationships, TFindFilter } from "@app/lib/knex"; + +import { ApprovalStatus } from "./access-approval-request-types"; + +export type TAccessApprovalRequestDALFactory = ReturnType; + +export const accessApprovalRequestDALFactory = (db: TDbClient) => { + const accessApprovalRequestOrm = ormify(db, TableName.AccessApprovalRequest); + + const findRequestsWithPrivilegeByPolicyIds = async (policyIds: string[]) => { + try { + const docs = await db(TableName.AccessApprovalRequest) + .whereIn(`${TableName.AccessApprovalRequest}.policyId`, policyIds) + + .leftJoin( + TableName.ProjectUserAdditionalPrivilege, + `${TableName.AccessApprovalRequest}.privilegeId`, + `${TableName.ProjectUserAdditionalPrivilege}.id` + ) + .leftJoin( + TableName.AccessApprovalPolicy, + `${TableName.AccessApprovalRequest}.policyId`, + `${TableName.AccessApprovalPolicy}.id` + ) + + .leftJoin( + TableName.AccessApprovalRequestReviewer, + `${TableName.AccessApprovalRequest}.id`, + `${TableName.AccessApprovalRequestReviewer}.requestId` + ) + .leftJoin( + TableName.AccessApprovalPolicyApprover, + `${TableName.AccessApprovalPolicy}.id`, + `${TableName.AccessApprovalPolicyApprover}.policyId` + ) + + .leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) + + .select(selectAllTableCols(TableName.AccessApprovalRequest)) + .select( + db.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"), + db.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"), + db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"), + db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"), + db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId") + ) + + .select(db.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover)) + + .select( + db.ref("projectId").withSchema(TableName.Environment), + db.ref("slug").withSchema(TableName.Environment).as("envSlug"), + db.ref("name").withSchema(TableName.Environment).as("envName") + ) + + .select( + db.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"), + db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus") + ) + + .select( + db + .ref("projectMembershipId") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("privilegeMembershipId"), + db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeIsTemporary"), + db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryMode"), + db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegeTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("privilegeTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("privilegeTemporaryAccessEndTime"), + + db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegePermissions") + ); + + const formattedDocs = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (doc) => ({ + ...AccessApprovalRequestsSchema.parse(doc), + projectId: doc.projectId, + environment: doc.envSlug, + environmentName: doc.envName, + policy: { + id: doc.policyId, + name: doc.policyName, + approvals: doc.policyApprovals, + secretPath: doc.policySecretPath, + envId: doc.policyEnvId + }, + privilege: doc.privilegeId + ? { + membershipId: doc.privilegeMembershipId, + isTemporary: doc.privilegeIsTemporary, + temporaryMode: doc.privilegeTemporaryMode, + temporaryRange: doc.privilegeTemporaryRange, + temporaryAccessStartTime: doc.privilegeTemporaryAccessStartTime, + temporaryAccessEndTime: doc.privilegeTemporaryAccessEndTime, + permissions: doc.privilegePermissions + } + : null, + + isApproved: !!doc.privilegeId + }), + childrenMapper: [ + { + key: "reviewerMemberId", + label: "reviewers" as const, + mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined) + }, + { key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId } + ] + }); + + if (!formattedDocs) return []; + + return formattedDocs.map((doc) => ({ + ...doc, + policy: { ...doc.policy, approvers: doc.approvers } + })); + } catch (error) { + throw new DatabaseError({ error, name: "FindRequestsWithPrivilege" }); + } + }; + + const findQuery = (filter: TFindFilter, tx: Knex) => + tx(TableName.AccessApprovalRequest) + .where(filter) + .join( + TableName.AccessApprovalPolicy, + `${TableName.AccessApprovalRequest}.policyId`, + `${TableName.AccessApprovalPolicy}.id` + ) + + .join( + TableName.AccessApprovalPolicyApprover, + `${TableName.AccessApprovalPolicy}.id`, + `${TableName.AccessApprovalPolicyApprover}.policyId` + ) + .leftJoin( + TableName.AccessApprovalRequestReviewer, + `${TableName.AccessApprovalRequest}.id`, + `${TableName.AccessApprovalRequestReviewer}.requestId` + ) + + .leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) + .select(selectAllTableCols(TableName.AccessApprovalRequest)) + .select( + tx.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId"), + tx.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"), + tx.ref("id").withSchema(TableName.AccessApprovalPolicy).as("policyId"), + tx.ref("name").withSchema(TableName.AccessApprovalPolicy).as("policyName"), + tx.ref("projectId").withSchema(TableName.Environment), + tx.ref("slug").withSchema(TableName.Environment).as("environment"), + tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"), + tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"), + tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover) + ); + + const findById = async (id: string, tx?: Knex) => { + try { + const sql = findQuery({ [`${TableName.AccessApprovalRequest}.id` as "id"]: id }, tx || db); + const docs = await sql; + const formatedDoc = sqlNestRelationships({ + data: docs, + key: "id", + parentMapper: (el) => ({ + ...AccessApprovalRequestsSchema.parse(el), + projectId: el.projectId, + environment: el.environment, + policy: { + id: el.policyId, + name: el.policyName, + approvals: el.policyApprovals, + secretPath: el.policySecretPath + } + }), + childrenMapper: [ + { + key: "reviewerMemberId", + label: "reviewers" as const, + mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined) + }, + { key: "approverId", label: "approvers" as const, mapper: ({ approverId }) => approverId } + ] + }); + if (!formatedDoc?.[0]) return; + return { + ...formatedDoc[0], + policy: { ...formatedDoc[0].policy, approvers: formatedDoc[0].approvers } + }; + } catch (error) { + throw new DatabaseError({ error, name: "FindByIdAccessApprovalRequest" }); + } + }; + + const getCount = async ({ projectId }: { projectId: string }) => { + try { + const accessRequests = await db(TableName.AccessApprovalRequest) + .leftJoin( + TableName.AccessApprovalPolicy, + `${TableName.AccessApprovalRequest}.policyId`, + `${TableName.AccessApprovalPolicy}.id` + ) + .leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) + .leftJoin( + TableName.ProjectUserAdditionalPrivilege, + `${TableName.AccessApprovalRequest}.privilegeId`, + `${TableName.ProjectUserAdditionalPrivilege}.id` + ) + + .leftJoin( + TableName.AccessApprovalRequestReviewer, + `${TableName.AccessApprovalRequest}.id`, + `${TableName.AccessApprovalRequestReviewer}.requestId` + ) + + .where(`${TableName.Environment}.projectId`, projectId) + .select(selectAllTableCols(TableName.AccessApprovalRequest)) + .select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")) + .select(db.ref("member").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerMemberId")); + + const formattedRequests = sqlNestRelationships({ + data: accessRequests, + key: "id", + parentMapper: (doc) => ({ + ...AccessApprovalRequestsSchema.parse(doc) + }), + childrenMapper: [ + { + key: "reviewerMemberId", + label: "reviewers" as const, + mapper: ({ reviewerMemberId: member, reviewerStatus: status }) => (member ? { member, status } : undefined) + } + ] + }); + + // an approval is pending if there is no reviewer rejections and no privilege ID is set + const pendingApprovals = formattedRequests.filter( + (req) => !req.privilegeId && !req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) + ); + + // an approval is finalized if there are any rejections or a privilege ID is set + const finalizedApprovals = formattedRequests.filter( + (req) => req.privilegeId || req.reviewers.some((r) => r.status === ApprovalStatus.REJECTED) + ); + + return { pendingCount: pendingApprovals.length, finalizedCount: finalizedApprovals.length }; + } catch (error) { + throw new DatabaseError({ error, name: "GetCountAccessApprovalRequest" }); + } + }; + + return { ...accessApprovalRequestOrm, findById, findRequestsWithPrivilegeByPolicyIds, getCount }; +}; diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts new file mode 100644 index 0000000000..251015b22e --- /dev/null +++ b/backend/src/ee/services/access-approval-request/access-approval-request-reviewer-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TAccessApprovalRequestReviewerDALFactory = ReturnType; + +export const accessApprovalRequestReviewerDALFactory = (db: TDbClient) => { + const secretApprovalRequestReviewerOrm = ormify(db, TableName.AccessApprovalRequestReviewer); + return secretApprovalRequestReviewerOrm; +}; diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts new file mode 100644 index 0000000000..d458d58ffe --- /dev/null +++ b/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts @@ -0,0 +1,230 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { + SecretApprovalRequestsSecretsSchema, + TableName, + TSecretApprovalRequestsSecrets, + TSecretTags +} from "@app/db/schemas"; +import { BadRequestError, DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; + +export type TAccessApprovalRequestSecretDALFactory = ReturnType; + +export const accessApprovalRequestSecretDALFactory = (db: TDbClient) => { + const accessApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret); + const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag); + + const bulkUpdateNoVersionIncrement = async (data: TSecretApprovalRequestsSecrets[], tx?: Knex) => { + try { + const existingApprovalSecrets = await accessApprovalRequestSecretOrm.find( + { + $in: { + id: data.map((el) => el.id) + } + }, + { tx } + ); + + if (existingApprovalSecrets.length !== data.length) { + throw new BadRequestError({ message: "Some of the secret approvals do not exist" }); + } + + if (data.length === 0) return []; + + const updatedApprovalSecrets = await (tx || db)(TableName.SecretApprovalRequestSecret) + .insert(data) + .onConflict("id") // this will cause a conflict then merge the data + .merge() // Merge the data with the existing data + .returning("*"); + + return updatedApprovalSecrets; + } catch (error) { + throw new DatabaseError({ error, name: "bulk update secret" }); + } + }; + + const findByRequestId = async (requestId: string, tx?: Knex) => { + try { + const doc = await (tx || db)({ + secVerTag: TableName.SecretTag + }) + .from(TableName.SecretApprovalRequestSecret) + .where({ requestId }) + .leftJoin( + TableName.SecretApprovalRequestSecretTag, + `${TableName.SecretApprovalRequestSecret}.id`, + `${TableName.SecretApprovalRequestSecretTag}.secretId` + ) + .leftJoin(TableName.SecretTag, `${TableName.SecretApprovalRequestSecretTag}.tagId`, `${TableName.SecretTag}.id`) + .leftJoin(TableName.Secret, `${TableName.SecretApprovalRequestSecret}.secretId`, `${TableName.Secret}.id`) + .leftJoin( + TableName.SecretVersion, + `${TableName.SecretVersion}.id`, + `${TableName.SecretApprovalRequestSecret}.secretVersion` + ) + .leftJoin( + TableName.SecretVersionTag, + `${TableName.SecretVersionTag}.${TableName.SecretVersion}Id`, + `${TableName.SecretVersion}.id` + ) + .leftJoin( + db.ref(TableName.SecretTag).as("secVerTag"), + `${TableName.SecretVersionTag}.${TableName.SecretTag}Id`, + db.ref("id").withSchema("secVerTag") + ) + .select(selectAllTableCols(TableName.SecretApprovalRequestSecret)) + .select({ + secVerTagId: "secVerTag.id", + secVerTagColor: "secVerTag.color", + secVerTagSlug: "secVerTag.slug", + secVerTagName: "secVerTag.name" + }) + .select( + db.ref("id").withSchema(TableName.SecretTag).as("tagId"), + db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"), + db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), + db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), + db.ref("name").withSchema(TableName.SecretTag).as("tagName") + ) + .select( + db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"), + db.ref("version").withSchema(TableName.Secret).as("orgSecVersion"), + db.ref("secretKeyIV").withSchema(TableName.Secret).as("orgSecKeyIV"), + db.ref("secretKeyTag").withSchema(TableName.Secret).as("orgSecKeyTag"), + db.ref("secretKeyCiphertext").withSchema(TableName.Secret).as("orgSecKeyCiphertext"), + db.ref("secretValueIV").withSchema(TableName.Secret).as("orgSecValueIV"), + db.ref("secretValueTag").withSchema(TableName.Secret).as("orgSecValueTag"), + db.ref("secretValueCiphertext").withSchema(TableName.Secret).as("orgSecValueCiphertext"), + db.ref("secretCommentIV").withSchema(TableName.Secret).as("orgSecCommentIV"), + db.ref("secretCommentTag").withSchema(TableName.Secret).as("orgSecCommentTag"), + db.ref("secretCommentCiphertext").withSchema(TableName.Secret).as("orgSecCommentCiphertext") + ) + .select( + db.ref("version").withSchema(TableName.SecretVersion).as("secVerVersion"), + db.ref("secretKeyIV").withSchema(TableName.SecretVersion).as("secVerKeyIV"), + db.ref("secretKeyTag").withSchema(TableName.SecretVersion).as("secVerKeyTag"), + db.ref("secretKeyCiphertext").withSchema(TableName.SecretVersion).as("secVerKeyCiphertext"), + db.ref("secretValueIV").withSchema(TableName.SecretVersion).as("secVerValueIV"), + db.ref("secretValueTag").withSchema(TableName.SecretVersion).as("secVerValueTag"), + db.ref("secretValueCiphertext").withSchema(TableName.SecretVersion).as("secVerValueCiphertext"), + db.ref("secretCommentIV").withSchema(TableName.SecretVersion).as("secVerCommentIV"), + db.ref("secretCommentTag").withSchema(TableName.SecretVersion).as("secVerCommentTag"), + db.ref("secretCommentCiphertext").withSchema(TableName.SecretVersion).as("secVerCommentCiphertext") + ); + const formatedDoc = sqlNestRelationships({ + data: doc, + key: "id", + parentMapper: (data) => SecretApprovalRequestsSecretsSchema.omit({ secretVersion: true }).parse(data), + childrenMapper: [ + { + key: "tagJnId", + label: "tags" as const, + mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({ + id, + name, + slug, + color + }) + }, + { + key: "secretId", + label: "secret" as const, + mapper: ({ + orgSecKeyIV, + orgSecKeyTag, + orgSecValueIV, + orgSecVersion, + orgSecValueTag, + orgSecCommentIV, + orgSecBlindIndex, + orgSecCommentTag, + orgSecKeyCiphertext, + orgSecValueCiphertext, + orgSecCommentCiphertext, + secretId + }) => + secretId + ? { + id: secretId, + version: orgSecVersion, + secretBlindIndex: orgSecBlindIndex, + secretKeyIV: orgSecKeyIV, + secretKeyTag: orgSecKeyTag, + secretKeyCiphertext: orgSecKeyCiphertext, + secretValueIV: orgSecValueIV, + secretValueTag: orgSecValueTag, + secretValueCiphertext: orgSecValueCiphertext, + secretCommentIV: orgSecCommentIV, + secretCommentTag: orgSecCommentTag, + secretCommentCiphertext: orgSecCommentCiphertext + } + : undefined + }, + { + key: "secretVersion", + label: "secretVersion" as const, + mapper: ({ + secVerCommentIV, + secVerCommentCiphertext, + secVerCommentTag, + secVerValueCiphertext, + secVerKeyIV, + secVerKeyTag, + secVerValueIV, + secretVersion, + secVerValueTag, + secVerKeyCiphertext, + secVerVersion + }) => + secretVersion + ? { + version: secVerVersion, + id: secretVersion, + secretKeyIV: secVerKeyIV, + secretKeyTag: secVerKeyTag, + secretKeyCiphertext: secVerKeyCiphertext, + secretValueIV: secVerValueIV, + secretValueTag: secVerValueTag, + secretValueCiphertext: secVerValueCiphertext, + secretCommentIV: secVerCommentIV, + secretCommentTag: secVerCommentTag, + secretCommentCiphertext: secVerCommentCiphertext + } + : undefined, + childrenMapper: [ + { + key: "secVerTagId", + label: "tags" as const, + mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({ + // eslint-disable-next-line + id, + // eslint-disable-next-line + name, + // eslint-disable-next-line + slug, + // eslint-disable-next-line + color + }) + } + ] + } + ] + }); + return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({ + ...el, + secret: secret?.[0], + secretVersion: secretVersion?.[0] + })); + } catch (error) { + throw new DatabaseError({ error, name: "FindByRequestId" }); + } + }; + return { + ...accessApprovalRequestSecretOrm, + findByRequestId, + bulkUpdateNoVersionIncrement, + insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany + }; +}; From ad405109a0526c4ea20ca127b0d452500d3f04a8 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:40:51 -0700 Subject: [PATCH 029/188] Feat: Request access --- .../access-approval-request-service.ts | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-service.ts diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts new file mode 100644 index 0000000000..d028429394 --- /dev/null +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -0,0 +1,269 @@ +import { ForbiddenError } from "@casl/ability"; +import ms from "ms"; + +import { ProjectMembershipRole } from "@app/db/schemas"; +import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; +import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; +import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; + +import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal"; +import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns"; +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; +import { TProjectUserAdditionalPrivilegeServiceFactory } from "../project-user-additional-privilege/project-user-additional-privilege-service"; +import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types"; +import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal"; +import { TAccessApprovalRequestReviewerDALFactory } from "./access-approval-request-reviewer-dal"; +import { + ApprovalStatus, + TCreateAccessApprovalRequestDTO, + TGetAccessRequestCountDTO, + TListApprovalRequestsDTO, + TReviewAccessRequestDTO +} from "./access-approval-request-types"; + +type TSecretApprovalRequestServiceFactoryDep = { + additionalPrivilegeService: TProjectUserAdditionalPrivilegeServiceFactory; + additionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory; + permissionService: TPermissionServiceFactory; + projectEnvDAL: Pick; + projectDAL: Pick; + accessApprovalRequestDAL: TAccessApprovalRequestDALFactory; + accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory; + accessApprovalRequestReviewerDAL: TAccessApprovalRequestReviewerDALFactory; + projectMembershipDAL: TProjectMembershipDALFactory; +}; + +export type TAccessApprovalRequestServiceFactory = ReturnType; + +export const accessApprovalRequestServiceFactory = ({ + projectDAL, + projectEnvDAL, + permissionService, + accessApprovalRequestDAL, + accessApprovalRequestReviewerDAL, + projectMembershipDAL, + accessApprovalPolicyDAL, + additionalPrivilegeDAL +}: TSecretApprovalRequestServiceFactoryDep) => { + const createAccessApprovalRequest = async ({ + isTemporary, + temporaryRange, + actorId, + envSlug, + permissions, + secretPath, + actor, + actorOrgId, + actorAuthMethod, + projectSlug + }: TCreateAccessApprovalRequestDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new UnauthorizedError({ message: "Project not found" }); + + // Anyone can create an access approval request. + const p = await permissionService.getProjectPermission(actor, actorId, project.id, actorAuthMethod, actorOrgId); + + if (!p.membership) throw new UnauthorizedError({ message: "You are not a member of this project" }); + + await projectDAL.checkProjectUpgradeStatus(project.id); + + const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug }); + + if (!environment) throw new UnauthorizedError({ message: "Environment not found" }); + if (!secretPath) throw new UnauthorizedError({ message: "Secret path is required" }); + + const policy = await accessApprovalPolicyDAL.findOne({ + envId: environment.id, + secretPath + }); + if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." }); + + const approvalRequest = await accessApprovalRequestDAL.create({ + policyId: policy.id, + requestedBy: p.membership.id, + temporaryRange: temporaryRange || null, + permissions: JSON.stringify(permissions), + isTemporary + }); + + return { request: approvalRequest }; + }; + + const listApprovalRequests = async ({ + projectSlug, + authorProjectMembershipId, + envSlug, + actor, + actorOrgId, + actorId, + actorAuthMethod + }: TListApprovalRequestsDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new UnauthorizedError({ message: "Project not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + project.id, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + + const policies = await accessApprovalPolicyDAL.find({ projectId: project.id }); + let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id)); + + if (authorProjectMembershipId) { + requests = requests.filter((request) => request.requestedBy === authorProjectMembershipId); + } + + if (envSlug) { + requests = requests.filter((request) => request.environment === envSlug); + } + + return { requests }; + }; + + const reviewAccessRequest = async ({ + requestId, + actor, + status, + actorId, + actorAuthMethod, + actorOrgId + }: TReviewAccessRequestDTO) => { + const accessApprovalRequest = await accessApprovalRequestDAL.findById(requestId); + if (!accessApprovalRequest) throw new BadRequestError({ message: "Secret approval request not found" }); + + const { policy } = accessApprovalRequest; + const { membership, hasRole } = await permissionService.getProjectPermission( + actor, + actorId, + accessApprovalRequest.projectId, + actorAuthMethod, + actorOrgId + ); + + if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" }); + + if ( + !hasRole(ProjectMembershipRole.Admin) && + accessApprovalRequest.requestedBy !== membership.id && // The request wasn't made by the current user + !policy.approvers.find((approverId) => approverId === membership.id) // The request isn't performed by an assigned approver + ) { + throw new UnauthorizedError({ message: "No access" }); + } + + const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id); + + await verifyApprovers({ + projectId: accessApprovalRequest.projectId, + orgId: actorOrgId, + envSlug: accessApprovalRequest.environment, + secretPath: accessApprovalRequest.policy.secretPath!, + actorAuthMethod, + permissionService, + approverProjectMemberships: [reviewerProjectMembership] + }); + + const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id }); + if (existingReviews.some((review) => review.status === ApprovalStatus.REJECTED)) { + throw new BadRequestError({ message: "The request has already been rejected by another reviewer" }); + } + + const reviewStatus = await accessApprovalRequestReviewerDAL.transaction(async (tx) => { + const review = await accessApprovalRequestReviewerDAL.findOne( + { + requestId: accessApprovalRequest.id, + member: membership.id + }, + tx + ); + if (!review) { + const newReview = await accessApprovalRequestReviewerDAL.create( + { + status, + requestId: accessApprovalRequest.id, + member: membership.id + }, + tx + ); + + const allReviews = [...existingReviews, newReview]; + + const approvedReviews = allReviews.filter((r) => r.status === ApprovalStatus.APPROVED); + + // If all approvers have approved the request, update the privilege to approved + if (approvedReviews.length === policy.approvers.length) { + if (accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) { + throw new BadRequestError({ message: "Temporary range is required for temporary access" }); + } + + let privilegeId: string | null = null; + + if (!accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) { + // Permanent access + const privilege = await additionalPrivilegeDAL.create({ + projectMembershipId: accessApprovalRequest.requestedBy, + slug: "", + permissions: JSON.stringify(accessApprovalRequest.permissions) + }); + privilegeId = privilege.id; + } else { + // Temporary access + const relativeTempAllocatedTimeInMs = ms(accessApprovalRequest.temporaryRange!); + const startTime = new Date(); + + const privilege = await additionalPrivilegeDAL.create({ + projectMembershipId: accessApprovalRequest.requestedBy, + slug: "", + permissions: JSON.stringify(accessApprovalRequest.permissions), + isTemporary: true, + temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, + temporaryRange: accessApprovalRequest.temporaryRange!, + temporaryAccessStartTime: startTime, + temporaryAccessEndTime: new Date(new Date(startTime).getTime() + relativeTempAllocatedTimeInMs) + }); + privilegeId = privilege.id; + } + + await accessApprovalRequestDAL.updateById(accessApprovalRequest.id, { privilegeId }, tx); + } + + return newReview; + } + throw new BadRequestError({ message: "You have already reviewed this request" }); + }); + + return reviewStatus; + }; + + const getCount = async ({ projectSlug, actor, actorAuthMethod, actorId, actorOrgId }: TGetAccessRequestCountDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new UnauthorizedError({ message: "Project not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + project.id, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + + const count = await accessApprovalRequestDAL.getCount({ projectId: project.id }); + + return { count }; + }; + + return { + createAccessApprovalRequest, + listApprovalRequests, + reviewAccessRequest, + getCount + }; +}; From ad6e2aeb9e7ca01403bfaa8098006d5280fa561f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:43:05 -0700 Subject: [PATCH 030/188] Feat: Request Access --- .../access-approval-request-types.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-types.ts diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts new file mode 100644 index 0000000000..6bba0c14d8 --- /dev/null +++ b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts @@ -0,0 +1,31 @@ +import { TProjectPermission } from "@app/lib/types"; + +export enum ApprovalStatus { + PENDING = "pending", + APPROVED = "approved", + REJECTED = "rejected" +} + +export type TGetAccessRequestCountDTO = { + projectSlug: string; +} & Omit; + +export type TReviewAccessRequestDTO = { + requestId: string; + status: ApprovalStatus; +} & Omit; + +export type TCreateAccessApprovalRequestDTO = { + projectSlug: string; + secretPath: string; + envSlug: string; + permissions: unknown; + isTemporary: boolean; + temporaryRange?: string; +} & Omit; + +export type TListApprovalRequestsDTO = { + projectSlug: string; + authorProjectMembershipId?: string; + envSlug?: string; +} & Omit; From 696479a2efb098b27f694e349c84449eb3da7ed2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:43:48 -0700 Subject: [PATCH 031/188] Fix: Remove redundant code --- .../src/components/permissions/PermissionDeniedBanner.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx index f9707d6347..b3c7a4f538 100644 --- a/frontend/src/components/permissions/PermissionDeniedBanner.tsx +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -3,8 +3,6 @@ import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -import { Button } from "../v2"; - type Props = { containerClassName?: string; className?: string; @@ -34,9 +32,6 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children )} - ); From b298eec9dba82f77a0f27d3153bb526808d9c91e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:44:00 -0700 Subject: [PATCH 032/188] Fix: Danger color not working on disabled buttons --- frontend/src/components/v2/Button/Button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/v2/Button/Button.tsx b/frontend/src/components/v2/Button/Button.tsx index 5536d699aa..7707805e98 100644 --- a/frontend/src/components/v2/Button/Button.tsx +++ b/frontend/src/components/v2/Button/Button.tsx @@ -29,7 +29,7 @@ const buttonVariants = cva( colorSchema: { primary: ["bg-primary", "text-black", "border-primary bg-opacity-90 hover:bg-opacity-100"], secondary: ["bg-mineshaft", "text-gray-300", "border-mineshaft hover:bg-opacity-80"], - danger: ["bg-red", "text-white", "border-red hover:bg-opacity-90"], + danger: ["!bg-red", "!text-white", "!border-red hover:!bg-opacity-90"], gray: ["bg-bunker-500", "text-bunker-200"] }, variant: { From 7fd1d729850da8dc177fcaff652ab18d6c62c274 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:44:25 -0700 Subject: [PATCH 033/188] Fix: Access Request setup --- backend/src/server/routes/index.ts | 35 +++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 0a2c77c9f7..905d4028c5 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -5,6 +5,9 @@ import { registerV1EERoutes } from "@app/ee/routes/v1"; import { accessApprovalPolicyApproverDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal"; import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal"; import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service"; +import { accessApprovalRequestDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-dal"; +import { accessApprovalRequestReviewerDALFactory } from "@app/ee/services/access-approval-request/access-approval-request-reviewer-dal"; +import { accessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service"; import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal"; import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue"; import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; @@ -212,7 +215,9 @@ export const registerRoutes = async ( const ldapGroupMapDAL = ldapGroupMapDALFactory(db); const accessApprovalPolicyDAL = accessApprovalPolicyDALFactory(db); + const accessApprovalRequestDAL = accessApprovalRequestDALFactory(db); const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db); + const accessApprovalRequestReviewerDAL = accessApprovalRequestReviewerDALFactory(db); const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db); const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db); @@ -273,14 +278,6 @@ export const registerRoutes = async ( }); const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL }); - const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({ - accessApprovalPolicyDAL, - accessApprovalPolicyApproverDAL, - permissionService, - projectEnvDAL, - projectMembershipDAL - }); - const samlService = samlConfigServiceFactory({ permissionService, orgBotDAL, @@ -613,6 +610,27 @@ export const registerRoutes = async ( secretQueueService }); + const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({ + accessApprovalPolicyDAL, + accessApprovalPolicyApproverDAL, + permissionService, + projectEnvDAL, + projectMembershipDAL, + projectDAL + }); + + const accessApprovalRequestService = accessApprovalRequestServiceFactory({ + projectDAL, + permissionService, + accessApprovalRequestReviewerDAL, + additionalPrivilegeDAL: projectUserAdditionalPrivilegeDAL, + projectMembershipDAL, + additionalPrivilegeService: projectUserAdditionalPrivilegeService, + accessApprovalPolicyDAL, + accessApprovalRequestDAL, + projectEnvDAL + }); + const secretRotationQueue = secretRotationQueueFactory({ telemetryService, secretRotationDAL, @@ -750,6 +768,7 @@ export const registerRoutes = async ( identityUa: identityUaService, secretApprovalPolicy: sapService, accessApprovalPolicy: accessApprovalPolicyService, + accessApprovalRequest: accessApprovalRequestService, secretApprovalRequest: sarService, secretRotation: secretRotationService, dynamicSecret: dynamicSecretService, From 1f68730aa3aedc49acfe06f06f6ad6cc685093c5 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:44:49 -0700 Subject: [PATCH 034/188] Fix: Improve disabled Select --- frontend/src/components/v2/Select/Select.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/v2/Select/Select.tsx b/frontend/src/components/v2/Select/Select.tsx index 2a76be2abf..12a9094e03 100644 --- a/frontend/src/components/v2/Select/Select.tsx +++ b/frontend/src/components/v2/Select/Select.tsx @@ -41,18 +41,22 @@ export const Select = forwardRef( ref={ref} className={twMerge( `inline-flex items-center justify-between rounded-md - bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200 focus:bg-mineshaft-700/80`, - className + bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none focus:bg-mineshaft-700/80 data-[placeholder]:text-mineshaft-200`, + className, + isDisabled && "cursor-not-allowed opacity-50" )} > {props.icon ? : placeholder} - {!isDisabled && ( - - - - )} + + + + Date: Wed, 3 Apr 2024 16:45:18 -0700 Subject: [PATCH 035/188] Update index.tsx --- frontend/src/hooks/api/accessApproval/index.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/hooks/api/accessApproval/index.tsx b/frontend/src/hooks/api/accessApproval/index.tsx index 537dfe3be4..ec9789d057 100644 --- a/frontend/src/hooks/api/accessApproval/index.tsx +++ b/frontend/src/hooks/api/accessApproval/index.tsx @@ -1,6 +1,13 @@ export { useCreateAccessApprovalPolicy, + useCreateAccessRequest, useDeleteAccessApprovalPolicy, + useReviewAccessRequest, useUpdateAccessApprovalPolicy } from "./mutation"; -export { useGetAccessApprovalPolicies } from "./queries"; +export { + useGetAccessApprovalPolicies, + useGetAccessApprovalRequests, + useGetAccessPolicyApprovalCount, + useGetAccessRequestsCount +} from "./queries"; From 7aee4fdfcdfdf3f312504e6e62841b6cc07a97e6 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:45:37 -0700 Subject: [PATCH 036/188] Feat: Request access --- .../src/hooks/api/accessApproval/mutation.tsx | 87 ++++++++-- .../src/hooks/api/accessApproval/queries.tsx | 149 ++++++++++++++++-- .../src/hooks/api/accessApproval/types.ts | 106 ++++++++++++- 3 files changed, 312 insertions(+), 30 deletions(-) diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx index 98e9b3f97f..7a150092c2 100644 --- a/frontend/src/hooks/api/accessApproval/mutation.tsx +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -1,26 +1,34 @@ +import { packRules } from "@casl/ability/extra"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { accessApprovalKeys } from "./queries"; -import { TCreateAccessPolicyDTO, TDeleteSecretPolicyDTO, TUpdateAccessPolicyDTO } from "./types"; +import { + TAccessApproval, + TCreateAccessPolicyDTO, + TCreateAccessRequestDTO, + TDeleteSecretPolicyDTO, + TUpdateAccessPolicyDTO +} from "./types"; export const useCreateAccessApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TCreateAccessPolicyDTO>({ - mutationFn: async ({ environment, workspaceId, approvals, approvers, name }) => { + mutationFn: async ({ environment, projectSlug, approvals, approvers, name, secretPath }) => { const { data } = await apiRequest.post("/api/v1/access-approvals", { environment, - workspaceId, + projectSlug, approvals, approvers, + secretPath, name }); return data; }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(workspaceId)); + onSuccess: (_, { projectSlug }) => { + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug)); } }); }; @@ -29,16 +37,17 @@ export const useUpdateAccessApprovalPolicy = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TUpdateAccessPolicyDTO>({ - mutationFn: async ({ id, approvers, approvals, name }) => { + mutationFn: async ({ id, approvers, approvals, name, secretPath }) => { const { data } = await apiRequest.patch(`/api/v1/access-approvals/${id}`, { approvals, approvers, + secretPath, name }); return data; }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(workspaceId)); + onSuccess: (_, { projectSlug }) => { + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug)); } }); }; @@ -51,8 +60,66 @@ export const useDeleteAccessApprovalPolicy = () => { const { data } = await apiRequest.delete(`/api/v1/access-approvals/${id}`); return data; }, - onSuccess: (_, { workspaceId }) => { - queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(workspaceId)); + onSuccess: (_, { projectSlug }) => { + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug)); + } + }); +}; + +export const useCreateAccessRequest = () => { + const queryClient = useQueryClient(); + return useMutation<{}, {}, TCreateAccessRequestDTO>({ + mutationFn: async ({ envSlug, projectSlug, secretPath, ...privilege }) => { + const { data } = await apiRequest.post( + "/api/v1/access-approval-requests", + { + ...privilege, + permissions: privilege.permissions ? packRules(privilege.permissions) : undefined + }, + { + params: { + envSlug, + projectSlug, + secretPath + } + } + ); + + return data; + }, + onSuccess: (_, { projectSlug }) => { + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequests(projectSlug)); + } + }); +}; + +export const useReviewAccessRequest = () => { + const queryClient = useQueryClient(); + return useMutation< + {}, + {}, + { + requestId: string; + status: "approved" | "rejected"; + projectSlug: string; + envSlug?: string; + requestedBy?: string; + } + >({ + mutationFn: async ({ requestId, status }) => { + const { data } = await apiRequest.post( + `/api/v1/access-approval-requests/${requestId}/review`, + { + status + } + ); + return data; + }, + onSuccess: (_, { projectSlug, envSlug, requestedBy }) => { + queryClient.invalidateQueries( + accessApprovalKeys.getAccessApprovalRequests(projectSlug, envSlug, requestedBy) + ); + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequestCount(projectSlug)); } }); }; diff --git a/frontend/src/hooks/api/accessApproval/queries.tsx b/frontend/src/hooks/api/accessApproval/queries.tsx index 8641225937..112c53fe3d 100644 --- a/frontend/src/hooks/api/accessApproval/queries.tsx +++ b/frontend/src/hooks/api/accessApproval/queries.tsx @@ -1,30 +1,125 @@ +import { PackRule, unpackRules } from "@casl/ability/extra"; import { useQuery, UseQueryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { TAccessApprovalPolicy, TGetSecretApprovalPoliciesDTO } from "./types"; +import { TProjectPermission } from "../roles/types"; +import { + TAccessApprovalPolicy, + TAccessApprovalRequest, + TAccessRequestCount, + TGetAccessApprovalRequestsDTO, + TGetAccessPolicyApprovalCountDTO +} from "./types"; export const accessApprovalKeys = { - getAccessApprovalPolicies: (workspaceId: string) => - [{ workspaceId }, "access-approval-policies"] as const, - getAccessApprovalPolicyOfABoard: (workspaceId: string, environment: string) => [ - { workspaceId, environment }, - "access-approval-policy" - ] + getAccessApprovalPolicies: (projectSlug: string) => + [{ projectSlug }, "access-approval-policies"] as const, + getAccessApprovalPolicyOfABoard: (workspaceId: string, environment: string) => + [{ workspaceId, environment }, "access-approval-policy"] as const, + + getAccessApprovalRequests: (projectSlug: string, envSlug?: string, requestedBy?: string) => + [{ projectSlug, envSlug, requestedBy }, "access-approval-requests"] as const, + getAccessApprovalRequestCount: (projectSlug: string) => + [{ projectSlug }, "access-approval-request-count"] as const }; -const fetchApprovalPolicies = async (workspaceId: string) => { +export const fetchPolicyApprovalCount = async ({ + projectSlug, + envSlug +}: TGetAccessPolicyApprovalCountDTO) => { + const { data } = await apiRequest.get<{ policyCount: number }>( + "/api/v1/access-approvals/policy-count", + { + params: { projectSlug, envSlug } + } + ); + return data.policyCount; +}; + +export const useGetAccessPolicyApprovalCount = ({ + projectSlug, + envSlug, + options = {} +}: TGetAccessPolicyApprovalCountDTO & { + options?: UseQueryOptions< + number, + unknown, + number, + ReturnType + >; +}) => + useQuery({ + queryFn: () => fetchPolicyApprovalCount({ projectSlug, envSlug }), + ...options, + enabled: Boolean(projectSlug) && (options?.enabled ?? true) + }); + +const fetchApprovalPolicies = async ({ projectSlug }: TGetAccessApprovalRequestsDTO) => { const { data } = await apiRequest.get<{ approvals: TAccessApprovalPolicy[] }>( "/api/v1/access-approvals", - { params: { workspaceId } } + { params: { projectSlug } } ); return data.approvals; }; -export const useGetAccessApprovalPolicies = ({ - workspaceId, +const fetchApprovalRequests = async ({ + projectSlug, + envSlug, + authorProjectMembershipId +}: TGetAccessApprovalRequestsDTO) => { + const { data } = await apiRequest.get<{ requests: TAccessApprovalRequest[] }>( + "/api/v1/access-approval-requests", + { params: { projectSlug, envSlug, authorProjectMembershipId } } + ); + + return data.requests.map((request) => ({ + ...request, + + privilege: request.privilege + ? { + ...request.privilege, + permissions: unpackRules( + request.privilege.permissions as unknown as PackRule[] + ) + } + : null, + permissions: unpackRules(request.permissions as unknown as PackRule[]) + })); +}; + +const fetchAccessRequestsCount = async (projectSlug: string) => { + const { data } = await apiRequest.get( + "/api/v1/access-approval-requests/count", + { params: { projectSlug } } + ); + return data; +}; + +export const useGetAccessRequestsCount = ({ + projectSlug, options = {} -}: TGetSecretApprovalPoliciesDTO & { +}: TGetAccessApprovalRequestsDTO & { + options?: UseQueryOptions< + TAccessRequestCount, + unknown, + { pendingCount: number; finalizedCount: number }, + ReturnType + >; +}) => + useQuery({ + queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug), + queryFn: () => fetchAccessRequestsCount(projectSlug), + ...options, + enabled: Boolean(projectSlug) && (options?.enabled ?? true) + }); + +export const useGetAccessApprovalPolicies = ({ + projectSlug, + envSlug, + authorProjectMembershipId, + options = {} +}: TGetAccessApprovalRequestsDTO & { options?: UseQueryOptions< TAccessApprovalPolicy[], unknown, @@ -33,8 +128,32 @@ export const useGetAccessApprovalPolicies = ({ >; }) => useQuery({ - queryKey: accessApprovalKeys.getAccessApprovalPolicies(workspaceId), - queryFn: () => fetchApprovalPolicies(workspaceId), + queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug), + queryFn: () => fetchApprovalPolicies({ projectSlug, envSlug, authorProjectMembershipId }), ...options, - enabled: Boolean(workspaceId) && (options?.enabled ?? true) + enabled: Boolean(projectSlug) && (options?.enabled ?? true) + }); + +export const useGetAccessApprovalRequests = ({ + projectSlug, + envSlug, + authorProjectMembershipId, + options = {} +}: TGetAccessApprovalRequestsDTO & { + options?: UseQueryOptions< + TAccessApprovalRequest[], + unknown, + TAccessApprovalRequest[], + ReturnType + >; +}) => + useQuery({ + queryKey: accessApprovalKeys.getAccessApprovalRequests( + projectSlug, + envSlug, + authorProjectMembershipId + ), + queryFn: () => fetchApprovalRequests({ projectSlug, envSlug, authorProjectMembershipId }), + ...options, + enabled: Boolean(projectSlug) && (options?.enabled ?? true) }); diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts index 63020166b9..1ffd0bf2e6 100644 --- a/frontend/src/hooks/api/accessApproval/types.ts +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -1,9 +1,11 @@ +import { TProjectPermission } from "../roles/types"; import { WorkspaceEnv } from "../workspace/types"; export type TAccessApprovalPolicy = { id: string; name: string; approvals: number; + secretPath: string; envId: string; workspace: string; environment: WorkspaceEnv; @@ -11,8 +13,99 @@ export type TAccessApprovalPolicy = { approvers: string[]; }; -export type TGetSecretApprovalPoliciesDTO = { - workspaceId: string; +export type TAccessApprovalRequest = { + id: string; + policyId: string; + privilegeId: string | null; + requestedBy: string; + createdAt: Date; + updatedAt: Date; + isTemporary: boolean; + temporaryRange: string | null | undefined; + + permissions: TProjectPermission[] | null; + + // Computed + environmentName: string; + isApproved: boolean; + + privilege: { + membershipId: string; + isTemporary: boolean; + temporaryMode?: string | null; + temporaryRange?: string | null; + temporaryAccessStartTime?: Date | null; + temporaryAccessEndTime?: Date | null; + permissions: TProjectPermission[]; + isApproved: boolean; + } | null; + + policy: { + id: string; + name: string; + approvals: number; + approvers: string[]; + secretPath?: string | null; + envId: string; + }; + + reviewers: { + member: string; + status: string; + }[]; +}; + +export type TAccessApproval = { + id: string; + policyId: string; + privilegeId: string; + requestedBy: string; +}; + +export type TAccessRequestCount = { + pendingCount: number; + finalizedCount: number; +}; + +export type TProjectUserPrivilege = { + projectMembershipId: string; + slug: string; + id: string; + createdAt: Date; + updatedAt: Date; + permissions?: TProjectPermission[]; +} & ( + | { + isTemporary: true; + temporaryMode: string; + temporaryRange: string; + temporaryAccessStartTime: string; + temporaryAccessEndTime?: string; + } + | { + isTemporary: false; + temporaryMode?: null; + temporaryRange?: null; + temporaryAccessStartTime?: null; + temporaryAccessEndTime?: null; + } +); + +export type TCreateAccessRequestDTO = { + envSlug: string; + projectSlug: string; + secretPath: string; +} & Omit; + +export type TGetAccessApprovalRequestsDTO = { + projectSlug: string; + envSlug?: string; + authorProjectMembershipId?: string; +}; + +export type TGetAccessPolicyApprovalCountDTO = { + projectSlug: string; + envSlug: string; }; export type TGetSecretApprovalPolicyOfBoardDTO = { @@ -22,24 +115,27 @@ export type TGetSecretApprovalPolicyOfBoardDTO = { }; export type TCreateAccessPolicyDTO = { - workspaceId: string; + projectSlug: string; name?: string; environment: string; approvers?: string[]; approvals?: number; + secretPath?: string; }; export type TUpdateAccessPolicyDTO = { id: string; name?: string; approvers?: string[]; + secretPath?: string; + environment?: string; approvals?: number; // for invalidating list - workspaceId: string; + projectSlug: string; }; export type TDeleteSecretPolicyDTO = { id: string; // for invalidating list - workspaceId: string; + projectSlug: string; }; 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 037/188] 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 d5cc9f15df..855047fe4c 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 2286837267..89058b08d6 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 && ( + + )} Date: Wed, 3 Apr 2024 16:49:48 -0700 Subject: [PATCH 038/188] Fix: Move to project slug --- .../AccessApprovalPolicyList.tsx | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx index 834b54f00c..7c7b354a83 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx @@ -21,7 +21,8 @@ import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission, - useSubscription + useSubscription, + useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useDeleteAccessApprovalPolicy, useGetWorkspaceUsers } from "@app/hooks/api"; @@ -43,12 +44,15 @@ export const AccessApprovalPolicyList = ({ workspaceId }: IProps) => { ] as const); const { permission } = useProjectPermission(); const { subscription } = useSubscription(); + const { currentWorkspace } = useWorkspace(); const { data: members } = useGetWorkspaceUsers(workspaceId); const { data: policies, isLoading: isPoliciesLoading } = useGetAccessApprovalPolicies({ - workspaceId, + projectSlug: currentWorkspace?.slug as string, options: { - enabled: permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) + enabled: + permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) && + !!currentWorkspace?.slug } }); @@ -56,9 +60,11 @@ export const AccessApprovalPolicyList = ({ workspaceId }: IProps) => { const handleDeletePolicy = async () => { const { id } = popUp.deletePolicy.data as TAccessApprovalPolicy; + if (!currentWorkspace?.slug) return; + try { await deleteSecretApprovalPolicy({ - workspaceId, + projectSlug: currentWorkspace?.slug, id }); createNotification({ @@ -114,6 +120,7 @@ export const AccessApprovalPolicyList = ({ workspaceId }: IProps) => {
+ @@ -129,21 +136,22 @@ export const AccessApprovalPolicyList = ({ workspaceId }: IProps) => { )} - {policies?.map((policy) => ( - handlePopUpOpen("secretPolicyForm", policy)} - onDelete={() => handlePopUpOpen("deletePolicy", policy)} - /> - ))} + {!!currentWorkspace && + policies?.map((policy) => ( + handlePopUpOpen("secretPolicyForm", policy)} + onDelete={() => handlePopUpOpen("deletePolicy", policy)} + /> + ))}
- + { + onToggleFolderSelect(folderName); + }} + onClick={(e) => { + e.stopPropagation(); + }} + className={twMerge("hidden group-hover:flex", isSelected && "flex")} + /> +
{folderName}
diff --git a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx index 076ff22c85..f1476c167d 100644 --- a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx @@ -11,7 +11,7 @@ import { import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -import { Button, TableContainer, Td, Tooltip, Tr } from "@app/components/v2"; +import { Button, Checkbox, TableContainer, Td, Tooltip, Tr } from "@app/components/v2"; import { useToggle } from "@app/hooks"; import { DecryptedSecret } from "@app/hooks/api/secrets/types"; @@ -23,6 +23,8 @@ type Props = { secretPath: string; environments: { name: string; slug: string }[]; expandableColWidth: number; + isSelected: boolean; + onToggleSecretSelect: (key: string) => void; getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined; onSecretCreate: (env: string, key: string, value: string) => Promise; onSecretUpdate: (env: string, key: string, value: string, secretId?: string) => Promise; @@ -39,7 +41,9 @@ export const SecretOverviewTableRow = ({ onSecretCreate, onSecretDelete, isImportedSecretPresentInEnv, - expandableColWidth + expandableColWidth, + onToggleSecretSelect, + isSelected }: Props) => { const [isFormExpanded, setIsFormExpanded] = useToggle(); const totalCols = environments.length + 1; // secret key row @@ -56,7 +60,21 @@ export const SecretOverviewTableRow = ({
- + { + onToggleSecretSelect(secretKey); + }} + onClick={(e) => { + e.stopPropagation(); + }} + className={twMerge("hidden group-hover:flex", isSelected && "flex")} + /> +
{secretKey}
diff --git a/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx b/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx new file mode 100644 index 0000000000..6b95ffccf2 --- /dev/null +++ b/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx @@ -0,0 +1,78 @@ +import { subject } from "@casl/ability"; +import { faMinusSquare, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { twMerge } from "tailwind-merge"; + +import { Button, DeleteActionModal, IconButton, Tooltip } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; +import { usePopUp } from "@app/hooks"; + +import { useSelectedEntries, useSelectedEntryActions } from "../../SecretOverviewPage.store"; + +type Props = { + secretPath: string; +}; + +export const SelectionPanel = ({ secretPath }: Props) => { + const { permission } = useProjectPermission(); + + const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ + "bulkDeleteEntries" + ] as const); + + const selectedEntries = useSelectedEntries(); + const { reset: resetSelectedEntries } = useSelectedEntryActions(); + const selectedCount = + Object.keys(selectedEntries.folder).length + Object.keys(selectedEntries.secret).length; + + const isMultiSelectActive = selectedCount > 0; + + // TODO: REVISIT RBAC + const shouldShowDelete = permission.can( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { environment: "", secretPath }) + ); + + const handleBulkDelete = async () => { + handlePopUpClose("bulkDeleteEntries"); + }; + + return ( + <> +
+
+ + + + + +
{selectedCount} Selected
+ {shouldShowDelete && ( + + )} +
+
+ handlePopUpToggle("bulkDeleteEntries", isOpen)} + onDeleteApproved={handleBulkDelete} + /> + + ); +}; From 5d7c0f30c8a01894f45712bdab12d63eb335ac1e Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 6 May 2024 22:58:35 -0700 Subject: [PATCH 008/188] Fix typo universal auth --- backend/src/server/routes/v1/identity-aws-iam-auth-router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts index 0245b7607f..12003a1588 100644 --- a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts +++ b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts @@ -20,7 +20,7 @@ export const registerIdentityAwsIamAuthRouter = async (server: FastifyZodProvide rateLimit: writeLimit }, schema: { - description: "Login with Universal Auth", + description: "Login with AWS IAM Auth", body: z.object({ identityId: z.string().describe(AWS_IAM_AUTH.LOGIN.identityId), iamHttpRequestMethod: z.string().default("POST").describe(AWS_IAM_AUTH.LOGIN.iamHttpRequestMethod), From e3c80309c3bfc9f7cd2705fefaa5bfa5489cad1c Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 6 May 2024 23:03:45 -0700 Subject: [PATCH 009/188] Move aws auth migration file to front --- ...ty-aws-iam-auth.ts => 20240507055915_identity-aws-iam-auth.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backend/src/db/migrations/{20240502044531_identity-aws-iam-auth.ts => 20240507055915_identity-aws-iam-auth.ts} (100%) diff --git a/backend/src/db/migrations/20240502044531_identity-aws-iam-auth.ts b/backend/src/db/migrations/20240507055915_identity-aws-iam-auth.ts similarity index 100% rename from backend/src/db/migrations/20240502044531_identity-aws-iam-auth.ts rename to backend/src/db/migrations/20240507055915_identity-aws-iam-auth.ts From a9b72b2da391003c18cdd2db230a02e49bc41b8b Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 7 May 2024 15:16:37 +0800 Subject: [PATCH 010/188] feat: added handling of folder/secret deletion --- .../src/hooks/api/secretFolders/queries.tsx | 16 +++- .../SecretOverviewPage/SecretOverviewPage.tsx | 9 ++- .../SelectionPanel/SelectionPanel.tsx | 78 ++++++++++++++++++- 3 files changed, 97 insertions(+), 6 deletions(-) diff --git a/frontend/src/hooks/api/secretFolders/queries.tsx b/frontend/src/hooks/api/secretFolders/queries.tsx index bcda2b0a49..71c63f3eb3 100644 --- a/frontend/src/hooks/api/secretFolders/queries.tsx +++ b/frontend/src/hooks/api/secretFolders/queries.tsx @@ -94,7 +94,21 @@ export const useGetFoldersByEnv = ({ [(folders || []).map((folder) => folder.data)] ); - return { folders, folderNames, isFolderPresentInEnv }; + const getFolderByNameAndEnv = useCallback( + (name: string, env: string) => { + const selectedEnvIndex = environments.indexOf(env); + if (selectedEnvIndex !== -1) { + return folders?.[selectedEnvIndex]?.data?.find( + ({ name: folderName }) => folderName === name + ); + } + + return undefined; + }, + [(folders || []).map((folder) => folder.data)] + ); + + return { folders, folderNames, isFolderPresentInEnv, getFolderByNameAndEnv }; }; export const useCreateFolder = () => { diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx index c6d97361aa..cd00af883c 100644 --- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx +++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx @@ -134,7 +134,8 @@ export const SecretOverviewPage = () => { secretPath, decryptFileKey: latestFileKey! }); - const { folders, folderNames, isFolderPresentInEnv } = useGetFoldersByEnv({ + + const { folders, folderNames, isFolderPresentInEnv, getFolderByNameAndEnv } = useGetFoldersByEnv({ projectId: workspaceId, path: secretPath, environments: userAvailableEnvs.map(({ slug }) => slug) @@ -548,7 +549,11 @@ export const SecretOverviewPage = () => {
- +
diff --git a/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx b/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx index 6b95ffccf2..f68e804bf5 100644 --- a/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx @@ -3,17 +3,27 @@ import { faMinusSquare, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; +import { createNotification } from "@app/components/notifications"; import { Button, DeleteActionModal, IconButton, Tooltip } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useWorkspace +} from "@app/context"; import { usePopUp } from "@app/hooks"; +import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api"; +import { DecryptedSecret, TDeleteSecretBatchDTO, TSecretFolder } from "@app/hooks/api/types"; import { useSelectedEntries, useSelectedEntryActions } from "../../SecretOverviewPage.store"; type Props = { secretPath: string; + getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined; + getFolderByNameAndEnv: (name: string, env: string) => TSecretFolder | undefined; }; -export const SelectionPanel = ({ secretPath }: Props) => { +export const SelectionPanel = ({ getFolderByNameAndEnv, getSecretByKey, secretPath }: Props) => { const { permission } = useProjectPermission(); const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ @@ -25,6 +35,12 @@ export const SelectionPanel = ({ secretPath }: Props) => { const selectedCount = Object.keys(selectedEntries.folder).length + Object.keys(selectedEntries.secret).length; + const { currentWorkspace } = useWorkspace(); + const workspaceId = currentWorkspace?.id || ""; + const userAvailableEnvs = currentWorkspace?.environments || []; + const { mutateAsync: deleteBatchSecretV3 } = useDeleteSecretBatch(); + const { mutateAsync: deleteFolder } = useDeleteFolder(); + const isMultiSelectActive = selectedCount > 0; // TODO: REVISIT RBAC @@ -34,7 +50,63 @@ export const SelectionPanel = ({ secretPath }: Props) => { ); const handleBulkDelete = async () => { - handlePopUpClose("bulkDeleteEntries"); + const promises = userAvailableEnvs.map(async (env) => { + await Promise.all( + Object.keys(selectedEntries.folder).map(async (folderName) => { + const folder = getFolderByNameAndEnv(folderName, env.slug); + if (folder) { + await deleteFolder({ + folderId: folder?.id, + path: secretPath, + environment: env.slug, + projectId: workspaceId + }); + } + }) + ); + + const secretsToDelete = Object.keys(selectedEntries.secret).reduce( + (accum: TDeleteSecretBatchDTO["secrets"], secretName) => { + const entry = getSecretByKey(env.slug, secretName); + if (entry) { + return [ + ...accum, + { + secretName: entry.key, + type: "shared" as "shared" + } + ]; + } + return accum; + }, + [] + ); + + if (secretsToDelete.length > 0) { + await deleteBatchSecretV3({ + secretPath, + workspaceId, + environment: env.slug, + secrets: secretsToDelete + }); + } + }); + + const results = await Promise.allSettled(promises); + const areEntriesDeleted = results.some((result) => result.status === "fulfilled"); + if (areEntriesDeleted) { + handlePopUpClose("bulkDeleteEntries"); + resetSelectedEntries(); + createNotification({ + type: "success", + text: "Successfully deleted selected secrets and folders" + }); + } else { + createNotification({ + type: "error", + text: "Failed to delete selected secrets and folders" + }); + } }; return ( From 536f51f6bacba9808e4e8e401bff6f2ffe0dce3d Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 7 May 2024 15:21:17 +0800 Subject: [PATCH 011/188] misc: added descriptive error message --- .../src/views/SecretOverviewPage/SecretOverviewPage.store.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.store.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.store.tsx index df239d6ae1..db544c7e83 100644 --- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.store.tsx +++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.store.tsx @@ -70,7 +70,7 @@ export const StoreProvider = ({ children }: { children: ReactNode }) => { const useStoreContext = (selector: (state: SelectedEntriesState) => T): T => { const ctx = useContext(StoreContext); - if (!ctx) throw new Error("Missing "); + if (!ctx) throw new Error("Missing context provider"); return useStore(ctx, selector); }; From b6a73459a8cfd1ecd74c639f9da28d2837f20d48 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 7 May 2024 16:37:10 +0800 Subject: [PATCH 012/188] misc: addressed rbac for bulk delete in overview --- .../SelectionPanel/SelectionPanel.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx b/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx index f68e804bf5..03d2d44be6 100644 --- a/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx @@ -43,14 +43,26 @@ export const SelectionPanel = ({ getFolderByNameAndEnv, getSecretByKey, secretPa const isMultiSelectActive = selectedCount > 0; - // TODO: REVISIT RBAC - const shouldShowDelete = permission.can( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment: "", secretPath }) + // user should have the ability to delete secrets/folders in at least one of the envs + const shouldShowDelete = userAvailableEnvs.some((env) => + permission.can( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath }) + ) ); const handleBulkDelete = async () => { const promises = userAvailableEnvs.map(async (env) => { + // additional check: ensure that bulk delete is only executed on envs that user has access to + if ( + permission.cannot( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { environment: env.slug, secretPath }) + ) + ) { + return; + } + await Promise.all( Object.keys(selectedEntries.folder).map(async (folderName) => { const folder = getFolderByNameAndEnv(folderName, env.slug); From 06c103c10af0ec6a150ae29303695d5a53e26d49 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Tue, 7 May 2024 22:19:20 +0800 Subject: [PATCH 013/188] misc: added handling for no changes made --- .../components/SelectionPanel/SelectionPanel.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx b/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx index 03d2d44be6..4f331e9891 100644 --- a/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx @@ -52,6 +52,8 @@ export const SelectionPanel = ({ getFolderByNameAndEnv, getSecretByKey, secretPa ); const handleBulkDelete = async () => { + let processedEntries = 0; + const promises = userAvailableEnvs.map(async (env) => { // additional check: ensure that bulk delete is only executed on envs that user has access to if ( @@ -67,6 +69,7 @@ export const SelectionPanel = ({ getFolderByNameAndEnv, getSecretByKey, secretPa Object.keys(selectedEntries.folder).map(async (folderName) => { const folder = getFolderByNameAndEnv(folderName, env.slug); if (folder) { + processedEntries += 1; await deleteFolder({ folderId: folder?.id, path: secretPath, @@ -95,6 +98,7 @@ export const SelectionPanel = ({ getFolderByNameAndEnv, getSecretByKey, secretPa ); if (secretsToDelete.length > 0) { + processedEntries += secretsToDelete.length; await deleteBatchSecretV3({ secretPath, workspaceId, @@ -106,7 +110,13 @@ export const SelectionPanel = ({ getFolderByNameAndEnv, getSecretByKey, secretPa const results = await Promise.allSettled(promises); const areEntriesDeleted = results.some((result) => result.status === "fulfilled"); - if (areEntriesDeleted) { + if (processedEntries === 0) { + handlePopUpClose("bulkDeleteEntries"); + createNotification({ + type: "info", + text: "No changes have been made. Ensure that you have sufficient access." + }); + } else if (areEntriesDeleted) { handlePopUpClose("bulkDeleteEntries"); resetSelectedEntries(); createNotification({ From c5cd5047d71cd1888b445f74e8b62dbeb37ea42a Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 7 May 2024 07:59:37 -0700 Subject: [PATCH 014/188] Update trusted email migration file with backfill --- .../20240507032811_trusted-saml-ldap-emails.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/backend/src/db/migrations/20240507032811_trusted-saml-ldap-emails.ts b/backend/src/db/migrations/20240507032811_trusted-saml-ldap-emails.ts index 63aa75ad8f..410ee0f00e 100644 --- a/backend/src/db/migrations/20240507032811_trusted-saml-ldap-emails.ts +++ b/backend/src/db/migrations/20240507032811_trusted-saml-ldap-emails.ts @@ -5,9 +5,16 @@ import { TableName } from "../schemas"; export async function up(knex: Knex): Promise { const isUsersTablePresent = await knex.schema.hasTable(TableName.Users); if (isUsersTablePresent) { - await knex.schema.alterTable(TableName.Users, (t) => { - t.boolean("isEmailVerified"); - }); + const hasIsEmailVerifiedColumn = await knex.schema.hasColumn(TableName.Users, "isEmailVerified"); + + if (!hasIsEmailVerifiedColumn) { + await knex.schema.alterTable(TableName.Users, (t) => { + t.boolean("isEmailVerified").defaultTo(false); + }); + } + + // Backfilling the isEmailVerified to true where isAccepted is true + await knex(TableName.Users).update({ isEmailVerified: true }).where("isAccepted", true); } const isUserAliasTablePresent = await knex.schema.hasTable(TableName.UserAliases); From 1d2f10178f59267743b5b815f3dd9dcdf041be18 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 29 Mar 2024 16:24:15 +0100 Subject: [PATCH 015/188] Draft --- .../src/ee/services/license/licence-fns.ts | 2 +- frontend/src/components/basic/Divider.tsx | 15 ++++++++ .../permissions/PermissionDeniedBanner.tsx | 36 ++++++++++--------- frontend/src/layouts/AppLayout/AppLayout.tsx | 4 +-- .../SecretApprovalPage/SecretApprovalPage.tsx | 22 +++++++----- 5 files changed, 51 insertions(+), 28 deletions(-) create mode 100644 frontend/src/components/basic/Divider.tsx diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 189a3c4e06..492d09ec3d 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ status: null, trial_end: null, has_used_trial: true, - secretApproval: false, + secretApproval: true, secretRotation: true }); diff --git a/frontend/src/components/basic/Divider.tsx b/frontend/src/components/basic/Divider.tsx new file mode 100644 index 0000000000..f1a570d7b1 --- /dev/null +++ b/frontend/src/components/basic/Divider.tsx @@ -0,0 +1,15 @@ +import { twMerge } from "tailwind-merge"; + +interface IProps { + className?: string; +} + +const Divider = ({ className }: IProps): JSX.Element => { + return ( +
+ + ); +}; + +export default Divider; diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx index 067ee9c4a9..f9707d6347 100644 --- a/frontend/src/components/permissions/PermissionDeniedBanner.tsx +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -3,6 +3,8 @@ import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; +import { Button } from "../v2"; + type Props = { containerClassName?: string; className?: string; @@ -17,24 +19,24 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children containerClassName )} > -
-
- -
-
-
Access Restricted
- {children || ( -
- Your role has limited permissions, please
contact your administrator to gain - access -
- )} +
+
+
+ +
+
+
Access Restricted
+ {children || ( +
+ Your role has limited permissions, please
contact your administrator to gain + access +
+ )} +
+
); diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 2550150f3a..a6c25c7c27 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -115,7 +115,7 @@ type TAddProjectFormData = yup.InferType; export const AppLayout = ({ children }: LayoutProps) => { const router = useRouter(); - + const { mutateAsync } = useGetOrgTrialUrl(); const { workspaces, currentWorkspace } = useWorkspace(); @@ -554,7 +554,7 @@ export const AppLayout = ({ children }: LayoutProps) => { } icon="system-outline-189-domain-verification" > - Secret Approvals + Approvals {Boolean(secretApprovalReqCount?.open) && ( {secretApprovalReqCount?.open} diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index ce1896efa4..4ab5830fe8 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import Divider from "@app/components/basic/Divider"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { useWorkspace } from "@app/context"; @@ -9,8 +10,10 @@ import { SecretApprovalPolicyList } from "./components/SecretApprovalPolicyList" import { SecretApprovalRequest } from "./components/SecretApprovalRequest"; enum TabSection { - ApprovalRequests = "approval-requests", - Rules = "approval-rules" + SecretApprovalRequests = "approval-requests", + SecretPolicies = "approval-rules", + ResourcePolicies = "resource-rules", + ResourceApprovalRequests = "resource-requests" } export const SecretApprovalPage = () => { @@ -21,7 +24,7 @@ export const SecretApprovalPage = () => {
-

Secret Approval Workflows

+

Approval Workflows

Create approval policies for any modifications to secrets in sensitive environments and folders. @@ -39,15 +42,18 @@ export const SecretApprovalPage = () => {

- + - Secret PRs - Policies + Secret Approvals + Secret Policies + + Resource Approvals + Resource Policies - + - + From 12c655a152e0e5d9e752ad5579333ad6e8d64277 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 1 Apr 2024 08:22:46 -0700 Subject: [PATCH 016/188] Feat: Request Access --- backend/src/@types/fastify.d.ts | 2 ++ backend/src/@types/knex.d.ts | 19 +++++++++++++++++++ backend/src/db/schemas/index.ts | 2 ++ backend/src/db/schemas/models.ts | 6 ++++++ backend/src/ee/routes/v1/index.ts | 3 +++ .../server/plugins/auth/inject-identity.ts | 8 ++++++++ 6 files changed, 40 insertions(+) diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index b3e9d99521..12c07486f2 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -1,6 +1,7 @@ import "fastify"; import { TUsers } from "@app/db/schemas"; +import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service"; import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TAuditLogStreamServiceFactory } from "@app/ee/services/audit-log-stream/audit-log-stream-service"; @@ -113,6 +114,7 @@ declare module "fastify" { identityAccessToken: TIdentityAccessTokenServiceFactory; identityProject: TIdentityProjectServiceFactory; identityUa: TIdentityUaServiceFactory; + accessApprovalPolicy: TAccessApprovalPolicyServiceFactory; secretApprovalPolicy: TSecretApprovalPolicyServiceFactory; secretApprovalRequest: TSecretApprovalRequestServiceFactory; secretRotation: TSecretRotationServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index a7d76e9449..b6e8744edf 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -2,6 +2,12 @@ import { Knex } from "knex"; import { TableName, + TAccessApprovalPolicies, + TAccessApprovalPoliciesApprovers, + TAccessApprovalPoliciesApproversInsert, + TAccessApprovalPoliciesApproversUpdate, + TAccessApprovalPoliciesInsert, + TAccessApprovalPoliciesUpdate, TApiKeys, TApiKeysInsert, TApiKeysUpdate, @@ -344,6 +350,19 @@ declare module "knex/types/tables" { TIdentityProjectAdditionalPrivilegeInsert, TIdentityProjectAdditionalPrivilegeUpdate >; + + [TableName.AccessApprovalPolicy]: Knex.CompositeTableType< + TAccessApprovalPolicies, + TAccessApprovalPoliciesInsert, + TAccessApprovalPoliciesUpdate + >; + + [TableName.AccessApprovalPolicyApprover]: Knex.CompositeTableType< + TAccessApprovalPoliciesApprovers, + TAccessApprovalPoliciesApproversInsert, + TAccessApprovalPoliciesApproversUpdate + >; + [TableName.ScimToken]: Knex.CompositeTableType; [TableName.SecretApprovalPolicy]: Knex.CompositeTableType< TSecretApprovalPolicies, diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 0eb9b19868..f02188577c 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -1,3 +1,5 @@ +export * from "./access-approval-policies"; +export * from "./access-approval-policies-approvers"; export * from "./api-keys"; export * from "./audit-log-streams"; export * from "./audit-logs"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 3baa7f40d2..8e66cabe4b 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -50,12 +50,18 @@ export enum TableName { IdentityProjectMembershipRole = "identity_project_membership_role", IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege", ScimToken = "scim_tokens", + + // New tables so far + AccessApprovalPolicy = "access_approval_policies", + AccessApprovalPolicyApprover = "access_approval_policies_approvers", + SecretApprovalPolicy = "secret_approval_policies", SecretApprovalPolicyApprover = "secret_approval_policies_approvers", SecretApprovalRequest = "secret_approval_requests", SecretApprovalRequestReviewer = "secret_approval_requests_reviewers", SecretApprovalRequestSecret = "secret_approval_requests_secrets", SecretApprovalRequestSecretTag = "secret_approval_request_secret_tags", + SecretRotation = "secret_rotations", SecretRotationOutput = "secret_rotation_outputs", SamlConfig = "saml_configs", diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index cf325b2e36..086a1b3d64 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,4 +1,5 @@ import { registerAuditLogStreamRouter } from "./audit-log-stream-router"; +import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router"; import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerGroupRouter } from "./group-router"; @@ -41,6 +42,8 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { prefix: "/secret-rotation-providers" }); + await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals" }); + await server.register( async (dynamicSecretRouter) => { await dynamicSecretRouter.register(registerDynamicSecretRouter); diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 4c06837972..71522dcc24 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -108,8 +108,16 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { if (req.url.includes("/api/v3/auth/")) { return; } + + console.log("authMode", authMode); + console.log("authMode", authMode); + console.log("authMode", authMode); + console.log("authMode", authMode); + if (!authMode) return; + console.log("authMode", authMode); + switch (authMode) { case AuthMode.JWT: { const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token); From 91bf6a6dad439a3ead05fbd9932ca5ff0b070ca1 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 1 Apr 2024 08:23:02 -0700 Subject: [PATCH 017/188] Fix: Remove logs --- backend/src/server/plugins/auth/inject-identity.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 71522dcc24..d8814dd40c 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -109,15 +109,8 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { return; } - console.log("authMode", authMode); - console.log("authMode", authMode); - console.log("authMode", authMode); - console.log("authMode", authMode); - if (!authMode) return; - console.log("authMode", authMode); - switch (authMode) { case AuthMode.JWT: { const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token); From 68a11db1c6914f4987ca0d022ec18331a32c286c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 1 Apr 2024 09:49:22 -0700 Subject: [PATCH 018/188] Feat: Request access --- backend/scripts/generate-schema-types.ts | 2 +- .../20240330075122_access-approval-policy.ts | 38 +++ .../access-approval-policies-approvers.ts | 25 ++ .../db/schemas/access-approval-policies.ts | 23 ++ .../v1/access-approval-policy-router.ts | 133 ++++++++++ .../access-approval-policy-approver-dal.ts | 10 + .../access-approval-policy-dal.ts | 76 ++++++ .../access-approval-policy-service.ts | 206 +++++++++++++++ .../access-approval-policy-types.ts | 27 ++ backend/src/server/routes/index.ts | 18 ++ .../src/hooks/api/accessApproval/index.tsx | 6 + .../src/hooks/api/accessApproval/mutation.tsx | 58 ++++ .../src/hooks/api/accessApproval/queries.tsx | 40 +++ .../src/hooks/api/accessApproval/types.ts | 45 ++++ frontend/src/hooks/api/index.tsx | 1 + frontend/src/hooks/api/types.ts | 17 +- .../SecretApprovalPage/SecretApprovalPage.tsx | 9 +- .../AccessApprovalPolicyList.tsx | 166 ++++++++++++ .../components/AccessApprovalPolicyRow.tsx | 145 ++++++++++ .../components/AccessPolicyForm.tsx | 250 ++++++++++++++++++ .../AccessApprovalPolicyList/index.tsx | 1 + 21 files changed, 1285 insertions(+), 11 deletions(-) create mode 100644 backend/src/db/migrations/20240330075122_access-approval-policy.ts create mode 100644 backend/src/db/schemas/access-approval-policies-approvers.ts create mode 100644 backend/src/db/schemas/access-approval-policies.ts create mode 100644 backend/src/ee/routes/v1/access-approval-policy-router.ts create mode 100644 backend/src/ee/services/access-approval-policy/access-approval-policy-approver-dal.ts create mode 100644 backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts create mode 100644 backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts create mode 100644 backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts create mode 100644 frontend/src/hooks/api/accessApproval/index.tsx create mode 100644 frontend/src/hooks/api/accessApproval/mutation.tsx create mode 100644 frontend/src/hooks/api/accessApproval/queries.tsx create mode 100644 frontend/src/hooks/api/accessApproval/types.ts create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/index.tsx diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 8c913991fd..28b736152c 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -5,7 +5,7 @@ import knex from "knex"; import { writeFileSync } from "fs"; dotenv.config({ - path: path.join(__dirname, "../../.env.migration") + path: path.join(__dirname, "../../.env") }); const db = knex({ diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts new file mode 100644 index 0000000000..0595d5c6e0 --- /dev/null +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -0,0 +1,38 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.AccessApprovalPolicy))) { + await knex.schema.createTable(TableName.AccessApprovalPolicy, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("name").notNullable(); + t.integer("approvals").defaultTo(1).notNullable(); + t.uuid("envId").notNullable(); + t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); + t.timestamps(true, true, true); + }); + } + await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); + + if (!(await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover))) { + await knex.schema.createTable(TableName.AccessApprovalPolicyApprover, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.uuid("approverId").notNullable(); + t.foreign("approverId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE"); + t.uuid("policyId").notNullable(); + t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE"); + t.timestamps(true, true, true); + }); + } + + await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy); + await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover); + await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); + await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); +} diff --git a/backend/src/db/schemas/access-approval-policies-approvers.ts b/backend/src/db/schemas/access-approval-policies-approvers.ts new file mode 100644 index 0000000000..4ebbfa9aec --- /dev/null +++ b/backend/src/db/schemas/access-approval-policies-approvers.ts @@ -0,0 +1,25 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const AccessApprovalPoliciesApproversSchema = z.object({ + id: z.string().uuid(), + approverId: z.string().uuid(), + policyId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TAccessApprovalPoliciesApprovers = z.infer; +export type TAccessApprovalPoliciesApproversInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TAccessApprovalPoliciesApproversUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/access-approval-policies.ts b/backend/src/db/schemas/access-approval-policies.ts new file mode 100644 index 0000000000..5500497022 --- /dev/null +++ b/backend/src/db/schemas/access-approval-policies.ts @@ -0,0 +1,23 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const AccessApprovalPoliciesSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + approvals: z.number().default(1), + envId: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TAccessApprovalPolicies = z.infer; +export type TAccessApprovalPoliciesInsert = Omit, TImmutableDBKeys>; +export type TAccessApprovalPoliciesUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/ee/routes/v1/access-approval-policy-router.ts b/backend/src/ee/routes/v1/access-approval-policy-router.ts new file mode 100644 index 0000000000..ec5331a891 --- /dev/null +++ b/backend/src/ee/routes/v1/access-approval-policy-router.ts @@ -0,0 +1,133 @@ +import { nanoid } from "nanoid"; +import { z } from "zod"; + +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { sapPubSchema } from "@app/server/routes/sanitizedSchemas"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/", + method: "POST", + schema: { + body: z + .object({ + workspaceId: z.string(), + name: z.string().optional(), + environment: z.string(), + approvers: z.string().array().min(1), + approvals: z.number().min(1).default(1) + }) + .refine((data) => data.approvals <= data.approvers.length, { + path: ["approvals"], + message: "The number of approvals should be lower than the number of approvers." + }), + response: { + 200: z.object({ + approval: sapPubSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const approval = await server.services.accessApprovalPolicy.createAccessApprovalPolicy({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.body.workspaceId, + ...req.body, + name: req.body.name ?? `${req.body.environment}-${nanoid(3)}` + }); + return { approval }; + } + }); + + server.route({ + url: "/", + method: "GET", + schema: { + querystring: z.object({ + workspaceId: z.string().trim() + }), + response: { + 200: z.object({ + approvals: sapPubSchema.merge(z.object({ approvers: z.string().array() })).array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const approvals = await server.services.accessApprovalPolicy.getAccessApprovalPolicyByProjectId({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + projectId: req.query.workspaceId + }); + return { approvals }; + } + }); + + server.route({ + url: "/:policyId", + method: "PATCH", + schema: { + params: z.object({ + policyId: z.string() + }), + body: z + .object({ + name: z.string().optional(), + approvers: z.string().array().min(1), + approvals: z.number().min(1).default(1) + }) + .refine((data) => data.approvals <= data.approvers.length, { + path: ["approvals"], + message: "The number of approvals should be lower than the number of approvers." + }), + response: { + 200: z.object({ + approval: sapPubSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + await server.services.accessApprovalPolicy.updateAccessApprovalPolicy({ + policyId: req.params.policyId, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + ...req.body + }); + } + }); + + server.route({ + url: "/:policyId", + method: "DELETE", + schema: { + params: z.object({ + policyId: z.string() + }), + response: { + 200: z.object({ + approval: sapPubSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const approval = await server.services.accessApprovalPolicy.deleteAccessApprovalPolicy({ + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + policyId: req.params.policyId + }); + return { approval }; + } + }); +}; diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-approver-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-approver-dal.ts new file mode 100644 index 0000000000..e14854d8ff --- /dev/null +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-approver-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TAccessApprovalPolicyApproverDALFactory = ReturnType; + +export const accessApprovalPolicyApproverDALFactory = (db: TDbClient) => { + const accessApprovalPolicyApproverOrm = ormify(db, TableName.AccessApprovalPolicyApprover); + return { ...accessApprovalPolicyApproverOrm }; +}; diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts new file mode 100644 index 0000000000..792a851c37 --- /dev/null +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-dal.ts @@ -0,0 +1,76 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { TableName, TAccessApprovalPolicies } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { buildFindFilter, mergeOneToManyRelation, ormify, selectAllTableCols, TFindFilter } from "@app/lib/knex"; + +export type TAccessApprovalPolicyDALFactory = ReturnType; + +export const accessApprovalPolicyDALFactory = (db: TDbClient) => { + const accessApprovalPolicyOrm = ormify(db, TableName.AccessApprovalPolicy); + + const sapFindQuery = async (tx: Knex, filter: TFindFilter) => { + const result = await tx(TableName.AccessApprovalPolicy) + // eslint-disable-next-line + .where(buildFindFilter(filter)) + .join(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`) + .join( + TableName.AccessApprovalPolicyApprover, + `${TableName.AccessApprovalPolicy}.id`, + `${TableName.AccessApprovalPolicyApprover}.policyId` + ) + .select(tx.ref("approverId").withSchema(TableName.AccessApprovalPolicyApprover)) + .select(tx.ref("name").withSchema(TableName.Environment).as("envName")) + .select(tx.ref("slug").withSchema(TableName.Environment).as("envSlug")) + .select(tx.ref("id").withSchema(TableName.Environment).as("envId")) + .select(tx.ref("projectId").withSchema(TableName.Environment)) + .select(selectAllTableCols(TableName.AccessApprovalPolicy)); + + return result; + }; + + const findById = async (id: string, tx?: Knex) => { + try { + const doc = await sapFindQuery(tx || db, { + [`${TableName.AccessApprovalPolicy}.id` as "id"]: id + }); + const formatedDoc = mergeOneToManyRelation( + doc, + "id", + ({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({ + ...el, + envId, + environment: { id: envId, name, slug } + }), + ({ approverId }) => approverId, + "approvers" + ); + return formatedDoc?.[0]; + } catch (error) { + throw new DatabaseError({ error, name: "FindById" }); + } + }; + + const find = async (filter: TFindFilter, tx?: Knex) => { + try { + const docs = await sapFindQuery(tx || db, filter); + const formatedDoc = mergeOneToManyRelation( + docs, + "id", + ({ approverId, envId, envName: name, envSlug: slug, ...el }) => ({ + ...el, + envId, + environment: { id: envId, name, slug } + }), + ({ approverId }) => approverId, + "approvers" + ); + return formatedDoc; + } catch (error) { + throw new DatabaseError({ error, name: "Find" }); + } + }; + + return { ...accessApprovalPolicyOrm, find, findById }; +}; 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 new file mode 100644 index 0000000000..d1f0ce1e72 --- /dev/null +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-service.ts @@ -0,0 +1,206 @@ +import { ForbiddenError } from "@casl/ability"; + +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { BadRequestError } from "@app/lib/errors"; +import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; +import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; + +import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal"; +import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal"; +import { + TCreateAccessApprovalPolicy, + TDeleteAccessApprovalPolicy, + TListAccessApprovalPoliciesDTO, + TUpdateAccessApprovalPolicy +} from "./access-approval-policy-types"; + +type TSecretApprovalPolicyServiceFactoryDep = { + permissionService: Pick; + accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory; + projectEnvDAL: Pick; + accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory; + projectMembershipDAL: Pick; +}; + +export type TAccessApprovalPolicyServiceFactory = ReturnType; + +export const accessApprovalPolicyServiceFactory = ({ + accessApprovalPolicyDAL, + accessApprovalPolicyApproverDAL, + permissionService, + projectEnvDAL, + projectMembershipDAL +}: TSecretApprovalPolicyServiceFactoryDep) => { + const createAccessApprovalPolicy = async ({ + name, + actor, + actorId, + actorOrgId, + actorAuthMethod, + approvals, + approvers, + projectId, + environment + }: TCreateAccessApprovalPolicy) => { + if (approvals > approvers.length) + throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.SecretApproval + ); + const env = await projectEnvDAL.findOne({ slug: environment, projectId }); + if (!env) throw new BadRequestError({ message: "Environment not found" }); + + const secretApprovers = await projectMembershipDAL.find({ + projectId, + $in: { id: approvers } + }); + if (secretApprovers.length !== approvers.length) { + throw new BadRequestError({ message: "Approver not found in project" }); + } + + const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => { + const doc = await accessApprovalPolicyDAL.create( + { + envId: env.id, + approvals, + name + }, + tx + ); + await accessApprovalPolicyApproverDAL.insertMany( + secretApprovers.map(({ id }) => ({ + approverId: id, + policyId: doc.id + })), + tx + ); + return doc; + }); + return { ...accessApproval, environment: env, projectId }; + }; + + const getAccessApprovalPolicyByProjectId = async ({ + actorId, + actor, + actorOrgId, + actorAuthMethod, + projectId + }: TListAccessApprovalPoliciesDTO) => { + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + + const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId }); + return accessApprovalPolicies; + }; + + const updateAccessApprovalPolicy = async ({ + policyId, + approvers, + name, + actorId, + actor, + actorOrgId, + actorAuthMethod, + approvals + }: TUpdateAccessApprovalPolicy) => { + const accessApprovalPolicy = await accessApprovalPolicyDAL.findById(policyId); + if (!accessApprovalPolicy) throw new BadRequestError({ message: "Secret approval policy not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + accessApprovalPolicy.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); + + const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { + const doc = await accessApprovalPolicyDAL.updateById( + accessApprovalPolicy.id, + { + approvals, + name + }, + tx + ); + if (approvers) { + // Find the workspace project memberships of the users passed in the approvers array + const secretApprovers = await projectMembershipDAL.find( + { + projectId: accessApprovalPolicy.projectId, + $in: { id: approvers } + }, + { tx } + ); + if (secretApprovers.length !== approvers.length) + throw new BadRequestError({ message: "Approver not found in project" }); + if (doc.approvals > secretApprovers.length) + throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); + await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx); + await accessApprovalPolicyApproverDAL.insertMany( + secretApprovers.map(({ id }) => ({ + approverId: id, + policyId: doc.id + })), + tx + ); + } + return doc; + }); + return { + ...updatedPolicy, + environment: accessApprovalPolicy.environment, + projectId: accessApprovalPolicy.projectId + }; + }; + + const deleteAccessApprovalPolicy = async ({ + policyId, + actor, + actorId, + actorAuthMethod, + actorOrgId + }: TDeleteAccessApprovalPolicy) => { + const policy = await accessApprovalPolicyDAL.findById(policyId); + if (!policy) throw new BadRequestError({ message: "Secret approval policy not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + policy.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.SecretApproval + ); + + await accessApprovalPolicyDAL.deleteById(policyId); + return policy; + }; + + return { + createAccessApprovalPolicy, + deleteAccessApprovalPolicy, + updateAccessApprovalPolicy, + getAccessApprovalPolicyByProjectId + }; +}; 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 new file mode 100644 index 0000000000..034132d04d --- /dev/null +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-types.ts @@ -0,0 +1,27 @@ +import { TProjectPermission } from "@app/lib/types"; + +export type TCreateAccessApprovalPolicy = { + approvals: number; + environment: string; + approvers: string[]; + projectId: string; + name: string; +} & Omit; + +export type TUpdateAccessApprovalPolicy = { + policyId: string; + approvals?: number; + approvers: string[]; + name?: string; +} & Omit; + +export type TDeleteAccessApprovalPolicy = { + policyId: string; +} & Omit; + +export type TListAccessApprovalPoliciesDTO = TProjectPermission; + +export type TGetBoardAccessApprovalPolicy = { + projectId: string; + environment: string; +} & Omit; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index aeb66d93f9..0a2c77c9f7 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -2,6 +2,9 @@ import { Knex } from "knex"; import { z } from "zod"; import { registerV1EERoutes } from "@app/ee/routes/v1"; +import { accessApprovalPolicyApproverDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-approver-dal"; +import { accessApprovalPolicyDALFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-dal"; +import { accessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service"; import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal"; import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue"; import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service"; @@ -207,6 +210,10 @@ export const registerRoutes = async ( const scimDAL = scimDALFactory(db); const ldapConfigDAL = ldapConfigDALFactory(db); const ldapGroupMapDAL = ldapGroupMapDALFactory(db); + + const accessApprovalPolicyDAL = accessApprovalPolicyDALFactory(db); + const accessApprovalPolicyApproverDAL = accessApprovalPolicyApproverDALFactory(db); + const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db); const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db); const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db); @@ -265,6 +272,15 @@ export const registerRoutes = async ( secretApprovalPolicyDAL }); const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL }); + + const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({ + accessApprovalPolicyDAL, + accessApprovalPolicyApproverDAL, + permissionService, + projectEnvDAL, + projectMembershipDAL + }); + const samlService = samlConfigServiceFactory({ permissionService, orgBotDAL, @@ -596,6 +612,7 @@ export const registerRoutes = async ( secretVersionTagDAL, secretQueueService }); + const secretRotationQueue = secretRotationQueueFactory({ telemetryService, secretRotationDAL, @@ -732,6 +749,7 @@ export const registerRoutes = async ( identityProject: identityProjectService, identityUa: identityUaService, secretApprovalPolicy: sapService, + accessApprovalPolicy: accessApprovalPolicyService, secretApprovalRequest: sarService, secretRotation: secretRotationService, dynamicSecret: dynamicSecretService, diff --git a/frontend/src/hooks/api/accessApproval/index.tsx b/frontend/src/hooks/api/accessApproval/index.tsx new file mode 100644 index 0000000000..537dfe3be4 --- /dev/null +++ b/frontend/src/hooks/api/accessApproval/index.tsx @@ -0,0 +1,6 @@ +export { + useCreateAccessApprovalPolicy, + useDeleteAccessApprovalPolicy, + useUpdateAccessApprovalPolicy +} from "./mutation"; +export { useGetAccessApprovalPolicies } from "./queries"; diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx new file mode 100644 index 0000000000..98e9b3f97f --- /dev/null +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -0,0 +1,58 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { accessApprovalKeys } from "./queries"; +import { TCreateAccessPolicyDTO, TDeleteSecretPolicyDTO, TUpdateAccessPolicyDTO } from "./types"; + +export const useCreateAccessApprovalPolicy = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TCreateAccessPolicyDTO>({ + mutationFn: async ({ environment, workspaceId, approvals, approvers, name }) => { + const { data } = await apiRequest.post("/api/v1/access-approvals", { + environment, + workspaceId, + approvals, + approvers, + name + }); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(workspaceId)); + } + }); +}; + +export const useUpdateAccessApprovalPolicy = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TUpdateAccessPolicyDTO>({ + mutationFn: async ({ id, approvers, approvals, name }) => { + const { data } = await apiRequest.patch(`/api/v1/access-approvals/${id}`, { + approvals, + approvers, + name + }); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(workspaceId)); + } + }); +}; + +export const useDeleteAccessApprovalPolicy = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, TDeleteSecretPolicyDTO>({ + mutationFn: async ({ id }) => { + const { data } = await apiRequest.delete(`/api/v1/access-approvals/${id}`); + return data; + }, + onSuccess: (_, { workspaceId }) => { + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(workspaceId)); + } + }); +}; diff --git a/frontend/src/hooks/api/accessApproval/queries.tsx b/frontend/src/hooks/api/accessApproval/queries.tsx new file mode 100644 index 0000000000..8641225937 --- /dev/null +++ b/frontend/src/hooks/api/accessApproval/queries.tsx @@ -0,0 +1,40 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TAccessApprovalPolicy, TGetSecretApprovalPoliciesDTO } from "./types"; + +export const accessApprovalKeys = { + getAccessApprovalPolicies: (workspaceId: string) => + [{ workspaceId }, "access-approval-policies"] as const, + getAccessApprovalPolicyOfABoard: (workspaceId: string, environment: string) => [ + { workspaceId, environment }, + "access-approval-policy" + ] +}; + +const fetchApprovalPolicies = async (workspaceId: string) => { + const { data } = await apiRequest.get<{ approvals: TAccessApprovalPolicy[] }>( + "/api/v1/access-approvals", + { params: { workspaceId } } + ); + return data.approvals; +}; + +export const useGetAccessApprovalPolicies = ({ + workspaceId, + options = {} +}: TGetSecretApprovalPoliciesDTO & { + options?: UseQueryOptions< + TAccessApprovalPolicy[], + unknown, + TAccessApprovalPolicy[], + ReturnType + >; +}) => + useQuery({ + queryKey: accessApprovalKeys.getAccessApprovalPolicies(workspaceId), + queryFn: () => fetchApprovalPolicies(workspaceId), + ...options, + enabled: Boolean(workspaceId) && (options?.enabled ?? true) + }); diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts new file mode 100644 index 0000000000..63020166b9 --- /dev/null +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -0,0 +1,45 @@ +import { WorkspaceEnv } from "../workspace/types"; + +export type TAccessApprovalPolicy = { + id: string; + name: string; + approvals: number; + envId: string; + workspace: string; + environment: WorkspaceEnv; + projectId: string; + approvers: string[]; +}; + +export type TGetSecretApprovalPoliciesDTO = { + workspaceId: string; +}; + +export type TGetSecretApprovalPolicyOfBoardDTO = { + workspaceId: string; + environment: string; + secretPath: string; +}; + +export type TCreateAccessPolicyDTO = { + workspaceId: string; + name?: string; + environment: string; + approvers?: string[]; + approvals?: number; +}; + +export type TUpdateAccessPolicyDTO = { + id: string; + name?: string; + approvers?: string[]; + approvals?: number; + // for invalidating list + workspaceId: string; +}; + +export type TDeleteSecretPolicyDTO = { + id: string; + // for invalidating list + workspaceId: string; +}; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 574da5a319..61e8cb6665 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -1,3 +1,4 @@ +export * from "./accessApproval"; export * from "./admin"; export * from "./apiKeys"; export * from "./auditLogs"; diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts index 49949d88e1..516a5d7cf3 100644 --- a/frontend/src/hooks/api/types.ts +++ b/frontend/src/hooks/api/types.ts @@ -1,5 +1,6 @@ import { ZodIssue } from "zod"; +export type { TAccessApprovalPolicy } from "./accessApproval/types"; export type { TAuditLogStream } from "./auditLogStreams/types"; export type { GetAuthTokenAPI } from "./auth/types"; export type { IncidentContact } from "./incidentContacts/types"; @@ -49,13 +50,13 @@ export enum ApiErrorTypes { export type TApiErrors = | { - error: ApiErrorTypes.ValidationError; - message: ZodIssue[]; - statusCode: 403; - } + error: ApiErrorTypes.ValidationError; + message: ZodIssue[]; + statusCode: 403; + } | { error: ApiErrorTypes.ForbiddenError; message: string; statusCode: 401 } | { - statusCode: 400; - message: string; - error: ApiErrorTypes.BadRequestError; - }; + statusCode: 400; + message: string; + error: ApiErrorTypes.BadRequestError; + }; diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index 4ab5830fe8..ebbfd7b5e2 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -6,6 +6,7 @@ import Divider from "@app/components/basic/Divider"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { useWorkspace } from "@app/context"; +import { AccessApprovalPolicyList } from "./components/AccessApprovalPolicyList"; import { SecretApprovalPolicyList } from "./components/SecretApprovalPolicyList"; import { SecretApprovalRequest } from "./components/SecretApprovalRequest"; @@ -47,8 +48,8 @@ export const SecretApprovalPage = () => { Secret Approvals Secret Policies - Resource Approvals - Resource Policies + Access Approvals + Access Policies @@ -56,6 +57,10 @@ export const SecretApprovalPage = () => { + + + +
); diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx new file mode 100644 index 0000000000..834b54f00c --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx @@ -0,0 +1,166 @@ +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 +} 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/AccessPolicyForm"; + +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 { data: members } = useGetWorkspaceUsers(workspaceId); + const { data: policies, isLoading: isPoliciesLoading } = useGetAccessApprovalPolicies({ + workspaceId, + options: { + enabled: permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) + } + }); + + const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteAccessApprovalPolicy(); + + const handleDeletePolicy = async () => { + const { id } = popUp.deletePolicy.data as TAccessApprovalPolicy; + 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 ( +
+
+
+ Access 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 PathApproval 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 new file mode 100644 index 0000000000..54eaae67c3 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx @@ -0,0 +1,145 @@ +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[]; + workspaceId: string; + onEdit: () => void; + onDelete: () => void; +}; + +export const AccessApprovalPolicyRow = ({ + policy, + members = [], + workspaceId, + onEdit, + onDelete +}: Props) => { + const [selectedApprovers, setSelectedApprovers] = useState([]); + const { mutate: updateAccessApprovalPolicy, isLoading } = useUpdateAccessApprovalPolicy(); + const { permission } = useProjectPermission(); + + return ( +
{policy.name}{policy.environment.slug} + { + if (!isOpen) { + updateAccessApprovalPolicy( + { + workspaceId, + 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.email} + + ); + })} + + + {policy.approvals} +
+ + {(isAllowed) => ( + + + + )} + + + {(isAllowed) => ( + + + + )} + +
+
Name Environment Secret PathEligible Approvers Approval Required
handlePopUpToggle("secretPolicyForm", isOpen)} members={members} From a4b6d2650a9d052b2fde1ddd39faa3deb2c60355 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:49:57 -0700 Subject: [PATCH 039/188] Fix: Move to project slug --- .../components/AccessApprovalPolicyRow.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx index 54eaae67c3..8554e928ee 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx @@ -22,7 +22,7 @@ import { TWorkspaceUser } from "@app/hooks/api/users/types"; type Props = { policy: TAccessApprovalPolicy; members?: TWorkspaceUser[]; - workspaceId: string; + projectSlug: string; onEdit: () => void; onDelete: () => void; }; @@ -30,7 +30,7 @@ type Props = { export const AccessApprovalPolicyRow = ({ policy, members = [], - workspaceId, + projectSlug, onEdit, onDelete }: Props) => { @@ -42,13 +42,14 @@ export const AccessApprovalPolicyRow = ({ {policy.name} {policy.environment.slug} + {policy.secretPath || "*"} { if (!isOpen) { updateAccessApprovalPolicy( { - workspaceId, + projectSlug, id: policy.id, approvers: selectedApprovers }, From ddd6adf804af8849bb81ade952b9bc107bf1dd92 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:50:16 -0700 Subject: [PATCH 040/188] Fix: Move to project slug --- .../components/AccessPolicyForm.tsx | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index 56a4bdef5f..6b53e2f1e3 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -32,7 +32,7 @@ type Props = { isOpen?: boolean; onToggle: (isOpen: boolean) => void; members?: TWorkspaceUser[]; - workspaceId: string; + projectSlug: string; editValues?: TAccessApprovalPolicy; }; @@ -40,7 +40,7 @@ const formSchema = z .object({ environment: z.string(), name: z.string().optional(), - secretPath: z.string().optional().nullable(), + secretPath: z.string().optional(), approvals: z.number().min(1), approvers: z.string().array().min(1) }) @@ -55,7 +55,7 @@ export const AccessPolicyForm = ({ isOpen, onToggle, members = [], - workspaceId, + projectSlug, editValues }: Props) => { const { @@ -80,10 +80,12 @@ export const AccessPolicyForm = ({ const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy(); const handleCreatePolicy = async (data: TFormSchema) => { + if (!projectSlug) return; + try { await createAccessApprovalPolicy({ ...data, - workspaceId + projectSlug }); createNotification({ type: "success", @@ -100,12 +102,14 @@ export const AccessPolicyForm = ({ }; const handleUpdatePolicy = async (data: TFormSchema) => { + if (!projectSlug) return; if (!editValues?.id) return; + try { await updateAccessApprovalPolicy({ id: editValues?.id, ...data, - workspaceId + projectSlug }); createNotification({ type: "success", @@ -154,6 +158,7 @@ export const AccessPolicyForm = ({ errorText={error?.message} > + + )} + /> + Date: Wed, 3 Apr 2024 16:50:32 -0700 Subject: [PATCH 041/188] Feat: Request access --- .../AccessApprovalRequest.tsx | 577 ++++++++++++++++++ 1 file changed, 577 insertions(+) create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx new file mode 100644 index 0000000000..ffb8ac049e --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -0,0 +1,577 @@ +/* eslint-disable no-nested-ternary */ +/* eslint-disable react/jsx-no-useless-fragment */ +import { useCallback, useMemo, useState } from "react"; +import { + faCheck, + faCheckCircle, + faChevronDown, + faLockOpen +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { formatDistance } from "date-fns"; +import { AnimatePresence, motion } from "framer-motion"; +import ms from "ms"; +import { twMerge } from "tailwind-merge"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, + EmptyState, + Modal, + ModalContent, + UpgradePlanModal +} from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useSubscription, + useWorkspace +} from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { useGetWorkspaceUsers, useReviewAccessRequest } from "@app/hooks/api"; +import { + useGetAccessApprovalPolicies, + useGetAccessApprovalRequests, + useGetAccessRequestsCount +} from "@app/hooks/api/accessApproval/queries"; +import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types"; +import { ApprovalStatus, TAccessApprovalPolicy, TWorkspaceUser } from "@app/hooks/api/types"; +import { SpecificPrivilegeSecretForm } from "@app/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection"; + +const DisplayBadge = ({ text, className }: { text: string; className?: string }) => { + return ( +
+ {text} +
+ ); +}; + +const ReviewRequestModal = ({ + isOpen, + onOpenChange, + request, + projectSlug, + selectedRequester, + selectedEnvSlug +}: { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + request: TAccessApprovalRequest & { user: TWorkspaceUser["user"] | null }; + projectSlug: string; + selectedRequester: string | undefined; + selectedEnvSlug: string | undefined; +}) => { + const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null); + + const accessDetails = { + env: request.environmentName, + // secret path will be inside $glob operator + secretPath: request.policy.secretPath, + read: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Read)), + edit: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Edit)), + create: request.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Create) + ), + delete: request.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Delete) + ), + + temporaryAccess: { + isTemporary: request.isTemporary, + temporaryRange: request.temporaryRange + } + }; + + const requestedAccess = useMemo(() => { + const access: string[] = []; + if (accessDetails.read) access.push("Read"); + if (accessDetails.edit) access.push("Edit"); + if (accessDetails.create) access.push("Create"); + if (accessDetails.delete) access.push("Delete"); + + return access.join(", "); + }, [accessDetails]); + + const getAccessLabel = () => { + if (!accessDetails.temporaryAccess.isTemporary || !accessDetails.temporaryAccess.temporaryRange) + return "Permanent"; + + // convert the range to human readable format + ms(ms(accessDetails.temporaryAccess.temporaryRange), { long: true }); + + return ( + + ); + }; + + const reviewAccessRequest = useReviewAccessRequest(); + + const handleReview = useCallback(async (status: "approved" | "rejected") => { + setIsLoading(status); + try { + await reviewAccessRequest.mutateAsync({ + requestId: request.id, + status, + projectSlug, + envSlug: selectedEnvSlug, + requestedBy: selectedRequester + }); + } catch (error) { + console.error(error); + setIsLoading(null); + return; + } + + createNotification({ + title: `Request ${status}`, + text: `The request has been ${status}`, + type: status === "approved" ? "success" : "info" + }); + + setIsLoading(null); + onOpenChange(false); + }, []); + + return ( + + +
+ + + {request.user?.firstName} {request.user?.lastName} ({request.user?.email}) + {" "} + is requesting access to the following resource + + +
+
+ Requested path: + +
+ +
+ Permissions: + +
+ +
+ Access Type: + {getAccessLabel()} +
+
+ +
+ + +
+
+
+
+ ); +}; + +const SelectAccessModal = ({ + isOpen, + onOpenChange, + policies +}: { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + policies: TAccessApprovalPolicy[]; +}) => { + return ( + + + onOpenChange(false)} policies={policies} /> + + + ); +}; + +const generateRequestText = (request: TAccessApprovalRequest) => { + const { isTemporary } = request; + + return ( + + Requested {isTemporary ? "temporary" : "permanent"} access to{" "} + + {request.policy.secretPath} + + in + + {request.environmentName} + + + ); +}; + +export const AccessApprovalRequest = ({ + projectSlug, + projectId +}: { + projectSlug: string; + projectId: string; +}) => { + const [selectedRequest, setSelectedRequest] = useState< + (TAccessApprovalRequest & { user: TWorkspaceUser["user"] | null }) | null + >(null); + + const { handlePopUpOpen, popUp, handlePopUpClose } = usePopUp([ + "requestAccess", + "reviewRequest", + "upgradePlan" + ] as const); + const { permission, membership } = useProjectPermission(); + const { subscription } = useSubscription(); + const { currentWorkspace } = useWorkspace(); + + const { data: members } = useGetWorkspaceUsers(projectId); + const membersGroupById = members?.reduce>( + (prev, curr) => ({ ...prev, [curr.id]: curr }), + {} + ); + + const [statusFilter, setStatusFilter] = useState<"open" | "close">("open"); + const [requestedByFilter, setRequestedByFilter] = useState(undefined); + const [envFilter, setEnvFilter] = useState(undefined); + + console.log("requestedByFilter", requestedByFilter); + console.log("envFilter", envFilter); + + const { data: requestCount } = useGetAccessRequestsCount({ + projectSlug + }); + + const { data: policies, isLoading: policiesLoading } = useGetAccessApprovalPolicies({ + projectSlug, + options: { + enabled: + permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) && + !!projectSlug + } + }); + + const { data: requests } = useGetAccessApprovalRequests({ + projectSlug, + authorProjectMembershipId: requestedByFilter, + envSlug: envFilter + }); + + const filteredRequests = useMemo(() => { + if (statusFilter === "open") return requests?.filter((request) => !request.isApproved); + if (statusFilter === "close") return requests?.filter((request) => request.isApproved); + + return requests; + }, [requests, statusFilter, requestedByFilter, envFilter]); + + const generateRequestDetails = (request: TAccessApprovalRequest) => { + const isReviewedByUser = + request.reviewers.findIndex(({ member }) => member === membership.id) !== -1; + const isRejectedByAnyone = request.reviewers.some( + ({ status }) => status === ApprovalStatus.REJECTED + ); + const isApprover = request.policy.approvers.indexOf(membership.id || "") !== -1; + const isAccepted = request.isApproved; + + const userReviewStatus = request.reviewers.find( + ({ member }) => member === membership.id + )?.status; + + let displayData: { label: string; colorClass: string } = { label: "", colorClass: "" }; + + const isExpired = + request.privilege && + request.isApproved && + new Date() > new Date(request.privilege.temporaryAccessEndTime || ("" as string)); + + if (isExpired) displayData = { label: "Access Expired", colorClass: "bg-red/20 text-red" }; + else if (isAccepted) + displayData = { label: "Access Granted", colorClass: "bg-green/20 text-green" }; + else if (isRejectedByAnyone) + displayData = { label: "Rejected", colorClass: "bg-red/20 text-red" }; + else if (userReviewStatus === ApprovalStatus.APPROVED) + displayData = { + label: `Pending ${request.policy.approvals - request.reviewers.length} reviews`, + colorClass: "bg-yellow/20 text-yellow" + }; + else if (!isReviewedByUser) + displayData = { + label: "Review Required", + colorClass: "bg-yellow/20 text-yellow" + }; + + return { + displayData, + isReviewedByUser, + isRejectedByAnyone, + isApprover, + userReviewStatus, + isAccepted + }; + }; + + return ( +
+
+
+ Access Approval Requests +
+ Request access to secrets in sensitive environments and folders. +
+
+
+ + {(isAllowed) => ( + + )} + +
+
+ + + +
+
setStatusFilter("open")} + onKeyDown={(evt) => { + if (evt.key === "Enter") setStatusFilter("open"); + }} + className={ + statusFilter === "close" ? "text-gray-500 duration-100 hover:text-gray-400" : "" + } + > + + {!!requestCount && requestCount?.pendingCount} Pending +
+
setStatusFilter("close")} + onKeyDown={(evt) => { + if (evt.key === "Enter") setStatusFilter("close"); + }} + > + + {!!requestCount && requestCount.finalizedCount} Completed +
+
+ + + + + + Select an environment + {currentWorkspace?.environments.map(({ slug, name }) => ( + setEnvFilter((state) => (state === slug ? undefined : slug))} + key={`request-filter-${slug}`} + icon={envFilter === slug && } + iconPos="right" + > + {name} + + ))} + + + + + + + + Select an author + {members?.map(({ user, id }) => ( + + setRequestedByFilter((state) => (state === id ? undefined : id)) + } + key={`request-filter-member-${id}`} + icon={requestedByFilter === id && } + iconPos="right" + > + {user.email} + + ))} + + +
+
+
+ {filteredRequests?.length === 0 && ( +
+ +
+ )} + {!!filteredRequests?.length && + requests?.map((request) => { + const details = generateRequestDetails(request); + + return ( +
{ + if (details.isReviewedByUser || details.isRejectedByAnyone) return; + + setSelectedRequest({ + ...request, + user: membersGroupById?.[request.requestedBy].user! + }); + handlePopUpOpen("reviewRequest"); + }} + onKeyDown={(evt) => { + if (evt.key === "Enter") { + setSelectedRequest({ + ...request, + user: membersGroupById?.[request.requestedBy].user! + }); + handlePopUpOpen("reviewRequest"); + } + }} + > +
+
+ + {generateRequestText(request)} +
+ + Requested {formatDistance(new Date(request.createdAt), new Date())} ago by{" "} + {membersGroupById?.[request.requestedBy]?.user?.firstName}{" "} + {membersGroupById?.[request.requestedBy]?.user?.lastName} ( + {membersGroupById?.[request.requestedBy]?.user?.email}){" "} + + {details.isApprover && + !details.isReviewedByUser && + !details.isAccepted && + "- Review required"} + + + + {details.isApprover && ( + + )} +
+
+ ); + })} +
+
+
+ + {!!policies && ( + handlePopUpClose("requestAccess")} + /> + )} + + {!!selectedRequest && ( + { + handlePopUpClose("reviewRequest"); + setSelectedRequest(null); + }} + /> + )} + + handlePopUpClose("upgradePlan")} + /> +
+ ); +}; From 0e95c1bcee020e072f488ec4b6521f3bba68fc08 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:50:47 -0700 Subject: [PATCH 042/188] Feat: Request access --- .../components/AccessApprovalPolicyRow.tsx | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx new file mode 100644 index 0000000000..8554e928ee --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx @@ -0,0 +1,146 @@ +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.email} + + ); + })} + + + + {policy.approvals} + +
+ + {(isAllowed) => ( + + + + )} + + + {(isAllowed) => ( + + + + )} + +
+ + + ); +}; From ac24c0f760f847fb5ed1b35623be364da4700e1c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:50:56 -0700 Subject: [PATCH 043/188] Feat: Request access --- .../components/AccessPolicyForm.tsx | 261 ++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx new file mode 100644 index 0000000000..9cf756c14d --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx @@ -0,0 +1,261 @@ +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) => { + 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 (!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.email} + + ); + })} + + + + )} + /> + ( + + field.onChange(parseInt(el.target.value, 10))} + /> + + )} + /> +
+ + +
+ +
+
+ ); +}; From 7f1c8d9ff649cad65da22599e562d2ac705fd8a2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:51:02 -0700 Subject: [PATCH 044/188] Create index.tsx --- .../components/AccessApprovalRequest/index.tsx | 1 + 1 file changed, 1 insertion(+) create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/index.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/index.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/index.tsx new file mode 100644 index 0000000000..ec0a8d7443 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/index.tsx @@ -0,0 +1 @@ +export { AccessApprovalRequest } from "./AccessApprovalRequest"; From 17587ff1b881a29a5860a1d71b1ae0f461f46f48 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:51:13 -0700 Subject: [PATCH 045/188] Fix: Minor fixes --- .../SecretApprovalPolicyList/SecretApprovalPolicyList.tsx | 2 -- .../components/SecretApprovalRequest/SecretApprovalRequest.tsx | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx index 345ff7d52e..d3a36988c1 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/SecretApprovalPolicyList.tsx @@ -46,7 +46,6 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => { ] as const); const { permission } = useProjectPermission(); const { subscription } = useSubscription(); - const { data: members } = useGetWorkspaceUsers(workspaceId); const { data: policies, isLoading: isPoliciesLoading } = useGetSecretApprovalPolicies({ @@ -120,7 +119,6 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => { Secret Path Eligible Approvers Approval Required - diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx index 90133803bd..96976d1405 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx @@ -183,7 +183,7 @@ export const SecretApprovalRequest = () => {
-
- {filteredRequests?.length === 0 && ( // + {filteredRequests?.length === 0 && (
From 7d1dff9e5a19ec9e6cd4bf5ce7e14c67801f1e86 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:00:14 -0700 Subject: [PATCH 145/188] Fix: Security vulnurbility making it possible to spoof env & secret path requested. --- backend/src/ee/routes/v1/access-approval-request-router.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 2a6142c88c..4b173cfa76 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -16,9 +16,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv temporaryRange: z.string().optional() }), querystring: z.object({ - projectSlug: z.string().trim(), - secretPath: z.string().trim(), - envSlug: z.string().trim() + projectSlug: z.string().trim() }), response: { 200: z.object({ @@ -33,9 +31,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, permissions: req.body.permissions, - envSlug: req.query.envSlug, actorOrgId: req.permission.orgId, - secretPath: req.query.secretPath, projectSlug: req.query.projectSlug, temporaryRange: req.body.temporaryRange, isTemporary: req.body.isTemporary From 038fe3508c5054608b85c5e03871e2e12a1fe51d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:03:19 -0700 Subject: [PATCH 146/188] Removed unnessecary types --- .../MemberRoleForm/SpecificPrivilegeSection.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 bb330aa4d1..88ff41cf80 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -269,7 +269,6 @@ export const SpecificPrivilegeSecretForm = ({ conditions.secretPath = { $glob: data.secretPath }; } await requestAccess.mutateAsync({ - ...data, ...(data.temporaryAccess.isTemporary && { temporaryRange: data.temporaryAccess.temporaryRange }), @@ -354,10 +353,7 @@ export const SpecificPrivilegeSecretForm = ({ 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) @@ -388,7 +384,7 @@ export const SpecificPrivilegeSecretForm = ({ if (isExpired) return "Access expired"; if (!temporaryAccessField?.isTemporary) return "Permanent"; - if (exactTime) { + if (exactTime && !policies) { return `Until ${format( new Date(temporaryAccessField.temporaryAccessEndTime || ""), "yyyy-MM-dd HH:mm:ss" From e8e1d46f0e03cdf9178faffe1a3e5f532cf29781 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:03:29 -0700 Subject: [PATCH 147/188] Capitalization --- .../AccessApprovalPolicyList/components/AccessPolicyForm.tsx | 2 +- .../AccessApprovalRequest/components/AccessPolicyForm.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index 6b53e2f1e3..dd51ea53de 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -135,7 +135,7 @@ export const AccessPolicyForm = ({ return ( - +
- + Date: Thu, 4 Apr 2024 01:03:44 -0700 Subject: [PATCH 148/188] Style: Fix styling --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 235737d4a6..b9d55ff010 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -26,7 +26,6 @@ import { import { Badge } from "@app/components/v2/Badge"; import { ProjectPermissionActions, - ProjectPermissionSub, useProjectPermission, useSubscription, useWorkspace From c66476e2b4b2d15805e2d353f4bcc47debdd8748 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 12:44:49 -0700 Subject: [PATCH 149/188] Fix: Multiple approvers acceptance bug --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index b9d55ff010..235737d4a6 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -26,6 +26,7 @@ import { import { Badge } from "@app/components/v2/Badge"; import { ProjectPermissionActions, + ProjectPermissionSub, useProjectPermission, useSubscription, useWorkspace From cb505d15258f044c6b27bf6b7f4cf08ec048348a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 29 Mar 2024 16:24:15 +0100 Subject: [PATCH 150/188] Draft --- backend/src/ee/services/license/licence-fns.ts | 2 +- .../src/components/permissions/PermissionDeniedBanner.tsx | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 189a3c4e06..492d09ec3d 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ status: null, trial_end: null, has_used_trial: true, - secretApproval: false, + secretApproval: true, secretRotation: true }); diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx index b3c7a4f538..f9707d6347 100644 --- a/frontend/src/components/permissions/PermissionDeniedBanner.tsx +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -3,6 +3,8 @@ import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; +import { Button } from "../v2"; + type Props = { containerClassName?: string; className?: string; @@ -32,6 +34,9 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children )}
+ ); From db0b4a5ad13df058774f04292564fdfa3fea0178 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 1 Apr 2024 09:49:22 -0700 Subject: [PATCH 151/188] Feat: Request access --- backend/scripts/generate-schema-types.ts | 2 +- .../access-approval-policy/access-approval-policy-service.ts | 2 -- frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx | 4 ++++ .../AccessApprovalPolicyList/components/AccessPolicyForm.tsx | 3 --- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 8c913991fd..28b736152c 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -5,7 +5,7 @@ import knex from "knex"; import { writeFileSync } from "fs"; dotenv.config({ - path: path.join(__dirname, "../../.env.migration") + path: path.join(__dirname, "../../.env") }); const db = knex({ 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 f143808692..50e1b04b43 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 @@ -155,7 +155,6 @@ export const accessApprovalPolicyServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { @@ -179,7 +178,6 @@ export const accessApprovalPolicyServiceFactory = ({ ); await verifyApprovers({ - projectId: accessApprovalPolicy.projectId, orgId: actorOrgId, envSlug: accessApprovalPolicy.environment.slug, secretPath: doc.secretPath!, diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index 9c273848f5..b3e0b62cac 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -65,6 +65,10 @@ export const SecretApprovalPage = () => { + + + + ); diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index dd51ea53de..081556041d 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -177,7 +177,6 @@ export const AccessPolicyForm = ({ /> ( @@ -187,8 +186,6 @@ export const AccessPolicyForm = ({ /> ( Date: Wed, 3 Apr 2024 16:37:49 -0700 Subject: [PATCH 152/188] Feat: Request access (new routes) --- backend/src/ee/routes/v1/access-approval-request-router.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 4b173cfa76..104f32665b 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -2,10 +2,6 @@ import { z } from "zod"; import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas"; import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types"; -import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AuthMode } from "@app/services/auth/auth-type"; - -export const registerAccessApprovalRequestRouter = async (server: FastifyZodProvider) => { server.route({ url: "/", method: "POST", From 7b8af89beefe33533bb469242bfe6e299e4ceb53 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:39:15 -0700 Subject: [PATCH 153/188] Fix: Validate approvers access --- .../access-approval-policy/access-approval-policy-service.ts | 1 + 1 file changed, 1 insertion(+) 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 50e1b04b43..ed812fba6f 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 @@ -155,6 +155,7 @@ export const accessApprovalPolicyServiceFactory = ({ actorAuthMethod, actorOrgId ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { From 801c0c5ada2204c9d95c4046a3b32aadff94ceb6 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:43:48 -0700 Subject: [PATCH 154/188] Fix: Remove redundant code --- .../src/components/permissions/PermissionDeniedBanner.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx index f9707d6347..b3c7a4f538 100644 --- a/frontend/src/components/permissions/PermissionDeniedBanner.tsx +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -3,8 +3,6 @@ import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -import { Button } from "../v2"; - type Props = { containerClassName?: string; className?: string; @@ -34,9 +32,6 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children )} - ); From e53d40f0e58cf364ce7c32f78c9497676758a0e0 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:51:21 -0700 Subject: [PATCH 155/188] Update SecretApprovalPage.tsx --- frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index b3e0b62cac..7b679a0298 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -67,7 +67,7 @@ export const SecretApprovalPage = () => { - + From 0580f37c5ec4be0644567040e20ec2e20d5747e7 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:53:02 -0700 Subject: [PATCH 156/188] Update generate-schema-types.ts --- backend/scripts/generate-schema-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 28b736152c..8c913991fd 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -5,7 +5,7 @@ import knex from "knex"; import { writeFileSync } from "fs"; dotenv.config({ - path: path.join(__dirname, "../../.env") + path: path.join(__dirname, "../../.env.migration") }); const db = knex({ From 9198eb5fbaf64e7cf5f7e7023709530c1305a5fa Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:43:45 -0700 Subject: [PATCH 157/188] Update licence-fns.ts --- backend/src/ee/services/license/licence-fns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 492d09ec3d..189a3c4e06 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ status: null, trial_end: null, has_used_trial: true, - secretApproval: true, + secretApproval: false, secretRotation: true }); From 1a2d8e96f3da6e88cbbf554a6293a4071349bcb0 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 3 Apr 2024 20:24:33 -0700 Subject: [PATCH 158/188] style changes --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 235737d4a6..6479117b44 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -317,7 +317,7 @@ export const AccessApprovalRequest = ({ )} {!!filteredRequests?.length && - filteredRequests?.map((request) => { + requests?.map((request) => { const details = generateRequestDetails(request); return ( From 949d210263cdf2867ee66c7fb6a72965fc2e97b7 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:29:20 -0700 Subject: [PATCH 159/188] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 6479117b44..235737d4a6 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -317,7 +317,7 @@ export const AccessApprovalRequest = ({ )} {!!filteredRequests?.length && - requests?.map((request) => { + filteredRequests?.map((request) => { const details = generateRequestDetails(request); return ( From 6e209bf099ddb39cecbff3e08acba0618984b468 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:33:58 -0700 Subject: [PATCH 160/188] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 235737d4a6..4896c9a3e0 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -311,7 +311,7 @@ export const AccessApprovalRequest = ({
- {filteredRequests?.length === 0 && ( + {filteredRequests?.length === 0 && ( //
From 29b26e3158983138b33b1a79e95f97438c4cf65b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:34:11 -0700 Subject: [PATCH 161/188] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 4896c9a3e0..235737d4a6 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -311,7 +311,7 @@ export const AccessApprovalRequest = ({
- {filteredRequests?.length === 0 && ( // + {filteredRequests?.length === 0 && (
From 56c8b4f5e56b9ac6c5812d85a30697d815159035 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:03:19 -0700 Subject: [PATCH 162/188] Removed unnessecary types --- .../MemberRoleForm/SpecificPrivilegeSection.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 88ff41cf80..9252f09e60 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -348,10 +348,7 @@ export const SpecificPrivilegeSecretForm = ({ await requestAccess.mutateAsync({ ...data, ...(data.temporaryAccess.isTemporary && { - temporaryAccessStartTime: data.temporaryAccess.temporaryAccessStartTime, - temporaryAccessEndTime: data.temporaryAccess.temporaryAccessEndTime, - temporaryRange: data.temporaryAccess.temporaryRange, - temporaryMode: "relative" + temporaryRange: data.temporaryAccess.temporaryRange }), projectSlug: currentWorkspace.slug, isTemporary: data.temporaryAccess.isTemporary, From ce1ad6f32e1cf16dd856e50095d7774ea4552c79 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 20:14:02 -0700 Subject: [PATCH 163/188] Fix: Rebase errors --- .../v1/access-approval-request-router.ts | 4 + .../access-approval-policy-service.ts | 1 + .../SpecificPrivilegeSection.tsx | 77 ------------------- .../components/AccessPolicyForm.tsx | 2 + 4 files changed, 7 insertions(+), 77 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 104f32665b..4b173cfa76 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -2,6 +2,10 @@ import { z } from "zod"; import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas"; import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerAccessApprovalRequestRouter = async (server: FastifyZodProvider) => { server.route({ url: "/", method: "POST", 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 ed812fba6f..f143808692 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 @@ -179,6 +179,7 @@ export const accessApprovalPolicyServiceFactory = ({ ); await verifyApprovers({ + projectId: accessApprovalPolicy.projectId, orgId: actorOrgId, envSlug: accessApprovalPolicy.environment.slug, secretPath: doc.secretPath!, 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 9252f09e60..6fb97063b6 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -237,83 +237,6 @@ export const SpecificPrivilegeSecretForm = ({ } }; - // 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.temporaryAccess.isTemporary && { - temporaryRange: data.temporaryAccess.temporaryRange - }), - projectSlug: currentWorkspace.slug, - 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 && !policies) { - 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; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index 081556041d..a07acb5849 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -186,6 +186,8 @@ export const AccessPolicyForm = ({ /> ( Date: Thu, 4 Apr 2024 21:00:02 -0700 Subject: [PATCH 164/188] Update SecretApprovalPage.tsx --- frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index 7b679a0298..9c273848f5 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -65,10 +65,6 @@ export const SecretApprovalPage = () => { - - - -
); From 4a0ccbe69e990d038772f68016d96b99931b6ebc Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 21:12:05 -0700 Subject: [PATCH 165/188] Fixed bugs --- .../AccessApprovalPolicyList/components/AccessPolicyForm.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index a07acb5849..dd51ea53de 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -177,6 +177,7 @@ export const AccessPolicyForm = ({ /> ( @@ -186,8 +187,8 @@ export const AccessPolicyForm = ({ /> ( Date: Thu, 4 Apr 2024 22:14:46 -0700 Subject: [PATCH 166/188] Migration improvements --- .../20240330075122_access-approval-policy.ts | 1 + .../20240401173320_access_approval_requests.ts | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts index e6d47d8648..2fc11bca90 100644 --- a/backend/src/db/migrations/20240330075122_access-approval-policy.ts +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -35,6 +35,7 @@ export async function up(knex: Knex): Promise { export async function down(knex: Knex): Promise { await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover); await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy); + await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); } diff --git a/backend/src/db/migrations/20240401173320_access_approval_requests.ts b/backend/src/db/migrations/20240401173320_access_approval_requests.ts index 901be9a78e..c03287b57c 100644 --- a/backend/src/db/migrations/20240401173320_access_approval_requests.ts +++ b/backend/src/db/migrations/20240401173320_access_approval_requests.ts @@ -43,9 +43,16 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { + const reviewerTableExists = await knex.schema.hasTable(TableName.AccessApprovalRequestReviewer); + const requestTableExists = await knex.schema.hasTable(TableName.AccessApprovalRequest); + await knex.schema.dropTableIfExists(TableName.AccessApprovalRequestReviewer); await knex.schema.dropTableIfExists(TableName.AccessApprovalRequest); - await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); - await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest); + if (reviewerTableExists) { + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); + } + if (requestTableExists) { + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest); + } } From fb6c4acf3199b48c5c3c60097a0db0e654ee9311 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:59:01 -0700 Subject: [PATCH 167/188] Delete access-approval-request-secret-dal.ts --- .../access-approval-request-secret-dal.ts | 230 ------------------ 1 file changed, 230 deletions(-) delete mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts deleted file mode 100644 index d458d58ffe..0000000000 --- a/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { Knex } from "knex"; - -import { TDbClient } from "@app/db"; -import { - SecretApprovalRequestsSecretsSchema, - TableName, - TSecretApprovalRequestsSecrets, - TSecretTags -} from "@app/db/schemas"; -import { BadRequestError, DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; - -export type TAccessApprovalRequestSecretDALFactory = ReturnType; - -export const accessApprovalRequestSecretDALFactory = (db: TDbClient) => { - const accessApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret); - const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag); - - const bulkUpdateNoVersionIncrement = async (data: TSecretApprovalRequestsSecrets[], tx?: Knex) => { - try { - const existingApprovalSecrets = await accessApprovalRequestSecretOrm.find( - { - $in: { - id: data.map((el) => el.id) - } - }, - { tx } - ); - - if (existingApprovalSecrets.length !== data.length) { - throw new BadRequestError({ message: "Some of the secret approvals do not exist" }); - } - - if (data.length === 0) return []; - - const updatedApprovalSecrets = await (tx || db)(TableName.SecretApprovalRequestSecret) - .insert(data) - .onConflict("id") // this will cause a conflict then merge the data - .merge() // Merge the data with the existing data - .returning("*"); - - return updatedApprovalSecrets; - } catch (error) { - throw new DatabaseError({ error, name: "bulk update secret" }); - } - }; - - const findByRequestId = async (requestId: string, tx?: Knex) => { - try { - const doc = await (tx || db)({ - secVerTag: TableName.SecretTag - }) - .from(TableName.SecretApprovalRequestSecret) - .where({ requestId }) - .leftJoin( - TableName.SecretApprovalRequestSecretTag, - `${TableName.SecretApprovalRequestSecret}.id`, - `${TableName.SecretApprovalRequestSecretTag}.secretId` - ) - .leftJoin(TableName.SecretTag, `${TableName.SecretApprovalRequestSecretTag}.tagId`, `${TableName.SecretTag}.id`) - .leftJoin(TableName.Secret, `${TableName.SecretApprovalRequestSecret}.secretId`, `${TableName.Secret}.id`) - .leftJoin( - TableName.SecretVersion, - `${TableName.SecretVersion}.id`, - `${TableName.SecretApprovalRequestSecret}.secretVersion` - ) - .leftJoin( - TableName.SecretVersionTag, - `${TableName.SecretVersionTag}.${TableName.SecretVersion}Id`, - `${TableName.SecretVersion}.id` - ) - .leftJoin( - db.ref(TableName.SecretTag).as("secVerTag"), - `${TableName.SecretVersionTag}.${TableName.SecretTag}Id`, - db.ref("id").withSchema("secVerTag") - ) - .select(selectAllTableCols(TableName.SecretApprovalRequestSecret)) - .select({ - secVerTagId: "secVerTag.id", - secVerTagColor: "secVerTag.color", - secVerTagSlug: "secVerTag.slug", - secVerTagName: "secVerTag.name" - }) - .select( - db.ref("id").withSchema(TableName.SecretTag).as("tagId"), - db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"), - db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), - db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), - db.ref("name").withSchema(TableName.SecretTag).as("tagName") - ) - .select( - db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"), - db.ref("version").withSchema(TableName.Secret).as("orgSecVersion"), - db.ref("secretKeyIV").withSchema(TableName.Secret).as("orgSecKeyIV"), - db.ref("secretKeyTag").withSchema(TableName.Secret).as("orgSecKeyTag"), - db.ref("secretKeyCiphertext").withSchema(TableName.Secret).as("orgSecKeyCiphertext"), - db.ref("secretValueIV").withSchema(TableName.Secret).as("orgSecValueIV"), - db.ref("secretValueTag").withSchema(TableName.Secret).as("orgSecValueTag"), - db.ref("secretValueCiphertext").withSchema(TableName.Secret).as("orgSecValueCiphertext"), - db.ref("secretCommentIV").withSchema(TableName.Secret).as("orgSecCommentIV"), - db.ref("secretCommentTag").withSchema(TableName.Secret).as("orgSecCommentTag"), - db.ref("secretCommentCiphertext").withSchema(TableName.Secret).as("orgSecCommentCiphertext") - ) - .select( - db.ref("version").withSchema(TableName.SecretVersion).as("secVerVersion"), - db.ref("secretKeyIV").withSchema(TableName.SecretVersion).as("secVerKeyIV"), - db.ref("secretKeyTag").withSchema(TableName.SecretVersion).as("secVerKeyTag"), - db.ref("secretKeyCiphertext").withSchema(TableName.SecretVersion).as("secVerKeyCiphertext"), - db.ref("secretValueIV").withSchema(TableName.SecretVersion).as("secVerValueIV"), - db.ref("secretValueTag").withSchema(TableName.SecretVersion).as("secVerValueTag"), - db.ref("secretValueCiphertext").withSchema(TableName.SecretVersion).as("secVerValueCiphertext"), - db.ref("secretCommentIV").withSchema(TableName.SecretVersion).as("secVerCommentIV"), - db.ref("secretCommentTag").withSchema(TableName.SecretVersion).as("secVerCommentTag"), - db.ref("secretCommentCiphertext").withSchema(TableName.SecretVersion).as("secVerCommentCiphertext") - ); - const formatedDoc = sqlNestRelationships({ - data: doc, - key: "id", - parentMapper: (data) => SecretApprovalRequestsSecretsSchema.omit({ secretVersion: true }).parse(data), - childrenMapper: [ - { - key: "tagJnId", - label: "tags" as const, - mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({ - id, - name, - slug, - color - }) - }, - { - key: "secretId", - label: "secret" as const, - mapper: ({ - orgSecKeyIV, - orgSecKeyTag, - orgSecValueIV, - orgSecVersion, - orgSecValueTag, - orgSecCommentIV, - orgSecBlindIndex, - orgSecCommentTag, - orgSecKeyCiphertext, - orgSecValueCiphertext, - orgSecCommentCiphertext, - secretId - }) => - secretId - ? { - id: secretId, - version: orgSecVersion, - secretBlindIndex: orgSecBlindIndex, - secretKeyIV: orgSecKeyIV, - secretKeyTag: orgSecKeyTag, - secretKeyCiphertext: orgSecKeyCiphertext, - secretValueIV: orgSecValueIV, - secretValueTag: orgSecValueTag, - secretValueCiphertext: orgSecValueCiphertext, - secretCommentIV: orgSecCommentIV, - secretCommentTag: orgSecCommentTag, - secretCommentCiphertext: orgSecCommentCiphertext - } - : undefined - }, - { - key: "secretVersion", - label: "secretVersion" as const, - mapper: ({ - secVerCommentIV, - secVerCommentCiphertext, - secVerCommentTag, - secVerValueCiphertext, - secVerKeyIV, - secVerKeyTag, - secVerValueIV, - secretVersion, - secVerValueTag, - secVerKeyCiphertext, - secVerVersion - }) => - secretVersion - ? { - version: secVerVersion, - id: secretVersion, - secretKeyIV: secVerKeyIV, - secretKeyTag: secVerKeyTag, - secretKeyCiphertext: secVerKeyCiphertext, - secretValueIV: secVerValueIV, - secretValueTag: secVerValueTag, - secretValueCiphertext: secVerValueCiphertext, - secretCommentIV: secVerCommentIV, - secretCommentTag: secVerCommentTag, - secretCommentCiphertext: secVerCommentCiphertext - } - : undefined, - childrenMapper: [ - { - key: "secVerTagId", - label: "tags" as const, - mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({ - // eslint-disable-next-line - id, - // eslint-disable-next-line - name, - // eslint-disable-next-line - slug, - // eslint-disable-next-line - color - }) - } - ] - } - ] - }); - return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({ - ...el, - secret: secret?.[0], - secretVersion: secretVersion?.[0] - })); - } catch (error) { - throw new DatabaseError({ error, name: "FindByRequestId" }); - } - }; - return { - ...accessApprovalRequestSecretOrm, - findByRequestId, - bulkUpdateNoVersionIncrement, - insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany - }; -}; From af0d31db2cfe32bfc6fd26f07bcd8ccbc59228b9 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:18:02 -0700 Subject: [PATCH 168/188] Fix: Improved migrations --- .../20240401173320_access_approval_requests.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/backend/src/db/migrations/20240401173320_access_approval_requests.ts b/backend/src/db/migrations/20240401173320_access_approval_requests.ts index c03287b57c..901be9a78e 100644 --- a/backend/src/db/migrations/20240401173320_access_approval_requests.ts +++ b/backend/src/db/migrations/20240401173320_access_approval_requests.ts @@ -43,16 +43,9 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { - const reviewerTableExists = await knex.schema.hasTable(TableName.AccessApprovalRequestReviewer); - const requestTableExists = await knex.schema.hasTable(TableName.AccessApprovalRequest); - await knex.schema.dropTableIfExists(TableName.AccessApprovalRequestReviewer); await knex.schema.dropTableIfExists(TableName.AccessApprovalRequest); - if (reviewerTableExists) { - await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); - } - if (requestTableExists) { - await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest); - } + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest); } From a579598b6dc7ea9d75348cf1797caba64088159b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:19:43 -0700 Subject: [PATCH 169/188] Chore: Moved verifyApprovers --- .../access-approval-policy/access-approval-policy-service.ts | 1 - 1 file changed, 1 deletion(-) 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 f143808692..51a51abb5a 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 @@ -54,7 +54,6 @@ export const accessApprovalPolicyServiceFactory = ({ if (approvals > approvers.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); - if (!secretPath) throw new BadRequestError({ message: "Secret path is required" }); const { permission } = await permissionService.getProjectPermission( actor, From 9c2591f3a61108b1cd57fe5a8b3bcd64842bfa3e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:20:18 -0700 Subject: [PATCH 170/188] Fix: Moved Divider to v2 --- frontend/src/components/basic/Divider.tsx | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 frontend/src/components/basic/Divider.tsx diff --git a/frontend/src/components/basic/Divider.tsx b/frontend/src/components/basic/Divider.tsx deleted file mode 100644 index f1a570d7b1..0000000000 --- a/frontend/src/components/basic/Divider.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { twMerge } from "tailwind-merge"; - -interface IProps { - className?: string; -} - -const Divider = ({ className }: IProps): JSX.Element => { - return ( -
- - ); -}; - -export default Divider; From 95c1fff7d3a77e9e7af09f86a31e3aab12ae0d98 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:21:57 -0700 Subject: [PATCH 171/188] Chore: Remove unused files --- .../components/AccessPolicyForm.tsx | 266 ------------------ .../components/AccessApprovalPolicyRow.tsx | 146 ---------- .../components/AccessPolicyForm.tsx | 261 ----------------- 3 files changed, 673 deletions(-) delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx deleted file mode 100644 index dd51ea53de..0000000000 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.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.email} - - ); - })} - - - - )} - /> - ( - - field.onChange(parseInt(el.target.value, 10))} - /> - - )} - /> -
- - -
- -
-
- ); -}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx deleted file mode 100644 index 8554e928ee..0000000000 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/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.email} - - ); - })} - - - - {policy.approvals} - -
- - {(isAllowed) => ( - - - - )} - - - {(isAllowed) => ( - - - - )} - -
- - - ); -}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx deleted file mode 100644 index 7e88da1005..0000000000 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx +++ /dev/null @@ -1,261 +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) => { - 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 (!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.email} - - ); - })} - - - - )} - /> - ( - - field.onChange(parseInt(el.target.value, 10))} - /> - - )} - /> -
- - -
- -
-
- ); -}; From 9a585ad9309ee89d12f83c55c2e965e732ef6db1 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 6 May 2024 11:24:04 +0200 Subject: [PATCH 172/188] Fix: Rebase error --- .../src/db/migrations/20240330075122_access-approval-policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts index 2fc11bca90..feeecd25b6 100644 --- a/backend/src/db/migrations/20240330075122_access-approval-policy.ts +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -12,7 +12,6 @@ export async function up(knex: Knex): Promise { t.string("secretPath"); t.uuid("envId").notNullable(); - t.string("secretPath"); t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); t.timestamps(true, true, true); }); @@ -24,6 +23,7 @@ export async function up(knex: Knex): Promise { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.uuid("approverId").notNullable(); t.foreign("approverId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE"); + t.uuid("policyId").notNullable(); t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE"); t.timestamps(true, true, true); From c08fcc6f5e5dc35ee4acb8ca68855b16a1175719 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 8 May 2024 00:12:55 +0800 Subject: [PATCH 173/188] adjustment: finalized notification text --- .../components/SelectionPanel/SelectionPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx b/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx index 4f331e9891..89fcef58c7 100644 --- a/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx @@ -114,7 +114,7 @@ export const SelectionPanel = ({ getFolderByNameAndEnv, getSecretByKey, secretPa handlePopUpClose("bulkDeleteEntries"); createNotification({ type: "info", - text: "No changes have been made. Ensure that you have sufficient access." + text: "You don't have access to delete selected items" }); } else if (areEntriesDeleted) { handlePopUpClose("bulkDeleteEntries"); From 306cf8733ee718be11252bf4437e9915062278e5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 7 May 2024 16:21:40 +0000 Subject: [PATCH 174/188] chore: renamed new migration files to latest timestamp (gh-action) --- ...pproval-policy.ts => 20240507162140_access-approval-policy.ts} | 0 ...01173320_access_approval_requests.ts => 20240507162141_access} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename backend/src/db/migrations/{20240330075122_access-approval-policy.ts => 20240507162140_access-approval-policy.ts} (100%) rename backend/src/db/migrations/{20240401173320_access_approval_requests.ts => 20240507162141_access} (100%) diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240507162140_access-approval-policy.ts similarity index 100% rename from backend/src/db/migrations/20240330075122_access-approval-policy.ts rename to backend/src/db/migrations/20240507162140_access-approval-policy.ts diff --git a/backend/src/db/migrations/20240401173320_access_approval_requests.ts b/backend/src/db/migrations/20240507162141_access similarity index 100% rename from backend/src/db/migrations/20240401173320_access_approval_requests.ts rename to backend/src/db/migrations/20240507162141_access From 3ce91b8a202fed7ce1114ade5e67e83b3c661cf3 Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Tue, 7 May 2024 22:25:36 +0530 Subject: [PATCH 175/188] doc: fixed typo in api privilege documentation --- backend/src/lib/api-docs/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 10006b9efb..61df46d92b 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -465,7 +465,7 @@ export const SECRET_TAGS = { export const IDENTITY_ADDITIONAL_PRIVILEGE = { CREATE: { projectSlug: "The slug of the project of the identity in.", - identityId: "The ID of the identity to delete.", + identityId: "The ID of the identity to create.", slug: "The slug of the privilege to create.", permissions: `The permission object for the privilege. - Read secrets From f957b9d970024896a41791269848e40edc5fe6b4 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 8 May 2024 01:03:41 +0800 Subject: [PATCH 176/188] misc: migrated to react-state --- .../pages/project/[id]/secrets/overview.tsx | 5 +- .../SecretOverviewPage.store.tsx | 78 ------------------- .../SecretOverviewPage/SecretOverviewPage.tsx | 70 +++++++++++++---- .../SelectionPanel/SelectionPanel.tsx | 20 ++++- 4 files changed, 74 insertions(+), 99 deletions(-) delete mode 100644 frontend/src/views/SecretOverviewPage/SecretOverviewPage.store.tsx diff --git a/frontend/src/pages/project/[id]/secrets/overview.tsx b/frontend/src/pages/project/[id]/secrets/overview.tsx index eaf915120b..69e2bc1968 100644 --- a/frontend/src/pages/project/[id]/secrets/overview.tsx +++ b/frontend/src/pages/project/[id]/secrets/overview.tsx @@ -2,7 +2,6 @@ import { useTranslation } from "react-i18next"; import Head from "next/head"; import { SecretOverviewPage } from "@app/views/SecretOverviewPage"; -import { StoreProvider } from "@app/views/SecretOverviewPage/SecretOverviewPage.store"; const Dashboard = () => { const { t } = useTranslation(); @@ -17,9 +16,7 @@ const Dashboard = () => {
- - - +
); diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.store.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.store.tsx deleted file mode 100644 index db544c7e83..0000000000 --- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.store.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { createContext, ReactNode, useContext, useEffect, useRef } from "react"; -import { useRouter } from "next/router"; -import { createStore, StateCreator, StoreApi, useStore } from "zustand"; - -export enum EntryType { - FOLDER = "folder", - SECRET = "secret" -} - -type SelectedEntriesState = { - selectedEntries: { - [EntryType.FOLDER]: Record; - [EntryType.SECRET]: Record; - }; - action: { - toggle: (type: EntryType, key: string) => void; - reset: () => void; - }; -}; - -const createSelectedSecretStore: StateCreator = (set) => ({ - selectedEntries: { - [EntryType.FOLDER]: {}, - [EntryType.SECRET]: {} - }, - action: { - toggle: (type: EntryType, key: string) => - set((state) => { - const isChecked = Boolean(state.selectedEntries[type]?.[key]); - const newChecks = { ...state.selectedEntries }; - // remove selection if its present else add it - if (isChecked) delete newChecks[type][key]; - else newChecks[type][key] = true; - return { selectedEntries: newChecks }; - }), - reset: () => - set({ - selectedEntries: { - [EntryType.FOLDER]: {}, - [EntryType.SECRET]: {} - } - }) - } -}); - -const StoreContext = createContext | null>(null); -export const StoreProvider = ({ children }: { children: ReactNode }) => { - const storeRef = useRef>(); - const router = useRouter(); - if (!storeRef.current) { - storeRef.current = createStore((...a) => ({ - ...createSelectedSecretStore(...a) - })); - } - - useEffect(() => { - const onRouteChangeStart = () => { - const state = storeRef.current?.getState(); - state?.action.reset(); - }; - - router.events.on("routeChangeStart", onRouteChangeStart); - return () => { - router.events.off("routeChangeStart", onRouteChangeStart); - }; - }, []); - - return {children}; -}; - -const useStoreContext = (selector: (state: SelectedEntriesState) => T): T => { - const ctx = useContext(StoreContext); - if (!ctx) throw new Error("Missing context provider"); - return useStore(ctx, selector); -}; - -export const useSelectedEntries = () => useStoreContext((state) => state.selectedEntries); -export const useSelectedEntryActions = () => useStoreContext((state) => state.action); diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx index cd00af883c..fab7ebecff 100644 --- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx +++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -71,7 +71,11 @@ import { SecretOverviewDynamicSecretRow } from "./components/SecretOverviewDynam import { SecretOverviewFolderRow } from "./components/SecretOverviewFolderRow"; import { SecretOverviewTableRow } from "./components/SecretOverviewTableRow"; import { SelectionPanel } from "./components/SelectionPanel/SelectionPanel"; -import { EntryType, useSelectedEntries, useSelectedEntryActions } from "./SecretOverviewPage.store"; + +export enum EntryType { + FOLDER = "folder", + SECRET = "secret" +} export const SecretOverviewPage = () => { const { t } = useTranslation(); @@ -84,15 +88,6 @@ export const SecretOverviewPage = () => { const [expandableTableWidth, setExpandableTableWidth] = useState(0); const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); - useEffect(() => { - const handleParentTableWidthResize = () => { - setExpandableTableWidth(parentTableRef.current?.clientWidth || 0); - }; - - window.addEventListener("resize", handleParentTableWidthResize); - return () => window.removeEventListener("resize", handleParentTableWidthResize); - }, []); - useEffect(() => { if (parentTableRef.current) { setExpandableTableWidth(parentTableRef.current.clientWidth); @@ -107,8 +102,55 @@ export const SecretOverviewPage = () => { const [searchFilter, setSearchFilter] = useState(""); const secretPath = (router.query?.secretPath as string) || "/"; - const selectedEntries = useSelectedEntries(); - const { toggle: toggleSelectedEntry } = useSelectedEntryActions(); + const [selectedEntries, setSelectedEntries] = useState<{ + [EntryType.FOLDER]: Record; + [EntryType.SECRET]: Record; + }>({ + [EntryType.FOLDER]: {}, + [EntryType.SECRET]: {} + }); + + const toggleSelectedEntry = useCallback( + (type: EntryType, key: string) => { + const isChecked = Boolean(selectedEntries[type]?.[key]); + const newChecks = { ...selectedEntries }; + + // remove selection if its present else add it + if (isChecked) { + delete newChecks[type][key]; + } else { + newChecks[type][key] = true; + } + + setSelectedEntries(newChecks); + }, + [selectedEntries] + ); + + const resetSelectedEntries = useCallback(() => { + setSelectedEntries({ + [EntryType.FOLDER]: {}, + [EntryType.SECRET]: {} + }); + }, []); + + useEffect(() => { + const handleParentTableWidthResize = () => { + setExpandableTableWidth(parentTableRef.current?.clientWidth || 0); + }; + + const onRouteChangeStart = () => { + resetSelectedEntries(); + }; + + router.events.on("routeChangeStart", onRouteChangeStart); + + window.addEventListener("resize", handleParentTableWidthResize); + return () => { + window.removeEventListener("resize", handleParentTableWidthResize); + router.events.off("routeChangeStart", onRouteChangeStart); + }; + }, []); useEffect(() => { if (!isWorkspaceLoading && !workspaceId && router.isReady) { @@ -553,6 +595,8 @@ export const SecretOverviewPage = () => { secretPath={secretPath} getSecretByKey={getSecretByKey} getFolderByNameAndEnv={getFolderByNameAndEnv} + selectedEntries={selectedEntries} + resetSelectedEntries={resetSelectedEntries} />
diff --git a/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx b/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx index 89fcef58c7..6a2fe9bf6a 100644 --- a/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SelectionPanel/SelectionPanel.tsx @@ -15,23 +15,35 @@ import { usePopUp } from "@app/hooks"; import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api"; import { DecryptedSecret, TDeleteSecretBatchDTO, TSecretFolder } from "@app/hooks/api/types"; -import { useSelectedEntries, useSelectedEntryActions } from "../../SecretOverviewPage.store"; +export enum EntryType { + FOLDER = "folder", + SECRET = "secret" +} type Props = { secretPath: string; getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined; getFolderByNameAndEnv: (name: string, env: string) => TSecretFolder | undefined; + resetSelectedEntries: () => void; + selectedEntries: { + [EntryType.FOLDER]: Record; + [EntryType.SECRET]: Record; + }; }; -export const SelectionPanel = ({ getFolderByNameAndEnv, getSecretByKey, secretPath }: Props) => { +export const SelectionPanel = ({ + getFolderByNameAndEnv, + getSecretByKey, + secretPath, + resetSelectedEntries, + selectedEntries +}: Props) => { const { permission } = useProjectPermission(); const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ "bulkDeleteEntries" ] as const); - const selectedEntries = useSelectedEntries(); - const { reset: resetSelectedEntries } = useSelectedEntryActions(); const selectedCount = Object.keys(selectedEntries.folder).length + Object.keys(selectedEntries.secret).length; From 3638645b8a079e0c08d66486cd30f3f1ee52049f Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 7 May 2024 13:15:15 -0400 Subject: [PATCH 177/188] get closed by user --- ...update-be-new-migration-latest-timestamp.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/update-be-new-migration-latest-timestamp.yml b/.github/workflows/update-be-new-migration-latest-timestamp.yml index 160828473e..f35c33142f 100644 --- a/.github/workflows/update-be-new-migration-latest-timestamp.yml +++ b/.github/workflows/update-be-new-migration-latest-timestamp.yml @@ -38,6 +38,23 @@ jobs: rm added_files.txt git commit -m "chore: renamed new migration files to latest timestamp (gh-action)" + - name: Get the username of the person who closed the PR + run: | + PR_NUMBER=${{ github.event.pull_request.number }} + REPO_NAME=${{ github.repository }} + GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} + + # Use GitHub API to fetch PR data + PR_DATA=$(curl \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/$REPO_NAME/pulls/$PR_NUMBER) + + # Extract the username of the person who closed the PR + CLOSED_BY=$(echo $PR_DATA | jq -r .closed_by.login) + + echo "Pull Request #$PR_NUMBER was closed by $CLOSED_BY" + - name: Create Pull Request if: env.SKIP_RENAME != 'true' uses: peter-evans/create-pull-request@v6 From f5f0bf3c833b3df2d11674579618f790b4a1d5ee Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 7 May 2024 13:16:42 -0400 Subject: [PATCH 178/188] Create 20240507162180_test --- backend/src/db/migrations/20240507162180_test | 1 + 1 file changed, 1 insertion(+) create mode 100644 backend/src/db/migrations/20240507162180_test diff --git a/backend/src/db/migrations/20240507162180_test b/backend/src/db/migrations/20240507162180_test new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/backend/src/db/migrations/20240507162180_test @@ -0,0 +1 @@ + From ba238a8f3b4d41a8bd0fc4780b30dcc567fa7191 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 7 May 2024 13:25:10 -0400 Subject: [PATCH 179/188] get pr details by pr number --- ...date-be-new-migration-latest-timestamp.yml | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/.github/workflows/update-be-new-migration-latest-timestamp.yml b/.github/workflows/update-be-new-migration-latest-timestamp.yml index f35c33142f..d193c8722d 100644 --- a/.github/workflows/update-be-new-migration-latest-timestamp.yml +++ b/.github/workflows/update-be-new-migration-latest-timestamp.yml @@ -39,22 +39,13 @@ jobs: git commit -m "chore: renamed new migration files to latest timestamp (gh-action)" - name: Get the username of the person who closed the PR - run: | + run: | PR_NUMBER=${{ github.event.pull_request.number }} - REPO_NAME=${{ github.repository }} - GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} - - # Use GitHub API to fetch PR data - PR_DATA=$(curl \ - -H "Authorization: token $GITHUB_TOKEN" \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/$REPO_NAME/pulls/$PR_NUMBER) + PR_CLOSER=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" "https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUMBER" | jq -r '.closed_by.login') + echo "PR Number: $PR_NUMBER" + echo "PR Closer: $PR_CLOSER" + echo "pr_closer=$PR_CLOSER" >> $GITHUB_OUTPUT - # Extract the username of the person who closed the PR - CLOSED_BY=$(echo $PR_DATA | jq -r .closed_by.login) - - echo "Pull Request #$PR_NUMBER was closed by $CLOSED_BY" - - name: Create Pull Request if: env.SKIP_RENAME != 'true' uses: peter-evans/create-pull-request@v6 From c8433f39ed48adb4f16823e6337d5e5b79f06b30 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 7 May 2024 13:26:42 -0400 Subject: [PATCH 180/188] Delete backend/src/db/migrations/20240507162180_test --- backend/src/db/migrations/20240507162180_test | 1 - 1 file changed, 1 deletion(-) delete mode 100644 backend/src/db/migrations/20240507162180_test diff --git a/backend/src/db/migrations/20240507162180_test b/backend/src/db/migrations/20240507162180_test deleted file mode 100644 index 8b13789179..0000000000 --- a/backend/src/db/migrations/20240507162180_test +++ /dev/null @@ -1 +0,0 @@ - From 7fdaa1543adfaafa2698881901d17720b5c02d5a Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 7 May 2024 13:26:52 -0400 Subject: [PATCH 181/188] Create 20240507162180_test --- backend/src/db/migrations/20240507162180_test | 1 + 1 file changed, 1 insertion(+) create mode 100644 backend/src/db/migrations/20240507162180_test diff --git a/backend/src/db/migrations/20240507162180_test b/backend/src/db/migrations/20240507162180_test new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/backend/src/db/migrations/20240507162180_test @@ -0,0 +1 @@ + From 02c158b4ed2889252265a6f11f7909bb5bda030a Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 7 May 2024 13:47:25 -0400 Subject: [PATCH 182/188] Delete backend/src/db/migrations/20240507162180_test --- backend/src/db/migrations/20240507162180_test | 1 - 1 file changed, 1 deletion(-) delete mode 100644 backend/src/db/migrations/20240507162180_test diff --git a/backend/src/db/migrations/20240507162180_test b/backend/src/db/migrations/20240507162180_test deleted file mode 100644 index 8b13789179..0000000000 --- a/backend/src/db/migrations/20240507162180_test +++ /dev/null @@ -1 +0,0 @@ - From c87620109b69f8c297024335aa420667ca30d82c Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 7 May 2024 13:58:10 -0400 Subject: [PATCH 183/188] Rename 20240507162141_access to 20240507162141_access.ts --- .../{20240507162141_access => 20240507162141_access.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backend/src/db/migrations/{20240507162141_access => 20240507162141_access.ts} (100%) diff --git a/backend/src/db/migrations/20240507162141_access b/backend/src/db/migrations/20240507162141_access.ts similarity index 100% rename from backend/src/db/migrations/20240507162141_access rename to backend/src/db/migrations/20240507162141_access.ts From 80f7ff1ea898d6d4659ee081bddcd5d805d5eca9 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 7 May 2024 14:09:38 -0400 Subject: [PATCH 184/188] Create 20240507162149_test.ts --- backend/src/db/migrations/20240507162149_test.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 backend/src/db/migrations/20240507162149_test.ts diff --git a/backend/src/db/migrations/20240507162149_test.ts b/backend/src/db/migrations/20240507162149_test.ts new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/backend/src/db/migrations/20240507162149_test.ts @@ -0,0 +1 @@ + From 183bde55ca705120707b501b8dd21e3aa7ed0e33 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 7 May 2024 13:43:29 -0400 Subject: [PATCH 185/188] correctly fetch merged by user login --- .../update-be-new-migration-latest-timestamp.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/update-be-new-migration-latest-timestamp.yml b/.github/workflows/update-be-new-migration-latest-timestamp.yml index d193c8722d..684c786541 100644 --- a/.github/workflows/update-be-new-migration-latest-timestamp.yml +++ b/.github/workflows/update-be-new-migration-latest-timestamp.yml @@ -38,13 +38,15 @@ jobs: rm added_files.txt git commit -m "chore: renamed new migration files to latest timestamp (gh-action)" - - name: Get the username of the person who closed the PR - run: | + - name: Get PR details + id: pr_details + run: | PR_NUMBER=${{ github.event.pull_request.number }} - PR_CLOSER=$(curl -s -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" "https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUMBER" | jq -r '.closed_by.login') + PR_MERGER=$(curl -s "https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUMBER" | jq -r '.merged_by.login') + echo "PR Number: $PR_NUMBER" - echo "PR Closer: $PR_CLOSER" - echo "pr_closer=$PR_CLOSER" >> $GITHUB_OUTPUT + echo "PR Merger: $PR_MERGER" + echo "pr_merger=$PR_MERGER" >> $GITHUB_OUTPUT - name: Create Pull Request if: env.SKIP_RENAME != 'true' @@ -54,3 +56,4 @@ jobs: commit-message: 'chore: renamed new migration files to latest UTC (gh-action)' title: 'GH Action: rename new migration file timestamp' branch-suffix: timestamp + reviewers: ${{ steps.pr_details.outputs.pr_merger }} From 00faa6257ff3723627792c3452f21ab922c95718 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 7 May 2024 14:27:33 -0400 Subject: [PATCH 186/188] Delete backend/src/db/migrations/20240507162149_test.ts --- backend/src/db/migrations/20240507162149_test.ts | 1 - 1 file changed, 1 deletion(-) delete mode 100644 backend/src/db/migrations/20240507162149_test.ts diff --git a/backend/src/db/migrations/20240507162149_test.ts b/backend/src/db/migrations/20240507162149_test.ts deleted file mode 100644 index 8b13789179..0000000000 --- a/backend/src/db/migrations/20240507162149_test.ts +++ /dev/null @@ -1 +0,0 @@ - From 0383ae9e8b2a1b15e3ffb4fe06d5f98fa7931f76 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 7 May 2024 14:27:44 -0400 Subject: [PATCH 187/188] Create 20240507162149_test.ts --- backend/src/db/migrations/20240507162149_test.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 backend/src/db/migrations/20240507162149_test.ts diff --git a/backend/src/db/migrations/20240507162149_test.ts b/backend/src/db/migrations/20240507162149_test.ts new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/backend/src/db/migrations/20240507162149_test.ts @@ -0,0 +1 @@ + From 3a1cdc4f447fdd71cd3ab8fe2c227b91b71900ba Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Tue, 7 May 2024 15:41:09 -0400 Subject: [PATCH 188/188] Delete backend/src/db/migrations/20240507162149_test.ts --- backend/src/db/migrations/20240507162149_test.ts | 1 - 1 file changed, 1 deletion(-) delete mode 100644 backend/src/db/migrations/20240507162149_test.ts diff --git a/backend/src/db/migrations/20240507162149_test.ts b/backend/src/db/migrations/20240507162149_test.ts deleted file mode 100644 index 8b13789179..0000000000 --- a/backend/src/db/migrations/20240507162149_test.ts +++ /dev/null @@ -1 +0,0 @@ -
+
{isRequestListEmpty && (
From ac92a916b47c6f895e0754eb74db3589d02eefae Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:51:21 -0700 Subject: [PATCH 046/188] Update SecretApprovalPage.tsx --- .../views/SecretApprovalPage/SecretApprovalPage.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index ebbfd7b5e2..35ab35f944 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -7,6 +7,7 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { AccessApprovalPolicyList } from "./components/AccessApprovalPolicyList"; +import { AccessApprovalRequest } from "./components/AccessApprovalRequest"; import { SecretApprovalPolicyList } from "./components/SecretApprovalPolicyList"; import { SecretApprovalRequest } from "./components/SecretApprovalRequest"; @@ -19,7 +20,8 @@ enum TabSection { export const SecretApprovalPage = () => { const { currentWorkspace } = useWorkspace(); - const workspaceId = currentWorkspace?.id || ""; + const projectId = currentWorkspace?.id || ""; + const projectSlug = currentWorkspace?.slug || ""; return (
@@ -55,11 +57,15 @@ export const SecretApprovalPage = () => { - + + + + + - +
From 352d363bd4e3b159e891cbab10f97c6c2d4d90ed Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:53:02 -0700 Subject: [PATCH 047/188] Update generate-schema-types.ts --- backend/scripts/generate-schema-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 28b736152c..8c913991fd 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -5,7 +5,7 @@ import knex from "knex"; import { writeFileSync } from "fs"; dotenv.config({ - path: path.join(__dirname, "../../.env") + path: path.join(__dirname, "../../.env.migration") }); const db = knex({ From aedc1f2441a5681e9d356e1daadb7f435f80ae68 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:13:23 -0700 Subject: [PATCH 048/188] Update SpecificPrivilegeSection.tsx --- .../SpecificPrivilegeSection.tsx | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) 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 89058b08d6..b0c312f09b 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -239,21 +239,6 @@ export const SpecificPrivilegeSecretForm = ({ } }; - - 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; @@ -327,14 +312,13 @@ export const SpecificPrivilegeSecretForm = ({ if (isExpired) return "Access expired"; if (!temporaryAccessField?.isTemporary) return "Permanent"; - if (exactTime) + if (exactTime) { return `Until ${format( new Date(temporaryAccessField.temporaryAccessEndTime || ""), "yyyy-MM-dd HH:mm:ss" )}`; + } return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); - }; - }; return ( From 076c70f6ff697efc4a45974570f42ae198f80a72 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:19:26 -0700 Subject: [PATCH 049/188] Removed logs --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index ffb8ac049e..f29b8a52ff 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -276,9 +276,6 @@ export const AccessApprovalRequest = ({ const [requestedByFilter, setRequestedByFilter] = useState(undefined); const [envFilter, setEnvFilter] = useState(undefined); - console.log("requestedByFilter", requestedByFilter); - console.log("envFilter", envFilter); - const { data: requestCount } = useGetAccessRequestsCount({ projectSlug }); From 6f4b62cfbb00243b6db811859d847b0b9e89e5d2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:20:33 -0700 Subject: [PATCH 050/188] Removed logs --- .../MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx | 1 - .../components/SecretApprovalRequestChanges.tsx | 2 -- 2 files changed, 3 deletions(-) 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 b0c312f09b..2993704bb9 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -349,7 +349,6 @@ export const SpecificPrivilegeSecretForm = ({ control={privilegeForm.control} name="secretPath" render={({ field }) => { - console.log(policies); if (policies) { return ( diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx index e1437b5637..80dbe9f737 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx @@ -83,7 +83,6 @@ export const SecretApprovalRequestChanges = ({ workspaceId, members = {} }: Props) => { - const { user } = useUser(); const { data: decryptFileKey } = useGetUserWsKey(workspaceId); const { @@ -94,7 +93,6 @@ export const SecretApprovalRequestChanges = ({ id: approvalRequestId, decryptKey: decryptFileKey! }); - console.log(secretApprovalRequestDetails); const { mutateAsync: updateSecretApprovalRequestStatus, From 890c8b89bead0faaa43a5ea027dac4be6ca127e6 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:41:17 -0700 Subject: [PATCH 051/188] Removed unused parameter --- .../ee/routes/v1/access-approval-request-router.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 696c8b76fa..2a6142c88c 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -1,9 +1,7 @@ -import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas"; import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types"; -import { alphaNumericNanoId } from "@app/lib/nanoid"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -13,16 +11,6 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv method: "POST", schema: { body: z.object({ - slug: z - .string() - .min(1) - .max(60) - .trim() - .default(`requested-privilege-${slugify(alphaNumericNanoId(12))}`) - .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }), permissions: z.any().array(), isTemporary: z.boolean(), temporaryRange: z.string().optional() From e6c086ab09d93db17e34c516a5d1cc2ce1b9bdd3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:42:14 -0700 Subject: [PATCH 052/188] Fix: Don't allow users to request access to the same resource with same permissions multiple times --- .../access-approval-request-service.ts | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index d028429394..3640d801e7 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -1,8 +1,10 @@ import { ForbiddenError } from "@casl/ability"; +import slugify from "@sindresorhus/slugify"; import ms from "ms"; import { ProjectMembershipRole } from "@app/db/schemas"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; @@ -64,9 +66,15 @@ export const accessApprovalRequestServiceFactory = ({ if (!project) throw new UnauthorizedError({ message: "Project not found" }); // Anyone can create an access approval request. - const p = await permissionService.getProjectPermission(actor, actorId, project.id, actorAuthMethod, actorOrgId); + const { membership } = await permissionService.getProjectPermission( + actor, + actorId, + project.id, + actorAuthMethod, + actorOrgId + ); - if (!p.membership) throw new UnauthorizedError({ message: "You are not a member of this project" }); + if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" }); await projectDAL.checkProjectUpgradeStatus(project.id); @@ -81,9 +89,27 @@ export const accessApprovalRequestServiceFactory = ({ }); if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." }); + const duplicateRequest = await accessApprovalRequestDAL.findOne({ + policyId: policy.id, + requestedBy: membership.id, + permissions: JSON.stringify(permissions), + temporaryRange: temporaryRange || null, + isTemporary + }); + + if (duplicateRequest.privilegeId) { + const privilege = await additionalPrivilegeDAL.findById(duplicateRequest.privilegeId); + + const isExpired = new Date() > new Date(privilege.temporaryAccessEndTime || ("" as string)); + + if (!isExpired) { + throw new BadRequestError({ message: "You have already requested access with the same permissions" }); + } + } + const approvalRequest = await accessApprovalRequestDAL.create({ policyId: policy.id, - requestedBy: p.membership.id, + requestedBy: membership.id, temporaryRange: temporaryRange || null, permissions: JSON.stringify(permissions), isTemporary @@ -209,7 +235,7 @@ export const accessApprovalRequestServiceFactory = ({ // Permanent access const privilege = await additionalPrivilegeDAL.create({ projectMembershipId: accessApprovalRequest.requestedBy, - slug: "", + slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, permissions: JSON.stringify(accessApprovalRequest.permissions) }); privilegeId = privilege.id; @@ -220,7 +246,7 @@ export const accessApprovalRequestServiceFactory = ({ const privilege = await additionalPrivilegeDAL.create({ projectMembershipId: accessApprovalRequest.requestedBy, - slug: "", + slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, permissions: JSON.stringify(accessApprovalRequest.permissions), isTemporary: true, temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, From d371c568f1beac2e53bd8d8415bb643280e43efb Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:42:27 -0700 Subject: [PATCH 053/188] Add count --- .../src/hooks/api/accessApproval/mutation.tsx | 5 ++++- frontend/src/layouts/AppLayout/AppLayout.tsx | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx index 7a150092c2..c81295e609 100644 --- a/frontend/src/hooks/api/accessApproval/mutation.tsx +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -88,7 +88,10 @@ export const useCreateAccessRequest = () => { return data; }, onSuccess: (_, { projectSlug }) => { - queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequests(projectSlug)); + queryClient.invalidateQueries([ + accessApprovalKeys.getAccessApprovalRequests(projectSlug), + accessApprovalKeys.getAccessApprovalRequestCount(projectSlug) + ]); } }); }; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index a6c25c7c27..2fcdc93397 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -5,7 +5,7 @@ /* eslint-disable no-var */ /* eslint-disable func-names */ -import { useEffect } from "react"; +import { useEffect, useMemo } from "react"; import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import Image from "next/image"; @@ -64,6 +64,7 @@ import { fetchOrgUsers, useAddUserToWsNonE2EE, useCreateWorkspace, + useGetAccessRequestsCount, useGetOrgTrialUrl, useGetSecretApprovalRequestCount, useGetUserAction, @@ -124,9 +125,15 @@ export const AppLayout = ({ children }: LayoutProps) => { const { user } = useUser(); const { subscription } = useSubscription(); const workspaceId = currentWorkspace?.id || ""; + const projectSlug = currentWorkspace?.slug || ""; const { data: updateClosed } = useGetUserAction("december_update_closed"); const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId }); + const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug }); + + const pendingRequestsCount = useMemo(() => { + return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0); + }, [secretApprovalReqCount, accessApprovalRequestCount]); const isAddingProjectsAllowed = subscription?.workspaceLimit ? subscription.workspacesUsed < subscription.workspaceLimit @@ -555,9 +562,12 @@ export const AppLayout = ({ children }: LayoutProps) => { icon="system-outline-189-domain-verification" > Approvals - {Boolean(secretApprovalReqCount?.open) && ( + {Boolean( + secretApprovalReqCount?.open || + accessApprovalRequestCount?.pendingCount + ) && ( - {secretApprovalReqCount?.open} + {pendingRequestsCount} )} From 073a9ee6a469dd7dc83b4ec27f1071fff3fd6d40 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:43:45 -0700 Subject: [PATCH 054/188] Update licence-fns.ts --- backend/src/ee/services/license/licence-fns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 492d09ec3d..189a3c4e06 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ status: null, trial_end: null, has_used_trial: true, - secretApproval: true, + secretApproval: false, secretRotation: true }); From 15c747e8e8bacce64b3f85c9b76416db5b0c5611 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:48:27 -0700 Subject: [PATCH 055/188] Fix: Request access permissions --- .../access-approval-policy-service.ts | 9 ++++++++- .../access-approval-request-service.ts | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) 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 30f5f10fe4..d46f93e1db 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 @@ -248,7 +248,14 @@ export const accessApprovalPolicyServiceFactory = ({ if (!project) throw new BadRequestError({ message: "Project not found" }); - await permissionService.getProjectPermission(actor, actorId, project.id, actorAuthMethod, actorOrgId); + const { membership } = await permissionService.getProjectPermission( + actor, + actorId, + project.id, + actorAuthMethod, + actorOrgId + ); + if (!membership) throw new BadRequestError({ message: "User not found in project" }); const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug }); if (!environment) throw new BadRequestError({ message: "Environment not found" }); diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 3640d801e7..780e986557 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -272,14 +272,14 @@ export const accessApprovalRequestServiceFactory = ({ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new UnauthorizedError({ message: "Project not found" }); - const { permission } = await permissionService.getProjectPermission( + const { membership } = await permissionService.getProjectPermission( actor, actorId, project.id, actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + if (!membership) throw new BadRequestError({ message: "User not found in project" }); const count = await accessApprovalRequestDAL.getCount({ projectId: project.id }); From b59413ded00731f2bb65c5214e1ee6ca10cb7398 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 3 Apr 2024 19:47:59 -0700 Subject: [PATCH 056/188] fix privilegeId issue --- .../access-approval-request/access-approval-request-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 780e986557..230431d18a 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -97,7 +97,7 @@ export const accessApprovalRequestServiceFactory = ({ isTemporary }); - if (duplicateRequest.privilegeId) { + if (duplicateRequest?.privilegeId) { const privilege = await additionalPrivilegeDAL.findById(duplicateRequest.privilegeId); const isExpired = new Date() > new Date(privilege.temporaryAccessEndTime || ("" as string)); From f8e0e01bb89328c4af02cc811bda414ce0489812 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:02:37 -0700 Subject: [PATCH 057/188] Fix: Access request query invalidation --- frontend/src/hooks/api/accessApproval/mutation.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx index c81295e609..518729aa5a 100644 --- a/frontend/src/hooks/api/accessApproval/mutation.tsx +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -89,7 +89,6 @@ export const useCreateAccessRequest = () => { }, onSuccess: (_, { projectSlug }) => { queryClient.invalidateQueries([ - accessApprovalKeys.getAccessApprovalRequests(projectSlug), accessApprovalKeys.getAccessApprovalRequestCount(projectSlug) ]); } From 8c256bd9c87f2d67c4c8c1be87ccbc36586b6e70 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:02:54 -0700 Subject: [PATCH 058/188] Fix: Status filtering & query invalidation --- .../AccessApprovalRequest.tsx | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index f29b8a52ff..8fe69a2d03 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -37,12 +37,14 @@ import { import { usePopUp } from "@app/hooks"; import { useGetWorkspaceUsers, useReviewAccessRequest } from "@app/hooks/api"; import { + accessApprovalKeys, useGetAccessApprovalPolicies, useGetAccessApprovalRequests, useGetAccessRequestsCount } from "@app/hooks/api/accessApproval/queries"; import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types"; import { ApprovalStatus, TAccessApprovalPolicy, TWorkspaceUser } from "@app/hooks/api/types"; +import { queryClient } from "@app/reactQuery"; import { SpecificPrivilegeSecretForm } from "@app/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection"; const DisplayBadge = ({ text, className }: { text: string; className?: string }) => { @@ -296,8 +298,18 @@ export const AccessApprovalRequest = ({ }); const filteredRequests = useMemo(() => { - if (statusFilter === "open") return requests?.filter((request) => !request.isApproved); - if (statusFilter === "close") return requests?.filter((request) => request.isApproved); + if (statusFilter === "open") + return requests?.filter( + (request) => + !request.isApproved && + !request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED) + ); + if (statusFilter === "close") + return requests?.filter( + (request) => + request.isApproved || + request.reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED) + ); return requests; }, [requests, statusFilter, requestedByFilter, envFilter]); @@ -481,7 +493,7 @@ export const AccessApprovalRequest = ({
)} {!!filteredRequests?.length && - requests?.map((request) => { + filteredRequests?.map((request) => { const details = generateRequestDetails(request); return ( @@ -546,7 +558,16 @@ export const AccessApprovalRequest = ({ handlePopUpClose("requestAccess")} + onOpenChange={() => { + queryClient.invalidateQueries( + accessApprovalKeys.getAccessApprovalRequests( + projectSlug, + envFilter, + requestedByFilter + ) + ); + handlePopUpClose("requestAccess"); + }} /> )} From 299653528c4ddb7fe588072d7f3f5e6e0d1177db Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 3 Apr 2024 20:24:33 -0700 Subject: [PATCH 059/188] style changes --- .../SecretApprovalPage/SecretApprovalPage.tsx | 8 +- .../AccessApprovalPolicyList.tsx | 6 +- .../AccessApprovalRequest.tsx | 73 +++++++++---------- 3 files changed, 42 insertions(+), 45 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index 35ab35f944..a128817934 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -47,11 +47,11 @@ export const SecretApprovalPage = () => {
- Secret Approvals - Secret Policies + Change Requests + Change Request Policies - Access Approvals - Access Policies + Access Requests + Access Request Policies diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx index 7c7b354a83..ba12c51893 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx @@ -83,11 +83,11 @@ export const AccessApprovalPolicyList = ({ workspaceId }: IProps) => { return (
-
+
- Access Approval Policies + Access Request Policies
- Implement policies to prevent unauthorized secret changes. + Implement secret request policies for specific secrets and environments.
diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 8fe69a2d03..eb8b886f2b 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -5,7 +5,8 @@ import { faCheck, faCheckCircle, faChevronDown, - faLockOpen + faLock, + faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { formatDistance } from "date-fns"; @@ -25,6 +26,7 @@ import { EmptyState, Modal, ModalContent, + Tooltip, UpgradePlanModal } from "@app/components/v2"; import { @@ -51,7 +53,7 @@ const DisplayBadge = ({ text, className }: { text: string; className?: string }) return (
@@ -155,28 +157,28 @@ const ReviewRequestModal = ({
{request.user?.firstName} {request.user?.lastName} ({request.user?.email}) {" "} - is requesting access to the following resource + is requesting access to the following resource: -
-
+
+
Requested path:
-
+
Permissions:
-
+
Access Type: {getAccessLabel()}
@@ -188,7 +190,7 @@ const ReviewRequestModal = ({ isDisabled={!!isLoading} onClick={() => handleReview("approved")} className="mt-4" - size="xs" + size="sm" > Approve Request @@ -196,9 +198,8 @@ const ReviewRequestModal = ({ isLoading={isLoading === "rejected"} isDisabled={!!isLoading} onClick={() => handleReview("rejected")} - className="mt-4" - size="xs" - colorSchema="danger" + className="mt-4 bg-transparent border-transparent hover:bg-red/20 hover:border-red text-mineshaft-200 hover:text-mineshaft-200" + size="sm" > Reject Request @@ -237,11 +238,11 @@ const generateRequestText = (request: TAccessApprovalRequest) => { return ( Requested {isTemporary ? "temporary" : "permanent"} access to{" "} - + {request.policy.secretPath} in - + {request.environmentName} @@ -362,9 +363,9 @@ export const AccessApprovalRequest = ({ return (
-
+
- Access Approval Requests + Access Requests
Request access to secrets in sensitive environments and folders.
@@ -375,19 +376,21 @@ export const AccessApprovalRequest = ({ a={ProjectPermissionSub.SecretApproval} > {(isAllowed) => ( - + + + )}
@@ -414,7 +417,7 @@ export const AccessApprovalRequest = ({ statusFilter === "close" ? "text-gray-500 duration-100 hover:text-gray-400" : "" } > - + {!!requestCount && requestCount?.pendingCount} Pending
)} {!!filteredRequests?.length && - filteredRequests?.map((request) => { + requests?.map((request) => { const details = generateRequestDetails(request); return ( @@ -524,7 +527,7 @@ export const AccessApprovalRequest = ({ >
- + {generateRequestText(request)}
@@ -532,12 +535,6 @@ export const AccessApprovalRequest = ({ {membersGroupById?.[request.requestedBy]?.user?.firstName}{" "} {membersGroupById?.[request.requestedBy]?.user?.lastName} ( {membersGroupById?.[request.requestedBy]?.user?.email}){" "} - - {details.isApprover && - !details.isReviewedByUser && - !details.isAccepted && - "- Review required"} - {details.isApprover && ( From 3a002b921aebad865c80617e0ccf88f1afd7b62c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:29:20 -0700 Subject: [PATCH 060/188] Update AccessApprovalRequest.tsx --- .../AccessApprovalRequest/AccessApprovalRequest.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index eb8b886f2b..8ac0b5b78d 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -167,7 +167,7 @@ const ReviewRequestModal = ({ is requesting access to the following resource: -
+
Requested path: @@ -198,7 +198,7 @@ const ReviewRequestModal = ({ isLoading={isLoading === "rejected"} isDisabled={!!isLoading} onClick={() => handleReview("rejected")} - className="mt-4 bg-transparent border-transparent hover:bg-red/20 hover:border-red text-mineshaft-200 hover:text-mineshaft-200" + className="mt-4 border-transparent bg-transparent text-mineshaft-200 hover:border-red hover:bg-red/20 hover:text-mineshaft-200" size="sm" > Reject Request @@ -363,7 +363,7 @@ export const AccessApprovalRequest = ({ return (
-
+
Access Requests
@@ -376,7 +376,10 @@ export const AccessApprovalRequest = ({ a={ProjectPermissionSub.SecretApproval} > {(isAllowed) => ( - +
)} {!!filteredRequests?.length && - requests?.map((request) => { + filteredRequests?.map((request) => { const details = generateRequestDetails(request); return ( From 8c40918cef5d64aaf74b7c8760758c7d0fcfa078 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:33:58 -0700 Subject: [PATCH 061/188] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 8ac0b5b78d..c112a5b550 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -493,7 +493,7 @@ export const AccessApprovalRequest = ({
- {filteredRequests?.length === 0 && ( + {filteredRequests?.length === 0 && ( //
From 4cb51805f02fcd50b6787c255eec2b7a533b030f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:34:11 -0700 Subject: [PATCH 062/188] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index c112a5b550..8ac0b5b78d 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -493,7 +493,7 @@ export const AccessApprovalRequest = ({
- {filteredRequests?.length === 0 && ( // + {filteredRequests?.length === 0 && (
From f1428d72c2bb4eeecbac2c23b4e54b8f1009bde0 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:00:14 -0700 Subject: [PATCH 063/188] Fix: Security vulnurbility making it possible to spoof env & secret path requested. --- backend/src/ee/routes/v1/access-approval-request-router.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 2a6142c88c..4b173cfa76 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -16,9 +16,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv temporaryRange: z.string().optional() }), querystring: z.object({ - projectSlug: z.string().trim(), - secretPath: z.string().trim(), - envSlug: z.string().trim() + projectSlug: z.string().trim() }), response: { 200: z.object({ @@ -33,9 +31,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, permissions: req.body.permissions, - envSlug: req.query.envSlug, actorOrgId: req.permission.orgId, - secretPath: req.query.secretPath, projectSlug: req.query.projectSlug, temporaryRange: req.body.temporaryRange, isTemporary: req.body.isTemporary From fb2ab200b9650cac0a44750d7168033ca4516f17 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:00:39 -0700 Subject: [PATCH 064/188] Feat: Request access, extract permission details --- .../access-approval-request-fns.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-fns.ts diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-fns.ts b/backend/src/ee/services/access-approval-request/access-approval-request-fns.ts new file mode 100644 index 0000000000..90b42aaf7a --- /dev/null +++ b/backend/src/ee/services/access-approval-request/access-approval-request-fns.ts @@ -0,0 +1,53 @@ +import { PackRule, unpackRules } from "@casl/ability/extra"; + +import { UnauthorizedError } from "@app/lib/errors"; + +import { TVerifyPermission } from "./access-approval-request-types"; + +function filterUnique(value: string, index: number, array: string[]) { + return array.indexOf(value) === index; +} + +export const verifyRequestedPermissions = ({ permissions }: TVerifyPermission) => { + const permission = unpackRules( + permissions as PackRule<{ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + conditions?: Record; + action: string; + subject: [string]; + }>[] + ); + + if (!permission || !permission.length) { + throw new UnauthorizedError({ message: "No permission provided" }); + } + + const requestedPermissions: string[] = []; + + for (const p of permission) { + if (p.action[0] === "read") requestedPermissions.push("Read Access"); + if (p.action[0] === "create") requestedPermissions.push("Create Access"); + if (p.action[0] === "delete") requestedPermissions.push("Delete Access"); + if (p.action[0] === "edit") requestedPermissions.push("Edit Access"); + } + + const firstPermission = permission[0]; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const permissionSecretPath = firstPermission.conditions?.secretPath?.$glob; + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-unsafe-assignment + const permissionEnv = firstPermission.conditions?.environment; + + if (!permissionEnv || typeof permissionEnv !== "string") { + throw new UnauthorizedError({ message: "Permission environment is not a string" }); + } + if (!permissionSecretPath || typeof permissionSecretPath !== "string") { + throw new UnauthorizedError({ message: "Permission path is not a string" }); + } + + return { + envSlug: permissionEnv, + secretPath: permissionSecretPath, + accessTypes: requestedPermissions.filter(filterUnique) + }; +}; From 6ae62675be59bbd60c1eb5935f487a5e1a03921d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:01:14 -0700 Subject: [PATCH 065/188] Feat: Send emails for access requests --- .../access-approval-request-service.ts | 103 +++++++++++++----- 1 file changed, 77 insertions(+), 26 deletions(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 230431d18a..0c170c40f2 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -1,22 +1,25 @@ -import { ForbiddenError } from "@casl/ability"; import slugify from "@sindresorhus/slugify"; import ms from "ms"; import { ProjectMembershipRole } from "@app/db/schemas"; +import { getConfig } from "@app/lib/config/env"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; +import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service"; +import { TUserDALFactory } from "@app/services/user/user-dal"; +import { TAccessApprovalPolicyApproverDALFactory } from "../access-approval-policy/access-approval-policy-approver-dal"; import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/access-approval-policy-dal"; import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; -import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeServiceFactory } from "../project-user-additional-privilege/project-user-additional-privilege-service"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types"; import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal"; +import { verifyRequestedPermissions } from "./access-approval-request-fns"; import { TAccessApprovalRequestReviewerDALFactory } from "./access-approval-request-reviewer-dal"; import { ApprovalStatus, @@ -30,12 +33,15 @@ type TSecretApprovalRequestServiceFactoryDep = { additionalPrivilegeService: TProjectUserAdditionalPrivilegeServiceFactory; additionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory; permissionService: TPermissionServiceFactory; + accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory; projectEnvDAL: Pick; projectDAL: Pick; accessApprovalRequestDAL: TAccessApprovalRequestDALFactory; accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory; accessApprovalRequestReviewerDAL: TAccessApprovalRequestReviewerDALFactory; projectMembershipDAL: TProjectMembershipDALFactory; + smtpService: TSmtpService; + userDAL: TUserDALFactory; }; export type TAccessApprovalRequestServiceFactory = ReturnType; @@ -48,20 +54,22 @@ export const accessApprovalRequestServiceFactory = ({ accessApprovalRequestReviewerDAL, projectMembershipDAL, accessApprovalPolicyDAL, - additionalPrivilegeDAL + accessApprovalPolicyApproverDAL, + additionalPrivilegeDAL, + smtpService, + userDAL }: TSecretApprovalRequestServiceFactoryDep) => { const createAccessApprovalRequest = async ({ isTemporary, temporaryRange, actorId, - envSlug, - permissions, - secretPath, + permissions: requestedPermissions, actor, actorOrgId, actorAuthMethod, projectSlug }: TCreateAccessApprovalRequestDTO) => { + const cfg = getConfig(); const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new UnauthorizedError({ message: "Project not found" }); @@ -73,15 +81,17 @@ export const accessApprovalRequestServiceFactory = ({ actorAuthMethod, actorOrgId ); - if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" }); + const requestedByUser = await userDAL.findUserByProjectMembershipId(membership.id); + if (!requestedByUser) throw new UnauthorizedError({ message: "User not found" }); + await projectDAL.checkProjectUpgradeStatus(project.id); + const { envSlug, secretPath, accessTypes } = verifyRequestedPermissions({ permissions: requestedPermissions }); const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug }); if (!environment) throw new UnauthorizedError({ message: "Environment not found" }); - if (!secretPath) throw new UnauthorizedError({ message: "Secret path is required" }); const policy = await accessApprovalPolicyDAL.findOne({ envId: environment.id, @@ -89,33 +99,75 @@ export const accessApprovalRequestServiceFactory = ({ }); if (!policy) throw new UnauthorizedError({ message: "No policy matching criteria was found." }); + const approvers = await accessApprovalPolicyApproverDAL.find({ + policyId: policy.id + }); + + const approverUsers = await userDAL.findUsersByProjectMembershipIds( + approvers.map((approver) => approver.approverId) + ); + + if (approverUsers.length !== approvers.length) { + throw new BadRequestError({ message: "Some approvers were not found" }); + } + const duplicateRequest = await accessApprovalRequestDAL.findOne({ policyId: policy.id, requestedBy: membership.id, - permissions: JSON.stringify(permissions), - temporaryRange: temporaryRange || null, + permissions: JSON.stringify(requestedPermissions), isTemporary }); - if (duplicateRequest?.privilegeId) { - const privilege = await additionalPrivilegeDAL.findById(duplicateRequest.privilegeId); + if (duplicateRequest) { + if (duplicateRequest.privilegeId) { + const privilege = await additionalPrivilegeDAL.findById(duplicateRequest.privilegeId); - const isExpired = new Date() > new Date(privilege.temporaryAccessEndTime || ("" as string)); + const isExpired = new Date() > new Date(privilege.temporaryAccessEndTime || ("" as string)); - if (!isExpired) { - throw new BadRequestError({ message: "You have already requested access with the same permissions" }); + if (!isExpired || !privilege.isTemporary) { + throw new BadRequestError({ message: "You already have an active privilege with the same criteria" }); + } + } else { + throw new BadRequestError({ message: "You already have a pending access request with the same criteria" }); } } - const approvalRequest = await accessApprovalRequestDAL.create({ - policyId: policy.id, - requestedBy: membership.id, - temporaryRange: temporaryRange || null, - permissions: JSON.stringify(permissions), - isTemporary + const approval = await accessApprovalRequestDAL.transaction(async (tx) => { + const approvalRequest = await accessApprovalRequestDAL.create( + { + policyId: policy.id, + requestedBy: membership.id, + temporaryRange: temporaryRange || null, + permissions: JSON.stringify(requestedPermissions), + isTemporary + }, + tx + ); + + await smtpService.sendMail({ + recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!), + subjectLine: "Access Approval Request", + + substitutions: { + projectName: project.name, + requesterFullName: `${requestedByUser.firstName} ${requestedByUser.lastName}`, + requesterEmail: requestedByUser.email, + isTemporary, + ...(isTemporary && { + expiresIn: ms(ms(temporaryRange || ""), { long: true }) + }), + secretPath, + environment: envSlug, + permissions: accessTypes, + approvalUrl: `${cfg.SITE_URL}/project/${project.id}/approval` + }, + template: SmtpTemplates.AccessApprovalRequest + }); + + return approvalRequest; }); - return { request: approvalRequest }; + return { request: approval }; }; const listApprovalRequests = async ({ @@ -130,15 +182,14 @@ export const accessApprovalRequestServiceFactory = ({ const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); if (!project) throw new UnauthorizedError({ message: "Project not found" }); - const { permission } = await permissionService.getProjectPermission( + const { membership } = await permissionService.getProjectPermission( actor, actorId, project.id, actorAuthMethod, actorOrgId ); - - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); + if (!membership) throw new UnauthorizedError({ message: "You are not a member of this project" }); const policies = await accessApprovalPolicyDAL.find({ projectId: project.id }); let requests = await accessApprovalRequestDAL.findRequestsWithPrivilegeByPolicyIds(policies.map((p) => p.id)); @@ -181,7 +232,7 @@ export const accessApprovalRequestServiceFactory = ({ accessApprovalRequest.requestedBy !== membership.id && // The request wasn't made by the current user !policy.approvers.find((approverId) => approverId === membership.id) // The request isn't performed by an assigned approver ) { - throw new UnauthorizedError({ message: "No access" }); + throw new UnauthorizedError({ message: "You are not authorized to approve this request" }); } const reviewerProjectMembership = await projectMembershipDAL.findById(membership.id); From c7b60bcf0ea94306b531d273b264bf76dc3b64e4 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:01:22 -0700 Subject: [PATCH 066/188] Update access-approval-request-types.ts --- .../access-approval-request-types.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts index 6bba0c14d8..e11ca58d53 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-types.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-types.ts @@ -6,6 +6,10 @@ export enum ApprovalStatus { REJECTED = "rejected" } +export type TVerifyPermission = { + permissions: unknown; +}; + export type TGetAccessRequestCountDTO = { projectSlug: string; } & Omit; @@ -17,8 +21,6 @@ export type TReviewAccessRequestDTO = { export type TCreateAccessApprovalRequestDTO = { projectSlug: string; - secretPath: string; - envSlug: string; permissions: unknown; isTemporary: boolean; temporaryRange?: string; From 608c7a4dee51f1ad6b1aeb41d49783a01650e368 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:01:32 -0700 Subject: [PATCH 067/188] Update index.ts --- backend/src/server/routes/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 905d4028c5..63c5609be8 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -628,7 +628,10 @@ export const registerRoutes = async ( additionalPrivilegeService: projectUserAdditionalPrivilegeService, accessApprovalPolicyDAL, accessApprovalRequestDAL, - projectEnvDAL + projectEnvDAL, + userDAL, + smtpService, + accessApprovalPolicyApproverDAL }); const secretRotationQueue = secretRotationQueueFactory({ From 8e1d19c0412bade9e4847feb90c4680d1c37fb42 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:02:24 -0700 Subject: [PATCH 068/188] Feat: access request emails --- .../accessApprovalRequest.handlebars | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 backend/src/services/smtp/templates/accessApprovalRequest.handlebars diff --git a/backend/src/services/smtp/templates/accessApprovalRequest.handlebars b/backend/src/services/smtp/templates/accessApprovalRequest.handlebars new file mode 100644 index 0000000000..82c66ce5fe --- /dev/null +++ b/backend/src/services/smtp/templates/accessApprovalRequest.handlebars @@ -0,0 +1,50 @@ + + + + + + Access Approval Request + + + +

Infisical

+

New access approval request pending your review

+

You have a new access approval request pending review in project "{{projectName}}".

+ +

+ {{requesterFullName}} + ({{requesterEmail}}) has requested + {{#if isTemporary}} + temporary + {{else}} + permanent + {{/if}} + access to + {{secretPath}} + in the + {{environment}} + environment. + + {{#if isTemporary}} +
+ This access will expire + {{expiresIn}} + after it has been approved. + {{/if}} +

+

+ The following permissions are requested: +

    + {{#each permissions}} +
  • {{this}}
  • + {{/each}} +
+

+ +

+ View the request and approve or deny it + here. +

+ + + \ No newline at end of file From c5d11eee7f47b4018e936535a34f59dfa1164e75 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:02:39 -0700 Subject: [PATCH 069/188] Feat: Find users by project membership ID's --- backend/src/services/user/user-dal.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backend/src/services/user/user-dal.ts b/backend/src/services/user/user-dal.ts index 530ca3ad1a..f2da0df0e2 100644 --- a/backend/src/services/user/user-dal.ts +++ b/backend/src/services/user/user-dal.ts @@ -74,6 +74,17 @@ export const userDALFactory = (db: TDbClient) => { } }; + const findUsersByProjectMembershipIds = async (projectMembershipIds: string[]) => { + try { + return await db(TableName.ProjectMembership) + .whereIn(`${TableName.ProjectMembership}.id`, projectMembershipIds) + .join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`) + .select("*"); + } catch (error) { + throw new DatabaseError({ error, name: "Find users by project membership ids" }); + } + }; + const createUserEncryption = async (data: TUserEncryptionKeysInsert, tx?: Knex) => { try { const [userEnc] = await (tx || db)(TableName.UserEncryptionKey).insert(data).returning("*"); @@ -140,6 +151,7 @@ export const userDALFactory = (db: TDbClient) => { findUserEncKeyByUserId, updateUserEncryptionByUserId, findUserByProjectMembershipId, + findUsersByProjectMembershipIds, upsertUserEncryptionKey, createUserEncryption, findOneUserAction, From cb8763bc9c97eeda013a89602860f590e8ee0340 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:02:47 -0700 Subject: [PATCH 070/188] Update smtp-service.ts --- backend/src/services/smtp/smtp-service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/services/smtp/smtp-service.ts b/backend/src/services/smtp/smtp-service.ts index 0b43ffb908..81680537da 100644 --- a/backend/src/services/smtp/smtp-service.ts +++ b/backend/src/services/smtp/smtp-service.ts @@ -21,6 +21,7 @@ export enum SmtpTemplates { EmailVerification = "emailVerification.handlebars", SecretReminder = "secretReminder.handlebars", EmailMfa = "emailMfa.handlebars", + AccessApprovalRequest = "accessApprovalRequest.handlebars", HistoricalSecretList = "historicalSecretLeakIncident.handlebars", NewDeviceJoin = "newDevice.handlebars", OrgInvite = "organizationInvitation.handlebars", From 54313f9c08fab5e83f72bf03aeda7cbc8a60c5da Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:02:55 -0700 Subject: [PATCH 071/188] Renaming --- frontend/src/hooks/api/accessApproval/mutation.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx index 518729aa5a..1bf3841ff1 100644 --- a/frontend/src/hooks/api/accessApproval/mutation.tsx +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -69,18 +69,16 @@ export const useDeleteAccessApprovalPolicy = () => { export const useCreateAccessRequest = () => { const queryClient = useQueryClient(); return useMutation<{}, {}, TCreateAccessRequestDTO>({ - mutationFn: async ({ envSlug, projectSlug, secretPath, ...privilege }) => { + mutationFn: async ({ projectSlug, ...request }) => { const { data } = await apiRequest.post( "/api/v1/access-approval-requests", { - ...privilege, - permissions: privilege.permissions ? packRules(privilege.permissions) : undefined + ...request, + permissions: request.permissions ? packRules(request.permissions) : undefined }, { params: { - envSlug, - projectSlug, - secretPath + projectSlug } } ); @@ -88,9 +86,7 @@ export const useCreateAccessRequest = () => { return data; }, onSuccess: (_, { projectSlug }) => { - queryClient.invalidateQueries([ - accessApprovalKeys.getAccessApprovalRequestCount(projectSlug) - ]); + queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequestCount(projectSlug)); } }); }; From 478520f090f94930bf70aecf0422c2269a89d1d8 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:03:10 -0700 Subject: [PATCH 072/188] Remove unnessecary types and projectMembershipid --- frontend/src/hooks/api/accessApproval/types.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/hooks/api/accessApproval/types.ts b/frontend/src/hooks/api/accessApproval/types.ts index 1ffd0bf2e6..2176b8bc1b 100644 --- a/frontend/src/hooks/api/accessApproval/types.ts +++ b/frontend/src/hooks/api/accessApproval/types.ts @@ -92,10 +92,8 @@ export type TProjectUserPrivilege = { ); export type TCreateAccessRequestDTO = { - envSlug: string; projectSlug: string; - secretPath: string; -} & Omit; +} & Omit; export type TGetAccessApprovalRequestsDTO = { projectSlug: string; From 2369ff6813a29a2e214ab59598f397ae49b40b56 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:03:19 -0700 Subject: [PATCH 073/188] Removed unnessecary types --- .../MemberRoleForm/SpecificPrivilegeSection.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) 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 2993704bb9..88dba72a1d 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -84,7 +84,6 @@ export const SpecificPrivilegeSecretForm = ({ onClose?: () => void; }) => { const { currentWorkspace } = useWorkspace(); - const { membership: projectMembership } = useProjectPermission(); const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ "deletePrivilege", @@ -271,17 +270,10 @@ export const SpecificPrivilegeSecretForm = ({ 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" + temporaryRange: data.temporaryAccess.temporaryRange }), - envSlug: data.environmentSlug, - secretPath: data.secretPath, projectSlug: currentWorkspace.slug, - projectMembershipId: projectMembership.id, isTemporary: data.temporaryAccess.isTemporary, permissions: actions .filter(({ allowed }) => allowed) @@ -312,7 +304,7 @@ export const SpecificPrivilegeSecretForm = ({ if (isExpired) return "Access expired"; if (!temporaryAccessField?.isTemporary) return "Permanent"; - if (exactTime) { + if (exactTime && !policies) { return `Until ${format( new Date(temporaryAccessField.temporaryAccessEndTime || ""), "yyyy-MM-dd HH:mm:ss" From 0990ce1f9257ca8892118b79e78997b679db0980 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:03:29 -0700 Subject: [PATCH 074/188] Capitalization --- .../AccessApprovalPolicyList/components/AccessPolicyForm.tsx | 2 +- .../AccessApprovalRequest/components/AccessPolicyForm.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index 6b53e2f1e3..dd51ea53de 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -135,7 +135,7 @@ export const AccessPolicyForm = ({ return ( - +
- + Date: Thu, 4 Apr 2024 01:03:44 -0700 Subject: [PATCH 075/188] Style: Fix styling --- .../AccessApprovalRequest.tsx | 133 +++++++++--------- 1 file changed, 70 insertions(+), 63 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 8ac0b5b78d..b072513dc2 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -15,7 +15,6 @@ import ms from "ms"; import { twMerge } from "tailwind-merge"; import { createNotification } from "@app/components/notifications"; -import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, DropdownMenu, @@ -31,7 +30,6 @@ import { } from "@app/components/v2"; import { ProjectPermissionActions, - ProjectPermissionSub, useProjectPermission, useSubscription, useWorkspace @@ -53,7 +51,7 @@ const DisplayBadge = ({ text, className }: { text: string; className?: string }) return (
@@ -232,20 +230,29 @@ const SelectAccessModal = ({ ); }; -const generateRequestText = (request: TAccessApprovalRequest) => { +const generateRequestText = (request: TAccessApprovalRequest, membershipId: string) => { const { isTemporary } = request; return ( - - Requested {isTemporary ? "temporary" : "permanent"} access to{" "} - - {request.policy.secretPath} - - in - - {request.environmentName} - - +
+
+ Requested {isTemporary ? "temporary" : "permanent"} access to{" "} + + {request.policy.secretPath} + + in + + {request.environmentName} + +
+
+ {request.requestedBy === membershipId && ( + + + + )} +
+
); }; @@ -265,7 +272,7 @@ export const AccessApprovalRequest = ({ "reviewRequest", "upgradePlan" ] as const); - const { permission, membership } = useProjectPermission(); + const { membership } = useProjectPermission(); const { subscription } = useSubscription(); const { currentWorkspace } = useWorkspace(); @@ -284,12 +291,7 @@ export const AccessApprovalRequest = ({ }); const { data: policies, isLoading: policiesLoading } = useGetAccessApprovalPolicies({ - projectSlug, - options: { - enabled: - permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval) && - !!projectSlug - } + projectSlug }); const { data: requests } = useGetAccessApprovalRequests({ @@ -371,31 +373,24 @@ export const AccessApprovalRequest = ({
- - {(isAllowed) => ( - - - - )} - + +
@@ -506,10 +501,11 @@ export const AccessApprovalRequest = ({
{ + if (!details.isApprover) return; if (details.isReviewedByUser || details.isRejectedByAnyone) return; setSelectedRequest({ @@ -519,6 +515,8 @@ export const AccessApprovalRequest = ({ handlePopUpOpen("reviewRequest"); }} onKeyDown={(evt) => { + if (!details.isApprover) return; + if (details.isReviewedByUser || details.isRejectedByAnyone) return; if (evt.key === "Enter") { setSelectedRequest({ ...request, @@ -528,24 +526,33 @@ export const AccessApprovalRequest = ({ } }} > -
-
- - {generateRequestText(request)} +
+
+
+ + {generateRequestText(request, membership.id)} +
+
+
+ {membersGroupById?.[request.requestedBy]?.user && ( + <> + Requested {formatDistance(new Date(request.createdAt), new Date())}{" "} + ago by {membersGroupById?.[request.requestedBy]?.user?.firstName}{" "} + {membersGroupById?.[request.requestedBy]?.user?.lastName} ( + {membersGroupById?.[request.requestedBy]?.user?.email}){" "} + + )} +
+
+ {details.isApprover && ( + + )} +
+
- - Requested {formatDistance(new Date(request.createdAt), new Date())} ago by{" "} - {membersGroupById?.[request.requestedBy]?.user?.firstName}{" "} - {membersGroupById?.[request.requestedBy]?.user?.lastName} ( - {membersGroupById?.[request.requestedBy]?.user?.email}){" "} - - - {details.isApprover && ( - - )}
); From da5278f6bf1f77b4233d8a8e5f959c0cf0204aad Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:03:58 -0700 Subject: [PATCH 076/188] Fix: Rename change -> secret --- frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx | 4 ++-- .../components/SecretApprovalRequestAction.tsx | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index a128817934..18ce5f391e 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -47,8 +47,8 @@ export const SecretApprovalPage = () => {
- Change Requests - Change Request Policies + Secret Requests + Secret Policies Access Requests Access Request Policies diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx index 51f26a5493..c3d804779a 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/components/SecretApprovalRequestAction.tsx @@ -37,7 +37,6 @@ export const SecretApprovalRequestAction = ({ workspaceId, canApprove }: Props) => { - const { mutateAsync: performSecretApprovalMerge, isLoading: isMerging } = usePerformSecretApprovalRequestMerge(); @@ -136,7 +135,7 @@ export const SecretApprovalRequestAction = ({
- Change request merged + Secret approval merged Merged by {statusChangeByEmail} @@ -150,7 +149,7 @@ export const SecretApprovalRequestAction = ({
- Change request has been closed + Secret approval has been closed Closed by {statusChangeByEmail} From 32a110e0ca8487946ee240a24461014778ae868d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 12:44:49 -0700 Subject: [PATCH 077/188] Fix: Multiple approvers acceptance bug --- .../access-approval-request-service.ts | 40 +++++++++++-------- .../AccessApprovalRequest.tsx | 32 ++++++++++----- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index 0c170c40f2..b7b61fd8e0 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -274,8 +274,8 @@ export const accessApprovalRequestServiceFactory = ({ const approvedReviews = allReviews.filter((r) => r.status === ApprovalStatus.APPROVED); - // If all approvers have approved the request, update the privilege to approved - if (approvedReviews.length === policy.approvers.length) { + // approvals is the required number of approvals. If the number of approved reviews is equal to the number of required approvals, then the request is approved. + if (approvedReviews.length === policy.approvals) { if (accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) { throw new BadRequestError({ message: "Temporary range is required for temporary access" }); } @@ -284,27 +284,33 @@ export const accessApprovalRequestServiceFactory = ({ if (!accessApprovalRequest.isTemporary && !accessApprovalRequest.temporaryRange) { // Permanent access - const privilege = await additionalPrivilegeDAL.create({ - projectMembershipId: accessApprovalRequest.requestedBy, - slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, - permissions: JSON.stringify(accessApprovalRequest.permissions) - }); + const privilege = await additionalPrivilegeDAL.create( + { + projectMembershipId: accessApprovalRequest.requestedBy, + slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, + permissions: JSON.stringify(accessApprovalRequest.permissions) + }, + tx + ); privilegeId = privilege.id; } else { // Temporary access const relativeTempAllocatedTimeInMs = ms(accessApprovalRequest.temporaryRange!); const startTime = new Date(); - const privilege = await additionalPrivilegeDAL.create({ - projectMembershipId: accessApprovalRequest.requestedBy, - slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, - permissions: JSON.stringify(accessApprovalRequest.permissions), - isTemporary: true, - temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, - temporaryRange: accessApprovalRequest.temporaryRange!, - temporaryAccessStartTime: startTime, - temporaryAccessEndTime: new Date(new Date(startTime).getTime() + relativeTempAllocatedTimeInMs) - }); + const privilege = await additionalPrivilegeDAL.create( + { + projectMembershipId: accessApprovalRequest.requestedBy, + slug: `requested-privilege-${slugify(alphaNumericNanoId(12))}`, + permissions: JSON.stringify(accessApprovalRequest.permissions), + isTemporary: true, + temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, + temporaryRange: accessApprovalRequest.temporaryRange!, + temporaryAccessStartTime: startTime, + temporaryAccessEndTime: new Date(new Date(startTime).getTime() + relativeTempAllocatedTimeInMs) + }, + tx + ); privilegeId = privilege.id; } diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index b072513dc2..2421a629d8 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -342,12 +342,14 @@ export const AccessApprovalRequest = ({ displayData = { label: "Access Granted", colorClass: "bg-green/20 text-green" }; else if (isRejectedByAnyone) displayData = { label: "Rejected", colorClass: "bg-red/20 text-red" }; - else if (userReviewStatus === ApprovalStatus.APPROVED) + else if (userReviewStatus === ApprovalStatus.APPROVED) { displayData = { - label: `Pending ${request.policy.approvals - request.reviewers.length} reviews`, + label: `Pending ${request.policy.approvals - request.reviewers.length} review${ + request.policy.approvals - request.reviewers.length > 1 ? "s" : "" + }`, colorClass: "bg-yellow/20 text-yellow" }; - else if (!isReviewedByUser) + } else if (!isReviewedByUser) displayData = { label: "Review Required", colorClass: "bg-yellow/20 text-yellow" @@ -499,14 +501,21 @@ export const AccessApprovalRequest = ({ return (
{ - if (!details.isApprover) return; - if (details.isReviewedByUser || details.isRejectedByAnyone) return; + if ( + !details.isApprover || + details.isReviewedByUser || + details.isRejectedByAnyone || + details.isAccepted + ) + return; setSelectedRequest({ ...request, @@ -515,8 +524,13 @@ export const AccessApprovalRequest = ({ handlePopUpOpen("reviewRequest"); }} onKeyDown={(evt) => { - if (!details.isApprover) return; - if (details.isReviewedByUser || details.isRejectedByAnyone) return; + if ( + !details.isApprover || + details.isAccepted || + details.isReviewedByUser || + details.isRejectedByAnyone + ) + return; if (evt.key === "Enter") { setSelectedRequest({ ...request, @@ -543,7 +557,7 @@ export const AccessApprovalRequest = ({ )}
-
+
{details.isApprover && ( Date: Fri, 29 Mar 2024 16:24:15 +0100 Subject: [PATCH 078/188] Draft --- backend/src/ee/services/license/licence-fns.ts | 2 +- .../src/components/permissions/PermissionDeniedBanner.tsx | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 189a3c4e06..492d09ec3d 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ status: null, trial_end: null, has_used_trial: true, - secretApproval: false, + secretApproval: true, secretRotation: true }); diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx index b3c7a4f538..f9707d6347 100644 --- a/frontend/src/components/permissions/PermissionDeniedBanner.tsx +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -3,6 +3,8 @@ import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; +import { Button } from "../v2"; + type Props = { containerClassName?: string; className?: string; @@ -32,6 +34,9 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children )}
+
); From 4326ce970a2093e9e28fc2791048ff3a974e0dd3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 1 Apr 2024 09:49:22 -0700 Subject: [PATCH 079/188] Feat: Request access --- backend/scripts/generate-schema-types.ts | 2 +- .../db/migrations/20240330075122_access-approval-policy.ts | 1 + .../access-approval-policy/access-approval-policy-service.ts | 4 ---- frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx | 4 ++++ .../AccessApprovalPolicyList/AccessApprovalPolicyList.tsx | 2 +- .../AccessApprovalPolicyList/components/AccessPolicyForm.tsx | 3 --- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 8c913991fd..28b736152c 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -5,7 +5,7 @@ import knex from "knex"; import { writeFileSync } from "fs"; dotenv.config({ - path: path.join(__dirname, "../../.env.migration") + path: path.join(__dirname, "../../.env") }); const db = knex({ diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts index 8203fb3334..20a50c37f9 100644 --- a/backend/src/db/migrations/20240330075122_access-approval-policy.ts +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -34,6 +34,7 @@ export async function up(knex: Knex): Promise { export async function down(knex: Knex): Promise { await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover); await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy); + await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); } 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 d46f93e1db..f08b941221 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 @@ -155,7 +155,6 @@ export const accessApprovalPolicyServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { @@ -179,7 +178,6 @@ export const accessApprovalPolicyServiceFactory = ({ ); await verifyApprovers({ - projectId: accessApprovalPolicy.projectId, orgId: actorOrgId, envSlug: accessApprovalPolicy.environment.slug, secretPath: doc.secretPath!, @@ -189,8 +187,6 @@ export const accessApprovalPolicyServiceFactory = ({ }); if (secretApprovers.length !== approvers.length) - throw new BadRequestError({ message: "Approver not found in project" }); - if (doc.approvals > secretApprovers.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); await accessApprovalPolicyApproverDAL.delete({ policyId: doc.id }, tx); await accessApprovalPolicyApproverDAL.insertMany( diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index 18ce5f391e..98ed330f59 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -67,6 +67,10 @@ export const SecretApprovalPage = () => { + + + +
); diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx index ba12c51893..3e8fdb3b96 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx @@ -83,7 +83,7 @@ export const AccessApprovalPolicyList = ({ workspaceId }: IProps) => { return (
-
+
Access Request Policies
diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index dd51ea53de..081556041d 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -177,7 +177,6 @@ export const AccessPolicyForm = ({ /> ( @@ -187,8 +186,6 @@ export const AccessPolicyForm = ({ /> ( Date: Wed, 3 Apr 2024 16:35:51 -0700 Subject: [PATCH 080/188] Feat: Request Access (migrations) --- .../src/db/migrations/20240330075122_access-approval-policy.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts index 20a50c37f9..8203fb3334 100644 --- a/backend/src/db/migrations/20240330075122_access-approval-policy.ts +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -34,7 +34,6 @@ export async function up(knex: Knex): Promise { export async function down(knex: Knex): Promise { await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover); await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy); - await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); } From 6d9de752d713673ab77605c1b575d66eec01dca1 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:37:49 -0700 Subject: [PATCH 081/188] Feat: Request access (new routes) --- backend/src/ee/routes/v1/access-approval-request-router.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 4b173cfa76..104f32665b 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -2,10 +2,6 @@ import { z } from "zod"; import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas"; import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types"; -import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AuthMode } from "@app/services/auth/auth-type"; - -export const registerAccessApprovalRequestRouter = async (server: FastifyZodProvider) => { server.route({ url: "/", method: "POST", From 5e1484bd05c86f6010da496e21112f72e4505395 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:39:15 -0700 Subject: [PATCH 082/188] Fix: Validate approvers access --- .../access-approval-policy/access-approval-policy-service.ts | 1 + 1 file changed, 1 insertion(+) 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 f08b941221..cca29e4312 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 @@ -155,6 +155,7 @@ export const accessApprovalPolicyServiceFactory = ({ actorAuthMethod, actorOrgId ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { From e48377dea9e2f116574f527942269dc40d54a492 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:43:48 -0700 Subject: [PATCH 083/188] Fix: Remove redundant code --- .../src/components/permissions/PermissionDeniedBanner.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx index f9707d6347..b3c7a4f538 100644 --- a/frontend/src/components/permissions/PermissionDeniedBanner.tsx +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -3,8 +3,6 @@ import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -import { Button } from "../v2"; - type Props = { containerClassName?: string; className?: string; @@ -34,9 +32,6 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children )}
-
); From 926d324ae33d8f9ea6aab493ab4902d42e2bdd5d 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 084/188] Fix: Added support for request access --- .../SpecificPrivilegeSection.tsx | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) 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 88dba72a1d..4a7e456623 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -311,6 +311,91 @@ export const SpecificPrivilegeSecretForm = ({ )}`; } 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 ( From 70c06c91c8d489003c428cbf4609b57f13e99d26 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:51:21 -0700 Subject: [PATCH 085/188] Update SecretApprovalPage.tsx --- frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index 98ed330f59..5bd6ecacff 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -69,7 +69,7 @@ export const SecretApprovalPage = () => { - +
From 5a04371fb0e0f13c6324d9e4fb66b84a2f499aeb Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:53:02 -0700 Subject: [PATCH 086/188] Update generate-schema-types.ts --- backend/scripts/generate-schema-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 28b736152c..8c913991fd 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -5,7 +5,7 @@ import knex from "knex"; import { writeFileSync } from "fs"; dotenv.config({ - path: path.join(__dirname, "../../.env") + path: path.join(__dirname, "../../.env.migration") }); const db = knex({ From 0aa77f90c80df578abb41d2bae50fc24de062681 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:13:23 -0700 Subject: [PATCH 087/188] Update SpecificPrivilegeSection.tsx --- .../MemberRoleForm/SpecificPrivilegeSection.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 4a7e456623..b47768d1fe 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -388,14 +388,13 @@ export const SpecificPrivilegeSecretForm = ({ if (isExpired) return "Access expired"; if (!temporaryAccessField?.isTemporary) return "Permanent"; - if (exactTime) + if (exactTime) { return `Until ${format( new Date(temporaryAccessField.temporaryAccessEndTime || ""), "yyyy-MM-dd HH:mm:ss" )}`; + } return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); - }; - }; return ( From a687b1d0db278c7346bbcaf7c0c35916bf700f2a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:43:45 -0700 Subject: [PATCH 088/188] Update licence-fns.ts --- backend/src/ee/services/license/licence-fns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 492d09ec3d..189a3c4e06 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ status: null, trial_end: null, has_used_trial: true, - secretApproval: true, + secretApproval: false, secretRotation: true }); From bb9503471f72ab4cb8bee57157e83ea5664670dd Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 3 Apr 2024 20:24:33 -0700 Subject: [PATCH 089/188] style changes --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 2421a629d8..963dce37cb 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -496,7 +496,7 @@ export const AccessApprovalRequest = ({
)} {!!filteredRequests?.length && - filteredRequests?.map((request) => { + requests?.map((request) => { const details = generateRequestDetails(request); return ( From 22470376d901d778ef8190dfa3c2b25fd82d0e8f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:29:20 -0700 Subject: [PATCH 090/188] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 963dce37cb..2421a629d8 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -496,7 +496,7 @@ export const AccessApprovalRequest = ({
)} {!!filteredRequests?.length && - requests?.map((request) => { + filteredRequests?.map((request) => { const details = generateRequestDetails(request); return ( From bc810ea567d6cf3c0d9b4d0abfbb3027bb6c37d1 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:33:58 -0700 Subject: [PATCH 091/188] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 2421a629d8..960bdc8b41 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -490,7 +490,7 @@ export const AccessApprovalRequest = ({
- {filteredRequests?.length === 0 && ( + {filteredRequests?.length === 0 && ( //
From 8e82bfae868b27b38b5b0b88c2475df3aa9a09fc Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:34:11 -0700 Subject: [PATCH 092/188] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 960bdc8b41..2421a629d8 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -490,7 +490,7 @@ export const AccessApprovalRequest = ({
- {filteredRequests?.length === 0 && ( // + {filteredRequests?.length === 0 && (
From dab69dcb517f294aa3040560e72c2d89f63e97f7 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:03:19 -0700 Subject: [PATCH 093/188] Removed unnessecary types --- .../MemberRoleForm/SpecificPrivilegeSection.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) 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 b47768d1fe..4dc7437c41 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -349,15 +349,9 @@ export const SpecificPrivilegeSecretForm = ({ await requestAccess.mutateAsync({ ...data, ...(data.temporaryAccess.isTemporary && { - temporaryAccessStartTime: data.temporaryAccess.temporaryAccessStartTime, - temporaryAccessEndTime: data.temporaryAccess.temporaryAccessEndTime, - temporaryRange: data.temporaryAccess.temporaryRange, - temporaryMode: "relative" + temporaryRange: data.temporaryAccess.temporaryRange }), - envSlug: data.environmentSlug, - secretPath: data.secretPath, projectSlug: currentWorkspace.slug, - projectMembershipId: projectMembership.id, isTemporary: data.temporaryAccess.isTemporary, permissions: actions .filter(({ allowed }) => allowed) @@ -388,7 +382,7 @@ export const SpecificPrivilegeSecretForm = ({ if (isExpired) return "Access expired"; if (!temporaryAccessField?.isTemporary) return "Permanent"; - if (exactTime) { + if (exactTime && !policies) { return `Until ${format( new Date(temporaryAccessField.temporaryAccessEndTime || ""), "yyyy-MM-dd HH:mm:ss" From c874c943c19c21c9a6f605088281de4e2e733bf0 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 20:14:02 -0700 Subject: [PATCH 094/188] Fix: Rebase errors --- .../v1/access-approval-request-router.ts | 4 + .../access-approval-policy-service.ts | 1 + .../SpecificPrivilegeSection.tsx | 77 ------------------- .../components/AccessPolicyForm.tsx | 2 + 4 files changed, 7 insertions(+), 77 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 104f32665b..4b173cfa76 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -2,6 +2,10 @@ import { z } from "zod"; import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas"; import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerAccessApprovalRequestRouter = async (server: FastifyZodProvider) => { server.route({ url: "/", method: "POST", 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 cca29e4312..c384dd71ab 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 @@ -179,6 +179,7 @@ export const accessApprovalPolicyServiceFactory = ({ ); await verifyApprovers({ + projectId: accessApprovalPolicy.projectId, orgId: actorOrgId, envSlug: accessApprovalPolicy.environment.slug, secretPath: doc.secretPath!, 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 4dc7437c41..2f59e553e2 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -238,83 +238,6 @@ export const SpecificPrivilegeSecretForm = ({ } }; - // 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.temporaryAccess.isTemporary && { - temporaryRange: data.temporaryAccess.temporaryRange - }), - projectSlug: currentWorkspace.slug, - 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 && !policies) { - 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; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index 081556041d..a07acb5849 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -186,6 +186,8 @@ export const AccessPolicyForm = ({ /> ( Date: Thu, 4 Apr 2024 21:00:02 -0700 Subject: [PATCH 095/188] Update SecretApprovalPage.tsx --- frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index 5bd6ecacff..d66a6c8ccf 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -63,11 +63,6 @@ export const SecretApprovalPage = () => { - - - - - From 01e7ed23bae8935537d09e27ded9280be53ffa4f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Thu, 4 Apr 2024 21:12:05 -0700 Subject: [PATCH 096/188] Fixed bugs --- ....ts => 20240330075120_org-memberships-unique-constraint.ts} | 0 .../AccessApprovalPolicyList/components/AccessPolicyForm.tsx | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) rename backend/src/db/migrations/{20240405000045_org-memberships-unique-constraint.ts => 20240330075120_org-memberships-unique-constraint.ts} (100%) diff --git a/backend/src/db/migrations/20240405000045_org-memberships-unique-constraint.ts b/backend/src/db/migrations/20240330075120_org-memberships-unique-constraint.ts similarity index 100% rename from backend/src/db/migrations/20240405000045_org-memberships-unique-constraint.ts rename to backend/src/db/migrations/20240330075120_org-memberships-unique-constraint.ts diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index a07acb5849..dd51ea53de 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -177,6 +177,7 @@ export const AccessPolicyForm = ({ /> ( @@ -186,8 +187,8 @@ export const AccessPolicyForm = ({ /> ( Date: Thu, 4 Apr 2024 22:14:46 -0700 Subject: [PATCH 097/188] Migration improvements --- .../20240330075122_access-approval-policy.ts | 17 ++++++++++++----- .../20240401173320_access_approval_requests.ts | 11 +++++++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts index 8203fb3334..79af3aa6e0 100644 --- a/backend/src/db/migrations/20240330075122_access-approval-policy.ts +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -14,8 +14,8 @@ export async function up(knex: Knex): Promise { t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); t.timestamps(true, true, true); }); + await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); } - await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); if (!(await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover))) { await knex.schema.createTable(TableName.AccessApprovalPolicyApprover, (t) => { @@ -26,14 +26,21 @@ export async function up(knex: Knex): Promise { t.foreign("policyId").references("id").inTable(TableName.AccessApprovalPolicy).onDelete("CASCADE"); t.timestamps(true, true, true); }); + await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); } - - await createOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); } export async function down(knex: Knex): Promise { + const approverTableExists = await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover); + const policyTableExists = await knex.schema.hasTable(TableName.AccessApprovalPolicy); + await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover); await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy); - await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); - await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); + + if (approverTableExists) { + await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); + } + if (policyTableExists) { + await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); + } } diff --git a/backend/src/db/migrations/20240401173320_access_approval_requests.ts b/backend/src/db/migrations/20240401173320_access_approval_requests.ts index 901be9a78e..c03287b57c 100644 --- a/backend/src/db/migrations/20240401173320_access_approval_requests.ts +++ b/backend/src/db/migrations/20240401173320_access_approval_requests.ts @@ -43,9 +43,16 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { + const reviewerTableExists = await knex.schema.hasTable(TableName.AccessApprovalRequestReviewer); + const requestTableExists = await knex.schema.hasTable(TableName.AccessApprovalRequest); + await knex.schema.dropTableIfExists(TableName.AccessApprovalRequestReviewer); await knex.schema.dropTableIfExists(TableName.AccessApprovalRequest); - await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); - await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest); + if (reviewerTableExists) { + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); + } + if (requestTableExists) { + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest); + } } From 1785548a40049e84bb94e4ea396cdd889d46750e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:55:54 -0700 Subject: [PATCH 098/188] Fix: Sort by createdAt --- .../access-approval-request/access-approval-request-dal.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts index 78fef7c8ab..c3f4c72a66 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-dal.ts @@ -81,7 +81,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => { .as("privilegeTemporaryAccessEndTime"), db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("privilegePermissions") - ); + ) + .orderBy(`${TableName.AccessApprovalRequest}.createdAt`, "desc"); const formattedDocs = sqlNestRelationships({ data: docs, From c7d2dfd351abccb027a2131bc447b360b7fbccaf Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:57:14 -0700 Subject: [PATCH 099/188] Fix: Requesting approvals on previously rejected resources --- .../access-approval-request-service.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index b7b61fd8e0..d9762d879c 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -128,7 +128,15 @@ export const accessApprovalRequestServiceFactory = ({ throw new BadRequestError({ message: "You already have an active privilege with the same criteria" }); } } else { - throw new BadRequestError({ message: "You already have a pending access request with the same criteria" }); + const reviewers = await accessApprovalRequestReviewerDAL.find({ + requestId: duplicateRequest.id + }); + + const isRejected = reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED); + + if (!isRejected) { + throw new BadRequestError({ message: "You already have a pending access request with the same criteria" }); + } } } From 44370d49e320284f6210956fc7945c34abba44a6 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:57:36 -0700 Subject: [PATCH 100/188] Fix: Add tooltip for clarity and fix wording --- .../SpecificPrivilegeSection.tsx | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) 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 2f59e553e2..795ccbb24c 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -344,20 +344,27 @@ export const SpecificPrivilegeSecretForm = ({ render={({ field }) => { if (policies) { return ( - - - + +
+ + + +
+
); } return ( @@ -515,8 +522,9 @@ export const SpecificPrivilegeSecretForm = ({ ); }} > - {temporaryAccessField.isTemporary ? "Restart" : "Grant"} + {temporaryAccessField.isTemporary && !policies ? "Restart" : "Grant"} + {temporaryAccessField.isTemporary && ( )}
From c911a7cd8115c820e21831f0ff08a3416f2e24df Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:57:59 -0700 Subject: [PATCH 101/188] Fix: Don't display requested by when user has no access to read workspace members --- .../AccessApprovalRequest.tsx | 57 ++++++++++--------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 2421a629d8..b6d69d0c97 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -30,6 +30,7 @@ import { } from "@app/components/v2"; import { ProjectPermissionActions, + ProjectPermissionSub, useProjectPermission, useSubscription, useWorkspace @@ -272,7 +273,7 @@ export const AccessApprovalRequest = ({ "reviewRequest", "upgradePlan" ] as const); - const { membership } = useProjectPermission(); + const { membership, permission } = useProjectPermission(); const { subscription } = useSubscription(); const { currentWorkspace } = useWorkspace(); @@ -460,33 +461,37 @@ export const AccessApprovalRequest = ({ ))} - - - - - - Select an author - {members?.map(({ user, id }) => ( - - setRequestedByFilter((state) => (state === id ? undefined : id)) + {!!permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Member) && ( + + + + + + Select an author + {members?.map(({ user, id }) => ( + + setRequestedByFilter((state) => (state === id ? undefined : id)) + } + key={`request-filter-member-${id}`} + icon={requestedByFilter === id && } + iconPos="right" + > + {user.email} + + ))} + + + )}
From 4e449f62c0e6bd7c8552d2aa4922153dd53df54c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:58:16 -0700 Subject: [PATCH 102/188] Fix: Don't display requested by when user has no access to read workspace members --- .../SecretApprovalRequest.tsx | 63 +++++++++++-------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx index 96976d1405..32a7ff6de0 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx @@ -19,7 +19,13 @@ import { EmptyState, Skeleton } from "@app/components/v2"; -import { useUser, useWorkspace } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useUser, + useWorkspace +} from "@app/context"; import { useGetSecretApprovalRequestCount, useGetSecretApprovalRequests, @@ -58,6 +64,7 @@ export const SecretApprovalRequest = () => { const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } = useGetSecretApprovalRequestCount({ workspaceId }); const { user: presentUser } = useUser(); + const { permission } = useProjectPermission(); const { data: members } = useGetWorkspaceUsers(workspaceId); const membersGroupById = members?.reduce>( (prev, curr) => ({ ...prev, [curr.id]: curr }), @@ -156,31 +163,37 @@ export const SecretApprovalRequest = () => { ))} - - - - - - Select an author - {members?.map(({ user, id }) => ( - setCommitterFilter((state) => (state === id ? undefined : id))} - key={`request-filter-member-${id}`} - icon={committerFilter === id && } - iconPos="right" + {!!permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.Member) && ( + + + + + + Select an author + {members?.map(({ user, id }) => ( + + setCommitterFilter((state) => (state === id ? undefined : id)) + } + key={`request-filter-member-${id}`} + icon={committerFilter === id && } + iconPos="right" + > + {user.email} + + ))} + + + )}
From dc3014409f39e8896d2fd466ed7e41b1dcaf916e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:59:01 -0700 Subject: [PATCH 103/188] Delete access-approval-request-secret-dal.ts --- .../access-approval-request-secret-dal.ts | 230 ------------------ 1 file changed, 230 deletions(-) delete mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts deleted file mode 100644 index d458d58ffe..0000000000 --- a/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { Knex } from "knex"; - -import { TDbClient } from "@app/db"; -import { - SecretApprovalRequestsSecretsSchema, - TableName, - TSecretApprovalRequestsSecrets, - TSecretTags -} from "@app/db/schemas"; -import { BadRequestError, DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; - -export type TAccessApprovalRequestSecretDALFactory = ReturnType; - -export const accessApprovalRequestSecretDALFactory = (db: TDbClient) => { - const accessApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret); - const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag); - - const bulkUpdateNoVersionIncrement = async (data: TSecretApprovalRequestsSecrets[], tx?: Knex) => { - try { - const existingApprovalSecrets = await accessApprovalRequestSecretOrm.find( - { - $in: { - id: data.map((el) => el.id) - } - }, - { tx } - ); - - if (existingApprovalSecrets.length !== data.length) { - throw new BadRequestError({ message: "Some of the secret approvals do not exist" }); - } - - if (data.length === 0) return []; - - const updatedApprovalSecrets = await (tx || db)(TableName.SecretApprovalRequestSecret) - .insert(data) - .onConflict("id") // this will cause a conflict then merge the data - .merge() // Merge the data with the existing data - .returning("*"); - - return updatedApprovalSecrets; - } catch (error) { - throw new DatabaseError({ error, name: "bulk update secret" }); - } - }; - - const findByRequestId = async (requestId: string, tx?: Knex) => { - try { - const doc = await (tx || db)({ - secVerTag: TableName.SecretTag - }) - .from(TableName.SecretApprovalRequestSecret) - .where({ requestId }) - .leftJoin( - TableName.SecretApprovalRequestSecretTag, - `${TableName.SecretApprovalRequestSecret}.id`, - `${TableName.SecretApprovalRequestSecretTag}.secretId` - ) - .leftJoin(TableName.SecretTag, `${TableName.SecretApprovalRequestSecretTag}.tagId`, `${TableName.SecretTag}.id`) - .leftJoin(TableName.Secret, `${TableName.SecretApprovalRequestSecret}.secretId`, `${TableName.Secret}.id`) - .leftJoin( - TableName.SecretVersion, - `${TableName.SecretVersion}.id`, - `${TableName.SecretApprovalRequestSecret}.secretVersion` - ) - .leftJoin( - TableName.SecretVersionTag, - `${TableName.SecretVersionTag}.${TableName.SecretVersion}Id`, - `${TableName.SecretVersion}.id` - ) - .leftJoin( - db.ref(TableName.SecretTag).as("secVerTag"), - `${TableName.SecretVersionTag}.${TableName.SecretTag}Id`, - db.ref("id").withSchema("secVerTag") - ) - .select(selectAllTableCols(TableName.SecretApprovalRequestSecret)) - .select({ - secVerTagId: "secVerTag.id", - secVerTagColor: "secVerTag.color", - secVerTagSlug: "secVerTag.slug", - secVerTagName: "secVerTag.name" - }) - .select( - db.ref("id").withSchema(TableName.SecretTag).as("tagId"), - db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"), - db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), - db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), - db.ref("name").withSchema(TableName.SecretTag).as("tagName") - ) - .select( - db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"), - db.ref("version").withSchema(TableName.Secret).as("orgSecVersion"), - db.ref("secretKeyIV").withSchema(TableName.Secret).as("orgSecKeyIV"), - db.ref("secretKeyTag").withSchema(TableName.Secret).as("orgSecKeyTag"), - db.ref("secretKeyCiphertext").withSchema(TableName.Secret).as("orgSecKeyCiphertext"), - db.ref("secretValueIV").withSchema(TableName.Secret).as("orgSecValueIV"), - db.ref("secretValueTag").withSchema(TableName.Secret).as("orgSecValueTag"), - db.ref("secretValueCiphertext").withSchema(TableName.Secret).as("orgSecValueCiphertext"), - db.ref("secretCommentIV").withSchema(TableName.Secret).as("orgSecCommentIV"), - db.ref("secretCommentTag").withSchema(TableName.Secret).as("orgSecCommentTag"), - db.ref("secretCommentCiphertext").withSchema(TableName.Secret).as("orgSecCommentCiphertext") - ) - .select( - db.ref("version").withSchema(TableName.SecretVersion).as("secVerVersion"), - db.ref("secretKeyIV").withSchema(TableName.SecretVersion).as("secVerKeyIV"), - db.ref("secretKeyTag").withSchema(TableName.SecretVersion).as("secVerKeyTag"), - db.ref("secretKeyCiphertext").withSchema(TableName.SecretVersion).as("secVerKeyCiphertext"), - db.ref("secretValueIV").withSchema(TableName.SecretVersion).as("secVerValueIV"), - db.ref("secretValueTag").withSchema(TableName.SecretVersion).as("secVerValueTag"), - db.ref("secretValueCiphertext").withSchema(TableName.SecretVersion).as("secVerValueCiphertext"), - db.ref("secretCommentIV").withSchema(TableName.SecretVersion).as("secVerCommentIV"), - db.ref("secretCommentTag").withSchema(TableName.SecretVersion).as("secVerCommentTag"), - db.ref("secretCommentCiphertext").withSchema(TableName.SecretVersion).as("secVerCommentCiphertext") - ); - const formatedDoc = sqlNestRelationships({ - data: doc, - key: "id", - parentMapper: (data) => SecretApprovalRequestsSecretsSchema.omit({ secretVersion: true }).parse(data), - childrenMapper: [ - { - key: "tagJnId", - label: "tags" as const, - mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({ - id, - name, - slug, - color - }) - }, - { - key: "secretId", - label: "secret" as const, - mapper: ({ - orgSecKeyIV, - orgSecKeyTag, - orgSecValueIV, - orgSecVersion, - orgSecValueTag, - orgSecCommentIV, - orgSecBlindIndex, - orgSecCommentTag, - orgSecKeyCiphertext, - orgSecValueCiphertext, - orgSecCommentCiphertext, - secretId - }) => - secretId - ? { - id: secretId, - version: orgSecVersion, - secretBlindIndex: orgSecBlindIndex, - secretKeyIV: orgSecKeyIV, - secretKeyTag: orgSecKeyTag, - secretKeyCiphertext: orgSecKeyCiphertext, - secretValueIV: orgSecValueIV, - secretValueTag: orgSecValueTag, - secretValueCiphertext: orgSecValueCiphertext, - secretCommentIV: orgSecCommentIV, - secretCommentTag: orgSecCommentTag, - secretCommentCiphertext: orgSecCommentCiphertext - } - : undefined - }, - { - key: "secretVersion", - label: "secretVersion" as const, - mapper: ({ - secVerCommentIV, - secVerCommentCiphertext, - secVerCommentTag, - secVerValueCiphertext, - secVerKeyIV, - secVerKeyTag, - secVerValueIV, - secretVersion, - secVerValueTag, - secVerKeyCiphertext, - secVerVersion - }) => - secretVersion - ? { - version: secVerVersion, - id: secretVersion, - secretKeyIV: secVerKeyIV, - secretKeyTag: secVerKeyTag, - secretKeyCiphertext: secVerKeyCiphertext, - secretValueIV: secVerValueIV, - secretValueTag: secVerValueTag, - secretValueCiphertext: secVerValueCiphertext, - secretCommentIV: secVerCommentIV, - secretCommentTag: secVerCommentTag, - secretCommentCiphertext: secVerCommentCiphertext - } - : undefined, - childrenMapper: [ - { - key: "secVerTagId", - label: "tags" as const, - mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({ - // eslint-disable-next-line - id, - // eslint-disable-next-line - name, - // eslint-disable-next-line - slug, - // eslint-disable-next-line - color - }) - } - ] - } - ] - }); - return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({ - ...el, - secret: secret?.[0], - secretVersion: secretVersion?.[0] - })); - } catch (error) { - throw new DatabaseError({ error, name: "FindByRequestId" }); - } - }; - return { - ...accessApprovalRequestSecretOrm, - findByRequestId, - bulkUpdateNoVersionIncrement, - insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany - }; -}; From 972ecc3e9213da0a2072b08c3b8b92ef5023ffa5 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:18:02 -0700 Subject: [PATCH 104/188] Fix: Improved migrations --- .../20240330075122_access-approval-policy.ts | 11 ++--------- .../20240401173320_access_approval_requests.ts | 11 ++--------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts index 79af3aa6e0..61cb9274c2 100644 --- a/backend/src/db/migrations/20240330075122_access-approval-policy.ts +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -31,16 +31,9 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { - const approverTableExists = await knex.schema.hasTable(TableName.AccessApprovalPolicyApprover); - const policyTableExists = await knex.schema.hasTable(TableName.AccessApprovalPolicy); - await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover); await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy); - if (approverTableExists) { - await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); - } - if (policyTableExists) { - await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); - } + await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); + await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); } diff --git a/backend/src/db/migrations/20240401173320_access_approval_requests.ts b/backend/src/db/migrations/20240401173320_access_approval_requests.ts index c03287b57c..901be9a78e 100644 --- a/backend/src/db/migrations/20240401173320_access_approval_requests.ts +++ b/backend/src/db/migrations/20240401173320_access_approval_requests.ts @@ -43,16 +43,9 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { - const reviewerTableExists = await knex.schema.hasTable(TableName.AccessApprovalRequestReviewer); - const requestTableExists = await knex.schema.hasTable(TableName.AccessApprovalRequest); - await knex.schema.dropTableIfExists(TableName.AccessApprovalRequestReviewer); await knex.schema.dropTableIfExists(TableName.AccessApprovalRequest); - if (reviewerTableExists) { - await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); - } - if (requestTableExists) { - await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest); - } + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequestReviewer); + await dropOnUpdateTrigger(knex, TableName.AccessApprovalRequest); } From 9d362b8597d49d8acbb51160893ba3b50ffe8acf Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:18:15 -0700 Subject: [PATCH 105/188] Chore: Cleaned up models --- backend/src/db/schemas/models.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index f401e822c8..24c6b89142 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -50,20 +50,16 @@ export enum TableName { IdentityProjectMembershipRole = "identity_project_membership_role", IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege", ScimToken = "scim_tokens", - - // New tables so far AccessApprovalPolicy = "access_approval_policies", AccessApprovalPolicyApprover = "access_approval_policies_approvers", AccessApprovalRequest = "access_approval_requests", AccessApprovalRequestReviewer = "access_approval_requests_reviewers", - SecretApprovalPolicy = "secret_approval_policies", SecretApprovalPolicyApprover = "secret_approval_policies_approvers", SecretApprovalRequest = "secret_approval_requests", SecretApprovalRequestReviewer = "secret_approval_requests_reviewers", SecretApprovalRequestSecret = "secret_approval_requests_secrets", SecretApprovalRequestSecretTag = "secret_approval_request_secret_tags", - SecretRotation = "secret_rotations", SecretRotationOutput = "secret_rotation_outputs", SamlConfig = "saml_configs", From dd43268506957f0b85cc1df3671a5afd60efae6c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:18:45 -0700 Subject: [PATCH 106/188] Fix: Made API endpoints more REST compliant --- .../ee/routes/v1/access-approval-policy-router.ts | 10 +++++----- backend/src/ee/routes/v1/index.ts | 4 ++-- frontend/src/hooks/api/accessApproval/mutation.tsx | 10 +++++----- frontend/src/hooks/api/accessApproval/queries.tsx | 14 +++++++------- 4 files changed, 19 insertions(+), 19 deletions(-) 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 8a80900426..3b8949d3bb 100644 --- a/backend/src/ee/routes/v1/access-approval-policy-router.ts +++ b/backend/src/ee/routes/v1/access-approval-policy-router.ts @@ -59,7 +59,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const approvals = await server.services.accessApprovalPolicy.getAccessApprovalPolicyByProjectId({ + const approvals = await server.services.accessApprovalPolicy.getAccessApprovalPolicyByProjectSlug({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -71,7 +71,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi }); server.route({ - url: "/policy-count", + url: "/count", method: "GET", schema: { querystring: z.object({ @@ -80,14 +80,14 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi }), response: { 200: z.object({ - policyCount: z.number() + count: z.number() }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const { policyCount } = await server.services.accessApprovalPolicy.getAccessPolicyCountByEnvSlug({ + const { count } = await server.services.accessApprovalPolicy.getAccessPolicyCountByEnvSlug({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -95,7 +95,7 @@ export const registerAccessApprovalPolicyRouter = async (server: FastifyZodProvi actorOrgId: req.permission.orgId, envSlug: req.query.envSlug }); - return { policyCount }; + return { count }; } }); diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index c73ed24c5c..fc5c0865d3 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -43,8 +43,8 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { prefix: "/secret-rotation-providers" }); - await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals" }); - await server.register(registerAccessApprovalRequestRouter, { prefix: "/access-approval-requests" }); + await server.register(registerAccessApprovalPolicyRouter, { prefix: "/access-approvals/policies" }); + await server.register(registerAccessApprovalRequestRouter, { prefix: "/access-approvals/requests" }); await server.register( async (dynamicSecretRouter) => { diff --git a/frontend/src/hooks/api/accessApproval/mutation.tsx b/frontend/src/hooks/api/accessApproval/mutation.tsx index 1bf3841ff1..5f595c8a2b 100644 --- a/frontend/src/hooks/api/accessApproval/mutation.tsx +++ b/frontend/src/hooks/api/accessApproval/mutation.tsx @@ -17,7 +17,7 @@ export const useCreateAccessApprovalPolicy = () => { return useMutation<{}, {}, TCreateAccessPolicyDTO>({ mutationFn: async ({ environment, projectSlug, approvals, approvers, name, secretPath }) => { - const { data } = await apiRequest.post("/api/v1/access-approvals", { + const { data } = await apiRequest.post("/api/v1/access-approvals/policies", { environment, projectSlug, approvals, @@ -38,7 +38,7 @@ export const useUpdateAccessApprovalPolicy = () => { return useMutation<{}, {}, TUpdateAccessPolicyDTO>({ mutationFn: async ({ id, approvers, approvals, name, secretPath }) => { - const { data } = await apiRequest.patch(`/api/v1/access-approvals/${id}`, { + const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, { approvals, approvers, secretPath, @@ -57,7 +57,7 @@ export const useDeleteAccessApprovalPolicy = () => { return useMutation<{}, {}, TDeleteSecretPolicyDTO>({ mutationFn: async ({ id }) => { - const { data } = await apiRequest.delete(`/api/v1/access-approvals/${id}`); + const { data } = await apiRequest.delete(`/api/v1/access-approvals/policies/${id}`); return data; }, onSuccess: (_, { projectSlug }) => { @@ -71,7 +71,7 @@ export const useCreateAccessRequest = () => { return useMutation<{}, {}, TCreateAccessRequestDTO>({ mutationFn: async ({ projectSlug, ...request }) => { const { data } = await apiRequest.post( - "/api/v1/access-approval-requests", + "/api/v1/access-approvals/requests", { ...request, permissions: request.permissions ? packRules(request.permissions) : undefined @@ -106,7 +106,7 @@ export const useReviewAccessRequest = () => { >({ mutationFn: async ({ requestId, status }) => { const { data } = await apiRequest.post( - `/api/v1/access-approval-requests/${requestId}/review`, + `/api/v1/access-approvals/requests/${requestId}/review`, { status } diff --git a/frontend/src/hooks/api/accessApproval/queries.tsx b/frontend/src/hooks/api/accessApproval/queries.tsx index 112c53fe3d..599962e433 100644 --- a/frontend/src/hooks/api/accessApproval/queries.tsx +++ b/frontend/src/hooks/api/accessApproval/queries.tsx @@ -19,7 +19,7 @@ export const accessApprovalKeys = { [{ workspaceId, environment }, "access-approval-policy"] as const, getAccessApprovalRequests: (projectSlug: string, envSlug?: string, requestedBy?: string) => - [{ projectSlug, envSlug, requestedBy }, "access-approval-requests"] as const, + [{ projectSlug, envSlug, requestedBy }, "access-approvals-requests"] as const, getAccessApprovalRequestCount: (projectSlug: string) => [{ projectSlug }, "access-approval-request-count"] as const }; @@ -28,13 +28,13 @@ export const fetchPolicyApprovalCount = async ({ projectSlug, envSlug }: TGetAccessPolicyApprovalCountDTO) => { - const { data } = await apiRequest.get<{ policyCount: number }>( - "/api/v1/access-approvals/policy-count", + const { data } = await apiRequest.get<{ count: number }>( + "/api/v1/access-approvals/policies/count", { params: { projectSlug, envSlug } } ); - return data.policyCount; + return data.count; }; export const useGetAccessPolicyApprovalCount = ({ @@ -57,7 +57,7 @@ export const useGetAccessPolicyApprovalCount = ({ const fetchApprovalPolicies = async ({ projectSlug }: TGetAccessApprovalRequestsDTO) => { const { data } = await apiRequest.get<{ approvals: TAccessApprovalPolicy[] }>( - "/api/v1/access-approvals", + "/api/v1/access-approvals/policies", { params: { projectSlug } } ); return data.approvals; @@ -69,7 +69,7 @@ const fetchApprovalRequests = async ({ authorProjectMembershipId }: TGetAccessApprovalRequestsDTO) => { const { data } = await apiRequest.get<{ requests: TAccessApprovalRequest[] }>( - "/api/v1/access-approval-requests", + "/api/v1/access-approvals/requests", { params: { projectSlug, envSlug, authorProjectMembershipId } } ); @@ -90,7 +90,7 @@ const fetchApprovalRequests = async ({ const fetchAccessRequestsCount = async (projectSlug: string) => { const { data } = await apiRequest.get( - "/api/v1/access-approval-requests/count", + "/api/v1/access-approvals/requests/count", { params: { projectSlug } } ); return data; From 5884565de74c28d77611e6a5aa77f117ef41fc2d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:19:17 -0700 Subject: [PATCH 107/188] Fix: Make verifyApprovers independent on memberships --- .../access-approval-policy-fns.ts | 7 +++---- .../access-approval-policy-types.ts | 12 +++--------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts b/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts index b570c32fa1..7b0a2681fa 100644 --- a/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts +++ b/backend/src/ee/services/access-approval-policy/access-approval-policy-fns.ts @@ -7,7 +7,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/pr import { TVerifyApprovers } from "./access-approval-policy-types"; export const verifyApprovers = async ({ - approverProjectMemberships, + userIds, projectId, orgId, envSlug, @@ -15,12 +15,11 @@ export const verifyApprovers = async ({ secretPath, permissionService }: TVerifyApprovers) => { - for (const approver of approverProjectMemberships) { + for await (const userId of userIds) { try { - // eslint-disable-next-line no-await-in-loop const { permission: approverPermission } = await permissionService.getProjectPermission( ActorType.USER, - approver.userId, + userId, projectId, actorAuthMethod, orgId 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 c7b452771f..601561b680 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,11 +1,10 @@ -import { TProjectMemberships } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; import { ActorAuthMethod } from "@app/services/auth/auth-type"; import { TPermissionServiceFactory } from "../permission/permission-service"; export type TVerifyApprovers = { - approverProjectMemberships: TProjectMemberships[]; + userIds: string[]; permissionService: Pick; envSlug: string; actorAuthMethod: ActorAuthMethod; @@ -16,7 +15,7 @@ export type TVerifyApprovers = { export type TCreateAccessApprovalPolicy = { approvals: number; - secretPath?: string | null; + secretPath: string; environment: string; approvers: string[]; projectSlug: string; @@ -26,7 +25,7 @@ export type TCreateAccessApprovalPolicy = { export type TUpdateAccessApprovalPolicy = { policyId: string; approvals?: number; - approvers: string[]; + approvers?: string[]; secretPath?: string; name?: string; } & Omit; @@ -43,8 +42,3 @@ export type TGetAccessPolicyCountByEnvironmentDTO = { export type TListAccessApprovalPoliciesDTO = { projectSlug: string; } & Omit; - -export type TGetBoardAccessApprovalPolicy = { - projectId: string; - environment: string; -} & Omit; From 2c1eecaf85acdaa1a6358b7d14604b87d6155891 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:19:43 -0700 Subject: [PATCH 108/188] Chore: Moved verifyApprovers --- .../access-approval-policy-service.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) 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 c384dd71ab..51a51abb5a 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 @@ -54,7 +54,6 @@ export const accessApprovalPolicyServiceFactory = ({ if (approvals > approvers.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); - if (!secretPath) throw new BadRequestError({ message: "Secret path is required" }); const { permission } = await permissionService.getProjectPermission( actor, @@ -75,6 +74,10 @@ export const accessApprovalPolicyServiceFactory = ({ $in: { id: approvers } }); + if (secretApprovers.length !== approvers.length) { + throw new BadRequestError({ message: "Approver not found in project" }); + } + await verifyApprovers({ projectId: project.id, orgId: actorOrgId, @@ -82,13 +85,9 @@ export const accessApprovalPolicyServiceFactory = ({ secretPath, actorAuthMethod, permissionService, - approverProjectMemberships: secretApprovers + userIds: secretApprovers.map((approver) => approver.userId) }); - if (secretApprovers.length !== approvers.length) { - throw new BadRequestError({ message: "Approver not found in project" }); - } - const accessApproval = await accessApprovalPolicyDAL.transaction(async (tx) => { const doc = await accessApprovalPolicyDAL.create( { @@ -111,7 +110,7 @@ export const accessApprovalPolicyServiceFactory = ({ return { ...accessApproval, environment: env, projectId: project.id }; }; - const getAccessApprovalPolicyByProjectId = async ({ + const getAccessApprovalPolicyByProjectSlug = async ({ actorId, actor, actorOrgId, @@ -185,7 +184,7 @@ export const accessApprovalPolicyServiceFactory = ({ secretPath: doc.secretPath!, actorAuthMethod, permissionService, - approverProjectMemberships: secretApprovers + userIds: secretApprovers.map((approver) => approver.userId) }); if (secretApprovers.length !== approvers.length) @@ -261,7 +260,7 @@ export const accessApprovalPolicyServiceFactory = ({ const policies = await accessApprovalPolicyDAL.find({ envId: environment.id, projectId: project.id }); if (!policies) throw new BadRequestError({ message: "No policies found" }); - return { policyCount: policies.length }; + return { count: policies.length }; }; return { @@ -269,6 +268,6 @@ export const accessApprovalPolicyServiceFactory = ({ createAccessApprovalPolicy, deleteAccessApprovalPolicy, updateAccessApprovalPolicy, - getAccessApprovalPolicyByProjectId + getAccessApprovalPolicyByProjectSlug }; }; From 0c1103e778af82262dcd3515e0b773bf3dfb70b2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:19:58 -0700 Subject: [PATCH 109/188] Fix: Pick --- .../access-approval-request-service.ts | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index d9762d879c..f0714ea346 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -16,7 +16,6 @@ import { TAccessApprovalPolicyDALFactory } from "../access-approval-policy/acces import { verifyApprovers } from "../access-approval-policy/access-approval-policy-fns"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal"; -import { TProjectUserAdditionalPrivilegeServiceFactory } from "../project-user-additional-privilege/project-user-additional-privilege-service"; import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types"; import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal"; import { verifyRequestedPermissions } from "./access-approval-request-fns"; @@ -30,18 +29,29 @@ import { } from "./access-approval-request-types"; type TSecretApprovalRequestServiceFactoryDep = { - additionalPrivilegeService: TProjectUserAdditionalPrivilegeServiceFactory; - additionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory; - permissionService: TPermissionServiceFactory; - accessApprovalPolicyApproverDAL: TAccessApprovalPolicyApproverDALFactory; + additionalPrivilegeDAL: Pick; + permissionService: Pick; + accessApprovalPolicyApproverDAL: Pick; projectEnvDAL: Pick; projectDAL: Pick; - accessApprovalRequestDAL: TAccessApprovalRequestDALFactory; - accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory; - accessApprovalRequestReviewerDAL: TAccessApprovalRequestReviewerDALFactory; - projectMembershipDAL: TProjectMembershipDALFactory; - smtpService: TSmtpService; - userDAL: TUserDALFactory; + accessApprovalRequestDAL: Pick< + TAccessApprovalRequestDALFactory, + | "create" + | "findRequestsWithPrivilegeByPolicyIds" + | "findById" + | "transaction" + | "updateById" + | "findOne" + | "getCount" + >; + accessApprovalPolicyDAL: Pick; + accessApprovalRequestReviewerDAL: Pick< + TAccessApprovalRequestReviewerDALFactory, + "create" | "find" | "findOne" | "transaction" + >; + projectMembershipDAL: Pick; + smtpService: Pick; + userDAL: Pick; }; export type TAccessApprovalRequestServiceFactory = ReturnType; @@ -107,10 +117,6 @@ export const accessApprovalRequestServiceFactory = ({ approvers.map((approver) => approver.approverId) ); - if (approverUsers.length !== approvers.length) { - throw new BadRequestError({ message: "Some approvers were not found" }); - } - const duplicateRequest = await accessApprovalRequestDAL.findOne({ policyId: policy.id, requestedBy: membership.id, @@ -252,7 +258,7 @@ export const accessApprovalRequestServiceFactory = ({ secretPath: accessApprovalRequest.policy.secretPath!, actorAuthMethod, permissionService, - approverProjectMemberships: [reviewerProjectMembership] + userIds: [reviewerProjectMembership.userId] }); const existingReviews = await accessApprovalRequestReviewerDAL.find({ requestId: accessApprovalRequest.id }); From 24a286e8986f34cbdc949fef052607079557e74a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:20:05 -0700 Subject: [PATCH 110/188] Update index.ts --- backend/src/server/routes/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 63c5609be8..269a97c49d 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -625,7 +625,6 @@ export const registerRoutes = async ( accessApprovalRequestReviewerDAL, additionalPrivilegeDAL: projectUserAdditionalPrivilegeDAL, projectMembershipDAL, - additionalPrivilegeService: projectUserAdditionalPrivilegeService, accessApprovalPolicyDAL, accessApprovalRequestDAL, projectEnvDAL, From f038b28c1c1c21836bcf6ca1304e70609d381d26 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:20:18 -0700 Subject: [PATCH 111/188] Fix: Moved Divider to v2 --- frontend/src/components/{basic => v2/Divider}/Divider.tsx | 4 +--- frontend/src/components/v2/Divider/index.tsx | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) rename frontend/src/components/{basic => v2/Divider}/Divider.tsx (78%) create mode 100644 frontend/src/components/v2/Divider/index.tsx diff --git a/frontend/src/components/basic/Divider.tsx b/frontend/src/components/v2/Divider/Divider.tsx similarity index 78% rename from frontend/src/components/basic/Divider.tsx rename to frontend/src/components/v2/Divider/Divider.tsx index f1a570d7b1..39b0f84c57 100644 --- a/frontend/src/components/basic/Divider.tsx +++ b/frontend/src/components/v2/Divider/Divider.tsx @@ -4,12 +4,10 @@ interface IProps { className?: string; } -const Divider = ({ className }: IProps): JSX.Element => { +export const Divider = ({ className }: IProps): JSX.Element => { return (
); }; - -export default Divider; diff --git a/frontend/src/components/v2/Divider/index.tsx b/frontend/src/components/v2/Divider/index.tsx new file mode 100644 index 0000000000..ac407aa37d --- /dev/null +++ b/frontend/src/components/v2/Divider/index.tsx @@ -0,0 +1 @@ +export { Divider } from "./Divider"; From 6aab28c4c7055730c100867da35ceed95e33a62c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:21:13 -0700 Subject: [PATCH 112/188] Feat: Badge component --- frontend/src/components/v2/Badge/Badge.tsx | 32 ++++++++++++++++++++++ frontend/src/components/v2/Badge/index.tsx | 1 + 2 files changed, 33 insertions(+) create mode 100644 frontend/src/components/v2/Badge/Badge.tsx create mode 100644 frontend/src/components/v2/Badge/index.tsx diff --git a/frontend/src/components/v2/Badge/Badge.tsx b/frontend/src/components/v2/Badge/Badge.tsx new file mode 100644 index 0000000000..321c032966 --- /dev/null +++ b/frontend/src/components/v2/Badge/Badge.tsx @@ -0,0 +1,32 @@ +import { cva, VariantProps } from "cva"; +import { twMerge } from "tailwind-merge"; + +interface IProps { + children: React.ReactNode; + className?: string; +} + +const badgeVariants = cva( + [ + "inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-xs text-yellow opacity-80 hover:opacity-100" + ], + { + variants: { + variant: { + primary: "bg-yellow/20 text-yellow", + danger: "bg-red/20 text-red", + success: "bg-green/20 text-green" + } + } + } +); + +export type BadgeProps = VariantProps & IProps; + +export const Badge = ({ children, className, variant }: BadgeProps) => { + return ( +
+ {children} +
+ ); +}; diff --git a/frontend/src/components/v2/Badge/index.tsx b/frontend/src/components/v2/Badge/index.tsx new file mode 100644 index 0000000000..5c7042709a --- /dev/null +++ b/frontend/src/components/v2/Badge/index.tsx @@ -0,0 +1 @@ +export { Badge } from "./Badge"; From 3d3b1eb21acb50b1aac29420de4ab866b71cd34c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:21:26 -0700 Subject: [PATCH 113/188] Fix: Use username instead of email --- frontend/src/pages/login/select-organization.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/pages/login/select-organization.tsx b/frontend/src/pages/login/select-organization.tsx index 586ed62f90..de866a3235 100644 --- a/frontend/src/pages/login/select-organization.tsx +++ b/frontend/src/pages/login/select-organization.tsx @@ -35,8 +35,6 @@ export default function LoginPage() { const selectOrg = useSelectOrganization(); const { user, isLoading: userLoading } = useUser(); - - const queryParams = new URLSearchParams(window.location.search); const logout = useLogoutUser(true); @@ -153,7 +151,7 @@ export default function LoginPage() {

- You‘re currently logged in as {user.email} + You‘re currently logged in as {user.username}

Not you?{" "} From cdd836d58f156150b303405cc8bb16cc05543b47 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:21:35 -0700 Subject: [PATCH 114/188] Fix: Columns --- .../AccessApprovalPolicyList/AccessApprovalPolicyList.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx index 3e8fdb3b96..aa47cce80a 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/AccessApprovalPolicyList.tsx @@ -30,7 +30,7 @@ import { useGetAccessApprovalPolicies } from "@app/hooks/api/accessApproval/quer import { TAccessApprovalPolicy } from "@app/hooks/api/types"; import { AccessApprovalPolicyRow } from "./components/AccessApprovalPolicyRow"; -import { AccessPolicyForm } from "./components/AccessPolicyForm"; +import { AccessPolicyForm } from "./components/AccessPolicyModal"; interface IProps { workspaceId: string; @@ -127,11 +127,11 @@ export const AccessApprovalPolicyList = ({ workspaceId }: IProps) => { {isPoliciesLoading && ( - + )} {!isPoliciesLoading && !policies?.length && ( - + From 8ed3c0cd68df192750fcbf120045f9bc01538a29 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:21:47 -0700 Subject: [PATCH 115/188] Fix: Use username instead of email --- .../components/AccessApprovalPolicyRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx index 8554e928ee..8476bac8d8 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessApprovalPolicyRow.tsx @@ -98,7 +98,7 @@ export const AccessApprovalPolicyRow = ({ iconPos="right" icon={isChecked && } > - {user.email} + {user.username} ); })} From 3637152a6bd628caa432005842afb03bc40a61ed Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:21:57 -0700 Subject: [PATCH 116/188] Chore: Remove unused files --- .../components/AccessPolicyForm.tsx | 266 ------------------ .../components/AccessApprovalPolicyRow.tsx | 146 ---------- .../components/AccessPolicyForm.tsx | 261 ----------------- 3 files changed, 673 deletions(-) delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx delete mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx deleted file mode 100644 index dd51ea53de..0000000000 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.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.email} - - ); - })} - - - - )} - /> - ( - - field.onChange(parseInt(el.target.value, 10))} - /> - - )} - /> -

- - -
- - - - ); -}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx deleted file mode 100644 index 8554e928ee..0000000000 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/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.email} - - ); - })} - - - - {policy.approvals} - -
- - {(isAllowed) => ( - - - - )} - - - {(isAllowed) => ( - - - - )} - -
- - - ); -}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx deleted file mode 100644 index 7e88da1005..0000000000 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx +++ /dev/null @@ -1,261 +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) => { - 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 (!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.email} - - ); - })} - - - - )} - /> - ( - - field.onChange(parseInt(el.target.value, 10))} - /> - - )} - /> -
- - -
- -
-
- ); -}; From c325674da0ccea8490202cfb9f31f4eb06cf7cab Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:22:21 -0700 Subject: [PATCH 117/188] Fix: Move standalone components to individual files --- .../components/AccessPolicyModal.tsx | 266 ++++++++++++++++++ .../components/RequestAccessModal.tsx | 25 ++ .../components/ReviewAccessModal.tsx | 158 +++++++++++ 3 files changed, 449 insertions(+) create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/RequestAccessModal.tsx create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx new file mode 100644 index 0000000000..6c0ee3fb61 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyModal.tsx @@ -0,0 +1,266 @@ +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/AccessApprovalRequest/components/RequestAccessModal.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/RequestAccessModal.tsx new file mode 100644 index 0000000000..735ca3b53a --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/RequestAccessModal.tsx @@ -0,0 +1,25 @@ +import { Modal, ModalContent } from "@app/components/v2"; +import { TAccessApprovalPolicy } from "@app/hooks/api/types"; +import { SpecificPrivilegeSecretForm } from "@app/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection"; + +export const RequestAccessModal = ({ + isOpen, + onOpenChange, + policies +}: { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + policies: TAccessApprovalPolicy[]; +}) => { + return ( + + + onOpenChange(false)} policies={policies} /> + + + ); +}; diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx new file mode 100644 index 0000000000..481cee9c64 --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/ReviewAccessModal.tsx @@ -0,0 +1,158 @@ +import { useCallback, useMemo, useState } from "react"; +import ms from "ms"; + +import { createNotification } from "@app/components/notifications"; +import { Button, Modal, ModalContent } from "@app/components/v2"; +import { Badge } from "@app/components/v2/Badge"; +import { ProjectPermissionActions } from "@app/context"; +import { useReviewAccessRequest } from "@app/hooks/api"; +import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types"; +import { TWorkspaceUser } from "@app/hooks/api/types"; + +export const ReviewAccessRequestModal = ({ + isOpen, + onOpenChange, + request, + projectSlug, + selectedRequester, + selectedEnvSlug +}: { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + request: TAccessApprovalRequest & { user: TWorkspaceUser["user"] | null }; + projectSlug: string; + selectedRequester: string | undefined; + selectedEnvSlug: string | undefined; +}) => { + const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null); + + const accessDetails = { + env: request.environmentName, + // secret path will be inside $glob operator + secretPath: request.policy.secretPath, + read: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Read)), + edit: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Edit)), + create: request.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Create) + ), + delete: request.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Delete) + ), + + temporaryAccess: { + isTemporary: request.isTemporary, + temporaryRange: request.temporaryRange + } + }; + + const requestedAccess = useMemo(() => { + const access: string[] = []; + if (accessDetails.read) access.push("Read"); + if (accessDetails.edit) access.push("Edit"); + if (accessDetails.create) access.push("Create"); + if (accessDetails.delete) access.push("Delete"); + + return access.join(", "); + }, [accessDetails]); + + const getAccessLabel = () => { + if (!accessDetails.temporaryAccess.isTemporary || !accessDetails.temporaryAccess.temporaryRange) + return "Permanent"; + + // convert the range to human readable format + ms(ms(accessDetails.temporaryAccess.temporaryRange), { long: true }); + + return ( + + {`Valid for ${ms(ms(accessDetails.temporaryAccess.temporaryRange), { + long: true + })} after approval`} + + ); + }; + + const reviewAccessRequest = useReviewAccessRequest(); + + const handleReview = useCallback(async (status: "approved" | "rejected") => { + setIsLoading(status); + try { + await reviewAccessRequest.mutateAsync({ + requestId: request.id, + status, + projectSlug, + envSlug: selectedEnvSlug, + requestedBy: selectedRequester + }); + } catch (error) { + console.error(error); + setIsLoading(null); + return; + } + + createNotification({ + title: `Request ${status}`, + text: `The request has been ${status}`, + type: status === "approved" ? "success" : "info" + }); + + setIsLoading(null); + onOpenChange(false); + }, []); + + return ( + + +
+ + + {request.user?.firstName} {request.user?.lastName} ({request.user?.email}) + {" "} + is requesting access to the following resource: + + +
+
+ Requested path: + {accessDetails.env + accessDetails.secretPath || ""} +
+ +
+ Permissions: + {requestedAccess} +
+ +
+ Access Type: + {getAccessLabel()} +
+
+ +
+ + +
+
+
+
+ ); +}; From a3d7c5f5996121d856e0b615c850c343aa38c247 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:22:33 -0700 Subject: [PATCH 118/188] Cleanup --- .../AccessApprovalRequest.tsx | 229 ++---------------- 1 file changed, 22 insertions(+), 207 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index b6d69d0c97..235737d4a6 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -1,6 +1,6 @@ /* eslint-disable no-nested-ternary */ /* eslint-disable react/jsx-no-useless-fragment */ -import { useCallback, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { faCheck, faCheckCircle, @@ -11,10 +11,7 @@ import { import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { formatDistance } from "date-fns"; import { AnimatePresence, motion } from "framer-motion"; -import ms from "ms"; -import { twMerge } from "tailwind-merge"; -import { createNotification } from "@app/components/notifications"; import { Button, DropdownMenu, @@ -23,11 +20,10 @@ import { DropdownMenuLabel, DropdownMenuTrigger, EmptyState, - Modal, - ModalContent, Tooltip, UpgradePlanModal } from "@app/components/v2"; +import { Badge } from "@app/components/v2/Badge"; import { ProjectPermissionActions, ProjectPermissionSub, @@ -36,7 +32,7 @@ import { useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; -import { useGetWorkspaceUsers, useReviewAccessRequest } from "@app/hooks/api"; +import { useGetWorkspaceUsers } from "@app/hooks/api"; import { accessApprovalKeys, useGetAccessApprovalPolicies, @@ -44,192 +40,11 @@ import { useGetAccessRequestsCount } from "@app/hooks/api/accessApproval/queries"; import { TAccessApprovalRequest } from "@app/hooks/api/accessApproval/types"; -import { ApprovalStatus, TAccessApprovalPolicy, TWorkspaceUser } from "@app/hooks/api/types"; +import { ApprovalStatus, TWorkspaceUser } from "@app/hooks/api/types"; import { queryClient } from "@app/reactQuery"; -import { SpecificPrivilegeSecretForm } from "@app/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection"; -const DisplayBadge = ({ text, className }: { text: string; className?: string }) => { - return ( -
- {text} -
- ); -}; - -const ReviewRequestModal = ({ - isOpen, - onOpenChange, - request, - projectSlug, - selectedRequester, - selectedEnvSlug -}: { - isOpen: boolean; - onOpenChange: (isOpen: boolean) => void; - request: TAccessApprovalRequest & { user: TWorkspaceUser["user"] | null }; - projectSlug: string; - selectedRequester: string | undefined; - selectedEnvSlug: string | undefined; -}) => { - const [isLoading, setIsLoading] = useState<"approved" | "rejected" | null>(null); - - const accessDetails = { - env: request.environmentName, - // secret path will be inside $glob operator - secretPath: request.policy.secretPath, - read: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Read)), - edit: request.permissions?.some(({ action }) => action.includes(ProjectPermissionActions.Edit)), - create: request.permissions?.some(({ action }) => - action.includes(ProjectPermissionActions.Create) - ), - delete: request.permissions?.some(({ action }) => - action.includes(ProjectPermissionActions.Delete) - ), - - temporaryAccess: { - isTemporary: request.isTemporary, - temporaryRange: request.temporaryRange - } - }; - - const requestedAccess = useMemo(() => { - const access: string[] = []; - if (accessDetails.read) access.push("Read"); - if (accessDetails.edit) access.push("Edit"); - if (accessDetails.create) access.push("Create"); - if (accessDetails.delete) access.push("Delete"); - - return access.join(", "); - }, [accessDetails]); - - const getAccessLabel = () => { - if (!accessDetails.temporaryAccess.isTemporary || !accessDetails.temporaryAccess.temporaryRange) - return "Permanent"; - - // convert the range to human readable format - ms(ms(accessDetails.temporaryAccess.temporaryRange), { long: true }); - - return ( - - ); - }; - - const reviewAccessRequest = useReviewAccessRequest(); - - const handleReview = useCallback(async (status: "approved" | "rejected") => { - setIsLoading(status); - try { - await reviewAccessRequest.mutateAsync({ - requestId: request.id, - status, - projectSlug, - envSlug: selectedEnvSlug, - requestedBy: selectedRequester - }); - } catch (error) { - console.error(error); - setIsLoading(null); - return; - } - - createNotification({ - title: `Request ${status}`, - text: `The request has been ${status}`, - type: status === "approved" ? "success" : "info" - }); - - setIsLoading(null); - onOpenChange(false); - }, []); - - return ( - - -
- - - {request.user?.firstName} {request.user?.lastName} ({request.user?.email}) - {" "} - is requesting access to the following resource: - - -
-
- Requested path: - -
- -
- Permissions: - -
- -
- Access Type: - {getAccessLabel()} -
-
- -
- - -
-
-
-
- ); -}; - -const SelectAccessModal = ({ - isOpen, - onOpenChange, - policies -}: { - isOpen: boolean; - onOpenChange: (isOpen: boolean) => void; - policies: TAccessApprovalPolicy[]; -}) => { - return ( - - - onOpenChange(false)} policies={policies} /> - - - ); -}; +import { RequestAccessModal } from "./components/RequestAccessModal"; +import { ReviewAccessRequestModal } from "./components/ReviewAccessModal"; const generateRequestText = (request: TAccessApprovalRequest, membershipId: string) => { const { isTemporary } = request; @@ -249,7 +64,7 @@ const generateRequestText = (request: TAccessApprovalRequest, membershipId: stri
{request.requestedBy === membershipId && ( - + Requested By You )}
@@ -331,29 +146,30 @@ export const AccessApprovalRequest = ({ ({ member }) => member === membership.id )?.status; - let displayData: { label: string; colorClass: string } = { label: "", colorClass: "" }; + let displayData: { label: string; type: "primary" | "danger" | "success" } = { + label: "", + type: "primary" + }; const isExpired = request.privilege && request.isApproved && new Date() > new Date(request.privilege.temporaryAccessEndTime || ("" as string)); - if (isExpired) displayData = { label: "Access Expired", colorClass: "bg-red/20 text-red" }; - else if (isAccepted) - displayData = { label: "Access Granted", colorClass: "bg-green/20 text-green" }; - else if (isRejectedByAnyone) - displayData = { label: "Rejected", colorClass: "bg-red/20 text-red" }; + if (isExpired) displayData = { label: "Access Expired", type: "danger" }; + else if (isAccepted) displayData = { label: "Access Granted", type: "success" }; + else if (isRejectedByAnyone) displayData = { label: "Rejected", type: "danger" }; else if (userReviewStatus === ApprovalStatus.APPROVED) { displayData = { label: `Pending ${request.policy.approvals - request.reviewers.length} review${ request.policy.approvals - request.reviewers.length > 1 ? "s" : "" }`, - colorClass: "bg-yellow/20 text-yellow" + type: "primary" }; } else if (!isReviewedByUser) displayData = { label: "Review Required", - colorClass: "bg-yellow/20 text-yellow" + type: "primary" }; return { @@ -486,7 +302,7 @@ export const AccessApprovalRequest = ({ icon={requestedByFilter === id && } iconPos="right" > - {user.email} + {user.username} ))} @@ -564,10 +380,9 @@ export const AccessApprovalRequest = ({
{details.isApprover && ( - + + {details.displayData.label} + )}
@@ -581,7 +396,7 @@ export const AccessApprovalRequest = ({ {!!policies && ( - { @@ -598,7 +413,7 @@ export const AccessApprovalRequest = ({ )} {!!selectedRequest && ( - Date: Tue, 9 Apr 2024 00:22:47 -0700 Subject: [PATCH 119/188] Fix: Moved from email to username --- .../components/SecretApprovalPolicyRow.tsx | 2 +- .../SecretApprovalPolicyList/components/SecretPolicyForm.tsx | 2 +- .../components/SecretApprovalRequest/SecretApprovalRequest.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx index c256af1c71..e0e4dd5fca 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretApprovalPolicyRow.tsx @@ -98,7 +98,7 @@ export const SecretApprovalPolicyRow = ({ iconPos="right" icon={isChecked && } > - {user.email} + {user.username} ); })} diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx index 185c596c26..db59761c11 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalPolicyList/components/SecretPolicyForm.tsx @@ -222,7 +222,7 @@ export const SecretPolicyForm = ({ iconPos="right" icon={isChecked && } > - {user.email} + {user.username} ); })} diff --git a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx index 32a7ff6de0..0d0c6213ae 100644 --- a/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx @@ -188,7 +188,7 @@ export const SecretApprovalRequest = () => { icon={committerFilter === id && } iconPos="right" > - {user.email} + {user.username} ))} From 0bbdf2a8f48db323358d8773f8ba3759186e32c2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Tue, 9 Apr 2024 00:22:52 -0700 Subject: [PATCH 120/188] Update SecretApprovalPage.tsx --- frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index d66a6c8ccf..3878ecdca1 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -2,8 +2,8 @@ import Link from "next/link"; import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import Divider from "@app/components/basic/Divider"; import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { Divider } from "@app/components/v2/Divider"; import { useWorkspace } from "@app/context"; import { AccessApprovalPolicyList } from "./components/AccessApprovalPolicyList"; From d659b5a6249bf34594d4312ac52ab79b8342e5e2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 12 Apr 2024 03:58:50 +0200 Subject: [PATCH 121/188] Fix: Duplicate access request check --- .../access-approval-request-service.ts | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts index f0714ea346..becdb78daf 100644 --- a/backend/src/ee/services/access-approval-request/access-approval-request-service.ts +++ b/backend/src/ee/services/access-approval-request/access-approval-request-service.ts @@ -37,6 +37,7 @@ type TSecretApprovalRequestServiceFactoryDep = { accessApprovalRequestDAL: Pick< TAccessApprovalRequestDALFactory, | "create" + | "find" | "findRequestsWithPrivilegeByPolicyIds" | "findById" | "transaction" @@ -117,31 +118,33 @@ export const accessApprovalRequestServiceFactory = ({ approvers.map((approver) => approver.approverId) ); - const duplicateRequest = await accessApprovalRequestDAL.findOne({ + const duplicateRequests = await accessApprovalRequestDAL.find({ policyId: policy.id, requestedBy: membership.id, permissions: JSON.stringify(requestedPermissions), isTemporary }); - if (duplicateRequest) { - if (duplicateRequest.privilegeId) { - const privilege = await additionalPrivilegeDAL.findById(duplicateRequest.privilegeId); + if (duplicateRequests?.length > 0) { + for await (const duplicateRequest of duplicateRequests) { + if (duplicateRequest.privilegeId) { + const privilege = await additionalPrivilegeDAL.findById(duplicateRequest.privilegeId); - const isExpired = new Date() > new Date(privilege.temporaryAccessEndTime || ("" as string)); + const isExpired = new Date() > new Date(privilege.temporaryAccessEndTime || ("" as string)); - if (!isExpired || !privilege.isTemporary) { - throw new BadRequestError({ message: "You already have an active privilege with the same criteria" }); - } - } else { - const reviewers = await accessApprovalRequestReviewerDAL.find({ - requestId: duplicateRequest.id - }); + if (!isExpired || !privilege.isTemporary) { + throw new BadRequestError({ message: "You already have an active privilege with the same criteria" }); + } + } else { + const reviewers = await accessApprovalRequestReviewerDAL.find({ + requestId: duplicateRequest.id + }); - const isRejected = reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED); + const isRejected = reviewers.some((reviewer) => reviewer.status === ApprovalStatus.REJECTED); - if (!isRejected) { - throw new BadRequestError({ message: "You already have a pending access request with the same criteria" }); + if (!isRejected) { + throw new BadRequestError({ message: "You already have a pending access request with the same criteria" }); + } } } } From 7ac3bb20df65beddee029ae0e54c0aeb6cfa58e8 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 12 Apr 2024 11:27:20 -0700 Subject: [PATCH 122/188] Update instance recognition of offline license --- backend/src/ee/services/license/license-service.ts | 4 ++-- backend/src/ee/services/license/license-types.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index e81f6dc12e..47b46d0100 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -121,8 +121,8 @@ export const licenseServiceFactory = ({ if (isValidOfflineLicense) { onPremFeatures = contents.license.features; - instanceType = InstanceType.EnterpriseOnPrem; - logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`); + instanceType = InstanceType.EnterpriseOnPremOffline; + logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`); isValidLicense = true; return; } diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index a2379ddaa5..0c8fdc197f 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -3,6 +3,7 @@ import { TOrgPermission } from "@app/lib/types"; export enum InstanceType { OnPrem = "self-hosted", EnterpriseOnPrem = "enterprise-self-hosted", + EnterpriseOnPremOffline = "enterprise-self-hosted-offline", Cloud = "cloud" } From 73d9fcc0de3b632cf0b22190f41e26d3d6bdd927 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 29 Apr 2024 19:11:13 +0200 Subject: [PATCH 123/188] Draft --- .../MemberRoleForm/SpecificPrivilegeSection.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 795ccbb24c..d39a88aa20 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -369,13 +369,13 @@ export const SpecificPrivilegeSecretForm = ({ } return ( - - + + ); }} /> From 2aacd5411636a9040dfd653d0366eed60b228768 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 29 Apr 2024 19:11:36 +0200 Subject: [PATCH 124/188] Update SpecificPrivilegeSection.tsx --- .../MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 d39a88aa20..6fb97063b6 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -134,7 +134,6 @@ export const SpecificPrivilegeSecretForm = ({ }); const temporaryAccessField = privilegeForm.watch("temporaryAccess"); - const selectedEnvironmentSlug = privilegeForm.watch("environmentSlug"); const selectedEnvironment = privilegeForm.watch("environmentSlug"); const secretPath = privilegeForm.watch("secretPath"); @@ -373,7 +372,7 @@ export const SpecificPrivilegeSecretForm = ({ {...field} isDisabled={isMemberEditDisabled} containerClassName="w-48" - environment={selectedEnvironmentSlug} + environment={selectedEnvironment} /> ); From 5894df4370556728099e394780d8595295dbd0e4 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Fri, 29 Mar 2024 16:24:15 +0100 Subject: [PATCH 125/188] Draft --- backend/src/ee/services/license/licence-fns.ts | 2 +- frontend/src/components/basic/Divider.tsx | 15 +++++++++++++++ .../permissions/PermissionDeniedBanner.tsx | 5 +++++ .../SecretApprovalPage/SecretApprovalPage.tsx | 6 ++++++ 4 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/basic/Divider.tsx diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 189a3c4e06..492d09ec3d 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ status: null, trial_end: null, has_used_trial: true, - secretApproval: false, + secretApproval: true, secretRotation: true }); diff --git a/frontend/src/components/basic/Divider.tsx b/frontend/src/components/basic/Divider.tsx new file mode 100644 index 0000000000..f1a570d7b1 --- /dev/null +++ b/frontend/src/components/basic/Divider.tsx @@ -0,0 +1,15 @@ +import { twMerge } from "tailwind-merge"; + +interface IProps { + className?: string; +} + +const Divider = ({ className }: IProps): JSX.Element => { + return ( +
+ + ); +}; + +export default Divider; diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx index b3c7a4f538..f9707d6347 100644 --- a/frontend/src/components/permissions/PermissionDeniedBanner.tsx +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -3,6 +3,8 @@ import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; +import { Button } from "../v2"; + type Props = { containerClassName?: string; className?: string; @@ -32,6 +34,9 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children )}
+
); diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index 3878ecdca1..fa083b9086 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -56,13 +56,19 @@ export const SecretApprovalPage = () => { + + + + + + From 810f670e64fea1f6096320a6166d3c0a99fc3b4b Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 1 Apr 2024 08:22:46 -0700 Subject: [PATCH 126/188] Feat: Request Access --- backend/src/ee/routes/v1/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index fc5c0865d3..16e23eb887 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,6 +1,6 @@ -import { registerAuditLogStreamRouter } from "./audit-log-stream-router"; import { registerAccessApprovalPolicyRouter } from "./access-approval-policy-router"; import { registerAccessApprovalRequestRouter } from "./access-approval-request-router"; +import { registerAuditLogStreamRouter } from "./audit-log-stream-router"; import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerGroupRouter } from "./group-router"; From 3331699f56be77e8bfd85d167c27db1e1e959acf Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Mon, 1 Apr 2024 09:49:22 -0700 Subject: [PATCH 127/188] Feat: Request access --- backend/scripts/generate-schema-types.ts | 2 +- .../20240330075122_access-approval-policy.ts | 3 +- .../access-approval-policy-service.ts | 1 - .../SecretApprovalPage/SecretApprovalPage.tsx | 7 - .../components/AccessPolicyForm.tsx | 250 ++++++++++++++++++ 5 files changed, 253 insertions(+), 10 deletions(-) create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 8c913991fd..28b736152c 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -5,7 +5,7 @@ import knex from "knex"; import { writeFileSync } from "fs"; dotenv.config({ - path: path.join(__dirname, "../../.env.migration") + path: path.join(__dirname, "../../.env") }); const db = knex({ diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts index 61cb9274c2..7aa9da6a0b 100644 --- a/backend/src/db/migrations/20240330075122_access-approval-policy.ts +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -9,8 +9,9 @@ export async function up(knex: Knex): Promise { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.string("name").notNullable(); t.integer("approvals").defaultTo(1).notNullable(); - t.uuid("envId").notNullable(); t.string("secretPath"); + + t.uuid("envId").notNullable(); t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); t.timestamps(true, true, true); }); 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 51a51abb5a..f5cce5db4b 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 @@ -154,7 +154,6 @@ export const accessApprovalPolicyServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { diff --git a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx index fa083b9086..9c273848f5 100644 --- a/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx +++ b/frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx @@ -53,22 +53,15 @@ export const SecretApprovalPage = () => { Access Requests Access Request Policies - - - - - - - diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx new file mode 100644 index 0000000000..56a4bdef5f --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -0,0 +1,250 @@ +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[]; + workspaceId: string; + editValues?: TAccessApprovalPolicy; +}; + +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 AccessPolicyForm = ({ + isOpen, + onToggle, + members = [], + workspaceId, + 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) => { + try { + await createAccessApprovalPolicy({ + ...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 updateAccessApprovalPolicy({ + id: editValues?.id, + ...data, + 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(({ 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.email} + + ); + })} + + + + )} + /> + ( + + field.onChange(parseInt(el.target.value, 10))} + /> + + )} + /> +
+ + +
+ +
+
+ ); +}; From 448f89fd1c4d536dfc4a15c53bebb469393ee067 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:35:51 -0700 Subject: [PATCH 128/188] Feat: Request Access (migrations) --- .../src/db/migrations/20240330075122_access-approval-policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/db/migrations/20240330075122_access-approval-policy.ts b/backend/src/db/migrations/20240330075122_access-approval-policy.ts index 7aa9da6a0b..e6d47d8648 100644 --- a/backend/src/db/migrations/20240330075122_access-approval-policy.ts +++ b/backend/src/db/migrations/20240330075122_access-approval-policy.ts @@ -12,6 +12,7 @@ export async function up(knex: Knex): Promise { t.string("secretPath"); t.uuid("envId").notNullable(); + t.string("secretPath"); t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE"); t.timestamps(true, true, true); }); @@ -34,7 +35,6 @@ export async function up(knex: Knex): Promise { export async function down(knex: Knex): Promise { await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicyApprover); await knex.schema.dropTableIfExists(TableName.AccessApprovalPolicy); - await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicyApprover); await dropOnUpdateTrigger(knex, TableName.AccessApprovalPolicy); } From 5c69bbf515f8c8debba601f6b6c8bb33328d1ea4 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:37:49 -0700 Subject: [PATCH 129/188] Feat: Request access (new routes) --- .../v1/access-approval-request-router.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 4b173cfa76..696c8b76fa 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -1,7 +1,9 @@ +import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas"; import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -11,12 +13,24 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv method: "POST", schema: { body: z.object({ + slug: z + .string() + .min(1) + .max(60) + .trim() + .default(`requested-privilege-${slugify(alphaNumericNanoId(12))}`) + .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }), permissions: z.any().array(), isTemporary: z.boolean(), temporaryRange: z.string().optional() }), querystring: z.object({ - projectSlug: z.string().trim() + projectSlug: z.string().trim(), + secretPath: z.string().trim(), + envSlug: z.string().trim() }), response: { 200: z.object({ @@ -31,7 +45,9 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, permissions: req.body.permissions, + envSlug: req.query.envSlug, actorOrgId: req.permission.orgId, + secretPath: req.query.secretPath, projectSlug: req.query.projectSlug, temporaryRange: req.body.temporaryRange, isTemporary: req.body.isTemporary From cd6be68461efddd5ce084f11e55de9d57f6bcba4 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:39:15 -0700 Subject: [PATCH 130/188] Fix: Validate approvers access --- .../access-approval-policy/access-approval-policy-service.ts | 2 ++ 1 file changed, 2 insertions(+) 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 f5cce5db4b..f143808692 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 @@ -54,6 +54,7 @@ export const accessApprovalPolicyServiceFactory = ({ if (approvals > approvers.length) throw new BadRequestError({ message: "Approvals cannot be greater than approvers" }); + if (!secretPath) throw new BadRequestError({ message: "Secret path is required" }); const { permission } = await permissionService.getProjectPermission( actor, @@ -154,6 +155,7 @@ export const accessApprovalPolicyServiceFactory = ({ actorAuthMethod, actorOrgId ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval); const updatedPolicy = await accessApprovalPolicyDAL.transaction(async (tx) => { From 812cced9d53e154a3a3cab9827386e2dd1dc6748 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:40:05 -0700 Subject: [PATCH 131/188] Feat: Request access --- .../access-approval-request-secret-dal.ts | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts diff --git a/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts b/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts new file mode 100644 index 0000000000..d458d58ffe --- /dev/null +++ b/backend/src/ee/services/access-approval-request/access-approval-request-secret-dal.ts @@ -0,0 +1,230 @@ +import { Knex } from "knex"; + +import { TDbClient } from "@app/db"; +import { + SecretApprovalRequestsSecretsSchema, + TableName, + TSecretApprovalRequestsSecrets, + TSecretTags +} from "@app/db/schemas"; +import { BadRequestError, DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; + +export type TAccessApprovalRequestSecretDALFactory = ReturnType; + +export const accessApprovalRequestSecretDALFactory = (db: TDbClient) => { + const accessApprovalRequestSecretOrm = ormify(db, TableName.SecretApprovalRequestSecret); + const secretApprovalRequestSecretTagOrm = ormify(db, TableName.SecretApprovalRequestSecretTag); + + const bulkUpdateNoVersionIncrement = async (data: TSecretApprovalRequestsSecrets[], tx?: Knex) => { + try { + const existingApprovalSecrets = await accessApprovalRequestSecretOrm.find( + { + $in: { + id: data.map((el) => el.id) + } + }, + { tx } + ); + + if (existingApprovalSecrets.length !== data.length) { + throw new BadRequestError({ message: "Some of the secret approvals do not exist" }); + } + + if (data.length === 0) return []; + + const updatedApprovalSecrets = await (tx || db)(TableName.SecretApprovalRequestSecret) + .insert(data) + .onConflict("id") // this will cause a conflict then merge the data + .merge() // Merge the data with the existing data + .returning("*"); + + return updatedApprovalSecrets; + } catch (error) { + throw new DatabaseError({ error, name: "bulk update secret" }); + } + }; + + const findByRequestId = async (requestId: string, tx?: Knex) => { + try { + const doc = await (tx || db)({ + secVerTag: TableName.SecretTag + }) + .from(TableName.SecretApprovalRequestSecret) + .where({ requestId }) + .leftJoin( + TableName.SecretApprovalRequestSecretTag, + `${TableName.SecretApprovalRequestSecret}.id`, + `${TableName.SecretApprovalRequestSecretTag}.secretId` + ) + .leftJoin(TableName.SecretTag, `${TableName.SecretApprovalRequestSecretTag}.tagId`, `${TableName.SecretTag}.id`) + .leftJoin(TableName.Secret, `${TableName.SecretApprovalRequestSecret}.secretId`, `${TableName.Secret}.id`) + .leftJoin( + TableName.SecretVersion, + `${TableName.SecretVersion}.id`, + `${TableName.SecretApprovalRequestSecret}.secretVersion` + ) + .leftJoin( + TableName.SecretVersionTag, + `${TableName.SecretVersionTag}.${TableName.SecretVersion}Id`, + `${TableName.SecretVersion}.id` + ) + .leftJoin( + db.ref(TableName.SecretTag).as("secVerTag"), + `${TableName.SecretVersionTag}.${TableName.SecretTag}Id`, + db.ref("id").withSchema("secVerTag") + ) + .select(selectAllTableCols(TableName.SecretApprovalRequestSecret)) + .select({ + secVerTagId: "secVerTag.id", + secVerTagColor: "secVerTag.color", + secVerTagSlug: "secVerTag.slug", + secVerTagName: "secVerTag.name" + }) + .select( + db.ref("id").withSchema(TableName.SecretTag).as("tagId"), + db.ref("id").withSchema(TableName.SecretApprovalRequestSecretTag).as("tagJnId"), + db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), + db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), + db.ref("name").withSchema(TableName.SecretTag).as("tagName") + ) + .select( + db.ref("secretBlindIndex").withSchema(TableName.Secret).as("orgSecBlindIndex"), + db.ref("version").withSchema(TableName.Secret).as("orgSecVersion"), + db.ref("secretKeyIV").withSchema(TableName.Secret).as("orgSecKeyIV"), + db.ref("secretKeyTag").withSchema(TableName.Secret).as("orgSecKeyTag"), + db.ref("secretKeyCiphertext").withSchema(TableName.Secret).as("orgSecKeyCiphertext"), + db.ref("secretValueIV").withSchema(TableName.Secret).as("orgSecValueIV"), + db.ref("secretValueTag").withSchema(TableName.Secret).as("orgSecValueTag"), + db.ref("secretValueCiphertext").withSchema(TableName.Secret).as("orgSecValueCiphertext"), + db.ref("secretCommentIV").withSchema(TableName.Secret).as("orgSecCommentIV"), + db.ref("secretCommentTag").withSchema(TableName.Secret).as("orgSecCommentTag"), + db.ref("secretCommentCiphertext").withSchema(TableName.Secret).as("orgSecCommentCiphertext") + ) + .select( + db.ref("version").withSchema(TableName.SecretVersion).as("secVerVersion"), + db.ref("secretKeyIV").withSchema(TableName.SecretVersion).as("secVerKeyIV"), + db.ref("secretKeyTag").withSchema(TableName.SecretVersion).as("secVerKeyTag"), + db.ref("secretKeyCiphertext").withSchema(TableName.SecretVersion).as("secVerKeyCiphertext"), + db.ref("secretValueIV").withSchema(TableName.SecretVersion).as("secVerValueIV"), + db.ref("secretValueTag").withSchema(TableName.SecretVersion).as("secVerValueTag"), + db.ref("secretValueCiphertext").withSchema(TableName.SecretVersion).as("secVerValueCiphertext"), + db.ref("secretCommentIV").withSchema(TableName.SecretVersion).as("secVerCommentIV"), + db.ref("secretCommentTag").withSchema(TableName.SecretVersion).as("secVerCommentTag"), + db.ref("secretCommentCiphertext").withSchema(TableName.SecretVersion).as("secVerCommentCiphertext") + ); + const formatedDoc = sqlNestRelationships({ + data: doc, + key: "id", + parentMapper: (data) => SecretApprovalRequestsSecretsSchema.omit({ secretVersion: true }).parse(data), + childrenMapper: [ + { + key: "tagJnId", + label: "tags" as const, + mapper: ({ tagId: id, tagName: name, tagSlug: slug, tagColor: color }) => ({ + id, + name, + slug, + color + }) + }, + { + key: "secretId", + label: "secret" as const, + mapper: ({ + orgSecKeyIV, + orgSecKeyTag, + orgSecValueIV, + orgSecVersion, + orgSecValueTag, + orgSecCommentIV, + orgSecBlindIndex, + orgSecCommentTag, + orgSecKeyCiphertext, + orgSecValueCiphertext, + orgSecCommentCiphertext, + secretId + }) => + secretId + ? { + id: secretId, + version: orgSecVersion, + secretBlindIndex: orgSecBlindIndex, + secretKeyIV: orgSecKeyIV, + secretKeyTag: orgSecKeyTag, + secretKeyCiphertext: orgSecKeyCiphertext, + secretValueIV: orgSecValueIV, + secretValueTag: orgSecValueTag, + secretValueCiphertext: orgSecValueCiphertext, + secretCommentIV: orgSecCommentIV, + secretCommentTag: orgSecCommentTag, + secretCommentCiphertext: orgSecCommentCiphertext + } + : undefined + }, + { + key: "secretVersion", + label: "secretVersion" as const, + mapper: ({ + secVerCommentIV, + secVerCommentCiphertext, + secVerCommentTag, + secVerValueCiphertext, + secVerKeyIV, + secVerKeyTag, + secVerValueIV, + secretVersion, + secVerValueTag, + secVerKeyCiphertext, + secVerVersion + }) => + secretVersion + ? { + version: secVerVersion, + id: secretVersion, + secretKeyIV: secVerKeyIV, + secretKeyTag: secVerKeyTag, + secretKeyCiphertext: secVerKeyCiphertext, + secretValueIV: secVerValueIV, + secretValueTag: secVerValueTag, + secretValueCiphertext: secVerValueCiphertext, + secretCommentIV: secVerCommentIV, + secretCommentTag: secVerCommentTag, + secretCommentCiphertext: secVerCommentCiphertext + } + : undefined, + childrenMapper: [ + { + key: "secVerTagId", + label: "tags" as const, + mapper: ({ secVerTagId: id, secVerTagName: name, secVerTagSlug: slug, secVerTagColor: color }) => ({ + // eslint-disable-next-line + id, + // eslint-disable-next-line + name, + // eslint-disable-next-line + slug, + // eslint-disable-next-line + color + }) + } + ] + } + ] + }); + return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({ + ...el, + secret: secret?.[0], + secretVersion: secretVersion?.[0] + })); + } catch (error) { + throw new DatabaseError({ error, name: "FindByRequestId" }); + } + }; + return { + ...accessApprovalRequestSecretOrm, + findByRequestId, + bulkUpdateNoVersionIncrement, + insertApprovalSecretTags: secretApprovalRequestSecretTagOrm.insertMany + }; +}; From 1ccd74e1a5104064f9ef6195208cec0de55665a2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:43:48 -0700 Subject: [PATCH 132/188] Fix: Remove redundant code --- .../src/components/permissions/PermissionDeniedBanner.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/components/permissions/PermissionDeniedBanner.tsx b/frontend/src/components/permissions/PermissionDeniedBanner.tsx index f9707d6347..b3c7a4f538 100644 --- a/frontend/src/components/permissions/PermissionDeniedBanner.tsx +++ b/frontend/src/components/permissions/PermissionDeniedBanner.tsx @@ -3,8 +3,6 @@ import { faLock } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; -import { Button } from "../v2"; - type Props = { containerClassName?: string; className?: string; @@ -34,9 +32,6 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children )}
-
); From 4eabbb3ac53ee612b693fd198ee6926b4404d160 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 133/188] Fix: Added support for request access --- .../SpecificPrivilegeSection.tsx | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) 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 6fb97063b6..dd09226eb0 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -311,6 +311,91 @@ export const SpecificPrivilegeSecretForm = ({ )}`; } 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 ( From 2c2afbea7a518c3bbb504df169216a0105cc5b99 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:50:16 -0700 Subject: [PATCH 134/188] Fix: Move to project slug --- .../components/AccessPolicyForm.tsx | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx index 56a4bdef5f..6b53e2f1e3 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalPolicyList/components/AccessPolicyForm.tsx @@ -32,7 +32,7 @@ type Props = { isOpen?: boolean; onToggle: (isOpen: boolean) => void; members?: TWorkspaceUser[]; - workspaceId: string; + projectSlug: string; editValues?: TAccessApprovalPolicy; }; @@ -40,7 +40,7 @@ const formSchema = z .object({ environment: z.string(), name: z.string().optional(), - secretPath: z.string().optional().nullable(), + secretPath: z.string().optional(), approvals: z.number().min(1), approvers: z.string().array().min(1) }) @@ -55,7 +55,7 @@ export const AccessPolicyForm = ({ isOpen, onToggle, members = [], - workspaceId, + projectSlug, editValues }: Props) => { const { @@ -80,10 +80,12 @@ export const AccessPolicyForm = ({ const { mutateAsync: updateAccessApprovalPolicy } = useUpdateAccessApprovalPolicy(); const handleCreatePolicy = async (data: TFormSchema) => { + if (!projectSlug) return; + try { await createAccessApprovalPolicy({ ...data, - workspaceId + projectSlug }); createNotification({ type: "success", @@ -100,12 +102,14 @@ export const AccessPolicyForm = ({ }; const handleUpdatePolicy = async (data: TFormSchema) => { + if (!projectSlug) return; if (!editValues?.id) return; + try { await updateAccessApprovalPolicy({ id: editValues?.id, ...data, - workspaceId + projectSlug }); createNotification({ type: "success", @@ -154,6 +158,7 @@ export const AccessPolicyForm = ({ errorText={error?.message} > + + )} + /> + Date: Wed, 3 Apr 2024 16:50:47 -0700 Subject: [PATCH 135/188] Feat: Request access --- .../components/AccessApprovalPolicyRow.tsx | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx new file mode 100644 index 0000000000..8554e928ee --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessApprovalPolicyRow.tsx @@ -0,0 +1,146 @@ +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.email} + + ); + })} + + + + {policy.approvals} + +
+ + {(isAllowed) => ( + + + + )} + + + {(isAllowed) => ( + + + + )} + +
+ + + ); +}; From 1afd120e8ee2e01c054e59553be6f04e1832eae4 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:50:56 -0700 Subject: [PATCH 136/188] Feat: Request access --- .../components/AccessPolicyForm.tsx | 261 ++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx new file mode 100644 index 0000000000..9cf756c14d --- /dev/null +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/components/AccessPolicyForm.tsx @@ -0,0 +1,261 @@ +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) => { + 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 (!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.email} + + ); + })} + + + + )} + /> + ( + + field.onChange(parseInt(el.target.value, 10))} + /> + + )} + /> +
+ + +
+ +
+
+ ); +}; From 3bb50b235d223eccc5391b558eb3e3f092ad338a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:53:02 -0700 Subject: [PATCH 137/188] Update generate-schema-types.ts --- backend/scripts/generate-schema-types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/scripts/generate-schema-types.ts b/backend/scripts/generate-schema-types.ts index 28b736152c..8c913991fd 100644 --- a/backend/scripts/generate-schema-types.ts +++ b/backend/scripts/generate-schema-types.ts @@ -5,7 +5,7 @@ import knex from "knex"; import { writeFileSync } from "fs"; dotenv.config({ - path: path.join(__dirname, "../../.env") + path: path.join(__dirname, "../../.env.migration") }); const db = knex({ From 3f0f45e85388fc796ac58ff4572c279d65c6b9b2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:13:23 -0700 Subject: [PATCH 138/188] Update SpecificPrivilegeSection.tsx --- .../MemberRoleForm/SpecificPrivilegeSection.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 dd09226eb0..bb330aa4d1 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -388,14 +388,13 @@ export const SpecificPrivilegeSecretForm = ({ if (isExpired) return "Access expired"; if (!temporaryAccessField?.isTemporary) return "Permanent"; - if (exactTime) + if (exactTime) { return `Until ${format( new Date(temporaryAccessField.temporaryAccessEndTime || ""), "yyyy-MM-dd HH:mm:ss" )}`; + } return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); - }; - }; return ( From a3b4b650d1b1949bef7d39fae7d150766baa7371 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:41:17 -0700 Subject: [PATCH 139/188] Removed unused parameter --- .../ee/routes/v1/access-approval-request-router.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/backend/src/ee/routes/v1/access-approval-request-router.ts b/backend/src/ee/routes/v1/access-approval-request-router.ts index 696c8b76fa..2a6142c88c 100644 --- a/backend/src/ee/routes/v1/access-approval-request-router.ts +++ b/backend/src/ee/routes/v1/access-approval-request-router.ts @@ -1,9 +1,7 @@ -import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { AccessApprovalRequestsReviewersSchema, AccessApprovalRequestsSchema } from "@app/db/schemas"; import { ApprovalStatus } from "@app/ee/services/access-approval-request/access-approval-request-types"; -import { alphaNumericNanoId } from "@app/lib/nanoid"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -13,16 +11,6 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv method: "POST", schema: { body: z.object({ - slug: z - .string() - .min(1) - .max(60) - .trim() - .default(`requested-privilege-${slugify(alphaNumericNanoId(12))}`) - .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") - .refine((v) => slugify(v) === v, { - message: "Slug must be a valid slug" - }), permissions: z.any().array(), isTemporary: z.boolean(), temporaryRange: z.string().optional() From 27447ddc88d2f7f5b9b896f2b5b7b01044a4879a Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:43:45 -0700 Subject: [PATCH 140/188] Update licence-fns.ts --- backend/src/ee/services/license/licence-fns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 492d09ec3d..189a3c4e06 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -33,7 +33,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ status: null, trial_end: null, has_used_trial: true, - secretApproval: true, + secretApproval: false, secretRotation: true }); From 923bf0204623d8d193f414a2b4ab5096451a4201 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Wed, 3 Apr 2024 20:24:33 -0700 Subject: [PATCH 141/188] style changes --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 235737d4a6..6479117b44 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -317,7 +317,7 @@ export const AccessApprovalRequest = ({
)} {!!filteredRequests?.length && - filteredRequests?.map((request) => { + requests?.map((request) => { const details = generateRequestDetails(request); return ( From 121902e51f761330dd63b76fab88f7dc5719d964 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:29:20 -0700 Subject: [PATCH 142/188] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 6479117b44..235737d4a6 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -317,7 +317,7 @@ export const AccessApprovalRequest = ({
)} {!!filteredRequests?.length && - requests?.map((request) => { + filteredRequests?.map((request) => { const details = generateRequestDetails(request); return ( From 350dd97b98e9ee906608962a2f9ff72808f966a3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:33:58 -0700 Subject: [PATCH 143/188] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 235737d4a6..4896c9a3e0 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -311,7 +311,7 @@ export const AccessApprovalRequest = ({
- {filteredRequests?.length === 0 && ( + {filteredRequests?.length === 0 && ( //
From 5117f5d3c10ce86dc6ddfc6f75092c4b6fa8328e Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Wed, 3 Apr 2024 20:34:11 -0700 Subject: [PATCH 144/188] Update AccessApprovalRequest.tsx --- .../components/AccessApprovalRequest/AccessApprovalRequest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx index 4896c9a3e0..235737d4a6 100644 --- a/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx +++ b/frontend/src/views/SecretApprovalPage/components/AccessApprovalRequest/AccessApprovalRequest.tsx @@ -311,7 +311,7 @@ export const AccessApprovalRequest = ({