Skip to content

Commit

Permalink
feat(s3): custom role for the bucket notifications handler (aws#17794)
Browse files Browse the repository at this point in the history
Allow users to pass a custom role to `Bucket`, which will be used by the notifications handler.

Fixes aws#9918, aws#13241.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
otaviomacedo authored and TikiTDO committed Feb 21, 2022
1 parent ad86acd commit ba6e2a5
Show file tree
Hide file tree
Showing 10 changed files with 140 additions and 41 deletions.
27 changes: 27 additions & 0 deletions packages/@aws-cdk/aws-s3/README.md
Expand Up @@ -249,6 +249,33 @@ const bucket = s3.Bucket.fromBucketAttributes(this, 'ImportedBucket', {
bucket.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.SnsDestination(topic));
```

When you add an event notification to a bucket, a custom resource is created to
manage the notifications. By default, a new role is created for the Lambda
function that implements this feature. If you want to use your own role instead,
you should provide it in the `Bucket` constructor:

```ts
declare const myRole: iam.IRole;
const bucket = new s3.Bucket(this, 'MyBucket', {
notificationsHandlerRole: myRole,
});
```

Whatever role you provide, the CDK will try to modify it by adding the
permissions from `AWSLambdaBasicExecutionRole` (an AWS managed policy) as well
as the permissions `s3:PutBucketNotification` and `s3:GetBucketNotification`.
If you’re passing an imported role, and you don’t want this to happen, configure
it to be immutable:

```ts
const importedRole = iam.Role.fromRoleArn(this, 'role', 'arn:aws:iam::123456789012:role/RoleName', {
mutable: false,
});
```

> If you provide an imported immutable role, make sure that it has at least all
> the permissions mentioned above. Otherwise, the deployment will fail!
[S3 Bucket Notifications]: https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html


Expand Down
37 changes: 31 additions & 6 deletions packages/@aws-cdk/aws-s3/lib/bucket.ts
Expand Up @@ -427,6 +427,13 @@ export interface BucketAttributes {
* @default - it's assumed the bucket is in the same region as the scope it's being imported into
*/
readonly region?: string;

/**
* The role to be used by the notifications handler
*
* @default - a new role will be created.
*/
readonly notificationsHandlerRole?: iam.IRole;
}

/**
Expand Down Expand Up @@ -484,14 +491,12 @@ export abstract class BucketBase extends Resource implements IBucket {
*/
protected abstract disallowPublicAccess?: boolean;

private readonly notifications: BucketNotifications;
private notifications?: BucketNotifications;

protected notificationsHandlerRole?: iam.IRole;

constructor(scope: Construct, id: string, props: ResourceProps = {}) {
super(scope, id, props);

// defines a BucketNotifications construct. Notice that an actual resource will only
// be added if there are notifications added, so we don't need to condition this.
this.notifications = new BucketNotifications(this, 'Notifications', { bucket: this });
}

/**
Expand Down Expand Up @@ -836,7 +841,17 @@ export abstract class BucketBase extends Resource implements IBucket {
* https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html
*/
public addEventNotification(event: EventType, dest: IBucketNotificationDestination, ...filters: NotificationKeyFilter[]) {
this.notifications.addNotification(event, dest, ...filters);
this.withNotifications(notifications => notifications.addNotification(event, dest, ...filters));
}

private withNotifications(cb: (notifications: BucketNotifications) => void) {
if (!this.notifications) {
this.notifications = new BucketNotifications(this, 'Notifications', {
bucket: this,
handlerRole: this.notificationsHandlerRole,
});
}
cb(this.notifications);
}

/**
Expand Down Expand Up @@ -1459,6 +1474,13 @@ export interface BucketProps {
*/
readonly transferAcceleration?: boolean;

/**
* The role to be used by the notifications handler
*
* @default - a new role will be created.
*/
readonly notificationsHandlerRole?: iam.IRole;

/**
* Inteligent Tiering Configurations
*
Expand Down Expand Up @@ -1542,6 +1564,7 @@ export class Bucket extends BucketBase {
public policy?: BucketPolicy = undefined;
protected autoCreatePolicy = false;
protected disallowPublicAccess = false;
protected notificationsHandlerRole = attrs.notificationsHandlerRole;

/**
* Exports this bucket from the stack.
Expand Down Expand Up @@ -1629,6 +1652,8 @@ export class Bucket extends BucketBase {
physicalName: props.bucketName,
});

this.notificationsHandlerRole = props.notificationsHandlerRole;

const { bucketEncryption, encryptionKey } = this.parseEncryption(props);

Bucket.validateBucketName(this.physicalName);
Expand Down
Expand Up @@ -7,6 +7,10 @@ import * as cdk from '@aws-cdk/core';
// eslint-disable-next-line no-duplicate-imports, import/order
import { Construct } from '@aws-cdk/core';

export class NotificationsResourceHandlerProps {
role?: iam.IRole;
}

/**
* A Lambda-based custom resource handler that provisions S3 bucket
* notifications for a bucket.
Expand All @@ -31,14 +35,14 @@ export class NotificationsResourceHandler extends Construct {
*
* @returns The ARN of the custom resource lambda function.
*/
public static singleton(context: Construct) {
public static singleton(context: Construct, props: NotificationsResourceHandlerProps = {}) {
const root = cdk.Stack.of(context);

// well-known logical id to ensure stack singletonity
const logicalId = 'BucketNotificationsHandler050a0587b7544547bf325f094a3db834';
let lambda = root.node.tryFindChild(logicalId) as NotificationsResourceHandler;
if (!lambda) {
lambda = new NotificationsResourceHandler(root, logicalId);
lambda = new NotificationsResourceHandler(root, logicalId, props);
}

return lambda;
Expand All @@ -53,19 +57,19 @@ export class NotificationsResourceHandler extends Construct {
/**
* The role of the handler's lambda function.
*/
public readonly role: iam.Role;
public readonly role: iam.IRole;

constructor(scope: Construct, id: string) {
constructor(scope: Construct, id: string, props: NotificationsResourceHandlerProps = {}) {
super(scope, id);

this.role = new iam.Role(this, 'Role', {
this.role = props.role ?? new iam.Role(this, 'Role', {
assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
],
});

this.role.addToPolicy(new iam.PolicyStatement({
this.role.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
);
this.role.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['s3:PutBucketNotification'],
resources: ['*'],
}));
Expand Down Expand Up @@ -95,4 +99,8 @@ export class NotificationsResourceHandler extends Construct {

this.functionArn = resource.getAtt('Arn').toString();
}

public addToRolePolicy(statement: iam.PolicyStatement) {
this.role.addToPrincipalPolicy(statement);
}
}
Expand Up @@ -13,6 +13,11 @@ interface NotificationsProps {
* The bucket to manage notifications for.
*/
bucket: IBucket;

/**
* The role to be used by the lambda handler
*/
handlerRole?: iam.IRole;
}

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

constructor(scope: Construct, id: string, props: NotificationsProps) {
super(scope, id);
this.bucket = props.bucket;
this.handlerRole = props.handlerRole;
}

/**
Expand Down Expand Up @@ -102,12 +109,14 @@ export class BucketNotifications extends Construct {
*/
private createResourceOnce() {
if (!this.resource) {
const handler = NotificationsResourceHandler.singleton(this);
const handler = NotificationsResourceHandler.singleton(this, {
role: this.handlerRole,
});

const managed = this.bucket instanceof Bucket;

if (!managed) {
handler.role.addToPolicy(new iam.PolicyStatement({
handler.addToRolePolicy(new iam.PolicyStatement({
actions: ['s3:GetBucketNotification'],
resources: ['*'],
}));
Expand Down
Expand Up @@ -110,7 +110,7 @@
"Properties": {
"Code": {
"S3Bucket": {
"Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3Bucket2C6C817C"
"Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3Bucket09A62232"
},
"S3Key": {
"Fn::Join": [
Expand All @@ -123,7 +123,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6"
"Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE"
}
]
}
Expand All @@ -136,7 +136,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6"
"Ref": "AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE"
}
]
}
Expand Down Expand Up @@ -228,7 +228,7 @@
"Properties": {
"Code": {
"S3Bucket": {
"Ref": "AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3BucketE1985B35"
"Ref": "AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3BucketB51EC107"
},
"S3Key": {
"Fn::Join": [
Expand All @@ -241,7 +241,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3VersionKey610C6DE2"
"Ref": "AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3VersionKey2B267DB5"
}
]
}
Expand All @@ -254,7 +254,7 @@
"Fn::Split": [
"||",
{
"Ref": "AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3VersionKey610C6DE2"
"Ref": "AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3VersionKey2B267DB5"
}
]
}
Expand Down Expand Up @@ -297,29 +297,29 @@
}
},
"Parameters": {
"AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3Bucket2C6C817C": {
"AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3Bucket09A62232": {
"Type": "String",
"Description": "S3 bucket for asset \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\""
"Description": "S3 bucket for asset \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\""
},
"AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709S3VersionKeyFA215BD6": {
"AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824S3VersionKeyA28118BE": {
"Type": "String",
"Description": "S3 key for asset version \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\""
"Description": "S3 key for asset version \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\""
},
"AssetParameters84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709ArtifactHash17D48178": {
"AssetParametersbe270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824ArtifactHash76F8FCF2": {
"Type": "String",
"Description": "Artifact hash for asset \"84e9b89449fe2573e51d08cc143e21116ed4608c6db56afffcb4ad85c8130709\""
"Description": "Artifact hash for asset \"be270bbdebe0851c887569796e3997437cca54ce86893ed94788500448e92824\""
},
"AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3BucketE1985B35": {
"AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3BucketB51EC107": {
"Type": "String",
"Description": "S3 bucket for asset \"618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abf\""
"Description": "S3 bucket for asset \"31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6\""
},
"AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfS3VersionKey610C6DE2": {
"AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6S3VersionKey2B267DB5": {
"Type": "String",
"Description": "S3 key for asset version \"618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abf\""
"Description": "S3 key for asset version \"31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6\""
},
"AssetParameters618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abfArtifactHash467DFC33": {
"AssetParameters31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6ArtifactHashEE982197": {
"Type": "String",
"Description": "Artifact hash for asset \"618bbe9863c0edd5c4ca2e24b5063762f020fafec018cd06f57e2bd9f2f48abf\""
"Description": "Artifact hash for asset \"31552cb1c5c4cdb0d9502dc59c3cd63cb519dcb7e320e60965c75940297ae3b6\""
}
}
}
Expand Up @@ -155,4 +155,4 @@
}
}
}
}
}
Expand Up @@ -71,4 +71,4 @@
}
}
}
]
]
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-s3/test/integ.bucket.expected.json
Expand Up @@ -173,4 +173,4 @@
}
}
}
}
}
12 changes: 9 additions & 3 deletions packages/@aws-cdk/aws-s3/test/integ.bucket.url.lit.expected.json
Expand Up @@ -44,7 +44,10 @@
[
"https://",
{
"Fn::GetAtt": ["MyBucketF68F3FF0", "RegionalDomainName"]
"Fn::GetAtt": [
"MyBucketF68F3FF0",
"RegionalDomainName"
]
},
"/myfolder/myfile.txt"
]
Expand All @@ -58,7 +61,10 @@
[
"https://",
{
"Fn::GetAtt": ["MyBucketF68F3FF0", "DomainName"]
"Fn::GetAtt": [
"MyBucketF68F3FF0",
"DomainName"
]
},
"/myfolder/myfile.txt"
]
Expand All @@ -80,4 +86,4 @@
}
}
}
}
}

0 comments on commit ba6e2a5

Please sign in to comment.