Skip to content

Commit

Permalink
fix(secretsmanager): allow templated string creation (#2010)
Browse files Browse the repository at this point in the history
Merge the StringGenerator argument types and do runtime analysis
so it becomes obvious how to do templated secret generation.
  • Loading branch information
jogold authored and rix0rrr committed Mar 26, 2019
1 parent e82e208 commit 4e105a3
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 14 deletions.
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
}
});
}
}
27 changes: 15 additions & 12 deletions packages/@aws-cdk/aws-secretsmanager/lib/secret.ts
Original file line number Diff line number Diff line change
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('`secretStringTemplate` and `generateStringKey` must be specified together.');
}

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();
}
};

0 comments on commit 4e105a3

Please sign in to comment.