-
Notifications
You must be signed in to change notification settings - Fork 4k
/
Copy pathkey.ts
364 lines (317 loc) · 10.4 KB
/
key.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
import * as iam from '@aws-cdk/aws-iam';
import { Construct, IResource, RemovalPolicy, Resource, Stack } from '@aws-cdk/core';
import { Alias } from './alias';
import { CfnKey } from './kms.generated';
/**
* A KMS Key, either managed by this CDK app, or imported.
*/
export interface IKey extends IResource {
/**
* The ARN of the key.
*
* @attribute
*/
readonly keyArn: string;
/**
* The ID of the key
* (the part that looks something like: 1234abcd-12ab-34cd-56ef-1234567890ab).
*
* @attribute
*/
readonly keyId: string;
/**
* Defines a new alias for the key.
*/
addAlias(alias: string): Alias;
/**
* Adds a statement to the KMS key resource policy.
* @param statement The policy statement to add
* @param allowNoOp If this is set to `false` and there is no policy
* defined (i.e. external key), the operation will fail. Otherwise, it will
* no-op.
*/
addToResourcePolicy(statement: iam.PolicyStatement, allowNoOp?: boolean): void;
/**
* Grant the indicated permissions on this key to the given principal
*/
grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant;
/**
* Grant decryption permisisons using this key to the given principal
*/
grantDecrypt(grantee: iam.IGrantable): iam.Grant;
/**
* Grant encryption permisisons using this key to the given principal
*/
grantEncrypt(grantee: iam.IGrantable): iam.Grant;
/**
* Grant encryption and decryption permisisons using this key to the given principal
*/
grantEncryptDecrypt(grantee: iam.IGrantable): iam.Grant;
}
abstract class KeyBase extends Resource implements IKey {
/**
* The ARN of the key.
*/
public abstract readonly keyArn: string;
public abstract readonly keyId: string;
/**
* Optional policy document that represents the resource policy of this key.
*
* If specified, addToResourcePolicy can be used to edit this policy.
* Otherwise this method will no-op.
*/
protected abstract readonly policy?: iam.PolicyDocument;
/**
* Collection of aliases added to the key
*
* Tracked to determine whether or not the aliasName should be added to the end of its ID
*/
private readonly aliases: Alias[] = [];
/**
* Defines a new alias for the key.
*/
public addAlias(aliasName: string): Alias {
const aliasId = this.aliases.length > 0 ? `Alias${aliasName}` : 'Alias';
const alias = new Alias(this, aliasId, { aliasName, targetKey: this });
this.aliases.push(alias);
return alias;
}
/**
* Adds a statement to the KMS key resource policy.
* @param statement The policy statement to add
* @param allowNoOp If this is set to `false` and there is no policy
* defined (i.e. external key), the operation will fail. Otherwise, it will
* no-op.
*/
public addToResourcePolicy(statement: iam.PolicyStatement, allowNoOp = true) {
const stack = Stack.of(this);
if (!this.policy) {
if (allowNoOp) { return; }
throw new Error(`Unable to add statement to IAM resource policy for KMS key: ${JSON.stringify(stack.resolve(this.keyArn))}`);
}
this.policy.addStatements(statement);
}
/**
* Grant the indicated permissions on this key to the given principal
*
* This modifies both the principal's policy as well as the resource policy,
* since the default CloudFormation setup for KMS keys is that the policy
* must not be empty and so default grants won't work.
*/
public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant {
// KMS verifies whether the principals included in its key policy actually exist.
// This is a problem if the stack the grantee is part of depends on the key stack
// (as it won't exist before the key policy is attempted to be created).
// In that case, make the account the resource policy principal
const granteeStackDependsOnKeyStack = this.granteeStackDependsOnKeyStack(grantee);
const principal = granteeStackDependsOnKeyStack
? new iam.AccountPrincipal(granteeStackDependsOnKeyStack)
: grantee.grantPrincipal;
const crossAccountAccess = this.isGranteeFromAnotherAccount(grantee);
const crossRegionAccess = this.isGranteeFromAnotherRegion(grantee);
const crossEnvironment = crossAccountAccess || crossRegionAccess;
return iam.Grant.addToPrincipalAndResource({
grantee,
actions,
resource: this,
resourcePolicyPrincipal: principal,
// if the key is used in a cross-environment matter,
// we can't access the Key ARN (they don't have physical names),
// so fall back to using '*'. ToDo we need to make this better... somehow
resourceArns: crossEnvironment ? ['*'] : [this.keyArn],
resourceSelfArns: crossEnvironment ? undefined : ['*'],
});
}
/**
* Grant decryption permisisons using this key to the given principal
*/
public grantDecrypt(grantee: iam.IGrantable): iam.Grant {
return this.grant(grantee,
'kms:Decrypt',
);
}
/**
* Grant encryption permisisons using this key to the given principal
*/
public grantEncrypt(grantee: iam.IGrantable): iam.Grant {
return this.grant(grantee,
'kms:Encrypt',
'kms:ReEncrypt*',
'kms:GenerateDataKey*'
);
}
/**
* Grant encryption and decryption permisisons using this key to the given principal
*/
public grantEncryptDecrypt(grantee: iam.IGrantable): iam.Grant {
return this.grant(grantee,
'kms:Decrypt',
'kms:Encrypt',
'kms:ReEncrypt*',
'kms:GenerateDataKey*'
);
}
/**
* Checks whether the grantee belongs to a stack that will be deployed
* after the stack containing this key.
*
* @param grantee the grantee to give permissions to
* @returns the account ID of the grantee stack if its stack does depend on this stack,
* undefined otherwise
*/
private granteeStackDependsOnKeyStack(grantee: iam.IGrantable): string | undefined {
if (!(Construct.isConstruct(grantee))) {
return undefined;
}
const keyStack = Stack.of(this);
const granteeStack = Stack.of(grantee);
if (keyStack === granteeStack) {
return undefined;
}
return granteeStack.dependencies.includes(keyStack)
? granteeStack.account
: undefined;
}
private isGranteeFromAnotherRegion(grantee: iam.IGrantable): boolean {
if (!(Construct.isConstruct(grantee))) {
return false;
}
const bucketStack = Stack.of(this);
const identityStack = Stack.of(grantee);
return bucketStack.region !== identityStack.region;
}
private isGranteeFromAnotherAccount(grantee: iam.IGrantable): boolean {
if (!(Construct.isConstruct(grantee))) {
return false;
}
const bucketStack = Stack.of(this);
const identityStack = Stack.of(grantee);
return bucketStack.account !== identityStack.account;
}
}
/**
* Construction properties for a KMS Key object
*/
export interface KeyProps {
/**
* A description of the key. Use a description that helps your users decide
* whether the key is appropriate for a particular task.
*
* @default - No description.
*/
readonly description?: string;
/**
* Initial alias to add to the key
*
* More aliases can be added later by calling `addAlias`.
*
* @default - No alias is added for the key.
*/
readonly alias?: string;
/**
* Indicates whether AWS KMS rotates the key.
*
* @default false
*/
readonly enableKeyRotation?: boolean;
/**
* Indicates whether the key is available for use.
*
* @default - Key is enabled.
*/
readonly enabled?: boolean;
/**
* Custom policy document to attach to the KMS key.
*
* @default - A policy document with permissions for the account root to
* administer the key will be created.
*/
readonly policy?: iam.PolicyDocument;
/**
* Whether the encryption key should be retained when it is removed from the Stack. This is useful when one wants to
* retain access to data that was encrypted with a key that is being retired.
*
* @default RemovalPolicy.Retain
*/
readonly removalPolicy?: RemovalPolicy;
}
/**
* Defines a KMS key.
*
* @resource AWS::KMS::Key
*/
export class Key extends KeyBase {
/**
* Import an externally defined KMS Key using its ARN.
*
* @param scope the construct that will "own" the imported key.
* @param id the id of the imported key in the construct tree.
* @param keyArn the ARN of an existing KMS key.
*/
public static fromKeyArn(scope: Construct, id: string, keyArn: string): IKey {
class Import extends KeyBase {
public readonly keyArn = keyArn;
public readonly keyId: string;
protected readonly policy?: iam.PolicyDocument | undefined = undefined;
constructor(keyId: string) {
super(scope, id);
this.keyId = keyId;
}
}
const keyResourceName = Stack.of(scope).parseArn(keyArn).resourceName;
if (!keyResourceName) {
throw new Error(`KMS key ARN must be in the format 'arn:aws:kms:<region>:<account>:key/<keyId>', got: '${keyArn}'`);
}
return new Import(keyResourceName);
}
public readonly keyArn: string;
public readonly keyId: string;
protected readonly policy?: iam.PolicyDocument;
constructor(scope: Construct, id: string, props: KeyProps = {}) {
super(scope, id);
if (props.policy) {
this.policy = props.policy;
} else {
this.policy = new iam.PolicyDocument();
this.allowAccountToAdmin();
}
const resource = new CfnKey(this, 'Resource', {
description: props.description,
enableKeyRotation: props.enableKeyRotation,
enabled: props.enabled,
keyPolicy: this.policy,
});
this.keyArn = resource.attrArn;
this.keyId = resource.ref;
resource.applyRemovalPolicy(props.removalPolicy);
if (props.alias !== undefined) {
this.addAlias(props.alias);
}
}
/**
* Let users from this account admin this key.
* @link https://aws.amazon.com/premiumsupport/knowledge-center/update-key-policy-future/
*/
private allowAccountToAdmin() {
const actions = [
"kms:Create*",
"kms:Describe*",
"kms:Enable*",
"kms:List*",
"kms:Put*",
"kms:Update*",
"kms:Revoke*",
"kms:Disable*",
"kms:Get*",
"kms:Delete*",
"kms:ScheduleKeyDeletion",
"kms:CancelKeyDeletion",
"kms:GenerateDataKey"
];
this.addToResourcePolicy(new iam.PolicyStatement({
resources: ['*'],
actions,
principals: [new iam.AccountRootPrincipal()]
}));
}
}