@@ -4,6 +4,7 @@ import { KeyLookupOptions } from './key-lookup';
44import { CfnKey , IKeyRef , KeyReference } from './kms.generated' ;
55import * as perms from './private/perms' ;
66import * as iam from '../../aws-iam' ;
7+ import { IResourceWithPolicy } from '../../aws-iam' ;
78import * as cxschema from '../../cloud-assembly-schema' ;
89import {
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+
109223abstract 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