Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(secretsmanager): allow templated string creation #2010

Merged
merged 7 commits into from
Mar 26, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/@aws-cdk/aws-rds/lib/database-secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
});
}
}
29 changes: 16 additions & 13 deletions packages/@aws-cdk/aws-secretsmanager/lib/secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export interface SecretProps {
* @default 32 characters with upper-case letters, lower-case letters, punctuation and numbers (at least one from each
* category), per the default values of ``SecretStringGenerator``.
*/
generateSecretString?: SecretStringGenerator;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why rid of the semicolon?

generateSecretString?: SecretStringGenerator

/**
* A name for the secret. Note that deleting secrets from SecretsManager does not happen immediately, but after a 7 to
Expand Down Expand Up @@ -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('Both `secretStringTemplate` and `generateStringKey` must be specified.');
}

const resource = new secretsmanager.CfnSecret(this, 'Resource', {
description: props.description,
kmsKeyId: props.encryptionKey && props.encryptionKey.keyArn,
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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::}}"
]
]
}
}
}
}
}
14 changes: 14 additions & 0 deletions packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
74 changes: 74 additions & 0 deletions packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}
};