Skip to content

Commit 9a76fdc

Browse files
authored
fix(core): implicit Aspect applications do not override custom Aspect applications (#34132)
Some CDK methods apply mutating Aspects on behalf of users. Since #32333, these Aspects have a priority of `MUTATING` to classify their behavior. If a user-applied Aspect (priority `DEFAULT`) now configures the same property as an implicitly added Aspect: * Before that change, the relative execution order depended on the location of the Aspects in the construct tree. * After that change, the user Aspect always "wins" (executes last) because its priority is higher. In this change, we roll back to the behavior from pre-2.172.0, and introduce a feature flag which gives the Aspects a priority only if the feature flag is enabled. This introduces the feature flag: ```json { "context": { "@aws-cdk/core:aspectPrioritiesMutating": true } } ``` Which sets the priority of Aspects added on your behalf a priority of `MUTATING` (200) (instead of the default `DEFAULT`, 500). * If you have given your own Aspect a priority of `MUTATING` already to make sure it can get overridden by another Aspect of priority `MUTATING`, this current change will not affect you (either with or without feature flag). * If you have come to rely on the new default priority being low already, you can set the above feature flag to re-enable the new behavior. ----------- Did not touch the following Aspects: - In `integ-tests-alpha`: overriding logical IDs in assertions stacks does not affect production infrastructure. - Tags: tags are exclusively manipulated through the official APIs, so there no conflict between custom and implicit Aspects. - CDK Pipelines: there cannot be a conflict because the customer can't create a default pipeline before the implicit Aspect. This PR also introduces some slight rendering and documentation changes to the feature flags to improve clarity of the purpose of certain fields and the produced report.
1 parent c5365a0 commit 9a76fdc

File tree

27 files changed

+654
-272
lines changed

27 files changed

+654
-272
lines changed

Diff for: packages/@aws-cdk/aws-servicecatalogappregistry-alpha/lib/application-associator.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as cdk from 'aws-cdk-lib/core';
2+
import * as cxapi from 'aws-cdk-lib/cx-api';
23
import { Construct } from 'constructs';
34
import { IApplication } from './application';
45
import { CheckedStageStackAssociator } from './aspects/stack-associator';
@@ -50,7 +51,9 @@ export class ApplicationAssociator extends Construct {
5051
this.associateCrossAccountStacks = targetBindResult.associateCrossAccountStacks;
5152
cdk.Aspects.of(scope).add(new CheckedStageStackAssociator(this, {
5253
associateCrossAccountStacks: this.associateCrossAccountStacks,
53-
}), { priority: cdk.AspectPriority.MUTATING });
54+
}), {
55+
priority: cdk.FeatureFlags.of(this).isEnabled(cxapi.ASPECT_PRIORITIES_MUTATING) ? cdk.AspectPriority.MUTATING : undefined,
56+
});
5457
}
5558

5659
/**
@@ -61,7 +64,9 @@ export class ApplicationAssociator extends Construct {
6164
this.associatedStages.add(stage);
6265
cdk.Aspects.of(stage).add(new CheckedStageStackAssociator(this, {
6366
associateCrossAccountStacks: this.associateCrossAccountStacks,
64-
}), { priority: cdk.AspectPriority.MUTATING });
67+
}), {
68+
priority: cdk.FeatureFlags.of(this).isEnabled(cxapi.ASPECT_PRIORITIES_MUTATING) ? cdk.AspectPriority.MUTATING : undefined,
69+
});
6570
return stage;
6671
}
6772

Diff for: packages/aws-cdk-lib/aws-autoscaling/lib/auto-scaling-group.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import * as iam from '../../aws-iam';
1717
import * as sns from '../../aws-sns';
1818
import {
1919
Annotations,
20-
AspectPriority,
2120
Aspects,
2221
Aws,
2322
CfnAutoScalingRollingUpdate, CfnCreationPolicy, CfnUpdatePolicy,
@@ -26,6 +25,7 @@ import {
2625
Tokenization, UnscopedValidationError, ValidationError, withResolved,
2726
} from '../../core';
2827
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';
28+
import { mutatingAspectPrio32333 } from '../../core/lib/private/aspect-prio';
2929
import { AUTOSCALING_GENERATE_LAUNCH_TEMPLATE } from '../../cx-api';
3030

3131
/**
@@ -1608,7 +1608,9 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements
16081608
this.spotPrice = props.spotPrice;
16091609

16101610
if (props.requireImdsv2) {
1611-
Aspects.of(this).add(new AutoScalingGroupRequireImdsv2Aspect(), { priority: AspectPriority.MUTATING });
1611+
Aspects.of(this).add(new AutoScalingGroupRequireImdsv2Aspect(), {
1612+
priority: mutatingAspectPrio32333(this),
1613+
});
16121614
}
16131615

16141616
this.node.addValidation({ validate: () => this.validateTargetGroup() });

Diff for: packages/aws-cdk-lib/aws-backup/lib/selection.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { BackupableResourcesCollector } from './backupable-resources-collector';
44
import { IBackupPlan } from './plan';
55
import { BackupResource, TagOperation } from './resource';
66
import * as iam from '../../aws-iam';
7-
import { Lazy, Resource, Aspects, AspectPriority } from '../../core';
7+
import { Lazy, Resource, Aspects } from '../../core';
88
import { addConstructMetadata } from '../../core/lib/metadata-resource';
9+
import { mutatingAspectPrio32333 } from '../../core/lib/private/aspect-prio';
910

1011
/**
1112
* Options for a BackupSelection
@@ -143,7 +144,9 @@ export class BackupSelection extends Resource implements iam.IGrantable {
143144
}
144145

145146
if (resource.construct) {
146-
Aspects.of(resource.construct).add(this.backupableResourcesCollector, { priority: AspectPriority.MUTATING });
147+
Aspects.of(resource.construct).add(this.backupableResourcesCollector, {
148+
priority: mutatingAspectPrio32333(resource.construct),
149+
});
147150
// Cannot push `this.backupableResourcesCollector.resources` to
148151
// `this.resources` here because it has not been evaluated yet.
149152
// Will be concatenated to `this.resources` in a `Lazy.list`

Diff for: packages/aws-cdk-lib/aws-ec2/lib/instance.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import { UserData } from './user-data';
1515
import { BlockDevice } from './volume';
1616
import { IVpc, Subnet, SubnetSelection } from './vpc';
1717
import * as iam from '../../aws-iam';
18-
import { Annotations, AspectPriority, Aspects, Duration, FeatureFlags, Fn, IResource, Lazy, Resource, Stack, Tags, Token } from '../../core';
18+
import { Annotations, Aspects, Duration, FeatureFlags, Fn, IResource, Lazy, Resource, Stack, Tags, Token } from '../../core';
1919
import { md5hash } from '../../core/lib/helpers-internal';
2020
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';
21+
import { mutatingAspectPrio32333 } from '../../core/lib/private/aspect-prio';
2122
import * as cxapi from '../../cx-api';
2223

2324
/**
@@ -671,7 +672,9 @@ export class Instance extends Resource implements IInstance {
671672
}));
672673

673674
if (props.requireImdsv2) {
674-
Aspects.of(this).add(new InstanceRequireImdsv2Aspect(), { priority: AspectPriority.MUTATING });
675+
Aspects.of(this).add(new InstanceRequireImdsv2Aspect(), {
676+
priority: mutatingAspectPrio32333(this),
677+
});
675678
}
676679
}
677680

Diff for: packages/aws-cdk-lib/aws-ecs/lib/cluster.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ import {
2424
IAspect,
2525
Token,
2626
Names,
27-
AspectPriority,
2827
FeatureFlags, Annotations,
2928
} from '../../core';
3029
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';
30+
import { mutatingAspectPrio32333 } from '../../core/lib/private/aspect-prio';
3131
import { Disable_ECS_IMDS_Blocking, Enable_IMDS_Blocking_Deprecated_Feature } from '../../cx-api';
3232

3333
const CLUSTER_SYMBOL = Symbol.for('@aws-cdk/aws-ecs/lib/cluster.Cluster');
@@ -331,7 +331,9 @@ export class Cluster extends Resource implements ICluster {
331331
// since it's harmless, but we'd prefer not to add unexpected new
332332
// resources to the stack which could surprise users working with
333333
// brown-field CDK apps and stacks.
334-
Aspects.of(this).add(new MaybeCreateCapacityProviderAssociations(this, id), { priority: AspectPriority.MUTATING });
334+
Aspects.of(this).add(new MaybeCreateCapacityProviderAssociations(this, id), {
335+
priority: mutatingAspectPrio32333(this),
336+
});
335337
}
336338

337339
/**

Diff for: packages/aws-cdk-lib/aws-iam/lib/permissions-boundary.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { IConstruct } from 'constructs';
22
import { CfnRole, CfnUser } from './iam.generated';
33
import { IManagedPolicy } from './managed-policy';
4-
import { AspectPriority, Aspects, CfnResource } from '../../core';
4+
import { Aspects, CfnResource } from '../../core';
5+
import { mutatingAspectPrio32333 } from '../../core/lib/private/aspect-prio';
56

67
/**
78
* Modify the Permissions Boundaries of Users and Roles in a construct tree
@@ -40,7 +41,9 @@ export class PermissionsBoundary {
4041
node.addPropertyOverride('PermissionsBoundary', boundaryPolicy.managedPolicyArn);
4142
}
4243
},
43-
}, { priority: AspectPriority.MUTATING });
44+
}, {
45+
priority: mutatingAspectPrio32333(this.scope),
46+
});
4447
}
4548

4649
/**
@@ -56,6 +59,8 @@ export class PermissionsBoundary {
5659
node.addPropertyDeletionOverride('PermissionsBoundary');
5760
}
5861
},
59-
}, { priority: AspectPriority.MUTATING });
62+
}, {
63+
priority: mutatingAspectPrio32333(this.scope),
64+
});
6065
}
6166
}

Diff for: packages/aws-cdk-lib/aws-iam/lib/role.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ import { ImportedRole } from './private/imported-role';
1313
import { MutatingPolicyDocumentAdapter } from './private/policydoc-adapter';
1414
import { PrecreatedRole } from './private/precreated-role';
1515
import { AttachedPolicies, UniqueStringSet } from './private/util';
16-
import { ArnFormat, Duration, Resource, Stack, Token, TokenComparison, Aspects, Annotations, RemovalPolicy, AspectPriority } from '../../core';
16+
import { ArnFormat, Duration, Resource, Stack, Token, TokenComparison, Aspects, Annotations, RemovalPolicy } from '../../core';
1717
import { getCustomizeRolesConfig, getPrecreatedRoleConfig, CUSTOMIZE_ROLES_CONTEXT_KEY, CustomizeRoleConfig } from '../../core/lib/helpers-internal';
1818
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';
19+
import { mutatingAspectPrio32333 } from '../../core/lib/private/aspect-prio';
1920

2021
const MAX_INLINE_SIZE = 10000;
2122
const MAX_MANAGEDPOL_SIZE = 6000;
@@ -496,7 +497,9 @@ export class Role extends Resource implements IRole {
496497
this.splitLargePolicy();
497498
}
498499
},
499-
}, { priority: AspectPriority.MUTATING });
500+
}, {
501+
priority: mutatingAspectPrio32333(this),
502+
});
500503
}
501504

502505
this.policyFragment = new ArnPrincipal(this.roleArn).policyFragment;

Diff for: packages/aws-cdk-lib/aws-iam/test/permissions-boundary.test.ts

+42-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as path from 'path';
22
import { Match, Template } from '../../assertions';
3-
import { App, CfnResource, CustomResourceProvider, CustomResourceProviderRuntime, Stack } from '../../core';
3+
import { App, AspectPriority, Aspects, CfnResource, CustomResourceProvider, CustomResourceProviderRuntime, Stack } from '../../core';
44
import * as iam from '../lib';
55

66
let app: App;
@@ -166,3 +166,44 @@ test('unapply inherited boundary from a user: order 2', () => {
166166
PermissionsBoundary: Match.absent(),
167167
});
168168
});
169+
170+
test.each([
171+
[undefined, false, 'OVERRIDDEN'],
172+
[AspectPriority.MUTATING, false, 'OVERRIDDEN'],
173+
[AspectPriority.MUTATING, true, 'OVERRIDDEN'],
174+
// custom DEFAULT, builtin MUTATING: custom wins and override is not applied
175+
[undefined, true, 'BASE'],
176+
])('overriding works if base PB is applied using Aspect with prio %p (feature flag %p)', (basePrio, featureFlag, winner) => {
177+
// When a custom aspect is used to apply a permissions boundary, and the built-in APIs to override it,
178+
// the override still works.
179+
180+
if (featureFlag !== undefined) {
181+
app = new App({ context: { '@aws-cdk/core:aspectPrioritiesMutating': featureFlag } });
182+
stack = new Stack(app, 'Stack');
183+
}
184+
185+
// GIVEN
186+
Aspects.of(stack).add({
187+
visit(node) {
188+
if (node instanceof CfnResource && node.cfnResourceType === 'AWS::IAM::Role') {
189+
node.addPropertyOverride('PermissionsBoundary', 'BASE');
190+
}
191+
},
192+
}, {
193+
priority: basePrio,
194+
});
195+
196+
const role = new iam.Role(stack, 'Role', {
197+
assumedBy: new iam.AnyPrincipal(),
198+
});
199+
200+
// WHEN
201+
iam.PermissionsBoundary.of(role).apply({
202+
managedPolicyArn: 'OVERRIDDEN',
203+
});
204+
205+
// THEN
206+
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', {
207+
PermissionsBoundary: winner,
208+
});
209+
});

Diff for: packages/aws-cdk-lib/aws-servicecatalog/lib/portfolio.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { IBucket } from '../../aws-s3';
1515
import * as sns from '../../aws-sns';
1616
import * as cdk from '../../core';
1717
import { addConstructMetadata } from '../../core/lib/metadata-resource';
18+
import { mutatingAspectPrio32333 } from '../../core/lib/private/aspect-prio';
1819

1920
/**
2021
* Options for portfolio share.
@@ -369,7 +370,9 @@ export class Portfolio extends PortfolioBase {
369370
(c as Portfolio).addBucketPermissionsToSharedAccounts();
370371
}
371372
},
372-
}, { priority: cdk.AspectPriority.MUTATING });
373+
}, {
374+
priority: mutatingAspectPrio32333(this),
375+
});
373376
}
374377

375378
protected generateUniqueHash(value: string): string {

Diff for: packages/aws-cdk-lib/core/lib/private/aspect-prio.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { IConstruct } from 'constructs';
2+
import * as cxapi from '../../../cx-api';
3+
import { AspectPriority } from '../aspect';
4+
import { FeatureFlags } from '../feature-flags';
5+
6+
/**
7+
* Return the aspect priority of Aspects changed in https://github.com/aws/aws-cdk/pull/32333
8+
*
9+
* We retroactively made those controllable using a feature flag.
10+
*
11+
* Aspects newly added since this change should unconditionally have a priority of `MUTATING`.
12+
*/
13+
export function mutatingAspectPrio32333(scope: IConstruct) {
14+
return FeatureFlags.of(scope).isEnabled(cxapi.ASPECT_PRIORITIES_MUTATING) ? AspectPriority.MUTATING : undefined;
15+
}

Diff for: packages/aws-cdk-lib/core/lib/removal-policies.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { IConstruct } from 'constructs';
22
import { Annotations } from './annotations';
33
import { Aspects, IAspect, AspectPriority } from './aspect';
44
import { CfnResource } from './cfn-resource';
5+
import { mutatingAspectPrio32333 } from './private/aspect-prio';
56
import { RemovalPolicy } from './removal-policy';
67

78
/**
@@ -137,7 +138,7 @@ export class RemovalPolicies {
137138
*/
138139
public apply(policy: RemovalPolicy, props: RemovalPolicyProps = {}) {
139140
Aspects.of(this.scope).add(new RemovalPolicyAspect(policy, props), {
140-
priority: props.priority ?? AspectPriority.MUTATING,
141+
priority: props.priority ?? mutatingAspectPrio32333(this.scope),
141142
});
142143
}
143144

Diff for: packages/aws-cdk-lib/core/lib/stack.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { IConstruct, Construct, Node } from 'constructs';
44
import { Annotations } from './annotations';
55
import { App } from './app';
66
import { Arn, ArnComponents, ArnFormat } from './arn';
7-
import { AspectPriority, Aspects } from './aspect';
7+
import { Aspects } from './aspect';
88
import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from './assets';
99
import { CfnElement } from './cfn-element';
1010
import { Fn } from './cfn-fn';
@@ -584,7 +584,9 @@ export class Stack extends Construct implements ITaggable {
584584
node.addPropertyOverride('PermissionsBoundary', permissionsBoundaryArn);
585585
}
586586
},
587-
}, { priority: AspectPriority.MUTATING });
587+
}, {
588+
priority: mutatingAspectPrio32333(this),
589+
});
588590
}
589591
}
590592

@@ -1838,4 +1840,5 @@ import { deployTimeLookup } from './private/region-lookup';
18381840
import { makeUniqueResourceName } from './private/unique-resource-name';
18391841
import { PRIVATE_CONTEXT_DEFAULT_STACK_SYNTHESIZER } from './private/private-context';
18401842
import { Intrinsic } from './private/intrinsic';
1843+
import { mutatingAspectPrio32333 } from './private/aspect-prio';
18411844
/* eslint-enable import/order */

Diff for: packages/aws-cdk-lib/core/lib/stage.ts

+14
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,20 @@ export interface StageProps {
6868
* Options for applying a permissions boundary to all IAM Roles
6969
* and Users created within this Stage
7070
*
71+
* Be aware that this feature uses Aspects, and the Aspects are applied at the
72+
* Stack level with a priority of `MUTATING` (if the feature flag
73+
* `@aws-cdk/core:aspectPrioritiesMutating` is set) or `DEFAULT` (if the flag
74+
* is not set). This is relevant if you are both using your own Aspects to
75+
* assign Permissions Boundaries, as well as specifying this property. The
76+
* Aspect added by this property will overwrite the Permissions Boundary
77+
* assigned by your own Aspect if both: (a) your Aspect has a lower or equal
78+
* priority to the automatic Aspect, and (b) your Aspect is applied *above*
79+
* the Stack level. If either of those conditions are not true, your own
80+
* Aspect will win.
81+
*
82+
* We recommend assigning Permissions Boundaries only using the provided APIs,
83+
* and not using custom Aspects.
84+
*
7185
* @default - no permissions boundary is applied
7286
*/
7387
readonly permissionsBoundary?: PermissionsBoundary;

Diff for: packages/aws-cdk-lib/core/lib/tag-aspect.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Construct, IConstruct } from 'constructs';
22
import { Annotations } from './annotations';
3-
import { IAspect, Aspects, AspectPriority, AspectOptions } from './aspect';
3+
import { IAspect, Aspects, AspectOptions } from './aspect';
4+
import { mutatingAspectPrio32333 } from './private/aspect-prio';
45
import { ITaggable, ITaggableV2, TagManager } from './tag-manager';
56

67
/**
@@ -160,7 +161,7 @@ export class Tags {
160161
*/
161162
public add(key: string, value: string, props: TagProps = {}) {
162163
const tag = new Tag(key, value, props);
163-
const options: AspectOptions = { priority: AspectPriority.MUTATING };
164+
const options: AspectOptions = { priority: mutatingAspectPrio32333(this.scope) };
164165
Aspects.of(this.scope).add(tag, options);
165166
}
166167

@@ -169,7 +170,7 @@ export class Tags {
169170
*/
170171
public remove(key: string, props: TagProps = {}) {
171172
const removeTag = new RemoveTag(key, props);
172-
const options: AspectOptions = { priority: AspectPriority.MUTATING };
173+
const options: AspectOptions = { priority: mutatingAspectPrio32333(this.scope) };
173174
Aspects.of(this.scope).add(removeTag, options);
174175
}
175176
}

Diff for: packages/aws-cdk-lib/core/test/aspect.test.ts

-1
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,6 @@ describe('aspect', () => {
232232
// THEN - check that Tags Aspect is applied to stack with mutating priority
233233
let aspectApplications = Aspects.of(stack).applied;
234234
expect(aspectApplications.length).toEqual(2);
235-
expect(aspectApplications[1].priority).toEqual(AspectPriority.MUTATING);
236235

237236
// THEN - both Aspects are successfully applied, new logging bucket is added with versioning enabled
238237
Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', {

Diff for: packages/aws-cdk-lib/core/test/tag-aspect.test.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,9 @@ describe('tag aspect', () => {
144144
expect(res2.tags.renderTags()).toEqual([{ key: 'first', value: 'there is only 1' }]);
145145
});
146146

147-
test('Tags applied without priority get mutating priority value', () => {
147+
test.each([false, true])('Tags applied without priority get priority value that depends on feature flag %p', (flag) => {
148148
const root = new Stack();
149+
root.node.setContext('@aws-cdk/core:aspectPrioritiesMutating', flag);
149150
const res = new TaggableResource(root, 'FakeResource', {
150151
type: 'AWS::Fake::Thing',
151152
});
@@ -154,11 +155,13 @@ describe('tag aspect', () => {
154155
Tags.of(res).add('first', 'there is only 1');
155156
Tags.of(res).remove('root');
156157

158+
const expected = flag ? AspectPriority.MUTATING : AspectPriority.DEFAULT;
159+
157160
const rootAspectApplications = Aspects.of(root).applied;
158-
expect(rootAspectApplications[0].priority).toEqual(AspectPriority.MUTATING);
161+
expect(rootAspectApplications[0].priority).toEqual(expected);
159162
const resAspectApplications = Aspects.of(res).applied;
160-
expect(resAspectApplications[0].priority).toEqual(AspectPriority.MUTATING);
161-
expect(resAspectApplications[1].priority).toEqual(AspectPriority.MUTATING);
163+
expect(resAspectApplications[0].priority).toEqual(expected);
164+
expect(resAspectApplications[1].priority).toEqual(expected);
162165
});
163166

164167
test('add will add a tag and remove will remove a tag if it exists', () => {

0 commit comments

Comments
 (0)