Skip to content

Commit 49d3863

Browse files
committed
KeyGrants
1 parent 14b0e0a commit 49d3863

File tree

1 file changed

+122
-89
lines changed
  • packages/aws-cdk-lib/aws-kms/lib

1 file changed

+122
-89
lines changed

packages/aws-cdk-lib/aws-kms/lib/key.ts

Lines changed: 122 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { KeyLookupOptions } from './key-lookup';
44
import { CfnKey, IKeyRef, KeyReference } from './kms.generated';
55
import * as perms from './private/perms';
66
import * as iam from '../../aws-iam';
7+
import { IResourceWithPolicy } from '../../aws-iam';
78
import * as cxschema from '../../cloud-assembly-schema';
89
import {
910
Arn,
@@ -106,6 +107,119 @@ export interface IKey extends IResource, IKeyRef {
106107
grantVerifyMac(grantee: iam.IGrantable): iam.Grant;
107108
}
108109

110+
/**
111+
* Allows granting of KMS key permissions to principals.
112+
*/
113+
export class KeyGrants {
114+
/**
115+
* Create a KeyGrants for a key with a resource policy.
116+
*/
117+
public static fromKeyWithPolicy(key: IKeyRef & IResourceWithPolicy): KeyGrants {
118+
return new KeyGrants(key);
119+
}
120+
121+
private constructor(private readonly key: IKeyRef & IResourceWithPolicy) {
122+
}
123+
124+
/**
125+
* Grant the indicated permissions on this key to the given principal
126+
*
127+
* This modifies both the principal's policy as well as the resource policy,
128+
* since the default CloudFormation setup for KMS keys is that the policy
129+
* must not be empty and so default grants won't work.
130+
*/
131+
public grant(grantee: iam.IGrantable, trustAccountIdentities = true, ...actions: string[]): iam.Grant {
132+
// KMS verifies whether the principals included in its key policy actually exist.
133+
// This is a problem if the stack the grantee is part of depends on the key stack
134+
// (as it won't exist before the key policy is attempted to be created).
135+
// In that case, make the account the resource policy principal
136+
const granteeStackDependsOnKeyStack = this.granteeStackDependsOnKeyStack(grantee);
137+
const principal = granteeStackDependsOnKeyStack
138+
? new iam.AccountPrincipal(granteeStackDependsOnKeyStack)
139+
: grantee.grantPrincipal;
140+
141+
const crossAccountAccess = this.isGranteeFromAnotherAccount(grantee);
142+
const crossRegionAccess = this.isGranteeFromAnotherRegion(grantee);
143+
const crossEnvironment = crossAccountAccess || crossRegionAccess;
144+
const grantOptions: iam.GrantWithResourceOptions = {
145+
grantee,
146+
actions,
147+
resource: this.key,
148+
resourceArns: [this.key.keyRef.keyArn],
149+
resourceSelfArns: crossEnvironment ? undefined : ['*'],
150+
};
151+
152+
if (trustAccountIdentities && !crossEnvironment) {
153+
return iam.Grant.addToPrincipalOrResource(grantOptions);
154+
} else {
155+
return iam.Grant.addToPrincipalAndResource({
156+
...grantOptions,
157+
// if the key is used in a cross-environment matter,
158+
// we can't access the Key ARN (they don't have physical names),
159+
// so fall back to using '*'. ToDo we need to make this better... somehow
160+
resourceArns: crossEnvironment ? ['*'] : [this.key.keyRef.keyArn],
161+
resourcePolicyPrincipal: principal,
162+
});
163+
}
164+
}
165+
166+
/**
167+
* Checks whether the grantee belongs to a stack that will be deployed
168+
* after the stack containing this key.
169+
*
170+
* @param grantee the grantee to give permissions to
171+
* @returns the account ID of the grantee stack if its stack does depend on this stack,
172+
* undefined otherwise
173+
*/
174+
private granteeStackDependsOnKeyStack(grantee: iam.IGrantable): string | undefined {
175+
const grantPrincipal = grantee.grantPrincipal;
176+
// this logic should only apply to newly created
177+
// (= not imported) resources
178+
if (!iam.principalIsOwnedResource(grantPrincipal)) {
179+
return undefined;
180+
}
181+
const keyStack = Stack.of(this.key);
182+
const granteeStack = Stack.of(grantPrincipal);
183+
if (keyStack === granteeStack) {
184+
return undefined;
185+
}
186+
187+
return granteeStack.dependencies.includes(keyStack)
188+
? granteeStack.account
189+
: undefined;
190+
}
191+
192+
private isGranteeFromAnotherAccount(grantee: iam.IGrantable): boolean {
193+
if (!iam.principalIsOwnedResource(grantee.grantPrincipal)) {
194+
return false;
195+
}
196+
const bucketStack = Stack.of(this.key);
197+
const identityStack = Stack.of(grantee.grantPrincipal);
198+
199+
if (FeatureFlags.of(this.key).isEnabled(cxapi.KMS_REDUCE_CROSS_ACCOUNT_REGION_POLICY_SCOPE)) {
200+
// if two compared stacks have the same region, this should return 'false' since it's from the
201+
// same region; if two stacks have different region, then compare env.account
202+
return bucketStack.account !== identityStack.account && this.key.env.account !== identityStack.account;
203+
}
204+
return bucketStack.account !== identityStack.account;
205+
}
206+
207+
private isGranteeFromAnotherRegion(grantee: iam.IGrantable): boolean {
208+
if (!iam.principalIsOwnedResource(grantee.grantPrincipal)) {
209+
return false;
210+
}
211+
const bucketStack = Stack.of(this.key);
212+
const identityStack = Stack.of(grantee.grantPrincipal);
213+
214+
if (FeatureFlags.of(this.key).isEnabled(cxapi.KMS_REDUCE_CROSS_ACCOUNT_REGION_POLICY_SCOPE)) {
215+
// if two compared stacks have the same region, this should return 'false' since it's from the
216+
// same region; if two stacks have different region, then compare env.region
217+
return bucketStack.region !== identityStack.region && this.key.env.region !== identityStack.region;
218+
}
219+
return bucketStack.region !== identityStack.region;
220+
}
221+
}
222+
109223
abstract class KeyBase extends Resource implements IKey {
110224
/**
111225
* The ARN of the key.
@@ -114,6 +228,11 @@ abstract class KeyBase extends Resource implements IKey {
114228

115229
public abstract readonly keyId: string;
116230

231+
/**
232+
* Allows granting of KMS key permissions to principals.
233+
*/
234+
public readonly grants = KeyGrants.fromKeyWithPolicy(this);
235+
117236
/**
118237
* Optional policy document that represents the resource policy of this key.
119238
*
@@ -190,37 +309,7 @@ abstract class KeyBase extends Resource implements IKey {
190309
* must not be empty and so default grants won't work.
191310
*/
192311
public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant {
193-
// KMS verifies whether the principals included in its key policy actually exist.
194-
// This is a problem if the stack the grantee is part of depends on the key stack
195-
// (as it won't exist before the key policy is attempted to be created).
196-
// In that case, make the account the resource policy principal
197-
const granteeStackDependsOnKeyStack = this.granteeStackDependsOnKeyStack(grantee);
198-
const principal = granteeStackDependsOnKeyStack
199-
? new iam.AccountPrincipal(granteeStackDependsOnKeyStack)
200-
: grantee.grantPrincipal;
201-
202-
const crossAccountAccess = this.isGranteeFromAnotherAccount(grantee);
203-
const crossRegionAccess = this.isGranteeFromAnotherRegion(grantee);
204-
const crossEnvironment = crossAccountAccess || crossRegionAccess;
205-
const grantOptions: iam.GrantWithResourceOptions = {
206-
grantee,
207-
actions,
208-
resource: this,
209-
resourceArns: [this.keyArn],
210-
resourceSelfArns: crossEnvironment ? undefined : ['*'],
211-
};
212-
if (this.trustAccountIdentities && !crossEnvironment) {
213-
return iam.Grant.addToPrincipalOrResource(grantOptions);
214-
} else {
215-
return iam.Grant.addToPrincipalAndResource({
216-
...grantOptions,
217-
// if the key is used in a cross-environment matter,
218-
// we can't access the Key ARN (they don't have physical names),
219-
// so fall back to using '*'. ToDo we need to make this better... somehow
220-
resourceArns: crossEnvironment ? ['*'] : [this.keyArn],
221-
resourcePolicyPrincipal: principal,
222-
});
223-
}
312+
return this.grants.grant(grantee, this.trustAccountIdentities, ...actions);
224313
}
225314

226315
/**
@@ -278,62 +367,6 @@ abstract class KeyBase extends Resource implements IKey {
278367
public grantVerifyMac(grantee: iam.IGrantable): iam.Grant {
279368
return this.grant(grantee, ...perms.VERIFY_HMAC_ACTIONS);
280369
}
281-
282-
/**
283-
* Checks whether the grantee belongs to a stack that will be deployed
284-
* after the stack containing this key.
285-
*
286-
* @param grantee the grantee to give permissions to
287-
* @returns the account ID of the grantee stack if its stack does depend on this stack,
288-
* undefined otherwise
289-
*/
290-
private granteeStackDependsOnKeyStack(grantee: iam.IGrantable): string | undefined {
291-
const grantPrincipal = grantee.grantPrincipal;
292-
// this logic should only apply to newly created
293-
// (= not imported) resources
294-
if (!iam.principalIsOwnedResource(grantPrincipal)) {
295-
return undefined;
296-
}
297-
const keyStack = Stack.of(this);
298-
const granteeStack = Stack.of(grantPrincipal);
299-
if (keyStack === granteeStack) {
300-
return undefined;
301-
}
302-
303-
return granteeStack.dependencies.includes(keyStack)
304-
? granteeStack.account
305-
: undefined;
306-
}
307-
308-
private isGranteeFromAnotherRegion(grantee: iam.IGrantable): boolean {
309-
if (!iam.principalIsOwnedResource(grantee.grantPrincipal)) {
310-
return false;
311-
}
312-
const bucketStack = Stack.of(this);
313-
const identityStack = Stack.of(grantee.grantPrincipal);
314-
315-
if (FeatureFlags.of(this).isEnabled(cxapi.KMS_REDUCE_CROSS_ACCOUNT_REGION_POLICY_SCOPE)) {
316-
// if two compared stacks have the same region, this should return 'false' since it's from the
317-
// same region; if two stacks have different region, then compare env.region
318-
return bucketStack.region !== identityStack.region && this.env.region !== identityStack.region;
319-
}
320-
return bucketStack.region !== identityStack.region;
321-
}
322-
323-
private isGranteeFromAnotherAccount(grantee: iam.IGrantable): boolean {
324-
if (!iam.principalIsOwnedResource(grantee.grantPrincipal)) {
325-
return false;
326-
}
327-
const bucketStack = Stack.of(this);
328-
const identityStack = Stack.of(grantee.grantPrincipal);
329-
330-
if (FeatureFlags.of(this).isEnabled(cxapi.KMS_REDUCE_CROSS_ACCOUNT_REGION_POLICY_SCOPE)) {
331-
// if two compared stacks have the same region, this should return 'false' since it's from the
332-
// same region; if two stacks have different region, then compare env.account
333-
return bucketStack.account !== identityStack.account && this.env.account !== identityStack.account;
334-
}
335-
return bucketStack.account !== identityStack.account;
336-
}
337370
}
338371

339372
/**
@@ -711,8 +744,8 @@ export class Key extends KeyBase {
711744
// We might make this parsing logic smarter later,
712745
// but let's start by erroring out.
713746
throw new ValidationError('Could not parse the PolicyDocument of the passed AWS::KMS::Key resource because it contains CloudFormation functions. ' +
714-
'This makes it impossible to create a mutable IKey from that Policy. ' +
715-
'You have to use fromKeyArn instead, passing it the ARN attribute property of the low-level CfnKey', cfnKey);
747+
'This makes it impossible to create a mutable IKey from that Policy. ' +
748+
'You have to use fromKeyArn instead, passing it the ARN attribute property of the low-level CfnKey', cfnKey);
716749
}
717750

718751
// change the key policy of the L1, so that all changes done in the L2 are reflected in the resulting template

0 commit comments

Comments
 (0)