From 0a39df7bb881edf68468d1abc8f4aea6ca008236 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 19 Mar 2019 12:46:30 +0100 Subject: [PATCH 01/16] feat(secretsmanager): add construct for RDS rotation single user Add construct for rotation schedule, secret target attachment and RDS rotation single user. --- .../@aws-cdk/aws-secretsmanager/lib/index.ts | 3 + .../lib/rds-rotation-single-user.ts | 204 ++++++++++++++++++ .../lib/rotation-schedule.ts | 49 +++++ .../lib/secret-target-attachment.ts | 71 ++++++ .../@aws-cdk/aws-secretsmanager/lib/secret.ts | 26 +++ .../@aws-cdk/aws-secretsmanager/package.json | 7 +- .../test/test.rds-rotation-single-user.ts | 199 +++++++++++++++++ .../test/test.rotation-schedule.ts | 42 ++++ .../test/test.secret-target-attachment.ts | 61 ++++++ 9 files changed, 661 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk/aws-secretsmanager/lib/rds-rotation-single-user.ts create mode 100644 packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts create mode 100644 packages/@aws-cdk/aws-secretsmanager/lib/secret-target-attachment.ts create mode 100644 packages/@aws-cdk/aws-secretsmanager/test/test.rds-rotation-single-user.ts create mode 100644 packages/@aws-cdk/aws-secretsmanager/test/test.rotation-schedule.ts create mode 100644 packages/@aws-cdk/aws-secretsmanager/test/test.secret-target-attachment.ts diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/index.ts b/packages/@aws-cdk/aws-secretsmanager/lib/index.ts index 0f425ff7d53ed..e4625f182fca0 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/index.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/index.ts @@ -1,5 +1,8 @@ export * from './secret'; export * from './secret-string'; +export * from './secret-target-attachment'; +export * from './rotation-schedule'; +export * from './rds-rotation-single-user'; // AWS::SecretsManager CloudFormation Resources: export * from './secretsmanager.generated'; diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/rds-rotation-single-user.ts b/packages/@aws-cdk/aws-secretsmanager/lib/rds-rotation-single-user.ts new file mode 100644 index 0000000000000..c7c1d8815986a --- /dev/null +++ b/packages/@aws-cdk/aws-secretsmanager/lib/rds-rotation-single-user.ts @@ -0,0 +1,204 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import lambda = require('@aws-cdk/aws-lambda'); +import serverless = require('@aws-cdk/aws-serverless'); +import cdk = require('@aws-cdk/cdk'); +import { ISecret } from './secret'; + +/** + * A serverless application location. + */ +export class ServerlessApplicationLocation { + public static readonly MariaDbRotationSingleUser = new ServerlessApplicationLocation('SecretsManagerRDSMariaDBRotationSingleUser', '1.0.46'); + public static readonly MysqlRotationSingleUser = new ServerlessApplicationLocation('SecretsManagerRDSMySQLRotationSingleUser', '1.0.74'); + public static readonly OracleRotationSingleUser = new ServerlessApplicationLocation('SecretsManagerRDSOracleRotationSingleUser', '1.0.45'); + public static readonly PostgresRotationSingleUser = new ServerlessApplicationLocation('SecretsManagerRDSPostgreSQLRotationSingleUser', '1.0.75'); + public static readonly SqlServerRotationSingleUser = new ServerlessApplicationLocation('SecretsManagerRDSSQLServerRotationSingleUser', '1.0.74'); + + public readonly applicationId: string; + public readonly semanticVersion: string; + + constructor(applicationId: string, semanticVersion: string) { + this.applicationId = `arn:aws:serverlessrepo:us-east-1:297356227824:applications/${applicationId}`; + this.semanticVersion = semanticVersion; + } +} + +/** + * The RDS database engine + */ +export enum RdsDatabaseEngine { + /** + * MariaDB + */ + MariaDb = 'mariadb', + + /** + * MySQL + */ + Mysql = 'mysql', + + /** + * Oracle + */ + Oracle = 'oracle', + + /** + * PostgreSQL + */ + Postgres = 'postgres', + + /** + * SQL Server + */ + SqlServer = 'sqlserver' +} + +/** + * Options to add single user rotation to a RDS instance or cluster. + */ +export interface RdsRotationSingleUserOptions { + /** + * Specifies the number of days after the previous rotation before + * Secrets Manager triggers the next automatic rotation. + * + * @default 30 days + */ + automaticallyAfterDays?: number; + + /** + * The location of the serverless application for the rotation. + * + * @default derived from the target's engine + */ + serverlessApplicationLocation?: ServerlessApplicationLocation +} + +/** + * Construction properties for a RdsRotationSingleUser. + */ +export interface RdsRotationSingleUserProps extends RdsRotationSingleUserOptions { + /** + * The secret to rotate. It must be a JSON string with the following format: + * { + * 'engine': , + * 'host': , + * 'username': , + * 'password': , + * 'dbname': , + * 'port': + * } + * + * This is typically the case for a secret referenced from an AWS::SecretsManager::SecretTargetAttachment + * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secrettargetattachment.html + */ + secret: ISecret; + + /** + * The database engine. Either `serverlessApplicationLocation` or `engine` must be specified. + * + * @default no engine specified + */ + engine?: RdsDatabaseEngine; + + /** + * The VPC where the Lambda rotation function will run. + */ + vpc: ec2.IVpcNetwork; + + /** + * The type of subnets in the VPC where the Lambda rotation function will run. + * + * @default private subnets + */ + vpcPlacement?: ec2.VpcPlacementStrategy; + + /** + * The connections object of the RDS database instance or cluster. + */ + connections: ec2.Connections; +} + +/** + * Single user secret rotation for a RDS database instance or cluster. + */ +export class RdsRotationSingleUser extends cdk.Construct { + constructor(scope: cdk.Construct, id: string, props: RdsRotationSingleUserProps) { + super(scope, id); + + if (!props.serverlessApplicationLocation && !props.engine) { + throw new Error('Either `serverlessApplicationLocation` or `engine` must be specified.'); + } + + if (!props.connections.defaultPortRange) { + throw new Error('The `connections` object must have a default port range.'); + } + + const rotationFunctionName = this.node.uniqueId; + + const securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { + vpc: props.vpc + }); + + const subnets = props.vpc.subnets(props.vpcPlacement); + + props.connections.allowDefaultPortFrom(securityGroup); + + const application = new serverless.CfnApplication(this, 'Resource', { + location: props.serverlessApplicationLocation || getApplicationLocation(props.engine), + parameters: { + endpoint: `https://secretsmanager.${this.node.stack.region}.${this.node.stack.urlSuffix}`, + functionName: rotationFunctionName, + vpcSecurityGroupIds: securityGroup.securityGroupId, + vpcSubnetIds: subnets.map(s => s.subnetId).join(',') + } + }); + + // Dummy import to reference this function in the rotation schedule + const rotationLambda = lambda.Function.import(this, 'RotationLambda', { + functionArn: this.node.stack.formatArn({ + service: 'lambda', + resource: 'function', + sep: ':', + resourceName: rotationFunctionName + }), + }); + + // Cannot use rotationLambda.addPermission because it currently does not + // return a cdk.Construct and we need to add a dependency. + const permission = new lambda.CfnPermission(this, 'Permission', { + action: 'lambda:InvokeFunction', + functionName: rotationFunctionName, + principal: `secretsmanager.${this.node.stack.urlSuffix}` + }); + permission.node.addDependency(application); // Add permission after application is deployed + + const rotationSchedule = props.secret.addRotationSchedule('RotationSchedule', { + rotationLambda, + automaticallyAfterDays: props.automaticallyAfterDays + }); + rotationSchedule.node.addDependency(permission); // Cannot rotate without permission + } +} + +/** + * Returns the location for the rotation single user application. + * + * @param engine the database engine + * @throws if the engine is not supported + */ +function getApplicationLocation(engine: string = ''): ServerlessApplicationLocation { + switch (engine) { + case RdsDatabaseEngine.MariaDb: + return ServerlessApplicationLocation.MariaDbRotationSingleUser; + case RdsDatabaseEngine.Mysql: + return ServerlessApplicationLocation.MysqlRotationSingleUser; + case RdsDatabaseEngine.Oracle: + return ServerlessApplicationLocation.OracleRotationSingleUser; + case RdsDatabaseEngine.Postgres: + return ServerlessApplicationLocation.PostgresRotationSingleUser; + case RdsDatabaseEngine.SqlServer: + return ServerlessApplicationLocation.SqlServerRotationSingleUser; + default: + throw new Error(`Engine ${engine} not supported for single user rotation.`); + } +} diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts b/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts new file mode 100644 index 0000000000000..5f3e77d63e634 --- /dev/null +++ b/packages/@aws-cdk/aws-secretsmanager/lib/rotation-schedule.ts @@ -0,0 +1,49 @@ +import lambda = require('@aws-cdk/aws-lambda'); +import cdk = require('@aws-cdk/cdk'); +import { ISecret } from './secret'; +import { CfnRotationSchedule } from './secretsmanager.generated'; + +/** + * Options to add a rotation schedule to a secret. + */ +export interface RotationScheduleOptions { + /** + * THe Lambda function that can rotate the secret. + */ + rotationLambda: lambda.IFunction; + + /** + * Specifies the number of days after the previous rotation before + * Secrets Manager triggers the next automatic rotation. + * + * @default 30 + */ + automaticallyAfterDays?: number; +} + +/** + * Construction properties for a RotationSchedule. + */ +export interface RotationScheduleProps extends RotationScheduleOptions { + /** + * The secret to rotate. + */ + secret: ISecret; +} + +/** + * A rotation schedule. + */ +export class RotationSchedule extends cdk.Construct { + constructor(scope: cdk.Construct, id: string, props: RotationScheduleProps) { + super(scope, id); + + new CfnRotationSchedule(this, 'Resource', { + secretId: props.secret.secretArn, + rotationLambdaArn: props.rotationLambda.functionArn, + rotationRules: { + automaticallyAfterDays: props.automaticallyAfterDays || 30 + } + }); + } +} diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret-target-attachment.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret-target-attachment.ts new file mode 100644 index 0000000000000..c28a58bf46897 --- /dev/null +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret-target-attachment.ts @@ -0,0 +1,71 @@ +import cdk = require('@aws-cdk/cdk'); +import { ISecret, Secret } from './secret'; +import { CfnSecretTargetAttachment } from './secretsmanager.generated'; + +/** + * The type of service or database that's being associated with the secret. + */ +export enum AttachmentTargetType { + /** + * A database instance + */ + Instance = 'AWS::RDS::DBInstance', + + /** + * A database cluster + */ + Cluster = 'AWS::RDS::DBCluster' +} + +/** + * Options to add a secret attachement to a secret. + */ +export interface SecretTargetAttachmentOptions { + /** + * The id of the target to attach the secret to. + */ + targetId: string; + + /** + * The type of the target to attach the secret to. + */ + targetType: AttachmentTargetType; +} + +/** + * Construction properties for a SecretAttachement. + */ +export interface SecretTargetAttachmentProps extends SecretTargetAttachmentOptions { + /** + * The secret to attach to the target. + */ + secret: ISecret; +} + +/** + * A secret target attachment. + */ +export class SecretTargetAttachment extends cdk.Construct { + /** + * The secret attached to the target. + */ + public readonly secret: ISecret; + + constructor(scope: cdk.Construct, id: string, props: SecretTargetAttachmentProps) { + super(scope, id); + + const attachment = new CfnSecretTargetAttachment(this, 'Resource', { + secretId: props.secret.secretArn, + targetId: props.targetId, + targetType: props.targetType + }); + + // This allows to reference the secret after attachment (dependency). When + // creating a secret for a RDS cluster or instance this is the secret that + // will be used as the input for the rotation. + this.secret = Secret.import(this, 'Secret', { + secretArn: attachment.secretTargetAttachmentSecretArn, + encryptionKey: props.secret.encryptionKey + }); + } +} diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts index 142592b8a6a29..074a378f9223e 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts @@ -1,7 +1,9 @@ import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); import cdk = require('@aws-cdk/cdk'); +import { RotationSchedule, RotationScheduleOptions } from './rotation-schedule'; import { SecretString } from './secret-string'; +import { SecretTargetAttachment, SecretTargetAttachmentOptions } from './secret-target-attachment'; import secretsmanager = require('./secretsmanager.generated'); /** @@ -51,6 +53,16 @@ export interface ISecret extends cdk.IConstruct { * stages is applied. */ grantRead(grantee: iam.IPrincipal, versionStages?: string[]): void; + + /** + * Adds a target attachment to the secret. + */ + addTargetAttachment(id: string, options: SecretTargetAttachmentOptions): SecretTargetAttachment; + + /** + * Adds a rotation schedule to the secret. + */ + addRotationSchedule(id: string, options: RotationScheduleOptions): RotationSchedule; } /** @@ -150,6 +162,20 @@ export abstract class SecretBase extends cdk.Construct implements ISecret { public jsonFieldValue(key: string): string { return this.secretString.jsonFieldValue(key); } + + public addTargetAttachment(id: string, options: SecretTargetAttachmentOptions): SecretTargetAttachment { + return new SecretTargetAttachment(this, id, { + secret: this, + ...options + }); + } + + public addRotationSchedule(id: string, options: RotationScheduleOptions): RotationSchedule { + return new RotationSchedule(this, id, { + secret: this, + ...options + }); + } } /** diff --git a/packages/@aws-cdk/aws-secretsmanager/package.json b/packages/@aws-cdk/aws-secretsmanager/package.json index 572469cbd8d52..1e6600c106c86 100644 --- a/packages/@aws-cdk/aws-secretsmanager/package.json +++ b/packages/@aws-cdk/aws-secretsmanager/package.json @@ -63,15 +63,20 @@ }, "dependencies": { "@aws-cdk/aws-iam": "^0.25.3", + "@aws-cdk/aws-ec2": "^0.25.3", "@aws-cdk/aws-kms": "^0.25.3", + "@aws-cdk/aws-lambda": "^0.25.3", + "@aws-cdk/aws-serverless": "^0.25.3", "@aws-cdk/cdk": "^0.25.3" }, "peerDependencies": { "@aws-cdk/aws-iam": "^0.25.3", + "@aws-cdk/aws-ec2": "^0.25.3", "@aws-cdk/aws-kms": "^0.25.3", + "@aws-cdk/aws-lambda": "^0.25.3", "@aws-cdk/cdk": "^0.25.3" }, "engines": { "node": ">= 8.10.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.rds-rotation-single-user.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.rds-rotation-single-user.ts new file mode 100644 index 0000000000000..f3d9959367715 --- /dev/null +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.rds-rotation-single-user.ts @@ -0,0 +1,199 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import secretsmanager = require('../lib'); + +// tslint:disable:object-literal-key-quotes + +export = { + 'create a rds rotation single user'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { + vpc, + }); + const connections = new ec2.Connections({ + defaultPortRange: new ec2.TcpPort(1521), + securityGroups: [securityGroup] + }); + const secret = new secretsmanager.Secret(stack, 'Secret'); + + // WHEN + new secretsmanager.RdsRotationSingleUser(stack, 'Rotation', { + secret, + engine: secretsmanager.RdsDatabaseEngine.Oracle, + vpc, + connections + }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + "IpProtocol": "tcp", + "Description": "from RotationSecurityGroup29D01037:1521", + "FromPort": 1521, + "GroupId": { + "Fn::GetAtt": [ + "SecurityGroupDD263621", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "RotationSecurityGroup3D2AB776", + "GroupId" + ] + }, + "ToPort": 1521 + })); + + expect(stack).to(haveResource('AWS::SecretsManager::RotationSchedule', { + "SecretId": { + "Ref": "SecretA720EF05" + }, + "RotationLambdaARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":lambda:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":function:Rotation" + ] + ] + }, + "RotationRules": { + "AutomaticallyAfterDays": 30 + } + })); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + "GroupDescription": "Rotation/SecurityGroup" + })); + + expect(stack).to(haveResource('AWS::Serverless::Application', { + "Location": { + "ApplicationId": "arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerRDSOracleRotationSingleUser", + "SemanticVersion": "1.0.45" + }, + "Parameters": { + "endpoint": { + "Fn::Join": [ + "", + [ + "https://secretsmanager.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + }, + "functionName": "Rotation", + "vpcSecurityGroupIds": { + "Fn::GetAtt": [ + "RotationSecurityGroup3D2AB776", + "GroupId" + ] + }, + "vpcSubnetIds": { + "Fn::Join": [ + "", + [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + ",", + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + }, + ",", + { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + ] + ] + } + } + })); + + expect(stack).to(haveResource('AWS::Lambda::Permission', { + "Action": "lambda:InvokeFunction", + "FunctionName": "Rotation", + "Principal": { + "Fn::Join": [ + "", + [ + "secretsmanager.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + })); + + test.done(); + }, + + 'throws when both application location and engine are not specified'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { + vpc, + }); + const connections = new ec2.Connections({ + defaultPortRange: new ec2.TcpPort(1521), + securityGroups: [securityGroup] + }); + const secret = new secretsmanager.Secret(stack, 'Secret'); + + // THEN + test.throws(() => new secretsmanager.RdsRotationSingleUser(stack, 'Rotation', { + secret, + vpc, + connections + }), /`serverlessApplicationLocation`.+`engine`/); + + test.done(); + }, + + 'throws when connections object has no default port range'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + const secret = new secretsmanager.Secret(stack, 'Secret'); + const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { + vpc, + }); + + // WHEN + const connections = new ec2.Connections({ + securityGroups: [securityGroup] + }); + + // THEN + test.throws(() => new secretsmanager.RdsRotationSingleUser(stack, 'Rotation', { + secret, + engine: secretsmanager.RdsDatabaseEngine.Mysql, + vpc, + connections + }), /`connections`/); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.rotation-schedule.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.rotation-schedule.ts new file mode 100644 index 0000000000000..c009d60e57722 --- /dev/null +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.rotation-schedule.ts @@ -0,0 +1,42 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import lambda = require('@aws-cdk/aws-lambda'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import secretsmanager = require('../lib'); + +export = { + 'create a rotation schedule'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const secret = new secretsmanager.Secret(stack, 'Secret'); + const rotationLambda = new lambda.Function(stack, 'Lambda', { + runtime: lambda.Runtime.NodeJS810, + code: lambda.Code.inline('export.handler = event => event;'), + handler: 'index.handler' + }); + + // WHEN + new secretsmanager.RotationSchedule(stack, 'RotationSchedule', { + secret, + rotationLambda + }); + + // THEN + expect(stack).to(haveResource('AWS::SecretsManager::RotationSchedule', { + SecretId: { + Ref: 'SecretA720EF05' + }, + RotationLambdaARN: { + 'Fn::GetAtt': [ + 'LambdaD247545B', + 'Arn' + ] + }, + RotationRules: { + AutomaticallyAfterDays: 30 + } + })); + + test.done(); + }, +}; diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.secret-target-attachment.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret-target-attachment.ts new file mode 100644 index 0000000000000..cd244c030a064 --- /dev/null +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.secret-target-attachment.ts @@ -0,0 +1,61 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import lambda = require('@aws-cdk/aws-lambda'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import secretsmanager = require('../lib'); + +export = { + 'create a secret attachment'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const secret = new secretsmanager.Secret(stack, 'Secret'); + + // WHEN + new secretsmanager.SecretTargetAttachment(stack, 'SecretTargetAttachment', { + secret, + targetId: 'instance', + targetType: secretsmanager.AttachmentTargetType.Instance + }); + + // THEN + expect(stack).to(haveResource('AWS::SecretsManager::SecretTargetAttachment', { + SecretId: { + Ref: 'SecretA720EF05' + }, + TargetId: 'instance', + TargetType: 'AWS::RDS::DBInstance' + })); + + test.done(); + }, + + 'add a rotation schedule to the secret returned by a target attachment'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const secret = new secretsmanager.Secret(stack, 'Secret'); + const attachment = new secretsmanager.SecretTargetAttachment(stack, 'SecretTargetAttachment', { + secret, + targetId: 'instance', + targetType: secretsmanager.AttachmentTargetType.Instance + }); + const rotationLambda = new lambda.Function(stack, 'Lambda', { + runtime: lambda.Runtime.NodeJS810, + code: lambda.Code.inline('export.handler = event => event;'), + handler: 'index.handler' + }); + + // WHEN + attachment.secret.addRotationSchedule('RotationSchedule', { + rotationLambda + }); + + // THEN + expect(stack).to(haveResource('AWS::SecretsManager::RotationSchedule', { + SecretId: { + Ref: 'SecretTargetAttachment0F6F22EB' // The secret returned by the attachment, not the secret itself. + } + })); + + test.done(); + } +}; From 1f1742bdff5f44e0c8af2bf1eaa9a9ab3067edf3 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 19 Mar 2019 15:59:14 +0100 Subject: [PATCH 02/16] Use a IConnectable for rds rotation --- .../lib/rds-rotation-single-user.ts | 10 +++++----- .../test/test.rds-rotation-single-user.ts | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/rds-rotation-single-user.ts b/packages/@aws-cdk/aws-secretsmanager/lib/rds-rotation-single-user.ts index c7c1d8815986a..3c2f2b7ee9910 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/rds-rotation-single-user.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/rds-rotation-single-user.ts @@ -113,9 +113,9 @@ export interface RdsRotationSingleUserProps extends RdsRotationSingleUserOptions vpcPlacement?: ec2.VpcPlacementStrategy; /** - * The connections object of the RDS database instance or cluster. + * The target database cluster or instance */ - connections: ec2.Connections; + target: ec2.IConnectable; } /** @@ -129,8 +129,8 @@ export class RdsRotationSingleUser extends cdk.Construct { throw new Error('Either `serverlessApplicationLocation` or `engine` must be specified.'); } - if (!props.connections.defaultPortRange) { - throw new Error('The `connections` object must have a default port range.'); + if (!props.target.connections.defaultPortRange) { + throw new Error('The `target` connections must have a default port range.'); } const rotationFunctionName = this.node.uniqueId; @@ -141,7 +141,7 @@ export class RdsRotationSingleUser extends cdk.Construct { const subnets = props.vpc.subnets(props.vpcPlacement); - props.connections.allowDefaultPortFrom(securityGroup); + props.target.connections.allowDefaultPortFrom(securityGroup); const application = new serverless.CfnApplication(this, 'Resource', { location: props.serverlessApplicationLocation || getApplicationLocation(props.engine), diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.rds-rotation-single-user.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.rds-rotation-single-user.ts index f3d9959367715..43b72dfb1bb4f 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.rds-rotation-single-user.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.rds-rotation-single-user.ts @@ -14,7 +14,7 @@ export = { const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc, }); - const connections = new ec2.Connections({ + const target = new ec2.Connections({ defaultPortRange: new ec2.TcpPort(1521), securityGroups: [securityGroup] }); @@ -25,7 +25,7 @@ export = { secret, engine: secretsmanager.RdsDatabaseEngine.Oracle, vpc, - connections + target }); // THEN @@ -156,7 +156,7 @@ export = { const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc, }); - const connections = new ec2.Connections({ + const target = new ec2.Connections({ defaultPortRange: new ec2.TcpPort(1521), securityGroups: [securityGroup] }); @@ -166,7 +166,7 @@ export = { test.throws(() => new secretsmanager.RdsRotationSingleUser(stack, 'Rotation', { secret, vpc, - connections + target }), /`serverlessApplicationLocation`.+`engine`/); test.done(); @@ -182,7 +182,7 @@ export = { }); // WHEN - const connections = new ec2.Connections({ + const target = new ec2.Connections({ securityGroups: [securityGroup] }); @@ -191,8 +191,8 @@ export = { secret, engine: secretsmanager.RdsDatabaseEngine.Mysql, vpc, - connections - }), /`connections`/); + target + }), /`target`.+default port range/); test.done(); } From bce612875bc6d1abc27e341ad73fc3c089b62efb Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 19 Mar 2019 16:13:57 +0100 Subject: [PATCH 03/16] Replace SecretTargetAttachment by AttachedSecret --- .../@aws-cdk/aws-secretsmanager/lib/index.ts | 1 - .../lib/secret-target-attachment.ts | 71 ----------- .../@aws-cdk/aws-secretsmanager/lib/secret.ts | 115 ++++++++++++++++-- .../test/test.secret-target-attachment.ts | 61 ---------- .../aws-secretsmanager/test/test.secret.ts | 59 +++++++++ 5 files changed, 161 insertions(+), 146 deletions(-) delete mode 100644 packages/@aws-cdk/aws-secretsmanager/lib/secret-target-attachment.ts delete mode 100644 packages/@aws-cdk/aws-secretsmanager/test/test.secret-target-attachment.ts diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/index.ts b/packages/@aws-cdk/aws-secretsmanager/lib/index.ts index e4625f182fca0..5328db068a1a3 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/index.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/index.ts @@ -1,6 +1,5 @@ export * from './secret'; export * from './secret-string'; -export * from './secret-target-attachment'; export * from './rotation-schedule'; export * from './rds-rotation-single-user'; diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret-target-attachment.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret-target-attachment.ts deleted file mode 100644 index c28a58bf46897..0000000000000 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret-target-attachment.ts +++ /dev/null @@ -1,71 +0,0 @@ -import cdk = require('@aws-cdk/cdk'); -import { ISecret, Secret } from './secret'; -import { CfnSecretTargetAttachment } from './secretsmanager.generated'; - -/** - * The type of service or database that's being associated with the secret. - */ -export enum AttachmentTargetType { - /** - * A database instance - */ - Instance = 'AWS::RDS::DBInstance', - - /** - * A database cluster - */ - Cluster = 'AWS::RDS::DBCluster' -} - -/** - * Options to add a secret attachement to a secret. - */ -export interface SecretTargetAttachmentOptions { - /** - * The id of the target to attach the secret to. - */ - targetId: string; - - /** - * The type of the target to attach the secret to. - */ - targetType: AttachmentTargetType; -} - -/** - * Construction properties for a SecretAttachement. - */ -export interface SecretTargetAttachmentProps extends SecretTargetAttachmentOptions { - /** - * The secret to attach to the target. - */ - secret: ISecret; -} - -/** - * A secret target attachment. - */ -export class SecretTargetAttachment extends cdk.Construct { - /** - * The secret attached to the target. - */ - public readonly secret: ISecret; - - constructor(scope: cdk.Construct, id: string, props: SecretTargetAttachmentProps) { - super(scope, id); - - const attachment = new CfnSecretTargetAttachment(this, 'Resource', { - secretId: props.secret.secretArn, - targetId: props.targetId, - targetType: props.targetType - }); - - // This allows to reference the secret after attachment (dependency). When - // creating a secret for a RDS cluster or instance this is the secret that - // will be used as the input for the rotation. - this.secret = Secret.import(this, 'Secret', { - secretArn: attachment.secretTargetAttachmentSecretArn, - encryptionKey: props.secret.encryptionKey - }); - } -} diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts index 074a378f9223e..ec75ffe873089 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts @@ -3,7 +3,6 @@ import kms = require('@aws-cdk/aws-kms'); import cdk = require('@aws-cdk/cdk'); import { RotationSchedule, RotationScheduleOptions } from './rotation-schedule'; import { SecretString } from './secret-string'; -import { SecretTargetAttachment, SecretTargetAttachmentOptions } from './secret-target-attachment'; import secretsmanager = require('./secretsmanager.generated'); /** @@ -54,11 +53,6 @@ export interface ISecret extends cdk.IConstruct { */ grantRead(grantee: iam.IPrincipal, versionStages?: string[]): void; - /** - * Adds a target attachment to the secret. - */ - addTargetAttachment(id: string, options: SecretTargetAttachmentOptions): SecretTargetAttachment; - /** * Adds a rotation schedule to the secret. */ @@ -163,13 +157,6 @@ export abstract class SecretBase extends cdk.Construct implements ISecret { return this.secretString.jsonFieldValue(key); } - public addTargetAttachment(id: string, options: SecretTargetAttachmentOptions): SecretTargetAttachment { - return new SecretTargetAttachment(this, id, { - secret: this, - ...options - }); - } - public addRotationSchedule(id: string, options: RotationScheduleOptions): RotationSchedule { return new RotationSchedule(this, id, { secret: this, @@ -210,6 +197,108 @@ export class Secret extends SecretBase { this.secretArn = resource.secretArn; } + /** + * Adds a target attachment to the secret. + * + * @returns an AttachedSecret + */ + public addTargetAttachment(id: string, options: AttachedSecretOptions): AttachedSecret { + return new AttachedSecret(this, id, { + secret: this, + ...options + }); + } + + public export(): SecretImportProps { + return { + encryptionKey: this.encryptionKey, + secretArn: this.secretArn, + }; + } +} + +/** + * 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 { + /** + * A database instance + */ + Instance = 'AWS::RDS::DBInstance', + + /** + * A database cluster + */ + Cluster = 'AWS::RDS::DBCluster' +} + +/** + * Attachment target specifications. + */ +export interface SecretAttachmentTargetProps { + /** + * The id of the target to attach the secret to. + */ + targetId: string; + + /** + * The type of the target to attach the secret to. + */ + targetType: AttachmentTargetType; +} + +/** + * Options to add a secret attachment to a secret. + */ +export interface AttachedSecretOptions { + /** + * The target to attach the secret to. + */ + target: ISecretAttachmentTarget; +} + +/** + * Construction properties for an AttachedSecret. + */ +export interface AttachedSecretProps extends AttachedSecretOptions { + /** + * The secret to attach to the target. + */ + secret: ISecret; +} + +/** + * An attached secret. + */ +export class AttachedSecret extends SecretBase implements ISecret { + public readonly encryptionKey?: kms.IEncryptionKey; + public readonly secretArn: string; + + constructor(scope: cdk.Construct, id: string, props: AttachedSecretProps) { + super(scope, id); + + const attachment = new secretsmanager.CfnSecretTargetAttachment(this, 'Resource', { + secretId: props.secret.secretArn, + targetId: props.target.asSecretAttachmentTarget().targetId, + targetType: props.target.asSecretAttachmentTarget().targetType + }); + + this.encryptionKey = props.secret.encryptionKey; + + // This allows to reference the secret after attachment (dependency). + this.secretArn = attachment.secretTargetAttachmentSecretArn; + } + public export(): SecretImportProps { return { encryptionKey: this.encryptionKey, diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.secret-target-attachment.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret-target-attachment.ts deleted file mode 100644 index cd244c030a064..0000000000000 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.secret-target-attachment.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { expect, haveResource } from '@aws-cdk/assert'; -import lambda = require('@aws-cdk/aws-lambda'); -import cdk = require('@aws-cdk/cdk'); -import { Test } from 'nodeunit'; -import secretsmanager = require('../lib'); - -export = { - 'create a secret attachment'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const secret = new secretsmanager.Secret(stack, 'Secret'); - - // WHEN - new secretsmanager.SecretTargetAttachment(stack, 'SecretTargetAttachment', { - secret, - targetId: 'instance', - targetType: secretsmanager.AttachmentTargetType.Instance - }); - - // THEN - expect(stack).to(haveResource('AWS::SecretsManager::SecretTargetAttachment', { - SecretId: { - Ref: 'SecretA720EF05' - }, - TargetId: 'instance', - TargetType: 'AWS::RDS::DBInstance' - })); - - test.done(); - }, - - 'add a rotation schedule to the secret returned by a target attachment'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - const secret = new secretsmanager.Secret(stack, 'Secret'); - const attachment = new secretsmanager.SecretTargetAttachment(stack, 'SecretTargetAttachment', { - secret, - targetId: 'instance', - targetType: secretsmanager.AttachmentTargetType.Instance - }); - const rotationLambda = new lambda.Function(stack, 'Lambda', { - runtime: lambda.Runtime.NodeJS810, - code: lambda.Code.inline('export.handler = event => event;'), - handler: 'index.handler' - }); - - // WHEN - attachment.secret.addRotationSchedule('RotationSchedule', { - rotationLambda - }); - - // THEN - expect(stack).to(haveResource('AWS::SecretsManager::RotationSchedule', { - SecretId: { - Ref: 'SecretTargetAttachment0F6F22EB' // The secret returned by the attachment, not the secret itself. - } - })); - - test.done(); - } -}; diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts index b45e670fec303..d95887d3f135f 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts @@ -1,6 +1,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); +import lambda = require('@aws-cdk/aws-lambda'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import secretsmanager = require('../lib'); @@ -255,6 +256,64 @@ export = { // THEN test.equals(secret.secretArn, secretArn); test.same(secret.encryptionKey, encryptionKey); + test.done(); + }, + + 'attached secret'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const secret = new secretsmanager.Secret(stack, 'Secret'); + const target: secretsmanager.ISecretAttachmentTarget = { + asSecretAttachmentTarget: () => ({ + targetId: 'instance', + targetType: secretsmanager.AttachmentTargetType.Instance + }) + }; + + // WHEN + secret.addTargetAttachment('AttachedSecret', { target }); + + // THEN + expect(stack).to(haveResource('AWS::SecretsManager::SecretTargetAttachment', { + SecretId: { + Ref: 'SecretA720EF05' + }, + TargetId: 'instance', + TargetType: 'AWS::RDS::DBInstance' + })); + + test.done(); + }, + + 'add a rotation schedule to an attached secret'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const secret = new secretsmanager.Secret(stack, 'Secret'); + const target: secretsmanager.ISecretAttachmentTarget = { + asSecretAttachmentTarget: () => ({ + targetId: 'cluster', + targetType: secretsmanager.AttachmentTargetType.Cluster + }) + }; + const attachedSecret = secret.addTargetAttachment('AttachedSecret', { target }); + const rotationLambda = new lambda.Function(stack, 'Lambda', { + runtime: lambda.Runtime.NodeJS810, + code: lambda.Code.inline('export.handler = event => event;'), + handler: 'index.handler' + }); + + // WHEN + attachedSecret.addRotationSchedule('RotationSchedule', { + rotationLambda + }); + + // THEN + expect(stack).to(haveResource('AWS::SecretsManager::RotationSchedule', { + SecretId: { + Ref: 'SecretAttachedSecret94145316' // The secret returned by the attachment, not the secret itself. + } + })); + test.done(); } }; From 81f2c5b2f35c86ebb4fe9883b39b7a1eef800a20 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 19 Mar 2019 16:30:04 +0100 Subject: [PATCH 04/16] Move rds rotation to aws-rds --- packages/@aws-cdk/aws-rds/lib/index.ts | 1 + .../lib/rotation-single-user.ts} | 32 +++++++++---------- packages/@aws-cdk/aws-rds/package.json | 3 +- .../test/test.rds-rotation-single-user.ts | 13 ++++---- .../@aws-cdk/aws-secretsmanager/lib/index.ts | 1 - 5 files changed, 26 insertions(+), 24 deletions(-) rename packages/@aws-cdk/{aws-secretsmanager/lib/rds-rotation-single-user.ts => aws-rds/lib/rotation-single-user.ts} (88%) rename packages/@aws-cdk/{aws-secretsmanager => aws-rds}/test/test.rds-rotation-single-user.ts (92%) diff --git a/packages/@aws-cdk/aws-rds/lib/index.ts b/packages/@aws-cdk/aws-rds/lib/index.ts index 4a0f2ed04ee88..bfc8288a8f6cf 100644 --- a/packages/@aws-cdk/aws-rds/lib/index.ts +++ b/packages/@aws-cdk/aws-rds/lib/index.ts @@ -3,6 +3,7 @@ export * from './cluster-ref'; export * from './instance'; export * from './props'; export * from './cluster-parameter-group'; +export * from './rotation-single-user'; // AWS::RDS CloudFormation Resources: export * from './rds.generated'; diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/rds-rotation-single-user.ts b/packages/@aws-cdk/aws-rds/lib/rotation-single-user.ts similarity index 88% rename from packages/@aws-cdk/aws-secretsmanager/lib/rds-rotation-single-user.ts rename to packages/@aws-cdk/aws-rds/lib/rotation-single-user.ts index 3c2f2b7ee9910..5f269534334e3 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/rds-rotation-single-user.ts +++ b/packages/@aws-cdk/aws-rds/lib/rotation-single-user.ts @@ -1,8 +1,8 @@ import ec2 = require('@aws-cdk/aws-ec2'); import lambda = require('@aws-cdk/aws-lambda'); +import secretsmanager = require('@aws-cdk/aws-secretsmanager'); import serverless = require('@aws-cdk/aws-serverless'); import cdk = require('@aws-cdk/cdk'); -import { ISecret } from './secret'; /** * A serverless application location. @@ -26,7 +26,7 @@ export class ServerlessApplicationLocation { /** * The RDS database engine */ -export enum RdsDatabaseEngine { +export enum DatabaseEngine { /** * MariaDB */ @@ -54,9 +54,9 @@ export enum RdsDatabaseEngine { } /** - * Options to add single user rotation to a RDS instance or cluster. + * Options to add single user rotation to a database instance or cluster. */ -export interface RdsRotationSingleUserOptions { +export interface RotationSingleUserOptions { /** * Specifies the number of days after the previous rotation before * Secrets Manager triggers the next automatic rotation. @@ -74,9 +74,9 @@ export interface RdsRotationSingleUserOptions { } /** - * Construction properties for a RdsRotationSingleUser. + * Construction properties for a RotationSingleUser. */ -export interface RdsRotationSingleUserProps extends RdsRotationSingleUserOptions { +export interface RotationSingleUserProps extends RotationSingleUserOptions { /** * The secret to rotate. It must be a JSON string with the following format: * { @@ -91,14 +91,14 @@ export interface RdsRotationSingleUserProps extends RdsRotationSingleUserOptions * This is typically the case for a secret referenced from an AWS::SecretsManager::SecretTargetAttachment * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secrettargetattachment.html */ - secret: ISecret; + secret: secretsmanager.ISecret; /** * The database engine. Either `serverlessApplicationLocation` or `engine` must be specified. * * @default no engine specified */ - engine?: RdsDatabaseEngine; + engine?: DatabaseEngine; /** * The VPC where the Lambda rotation function will run. @@ -119,10 +119,10 @@ export interface RdsRotationSingleUserProps extends RdsRotationSingleUserOptions } /** - * Single user secret rotation for a RDS database instance or cluster. + * Single user secret rotation for a database instance or cluster. */ -export class RdsRotationSingleUser extends cdk.Construct { - constructor(scope: cdk.Construct, id: string, props: RdsRotationSingleUserProps) { +export class RotationSingleUser extends cdk.Construct { + constructor(scope: cdk.Construct, id: string, props: RotationSingleUserProps) { super(scope, id); if (!props.serverlessApplicationLocation && !props.engine) { @@ -188,15 +188,15 @@ export class RdsRotationSingleUser extends cdk.Construct { */ function getApplicationLocation(engine: string = ''): ServerlessApplicationLocation { switch (engine) { - case RdsDatabaseEngine.MariaDb: + case DatabaseEngine.MariaDb: return ServerlessApplicationLocation.MariaDbRotationSingleUser; - case RdsDatabaseEngine.Mysql: + case DatabaseEngine.Mysql: return ServerlessApplicationLocation.MysqlRotationSingleUser; - case RdsDatabaseEngine.Oracle: + case DatabaseEngine.Oracle: return ServerlessApplicationLocation.OracleRotationSingleUser; - case RdsDatabaseEngine.Postgres: + case DatabaseEngine.Postgres: return ServerlessApplicationLocation.PostgresRotationSingleUser; - case RdsDatabaseEngine.SqlServer: + case DatabaseEngine.SqlServer: return ServerlessApplicationLocation.SqlServerRotationSingleUser; default: throw new Error(`Engine ${engine} not supported for single user rotation.`); diff --git a/packages/@aws-cdk/aws-rds/package.json b/packages/@aws-cdk/aws-rds/package.json index 39fa61530a2fa..5e9d0562d1ad1 100644 --- a/packages/@aws-cdk/aws-rds/package.json +++ b/packages/@aws-cdk/aws-rds/package.json @@ -64,6 +64,7 @@ "@aws-cdk/aws-ec2": "^0.25.3", "@aws-cdk/aws-iam": "^0.25.3", "@aws-cdk/aws-kms": "^0.25.3", + "@aws-cdk/aws-secretsmanager": "^0.25.3", "@aws-cdk/cdk": "^0.25.3" }, "homepage": "https://github.com/awslabs/aws-cdk", @@ -74,4 +75,4 @@ "engines": { "node": ">= 8.10.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.rds-rotation-single-user.ts b/packages/@aws-cdk/aws-rds/test/test.rds-rotation-single-user.ts similarity index 92% rename from packages/@aws-cdk/aws-secretsmanager/test/test.rds-rotation-single-user.ts rename to packages/@aws-cdk/aws-rds/test/test.rds-rotation-single-user.ts index 43b72dfb1bb4f..5e3abe2e51cf2 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.rds-rotation-single-user.ts +++ b/packages/@aws-cdk/aws-rds/test/test.rds-rotation-single-user.ts @@ -1,8 +1,9 @@ import { expect, haveResource } from '@aws-cdk/assert'; import ec2 = require('@aws-cdk/aws-ec2'); +import secretsmanager = require('@aws-cdk/aws-secretsmanager'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; -import secretsmanager = require('../lib'); +import rds = require('../lib'); // tslint:disable:object-literal-key-quotes @@ -21,9 +22,9 @@ export = { const secret = new secretsmanager.Secret(stack, 'Secret'); // WHEN - new secretsmanager.RdsRotationSingleUser(stack, 'Rotation', { + new rds.RotationSingleUser(stack, 'Rotation', { secret, - engine: secretsmanager.RdsDatabaseEngine.Oracle, + engine: rds.DatabaseEngine.Oracle, vpc, target }); @@ -163,7 +164,7 @@ export = { const secret = new secretsmanager.Secret(stack, 'Secret'); // THEN - test.throws(() => new secretsmanager.RdsRotationSingleUser(stack, 'Rotation', { + test.throws(() => new rds.RotationSingleUser(stack, 'Rotation', { secret, vpc, target @@ -187,9 +188,9 @@ export = { }); // THEN - test.throws(() => new secretsmanager.RdsRotationSingleUser(stack, 'Rotation', { + test.throws(() => new rds.RotationSingleUser(stack, 'Rotation', { secret, - engine: secretsmanager.RdsDatabaseEngine.Mysql, + engine: rds.DatabaseEngine.Mysql, vpc, target }), /`target`.+default port range/); diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/index.ts b/packages/@aws-cdk/aws-secretsmanager/lib/index.ts index 5328db068a1a3..ab6379c2d85a8 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/index.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/index.ts @@ -1,7 +1,6 @@ export * from './secret'; export * from './secret-string'; export * from './rotation-schedule'; -export * from './rds-rotation-single-user'; // AWS::SecretsManager CloudFormation Resources: export * from './secretsmanager.generated'; From 6554f2fb3edbbac3b6236b749fc9c5863d027bbd Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 19 Mar 2019 16:48:25 +0100 Subject: [PATCH 05/16] Implement ISecretAttachmentTarget for cluster --- packages/@aws-cdk/aws-rds/lib/cluster-ref.ts | 3 +- packages/@aws-cdk/aws-rds/lib/cluster.ts | 58 +++++++++++++++++++- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/lib/cluster-ref.ts b/packages/@aws-cdk/aws-rds/lib/cluster-ref.ts index 3a6803c587ecb..6bbb799f1e765 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster-ref.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster-ref.ts @@ -1,10 +1,11 @@ import ec2 = require('@aws-cdk/aws-ec2'); +import secretsmanager = require('@aws-cdk/aws-secretsmanager'); import cdk = require('@aws-cdk/cdk'); /** * Create a clustered database with a given number of instances. */ -export interface IDatabaseCluster extends cdk.IConstruct, ec2.IConnectable { +export interface IDatabaseCluster extends cdk.IConstruct, ec2.IConnectable, secretsmanager.ISecretAttachmentTarget { /** * Identifier of the cluster */ diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index 5510bfafebc1c..d136c3a534320 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -1,4 +1,5 @@ import ec2 = require('@aws-cdk/aws-ec2'); +import secretsmanager = require('@aws-cdk/aws-secretsmanager'); import cdk = require('@aws-cdk/cdk'); import { IClusterParameterGroup } from './cluster-parameter-group'; import { DatabaseClusterImportProps, Endpoint, IDatabaseCluster } from './cluster-ref'; @@ -91,9 +92,9 @@ export interface DatabaseClusterProps { } /** - * Create a clustered database with a given number of instances. + * A new or imported clustered database. */ -export class DatabaseCluster extends cdk.Construct implements IDatabaseCluster { +export abstract class DatabaseClusterBase extends cdk.Construct implements IDatabaseCluster { /** * Import an existing DatabaseCluster from properties */ @@ -101,6 +102,57 @@ export class DatabaseCluster extends cdk.Construct implements IDatabaseCluster { return new ImportedDatabaseCluster(scope, id, props); } + /** + * Identifier of the cluster + */ + public abstract readonly clusterIdentifier: string; + /** + * Identifiers of the replicas + */ + public abstract readonly instanceIdentifiers: string[]; + + /** + * The endpoint to use for read/write operations + */ + public abstract readonly clusterEndpoint: Endpoint; + + /** + * Endpoint to use for load-balanced read-only operations. + */ + public abstract readonly readerEndpoint: Endpoint; + + /** + * Endpoints which address each individual replica. + */ + public abstract readonly instanceEndpoints: Endpoint[]; + + /** + * Access to the network connections + */ + public abstract readonly connections: ec2.Connections; + + /** + * Security group identifier of this database + */ + public abstract readonly securityGroupId: string; + + public abstract export(): DatabaseClusterImportProps; + + /** + * Renders the secret attachment target specifications. + */ + public asSecretAttachmentTarget(): secretsmanager.SecretAttachmentTargetProps { + return { + targetId: this.clusterIdentifier, + targetType: secretsmanager.AttachmentTargetType.Cluster + }; + } +} + +/** + * Create a clustered database with a given number of instances. + */ +export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseCluster { /** * Identifier of the cluster */ @@ -248,7 +300,7 @@ function databaseInstanceType(instanceType: ec2.InstanceType) { /** * An imported Database Cluster */ -class ImportedDatabaseCluster extends cdk.Construct implements IDatabaseCluster { +class ImportedDatabaseCluster extends DatabaseClusterBase implements IDatabaseCluster { /** * Default port to connect to this database */ From 2a99fb4a6baa138882f3dfe29fb5a08193259015 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 19 Mar 2019 17:06:58 +0100 Subject: [PATCH 06/16] Update secretsmanager README --- packages/@aws-cdk/aws-secretsmanager/README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-secretsmanager/README.md b/packages/@aws-cdk/aws-secretsmanager/README.md index a5c71b8a2b6f1..3acc6c5c1f8a4 100644 --- a/packages/@aws-cdk/aws-secretsmanager/README.md +++ b/packages/@aws-cdk/aws-secretsmanager/README.md @@ -28,4 +28,19 @@ const secret = Secret.import(scope, 'ImportedSecret', { ``` SecretsManager secret values can only be used in select set of properties. For the -list of properties, see [the CloudFormation Dynamic References documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.htm). \ No newline at end of file +list of properties, see [the CloudFormation Dynamic References documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.htm). + +### Rotating a Secret +A rotation schedule can be added to a Secret: +```ts +const fn = new lambda.Function(...); +const secret = new secretsManager.Secret(this, 'Secret'); + +secret.addRotationSchedule('RotationSchedule', { + rotationLambda: fn, + automaticallyAfterDays: 15 +}); +``` +See [Overview of the Lambda Rotation Function](https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotating-secrets-lambda-function-overview.html) on how to implement a Lambda Rotation Function. + +For RDS credentials rotation, see [aws-rds](https://github.com/awslabs/aws-cdk/blob/master/packages/%40aws-cdk/aws-rds/README.md). From 87cf2491751f7defe38b8d86bfab1c266b1bdd51 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 19 Mar 2019 18:01:55 +0100 Subject: [PATCH 07/16] Add secret and rotation to cluster --- packages/@aws-cdk/aws-rds/lib/cluster.ts | 83 +++++++++++++++++- .../@aws-cdk/aws-rds/lib/database-secret.ts | 39 +++++++++ packages/@aws-cdk/aws-rds/lib/index.ts | 1 + packages/@aws-cdk/aws-rds/lib/props.ts | 15 +++- .../@aws-cdk/aws-rds/test/test.cluster.ts | 57 +++++++++++++ .../test/test.rds-rotation-single-user.ts | 85 +++++++++++++------ 6 files changed, 246 insertions(+), 34 deletions(-) create mode 100644 packages/@aws-cdk/aws-rds/lib/database-secret.ts diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index d136c3a534320..c17cbd8a54f4d 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -3,8 +3,10 @@ import secretsmanager = require('@aws-cdk/aws-secretsmanager'); import cdk = require('@aws-cdk/cdk'); import { IClusterParameterGroup } from './cluster-parameter-group'; import { DatabaseClusterImportProps, Endpoint, IDatabaseCluster } from './cluster-ref'; +import { DatabaseSecret } from './database-secret'; import { BackupProps, DatabaseClusterEngine, InstanceProps, Login } from './props'; import { CfnDBCluster, CfnDBInstance, CfnDBSubnetGroup } from './rds.generated'; +import { DatabaseEngine, RotationSingleUser, RotationSingleUserOptions } from './rotation-single-user'; /** * Properties for a new database cluster @@ -188,10 +190,33 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu */ public readonly securityGroupId: string; + /** + * The secret attached to this cluster + */ + public readonly secret?: secretsmanager.ISecret; + + /** + * The database engine of this cluster + */ + public readonly engine: DatabaseClusterEngine; + + /** + * The VPC where the DB subnet group is created. + */ + public readonly vpc: ec2.IVpcNetwork; + + /** + * The subnets used by the DB subnet group. + */ + public readonly vpcPlacement?: ec2.VpcPlacementStrategy; + constructor(scope: cdk.Construct, id: string, props: DatabaseClusterProps) { super(scope, id); - const subnets = props.instanceProps.vpc.subnets(props.instanceProps.vpcPlacement); + this.vpc = props.instanceProps.vpc; + this.vpcPlacement = props.instanceProps.vpcPlacement; + + const subnets = this.vpc.subnets(this.vpcPlacement); // Cannot test whether the subnets are in different AZs, but at least we can test the amount. if (subnets.length < 2) { @@ -210,17 +235,27 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu }); this.securityGroupId = securityGroup.securityGroupId; + let secret; + if (!props.masterUser.password) { + secret = new DatabaseSecret(this, 'Secret', { + username: props.masterUser.username, + encryptionKey: props.masterUser.kmsKey + }); + } + + this.engine = props.engine; + const cluster = new CfnDBCluster(this, 'Resource', { // Basic - engine: props.engine, + engine: this.engine, dbClusterIdentifier: props.clusterIdentifier, dbSubnetGroupName: subnetGroup.ref, vpcSecurityGroupIds: [this.securityGroupId], port: props.port, dbClusterParameterGroupName: props.parameterGroup && props.parameterGroup.parameterGroupName, // Admin - masterUsername: props.masterUser.username, - masterUserPassword: props.masterUser.password, + masterUsername: secret ? secret.jsonFieldValue('username') : props.masterUser.username, + masterUserPassword: secret ? secret.jsonFieldValue('password') : props.masterUser.password, backupRetentionPeriod: props.backup && props.backup.retentionDays, preferredBackupWindow: props.backup && props.backup.preferredWindow, preferredMaintenanceWindow: props.preferredMaintenanceWindow, @@ -234,6 +269,12 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu this.clusterEndpoint = new Endpoint(cluster.dbClusterEndpointAddress, cluster.dbClusterEndpointPort); this.readerEndpoint = new Endpoint(cluster.dbClusterReadEndpointAddress, cluster.dbClusterEndpointPort); + if (secret) { + this.secret = secret.addTargetAttachment('AttachedSecret', { + target: this + }); + } + const instanceCount = props.instances != null ? props.instances : 2; if (instanceCount < 1) { throw new Error('At least one instance is required'); @@ -272,6 +313,23 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu this.connections = new ec2.Connections({ securityGroups: [securityGroup], defaultPortRange }); } + /** + * Adds the single user rotation of the master password to this cluster. + */ + public addRotationSingleUser(id: string, options: RotationSingleUserOptions = {}): RotationSingleUser { + if (!this.secret) { + throw new Error('Cannot add single user rotation for a cluster without secret.'); + } + return new RotationSingleUser(this, id, { + secret: this.secret, + engine: toDatabaseEngine(this.engine), + vpc: this.vpc, + vpcPlacement: this.vpcPlacement, + target: this, + ...options + }); + } + /** * Export a Database Cluster for importing in another stack */ @@ -360,3 +418,20 @@ class ImportedDatabaseCluster extends DatabaseClusterBase implements IDatabaseCl return this.props; } } + +/** + * Transforms a DatbaseClusterEngine to a DatabaseEngine. + * + * @param engine the engine to transform + */ +function toDatabaseEngine(engine: DatabaseClusterEngine): DatabaseEngine { + switch (engine) { + case DatabaseClusterEngine.Aurora: + case DatabaseClusterEngine.AuroraMysql: + return DatabaseEngine.Mysql; + case DatabaseClusterEngine.AuroraPostgresql: + return DatabaseEngine.Postgres; + default: + throw new Error('Unkown engine'); + } +} diff --git a/packages/@aws-cdk/aws-rds/lib/database-secret.ts b/packages/@aws-cdk/aws-rds/lib/database-secret.ts new file mode 100644 index 0000000000000..356fe72a0abb4 --- /dev/null +++ b/packages/@aws-cdk/aws-rds/lib/database-secret.ts @@ -0,0 +1,39 @@ +import kms = require('@aws-cdk/aws-kms'); +import secretsmanager = require('@aws-cdk/aws-secretsmanager'); +import cdk = require('@aws-cdk/cdk'); + +/** + * Construction properties for a DatabaseSecret. + */ +export interface DatabaseSecretProps { + /** + * The username. + * + * @default admin + */ + username?: string; + + /** + * The KMS key to use to encrypt the secret. + * + * @default default master key + */ + encryptionKey?: kms.IEncryptionKey; +} + +/** + * A database secret. + */ +export class DatabaseSecret extends secretsmanager.Secret { + constructor(scope: cdk.Construct, id: string, props: DatabaseSecretProps = {}) { + super(scope, id, { + encryptionKey: props.encryptionKey, + generateSecretString: ({ + passwordLength: 30, // Oracle password cannot have more than 30 characters + secretStringTemplate: JSON.stringify({ username: props.username || 'admin' }), + generateStringKey: 'password', + excludeCharacters: '"@/\\' + }) as secretsmanager.TemplatedSecretStringGenerator + }); + } +} diff --git a/packages/@aws-cdk/aws-rds/lib/index.ts b/packages/@aws-cdk/aws-rds/lib/index.ts index bfc8288a8f6cf..290e0153e4c74 100644 --- a/packages/@aws-cdk/aws-rds/lib/index.ts +++ b/packages/@aws-cdk/aws-rds/lib/index.ts @@ -4,6 +4,7 @@ export * from './instance'; export * from './props'; export * from './cluster-parameter-group'; export * from './rotation-single-user'; +export * from './database-secret'; // AWS::RDS CloudFormation Resources: export * from './rds.generated'; diff --git a/packages/@aws-cdk/aws-rds/lib/props.ts b/packages/@aws-cdk/aws-rds/lib/props.ts index 366260c82791f..68925721034c1 100644 --- a/packages/@aws-cdk/aws-rds/lib/props.ts +++ b/packages/@aws-cdk/aws-rds/lib/props.ts @@ -1,4 +1,5 @@ import ec2 = require('@aws-cdk/aws-ec2'); +import kms = require('@aws-cdk/aws-kms'); /** * The engine for the database cluster @@ -69,10 +70,18 @@ export interface Login { /** * Password * - * Do not put passwords in your CDK code directly. Import it from a Stack - * Parameter or the SSM Parameter Store instead. + * Do not put passwords in your CDK code directly. + * + * @default a Secrets Manager generated password + */ + password?: string; + + /** + * KMS encryption key to encrypt the generated secret. + * + * @default default master key */ - password: string; + kmsKey?: kms.IEncryptionKey; } /** diff --git a/packages/@aws-cdk/aws-rds/test/test.cluster.ts b/packages/@aws-cdk/aws-rds/test/test.cluster.ts index e36c960e30579..7b1760b8a085b 100644 --- a/packages/@aws-cdk/aws-rds/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.cluster.ts @@ -175,6 +175,63 @@ export = { // THEN test.deepEqual(stack.node.resolve(exported), { parameterGroupName: { 'Fn::ImportValue': 'ParamsParameterGroupNameA6B808D7' } }); test.deepEqual(stack.node.resolve(imported.parameterGroupName), { 'Fn::ImportValue': 'ParamsParameterGroupNameA6B808D7' }); + test.done(); + }, + + 'creates a secret when master credentials are not specified'(test: Test) { + // GIVEN + const stack = testStack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AuroraMysql, + masterUser: { + username: 'admin' + }, + instanceProps: { + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), + vpc + } + }); + + // THEN + expect(stack).to(haveResource('AWS::RDS::DBCluster', { + MasterUsername: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'DatabaseSecret3B817195' + }, + ':SecretString:username::}}' + ] + ] + }, + MasterUserPassword: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'DatabaseSecret3B817195' + }, + ':SecretString:password::}}' + ] + ] + }, + })); + + expect(stack).to(haveResource('AWS::SecretsManager::Secret', { + GenerateSecretString: { + ExcludeCharacters: '\"@/\\', + GenerateStringKey: 'password', + PasswordLength: 30, + SecretStringTemplate: '{"username":"admin"}' + } + })); + test.done(); } }; diff --git a/packages/@aws-cdk/aws-rds/test/test.rds-rotation-single-user.ts b/packages/@aws-cdk/aws-rds/test/test.rds-rotation-single-user.ts index 5e3abe2e51cf2..078f94fb1cee8 100644 --- a/packages/@aws-cdk/aws-rds/test/test.rds-rotation-single-user.ts +++ b/packages/@aws-cdk/aws-rds/test/test.rds-rotation-single-user.ts @@ -8,50 +8,57 @@ import rds = require('../lib'); // tslint:disable:object-literal-key-quotes export = { - 'create a rds rotation single user'(test: Test) { + 'add a rds rotation single user to a cluster'(test: Test) { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'VPC'); - const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { - vpc, - }); - const target = new ec2.Connections({ - defaultPortRange: new ec2.TcpPort(1521), - securityGroups: [securityGroup] + const cluster = new rds.DatabaseCluster(stack, 'Database', { + engine: rds.DatabaseClusterEngine.AuroraMysql, + masterUser: { + username: 'admin' + }, + instanceProps: { + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), + vpc + } }); - const secret = new secretsmanager.Secret(stack, 'Secret'); // WHEN - new rds.RotationSingleUser(stack, 'Rotation', { - secret, - engine: rds.DatabaseEngine.Oracle, - vpc, - target - }); + cluster.addRotationSingleUser('Rotation'); // THEN expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { "IpProtocol": "tcp", - "Description": "from RotationSecurityGroup29D01037:1521", - "FromPort": 1521, + "Description": "from DatabaseRotationSecurityGroup1C5A8031:{IndirectPort}", + "FromPort": { + "Fn::GetAtt": [ + "DatabaseB269D8BB", + "Endpoint.Port" + ] + }, "GroupId": { "Fn::GetAtt": [ - "SecurityGroupDD263621", + "DatabaseSecurityGroup5C91FDCB", "GroupId" ] }, "SourceSecurityGroupId": { "Fn::GetAtt": [ - "RotationSecurityGroup3D2AB776", + "DatabaseRotationSecurityGroup17736B63", "GroupId" ] }, - "ToPort": 1521 + "ToPort": { + "Fn::GetAtt": [ + "DatabaseB269D8BB", + "Endpoint.Port" + ] + } })); expect(stack).to(haveResource('AWS::SecretsManager::RotationSchedule', { "SecretId": { - "Ref": "SecretA720EF05" + "Ref": "DatabaseSecretAttachedSecretE6CAC445" }, "RotationLambdaARN": { "Fn::Join": [ @@ -69,7 +76,7 @@ export = { { "Ref": "AWS::AccountId" }, - ":function:Rotation" + ":function:DatabaseRotation0D47EBD2" ] ] }, @@ -79,13 +86,13 @@ export = { })); expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { - "GroupDescription": "Rotation/SecurityGroup" + "GroupDescription": "Database/Rotation/SecurityGroup" })); expect(stack).to(haveResource('AWS::Serverless::Application', { "Location": { - "ApplicationId": "arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerRDSOracleRotationSingleUser", - "SemanticVersion": "1.0.45" + "ApplicationId": "arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerRDSMySQLRotationSingleUser", + "SemanticVersion": "1.0.74" }, "Parameters": { "endpoint": { @@ -103,10 +110,10 @@ export = { ] ] }, - "functionName": "Rotation", + "functionName": "DatabaseRotation0D47EBD2", "vpcSecurityGroupIds": { "Fn::GetAtt": [ - "RotationSecurityGroup3D2AB776", + "DatabaseRotationSecurityGroup17736B63", "GroupId" ] }, @@ -133,7 +140,7 @@ export = { expect(stack).to(haveResource('AWS::Lambda::Permission', { "Action": "lambda:InvokeFunction", - "FunctionName": "Rotation", + "FunctionName": "DatabaseRotation0D47EBD2", "Principal": { "Fn::Join": [ "", @@ -150,6 +157,30 @@ export = { test.done(); }, + 'throws when trying to add rotation to a cluster without secret'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + + // WHEN + const cluster = new rds.DatabaseCluster(stack, 'Database', { + engine: rds.DatabaseClusterEngine.AuroraMysql, + masterUser: { + username: 'admin', + password: 'tooshort' + }, + instanceProps: { + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), + vpc + } + }); + + // THEN + test.throws(() => cluster.addRotationSingleUser('Rotation'), /without secret/); + + test.done(); + }, + 'throws when both application location and engine are not specified'(test: Test) { // GIVEN const stack = new cdk.Stack(); From d16eaa2a0dc4857517af4477af3c0399c3cbe142 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 19 Mar 2019 18:52:22 +0100 Subject: [PATCH 08/16] Add integration tests for cluster rotation --- .../integ.cluster-rotation.lit.expected.json | 790 ++++++++++++++++++ .../test/integ.cluster-rotation.lit.ts | 25 + 2 files changed, 815 insertions(+) create mode 100644 packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json create mode 100644 packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.ts diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json new file mode 100644 index 0000000000000..6f80809f40e1e --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json @@ -0,0 +1,790 @@ +{ + "Transform": "AWS::Serverless-2016-10-31", + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PublicSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableAssociation0B0896DC": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1DefaultRoute91CEF279": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet1EIP6AD938E8": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PublicSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTableAssociation5A808732": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2DefaultRouteB7481BBA": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet2EIP4947BC00": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet3Subnet631C5E25": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PublicSubnet3" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VPCPublicSubnet3RouteTable98AE0E14": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3RouteTableAssociation427FE0C6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet3RouteTable98AE0E14" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + } + } + }, + "VPCPublicSubnet3DefaultRouteA0D29D46": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet3RouteTable98AE0E14" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet3EIPAD4BC883": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet3NATGatewayD3048F5C": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet3EIPAD4BC883", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PrivateSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableAssociation347902D1": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCPrivateSubnet2SubnetCFCDAA7A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PrivateSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTableAssociation0C73D413": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + } + } + }, + "VPCPrivateSubnet3Subnet3EDCD457": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PrivateSubnet3" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "VPCPrivateSubnet3RouteTable192186F8": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC/PrivateSubnet3" + } + ] + } + }, + "VPCPrivateSubnet3RouteTableAssociationC28D144E": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet3RouteTable192186F8" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + } + }, + "VPCPrivateSubnet3DefaultRoute27F311AE": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet3RouteTable192186F8" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet3NATGatewayD3048F5C" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-cluster-rotation/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "DatabaseSubnets56F17B9A": { + "Type": "AWS::RDS::DBSubnetGroup", + "Properties": { + "DBSubnetGroupDescription": "Subnets for Database database", + "SubnetIds": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + }, + { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + ] + } + }, + "DatabaseSecurityGroup5C91FDCB": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "RDS security group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "DatabaseSecurityGroupfromawscdkrdsclusterrotationDatabaseRotationSecurityGroup35913E19IndirectPort12DA2942": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "from awscdkrdsclusterrotationDatabaseRotationSecurityGroup35913E19:{IndirectPort}", + "FromPort": { + "Fn::GetAtt": [ + "DatabaseB269D8BB", + "Endpoint.Port" + ] + }, + "GroupId": { + "Fn::GetAtt": [ + "DatabaseSecurityGroup5C91FDCB", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "DatabaseRotationSecurityGroup17736B63", + "GroupId" + ] + }, + "ToPort": { + "Fn::GetAtt": [ + "DatabaseB269D8BB", + "Endpoint.Port" + ] + } + } + }, + "DatabaseSecret3B817195": { + "Type": "AWS::SecretsManager::Secret", + "Properties": { + "GenerateSecretString": { + "ExcludeCharacters": "\"@/\\", + "GenerateStringKey": "password", + "PasswordLength": 30, + "SecretStringTemplate": "{\"username\":\"admin\"}" + } + } + }, + "DatabaseSecretAttachedSecretE6CAC445": { + "Type": "AWS::SecretsManager::SecretTargetAttachment", + "Properties": { + "SecretId": { + "Ref": "DatabaseSecret3B817195" + }, + "TargetId": { + "Ref": "DatabaseB269D8BB" + }, + "TargetType": "AWS::RDS::DBCluster" + } + }, + "DatabaseSecretAttachedSecretRotationSchedule93D67FF7": { + "Type": "AWS::SecretsManager::RotationSchedule", + "Properties": { + "SecretId": { + "Ref": "DatabaseSecretAttachedSecretE6CAC445" + }, + "RotationLambdaARN": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":lambda:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":function:awscdkrdsclusterrotationDatabaseRotation30042AAE" + ] + ] + }, + "RotationRules": { + "AutomaticallyAfterDays": 30 + } + }, + "DependsOn": [ + "DatabaseRotationPermission64416CB0" + ] + }, + "DatabaseB269D8BB": { + "Type": "AWS::RDS::DBCluster", + "Properties": { + "Engine": "aurora", + "DBSubnetGroupName": { + "Ref": "DatabaseSubnets56F17B9A" + }, + "MasterUsername": { + "Fn::Join": [ + "", + [ + "{{resolve:secretsmanager:", + { + "Ref": "DatabaseSecret3B817195" + }, + ":SecretString:username::}}" + ] + ] + }, + "MasterUserPassword": { + "Fn::Join": [ + "", + [ + "{{resolve:secretsmanager:", + { + "Ref": "DatabaseSecret3B817195" + }, + ":SecretString:password::}}" + ] + ] + }, + "StorageEncrypted": false, + "VpcSecurityGroupIds": [ + { + "Fn::GetAtt": [ + "DatabaseSecurityGroup5C91FDCB", + "GroupId" + ] + } + ] + } + }, + "DatabaseInstance1844F58FD": { + "Type": "AWS::RDS::DBInstance", + "Properties": { + "DBInstanceClass": "db.t2.small", + "DBClusterIdentifier": { + "Ref": "DatabaseB269D8BB" + }, + "DBSubnetGroupName": { + "Ref": "DatabaseSubnets56F17B9A" + }, + "Engine": "aurora" + }, + "DependsOn": [ + "VPCPrivateSubnet1DefaultRouteAE1D6490", + "VPCPrivateSubnet2DefaultRouteF4F5CFD2", + "VPCPrivateSubnet3DefaultRoute27F311AE" + ] + }, + "DatabaseInstance2AA380DEE": { + "Type": "AWS::RDS::DBInstance", + "Properties": { + "DBInstanceClass": "db.t2.small", + "DBClusterIdentifier": { + "Ref": "DatabaseB269D8BB" + }, + "DBSubnetGroupName": { + "Ref": "DatabaseSubnets56F17B9A" + }, + "Engine": "aurora" + }, + "DependsOn": [ + "VPCPrivateSubnet1DefaultRouteAE1D6490", + "VPCPrivateSubnet2DefaultRouteF4F5CFD2", + "VPCPrivateSubnet3DefaultRoute27F311AE" + ] + }, + "DatabaseRotationSecurityGroup17736B63": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-cdk-rds-cluster-rotation/Database/Rotation/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "DatabaseRotation6B6E1D86": { + "Type": "AWS::Serverless::Application", + "Properties": { + "Location": { + "ApplicationId": "arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerRDSMySQLRotationSingleUser", + "SemanticVersion": "1.0.74" + }, + "Parameters": { + "endpoint": { + "Fn::Join": [ + "", + [ + "https://secretsmanager.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + }, + "functionName": "awscdkrdsclusterrotationDatabaseRotation30042AAE", + "vpcSecurityGroupIds": { + "Fn::GetAtt": [ + "DatabaseRotationSecurityGroup17736B63", + "GroupId" + ] + }, + "vpcSubnetIds": { + "Fn::Join": [ + "", + [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + ",", + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + }, + ",", + { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + ] + ] + } + } + } + }, + "DatabaseRotationPermission64416CB0": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": "awscdkrdsclusterrotationDatabaseRotation30042AAE", + "Principal": { + "Fn::Join": [ + "", + [ + "secretsmanager.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + }, + "DependsOn": [ + "DatabaseRotation6B6E1D86" + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.ts b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.ts new file mode 100644 index 0000000000000..bcd41ecf298d4 --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.ts @@ -0,0 +1,25 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import rds = require('../lib'); + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-rds-cluster-rotation'); + +const vpc = new ec2.VpcNetwork(stack, 'VPC'); + +/// !show +const cluster = new rds.DatabaseCluster(stack, 'Database', { + engine: rds.DatabaseClusterEngine.Aurora, + masterUser: { + username: 'admin' + }, + instanceProps: { + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), + vpc + } +}); + +cluster.addRotationSingleUser('Rotation'); +/// !hide + +app.run(); From e381a812f20d01187eb5b69ed6ceb158beabac15 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 19 Mar 2019 18:52:33 +0100 Subject: [PATCH 09/16] Update README for aws-rds --- packages/@aws-cdk/aws-rds/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 9fd10331d0170..896525719b84e 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -22,8 +22,7 @@ your instances will be launched privately or publicly: const cluster = new DatabaseCluster(this, 'Database', { engine: DatabaseClusterEngine.Aurora, masterUser: { - username: 'admin', - password: '7959866cacc02c2d243ecfe177464fe6', + username: 'admin' }, instanceProps: { instanceType: new InstanceTypePair(InstanceClass.Burstable2, InstanceSize.Small), @@ -34,6 +33,7 @@ const cluster = new DatabaseCluster(this, 'Database', { } }); ``` +By default, the master password will be generated and stored in AWS Secret Managers. Your cluster will be empty by default. To add a default database upon construction, specify the `defaultDatabaseName` attribute. @@ -53,3 +53,6 @@ attributes: ```ts const writeAddress = cluster.clusterEndpoint.socketAddress; // "HOSTNAME:PORT" ``` + +### Rotating master password +When the master password is generated and stored in AWS Secrets Manager, it can be rotated automatically: [example of setting up master password rotation](test/integ.cluster-rotation.lit.ts) From e2b9f13f72ab820c250e54dd4c59203d9ee481c3 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 19 Mar 2019 19:51:40 +0100 Subject: [PATCH 10/16] Typo --- packages/@aws-cdk/aws-rds/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 896525719b84e..8d16d98d4d979 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -33,7 +33,7 @@ const cluster = new DatabaseCluster(this, 'Database', { } }); ``` -By default, the master password will be generated and stored in AWS Secret Managers. +By default, the master password will be generated and stored in AWS Secrets Managers. Your cluster will be empty by default. To add a default database upon construction, specify the `defaultDatabaseName` attribute. From f014a07f1400518f450a72398e2977b35a07ff62 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 19 Mar 2019 19:51:54 +0100 Subject: [PATCH 11/16] Fix dependencies --- packages/@aws-cdk/aws-rds/package.json | 4 ++++ packages/@aws-cdk/aws-secretsmanager/package.json | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-rds/package.json b/packages/@aws-cdk/aws-rds/package.json index 5e9d0562d1ad1..46a4582a60110 100644 --- a/packages/@aws-cdk/aws-rds/package.json +++ b/packages/@aws-cdk/aws-rds/package.json @@ -64,12 +64,16 @@ "@aws-cdk/aws-ec2": "^0.25.3", "@aws-cdk/aws-iam": "^0.25.3", "@aws-cdk/aws-kms": "^0.25.3", + "@aws-cdk/aws-lambda": "^0.25.3", "@aws-cdk/aws-secretsmanager": "^0.25.3", + "@aws-cdk/aws-serverless": "^0.25.3", "@aws-cdk/cdk": "^0.25.3" }, "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { "@aws-cdk/aws-ec2": "^0.25.3", + "@aws-cdk/aws-kms": "^0.25.3", + "@aws-cdk/aws-secretsmanager": "^0.25.3", "@aws-cdk/cdk": "^0.25.3" }, "engines": { diff --git a/packages/@aws-cdk/aws-secretsmanager/package.json b/packages/@aws-cdk/aws-secretsmanager/package.json index 1e6600c106c86..b5ea8b6a513d9 100644 --- a/packages/@aws-cdk/aws-secretsmanager/package.json +++ b/packages/@aws-cdk/aws-secretsmanager/package.json @@ -66,7 +66,6 @@ "@aws-cdk/aws-ec2": "^0.25.3", "@aws-cdk/aws-kms": "^0.25.3", "@aws-cdk/aws-lambda": "^0.25.3", - "@aws-cdk/aws-serverless": "^0.25.3", "@aws-cdk/cdk": "^0.25.3" }, "peerDependencies": { From 9aa5fe7d5466a3638d458a2eb979403e62e22df3 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 19 Mar 2019 19:53:10 +0100 Subject: [PATCH 12/16] Rename test --- ...t.rds-rotation-single-user.ts => test.rotation-single-user.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/@aws-cdk/aws-rds/test/{test.rds-rotation-single-user.ts => test.rotation-single-user.ts} (100%) diff --git a/packages/@aws-cdk/aws-rds/test/test.rds-rotation-single-user.ts b/packages/@aws-cdk/aws-rds/test/test.rotation-single-user.ts similarity index 100% rename from packages/@aws-cdk/aws-rds/test/test.rds-rotation-single-user.ts rename to packages/@aws-cdk/aws-rds/test/test.rotation-single-user.ts From f4b44034d2c06d8b13975769ea3ea554e0ab812c Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 19 Mar 2019 19:53:28 +0100 Subject: [PATCH 13/16] Remove default username in DatabaseSecret --- packages/@aws-cdk/aws-rds/lib/database-secret.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/lib/database-secret.ts b/packages/@aws-cdk/aws-rds/lib/database-secret.ts index 356fe72a0abb4..c28750e1b56cc 100644 --- a/packages/@aws-cdk/aws-rds/lib/database-secret.ts +++ b/packages/@aws-cdk/aws-rds/lib/database-secret.ts @@ -8,10 +8,8 @@ import cdk = require('@aws-cdk/cdk'); export interface DatabaseSecretProps { /** * The username. - * - * @default admin */ - username?: string; + username: string; /** * The KMS key to use to encrypt the secret. @@ -25,12 +23,12 @@ export interface DatabaseSecretProps { * A database secret. */ export class DatabaseSecret extends secretsmanager.Secret { - constructor(scope: cdk.Construct, id: string, props: DatabaseSecretProps = {}) { + constructor(scope: cdk.Construct, id: string, props: DatabaseSecretProps) { super(scope, id, { encryptionKey: props.encryptionKey, generateSecretString: ({ passwordLength: 30, // Oracle password cannot have more than 30 characters - secretStringTemplate: JSON.stringify({ username: props.username || 'admin' }), + secretStringTemplate: JSON.stringify({ username: props.username }), generateStringKey: 'password', excludeCharacters: '"@/\\' }) as secretsmanager.TemplatedSecretStringGenerator From 304c1d55b36677e40ead2000f0bb123ac85539ce Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 19 Mar 2019 20:06:28 +0100 Subject: [PATCH 14/16] Fix typos --- packages/@aws-cdk/aws-rds/README.md | 2 +- packages/@aws-cdk/aws-rds/lib/cluster.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 8d16d98d4d979..8bd82c506e2f1 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -33,7 +33,7 @@ const cluster = new DatabaseCluster(this, 'Database', { } }); ``` -By default, the master password will be generated and stored in AWS Secrets Managers. +By default, the master password will be generated and stored in AWS Secrets Manager. Your cluster will be empty by default. To add a default database upon construction, specify the `defaultDatabaseName` attribute. diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index c17cbd8a54f4d..66fda81a3fac2 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -432,6 +432,6 @@ function toDatabaseEngine(engine: DatabaseClusterEngine): DatabaseEngine { case DatabaseClusterEngine.AuroraPostgresql: return DatabaseEngine.Postgres; default: - throw new Error('Unkown engine'); + throw new Error('Unknown engine'); } } From df59fca951be8cadda5384cb0b62a8a1f926536b Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 20 Mar 2019 09:22:25 +0100 Subject: [PATCH 15/16] Update README for aws-rds --- packages/@aws-cdk/aws-rds/README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 8bd82c506e2f1..591041533155d 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -56,3 +56,25 @@ const writeAddress = cluster.clusterEndpoint.socketAddress; // "HOSTNAME:PORT" ### Rotating master password When the master password is generated and stored in AWS Secrets Manager, it can be rotated automatically: [example of setting up master password rotation](test/integ.cluster-rotation.lit.ts) + +Rotation of the master password is also supported for an existing cluster: +```ts +new rds.RotationSingleUser(stack, 'Rotation', { + secret: importedSecret, + engine: DatabaseEngine.Oracle, + target: importedCluster, + vpc: importedVpc, +}) +``` + +The `importedSecret` must be a JSON string with the following format: +```json +{ + "engine": "", + "host": "", + "username": "", + "password": "", + "dbname": "", + "port": "" +} +``` From fdb3236ccc9af78b2010bab33afd4433d6b9c22e Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Wed, 20 Mar 2019 11:43:42 +0100 Subject: [PATCH 16/16] Move literal example to new line --- packages/@aws-cdk/aws-rds/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 591041533155d..74c6a8f402e93 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -55,7 +55,9 @@ const writeAddress = cluster.clusterEndpoint.socketAddress; // "HOSTNAME:PORT" ``` ### Rotating master password -When the master password is generated and stored in AWS Secrets Manager, it can be rotated automatically: [example of setting up master password rotation](test/integ.cluster-rotation.lit.ts) +When the master password is generated and stored in AWS Secrets Manager, it can be rotated automatically: + +[example of setting up master password rotation](test/integ.cluster-rotation.lit.ts) Rotation of the master password is also supported for an existing cluster: ```ts