From 4e105a3904c6bd91f66d60e2d77ab42afe36549b Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 26 Mar 2019 15:08:18 +0100 Subject: [PATCH] fix(secretsmanager): allow templated string creation (#2010) Merge the StringGenerator argument types and do runtime analysis so it becomes obvious how to do templated secret generation. --- .../@aws-cdk/aws-rds/lib/database-secret.ts | 4 +- .../@aws-cdk/aws-secretsmanager/lib/secret.ts | 27 ++++--- .../test/integ.secret.lit.expected.json | 40 ++++++++++ .../test/integ.secret.lit.ts | 14 ++++ .../aws-secretsmanager/test/test.secret.ts | 74 +++++++++++++++++++ 5 files changed, 145 insertions(+), 14 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/lib/database-secret.ts b/packages/@aws-cdk/aws-rds/lib/database-secret.ts index c28750e1b56cc..fb2783f4f6edd 100644 --- a/packages/@aws-cdk/aws-rds/lib/database-secret.ts +++ b/packages/@aws-cdk/aws-rds/lib/database-secret.ts @@ -26,12 +26,12 @@ export class DatabaseSecret extends secretsmanager.Secret { constructor(scope: cdk.Construct, id: string, props: DatabaseSecretProps) { super(scope, id, { encryptionKey: props.encryptionKey, - generateSecretString: ({ + generateSecretString: { passwordLength: 30, // Oracle password cannot have more than 30 characters secretStringTemplate: JSON.stringify({ username: props.username }), generateStringKey: 'password', excludeCharacters: '"@/\\' - }) as secretsmanager.TemplatedSecretStringGenerator + } }); } } diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts index ec75ffe873089..bd79a0b7eadad 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts @@ -186,6 +186,12 @@ export class Secret extends SecretBase { constructor(scope: cdk.Construct, id: string, props: SecretProps = {}) { super(scope, id); + if (props.generateSecretString && + (props.generateSecretString.secretStringTemplate || props.generateSecretString.generateStringKey) && + !(props.generateSecretString.secretStringTemplate && props.generateSecretString.generateStringKey)) { + throw new Error('`secretStringTemplate` and `generateStringKey` must be specified together.'); + } + const resource = new secretsmanager.CfnSecret(this, 'Resource', { description: props.description, kmsKeyId: props.encryptionKey && props.encryptionKey.keyArn, @@ -367,24 +373,21 @@ export interface SecretStringGenerator { * @default false */ excludeNumbers?: boolean; -} -/** - * Configuration to generate secrets such as passwords automatically, and include them in a JSON object template. - */ -export interface TemplatedSecretStringGenerator extends SecretStringGenerator { /** - * The JSON key name that's used to add the generated password to the JSON structure specified by the - * ``secretStringTemplate`` parameter. + * A properly structured JSON string that the generated password can be added to. The ``generateStringKey`` is + * combined with the generated random string and inserted into the JSON structure that's specified by this parameter. + * The merged JSON string is returned as the completed SecretString of the secret. If you specify ``secretStringTemplate`` + * then ``generateStringKey`` must be also be specified. */ - generateStringKey: string; + secretStringTemplate?: string; /** - * A properly structured JSON string that the generated password can be added to. The ``generateStringKey`` is - * combined with the generated random string and inserted into the JSON structure that's specified by this parameter. - * The merged JSON string is returned as the completed SecretString of the secret. + * The JSON key name that's used to add the generated password to the JSON structure specified by the + * ``secretStringTemplate`` parameter. If you specify ``generateStringKey`` then ``secretStringTemplate`` + * must be also be specified. */ - secretStringTemplate: string; + generateStringKey?: string; } class ImportedSecret extends SecretBase { diff --git a/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.expected.json b/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.expected.json index 2eb92c11cc1b1..b09235155139e 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.expected.json +++ b/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.expected.json @@ -79,6 +79,46 @@ } } } + }, + "TemplatedSecret3D98B577": { + "Type": "AWS::SecretsManager::Secret", + "Properties": { + "GenerateSecretString": { + "GenerateStringKey": "password", + "SecretStringTemplate": "{\"username\":\"user\"}" + } + } + }, + "OtherUser6093621C": { + "Type": "AWS::IAM::User", + "Properties": { + "LoginProfile": { + "Password": { + "Fn::Join": [ + "", + [ + "{{resolve:secretsmanager:", + { + "Ref": "TemplatedSecret3D98B577" + }, + ":SecretString:password::}}" + ] + ] + } + }, + "UserName": { + "Fn::Join": [ + "", + [ + "{{resolve:secretsmanager:", + { + "Ref": "TemplatedSecret3D98B577" + }, + ":SecretString:username::}}" + ] + ] + } + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts b/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts index 4a90c8f6ef424..171426e6ba45c 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts @@ -9,12 +9,26 @@ class SecretsManagerStack extends cdk.Stack { const role = new iam.Role(this, 'TestRole', { assumedBy: new iam.AccountRootPrincipal() }); /// !show + // Default secret const secret = new secretsManager.Secret(this, 'Secret'); secret.grantRead(role); new iam.User(this, 'User', { password: secret.stringValue }); + + // Templated secret + const templatedSecret = new secretsManager.Secret(this, 'TemplatedSecret', { + generateSecretString: { + secretStringTemplate: JSON.stringify({ username: 'user' }), + generateStringKey: 'password' + } + }); + + new iam.User(this, 'OtherUser', { + userName: templatedSecret.jsonFieldValue('username'), + password: templatedSecret.jsonFieldValue('password') + }); /// !hide } } diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts index d95887d3f135f..d8d6435ef7d59 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts @@ -22,6 +22,52 @@ export = { test.done(); }, + 'secret with generate secret string options'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new secretsmanager.Secret(stack, 'Secret', { + generateSecretString: { + excludeUppercase: true, + passwordLength: 20 + } + }); + + // THEN + expect(stack).to(haveResource('AWS::SecretsManager::Secret', { + GenerateSecretString: { + ExcludeUppercase: true, + PasswordLength: 20 + } + })); + + test.done(); + }, + + 'templated secret string'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new secretsmanager.Secret(stack, 'Secret', { + generateSecretString: { + secretStringTemplate: JSON.stringify({ username: 'username' }), + generateStringKey: 'password' + } + }); + + // THEN + expect(stack).to(haveResource('AWS::SecretsManager::Secret', { + GenerateSecretString: { + SecretStringTemplate: '{"username":"username"}', + GenerateStringKey: 'password' + } + })); + + test.done(); + }, + 'grantRead'(test: Test) { // GIVEN const stack = new cdk.Stack(); @@ -314,6 +360,34 @@ export = { } })); + test.done(); + }, + + 'throws when specifying secretStringTemplate but not generateStringKey'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + test.throws(() => new secretsmanager.Secret(stack, 'Secret', { + generateSecretString: { + secretStringTemplate: JSON.stringify({ username: 'username' }) + } + }), /`secretStringTemplate`.+`generateStringKey`/); + + test.done(); + }, + + 'throws when specifying generateStringKey but not secretStringTemplate'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + test.throws(() => new secretsmanager.Secret(stack, 'Secret', { + generateSecretString: { + generateStringKey: 'password' + } + }), /`secretStringTemplate`.+`generateStringKey`/); + test.done(); } };