feat: add option to maks and protect gitlab secrets

This commit is contained in:
Sheen Capadngan
2024-06-07 21:46:34 +08:00
parent c1ca2a6f8c
commit 2bd9ad0137
7 changed files with 65 additions and 11 deletions

View File

@@ -674,7 +674,9 @@ export const INTEGRATION = {
secretGCPLabel: "The label for GCP secrets.", secretGCPLabel: "The label for GCP secrets.",
secretAWSTag: "The tags for AWS secrets.", secretAWSTag: "The tags for AWS secrets.",
kmsKeyId: "The ID of the encryption key from AWS KMS.", 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: { UPDATE: {

View File

@@ -73,7 +73,9 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
.optional() .optional()
.describe(INTEGRATION.CREATE.metadata.secretAWSTag), .describe(INTEGRATION.CREATE.metadata.secretAWSTag),
kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId), 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({}) .default({})
}), }),

View File

@@ -1917,13 +1917,13 @@ const syncSecretsGitLab = async ({
return allEnvVariables; return allEnvVariables;
}; };
const metadata = z.record(z.any()).parse(integration.metadata);
const allEnvVariables = await getAllEnvVariables(integration?.appId as string, accessToken); const allEnvVariables = await getAllEnvVariables(integration?.appId as string, accessToken);
const getSecretsRes: GitLabSecret[] = allEnvVariables const getSecretsRes: GitLabSecret[] = allEnvVariables
.filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment) .filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment)
.filter((gitLabSecret) => { .filter((gitLabSecret) => {
let isValid = true; let isValid = true;
const metadata = z.record(z.any()).parse(integration.metadata);
if (metadata.secretPrefix && !gitLabSecret.key.startsWith(metadata.secretPrefix)) { if (metadata.secretPrefix && !gitLabSecret.key.startsWith(metadata.secretPrefix)) {
isValid = false; isValid = false;
} }
@@ -1943,8 +1943,8 @@ const syncSecretsGitLab = async ({
{ {
key, key,
value: secrets[key].value, value: secrets[key].value,
protected: false, protected: Boolean(metadata.shouldProtectSecrets),
masked: false, masked: Boolean(metadata.shouldMaskSecrets),
raw: false, raw: false,
environment_scope: integration.targetEnvironment environment_scope: integration.targetEnvironment
}, },
@@ -1961,7 +1961,9 @@ const syncSecretsGitLab = async ({
`${gitLabApiUrl}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`, `${gitLabApiUrl}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`,
{ {
...existingSecret, ...existingSecret,
value: secrets[existingSecret.key].value value: secrets[existingSecret.key].value,
protected: Boolean(metadata.shouldProtectSecrets),
masked: Boolean(metadata.shouldMaskSecrets)
}, },
{ {
headers: { headers: {

View File

@@ -29,6 +29,8 @@ export type TCreateIntegrationDTO = {
}[]; }[];
kmsKeyId?: string; kmsKeyId?: string;
shouldDisableDelete?: boolean; shouldDisableDelete?: boolean;
shouldMaskSecrets?: boolean;
shouldProtectSecrets?: boolean;
}; };
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;

View File

@@ -1,4 +1,6 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { AxiosError } from "axios";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
@@ -567,11 +569,14 @@ export const secretQueueFactory = ({
isSynced: true isSynced: true
}); });
} catch (err: unknown) { } 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, { await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id, lastSyncJobId: job.id,
lastUsed: new Date(), lastUsed: new Date(),
syncMessage: (err as Error)?.message, syncMessage: message,
isSynced: false isSynced: false
}); });
} }

View File

@@ -73,6 +73,8 @@ export const useCreateIntegration = () => {
}[]; }[];
kmsKeyId?: string; kmsKeyId?: string;
shouldDisableDelete?: boolean; shouldDisableDelete?: boolean;
shouldMaskSecrets?: boolean;
shouldProtectSecrets?: boolean;
}; };
}) => { }) => {
const { const {

View File

@@ -25,6 +25,7 @@ import {
ModalContent, ModalContent,
Select, Select,
SelectItem, SelectItem,
Switch,
Tab, Tab,
TabList, TabList,
TabPanel, TabPanel,
@@ -58,7 +59,9 @@ const schema = yup.object({
targetAppId: yup.string().required("GitLab project is required"), targetAppId: yup.string().required("GitLab project is required"),
targetEnvironment: yup.string(), targetEnvironment: yup.string(),
secretPrefix: yup.string(), secretPrefix: yup.string(),
secretSuffix: yup.string() secretSuffix: yup.string(),
shouldMaskSecrets: yup.boolean(),
shouldProtectSecrets: yup.boolean()
}); });
type FormData = yup.InferType<typeof schema>; type FormData = yup.InferType<typeof schema>;
@@ -138,7 +141,9 @@ export default function GitLabCreateIntegrationPage() {
targetAppId, targetAppId,
targetEnvironment, targetEnvironment,
secretPrefix, secretPrefix,
secretSuffix secretSuffix,
shouldMaskSecrets,
shouldProtectSecrets
}: FormData) => { }: FormData) => {
try { try {
setIsLoading(true); setIsLoading(true);
@@ -156,7 +161,9 @@ export default function GitLabCreateIntegrationPage() {
secretPath, secretPath,
metadata: { metadata: {
secretPrefix, secretPrefix,
secretSuffix secretSuffix,
shouldMaskSecrets,
shouldProtectSecrets
} }
}); });
@@ -390,6 +397,38 @@ export default function GitLabCreateIntegrationPage() {
exit={{ opacity: 0, translateX: 30 }} exit={{ opacity: 0, translateX: 30 }}
className="pb-[14.25rem]" className="pb-[14.25rem]"
> >
<div className="ml-1">
<Controller
control={control}
name="shouldMaskSecrets"
render={({ field: { onChange, value } }) => (
<Switch
id="should-mask-secrets"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
>
<div className="max-w-md">
Mask variables (requires secret values to match regex)
</div>
</Switch>
)}
/>
</div>
<div className="ml-1 mt-4 mb-5">
<Controller
control={control}
name="shouldProtectSecrets"
render={({ field: { onChange, value } }) => (
<Switch
id="should-protect-secrets"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
>
Protect secrets (only use in protected branches and tags)
</Switch>
)}
/>
</div>
<Controller <Controller
control={control} control={control}
name="secretPrefix" name="secretPrefix"