Skip to content

Commit

Permalink
feat(secretsmanager): L2 construct for Secret (#1686)
Browse files Browse the repository at this point in the history
  • Loading branch information
RomainMuller authored Feb 6, 2019
1 parent d17a6e9 commit 8da9115
Show file tree
Hide file tree
Showing 9 changed files with 637 additions and 11 deletions.
19 changes: 19 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,22 @@
```ts
const secretsmanager = require('@aws-cdk/aws-secretsmanager');
```

### Create a new Secret in a Stack

In order to have SecretsManager generate a new secret value automatically, you can get started with the following:

[example of creating a secret](test/integ.secret.lit.ts)

The `Secret` construct does not allow specifying the `SecretString` property of the `AWS::SecretsManager::Secret`
resource as this will almost always lead to the secret being surfaced in plain text and possibly committed to your
source control. If you need to use a pre-existing secret, the recommended way is to manually provision
the secret in *AWS SecretsManager* and use the `Secret.import` method to make it available in your CDK Application:

```ts
const secret = Secret.import(scope, 'ImportedSecret', {
secretArn: 'arn:aws:secretsmanager:<region>:<account-id-number>:secret:<secret-name>-<random-6-characters>',
// If the secret is encrypted using a KMS-hosted CMK, either import or reference that key:
encryptionKey,
});
```
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
270 changes: 270 additions & 0 deletions packages/@aws-cdk/aws-secretsmanager/lib/secret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import iam = require('@aws-cdk/aws-iam');
import kms = require('@aws-cdk/aws-kms');
import cdk = require('@aws-cdk/cdk');
import { SecretString } from './secret-string';
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 not specified, the default
* KMS 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;

/**
* Returns a SecretString corresponding to this secret, so that the secret value can be referred to from other parts
* of the application (such as an RDS instance's master user password property).
*/
toSecretString(): SecretString;

/**
* 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;

/**
* Configuration for how to generate a secret value.
*
* @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;

/**
* 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;

private secretString?: SecretString;

public abstract export(): SecretImportProps;

public grantRead(grantee: iam.IPrincipal, versionStages?: string[]): void {
// @see https://docs.aws.amazon.com/fr_fr/secretsmanager/latest/userguide/auth-and-access_identity-based-policies.html
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) {
// @see https://docs.aws.amazon.com/fr_fr/kms/latest/developerguide/services-secrets-manager.html
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`
}));
}
}

public toSecretString() {
this.secretString = this.secretString || new SecretString(this, 'SecretString', { secretId: this.secretArn });
return this.secretString;
}
}

/**
* 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);

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

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

public export(): SecretImportProps {
return {
encryptionKey: this.encryptionKey,
secretArn: this.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, private readonly props: SecretImportProps) {
super(scope, id);

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

public export() {
return this.props;
}
}
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": {}
}
}
}
}
Loading

0 comments on commit 8da9115

Please sign in to comment.