Skip to content

Commit 8da9115

Browse files
authored
feat(secretsmanager): L2 construct for Secret (#1686)
1 parent d17a6e9 commit 8da9115

File tree

9 files changed

+637
-11
lines changed

9 files changed

+637
-11
lines changed

packages/@aws-cdk/aws-secretsmanager/README.md

+19
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,22 @@
33
```ts
44
const secretsmanager = require('@aws-cdk/aws-secretsmanager');
55
```
6+
7+
### Create a new Secret in a Stack
8+
9+
In order to have SecretsManager generate a new secret value automatically, you can get started with the following:
10+
11+
[example of creating a secret](test/integ.secret.lit.ts)
12+
13+
The `Secret` construct does not allow specifying the `SecretString` property of the `AWS::SecretsManager::Secret`
14+
resource as this will almost always lead to the secret being surfaced in plain text and possibly committed to your
15+
source control. If you need to use a pre-existing secret, the recommended way is to manually provision
16+
the secret in *AWS SecretsManager* and use the `Secret.import` method to make it available in your CDK Application:
17+
18+
```ts
19+
const secret = Secret.import(scope, 'ImportedSecret', {
20+
secretArn: 'arn:aws:secretsmanager:<region>:<account-id-number>:secret:<secret-name>-<random-6-characters>',
21+
// If the secret is encrypted using a KMS-hosted CMK, either import or reference that key:
22+
encryptionKey,
23+
});
24+
```

packages/@aws-cdk/aws-secretsmanager/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './secret';
12
export * from './secret-string';
23

34
// AWS::SecretsManager CloudFormation Resources:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import iam = require('@aws-cdk/aws-iam');
2+
import kms = require('@aws-cdk/aws-kms');
3+
import cdk = require('@aws-cdk/cdk');
4+
import { SecretString } from './secret-string';
5+
import secretsmanager = require('./secretsmanager.generated');
6+
7+
/**
8+
* A secret in AWS Secrets Manager.
9+
*/
10+
export interface ISecret extends cdk.IConstruct {
11+
/**
12+
* The customer-managed encryption key that is used to encrypt this secret, if any. When not specified, the default
13+
* KMS key for the account and region is being used.
14+
*/
15+
readonly encryptionKey?: kms.IEncryptionKey;
16+
17+
/**
18+
* The ARN of the secret in AWS Secrets Manager.
19+
*/
20+
readonly secretArn: string;
21+
22+
/**
23+
* Returns a SecretString corresponding to this secret, so that the secret value can be referred to from other parts
24+
* of the application (such as an RDS instance's master user password property).
25+
*/
26+
toSecretString(): SecretString;
27+
28+
/**
29+
* Exports this secret.
30+
*
31+
* @return import props that can be passed back to ``Secret.import``.
32+
*/
33+
export(): SecretImportProps;
34+
35+
/**
36+
* Grants reading the secret value to some role.
37+
*
38+
* @param grantee the principal being granted permission.
39+
* @param versionStages the version stages the grant is limited to. If not specified, no restriction on the version
40+
* stages is applied.
41+
*/
42+
grantRead(grantee: iam.IPrincipal, versionStages?: string[]): void;
43+
}
44+
45+
/**
46+
* The properties required to create a new secret in AWS Secrets Manager.
47+
*/
48+
export interface SecretProps {
49+
/**
50+
* An optional, human-friendly description of the secret.
51+
*/
52+
description?: string;
53+
54+
/**
55+
* The customer-managed encryption key to use for encrypting the secret value.
56+
*
57+
* @default a default KMS key for the account and region is used.
58+
*/
59+
encryptionKey?: kms.IEncryptionKey;
60+
61+
/**
62+
* Configuration for how to generate a secret value.
63+
*
64+
* @default 32 characters with upper-case letters, lower-case letters, punctuation and numbers (at least one from each
65+
* category), per the default values of ``SecretStringGenerator``.
66+
*/
67+
generateSecretString?: SecretStringGenerator;
68+
69+
/**
70+
* A name for the secret. Note that deleting secrets from SecretsManager does not happen immediately, but after a 7 to
71+
* 30 days blackout period. During that period, it is not possible to create another secret that shares the same name.
72+
*
73+
* @default a name is generated by CloudFormation.
74+
*/
75+
name?: string;
76+
}
77+
78+
/**
79+
* Attributes required to import an existing secret into the Stack.
80+
*/
81+
export interface SecretImportProps {
82+
/**
83+
* The encryption key that is used to encrypt the secret, unless the default SecretsManager key is used.
84+
*/
85+
encryptionKey?: kms.IEncryptionKey;
86+
87+
/**
88+
* The ARN of the secret in SecretsManager.
89+
*/
90+
secretArn: string;
91+
}
92+
93+
/**
94+
* The common behavior of Secrets. Users should not use this class directly, and instead use ``Secret``.
95+
*/
96+
export abstract class SecretBase extends cdk.Construct implements ISecret {
97+
public abstract readonly encryptionKey?: kms.IEncryptionKey;
98+
public abstract readonly secretArn: string;
99+
100+
private secretString?: SecretString;
101+
102+
public abstract export(): SecretImportProps;
103+
104+
public grantRead(grantee: iam.IPrincipal, versionStages?: string[]): void {
105+
// @see https://docs.aws.amazon.com/fr_fr/secretsmanager/latest/userguide/auth-and-access_identity-based-policies.html
106+
const statement = new iam.PolicyStatement()
107+
.allow()
108+
.addAction('secretsmanager:GetSecretValue')
109+
.addResource(this.secretArn);
110+
if (versionStages != null) {
111+
statement.addCondition('ForAnyValue:StringEquals', {
112+
'secretsmanager:VersionStage': versionStages
113+
});
114+
}
115+
grantee.addToPolicy(statement);
116+
117+
if (this.encryptionKey) {
118+
// @see https://docs.aws.amazon.com/fr_fr/kms/latest/developerguide/services-secrets-manager.html
119+
this.encryptionKey.addToResourcePolicy(new iam.PolicyStatement()
120+
.allow()
121+
.addPrincipal(grantee.principal)
122+
.addAction('kms:Decrypt')
123+
.addAllResources()
124+
.addCondition('StringEquals', {
125+
'kms:ViaService': `secretsmanager.${cdk.Stack.find(this).region}.amazonaws.com`
126+
}));
127+
}
128+
}
129+
130+
public toSecretString() {
131+
this.secretString = this.secretString || new SecretString(this, 'SecretString', { secretId: this.secretArn });
132+
return this.secretString;
133+
}
134+
}
135+
136+
/**
137+
* Creates a new secret in AWS SecretsManager.
138+
*/
139+
export class Secret extends SecretBase {
140+
/**
141+
* Import an existing secret into the Stack.
142+
*
143+
* @param scope the scope of the import.
144+
* @param id the ID of the imported Secret in the construct tree.
145+
* @param props the attributes of the imported secret.
146+
*/
147+
public static import(scope: cdk.Construct, id: string, props: SecretImportProps): ISecret {
148+
return new ImportedSecret(scope, id, props);
149+
}
150+
151+
public readonly encryptionKey?: kms.IEncryptionKey;
152+
public readonly secretArn: string;
153+
154+
constructor(scope: cdk.Construct, id: string, props: SecretProps = {}) {
155+
super(scope, id);
156+
157+
const resource = new secretsmanager.CfnSecret(this, 'Resource', {
158+
description: props.description,
159+
kmsKeyId: props.encryptionKey && props.encryptionKey.keyArn,
160+
generateSecretString: props.generateSecretString || {},
161+
name: props.name,
162+
});
163+
164+
this.encryptionKey = props.encryptionKey;
165+
this.secretArn = resource.secretArn;
166+
}
167+
168+
public export(): SecretImportProps {
169+
return {
170+
encryptionKey: this.encryptionKey,
171+
secretArn: this.secretArn,
172+
};
173+
}
174+
}
175+
176+
/**
177+
* Configuration to generate secrets such as passwords automatically.
178+
*/
179+
export interface SecretStringGenerator {
180+
/**
181+
* Specifies that the generated password shouldn't include uppercase letters.
182+
*
183+
* @default false
184+
*/
185+
excludeUppercase?: boolean;
186+
187+
/**
188+
* Specifies whether the generated password must include at least one of every allowed character type.
189+
*
190+
* @default true
191+
*/
192+
requireEachIncludedType?: boolean;
193+
194+
/**
195+
* Specifies that the generated password can include the space character.
196+
*
197+
* @default false
198+
*/
199+
includeSpace?: boolean;
200+
201+
/**
202+
* A string that includes characters that shouldn't be included in the generated password. The string can be a minimum
203+
* of ``0`` and a maximum of ``4096`` characters long.
204+
*
205+
* @default no exclusions
206+
*/
207+
excludeCharacters?: string;
208+
209+
/**
210+
* The desired length of the generated password.
211+
*
212+
* @default 32
213+
*/
214+
passwordLength?: number;
215+
216+
/**
217+
* Specifies that the generated password shouldn't include punctuation characters.
218+
*
219+
* @default false
220+
*/
221+
excludePunctuation?: boolean;
222+
223+
/**
224+
* Specifies that the generated password shouldn't include lowercase letters.
225+
*
226+
* @default false
227+
*/
228+
excludeLowercase?: boolean;
229+
230+
/**
231+
* Specifies that the generated password shouldn't include digits.
232+
*
233+
* @default false
234+
*/
235+
excludeNumbers?: boolean;
236+
}
237+
238+
/**
239+
* Configuration to generate secrets such as passwords automatically, and include them in a JSON object template.
240+
*/
241+
export interface TemplatedSecretStringGenerator extends SecretStringGenerator {
242+
/**
243+
* The JSON key name that's used to add the generated password to the JSON structure specified by the
244+
* ``secretStringTemplate`` parameter.
245+
*/
246+
generateStringKey: string;
247+
248+
/**
249+
* A properly structured JSON string that the generated password can be added to. The ``generateStringKey`` is
250+
* combined with the generated random string and inserted into the JSON structure that's specified by this parameter.
251+
* The merged JSON string is returned as the completed SecretString of the secret.
252+
*/
253+
secretStringTemplate: string;
254+
}
255+
256+
class ImportedSecret extends SecretBase {
257+
public readonly encryptionKey?: kms.IEncryptionKey;
258+
public readonly secretArn: string;
259+
260+
constructor(scope: cdk.Construct, id: string, private readonly props: SecretImportProps) {
261+
super(scope, id);
262+
263+
this.encryptionKey = props.encryptionKey;
264+
this.secretArn = props.secretArn;
265+
}
266+
267+
public export() {
268+
return this.props;
269+
}
270+
}

packages/@aws-cdk/aws-secretsmanager/package.json

+7-2
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,20 @@
5858
"@aws-cdk/assert": "^0.23.0",
5959
"cdk-build-tools": "^0.23.0",
6060
"cfn2ts": "^0.23.0",
61-
"pkglint": "^0.23.0"
61+
"pkglint": "^0.23.0",
62+
"cdk-integ-tools": "^0.23.0"
6263
},
6364
"dependencies": {
65+
"@aws-cdk/aws-kms": "^0.23.0",
66+
"@aws-cdk/aws-iam": "^0.23.0",
6467
"@aws-cdk/cdk": "^0.23.0"
6568
},
6669
"peerDependencies": {
70+
"@aws-cdk/aws-kms": "^0.23.0",
71+
"@aws-cdk/aws-iam": "^0.23.0",
6772
"@aws-cdk/cdk": "^0.23.0"
6873
},
6974
"engines": {
7075
"node": ">= 8.10.0"
7176
}
72-
}
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"Resources": {
3+
"TestRole6C9272DF": {
4+
"Type": "AWS::IAM::Role",
5+
"Properties": {
6+
"AssumeRolePolicyDocument": {
7+
"Statement": [
8+
{
9+
"Action": "sts:AssumeRole",
10+
"Effect": "Allow",
11+
"Principal": {
12+
"AWS": {
13+
"Fn::Join": [
14+
"",
15+
[
16+
"arn:",
17+
{
18+
"Ref": "AWS::Partition"
19+
},
20+
":iam::",
21+
{
22+
"Ref": "AWS::AccountId"
23+
},
24+
":root"
25+
]
26+
]
27+
}
28+
}
29+
}
30+
],
31+
"Version": "2012-10-17"
32+
}
33+
}
34+
},
35+
"TestRoleDefaultPolicyD1C92014": {
36+
"Type": "AWS::IAM::Policy",
37+
"Properties": {
38+
"PolicyDocument": {
39+
"Statement": [
40+
{
41+
"Action": "secretsmanager:GetSecretValue",
42+
"Effect": "Allow",
43+
"Resource": {
44+
"Ref": "SecretA720EF05"
45+
}
46+
}
47+
],
48+
"Version": "2012-10-17"
49+
},
50+
"PolicyName": "TestRoleDefaultPolicyD1C92014",
51+
"Roles": [
52+
{
53+
"Ref": "TestRole6C9272DF"
54+
}
55+
]
56+
}
57+
},
58+
"SecretA720EF05": {
59+
"Type": "AWS::SecretsManager::Secret",
60+
"Properties": {
61+
"GenerateSecretString": {}
62+
}
63+
}
64+
}
65+
}

0 commit comments

Comments
 (0)