diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 5326a977c..08b33e10a 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1136,7 +1136,8 @@ export const INTEGRATION = { shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.", shouldProtectSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Protected'.", shouldEnableDelete: "The flag to enable deletion of secrets.", - octopusDeployScopeValues: "Specifies the scope values to set on synced secrets to Octopus Deploy." + octopusDeployScopeValues: "Specifies the scope values to set on synced secrets to Octopus Deploy.", + metadataSyncMode: "The mode for syncing metadata to external system" } }, UPDATE: { diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index d6da2194d..16a717bd6 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -427,3 +427,8 @@ export const getIntegrationOptions = async () => { return INTEGRATION_OPTIONS; }; + +export enum IntegrationMetadataSyncMode { + CUSTOM = "custom", + SECRET_METADATA = "secret-metadata" +} diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index 4fd139608..0519b70d2 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -38,6 +38,7 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/ import { TIntegrationDALFactory } from "../integration/integration-dal"; import { IntegrationMetadataSchema } from "../integration/integration-schema"; +import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { IntegrationAuthMetadataSchema } from "./integration-auth-schema"; import { CircleCiScope, @@ -48,6 +49,7 @@ import { import { IntegrationInitialSyncBehavior, IntegrationMappingBehavior, + IntegrationMetadataSyncMode, Integrations, IntegrationUrls } from "./integration-list"; @@ -1074,14 +1076,14 @@ const syncSecretsAWSSecretManager = async ({ projectId }: { integration: TIntegrations; - secrets: Record; + secrets: Record; accessId: string | null; accessToken: string; awsAssumeRoleArn: string | null; projectId?: string; }) => { const appCfg = getConfig(); - const metadata = z.record(z.any()).parse(integration.metadata || {}); + const metadata = IntegrationMetadataSchema.parse(integration.metadata || {}); if (!accessId && !awsAssumeRoleArn) { throw new Error("AWS access ID/AWS Assume Role is required"); @@ -1129,8 +1131,25 @@ const syncSecretsAWSSecretManager = async ({ const processAwsSecret = async ( secretId: string, - secretValue: Record | string + secretValue: Record | string, + secretMetadata?: ResourceMetadataDTO ) => { + const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined; + const shouldTag = + (secretAWSTag && secretAWSTag.length) || + (metadata.metadataSyncMode === IntegrationMetadataSyncMode.SECRET_METADATA && + metadata.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE); + const tagArray = + (metadata.metadataSyncMode === IntegrationMetadataSyncMode.SECRET_METADATA ? secretMetadata : secretAWSTag) ?? []; + + const integrationTagObj = tagArray.reduce( + (acc, item) => { + acc[item.key] = item.value; + return acc; + }, + {} as Record + ); + try { const awsSecretManagerSecret = await secretsManager.send( new GetSecretValueCommand({ @@ -1165,9 +1184,7 @@ const syncSecretsAWSSecretManager = async ({ } } - const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined; - - if (secretAWSTag && secretAWSTag.length) { + if (shouldTag) { const describedSecret = await secretsManager.send( // requires secretsmanager:DescribeSecret policy new DescribeSecretCommand({ @@ -1177,14 +1194,6 @@ const syncSecretsAWSSecretManager = async ({ 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) { @@ -1216,7 +1225,7 @@ const syncSecretsAWSSecretManager = async ({ } }); - secretAWSTag?.forEach((tag) => { + tagArray.forEach((tag) => { if (!(tag.key in awsTagObj)) { // create tag in AWS secret manager tagsToUpdate.push({ @@ -1253,8 +1262,8 @@ const syncSecretsAWSSecretManager = async ({ Name: secretId, SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue), ...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }), - Tags: metadata.secretAWSTag - ? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ + Tags: shouldTag + ? tagArray.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value })) @@ -1271,7 +1280,7 @@ const syncSecretsAWSSecretManager = async ({ if (metadata.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE) { for await (const [key, value] of Object.entries(secrets)) { - await processAwsSecret(key, value.value); + await processAwsSecret(key, value.value, value.secretMetadata); } } else { await processAwsSecret(integration.app as string, getSecretKeyValuePair(secrets)); @@ -4392,7 +4401,7 @@ export const syncIntegrationSecrets = async ({ secretPath: string; }; integrationAuth: TIntegrationAuths; - secrets: Record; + secrets: Record; accessId: string | null; awsAssumeRoleArn: string | null; accessToken: string; diff --git a/backend/src/services/integration/integration-schema.ts b/backend/src/services/integration/integration-schema.ts index de4790188..084946c0b 100644 --- a/backend/src/services/integration/integration-schema.ts +++ b/backend/src/services/integration/integration-schema.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { INTEGRATION } from "@app/lib/api-docs"; -import { IntegrationMappingBehavior } from "../integration-auth/integration-list"; +import { IntegrationMappingBehavior, IntegrationMetadataSyncMode } from "../integration-auth/integration-list"; export const IntegrationMetadataSchema = z.object({ initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir), @@ -50,6 +50,11 @@ export const IntegrationMetadataSchema = z.object({ shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets), shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets), + metadataSyncMode: z + .nativeEnum(IntegrationMetadataSyncMode) + .optional() + .describe(INTEGRATION.CREATE.metadata.metadataSyncMode), + octopusDeployScopeValues: z .object({ // in Octopus Deploy Scope Value Format diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index bd35f8bc3..0f61e8c48 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -47,6 +47,7 @@ import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; +import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns"; @@ -121,7 +122,12 @@ export const uniqueSecretQueueKey = (environment: string, secretPath: string) => type TIntegrationSecret = Record< string, - { value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined } + { + value: string; + comment?: string; + skipMultilineEncoding?: boolean | null | undefined; + secretMetadata?: ResourceMetadataDTO; + } >; // TODO(akhilmhdh): split this into multiple queue @@ -370,6 +376,7 @@ export const secretQueueFactory = ({ } content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding); + content[secretKey].secretMetadata = secret.secretMetadata; }) ); @@ -395,7 +402,8 @@ export const secretQueueFactory = ({ content[importedSecret.key] = { skipMultilineEncoding: importedSecret.skipMultilineEncoding, comment: importedSecret.secretComment, - value: importedSecret.secretValue || "" + value: importedSecret.secretValue || "", + secretMetadata: importedSecret.secretMetadata }; } } diff --git a/docs/images/integrations/aws/integrations-aws-secret-manager-options.png b/docs/images/integrations/aws/integrations-aws-secret-manager-options.png index f8492cdfa..9450df8b7 100644 Binary files a/docs/images/integrations/aws/integrations-aws-secret-manager-options.png and b/docs/images/integrations/aws/integrations-aws-secret-manager-options.png differ diff --git a/docs/integrations/cloud/aws-secret-manager.mdx b/docs/integrations/cloud/aws-secret-manager.mdx index 64df1df32..1f3a0da1f 100644 --- a/docs/integrations/cloud/aws-secret-manager.mdx +++ b/docs/integrations/cloud/aws-secret-manager.mdx @@ -17,6 +17,7 @@ Prerequisites: If your instance is deployed on AWS, the aws-sdk will automatically retrieve the credentials. Ensure that you assign the provided permission policy to your deployed instance, such as ECS or EC2. The following steps are for instances not deployed on AWS + Navigate to [Create IAM User](https://console.aws.amazon.com/iamv2/home#/users/create) in your AWS Console. @@ -40,9 +41,10 @@ The following steps are for instances not deployed on AWS Obtain the AWS access key ID and secret access key for your IAM User by navigating to IAM > Users > [Your User] > Security credentials > Access keys. - ![Access Key Step 1](../../images/integrations/aws/integrations-aws-access-key-1.png) - ![Access Key Step 2](../../images/integrations/aws/integrations-aws-access-key-2.png) - ![Access Key Step 3](../../images/integrations/aws/integrations-aws-access-key-3.png) +![Access Key Step 1](../../images/integrations/aws/integrations-aws-access-key-1.png) +![Access Key Step 2](../../images/integrations/aws/integrations-aws-access-key-2.png) +![Access Key Step 3](../../images/integrations/aws/integrations-aws-access-key-3.png) + 1. Set the access key as **CLIENT_ID_AWS_INTEGRATION**. @@ -59,6 +61,7 @@ The following steps are for instances not deployed on AWS 2. Select **AWS Account** as the **Trusted Entity Type**. 3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead. 4. Optionally, enable **Require external ID** and enter your **project ID** to further enhance security. + @@ -89,11 +92,13 @@ The following steps are for instances not deployed on AWS ] } ``` + - - ![Copy IAM Role ARN](../../images/integrations/aws/integration-aws-iam-assume-arn.png) - + + ![Copy IAM Role + ARN](../../images/integrations/aws/integration-aws-iam-assume-arn.png) + 1. Navigate to your project's integrations tab in Infisical. @@ -104,6 +109,7 @@ The following steps are for instances not deployed on AWS ![Select Assume Role](../../images/integrations/aws/integration-aws-iam-assume-select.png) 4. Provide the **AWS IAM Role ARN** obtained from the previous step. + Select how you want to integration to work by specifying a number of parameters: @@ -127,6 +133,12 @@ The following steps are for instances not deployed on AWS Optionally, you can add tags or specify the encryption key of all the secrets created via this integration: + + The sync mode for AWS tags. The supported options are `Secret Metadata` and `Custom`. If `Secret Metadata` is selected, + the metadata of the Infisical secrets are used as tags in AWS. If custom is selected, then the key/value of the **Secret Tag** field is used. `Secret Metadata` mode + is only supported for one-to-one integrations. + + The Key/Value of a tag that will be added to secrets in AWS. Please note that it is possible to add multiple tags via API. diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index 5c059ae98..8aee5d242 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -4,7 +4,12 @@ import { createNotification } from "@app/components/notifications"; import { apiRequest } from "@app/config/request"; import { workspaceKeys } from "../workspace"; -import { TCloudIntegration, TIntegrationWithEnv, TOctopusDeployScopeValues } from "./types"; +import { + IntegrationMetadataSyncMode, + TCloudIntegration, + TIntegrationWithEnv, + TOctopusDeployScopeValues +} from "./types"; export const integrationQueryKeys = { getIntegrations: () => ["integrations"] as const, @@ -89,6 +94,7 @@ export const useCreateIntegration = () => { shouldProtectSecrets?: boolean; shouldEnableDelete?: boolean; octopusDeployScopeValues?: TOctopusDeployScopeValues; + metadataSyncMode?: IntegrationMetadataSyncMode; }; }) => { const { diff --git a/frontend/src/hooks/api/integrations/types.ts b/frontend/src/hooks/api/integrations/types.ts index 7054befc7..b7adb0f53 100644 --- a/frontend/src/hooks/api/integrations/types.ts +++ b/frontend/src/hooks/api/integrations/types.ts @@ -62,6 +62,7 @@ export type TIntegration = { octopusDeployScopeValues?: TOctopusDeployScopeValues; awsIamRole?: string; region?: string; + metadataSyncMode?: IntegrationMetadataSyncMode; }; }; @@ -92,3 +93,8 @@ export enum IntegrationMappingBehavior { ONE_TO_ONE = "one-to-one", MANY_TO_ONE = "many-to-one" } + +export enum IntegrationMetadataSyncMode { + CUSTOM = "custom", + SECRET_METADATA = "secret-metadata" +} diff --git a/frontend/src/pages/integrations/aws-secret-manager/create.tsx b/frontend/src/pages/integrations/aws-secret-manager/create.tsx index 136dffa4b..beadcdfd6 100644 --- a/frontend/src/pages/integrations/aws-secret-manager/create.tsx +++ b/frontend/src/pages/integrations/aws-secret-manager/create.tsx @@ -19,7 +19,10 @@ import z from "zod"; import { SecretPathInput } from "@app/components/v2/SecretPathInput"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries"; -import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types"; +import { + IntegrationMappingBehavior, + IntegrationMetadataSyncMode +} from "@app/hooks/api/integrations/types"; import { Badge, @@ -97,6 +100,7 @@ const schema = z mappingBehavior: z.nativeEnum(IntegrationMappingBehavior), kmsKeyId: z.string().optional(), shouldTag: z.boolean().optional(), + metadataSyncMode: z.nativeEnum(IntegrationMetadataSyncMode).optional(), tags: z .object({ key: z.string(), @@ -139,6 +143,7 @@ export default function AWSSecretManagerCreateIntegrationPage() { }); const shouldTagState = watch("shouldTag"); + const selectedMetadataSyncMode = watch("metadataSyncMode"); const selectedSourceEnvironment = watch("sourceEnvironment"); const selectedAWSRegion = watch("awsRegion"); const selectedMappingBehavior = watch("mappingBehavior"); @@ -171,7 +176,8 @@ export default function AWSSecretManagerCreateIntegrationPage() { tags, secretPrefix, kmsKeyId, - mappingBehavior + mappingBehavior, + metadataSyncMode }: TFormSchema) => { try { if (!integrationAuth?.id) return; @@ -186,7 +192,8 @@ export default function AWSSecretManagerCreateIntegrationPage() { metadata: { ...(shouldTag ? { - secretAWSTag: tags + secretAWSTag: tags, + metadataSyncMode } : {}), ...(secretPrefix && { secretPrefix }), @@ -339,7 +346,12 @@ export default function AWSSecretManagerCreateIntegrationPage() { > + )} /> - ( - - - - )} - /> - - )} - + )} + {shouldTagState && + selectedMetadataSyncMode === IntegrationMetadataSyncMode.CUSTOM && ( +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ )} {