From 2e8219c7130845d35097f2a3e6238b984f924cb4 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 16 Jun 2022 20:37:11 +0200 Subject: [PATCH 1/3] feat(secretsmanager): exclude characters for hosted rotation Use the same default exclude characters as the one used in `@aws-cdk/aws-rds`. A subsequent PR should replace the secret rotation implementation in `@aws-cdk/aws-rds` with a hosted rotation now that exclude chars are supported. --- .../lib/rotation-schedule.ts | 16 +++++ ...k-integ-secret-hosted-rotation.assets.json | 19 ++++++ ...integ-secret-hosted-rotation.template.json | 1 + .../hosted-rotation.integ.snapshot/cdk.out | 2 +- .../hosted-rotation.integ.snapshot/integ.json | 4 +- .../manifest.json | 2 +- .../hosted-rotation.integ.snapshot/tree.json | 7 ++- .../test/rotation-schedule.test.ts | 61 +++++++++++++++++++ 8 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk-integ-secret-hosted-rotation.assets.json diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts b/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts index 673622f23887b..68e237274a4d5 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts @@ -7,6 +7,14 @@ import { Construct } from 'constructs'; import { ISecret } from './secret'; import { CfnRotationSchedule } from './secretsmanager.generated'; +/** + * The default set of characters we exclude from generated passwords for database users. + * It's a combination of characters that have a tendency to cause problems in shell scripts, + * some engine-specific characters (for example, Oracle doesn't like '@' in its passwords), + * and some that trip up other services, like DMS. + */ +const DEFAULT_PASSWORD_EXCLUDE_CHARS = " %+~`#$&*()|[]{}:;<>?!'/@\"\\"; + /** * Options to add a rotation schedule to a secret. */ @@ -162,6 +170,13 @@ export interface SingleUserHostedRotationOptions { * @default - the Vpc default strategy if not specified. */ readonly vpcSubnets?: ec2.SubnetSelection; + + /** + * A string of the characters that you don't want in the password + * + * @default " %+~`#$&*()|[]{}:;<>?!'/@\"\\" + */ + readonly excludeCharacters?: string, } /** @@ -292,6 +307,7 @@ export class HostedRotation implements ec2.IConnectable { rotationLambdaName: this.props.functionName, vpcSecurityGroupIds: this._connections?.securityGroups?.map(s => s.securityGroupId).join(','), vpcSubnetIds: this.props.vpc?.selectSubnets(this.props.vpcSubnets).subnetIds.join(','), + excludeCharacters: this.props.excludeCharacters ?? DEFAULT_PASSWORD_EXCLUDE_CHARS, }; } diff --git a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk-integ-secret-hosted-rotation.assets.json b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk-integ-secret-hosted-rotation.assets.json new file mode 100644 index 0000000000000..b9ac240730a18 --- /dev/null +++ b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk-integ-secret-hosted-rotation.assets.json @@ -0,0 +1,19 @@ +{ + "version": "20.0.0", + "files": { + "21f2d504aff41b7e9afcee22f05e0b68d6540a5cb042a49fac52a6b1c59ec035": { + "source": { + "path": "cdk-integ-secret-hosted-rotation.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21f2d504aff41b7e9afcee22f05e0b68d6540a5cb042a49fac52a6b1c59ec035.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk-integ-secret-hosted-rotation.template.json b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk-integ-secret-hosted-rotation.template.json index fd888fba0e85b..e9d1a93e86765 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk-integ-secret-hosted-rotation.template.json +++ b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk-integ-secret-hosted-rotation.template.json @@ -16,6 +16,7 @@ "Ref": "SecretA720EF05" }, "HostedRotationLambda": { + "ExcludeCharacters": " %+~`#$&*()|[]{}:;<>?!'/@\"\\", "RotationType": "MySQLSingleUser" }, "RotationRules": { diff --git a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk.out index 90bef2e09ad39..588d7b269d34f 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk.out @@ -1 +1 @@ -{"version":"17.0.0"} \ No newline at end of file +{"version":"20.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/integ.json b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/integ.json index bf99d8199afe4..1a8e6dc204e86 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/integ.json +++ b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/integ.json @@ -1,7 +1,7 @@ { - "version": "18.0.0", + "version": "20.0.0", "testCases": { - "aws-secretsmanager/test/integ.hosted-rotation": { + "integ.hosted-rotation": { "stacks": [ "cdk-integ-secret-hosted-rotation" ], diff --git a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/manifest.json index e6ed94cb242a8..f3c807a02c65a 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "17.0.0", + "version": "20.0.0", "artifacts": { "Tree": { "type": "cdk:tree", diff --git a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/tree.json b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/tree.json index b4e06e0b6f720..cb9a29311a424 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/tree.json +++ b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/tree.json @@ -8,8 +8,8 @@ "id": "Tree", "path": "Tree", "constructInfo": { - "fqn": "@aws-cdk/core.Construct", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.0.9" } }, "cdk-integ-secret-hosted-rotation": { @@ -48,7 +48,8 @@ "Ref": "SecretA720EF05" }, "hostedRotationLambda": { - "rotationType": "MySQLSingleUser" + "rotationType": "MySQLSingleUser", + "excludeCharacters": " %+~`#$&*()|[]{}:;<>?!'/@\"\\" }, "rotationRules": { "automaticallyAfterDays": 30 diff --git a/packages/@aws-cdk/aws-secretsmanager/test/rotation-schedule.test.ts b/packages/@aws-cdk/aws-secretsmanager/test/rotation-schedule.test.ts index 26f4ad5a753f5..0ff0941b901c2 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/rotation-schedule.test.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/rotation-schedule.test.ts @@ -230,6 +230,7 @@ describe('hosted rotation', () => { }, HostedRotationLambda: { RotationType: 'MySQLSingleUser', + ExcludeCharacters: " %+~`#$&*()|[]{}:;<>?!'/@\"\\", }, RotationRules: { AutomaticallyAfterDays: 30, @@ -514,6 +515,66 @@ describe('hosted rotation', () => { expect(() => hostedRotation.connections.allowToAnyIpv4(ec2.Port.allTraffic())) .toThrow(/Cannot use connections for a hosted rotation that is not deployed in a VPC/); }); + + test('can customize exclude characters', () => { + // GIVEN + const app = new cdk.App(); + stack = new cdk.Stack(app, 'TestStack'); + const secret = new secretsmanager.Secret(stack, 'Secret'); + + // WHEN + secret.addRotationSchedule('RotationSchedule', { + hostedRotation: secretsmanager.HostedRotation.mysqlSingleUser({ + excludeCharacters: '()', + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { + HostedRotationLambda: { + RotationType: 'MySQLSingleUser', + ExcludeCharacters: '()', + }, + }); + + expect(app.synth().getStackByName(stack.stackName).template).toEqual(expect.objectContaining({ + Transform: 'AWS::SecretsManager-2020-07-23', + })); + + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::ResourcePolicy', { + ResourcePolicy: { + Statement: [ + { + Action: 'secretsmanager:DeleteSecret', + Effect: 'Deny', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + SecretId: { + Ref: 'SecretA720EF05', + }, + }); + }); }); describe('manual rotations', () => { From a3d8fc1b251c387c3019fc25e687c7e8d2dc86a9 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 16 Jun 2022 20:49:50 +0200 Subject: [PATCH 2/3] README --- packages/@aws-cdk/aws-secretsmanager/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/@aws-cdk/aws-secretsmanager/README.md b/packages/@aws-cdk/aws-secretsmanager/README.md index 6f72a6bb44d2d..8d1718b656c16 100644 --- a/packages/@aws-cdk/aws-secretsmanager/README.md +++ b/packages/@aws-cdk/aws-secretsmanager/README.md @@ -145,6 +145,9 @@ secret.addRotationSchedule('RotationSchedule', { hostedRotation: myHostedRotatio dbConnections.allowDefaultPortFrom(myHostedRotation); ``` +Use the `excludeCharacters` option to customize the characters excluded +from the generated password when it is rotated. + See also [Automating secret creation in AWS CloudFormation](https://docs.aws.amazon.com/secretsmanager/latest/userguide/integrating_cloudformation.html). ## Rotating database credentials From 2a13c80a363086b5cc2a35d6cfcdf84781cc448c Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Thu, 16 Jun 2022 21:17:08 +0200 Subject: [PATCH 3/3] better default --- .../@aws-cdk/aws-secretsmanager/README.md | 7 +- .../lib/rotation-schedule.ts | 11 +- .../@aws-cdk/aws-secretsmanager/lib/secret.ts | 25 ++++ ...k-integ-secret-hosted-rotation.assets.json | 4 +- ...integ-secret-hosted-rotation.template.json | 65 +++++++++- .../manifest.json | 18 +++ .../hosted-rotation.integ.snapshot/tree.json | 115 +++++++++++++++++- .../test/integ.hosted-rotation.ts | 10 +- .../test/rotation-schedule.test.ts | 54 +++----- 9 files changed, 265 insertions(+), 44 deletions(-) diff --git a/packages/@aws-cdk/aws-secretsmanager/README.md b/packages/@aws-cdk/aws-secretsmanager/README.md index 8d1718b656c16..780bdc8e022e6 100644 --- a/packages/@aws-cdk/aws-secretsmanager/README.md +++ b/packages/@aws-cdk/aws-secretsmanager/README.md @@ -145,8 +145,11 @@ secret.addRotationSchedule('RotationSchedule', { hostedRotation: myHostedRotatio dbConnections.allowDefaultPortFrom(myHostedRotation); ``` -Use the `excludeCharacters` option to customize the characters excluded -from the generated password when it is rotated. +Use the `excludeCharacters` option to customize the characters excluded from +the generated password when it is rotated. By default, the rotation excludes +the same characters as the ones excluded for the secret. If none are defined +then the following set is used: ``% +~`#$&*()|[]{}:;<>?!'/@"\``. + See also [Automating secret creation in AWS CloudFormation](https://docs.aws.amazon.com/secretsmanager/latest/userguide/integrating_cloudformation.html). diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts b/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts index 68e237274a4d5..4174a56fa36a1 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts @@ -4,7 +4,7 @@ import * as kms from '@aws-cdk/aws-kms'; import * as lambda from '@aws-cdk/aws-lambda'; import { Duration, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { ISecret } from './secret'; +import { ISecret, Secret } from './secret'; import { CfnRotationSchedule } from './secretsmanager.generated'; /** @@ -174,7 +174,8 @@ export interface SingleUserHostedRotationOptions { /** * A string of the characters that you don't want in the password * - * @default " %+~`#$&*()|[]{}:;<>?!'/@\"\\" + * @default the same exclude characters as the ones used for the + * secret or " %+~`#$&*()|[]{}:;<>?!'/@\"\\" */ readonly excludeCharacters?: string, } @@ -299,6 +300,10 @@ export class HostedRotation implements ec2.IConnectable { this.masterSecret.denyAccountRootDelete(); } + const defaultExcludeCharacters = Secret.isSecret(secret) + ? secret.excludeCharacters ?? DEFAULT_PASSWORD_EXCLUDE_CHARS + : DEFAULT_PASSWORD_EXCLUDE_CHARS; + return { rotationType: this.type.name, kmsKeyArn: secret.encryptionKey?.keyArn, @@ -307,7 +312,7 @@ export class HostedRotation implements ec2.IConnectable { rotationLambdaName: this.props.functionName, vpcSecurityGroupIds: this._connections?.securityGroups?.map(s => s.securityGroupId).join(','), vpcSubnetIds: this.props.vpc?.selectSubnets(this.props.vpcSubnets).subnetIds.join(','), - excludeCharacters: this.props.excludeCharacters ?? DEFAULT_PASSWORD_EXCLUDE_CHARS, + excludeCharacters: this.props.excludeCharacters ?? defaultExcludeCharacters, }; } diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts index 7226d96901c3e..143607650d773 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts @@ -7,6 +7,8 @@ import { ResourcePolicy } from './policy'; import { RotationSchedule, RotationScheduleOptions } from './rotation-schedule'; import * as secretsmanager from './secretsmanager.generated'; +const SECRET_SYMBOL = Symbol.for('@aws-cdk/secretsmanager.Secret'); + /** * A secret in AWS Secrets Manager. */ @@ -446,6 +448,12 @@ abstract class SecretBase extends Resource implements ISecret { * 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 { @@ -553,6 +561,12 @@ export class Secret extends SecretBase { 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; @@ -609,6 +623,8 @@ export class Secret extends SecretBase { for (const replica of props.replicaRegions ?? []) { this.addReplicaRegion(replica.region, replica.encryptionKey); } + + this.excludeCharacters = props.generateSecretString?.excludeCharacters; } /** @@ -925,3 +941,12 @@ function parseSecretNameForOwnedSecret(construct: Construct, secretArn: string, 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, +}); diff --git a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk-integ-secret-hosted-rotation.assets.json b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk-integ-secret-hosted-rotation.assets.json index b9ac240730a18..6804fe656f48f 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk-integ-secret-hosted-rotation.assets.json +++ b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk-integ-secret-hosted-rotation.assets.json @@ -1,7 +1,7 @@ { "version": "20.0.0", "files": { - "21f2d504aff41b7e9afcee22f05e0b68d6540a5cb042a49fac52a6b1c59ec035": { + "80e7147ae17e29a7810c1890b8caa90a140f0089dcb2dce470bd13d88e5acc41": { "source": { "path": "cdk-integ-secret-hosted-rotation.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "21f2d504aff41b7e9afcee22f05e0b68d6540a5cb042a49fac52a6b1c59ec035.json", + "objectKey": "80e7147ae17e29a7810c1890b8caa90a140f0089dcb2dce470bd13d88e5acc41.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk-integ-secret-hosted-rotation.template.json b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk-integ-secret-hosted-rotation.template.json index e9d1a93e86765..954822d62f051 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk-integ-secret-hosted-rotation.template.json +++ b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/cdk-integ-secret-hosted-rotation.template.json @@ -1,5 +1,7 @@ { - "Transform": "AWS::SecretsManager-2020-07-23", + "Transform": [ + "AWS::SecretsManager-2020-07-23" + ], "Resources": { "SecretA720EF05": { "Type": "AWS::SecretsManager::Secret", @@ -59,6 +61,67 @@ "Ref": "SecretA720EF05" } } + }, + "CustomSecret5DC95D87": { + "Type": "AWS::SecretsManager::Secret", + "Properties": { + "GenerateSecretString": { + "ExcludeCharacters": "&@/" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomSecretScheduleDD99F351": { + "Type": "AWS::SecretsManager::RotationSchedule", + "Properties": { + "SecretId": { + "Ref": "CustomSecret5DC95D87" + }, + "HostedRotationLambda": { + "ExcludeCharacters": "&@/", + "RotationType": "MySQLSingleUser" + }, + "RotationRules": { + "AutomaticallyAfterDays": 30 + } + } + }, + "CustomSecretPolicy8E81D2EB": { + "Type": "AWS::SecretsManager::ResourcePolicy", + "Properties": { + "ResourcePolicy": { + "Statement": [ + { + "Action": "secretsmanager:DeleteSecret", + "Effect": "Deny", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "SecretId": { + "Ref": "CustomSecret5DC95D87" + } + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/manifest.json index f3c807a02c65a..d3fc87c9ee562 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/manifest.json @@ -32,6 +32,24 @@ "type": "aws:cdk:logicalId", "data": "SecretPolicy06C9821C" } + ], + "/cdk-integ-secret-hosted-rotation/CustomSecret/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomSecret5DC95D87" + } + ], + "/cdk-integ-secret-hosted-rotation/CustomSecret/Schedule/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomSecretScheduleDD99F351" + } + ], + "/cdk-integ-secret-hosted-rotation/CustomSecret/Policy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomSecretPolicy8E81D2EB" + } ] }, "displayName": "cdk-integ-secret-hosted-rotation" diff --git a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/tree.json b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/tree.json index cb9a29311a424..40269344c6d0f 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/tree.json +++ b/packages/@aws-cdk/aws-secretsmanager/test/hosted-rotation.integ.snapshot/tree.json @@ -9,7 +9,7 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.0.9" + "version": "10.1.33" } }, "cdk-integ-secret-hosted-rotation": { @@ -126,6 +126,119 @@ "fqn": "@aws-cdk/aws-secretsmanager.Secret", "version": "0.0.0" } + }, + "CustomSecret": { + "id": "CustomSecret", + "path": "cdk-integ-secret-hosted-rotation/CustomSecret", + "children": { + "Resource": { + "id": "Resource", + "path": "cdk-integ-secret-hosted-rotation/CustomSecret/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SecretsManager::Secret", + "aws:cdk:cloudformation:props": { + "generateSecretString": { + "excludeCharacters": "&@/" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-secretsmanager.CfnSecret", + "version": "0.0.0" + } + }, + "Schedule": { + "id": "Schedule", + "path": "cdk-integ-secret-hosted-rotation/CustomSecret/Schedule", + "children": { + "Resource": { + "id": "Resource", + "path": "cdk-integ-secret-hosted-rotation/CustomSecret/Schedule/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SecretsManager::RotationSchedule", + "aws:cdk:cloudformation:props": { + "secretId": { + "Ref": "CustomSecret5DC95D87" + }, + "hostedRotationLambda": { + "rotationType": "MySQLSingleUser", + "excludeCharacters": "&@/" + }, + "rotationRules": { + "automaticallyAfterDays": 30 + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-secretsmanager.CfnRotationSchedule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-secretsmanager.RotationSchedule", + "version": "0.0.0" + } + }, + "Policy": { + "id": "Policy", + "path": "cdk-integ-secret-hosted-rotation/CustomSecret/Policy", + "children": { + "Resource": { + "id": "Resource", + "path": "cdk-integ-secret-hosted-rotation/CustomSecret/Policy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SecretsManager::ResourcePolicy", + "aws:cdk:cloudformation:props": { + "resourcePolicy": { + "Statement": [ + { + "Action": "secretsmanager:DeleteSecret", + "Effect": "Deny", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "secretId": { + "Ref": "CustomSecret5DC95D87" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-secretsmanager.CfnResourcePolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-secretsmanager.ResourcePolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-secretsmanager.Secret", + "version": "0.0.0" + } } }, "constructInfo": { diff --git a/packages/@aws-cdk/aws-secretsmanager/test/integ.hosted-rotation.ts b/packages/@aws-cdk/aws-secretsmanager/test/integ.hosted-rotation.ts index 10109f91496cd..16a9e5352f4de 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/integ.hosted-rotation.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/integ.hosted-rotation.ts @@ -6,10 +6,18 @@ class TestStack extends cdk.Stack { super(scope, id); const secret = new secretsmanager.Secret(this, 'Secret'); - secret.addRotationSchedule('Schedule', { hostedRotation: secretsmanager.HostedRotation.mysqlSingleUser(), }); + + const customSecret = new secretsmanager.Secret(this, 'CustomSecret', { + generateSecretString: { + excludeCharacters: '&@/', + }, + }); + customSecret.addRotationSchedule('Schedule', { + hostedRotation: secretsmanager.HostedRotation.mysqlSingleUser(), + }); } } diff --git a/packages/@aws-cdk/aws-secretsmanager/test/rotation-schedule.test.ts b/packages/@aws-cdk/aws-secretsmanager/test/rotation-schedule.test.ts index ded4a6f0a46ef..9acb8991794ec 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/rotation-schedule.test.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/rotation-schedule.test.ts @@ -536,42 +536,28 @@ describe('hosted rotation', () => { ExcludeCharacters: '()', }, }); + }); - expect(app.synth().getStackByName(stack.stackName).template).toEqual(expect.objectContaining({ - Transform: 'AWS::SecretsManager-2020-07-23', - })); - - Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::ResourcePolicy', { - ResourcePolicy: { - Statement: [ - { - Action: 'secretsmanager:DeleteSecret', - Effect: 'Deny', - Principal: { - AWS: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':iam::', - { - Ref: 'AWS::AccountId', - }, - ':root', - ], - ], - }, - }, - Resource: '*', - }, - ], - Version: '2012-10-17', + test('exclude characters default to secret exclude characters', () => { + // GIVEN + const app = new cdk.App(); + stack = new cdk.Stack(app, 'TestStack'); + const secret = new secretsmanager.Secret(stack, 'Secret', { + generateSecretString: { + excludeCharacters: '[]', }, - SecretId: { - Ref: 'SecretA720EF05', + }); + + // WHEN + secret.addRotationSchedule('RotationSchedule', { + hostedRotation: secretsmanager.HostedRotation.mysqlSingleUser(), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::SecretsManager::RotationSchedule', { + HostedRotationLambda: { + RotationType: 'MySQLSingleUser', + ExcludeCharacters: '[]', }, }); });