From 2bd9ad013753a4eb3859920e7bb7fa1f0cc00639 Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Fri, 7 Jun 2024 21:46:34 +0800 Subject: [PATCH] feat: add option to maks and protect gitlab secrets --- backend/src/lib/api-docs/constants.ts | 4 +- .../server/routes/v1/integration-router.ts | 4 +- .../integration-sync-secret.ts | 10 +++-- .../services/integration/integration-types.ts | 2 + backend/src/services/secret/secret-queue.ts | 9 +++- .../src/hooks/api/integrations/queries.tsx | 2 + .../src/pages/integrations/gitlab/create.tsx | 45 +++++++++++++++++-- 7 files changed, 65 insertions(+), 11 deletions(-) diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index da82016f1..deeaf5cd4 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -674,7 +674,9 @@ export const INTEGRATION = { secretGCPLabel: "The label for GCP secrets.", secretAWSTag: "The tags for AWS secrets.", kmsKeyId: "The ID of the encryption key from AWS KMS.", - shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store." + shouldDisableDelete: "The flag to disable deletion of secrets in AWS Parameter Store.", + shouldMaskSecrets: "The flag to determine the visibility of secrets to sync. Used by Gitlab", + shouldProtectSecrets: "The flag to determine usage rules of secrets to sync. Used by Gitlab" } }, UPDATE: { diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index bdb58aa8b..1c6bad7c7 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -73,7 +73,9 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { .optional() .describe(INTEGRATION.CREATE.metadata.secretAWSTag), kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId), - shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete) + shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete), + shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets), + shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets) }) .default({}) }), diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 9d8ee8895..15c80068d 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -1917,13 +1917,13 @@ const syncSecretsGitLab = async ({ return allEnvVariables; }; + const metadata = z.record(z.any()).parse(integration.metadata); const allEnvVariables = await getAllEnvVariables(integration?.appId as string, accessToken); const getSecretsRes: GitLabSecret[] = allEnvVariables .filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment) .filter((gitLabSecret) => { let isValid = true; - const metadata = z.record(z.any()).parse(integration.metadata); if (metadata.secretPrefix && !gitLabSecret.key.startsWith(metadata.secretPrefix)) { isValid = false; } @@ -1943,8 +1943,8 @@ const syncSecretsGitLab = async ({ { key, value: secrets[key].value, - protected: false, - masked: false, + protected: Boolean(metadata.shouldProtectSecrets), + masked: Boolean(metadata.shouldMaskSecrets), raw: false, environment_scope: integration.targetEnvironment }, @@ -1961,7 +1961,9 @@ const syncSecretsGitLab = async ({ `${gitLabApiUrl}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`, { ...existingSecret, - value: secrets[existingSecret.key].value + value: secrets[existingSecret.key].value, + protected: Boolean(metadata.shouldProtectSecrets), + masked: Boolean(metadata.shouldMaskSecrets) }, { headers: { diff --git a/backend/src/services/integration/integration-types.ts b/backend/src/services/integration/integration-types.ts index 9c75cad2d..b96bb6ef3 100644 --- a/backend/src/services/integration/integration-types.ts +++ b/backend/src/services/integration/integration-types.ts @@ -29,6 +29,8 @@ export type TCreateIntegrationDTO = { }[]; kmsKeyId?: string; shouldDisableDelete?: boolean; + shouldMaskSecrets?: boolean; + shouldProtectSecrets?: boolean; }; } & Omit; diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index d40a18e5e..07db646f8 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -1,4 +1,6 @@ /* eslint-disable no-await-in-loop */ +import { AxiosError } from "axios"; + import { getConfig } from "@app/lib/config/env"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; @@ -567,11 +569,14 @@ export const secretQueueFactory = ({ isSynced: true }); } catch (err: unknown) { - logger.info("Secret integration sync error:", err); + logger.info("Secret integration sync error: %o", err); + const message = + err instanceof AxiosError ? JSON.stringify((err as AxiosError)?.response?.data) : (err as Error)?.message; + await integrationDAL.updateById(integration.id, { lastSyncJobId: job.id, lastUsed: new Date(), - syncMessage: (err as Error)?.message, + syncMessage: message, isSynced: false }); } diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index 3aa8f3ed1..2036f6718 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -73,6 +73,8 @@ export const useCreateIntegration = () => { }[]; kmsKeyId?: string; shouldDisableDelete?: boolean; + shouldMaskSecrets?: boolean; + shouldProtectSecrets?: boolean; }; }) => { const { diff --git a/frontend/src/pages/integrations/gitlab/create.tsx b/frontend/src/pages/integrations/gitlab/create.tsx index 529fd037e..8f8f27469 100644 --- a/frontend/src/pages/integrations/gitlab/create.tsx +++ b/frontend/src/pages/integrations/gitlab/create.tsx @@ -25,6 +25,7 @@ import { ModalContent, Select, SelectItem, + Switch, Tab, TabList, TabPanel, @@ -58,7 +59,9 @@ const schema = yup.object({ targetAppId: yup.string().required("GitLab project is required"), targetEnvironment: yup.string(), secretPrefix: yup.string(), - secretSuffix: yup.string() + secretSuffix: yup.string(), + shouldMaskSecrets: yup.boolean(), + shouldProtectSecrets: yup.boolean() }); type FormData = yup.InferType; @@ -138,7 +141,9 @@ export default function GitLabCreateIntegrationPage() { targetAppId, targetEnvironment, secretPrefix, - secretSuffix + secretSuffix, + shouldMaskSecrets, + shouldProtectSecrets }: FormData) => { try { setIsLoading(true); @@ -156,7 +161,9 @@ export default function GitLabCreateIntegrationPage() { secretPath, metadata: { secretPrefix, - secretSuffix + secretSuffix, + shouldMaskSecrets, + shouldProtectSecrets } }); @@ -390,6 +397,38 @@ export default function GitLabCreateIntegrationPage() { exit={{ opacity: 0, translateX: 30 }} className="pb-[14.25rem]" > +
+ ( + onChange(isChecked)} + isChecked={value} + > +
+ Mask variables (requires secret values to match regex) +
+
+ )} + /> +
+
+ ( + onChange(isChecked)} + isChecked={value} + > + Protect secrets (only use in protected branches and tags) + + )} + /> +