From 0cdade6a2d5296fa01ee06f32ad72d5c2ba23f86 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 10 May 2024 19:07:44 -0700 Subject: [PATCH 1/3] Update AWS SM integration to allow updating tags --- .../server/routes/v1/integration-router.ts | 28 +++++- .../integration-sync-secret.ts | 85 +++++++++++++++++++ .../integration/integration-service.ts | 15 +++- .../services/integration/integration-types.ts | 14 +++ .../integrations/cloud/aws-secret-manager.mdx | 1 + 5 files changed, 140 insertions(+), 3 deletions(-) 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 From 818b136836fa5f7661cdb5abed69065d1fabeafe Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 10 May 2024 19:17:40 -0700 Subject: [PATCH 2/3] Make app and appId optional in update integration endpoint --- backend/src/server/routes/v1/integration-router.ts | 4 ++-- backend/src/services/integration/integration-types.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index fdd9cf4f4..e40e3f799 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -143,8 +143,8 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { integrationId: z.string().trim().describe(INTEGRATION.UPDATE.integrationId) }), body: z.object({ - app: z.string().trim().describe(INTEGRATION.UPDATE.app), - appId: z.string().trim().describe(INTEGRATION.UPDATE.appId), + app: z.string().trim().optional().describe(INTEGRATION.UPDATE.app), + appId: z.string().trim().optional().describe(INTEGRATION.UPDATE.appId), isActive: z.boolean().describe(INTEGRATION.UPDATE.isActive), secretPath: z .string() diff --git a/backend/src/services/integration/integration-types.ts b/backend/src/services/integration/integration-types.ts index 909ab517a..50d0de762 100644 --- a/backend/src/services/integration/integration-types.ts +++ b/backend/src/services/integration/integration-types.ts @@ -33,8 +33,8 @@ export type TCreateIntegrationDTO = { export type TUpdateIntegrationDTO = { id: string; - app: string; - appId: string; + app?: string; + appId?: string; isActive?: boolean; secretPath: string; targetEnvironment: string; From c1fb8f47bf17b325f2374cb2b7acd02913f8e349 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 12 May 2024 08:57:41 -0700 Subject: [PATCH 3/3] Add UntagResource IAM policy requirement for AWS SM integration docs --- docs/integrations/cloud/aws-secret-manager.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/integrations/cloud/aws-secret-manager.mdx b/docs/integrations/cloud/aws-secret-manager.mdx index 120326f66..db95c8308 100644 --- a/docs/integrations/cloud/aws-secret-manager.mdx +++ b/docs/integrations/cloud/aws-secret-manager.mdx @@ -31,6 +31,7 @@ Prerequisites: "secretsmanager:UpdateSecret", "secretsmanager:DescribeSecret", // if you need to add tags to secrets "secretsmanager:TagResource", // if you need to add tags to secrets + "secretsmanager:UntagResource", // 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 "kms:Encrypt", // if you need to specify the KMS key