diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index da82016f1..18db346dc 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -674,7 +674,8 @@ 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.", + shouldEnableDelete: "The flag to enable deletion of secrets" } }, UPDATE: { diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index bdb58aa8b..97a7f4d7a 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -8,7 +8,7 @@ import { writeLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { IntegrationMappingBehavior } from "@app/services/integration-auth/integration-list"; +import { IntegrationMetadataSchema } from "@app/services/integration/integration-schema"; import { PostHogEventTypes, TIntegrationCreatedEvent } from "@app/services/telemetry/telemetry-types"; export const registerIntegrationRouter = async (server: FastifyZodProvider) => { @@ -46,36 +46,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { path: z.string().trim().optional().describe(INTEGRATION.CREATE.path), region: z.string().trim().optional().describe(INTEGRATION.CREATE.region), scope: z.string().trim().optional().describe(INTEGRATION.CREATE.scope), - metadata: z - .object({ - secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix), - secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix), - initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir), - mappingBehavior: z - .nativeEnum(IntegrationMappingBehavior) - .optional() - .describe(INTEGRATION.CREATE.metadata.mappingBehavior), - shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy), - secretGCPLabel: z - .object({ - labelName: z.string(), - labelValue: z.string() - }) - .optional() - .describe(INTEGRATION.CREATE.metadata.secretGCPLabel), - secretAWSTag: z - .array( - z.object({ - key: z.string(), - value: z.string() - }) - ) - .optional() - .describe(INTEGRATION.CREATE.metadata.secretAWSTag), - kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId), - shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete) - }) - .default({}) + metadata: IntegrationMetadataSchema.default({}) }), response: { 200: z.object({ @@ -161,33 +132,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment), owner: z.string().trim().describe(INTEGRATION.UPDATE.owner), environment: z.string().trim().describe(INTEGRATION.UPDATE.environment), - metadata: z - .object({ - secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix), - secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix), - initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir), - mappingBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.mappingBehavior), - shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy), - secretGCPLabel: z - .object({ - labelName: z.string(), - labelValue: z.string() - }) - .optional() - .describe(INTEGRATION.CREATE.metadata.secretGCPLabel), - secretAWSTag: z - .array( - z.object({ - key: z.string(), - value: z.string() - }) - ) - .optional() - .describe(INTEGRATION.CREATE.metadata.secretAWSTag), - kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId), - shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete) - }) - .optional() + metadata: IntegrationMetadataSchema.optional() }), response: { 200: z.object({ diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 0ae0a0275..36228f03f 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -31,6 +31,7 @@ import { logger } from "@app/lib/logger"; import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types"; import { TIntegrationDALFactory } from "../integration/integration-dal"; +import { IntegrationMetadataSchema } from "../integration/integration-schema"; import { IntegrationInitialSyncBehavior, IntegrationMappingBehavior, @@ -1363,38 +1364,41 @@ const syncSecretsGitHub = async ({ } } - for await (const encryptedSecret of encryptedSecrets) { - if ( - !(encryptedSecret.name in secrets) && - !(appendices?.prefix !== undefined && !encryptedSecret.name.startsWith(appendices?.prefix)) && - !(appendices?.suffix !== undefined && !encryptedSecret.name.endsWith(appendices?.suffix)) - ) { - switch (integration.scope) { - case GithubScope.Org: { - await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", { - org: integration.owner as string, - secret_name: encryptedSecret.name - }); - break; - } - case GithubScope.Env: { - await octokit.request( - "DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}", - { - repository_id: Number(integration.appId), - environment_name: integration.targetEnvironmentId as string, + const metadata = IntegrationMetadataSchema.parse(integration.metadata); + if (metadata.shouldEnableDelete) { + for await (const encryptedSecret of encryptedSecrets) { + if ( + !(encryptedSecret.name in secrets) && + !(appendices?.prefix !== undefined && !encryptedSecret.name.startsWith(appendices?.prefix)) && + !(appendices?.suffix !== undefined && !encryptedSecret.name.endsWith(appendices?.suffix)) + ) { + switch (integration.scope) { + case GithubScope.Org: { + await octokit.request("DELETE /orgs/{org}/actions/secrets/{secret_name}", { + org: integration.owner as string, secret_name: encryptedSecret.name - } - ); - break; - } - default: { - await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", { - owner: integration.owner as string, - repo: integration.app as string, - secret_name: encryptedSecret.name - }); - break; + }); + break; + } + case GithubScope.Env: { + await octokit.request( + "DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}", + { + repository_id: Number(integration.appId), + environment_name: integration.targetEnvironmentId as string, + secret_name: encryptedSecret.name + } + ); + break; + } + default: { + await octokit.request("DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", { + owner: integration.owner as string, + repo: integration.app as string, + secret_name: encryptedSecret.name + }); + break; + } } } } diff --git a/backend/src/services/integration/integration-schema.ts b/backend/src/services/integration/integration-schema.ts new file mode 100644 index 000000000..d1b6bcc77 --- /dev/null +++ b/backend/src/services/integration/integration-schema.ts @@ -0,0 +1,35 @@ +import { z } from "zod"; + +import { INTEGRATION } from "@app/lib/api-docs"; + +import { IntegrationMappingBehavior } from "../integration-auth/integration-list"; + +export const IntegrationMetadataSchema = z.object({ + secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix), + secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix), + initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir), + mappingBehavior: z + .nativeEnum(IntegrationMappingBehavior) + .optional() + .describe(INTEGRATION.CREATE.metadata.mappingBehavior), + shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy), + secretGCPLabel: z + .object({ + labelName: z.string(), + labelValue: z.string() + }) + .optional() + .describe(INTEGRATION.CREATE.metadata.secretGCPLabel), + secretAWSTag: z + .array( + z.object({ + key: z.string(), + value: z.string() + }) + ) + .optional() + .describe(INTEGRATION.CREATE.metadata.secretAWSTag), + kmsKeyId: z.string().optional().describe(INTEGRATION.CREATE.metadata.kmsKeyId), + shouldDisableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldDisableDelete), + shouldEnableDelete: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldEnableDelete) +}); diff --git a/backend/src/services/integration/integration-types.ts b/backend/src/services/integration/integration-types.ts index 9c75cad2d..db136c1ed 100644 --- a/backend/src/services/integration/integration-types.ts +++ b/backend/src/services/integration/integration-types.ts @@ -29,6 +29,7 @@ export type TCreateIntegrationDTO = { }[]; kmsKeyId?: string; shouldDisableDelete?: boolean; + shouldEnableDelete?: boolean; }; } & Omit; @@ -54,6 +55,7 @@ export type TUpdateIntegrationDTO = { }[]; kmsKeyId?: string; shouldDisableDelete?: boolean; + shouldEnableDelete?: boolean; }; } & Omit; diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index 3aa8f3ed1..e561280c6 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -73,6 +73,7 @@ export const useCreateIntegration = () => { }[]; kmsKeyId?: string; shouldDisableDelete?: boolean; + shouldEnableDelete?: boolean; }; }) => { const { diff --git a/frontend/src/pages/integrations/github/create.tsx b/frontend/src/pages/integrations/github/create.tsx index e42a7e9eb..f92a98943 100644 --- a/frontend/src/pages/integrations/github/create.tsx +++ b/frontend/src/pages/integrations/github/create.tsx @@ -33,6 +33,7 @@ import { Input, Select, SelectItem, + Switch, Tab, TabList, TabPanel, @@ -59,7 +60,7 @@ const schema = yup.object({ selectedSourceEnvironment: yup.string().trim().required("Project Environment is required"), secretPath: yup.string().trim().required("Secrets Path is required"), secretSuffix: yup.string().trim().optional(), - + shouldEnableDelete: yup.boolean().optional(), scope: yup.mixed().oneOf(targetEnv.slice()).required(), repoIds: yup.mixed().when("scope", { @@ -98,7 +99,6 @@ type FormData = yup.InferType; export default function GitHubCreateIntegrationPage() { const router = useRouter(); const { mutateAsync } = useCreateIntegration(); - const integrationAuthId = (queryString.parse(router.asPath.split("?")[1]).integrationAuthId as string) ?? ""; @@ -120,7 +120,8 @@ export default function GitHubCreateIntegrationPage() { defaultValues: { secretPath: "/", scope: "github-repo", - repoIds: [] + repoIds: [], + shouldEnableDelete: false } }); @@ -177,7 +178,8 @@ export default function GitHubCreateIntegrationPage() { app: targetApp.name, // repo name owner: targetApp.owner, // repo owner metadata: { - secretSuffix: data.secretSuffix + secretSuffix: data.secretSuffix, + shouldEnableDelete: data.shouldEnableDelete } }); }) @@ -194,7 +196,8 @@ export default function GitHubCreateIntegrationPage() { scope: data.scope, owner: integrationAuthOrgs?.find((e) => e.orgId === data.orgId)?.name, metadata: { - secretSuffix: data.secretSuffix + secretSuffix: data.secretSuffix, + shouldEnableDelete: data.shouldEnableDelete } }); break; @@ -211,7 +214,8 @@ export default function GitHubCreateIntegrationPage() { owner: repoOwner, targetEnvironmentId: data.envId, metadata: { - secretSuffix: data.secretSuffix + secretSuffix: data.secretSuffix, + shouldEnableDelete: data.shouldEnableDelete } }); break; @@ -546,6 +550,21 @@ export default function GitHubCreateIntegrationPage() { animate={{ opacity: 1, translateX: 0 }} exit={{ opacity: 0, translateX: 30 }} > +
+ ( + onChange(isChecked)} + isChecked={value} + > + Delete secrets in Github that are not in Infisical + + )} + /> +