Skip to content

Commit bc2f733

Browse files
authored
feat(aws-s3-stepfunctions): Changed escape hatch to eventBridgeEnabled prop (#666)
* changed escape hatch to eventBridgeEnabled prop * suppres cfn nag notification handler warnings * suppress notification handler cfn nag warnings * suppress notification handler cfn nag warnings * added comments of required eventBridgeEnabled property
1 parent bb21759 commit bc2f733

12 files changed

+741
-51
lines changed

source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.customLoggingBucket.expected.json

+120-5
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,6 @@
114114
"Ref": "tests3stepfunctiontests3stepfunctionWS3LoggingBucketB716417C"
115115
}
116116
},
117-
"NotificationConfiguration": {
118-
"EventBridgeConfiguration": {
119-
"EventBridgeEnabled": true
120-
}
121-
},
122117
"PublicAccessBlockConfiguration": {
123118
"BlockPublicAcls": true,
124119
"BlockPublicPolicy": true,
@@ -179,6 +174,24 @@
179174
}
180175
}
181176
},
177+
"tests3stepfunctiontests3stepfunctionWS3BucketNotificationsC4380C1D": {
178+
"Type": "Custom::S3BucketNotifications",
179+
"Properties": {
180+
"ServiceToken": {
181+
"Fn::GetAtt": [
182+
"BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691",
183+
"Arn"
184+
]
185+
},
186+
"BucketName": {
187+
"Ref": "tests3stepfunctiontests3stepfunctionWS3Bucket9BE64924"
188+
},
189+
"NotificationConfiguration": {
190+
"EventBridgeConfiguration": {}
191+
},
192+
"Managed": true
193+
}
194+
},
182195
"tests3stepfunctiontests3stepfunctionWtests3stepfunctionWeventrulestepfunctionconstructStateMachineLogGroupE83EECDD": {
183196
"Type": "AWS::Logs::LogGroup",
184197
"Properties": {
@@ -461,6 +474,108 @@
461474
"Statistic": "Maximum",
462475
"Threshold": 1
463476
}
477+
},
478+
"BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC": {
479+
"Type": "AWS::IAM::Role",
480+
"Properties": {
481+
"AssumeRolePolicyDocument": {
482+
"Statement": [
483+
{
484+
"Action": "sts:AssumeRole",
485+
"Effect": "Allow",
486+
"Principal": {
487+
"Service": "lambda.amazonaws.com"
488+
}
489+
}
490+
],
491+
"Version": "2012-10-17"
492+
},
493+
"ManagedPolicyArns": [
494+
{
495+
"Fn::Join": [
496+
"",
497+
[
498+
"arn:",
499+
{
500+
"Ref": "AWS::Partition"
501+
},
502+
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
503+
]
504+
]
505+
}
506+
]
507+
}
508+
},
509+
"BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36": {
510+
"Type": "AWS::IAM::Policy",
511+
"Properties": {
512+
"PolicyDocument": {
513+
"Statement": [
514+
{
515+
"Action": "s3:PutBucketNotification",
516+
"Effect": "Allow",
517+
"Resource": "*"
518+
}
519+
],
520+
"Version": "2012-10-17"
521+
},
522+
"PolicyName": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36",
523+
"Roles": [
524+
{
525+
"Ref": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC"
526+
}
527+
]
528+
},
529+
"Metadata": {
530+
"cfn_nag": {
531+
"rules_to_suppress": [
532+
{
533+
"id": "W12",
534+
"reason": "Bucket resource is '*' due to circular dependency with bucket and role creation at the same time"
535+
}
536+
]
537+
}
538+
}
539+
},
540+
"BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691": {
541+
"Type": "AWS::Lambda::Function",
542+
"Properties": {
543+
"Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)",
544+
"Code": {
545+
"ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nEVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration'\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n external_notifications = find_external_notifications(bucket, stack_id)\n\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n\n if EVENTBRIDGE_CONFIGURATION in notification_configuration:\n notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION]\n elif EVENTBRIDGE_CONFIGURATION in external_notifications:\n notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return notifications\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n if EVENTBRIDGE_CONFIGURATION in existing_notifications:\n external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return external_notifications\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n"
546+
},
547+
"Handler": "index.handler",
548+
"Role": {
549+
"Fn::GetAtt": [
550+
"BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC",
551+
"Arn"
552+
]
553+
},
554+
"Runtime": "python3.7",
555+
"Timeout": 300
556+
},
557+
"DependsOn": [
558+
"BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36",
559+
"BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC"
560+
],
561+
"Metadata": {
562+
"cfn_nag": {
563+
"rules_to_suppress": [
564+
{
565+
"id": "W58",
566+
"reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with tighter permissions."
567+
},
568+
{
569+
"id": "W89",
570+
"reason": "This is not a rule for the general case, just for specific use cases/industries"
571+
},
572+
{
573+
"id": "W92",
574+
"reason": "Impossible for us to define the correct concurrency for clients"
575+
}
576+
]
577+
}
578+
}
464579
}
465580
},
466581
"Mappings": {

source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.pre-existing-bucket.expected.json

+120-5
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@
1212
}
1313
]
1414
},
15-
"NotificationConfiguration": {
16-
"EventBridgeConfiguration": {
17-
"EventBridgeEnabled": true
18-
}
19-
},
2015
"VersioningConfiguration": {
2116
"Status": "Enabled"
2217
}
@@ -42,6 +37,126 @@
4237
}
4338
}
4439
},
40+
"existingScriptLocationNotificationsC550EA17": {
41+
"Type": "Custom::S3BucketNotifications",
42+
"Properties": {
43+
"ServiceToken": {
44+
"Fn::GetAtt": [
45+
"BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691",
46+
"Arn"
47+
]
48+
},
49+
"BucketName": {
50+
"Ref": "existingScriptLocation845F3C51"
51+
},
52+
"NotificationConfiguration": {
53+
"EventBridgeConfiguration": {}
54+
},
55+
"Managed": true
56+
}
57+
},
58+
"BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC": {
59+
"Type": "AWS::IAM::Role",
60+
"Properties": {
61+
"AssumeRolePolicyDocument": {
62+
"Statement": [
63+
{
64+
"Action": "sts:AssumeRole",
65+
"Effect": "Allow",
66+
"Principal": {
67+
"Service": "lambda.amazonaws.com"
68+
}
69+
}
70+
],
71+
"Version": "2012-10-17"
72+
},
73+
"ManagedPolicyArns": [
74+
{
75+
"Fn::Join": [
76+
"",
77+
[
78+
"arn:",
79+
{
80+
"Ref": "AWS::Partition"
81+
},
82+
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
83+
]
84+
]
85+
}
86+
]
87+
}
88+
},
89+
"BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36": {
90+
"Type": "AWS::IAM::Policy",
91+
"Properties": {
92+
"PolicyDocument": {
93+
"Statement": [
94+
{
95+
"Action": "s3:PutBucketNotification",
96+
"Effect": "Allow",
97+
"Resource": "*"
98+
}
99+
],
100+
"Version": "2012-10-17"
101+
},
102+
"PolicyName": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36",
103+
"Roles": [
104+
{
105+
"Ref": "BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC"
106+
}
107+
]
108+
},
109+
"Metadata": {
110+
"cfn_nag": {
111+
"rules_to_suppress": [
112+
{
113+
"id": "W12",
114+
"reason": "Bucket resource is '*' due to circular dependency with bucket and role creation at the same time"
115+
}
116+
]
117+
}
118+
}
119+
},
120+
"BucketNotificationsHandler050a0587b7544547bf325f094a3db8347ECC3691": {
121+
"Type": "AWS::Lambda::Function",
122+
"Properties": {
123+
"Description": "AWS CloudFormation handler for \"Custom::S3BucketNotifications\" resources (@aws-cdk/aws-s3)",
124+
"Code": {
125+
"ZipFile": "import boto3 # type: ignore\nimport json\nimport logging\nimport urllib.request\n\ns3 = boto3.client(\"s3\")\n\nEVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration'\n\nCONFIGURATION_TYPES = [\"TopicConfigurations\", \"QueueConfigurations\", \"LambdaFunctionConfigurations\"]\n\ndef handler(event: dict, context):\n response_status = \"SUCCESS\"\n error_message = \"\"\n try:\n props = event[\"ResourceProperties\"]\n bucket = props[\"BucketName\"]\n notification_configuration = props[\"NotificationConfiguration\"]\n request_type = event[\"RequestType\"]\n managed = props.get('Managed', 'true').lower() == 'true'\n stack_id = event['StackId']\n\n if managed:\n config = handle_managed(request_type, notification_configuration)\n else:\n config = handle_unmanaged(bucket, stack_id, request_type, notification_configuration)\n\n put_bucket_notification_configuration(bucket, config)\n except Exception as e:\n logging.exception(\"Failed to put bucket notification configuration\")\n response_status = \"FAILED\"\n error_message = f\"Error: {str(e)}. \"\n finally:\n submit_response(event, context, response_status, error_message)\n\ndef handle_managed(request_type, notification_configuration):\n if request_type == 'Delete':\n return {}\n return notification_configuration\n\ndef handle_unmanaged(bucket, stack_id, request_type, notification_configuration):\n external_notifications = find_external_notifications(bucket, stack_id)\n\n if request_type == 'Delete':\n return external_notifications\n\n def with_id(notification):\n notification['Id'] = f\"{stack_id}-{hash(json.dumps(notification, sort_keys=True))}\"\n return notification\n\n notifications = {}\n for t in CONFIGURATION_TYPES:\n external = external_notifications.get(t, [])\n incoming = [with_id(n) for n in notification_configuration.get(t, [])]\n notifications[t] = external + incoming\n\n if EVENTBRIDGE_CONFIGURATION in notification_configuration:\n notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION]\n elif EVENTBRIDGE_CONFIGURATION in external_notifications:\n notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return notifications\n\ndef find_external_notifications(bucket, stack_id):\n existing_notifications = get_bucket_notification_configuration(bucket)\n external_notifications = {}\n for t in CONFIGURATION_TYPES:\n external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f\"{stack_id}-\")]\n\n if EVENTBRIDGE_CONFIGURATION in existing_notifications:\n external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION]\n\n return external_notifications\n\ndef get_bucket_notification_configuration(bucket):\n return s3.get_bucket_notification_configuration(Bucket=bucket)\n\ndef put_bucket_notification_configuration(bucket, notification_configuration):\n s3.put_bucket_notification_configuration(Bucket=bucket, NotificationConfiguration=notification_configuration)\n\ndef submit_response(event: dict, context, response_status: str, error_message: str):\n response_body = json.dumps(\n {\n \"Status\": response_status,\n \"Reason\": f\"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}\",\n \"PhysicalResourceId\": event.get(\"PhysicalResourceId\") or event[\"LogicalResourceId\"],\n \"StackId\": event[\"StackId\"],\n \"RequestId\": event[\"RequestId\"],\n \"LogicalResourceId\": event[\"LogicalResourceId\"],\n \"NoEcho\": False,\n }\n ).encode(\"utf-8\")\n headers = {\"content-type\": \"\", \"content-length\": str(len(response_body))}\n try:\n req = urllib.request.Request(url=event[\"ResponseURL\"], headers=headers, data=response_body, method=\"PUT\")\n with urllib.request.urlopen(req) as response:\n print(response.read().decode(\"utf-8\"))\n print(\"Status code: \" + response.reason)\n except Exception as e:\n print(\"send(..) failed executing request.urlopen(..): \" + str(e))\n"
126+
},
127+
"Handler": "index.handler",
128+
"Role": {
129+
"Fn::GetAtt": [
130+
"BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC",
131+
"Arn"
132+
]
133+
},
134+
"Runtime": "python3.7",
135+
"Timeout": 300
136+
},
137+
"DependsOn": [
138+
"BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36",
139+
"BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC"
140+
],
141+
"Metadata": {
142+
"cfn_nag": {
143+
"rules_to_suppress": [
144+
{
145+
"id": "W58",
146+
"reason": "Lambda functions has the required permission to write CloudWatch Logs. It uses custom policy instead of arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole with tighter permissions."
147+
},
148+
{
149+
"id": "W89",
150+
"reason": "This is not a rule for the general case, just for specific use cases/industries"
151+
},
152+
{
153+
"id": "W92",
154+
"reason": "Impossible for us to define the correct concurrency for clients"
155+
}
156+
]
157+
}
158+
}
159+
},
45160
"tests3stepfunctionpreexistingbucketconstructtests3stepfunctionpreexistingbucketconstructWtests3stepfunctionpreexistingbucketconstructWeventrulestepfunctionconstructStateMachineLogGroupF215071C": {
46161
"Type": "AWS::Logs::LogGroup",
47162
"Properties": {

source/patterns/@aws-solutions-constructs/aws-s3-step-function/test/integ.pre-existing-bucket.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,14 @@
1515
import { App, Stack, RemovalPolicy } from "@aws-cdk/core";
1616
import { S3ToStepFunction, S3ToStepFunctionProps } from "../lib";
1717
import * as stepfunctions from '@aws-cdk/aws-stepfunctions';
18-
import * as s3 from '@aws-cdk/aws-s3';
19-
import { CreateScrapBucket } from '@aws-solutions-constructs/core';
20-
import { generateIntegStackName } from '@aws-solutions-constructs/core';
18+
import { CreateScrapBucket, generateIntegStackName, addCfnNagS3BucketNotificationRulesToSuppress } from '@aws-solutions-constructs/core';
2119

2220
const app = new App();
2321
const stack = new Stack(app, generateIntegStackName(__filename));
2422

25-
const existingBucket = CreateScrapBucket(stack, {});
26-
const cfnBucket = existingBucket.node.defaultChild as s3.CfnBucket;
27-
cfnBucket.addPropertyOverride('NotificationConfiguration.EventBridgeConfiguration.EventBridgeEnabled', true);
23+
const existingBucket = CreateScrapBucket(stack, {
24+
eventBridgeEnabled: true
25+
});
2826

2927
const startState = new stepfunctions.Pass(stack, 'StartState');
3028

@@ -40,4 +38,6 @@ const props: S3ToStepFunctionProps = {
4038

4139
new S3ToStepFunction(stack, 'test-s3-step-function-pre-existing-bucket-construct', props);
4240

41+
addCfnNagS3BucketNotificationRulesToSuppress(stack, 'BucketNotificationsHandler050a0587b7544547bf325f094a3db834');
42+
4343
app.synth();

0 commit comments

Comments
 (0)