diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index 975e6e7c8..fdd9cf4f4 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -154,7 +154,33 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { .describe(INTEGRATION.UPDATE.secretPath), targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment), owner: z.string().trim().describe(INTEGRATION.UPDATE.owner), - environment: z.string().trim().describe(INTEGRATION.UPDATE.environment) + 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), + 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() }), 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 4e1dbd7ae..963db1e1e 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -9,9 +9,12 @@ import { CreateSecretCommand, + DescribeSecretCommand, GetSecretValueCommand, ResourceNotFoundException, SecretsManagerClient, + TagResourceCommand, + UntagResourceCommand, UpdateSecretCommand } from "@aws-sdk/client-secrets-manager"; import { Octokit } from "@octokit/rest"; @@ -574,6 +577,7 @@ const syncSecretsAWSSecretManager = async ({ if (awsSecretManagerSecret?.SecretString) { awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString); } + if (!isEqual(awsSecretManagerSecretObj, secKeyVal)) { await secretsManager.send( new UpdateSecretCommand({ @@ -582,7 +586,88 @@ const syncSecretsAWSSecretManager = async ({ }) ); } + + const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined; + + if (secretAWSTag && secretAWSTag.length) { + const describedSecret = await secretsManager.send( + // requires secretsmanager:DescribeSecret policy + new DescribeSecretCommand({ + SecretId: integration.app as string + }) + ); + + if (!describedSecret.Tags) return; + + const integrationTagObj = secretAWSTag.reduce( + (acc, item) => { + acc[item.key] = item.value; + return acc; + }, + {} as Record + ); + + const awsTagObj = (describedSecret.Tags || []).reduce( + (acc, item) => { + if (item.Key && item.Value) { + acc[item.Key] = item.Value; + } + return acc; + }, + {} as Record + ); + + const tagsToUpdate: { Key: string; Value: string }[] = []; + const tagsToDelete: { Key: string; Value: string }[] = []; + + describedSecret.Tags?.forEach((tag) => { + if (tag.Key && tag.Value) { + if (!(tag.Key in integrationTagObj)) { + // delete tag from AWS secret manager + tagsToDelete.push({ + Key: tag.Key, + Value: tag.Value + }); + } else if (tag.Value !== integrationTagObj[tag.Key]) { + // update tag in AWS secret manager + tagsToUpdate.push({ + Key: tag.Key, + Value: integrationTagObj[tag.Key] + }); + } + } + }); + + secretAWSTag?.forEach((tag) => { + if (!(tag.key in awsTagObj)) { + // create tag in AWS secret manager + tagsToUpdate.push({ + Key: tag.key, + Value: tag.value + }); + } + }); + + if (tagsToUpdate.length) { + await secretsManager.send( + new TagResourceCommand({ + SecretId: integration.app as string, + Tags: tagsToUpdate + }) + ); + } + + if (tagsToDelete.length) { + await secretsManager.send( + new UntagResourceCommand({ + SecretId: integration.app as string, + TagKeys: tagsToDelete.map((tag) => tag.Key) + }) + ); + } + } } catch (err) { + // case when AWS manager can't find the specified secret if (err instanceof ResourceNotFoundException && secretsManager) { await secretsManager.send( new CreateSecretCommand({ diff --git a/backend/src/services/integration/integration-service.ts b/backend/src/services/integration/integration-service.ts index a3c4c84db..f4f8cdb44 100644 --- a/backend/src/services/integration/integration-service.ts +++ b/backend/src/services/integration/integration-service.ts @@ -103,7 +103,8 @@ export const integrationServiceFactory = ({ owner, isActive, environment, - secretPath + secretPath, + metadata }: TUpdateIntegrationDTO) => { const integration = await integrationDAL.findById(id); if (!integration) throw new BadRequestError({ message: "Integration auth not found" }); @@ -127,7 +128,17 @@ export const integrationServiceFactory = ({ appId, targetEnvironment, owner, - secretPath + secretPath, + metadata: { + ...(integration.metadata as object), + ...metadata + } + }); + + await secretQueueService.syncIntegrations({ + environment: folder.environment.slug, + secretPath, + projectId: folder.projectId }); return updatedIntegration; diff --git a/backend/src/services/integration/integration-types.ts b/backend/src/services/integration/integration-types.ts index 1913dd31f..909ab517a 100644 --- a/backend/src/services/integration/integration-types.ts +++ b/backend/src/services/integration/integration-types.ts @@ -40,6 +40,20 @@ export type TUpdateIntegrationDTO = { targetEnvironment: string; owner: string; environment: string; + metadata?: { + secretPrefix?: string; + secretSuffix?: string; + secretGCPLabel?: { + labelName: string; + labelValue: string; + }; + secretAWSTag?: { + key: string; + value: string; + }[]; + kmsKeyId?: string; + shouldDisableDelete?: boolean; + }; } & Omit; export type TDeleteIntegrationDTO = { diff --git a/docs/integrations/cloud/aws-secret-manager.mdx b/docs/integrations/cloud/aws-secret-manager.mdx index 6fc83eaff..120326f66 100644 --- a/docs/integrations/cloud/aws-secret-manager.mdx +++ b/docs/integrations/cloud/aws-secret-manager.mdx @@ -29,6 +29,7 @@ Prerequisites: "secretsmanager:GetSecretValue", "secretsmanager:CreateSecret", "secretsmanager:UpdateSecret", + "secretsmanager:DescribeSecret", // if you need to add tags to secrets "secretsmanager:TagResource", // if you need to add tags to secrets "kms:ListKeys", // if you need to specify the KMS key "kms:ListAliases", // if you need to specify the KMS key