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

feat(secretsmanager): L2 construct for Secret #1686

Merged
merged 9 commits into from
Feb 6, 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: 4 additions & 0 deletions packages/@aws-cdk/aws-secretsmanager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
```ts
const secretsmanager = require('@aws-cdk/aws-secretsmanager');
```

### Create a new Secret in a Stack

[example of creating a secret](test/integ.secret.lit.ts)
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-secretsmanager/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './secret';
export * from './secret-string';

// AWS::SecretsManager CloudFormation Resources:
Expand Down
259 changes: 259 additions & 0 deletions packages/@aws-cdk/aws-secretsmanager/lib/secret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import iam = require('@aws-cdk/aws-iam');
import kms = require('@aws-cdk/aws-kms');
import cdk = require('@aws-cdk/cdk');
import secretsmanager = require('./secretsmanager.generated');

/**
* A secret in AWS Secrets Manager.
*/
export interface ISecret extends cdk.IConstruct {
/**
* The customer-managed encryption key that is used to encrypt this secret, if any. When ``unknown``, the default KMS
RomainMuller marked this conversation as resolved.
Show resolved Hide resolved
* key for the account and region is being used.
*/
readonly encryptionKey?: kms.IEncryptionKey;

/**
* The ARN of the secret in AWS Secrets Manager.
*/
readonly secretArn: string;

/**
* Exports this secret.
*
* @return import props that can be passed back to ``Secret.import``.
*/
export(): SecretImportProps;

/**
* Grants reading the secret value to some role.
*
* @param grantee the principal being granted permission.
* @param versionStages the version stages the grant is limited to. If not specified, no restriction on the version
* stages is applied.
*/
grantRead(grantee: iam.IPrincipal, versionStages?: string[]): void;
}

/**
* The properties required to create a new secret in AWS Secrets Manager.
*/
export interface SecretProps {
/**
* An optional, human-friendly description of the secret.
*/
description?: string;

/**
* The customer-managed encryption key to use for encrypting the secret value.
*
* @default a default KMS key for the account and region is used.
*/
encryptionKey?: kms.IEncryptionKey;

/**
* The value of the secret. Either this or ``generateSecretString`` must be specified, but not both.
*
* @default a secret is generated using ``generateSecretString`` configuration.
*/
secretString?: string;
RomainMuller marked this conversation as resolved.
Show resolved Hide resolved

/**
* Configuration for how to generate a secret value. Either this or ``secretString`` must be specified, but not both.
*
* @default the secret value specified in ``secretString`` is used.
*/
generateSecretString?: SecretStringGenerator;

/**
* A name for the secret. Note that deleting secrets from SecretsManager does not happen immediately, but after a 7 to
* 30 days blackout period. During that period, it is not possible to create another secret that shares the same name.
*
* @default a name is generated by CloudFormation.
*/
name?: string;
}

/**
* Attributes required to import an existing secret into the Stack.
*/
export interface SecretImportProps {
/**
* The encryption key that is used to encrypt the secret, unless the default SecretsManager key is used.
*/
encryptionKey?: kms.IEncryptionKey;

/**
* The ARN of the secret in SecretsManager.
*/
secretArn: string;
}

/**
* The common behavior of Secrets. Users should not use this class directly, and instead use ``Secret``.
*/
export abstract class SecretBase extends cdk.Construct implements ISecret {
public abstract readonly encryptionKey?: kms.IEncryptionKey;
public abstract readonly secretArn: string;

public export(): SecretImportProps {
RomainMuller marked this conversation as resolved.
Show resolved Hide resolved
return {
encryptionKey: this.encryptionKey,
secretArn: this.secretArn,
};
}

public grantRead(grantee: iam.IPrincipal, versionStages?: string[]): void {
const statement = new iam.PolicyStatement()
.allow()
.addAction('secretsmanager:GetSecretValue')
.addResource(this.secretArn);
if (versionStages != null) {
statement.addCondition('ForAnyValue:StringEquals', {
'secretsmanager:VersionStage': versionStages
});
}
grantee.addToPolicy(statement);

if (this.encryptionKey) {
RomainMuller marked this conversation as resolved.
Show resolved Hide resolved
this.encryptionKey.addToResourcePolicy(new iam.PolicyStatement()
.allow()
.addPrincipal(grantee.principal)
.addAction('kms:Decrypt')
.addAllResources()
.addCondition('StringEquals', {
'kms:ViaService': `secretsmanager.${cdk.Stack.find(this).region}.amazonaws.com`
}));
}
}
}

/**
* Creates a new secret in AWS SecretsManager.
*/
export class Secret extends SecretBase {
/**
* Import an existing secret into the Stack.
*
* @param scope the scope of the import.
* @param id the ID of the imported Secret in the construct tree.
* @param props the attributes of the imported secret.
*/
public static import(scope: cdk.Construct, id: string, props: SecretImportProps): ISecret {
return new ImportedSecret(scope, id, props);
}

public readonly encryptionKey?: kms.IEncryptionKey;
public readonly secretArn: string;

constructor(scope: cdk.Construct, id: string, props: SecretProps) {
super(scope, id);

if ((props.secretString == null) === (props.generateSecretString == null)) {
RomainMuller marked this conversation as resolved.
Show resolved Hide resolved
throw new Error('Either secretString or generateSecretString must be specified, but not both.');
}

const resource = new secretsmanager.CfnSecret(this, 'Resource', {
description: props.description,
kmsKeyId: props.encryptionKey && props.encryptionKey.keyArn,
secretString: props.secretString,
generateSecretString: props.generateSecretString,
name: props.name,
});

this.encryptionKey = props.encryptionKey;
this.secretArn = resource.secretArn;
}
}

/**
* Configuration to generate secrets such as passwords automatically.
*/
export interface SecretStringGenerator {
/**
* Specifies that the generated password shouldn't include uppercase letters.
*
* @default false
*/
excludeUppercase?: boolean;

/**
* Specifies whether the generated password must include at least one of every allowed character type.
*
* @default true
*/
requireEachIncludedType?: boolean;

/**
* Specifies that the generated password can include the space character.
*
* @default false
*/
includeSpace?: boolean;

/**
* A string that includes characters that shouldn't be included in the generated password. The string can be a minimum
* of ``0`` and a maximum of ``4096`` characters long.
*
* @default no exclusions
*/
excludeCharacters?: string;

/**
* The desired length of the generated password.
*
* @default 32
*/
passwordLength?: number;

/**
* Specifies that the generated password shouldn't include punctuation characters.
*
* @default false
*/
excludePunctuation?: boolean;

/**
* Specifies that the generated password shouldn't include lowercase letters.
*
* @default false
*/
excludeLowercase?: boolean;

/**
* Specifies that the generated password shouldn't include digits.
*
* @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.
*/
generateStringKey: 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.
*/
secretStringTemplate: string;
}

class ImportedSecret extends SecretBase {
public readonly encryptionKey?: kms.IEncryptionKey;
public readonly secretArn: string;

constructor(scope: cdk.Construct, id: string, props: SecretImportProps) {
super(scope, id);

this.encryptionKey = props.encryptionKey;
this.secretArn = props.secretArn;
}
}
9 changes: 7 additions & 2 deletions packages/@aws-cdk/aws-secretsmanager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,20 @@
"@aws-cdk/assert": "^0.23.0",
"cdk-build-tools": "^0.23.0",
"cfn2ts": "^0.23.0",
"pkglint": "^0.23.0"
"pkglint": "^0.23.0",
"cdk-integ-tools": "^0.23.0"
},
"dependencies": {
"@aws-cdk/aws-kms": "^0.23.0",
"@aws-cdk/aws-iam": "^0.23.0",
"@aws-cdk/cdk": "^0.23.0"
},
"peerDependencies": {
"@aws-cdk/aws-kms": "^0.23.0",
"@aws-cdk/aws-iam": "^0.23.0",
"@aws-cdk/cdk": "^0.23.0"
},
"engines": {
"node": ">= 8.10.0"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"Resources": {
"TestRole6C9272DF": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"AWS": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::",
{
"Ref": "AWS::AccountId"
},
":root"
]
]
}
}
}
],
"Version": "2012-10-17"
}
}
},
"TestRoleDefaultPolicyD1C92014": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "secretsmanager:GetSecretValue",
"Effect": "Allow",
"Resource": {
"Ref": "SecretA720EF05"
}
}
],
"Version": "2012-10-17"
},
"PolicyName": "TestRoleDefaultPolicyD1C92014",
"Roles": [
{
"Ref": "TestRole6C9272DF"
}
]
}
},
"SecretA720EF05": {
"Type": "AWS::SecretsManager::Secret",
"Properties": {
"GenerateSecretString": {}
}
}
}
}
16 changes: 16 additions & 0 deletions packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import iam = require('@aws-cdk/aws-iam');
import cdk = require('@aws-cdk/cdk');
import secretsManager = require('../lib');

const app = new cdk.App();
const stack = new cdk.Stack(app, 'Integ-SecretsManager-Secret');
const role = new iam.Role(stack, 'TestRole', { assumedBy: new iam.AccountRootPrincipal() });

/// !show
const secret = new secretsManager.Secret(stack, 'Secret', {
generateSecretString: {}
});
secret.grantRead(role);
/// !hide

app.run();
Loading