From 5a34a6599db7224e88da70ee8a9e8dff996e6480 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Wed, 25 May 2022 23:03:39 +0000 Subject: [PATCH] Events - send events to (subset of) targets (#5169) --- docs/docs/services/events.rst | 2 + moto/events/models.py | 11 + moto/events/notifications.py | 65 ++++ moto/s3/models.py | 18 + .../test_events_lambdatriggers_integration.py | 335 ++++++++++++++++++ 5 files changed, 431 insertions(+) create mode 100644 moto/events/notifications.py create mode 100644 tests/test_events/test_events_lambdatriggers_integration.py diff --git a/docs/docs/services/events.rst b/docs/docs/services/events.rst index b74823e12..84a723f14 100644 --- a/docs/docs/services/events.rst +++ b/docs/docs/services/events.rst @@ -12,6 +12,8 @@ events ====== +.. autoclass:: moto.events.models.EventsBackend + |start-h3| Example usage |end-h3| .. sourcecode:: python diff --git a/moto/events/models.py b/moto/events/models.py index 2a6e572cd..a6bb0f4c5 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -824,6 +824,9 @@ class EventPattern: self._raw_pattern = raw_pattern self._pattern = pattern + def get_pattern(self): + return self._pattern + def matches_event(self, event): if not self._pattern: return True @@ -921,6 +924,14 @@ class EventPatternParser: class EventsBackend(BaseBackend): + """ + When a event occurs, the appropriate targets are triggered for a subset of usecases. + + Supported events: S3:CreateBucket + + Supported targets: AWSLambda functions + """ + ACCOUNT_ID = re.compile(r"^(\d{1,12}|\*)$") STATEMENT_ID = re.compile(r"^[a-zA-Z0-9-_]{1,64}$") _CRON_REGEX = re.compile(r"^cron\(.*\)") diff --git a/moto/events/notifications.py b/moto/events/notifications.py new file mode 100644 index 000000000..d090f670a --- /dev/null +++ b/moto/events/notifications.py @@ -0,0 +1,65 @@ +import json + + +_EVENT_S3_OBJECT_CREATED = { + "version": "0", + "id": "17793124-05d4-b198-2fde-7ededc63b103", + "detail-type": "Object Created", + "source": "aws.s3", + "account": "123456789012", + "time": "2021-11-12T00:00:00Z", + "region": None, + "resources": [], + "detail": None, +} + + +def send_notification(source, event_name, region, resources, detail): + try: + _send_safe_notification(source, event_name, region, resources, detail) + except: # noqa + # If anything goes wrong, we should never fail + pass + + +def _send_safe_notification(source, event_name, region, resources, detail): + from .models import events_backends + + event = None + if source == "aws.s3" and event_name == "CreateBucket": + event = _EVENT_S3_OBJECT_CREATED.copy() + event["region"] = region + event["resources"] = resources + event["detail"] = detail + + if event is None: + return + + for backend in events_backends.values(): + applicable_targets = [] + for rule in backend.rules.values(): + if rule.state != "ENABLED": + continue + pattern = rule.event_pattern.get_pattern() + if source in pattern.get("source", []): + if event_name in pattern.get("detail", {}).get("eventName", []): + applicable_targets.extend(rule.targets) + + for target in applicable_targets: + if target.get("Arn", "").startswith("arn:aws:lambda"): + _invoke_lambda(target.get("Arn"), event=event) + + +def _invoke_lambda(fn_arn, event): + from moto.awslambda import lambda_backends + + lmbda_region = fn_arn.split(":")[3] + + body = json.dumps(event) + lambda_backends[lmbda_region].invoke( + function_name=fn_arn, + qualifier=None, + body=body, + headers=dict(), + response_headers=dict(), + ) diff --git a/moto/s3/models.py b/moto/s3/models.py index 346ac5f79..fdf78c273 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -58,6 +58,7 @@ from moto.s3.exceptions import ( from .cloud_formation import cfn_to_api_encryption, is_replacement_update from . import notifications from .utils import clean_key_name, _VersionedKeyStore, undo_clean_key_name +from ..events.notifications import send_notification as events_send_notification from ..settings import get_s3_default_key_buffer_size, S3_UPLOAD_PART_MIN_SIZE MAX_BUCKET_NAME_LENGTH = 63 @@ -1463,6 +1464,23 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider): new_bucket = FakeBucket(name=bucket_name, region_name=region_name) self.buckets[bucket_name] = new_bucket + + notification_detail = { + "version": "0", + "bucket": {"name": bucket_name}, + "request-id": "N4N7GDK58NMKJ12R", + "requester": get_account_id(), + "source-ip-address": "1.2.3.4", + "reason": "PutObject", + } + events_send_notification( + source="aws.s3", + event_name="CreateBucket", + region=region_name, + resources=[f"arn:aws:s3:::{bucket_name}"], + detail=notification_detail, + ) + return new_bucket def list_buckets(self): diff --git a/tests/test_events/test_events_lambdatriggers_integration.py b/tests/test_events/test_events_lambdatriggers_integration.py new file mode 100644 index 000000000..387b869d2 --- /dev/null +++ b/tests/test_events/test_events_lambdatriggers_integration.py @@ -0,0 +1,335 @@ +import boto3 +import json + +from moto import mock_events, mock_iam, mock_lambda, mock_logs, mock_s3 +from moto.core import ACCOUNT_ID +from ..test_awslambda.utilities import get_test_zip_file1, wait_for_log_msg + + +@mock_events +@mock_iam +@mock_lambda +@mock_logs +@mock_s3 +def test_creating_bucket__invokes_lambda(): + iam_client = boto3.client("iam", "us-east-1") + lambda_client = boto3.client("lambda", "us-east-1") + events_client = boto3.client("events", "us-east-1") + s3_client = boto3.client("s3", "us-east-1") + + role = iam_client.create_role( + RoleName="foobar", + AssumeRolePolicyDocument="{}", + )["Role"] + + func = lambda_client.create_function( + FunctionName="foobar", + Runtime="python3.8", + Role=role["Arn"], + Handler="lambda_function.lambda_handler", + Code={"ZipFile": get_test_zip_file1()}, + ) + + events_client.put_rule( + Name="foobarrule", + EventPattern="""{ + "source": [ + "aws.s3" + ], + "detail-type": [ + "AWS API Call via CloudTrail" + ], + "detail": { + "eventSource": [ + "s3.amazonaws.com" + ], + "eventName": [ + "CreateBucket" + ] + } + }""", + State="ENABLED", + RoleArn=role["Arn"], + ) + + events_client.put_targets( + Rule="foobarrule", + Targets=[ + { + "Id": "n/a", + "Arn": func["FunctionArn"], + "RoleArn": role["Arn"], + } + ], + ) + + bucket_name = "foobar" + s3_client.create_bucket( + ACL="public-read-write", + Bucket=bucket_name, + ) + + expected_msg = '"detail-type":"Object Created"' + log_group = f"/aws/lambda/{bucket_name}" + msg_showed_up, all_logs = wait_for_log_msg(expected_msg, log_group, wait_time=5) + + assert ( + msg_showed_up + ), "Lambda was not invoked after creating an S3 bucket. All logs: " + str(all_logs) + + event = json.loads(list([line for line in all_logs if expected_msg in line])[-1]) + + event.should.have.key("detail-type").equals("Object Created") + event.should.have.key("source").equals("aws.s3") + event.should.have.key("account").equals(ACCOUNT_ID) + event.should.have.key("time") + event.should.have.key("region").equals("us-east-1") + event.should.have.key("resources").equals([f"arn:aws:s3:::{bucket_name}"]) + + +@mock_events +@mock_iam +@mock_lambda +@mock_logs +@mock_s3 +def test_create_disabled_rule(): + iam_client = boto3.client("iam", "us-east-1") + lambda_client = boto3.client("lambda", "us-east-1") + events_client = boto3.client("events", "us-east-1") + s3_client = boto3.client("s3", "us-east-1") + + role = iam_client.create_role( + RoleName="foobar", + AssumeRolePolicyDocument="{}", + )["Role"] + + func = lambda_client.create_function( + FunctionName="foobar", + Runtime="python3.8", + Role=role["Arn"], + Handler="lambda_function.lambda_handler", + Code={"ZipFile": get_test_zip_file1()}, + ) + + events_client.put_rule( + Name="foobarrule", + EventPattern="""{ + "source": [ + "aws.s3" + ], + "detail-type": [ + "AWS API Call via CloudTrail" + ], + "detail": { + "eventSource": [ + "s3.amazonaws.com" + ], + "eventName": [ + "CreateBucket" + ] + } + }""", + State="DISABLED", + RoleArn=role["Arn"], + ) + + events_client.put_targets( + Rule="foobarrule", + Targets=[ + { + "Id": "n/a", + "Arn": func["FunctionArn"], + "RoleArn": role["Arn"], + } + ], + ) + + bucket_name = "foobar" + s3_client.create_bucket( + ACL="public-read-write", + Bucket=bucket_name, + ) + + expected_msg = '"detail-type":"Object Created"' + log_group = f"/aws/lambda/{bucket_name}" + msg_showed_up, _ = wait_for_log_msg(expected_msg, log_group, wait_time=5) + msg_showed_up.should.equal(False) + + +@mock_events +@mock_iam +@mock_logs +@mock_s3 +def test_create_rule_for_unsupported_target_arn(): + iam_client = boto3.client("iam", "us-east-1") + events_client = boto3.client("events", "us-east-1") + s3_client = boto3.client("s3", "us-east-1") + + role = iam_client.create_role( + RoleName="foobar", + AssumeRolePolicyDocument="{}", + )["Role"] + + events_client.put_rule( + Name="foobarrule", + EventPattern="""{ + "source": [ + "aws.s3" + ], + "detail-type": [ + "AWS API Call via CloudTrail" + ], + "detail": { + "eventSource": [ + "s3.amazonaws.com" + ], + "eventName": [ + "CreateBucket" + ] + } + }""", + State="ENABLED", + RoleArn=role["Arn"], + ) + + events_client.put_targets( + Rule="foobarrule", + Targets=[ + { + "Id": "n/a", + "Arn": "arn:aws:unknown", + "RoleArn": role["Arn"], + } + ], + ) + + bucket_name = "foobar" + s3_client.create_bucket( + ACL="public-read-write", + Bucket=bucket_name, + ) + + expected_msg = '"detail-type":"Object Created"' + log_group = f"/aws/lambda/{bucket_name}" + msg_showed_up, _ = wait_for_log_msg(expected_msg, log_group, wait_time=5) + msg_showed_up.should.equal(False) + + +@mock_events +@mock_iam +@mock_lambda +@mock_logs +@mock_s3 +def test_creating_bucket__but_invoke_lambda_on_create_object(): + iam_client = boto3.client("iam", "us-east-1") + lambda_client = boto3.client("lambda", "us-east-1") + events_client = boto3.client("events", "us-east-1") + s3_client = boto3.client("s3", "us-east-1") + + role = iam_client.create_role( + RoleName="foobar", + AssumeRolePolicyDocument="{}", + )["Role"] + + func = lambda_client.create_function( + FunctionName="foobar", + Runtime="python3.8", + Role=role["Arn"], + Handler="lambda_function.lambda_handler", + Code={"ZipFile": get_test_zip_file1()}, + ) + + events_client.put_rule( + Name="foobarrule", + EventPattern="""{ + "source": [ + "aws.s3" + ], + "detail": { + "eventSource": [ + "s3.amazonaws.com" + ], + "eventName": [ + "CreateObject" + ] + } + }""", + State="ENABLED", + RoleArn=role["Arn"], + ) + + events_client.put_targets( + Rule="foobarrule", + Targets=[ + { + "Id": "n/a", + "Arn": func["FunctionArn"], + "RoleArn": role["Arn"], + } + ], + ) + + bucket_name = "foobar" + s3_client.create_bucket( + ACL="public-read-write", + Bucket=bucket_name, + ) + + expected_msg = '"detail-type":"Object Created"' + log_group = f"/aws/lambda/{bucket_name}" + msg_showed_up, _ = wait_for_log_msg(expected_msg, log_group, wait_time=5) + msg_showed_up.should.equal(False) + + +@mock_events +@mock_iam +@mock_s3 +def test_creating_bucket__succeeds_despite_unknown_lambda(): + iam_client = boto3.client("iam", "us-east-1") + events_client = boto3.client("events", "us-east-1") + s3_client = boto3.client("s3", "us-east-1") + + role = iam_client.create_role( + RoleName="foobar", + AssumeRolePolicyDocument="{}", + )["Role"] + + events_client.put_rule( + Name="foobarrule", + EventPattern="""{ + "source": [ + "aws.s3" + ], + "detail-type": [ + "AWS API Call via CloudTrail" + ], + "detail": { + "eventSource": [ + "s3.amazonaws.com" + ], + "eventName": [ + "CreateBucket" + ] + } + }""", + State="ENABLED", + RoleArn=role["Arn"], + ) + + events_client.put_targets( + Rule="foobarrule", + Targets=[ + { + "Id": "n/a", + "Arn": "arn:aws:lambda:unknown", + "RoleArn": role["Arn"], + } + ], + ) + + bucket_name = "foobar" + bucket = s3_client.create_bucket( + ACL="public-read-write", + Bucket=bucket_name, + ) + bucket.shouldnt.equal(None)