Skip to content

Commit 43f232d

Browse files
authored
feat(s3): custom role for the bucket notifications handler (#17794)
Allow users to pass a custom role to `Bucket`, which will be used by the notifications handler. Fixes #9918, #13241. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent e0b7d99 commit 43f232d

10 files changed

+140
-41
lines changed

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

+27
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,33 @@ const bucket = s3.Bucket.fromBucketAttributes(this, 'ImportedBucket', {
249249
bucket.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.SnsDestination(topic));
250250
```
251251

252+
When you add an event notification to a bucket, a custom resource is created to
253+
manage the notifications. By default, a new role is created for the Lambda
254+
function that implements this feature. If you want to use your own role instead,
255+
you should provide it in the `Bucket` constructor:
256+
257+
```ts
258+
declare const myRole: iam.IRole;
259+
const bucket = new s3.Bucket(this, 'MyBucket', {
260+
notificationsHandlerRole: myRole,
261+
});
262+
```
263+
264+
Whatever role you provide, the CDK will try to modify it by adding the
265+
permissions from `AWSLambdaBasicExecutionRole` (an AWS managed policy) as well
266+
as the permissions `s3:PutBucketNotification` and `s3:GetBucketNotification`.
267+
If you’re passing an imported role, and you don’t want this to happen, configure
268+
it to be immutable:
269+
270+
```ts
271+
const importedRole = iam.Role.fromRoleArn(this, 'role', 'arn:aws:iam::123456789012:role/RoleName', {
272+
mutable: false,
273+
});
274+
```
275+
276+
> If you provide an imported immutable role, make sure that it has at least all
277+
> the permissions mentioned above. Otherwise, the deployment will fail!
278+
252279
[S3 Bucket Notifications]: https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html
253280

254281

packages/@aws-cdk/aws-s3/lib/bucket.ts

+31-6
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,13 @@ export interface BucketAttributes {
427427
* @default - it's assumed the bucket is in the same region as the scope it's being imported into
428428
*/
429429
readonly region?: string;
430+
431+
/**
432+
* The role to be used by the notifications handler
433+
*
434+
* @default - a new role will be created.
435+
*/
436+
readonly notificationsHandlerRole?: iam.IRole;
430437
}
431438

432439
/**
@@ -484,14 +491,12 @@ export abstract class BucketBase extends Resource implements IBucket {
484491
*/
485492
protected abstract disallowPublicAccess?: boolean;
486493

487-
private readonly notifications: BucketNotifications;
494+
private notifications?: BucketNotifications;
495+
496+
protected notificationsHandlerRole?: iam.IRole;
488497

489498
constructor(scope: Construct, id: string, props: ResourceProps = {}) {
490499
super(scope, id, props);
491-
492-
// defines a BucketNotifications construct. Notice that an actual resource will only
493-
// be added if there are notifications added, so we don't need to condition this.
494-
this.notifications = new BucketNotifications(this, 'Notifications', { bucket: this });
495500
}
496501

497502
/**
@@ -836,7 +841,17 @@ export abstract class BucketBase extends Resource implements IBucket {
836841
* https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html
837842
*/
838843
public addEventNotification(event: EventType, dest: IBucketNotificationDestination, ...filters: NotificationKeyFilter[]) {
839-
this.notifications.addNotification(event, dest, ...filters);
844+
this.withNotifications(notifications => notifications.addNotification(event, dest, ...filters));
845+
}
846+
847+
private withNotifications(cb: (notifications: BucketNotifications) => void) {
848+
if (!this.notifications) {
849+
this.notifications = new BucketNotifications(this, 'Notifications', {
850+
bucket: this,
851+
handlerRole: this.notificationsHandlerRole,
852+
});
853+
}
854+
cb(this.notifications);
840855
}
841856

842857
/**
@@ -1459,6 +1474,13 @@ export interface BucketProps {
14591474
*/
14601475
readonly transferAcceleration?: boolean;
14611476

1477+
/**
1478+
* The role to be used by the notifications handler
1479+
*
1480+
* @default - a new role will be created.
1481+
*/
1482+
readonly notificationsHandlerRole?: iam.IRole;
1483+
14621484
/**
14631485
* Inteligent Tiering Configurations
14641486
*
@@ -1542,6 +1564,7 @@ export class Bucket extends BucketBase {
15421564
public policy?: BucketPolicy = undefined;
15431565
protected autoCreatePolicy = false;
15441566
protected disallowPublicAccess = false;
1567+
protected notificationsHandlerRole = attrs.notificationsHandlerRole;
15451568

15461569
/**
15471570
* Exports this bucket from the stack.
@@ -1629,6 +1652,8 @@ export class Bucket extends BucketBase {
16291652
physicalName: props.bucketName,
16301653
});
16311654

1655+
this.notificationsHandlerRole = props.notificationsHandlerRole;
1656+
16321657
const { bucketEncryption, encryptionKey } = this.parseEncryption(props);
16331658

16341659
Bucket.validateBucketName(this.physicalName);

packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource-handler.ts

+17-9
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import * as cdk from '@aws-cdk/core';
77
// eslint-disable-next-line no-duplicate-imports, import/order
88
import { Construct } from '@aws-cdk/core';
99

10+
export class NotificationsResourceHandlerProps {
11+
role?: iam.IRole;
12+
}
13+
1014
/**
1115
* A Lambda-based custom resource handler that provisions S3 bucket
1216
* notifications for a bucket.
@@ -31,14 +35,14 @@ export class NotificationsResourceHandler extends Construct {
3135
*
3236
* @returns The ARN of the custom resource lambda function.
3337
*/
34-
public static singleton(context: Construct) {
38+
public static singleton(context: Construct, props: NotificationsResourceHandlerProps = {}) {
3539
const root = cdk.Stack.of(context);
3640

3741
// well-known logical id to ensure stack singletonity
3842
const logicalId = 'BucketNotificationsHandler050a0587b7544547bf325f094a3db834';
3943
let lambda = root.node.tryFindChild(logicalId) as NotificationsResourceHandler;
4044
if (!lambda) {
41-
lambda = new NotificationsResourceHandler(root, logicalId);
45+
lambda = new NotificationsResourceHandler(root, logicalId, props);
4246
}
4347

4448
return lambda;
@@ -53,19 +57,19 @@ export class NotificationsResourceHandler extends Construct {
5357
/**
5458
* The role of the handler's lambda function.
5559
*/
56-
public readonly role: iam.Role;
60+
public readonly role: iam.IRole;
5761

58-
constructor(scope: Construct, id: string) {
62+
constructor(scope: Construct, id: string, props: NotificationsResourceHandlerProps = {}) {
5963
super(scope, id);
6064

61-
this.role = new iam.Role(this, 'Role', {
65+
this.role = props.role ?? new iam.Role(this, 'Role', {
6266
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
63-
managedPolicies: [
64-
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
65-
],
6667
});
6768

68-
this.role.addToPolicy(new iam.PolicyStatement({
69+
this.role.addManagedPolicy(
70+
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
71+
);
72+
this.role.addToPrincipalPolicy(new iam.PolicyStatement({
6973
actions: ['s3:PutBucketNotification'],
7074
resources: ['*'],
7175
}));
@@ -95,4 +99,8 @@ export class NotificationsResourceHandler extends Construct {
9599

96100
this.functionArn = resource.getAtt('Arn').toString();
97101
}
102+
103+
public addToRolePolicy(statement: iam.PolicyStatement) {
104+
this.role.addToPrincipalPolicy(statement);
105+
}
98106
}

packages/@aws-cdk/aws-s3/lib/notifications-resource/notifications-resource.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ interface NotificationsProps {
1313
* The bucket to manage notifications for.
1414
*/
1515
bucket: IBucket;
16+
17+
/**
18+
* The role to be used by the lambda handler
19+
*/
20+
handlerRole?: iam.IRole;
1621
}
1722

1823
/**
@@ -36,10 +41,12 @@ export class BucketNotifications extends Construct {
3641
private readonly topicNotifications = new Array<TopicConfiguration>();
3742
private resource?: cdk.CfnResource;
3843
private readonly bucket: IBucket;
44+
private readonly handlerRole?: iam.IRole;
3945

4046
constructor(scope: Construct, id: string, props: NotificationsProps) {
4147
super(scope, id);
4248
this.bucket = props.bucket;
49+
this.handlerRole = props.handlerRole;
4350
}
4451

4552
/**
@@ -102,12 +109,14 @@ export class BucketNotifications extends Construct {
102109
*/
103110
private createResourceOnce() {
104111
if (!this.resource) {
105-
const handler = NotificationsResourceHandler.singleton(this);
112+
const handler = NotificationsResourceHandler.singleton(this, {
113+
role: this.handlerRole,
114+
});
106115

107116
const managed = this.bucket instanceof Bucket;
108117

109118
if (!managed) {
110-
handler.role.addToPolicy(new iam.PolicyStatement({
119+
handler.addToRolePolicy(new iam.PolicyStatement({
111120
actions: ['s3:GetBucketNotification'],
112121
resources: ['*'],
113122
}));

packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json

+18-18
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
"Properties": {
111111
"Code": {
112112
"S3Bucket": {
113-
"Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3Bucket2C6C817C"
113+
"Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3Bucket09A62232"
114114
},
115115
"S3Key": {
116116
"Fn::Join": [
@@ -123,7 +123,7 @@
123123
"Fn::Split": [
124124
"||",
125125
{
126-
"Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6"
126+
"Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE"
127127
}
128128
]
129129
}
@@ -136,7 +136,7 @@
136136
"Fn::Split": [
137137
"||",
138138
{
139-
"Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6"
139+
"Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE"
140140
}
141141
]
142142
}
@@ -228,7 +228,7 @@
228228
"Properties": {
229229
"Code": {
230230
"S3Bucket": {
231-
"Ref": "AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3BucketE1985B35"
231+
"Ref": "AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3BucketB51EC107"
232232
},
233233
"S3Key": {
234234
"Fn::Join": [
@@ -241,7 +241,7 @@
241241
"Fn::Split": [
242242
"||",
243243
{
244-
"Ref": "AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3VersionKey610C6DE2"
244+
"Ref": "AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3VersionKey2B267DB5"
245245
}
246246
]
247247
}
@@ -254,7 +254,7 @@
254254
"Fn::Split": [
255255
"||",
256256
{
257-
"Ref": "AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3VersionKey610C6DE2"
257+
"Ref": "AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3VersionKey2B267DB5"
258258
}
259259
]
260260
}
@@ -297,29 +297,29 @@
297297
}
298298
},
299299
"Parameters": {
300-
"AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3Bucket2C6C817C": {
300+
"AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3Bucket09A62232": {
301301
"Type": "String",
302-
"Description": "S3 bucket for asset \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\""
302+
"Description": "S3 bucket for asset \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\""
303303
},
304-
"AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6": {
304+
"AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE": {
305305
"Type": "String",
306-
"Description": "S3 key for asset version \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\""
306+
"Description": "S3 key for asset version \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\""
307307
},
308-
"AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709ArtifactHash17D48178": {
308+
"AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824ArtifactHash76F8FCF2": {
309309
"Type": "String",
310-
"Description": "Artifact hash for asset \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\""
310+
"Description": "Artifact hash for asset \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\""
311311
},
312-
"AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3BucketE1985B35": {
312+
"AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3BucketB51EC107": {
313313
"Type": "String",
314-
"Description": "S3 bucket for asset \"618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abf\""
314+
"Description": "S3 bucket for asset \"31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6\""
315315
},
316-
"AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3VersionKey610C6DE2": {
316+
"AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3VersionKey2B267DB5": {
317317
"Type": "String",
318-
"Description": "S3 key for asset version \"618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abf\""
318+
"Description": "S3 key for asset version \"31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6\""
319319
},
320-
"AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfArtifactHash467DFC33": {
320+
"AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6ArtifactHashEE982197": {
321321
"Type": "String",
322-
"Description": "Artifact hash for asset \"618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abf\""
322+
"Description": "Artifact hash for asset \"31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6\""
323323
}
324324
}
325325
}

packages/@aws-cdk/aws-s3/test/integ.bucket-inventory.expected.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,4 @@
155155
}
156156
}
157157
}
158-
}
158+
}

packages/@aws-cdk/aws-s3/test/integ.bucket-sharing.lit.expected.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,4 @@
7171
}
7272
}
7373
}
74-
]
74+
]

packages/@aws-cdk/aws-s3/test/integ.bucket.expected.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -173,4 +173,4 @@
173173
}
174174
}
175175
}
176-
}
176+
}

packages/@aws-cdk/aws-s3/test/integ.bucket.url.lit.expected.json

+9-3
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@
4444
[
4545
"https://",
4646
{
47-
"Fn::GetAtt": ["MyBucketF68F3FF0", "RegionalDomainName"]
47+
"Fn::GetAtt": [
48+
"MyBucketF68F3FF0",
49+
"RegionalDomainName"
50+
]
4851
},
4952
"/myfolder/myfile.txt"
5053
]
@@ -58,7 +61,10 @@
5861
[
5962
"https://",
6063
{
61-
"Fn::GetAtt": ["MyBucketF68F3FF0", "DomainName"]
64+
"Fn::GetAtt": [
65+
"MyBucketF68F3FF0",
66+
"DomainName"
67+
]
6268
},
6369
"/myfolder/myfile.txt"
6470
]
@@ -80,4 +86,4 @@
8086
}
8187
}
8288
}
83-
}
89+
}

0 commit comments

Comments
 (0)