packages/aws-cdk-lib/aws-secretsmanager/lib/secret.ts (452 lines of code) (raw):

import { IConstruct, Construct } from 'constructs'; import { ResourcePolicy } from './policy'; import { RotationSchedule, RotationScheduleOptions } from './rotation-schedule'; import * as secretsmanager from './secretsmanager.generated'; import * as iam from '../../aws-iam'; import * as kms from '../../aws-kms'; import { ArnFormat, FeatureFlags, Fn, IResolveContext, IResource, Lazy, RemovalPolicy, Resource, ResourceProps, SecretValue, Stack, Token, TokenComparison } from '../../core'; import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource'; import * as cxapi from '../../cx-api'; const SECRET_SYMBOL = Symbol.for('@aws-cdk/secretsmanager.Secret'); /** * A secret in AWS Secrets Manager. */ export interface ISecret extends IResource { /** * The customer-managed encryption key that is used to encrypt this secret, if any. When not specified, the default * KMS key for the account and region is being used. */ readonly encryptionKey?: kms.IKey; /** * The ARN of the secret in AWS Secrets Manager. Will return the full ARN if available, otherwise a partial arn. * For secrets imported by the deprecated `fromSecretName`, it will return the `secretName`. * @attribute */ readonly secretArn: string; /** * The full ARN of the secret in AWS Secrets Manager, which is the ARN including the Secrets Manager-supplied 6-character suffix. * This is equal to `secretArn` in most cases, but is undefined when a full ARN is not available (e.g., secrets imported by name). */ readonly secretFullArn?: string; /** * The name of the secret. * * For "owned" secrets, this will be the full resource name (secret name + suffix), unless the * '@aws-cdk/aws-secretsmanager:parseOwnedSecretName' feature flag is set. */ readonly secretName: string; /** * Retrieve the value of the stored secret as a `SecretValue`. * @attribute */ readonly secretValue: SecretValue; /** * Interpret the secret as a JSON object and return a field's value from it as a `SecretValue`. */ secretValueFromJson(key: string): SecretValue; /** * Grants reading the secret value to some role. * * @param grantee the principal being granted permission. * @param versionStages the version stages the grant is limited to. If not specified, no restriction on the version * stages is applied. */ grantRead(grantee: iam.IGrantable, versionStages?: string[]): iam.Grant; /** * Grants writing and updating the secret value to some role. * * @param grantee the principal being granted permission. */ grantWrite(grantee: iam.IGrantable): iam.Grant; /** * Adds a rotation schedule to the secret. */ addRotationSchedule(id: string, options: RotationScheduleOptions): RotationSchedule; /** * Adds a statement to the IAM resource policy associated with this secret. * * If this secret was created in this stack, a resource policy will be * automatically created upon the first call to `addToResourcePolicy`. If * the secret is imported, then this is a no-op. */ addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult; /** * Denies the `DeleteSecret` action to all principals within the current * account. */ denyAccountRootDelete(): void; /** * Attach a target to this secret. * * @param target The target to attach. * @returns An attached secret */ attach(target: ISecretAttachmentTarget): ISecret; } /** * The properties required to create a new secret in AWS Secrets Manager. */ export interface SecretProps { /** * An optional, human-friendly description of the secret. * * @default - No description. */ readonly description?: string; /** * The customer-managed encryption key to use for encrypting the secret value. * * @default - A default KMS key for the account and region is used. */ readonly encryptionKey?: kms.IKey; /** * Configuration for how to generate a secret value. * * Only one of `secretString` and `generateSecretString` can be provided. * * @default - 32 characters with upper-case letters, lower-case letters, punctuation and numbers (at least one from each * category), per the default values of ``SecretStringGenerator``. */ readonly generateSecretString?: SecretStringGenerator; /** * A name for the secret. Note that deleting secrets from SecretsManager does not happen immediately, but after a 7 to * 30 days blackout period. During that period, it is not possible to create another secret that shares the same name. * * @default - A name is generated by CloudFormation. */ readonly secretName?: string; /** * Initial value for the secret * * **NOTE:** *It is **highly** encouraged to leave this field undefined and allow SecretsManager to create the secret value. * The secret string -- if provided -- will be included in the output of the cdk as part of synthesis, * and will appear in the CloudFormation template in the console. This can be secure(-ish) if that value is merely reference to * another resource (or one of its attributes), but if the value is a plaintext string, it will be visible to anyone with access * to the CloudFormation template (via the AWS Console, SDKs, or CLI). * * Specifies text data that you want to encrypt and store in this new version of the secret. * May be a simple string value, or a string representation of a JSON structure. * * Only one of `secretStringBeta1`, `secretStringValue`, and `generateSecretString` can be provided. * * @default - SecretsManager generates a new secret value. * @deprecated Use `secretStringValue` instead. */ readonly secretStringBeta1?: SecretStringValueBeta1; /** * Initial value for the secret * * **NOTE:** *It is **highly** encouraged to leave this field undefined and allow SecretsManager to create the secret value. * The secret string -- if provided -- will be included in the output of the cdk as part of synthesis, * and will appear in the CloudFormation template in the console. This can be secure(-ish) if that value is merely reference to * another resource (or one of its attributes), but if the value is a plaintext string, it will be visible to anyone with access * to the CloudFormation template (via the AWS Console, SDKs, or CLI). * * Specifies text data that you want to encrypt and store in this new version of the secret. * May be a simple string value. To provide a string representation of JSON structure, use `SecretProps.secretObjectValue` instead. * * Only one of `secretStringBeta1`, `secretStringValue`, 'secretObjectValue', and `generateSecretString` can be provided. * * @default - SecretsManager generates a new secret value. */ readonly secretStringValue?: SecretValue; /** * Initial value for a JSON secret * * **NOTE:** *It is **highly** encouraged to leave this field undefined and allow SecretsManager to create the secret value. * The secret object -- if provided -- will be included in the output of the cdk as part of synthesis, * and will appear in the CloudFormation template in the console. This can be secure(-ish) if that value is merely reference to * another resource (or one of its attributes), but if the value is a plaintext string, it will be visible to anyone with access * to the CloudFormation template (via the AWS Console, SDKs, or CLI). * * Specifies a JSON object that you want to encrypt and store in this new version of the secret. * To specify a simple string value instead, use `SecretProps.secretStringValue` * * Only one of `secretStringBeta1`, `secretStringValue`, 'secretObjectValue', and `generateSecretString` can be provided. * * @example * declare const user: iam.User; * declare const accessKey: iam.AccessKey; * declare const stack: Stack; * new secretsmanager.Secret(stack, 'JSONSecret', { * secretObjectValue: { * username: SecretValue.unsafePlainText(user.userName), // intrinsic reference, not exposed as plaintext * database: SecretValue.unsafePlainText('foo'), // rendered as plain text, but not a secret * password: accessKey.secretAccessKey, // SecretValue * }, * }); * * @default - SecretsManager generates a new secret value. */ readonly secretObjectValue?: { [key: string]: SecretValue }; /** * Policy to apply when the secret is removed from this stack. * * @default - Not set. */ readonly removalPolicy?: RemovalPolicy; /** * A list of regions where to replicate this secret. * * @default - Secret is not replicated */ readonly replicaRegions?: ReplicaRegion[]; } /** * Secret replica region */ export interface ReplicaRegion { /** * The name of the region */ readonly region: string; /** * The customer-managed encryption key to use for encrypting the secret value. * * @default - A default KMS key for the account and region is used. */ readonly encryptionKey?: kms.IKey; } /** * An experimental class used to specify an initial secret value for a Secret. * * The class wraps a simple string (or JSON representation) in order to provide some safety checks and warnings * about the dangers of using plaintext strings as initial secret seed values via CDK/CloudFormation. * * @deprecated Use `cdk.SecretValue` instead. */ export class SecretStringValueBeta1 { /** * Creates a `SecretStringValueBeta1` from a plaintext value. * * This approach is inherently unsafe, as the secret value may be visible in your source control repository * and will also appear in plaintext in the resulting CloudFormation template, including in the AWS Console or APIs. * Usage of this method is discouraged, especially for production workloads. */ public static fromUnsafePlaintext(secretValue: string) { return new SecretStringValueBeta1(secretValue); } /** * Creates a `SecretValueValueBeta1` from a string value coming from a Token. * * The intent is to enable creating secrets from references (e.g., `Ref`, `Fn::GetAtt`) from other resources. * This might be the direct output of another Construct, or the output of a Custom Resource. * This method throws if it determines the input is an unsafe plaintext string. * * For example: * * ```ts * // Creates a new IAM user, access and secret keys, and stores the secret access key in a Secret. * const user = new iam.User(this, 'User'); * const accessKey = new iam.AccessKey(this, 'AccessKey', { user }); * const secret = new secretsmanager.Secret(this, 'Secret', { * secretStringValue: accessKey.secretAccessKey, * }); * ``` * * The secret may also be embedded in a string representation of a JSON structure: * * ```ts * const user = new iam.User(this, 'User'); * const accessKey = new iam.AccessKey(this, 'AccessKey', { user }); * const secretValue = secretsmanager.SecretStringValueBeta1.fromToken(JSON.stringify({ * username: user.userName, * database: 'foo', * password: accessKey.secretAccessKey.unsafeUnwrap(), * })); * ``` * * Note that the value being a Token does *not* guarantee safety. For example, a Lazy-evaluated string * (e.g., `Lazy.string({ produce: () => 'myInsecurePassword' }))`) is a Token, but as the output is * ultimately a plaintext string, and so insecure. * * @param secretValueFromToken a secret value coming from a Construct attribute or Custom Resource output */ public static fromToken(secretValueFromToken: string) { if (!Token.isUnresolved(secretValueFromToken)) { throw new Error('SecretStringValueBeta1 appears to be plaintext (unsafe) string (or resolved Token); use fromUnsafePlaintext if this is intentional'); } return new SecretStringValueBeta1(secretValueFromToken); } private constructor(private readonly _secretValue: string) { } /** Returns the secret value */ public secretValue(): string { return this._secretValue; } } /** * Attributes required to import an existing secret into the Stack. * One ARN format (`secretArn`, `secretCompleteArn`, `secretPartialArn`) must be provided. */ export interface SecretAttributes { /** * The encryption key that is used to encrypt the secret, unless the default SecretsManager key is used. */ readonly encryptionKey?: kms.IKey; /** * The ARN of the secret in SecretsManager. * Cannot be used with `secretCompleteArn` or `secretPartialArn`. * @deprecated use `secretCompleteArn` or `secretPartialArn` instead. */ readonly secretArn?: string; /** * The complete ARN of the secret in SecretsManager. This is the ARN including the Secrets Manager 6-character suffix. * Cannot be used with `secretArn` or `secretPartialArn`. */ readonly secretCompleteArn?: string; /** * The partial ARN of the secret in SecretsManager. This is the ARN without the Secrets Manager 6-character suffix. * Cannot be used with `secretArn` or `secretCompleteArn`. */ readonly secretPartialArn?: string; } /** * The common behavior of Secrets. Users should not use this class directly, and instead use ``Secret``. */ abstract class SecretBase extends Resource implements ISecret { public abstract readonly encryptionKey?: kms.IKey; public abstract readonly secretArn: string; public abstract readonly secretName: string; protected abstract readonly autoCreatePolicy: boolean; private policy?: ResourcePolicy; private _arnForPolicies: string; constructor(scope: Construct, id: string, props: ResourceProps = {}) { super(scope, id, props); this._arnForPolicies = Lazy.uncachedString({ produce: (context: IResolveContext) => { const consumingStack = Stack.of(context.scope); if (this.stack.account !== consumingStack.account || (this.stack.region !== consumingStack.region && !consumingStack._crossRegionReferences) || !this.secretFullArn) { return `${this.secretArn}-??????`; } else { return this.secretFullArn; } }, }); this.node.addValidation({ validate: () => this.policy?.document.validateForResourcePolicy() ?? [] }); } public get secretFullArn(): string | undefined { return this.secretArn; } public grantRead(grantee: iam.IGrantable, versionStages?: string[]): iam.Grant { // @see https://docs.aws.amazon.com/secretsmanager/latest/userguide/auth-and-access_identity-based-policies.html const result = iam.Grant.addToPrincipalOrResource({ grantee, actions: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], resourceArns: [this.arnForPolicies], resource: this, }); const statement = result.principalStatement || result.resourceStatement; if (versionStages != null && statement) { statement.addCondition('ForAnyValue:StringEquals', { 'secretsmanager:VersionStage': versionStages, }); } if (this.encryptionKey) { // @see https://docs.aws.amazon.com/kms/latest/developerguide/services-secrets-manager.html this.encryptionKey.grantDecrypt( new kms.ViaServicePrincipal(`secretsmanager.${Stack.of(this).region}.amazonaws.com`, grantee.grantPrincipal), ); } const crossAccount = Token.compareStrings(Stack.of(this).account, grantee.grantPrincipal.principalAccount || ''); // Throw if secret is not imported and it's shared cross account and no KMS key is provided if (this instanceof Secret && result.resourceStatement && (!this.encryptionKey && crossAccount === TokenComparison.DIFFERENT)) { throw new Error('KMS Key must be provided for cross account access to Secret'); } return result; } public grantWrite(grantee: iam.IGrantable): iam.Grant { // See https://docs.aws.amazon.com/secretsmanager/latest/userguide/auth-and-access_identity-based-policies.html const result = iam.Grant.addToPrincipalOrResource({ grantee, actions: ['secretsmanager:PutSecretValue', 'secretsmanager:UpdateSecret'], resourceArns: [this.arnForPolicies], resource: this, }); if (this.encryptionKey) { // See https://docs.aws.amazon.com/kms/latest/developerguide/services-secrets-manager.html this.encryptionKey.grantEncrypt( new kms.ViaServicePrincipal(`secretsmanager.${Stack.of(this).region}.amazonaws.com`, grantee.grantPrincipal), ); } // Throw if secret is not imported and it's shared cross account and no KMS key is provided if (this instanceof Secret && result.resourceStatement && !this.encryptionKey) { throw new Error('KMS Key must be provided for cross account access to Secret'); } return result; } public get secretValue() { return this.secretValueFromJson(''); } public secretValueFromJson(jsonField: string) { return SecretValue.secretsManager(this.secretArn, { jsonField }); } public addRotationSchedule(id: string, options: RotationScheduleOptions): RotationSchedule { return new RotationSchedule(this, id, { secret: this, ...options, }); } public addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult { if (!this.policy && this.autoCreatePolicy) { this.policy = new ResourcePolicy(this, 'Policy', { secret: this }); } if (this.policy) { this.policy.document.addStatements(statement); return { statementAdded: true, policyDependable: this.policy }; } return { statementAdded: false }; } public denyAccountRootDelete() { this.addToResourcePolicy(new iam.PolicyStatement({ actions: ['secretsmanager:DeleteSecret'], effect: iam.Effect.DENY, resources: ['*'], principals: [new iam.AccountRootPrincipal()], })); } /** * Provides an identifier for this secret for use in IAM policies. * If there is a full ARN, this is just the ARN; * if we have a partial ARN -- due to either importing by secret name or partial ARN -- * then we need to add a suffix to capture the full ARN's format. */ protected get arnForPolicies() { return this._arnForPolicies; } /** * Attach a target to this secret * * @param target The target to attach * @returns An attached secret */ public attach(target: ISecretAttachmentTarget): ISecret { const id = 'Attachment'; const existing = this.node.tryFindChild(id); if (existing) { throw new Error('Secret is already attached to a target.'); } return new SecretTargetAttachment(this, id, { secret: this, target, }); } } /** * Creates a new secret in AWS SecretsManager. */ export class Secret extends SecretBase { /** * Return whether the given object is a Secret. */ public static isSecret(x: any): x is Secret { return x !== null && typeof(x) === 'object' && SECRET_SYMBOL in x; } /** @deprecated use `fromSecretCompleteArn` or `fromSecretPartialArn` */ public static fromSecretArn(scope: Construct, id: string, secretArn: string): ISecret { const attrs = arnIsComplete(secretArn) ? { secretCompleteArn: secretArn } : { secretPartialArn: secretArn }; return Secret.fromSecretAttributes(scope, id, attrs); } /** Imports a secret by complete ARN. The complete ARN is the ARN with the Secrets Manager-supplied suffix. */ public static fromSecretCompleteArn(scope: Construct, id: string, secretCompleteArn: string): ISecret { return Secret.fromSecretAttributes(scope, id, { secretCompleteArn }); } /** Imports a secret by partial ARN. The partial ARN is the ARN without the Secrets Manager-supplied suffix. */ public static fromSecretPartialArn(scope: Construct, id: string, secretPartialArn: string): ISecret { return Secret.fromSecretAttributes(scope, id, { secretPartialArn }); } /** * Imports a secret by secret name; the ARN of the Secret will be set to the secret name. * A secret with this name must exist in the same account & region. * @deprecated use `fromSecretNameV2` */ public static fromSecretName(scope: Construct, id: string, secretName: string): ISecret { return new class extends SecretBase { public readonly encryptionKey = undefined; public readonly secretArn = secretName; public readonly secretName = secretName; protected readonly autoCreatePolicy = false; public get secretFullArn() { return undefined; } // Overrides the secretArn for grant* methods, where the secretArn must be in ARN format. // Also adds a wildcard to the resource name to support the SecretsManager-provided suffix. protected get arnForPolicies() { return Stack.of(this).formatArn({ service: 'secretsmanager', resource: 'secret', resourceName: this.secretName + '*', arnFormat: ArnFormat.COLON_RESOURCE_NAME, }); } }(scope, id); } /** * Imports a secret by secret name. * A secret with this name must exist in the same account & region. * Replaces the deprecated `fromSecretName`. * Please note this method returns ISecret that only contains partial ARN and could lead to AccessDeniedException * when you pass the partial ARN to CLI or SDK to get the secret value. If your secret name ends with a hyphen and * 6 characters, you should always use fromSecretCompleteArn() to avoid potential AccessDeniedException. * @see https://docs.aws.amazon.com/secretsmanager/latest/userguide/troubleshoot.html#ARN_secretnamehyphen */ public static fromSecretNameV2(scope: Construct, id: string, secretName: string): ISecret { return new class extends SecretBase { public readonly encryptionKey = undefined; public readonly secretName = secretName; public readonly secretArn = this.partialArn; protected readonly autoCreatePolicy = false; public get secretFullArn() { return undefined; } // Creates a "partial" ARN from the secret name. The "full" ARN would include the SecretsManager-provided suffix. private get partialArn(): string { return Stack.of(this).formatArn({ service: 'secretsmanager', resource: 'secret', resourceName: secretName, arnFormat: ArnFormat.COLON_RESOURCE_NAME, }); } }(scope, id); } /** * Import an existing secret into the Stack. * * @param scope the scope of the import. * @param id the ID of the imported Secret in the construct tree. * @param attrs the attributes of the imported secret. */ public static fromSecretAttributes(scope: Construct, id: string, attrs: SecretAttributes): ISecret { let secretArn: string; let secretArnIsPartial: boolean; if (attrs.secretArn) { if (attrs.secretCompleteArn || attrs.secretPartialArn) { throw new Error('cannot use `secretArn` with `secretCompleteArn` or `secretPartialArn`'); } secretArn = attrs.secretArn; secretArnIsPartial = false; } else { if ((attrs.secretCompleteArn && attrs.secretPartialArn) || (!attrs.secretCompleteArn && !attrs.secretPartialArn)) { throw new Error('must use only one of `secretCompleteArn` or `secretPartialArn`'); } if (attrs.secretCompleteArn && !arnIsComplete(attrs.secretCompleteArn)) { throw new Error('`secretCompleteArn` does not appear to be complete; missing 6-character suffix'); } [secretArn, secretArnIsPartial] = attrs.secretCompleteArn ? [attrs.secretCompleteArn, false] : [attrs.secretPartialArn!, true]; } return new class extends SecretBase { public readonly encryptionKey = attrs.encryptionKey; public readonly secretArn = secretArn; public readonly secretName = parseSecretName(scope, secretArn); protected readonly autoCreatePolicy = false; public get secretFullArn() { return secretArnIsPartial ? undefined : secretArn; } protected get arnForPolicies() { return secretArnIsPartial ? `${secretArn}-??????` : secretArn; } }(scope, id, { environmentFromArn: secretArn }); } public readonly encryptionKey?: kms.IKey; public readonly secretArn: string; public readonly secretName: string; /** * The string of the characters that are excluded in this secret * when it is generated. */ public readonly excludeCharacters?: string; private replicaRegions: secretsmanager.CfnSecret.ReplicaRegionProperty[] = []; protected readonly autoCreatePolicy = true; constructor(scope: Construct, id: string, props: SecretProps = {}) { super(scope, id, { physicalName: props.secretName, }); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); if (props.generateSecretString && (props.generateSecretString.secretStringTemplate || props.generateSecretString.generateStringKey) && !(props.generateSecretString.secretStringTemplate && props.generateSecretString.generateStringKey)) { throw new Error('`secretStringTemplate` and `generateStringKey` must be specified together.'); } if ((props.generateSecretString ? 1 : 0) + (props.secretStringBeta1 ? 1 : 0) + (props.secretStringValue ? 1 : 0) + (props.secretObjectValue ? 1 : 0) > 1) { throw new Error('Cannot specify more than one of `generateSecretString`, `secretStringValue`, `secretObjectValue`, and `secretStringBeta1`.'); } const secretString = props.secretObjectValue ? this.resolveSecretObjectValue(props.secretObjectValue) : props.secretStringValue?.unsafeUnwrap() ?? props.secretStringBeta1?.secretValue(); const resource = new secretsmanager.CfnSecret(this, 'Resource', { description: props.description, kmsKeyId: props.encryptionKey && props.encryptionKey.keyArn, generateSecretString: props.generateSecretString ?? (secretString ? undefined : {}), secretString, name: this.physicalName, replicaRegions: Lazy.any({ produce: () => this.replicaRegions }, { omitEmptyArray: true }), }); resource.applyRemovalPolicy(props.removalPolicy, { default: RemovalPolicy.DESTROY, }); this.secretArn = this.getResourceArnAttribute(resource.ref, { service: 'secretsmanager', resource: 'secret', resourceName: this.physicalName, arnFormat: ArnFormat.COLON_RESOURCE_NAME, }); this.encryptionKey = props.encryptionKey; const parseOwnedSecretName = FeatureFlags.of(this).isEnabled(cxapi.SECRETS_MANAGER_PARSE_OWNED_SECRET_NAME); this.secretName = parseOwnedSecretName ? parseSecretNameForOwnedSecret(this, this.secretArn, props.secretName) : parseSecretName(this, this.secretArn); // @see https://docs.aws.amazon.com/kms/latest/developerguide/services-secrets-manager.html#asm-authz const principal = new kms.ViaServicePrincipal(`secretsmanager.${Stack.of(this).region}.amazonaws.com`, new iam.AccountPrincipal(Stack.of(this).account)); this.encryptionKey?.grantEncryptDecrypt(principal); this.encryptionKey?.grant(principal, 'kms:CreateGrant', 'kms:DescribeKey'); for (const replica of props.replicaRegions ?? []) { this.addReplicaRegion(replica.region, replica.encryptionKey); } this.excludeCharacters = props.generateSecretString?.excludeCharacters; } private resolveSecretObjectValue(secretObject: { [key: string]: SecretValue }): string { const resolvedObject: { [key: string]: string } = {}; for (const [key, value] of Object.entries(secretObject)) { resolvedObject[key] = value.unsafeUnwrap(); } return JSON.stringify(resolvedObject); } /** * Adds a target attachment to the secret. * * @returns an AttachedSecret * * @deprecated use `attach()` instead */ @MethodMetadata() public addTargetAttachment(id: string, options: AttachedSecretOptions): SecretTargetAttachment { return new SecretTargetAttachment(this, id, { secret: this, ...options, }); } /** * Adds a replica region for the secret * * @param region The name of the region * @param encryptionKey The customer-managed encryption key to use for encrypting the secret value. */ @MethodMetadata() public addReplicaRegion(region: string, encryptionKey?: kms.IKey): void { const stack = Stack.of(this); if (!Token.isUnresolved(stack.region) && !Token.isUnresolved(region) && region === stack.region) { throw new Error('Cannot add the region where this stack is deployed as a replica region.'); } this.replicaRegions.push({ region, kmsKeyId: encryptionKey?.keyArn, }); } } /** * A secret attachment target. */ export interface ISecretAttachmentTarget { /** * Renders the target specifications. */ asSecretAttachmentTarget(): SecretAttachmentTargetProps; } /** * The type of service or database that's being associated with the secret. */ export enum AttachmentTargetType { /** * AWS::RDS::DBInstance */ RDS_DB_INSTANCE = 'AWS::RDS::DBInstance', /** * A database instance * * @deprecated use RDS_DB_INSTANCE instead */ INSTANCE = 'deprecated_AWS::RDS::DBInstance', /** * AWS::RDS::DBCluster */ RDS_DB_CLUSTER = 'AWS::RDS::DBCluster', /** * A database cluster * * @deprecated use RDS_DB_CLUSTER instead */ CLUSTER = 'deprecated_AWS::RDS::DBCluster', /** * AWS::RDS::DBProxy */ RDS_DB_PROXY = 'AWS::RDS::DBProxy', /** * AWS::Redshift::Cluster */ REDSHIFT_CLUSTER = 'AWS::Redshift::Cluster', /** * AWS::DocDB::DBInstance */ DOCDB_DB_INSTANCE = 'AWS::DocDB::DBInstance', /** * AWS::DocDB::DBCluster */ DOCDB_DB_CLUSTER = 'AWS::DocDB::DBCluster', } /** * Attachment target specifications. */ export interface SecretAttachmentTargetProps { /** * The id of the target to attach the secret to. */ readonly targetId: string; /** * The type of the target to attach the secret to. */ readonly targetType: AttachmentTargetType; } /** * Options to add a secret attachment to a secret. */ export interface AttachedSecretOptions { /** * The target to attach the secret to. */ readonly target: ISecretAttachmentTarget; } /** * Construction properties for an AttachedSecret. */ export interface SecretTargetAttachmentProps extends AttachedSecretOptions { /** * The secret to attach to the target. */ readonly secret: ISecret; } export interface ISecretTargetAttachment extends ISecret { /** * Same as `secretArn` * * @attribute */ readonly secretTargetAttachmentSecretArn: string; } /** * An attached secret. */ export class SecretTargetAttachment extends SecretBase implements ISecretTargetAttachment { public static fromSecretTargetAttachmentSecretArn(scope: Construct, id: string, secretTargetAttachmentSecretArn: string): ISecretTargetAttachment { class Import extends SecretBase implements ISecretTargetAttachment { public encryptionKey?: kms.IKey | undefined; public secretArn = secretTargetAttachmentSecretArn; public secretTargetAttachmentSecretArn = secretTargetAttachmentSecretArn; public secretName = parseSecretName(scope, secretTargetAttachmentSecretArn); protected readonly autoCreatePolicy = false; } return new Import(scope, id); } public readonly encryptionKey?: kms.IKey; public readonly secretArn: string; public readonly secretName: string; /** * @attribute */ public readonly secretTargetAttachmentSecretArn: string; protected readonly autoCreatePolicy = true; private readonly attachedSecret: ISecret; constructor(scope: Construct, id: string, props: SecretTargetAttachmentProps) { super(scope, id); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); this.attachedSecret = props.secret; const attachment = new secretsmanager.CfnSecretTargetAttachment(this, 'Resource', { secretId: this.attachedSecret.secretArn, targetId: props.target.asSecretAttachmentTarget().targetId, targetType: attachmentTargetTypeToString(props.target.asSecretAttachmentTarget().targetType), }); this.encryptionKey = this.attachedSecret.encryptionKey; this.secretName = this.attachedSecret.secretName; // This allows to reference the secret after attachment (dependency). this.secretArn = attachment.ref; this.secretTargetAttachmentSecretArn = attachment.ref; } /** * Forward any additions to the resource policy to the original secret. * This is required because a secret can only have a single resource policy. * If we do not forward policy additions, a new policy resource is created using the secret attachment ARN. * This ends up being rejected by CloudFormation. */ @MethodMetadata() public addToResourcePolicy(statement: iam.PolicyStatement): iam.AddToResourcePolicyResult { if (FeatureFlags.of(this).isEnabled(cxapi.SECRETS_MANAGER_TARGET_ATTACHMENT_RESOURCE_POLICY)) { return this.attachedSecret.addToResourcePolicy(statement); } return super.addToResourcePolicy(statement); } } /** * Configuration to generate secrets such as passwords automatically. */ export interface SecretStringGenerator { /** * Specifies that the generated password shouldn't include uppercase letters. * * @default false */ readonly excludeUppercase?: boolean; /** * Specifies whether the generated password must include at least one of every allowed character type. * * @default true */ readonly requireEachIncludedType?: boolean; /** * Specifies that the generated password can include the space character. * * @default false */ readonly includeSpace?: boolean; /** * A string that includes characters that shouldn't be included in the generated password. The string can be a minimum * of ``0`` and a maximum of ``4096`` characters long. * * @default no exclusions */ readonly excludeCharacters?: string; /** * The desired length of the generated password. * * @default 32 */ readonly passwordLength?: number; /** * Specifies that the generated password shouldn't include punctuation characters. * * @default false */ readonly excludePunctuation?: boolean; /** * Specifies that the generated password shouldn't include lowercase letters. * * @default false */ readonly excludeLowercase?: boolean; /** * Specifies that the generated password shouldn't include digits. * * @default false */ readonly excludeNumbers?: boolean; /** * A properly structured JSON string that the generated password can be added to. The ``generateStringKey`` is * combined with the generated random string and inserted into the JSON structure that's specified by this parameter. * The merged JSON string is returned as the completed SecretString of the secret. If you specify ``secretStringTemplate`` * then ``generateStringKey`` must be also be specified. */ readonly secretStringTemplate?: string; /** * The JSON key name that's used to add the generated password to the JSON structure specified by the * ``secretStringTemplate`` parameter. If you specify ``generateStringKey`` then ``secretStringTemplate`` * must be also be specified. */ readonly generateStringKey?: string; } /** Parses the secret name from the ARN. */ function parseSecretName(construct: IConstruct, secretArn: string) { const resourceName = Stack.of(construct).splitArn(secretArn, ArnFormat.COLON_RESOURCE_NAME).resourceName; if (resourceName) { // Can't operate on the token to remove the SecretsManager suffix, so just return the full secret name if (Token.isUnresolved(resourceName)) { return resourceName; } // Secret resource names are in the format `${secretName}-${6-character SecretsManager suffix}` // If there is no hyphen (or 6-character suffix) assume no suffix was provided, and return the whole name. const lastHyphenIndex = resourceName.lastIndexOf('-'); const hasSecretsSuffix = lastHyphenIndex !== -1 && resourceName.slice(lastHyphenIndex + 1).length === 6; return hasSecretsSuffix ? resourceName.slice(0, lastHyphenIndex) : resourceName; } throw new Error('invalid ARN format; no secret name provided'); } /** * Parses the secret name from the ARN of an owned secret. With owned secrets we know a few things we don't with imported secrets: * - The ARN is guaranteed to be a full ARN, with suffix. * - The name -- if provided -- will tell us how many hyphens to expect in the final secret name. * - If the name is not provided, we know the format used by CloudFormation for auto-generated names. * * Note: This is done rather than just returning the secret name passed in by the user to keep the relationship * explicit between the Secret and wherever the secretName might be used (i.e., using Tokens). */ function parseSecretNameForOwnedSecret(construct: Construct, secretArn: string, secretName?: string) { const resourceName = Stack.of(construct).splitArn(secretArn, ArnFormat.COLON_RESOURCE_NAME).resourceName; if (!resourceName) { throw new Error('invalid ARN format; no secret name provided'); } // Secret name was explicitly provided, but is unresolved; best option is to use it directly. // If it came from another Secret, it should (hopefully) already be properly formatted. if (secretName && Token.isUnresolved(secretName)) { return secretName; } // If no secretName was provided, the name will be automatically generated by CloudFormation. // The autogenerated names have the form of `${logicalID}-${random}`. // Otherwise, we can use the existing secretName to determine how to parse the resulting resourceName. const secretNameHyphenatedSegments = secretName ? secretName.split('-').length : 2; // 2 => [0, 1] const segmentIndexes = [...new Array(secretNameHyphenatedSegments)].map((_, i) => i); // Create the secret name from the resource name by joining all the known segments together. // This should have the effect of stripping the final hyphen and SecretManager suffix. return Fn.join('-', segmentIndexes.map(i => Fn.select(i, Fn.split('-', resourceName)))); } /** Performs a best guess if an ARN is complete, based on if it ends with a 6-character suffix. */ function arnIsComplete(secretArn: string): boolean { return Token.isUnresolved(secretArn) || /-[a-z0-9]{6}$/i.test(secretArn); } /** * Mark all instances of 'Secret'. */ Object.defineProperty(Secret.prototype, SECRET_SYMBOL, { value: true, enumerable: false, writable: false, }); function attachmentTargetTypeToString(x: AttachmentTargetType): string { switch (x) { case AttachmentTargetType.RDS_DB_INSTANCE: case AttachmentTargetType.INSTANCE: return 'AWS::RDS::DBInstance'; case AttachmentTargetType.RDS_DB_CLUSTER: case AttachmentTargetType.CLUSTER: return 'AWS::RDS::DBCluster'; case AttachmentTargetType.RDS_DB_PROXY: return 'AWS::RDS::DBProxy'; case AttachmentTargetType.REDSHIFT_CLUSTER: return 'AWS::Redshift::Cluster'; case AttachmentTargetType.DOCDB_DB_INSTANCE: return 'AWS::DocDB::DBInstance'; case AttachmentTargetType.DOCDB_DB_CLUSTER: return 'AWS::DocDB::DBCluster'; } }