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 8 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
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).
*/
asSecretString(): SecretString;
Copy link
Contributor

Choose a reason for hiding this comment

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

rename to toSecretString. We use the "asXXX" notation for inversion of control methods and users are not supposed to use them directly.


/**
* 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 asSecretString() {
this.secretString = this.secretString || new SecretString(this, 'SecretString', { secretId: this.secretArn });
return this.secretString;
}

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) {
RomainMuller marked this conversation as resolved.
Show resolved Hide resolved
// @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`
}));
}
}
}

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