diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index eccd765dd..c026ef601 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -2718,12 +2718,12 @@ - [ ] upgrade_elasticsearch_domain ## events -48% implemented +58% implemented - [ ] activate_event_source -- [ ] create_event_bus +- [X] create_event_bus - [ ] create_partner_event_source - [ ] deactivate_event_source -- [ ] delete_event_bus +- [X] delete_event_bus - [ ] delete_partner_event_source - [X] delete_rule - [X] describe_event_bus @@ -2732,7 +2732,7 @@ - [X] describe_rule - [X] disable_rule - [X] enable_rule -- [ ] list_event_buses +- [X] list_event_buses - [ ] list_event_sources - [ ] list_partner_event_source_accounts - [ ] list_partner_event_sources diff --git a/moto/events/models.py b/moto/events/models.py index e69062b2c..be4153b9f 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -5,6 +5,7 @@ import boto3 from moto.core.exceptions import JsonRESTError from moto.core import BaseBackend, BaseModel +from moto.sts.models import ACCOUNT_ID class Rule(BaseModel): @@ -54,6 +55,42 @@ class Rule(BaseModel): self.targets.pop(index) +class EventBus(BaseModel): + def __init__(self, region_name, name): + self.region = region_name + self.name = name + + self._permissions = {} + + @property + def arn(self): + return "arn:aws:events:{region}:{account_id}:event-bus/{name}".format( + region=self.region, account_id=ACCOUNT_ID, name=self.name + ) + + @property + def policy(self): + if not len(self._permissions): + return None + + policy = {"Version": "2012-10-17", "Statement": []} + + for sid, permission in self._permissions.items(): + policy["Statement"].append( + { + "Sid": sid, + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::{}:root".format(permission["Principal"]) + }, + "Action": permission["Action"], + "Resource": self.arn, + } + ) + + return json.dumps(policy) + + class EventsBackend(BaseBackend): ACCOUNT_ID = re.compile(r"^(\d{1,12}|\*)$") STATEMENT_ID = re.compile(r"^[a-zA-Z0-9-_]{1,64}$") @@ -65,13 +102,19 @@ class EventsBackend(BaseBackend): self.rules_order = [] self.next_tokens = {} self.region_name = region_name - self.permissions = {} + self.event_buses = {} + self.event_sources = {} + + self._add_default_event_bus() def reset(self): region_name = self.region_name self.__dict__ = {} self.__init__(region_name) + def _add_default_event_bus(self): + self.event_buses["default"] = EventBus(self.region_name, "default") + def _get_rule_by_index(self, i): return self.rules.get(self.rules_order[i]) @@ -221,9 +264,17 @@ class EventsBackend(BaseBackend): def test_event_pattern(self): raise NotImplementedError() - def put_permission(self, action, principal, statement_id): + def put_permission(self, event_bus_name, action, principal, statement_id): + if not event_bus_name: + event_bus_name = "default" + + event_bus = self.describe_event_bus(event_bus_name) + if action is None or action != "events:PutEvents": - raise JsonRESTError("InvalidParameterValue", "Action must be PutEvents") + raise JsonRESTError( + "ValidationException", + "Provided value in parameter 'action' is not supported.", + ) if principal is None or self.ACCOUNT_ID.match(principal) is None: raise JsonRESTError( @@ -235,34 +286,81 @@ class EventsBackend(BaseBackend): "InvalidParameterValue", "StatementId must match ^[a-zA-Z0-9-_]{1,64}$" ) - self.permissions[statement_id] = {"action": action, "principal": principal} + event_bus._permissions[statement_id] = { + "Action": action, + "Principal": principal, + } - def remove_permission(self, statement_id): - try: - del self.permissions[statement_id] - except KeyError: - raise JsonRESTError("ResourceNotFoundException", "StatementId not found") + def remove_permission(self, event_bus_name, statement_id): + if not event_bus_name: + event_bus_name = "default" - def describe_event_bus(self): - arn = "arn:aws:events:{0}:000000000000:event-bus/default".format( - self.region_name - ) - statements = [] - for statement_id, data in self.permissions.items(): - statements.append( - { - "Sid": statement_id, - "Effect": "Allow", - "Principal": { - "AWS": "arn:aws:iam::{0}:root".format(data["principal"]) - }, - "Action": data["action"], - "Resource": arn, - } + event_bus = self.describe_event_bus(event_bus_name) + + if not len(event_bus._permissions): + raise JsonRESTError( + "ResourceNotFoundException", "EventBus does not have a policy." ) - policy = {"Version": "2012-10-17", "Statement": statements} - policy_json = json.dumps(policy) - return {"Policy": policy_json, "Name": "default", "Arn": arn} + + if not event_bus._permissions.pop(statement_id, None): + raise JsonRESTError( + "ResourceNotFoundException", + "Statement with the provided id does not exist.", + ) + + def describe_event_bus(self, name): + if not name: + name = "default" + + event_bus = self.event_buses.get(name) + + if not event_bus: + raise JsonRESTError( + "ResourceNotFoundException", + "Event bus {} does not exist.".format(name), + ) + + return event_bus + + def create_event_bus(self, name, event_source_name): + if name in self.event_buses: + raise JsonRESTError( + "ResourceAlreadyExistsException", + "Event bus {} already exists.".format(name), + ) + + if not event_source_name and "/" in name: + raise JsonRESTError( + "ValidationException", "Event bus name must not contain '/'." + ) + + if event_source_name and event_source_name not in self.event_sources: + raise JsonRESTError( + "ResourceNotFoundException", + "Event source {} does not exist.".format(event_source_name), + ) + + self.event_buses[name] = EventBus(self.region_name, name) + + return self.event_buses[name] + + def list_event_buses(self, name_prefix): + if name_prefix: + return [ + event_bus + for event_bus in self.event_buses.values() + if event_bus.name.startswith(name_prefix) + ] + + return list(self.event_buses.values()) + + def delete_event_bus(self, name): + if name == "default": + raise JsonRESTError( + "ValidationException", "Cannot delete event bus default." + ) + + self.event_buses.pop(name, None) available_regions = boto3.session.Session().get_available_regions("events") diff --git a/moto/events/responses.py b/moto/events/responses.py index 39c5c75dc..98a33218a 100644 --- a/moto/events/responses.py +++ b/moto/events/responses.py @@ -238,20 +238,68 @@ class EventsHandler(BaseResponse): pass def put_permission(self): + event_bus_name = self._get_param("EventBusName") action = self._get_param("Action") principal = self._get_param("Principal") statement_id = self._get_param("StatementId") - self.events_backend.put_permission(action, principal, statement_id) + self.events_backend.put_permission( + event_bus_name, action, principal, statement_id + ) return "" def remove_permission(self): + event_bus_name = self._get_param("EventBusName") statement_id = self._get_param("StatementId") - self.events_backend.remove_permission(statement_id) + self.events_backend.remove_permission(event_bus_name, statement_id) return "" def describe_event_bus(self): - return json.dumps(self.events_backend.describe_event_bus()) + name = self._get_param("Name") + + event_bus = self.events_backend.describe_event_bus(name) + response = { + "Name": event_bus.name, + "Arn": event_bus.arn, + } + + if event_bus.policy: + response["Policy"] = event_bus.policy + + return json.dumps(response), self.response_headers + + def create_event_bus(self): + name = self._get_param("Name") + event_source_name = self._get_param("EventSourceName") + + event_bus = self.events_backend.create_event_bus(name, event_source_name) + + return json.dumps({"EventBusArn": event_bus.arn}), self.response_headers + + def list_event_buses(self): + name_prefix = self._get_param("NamePrefix") + # ToDo: add 'NextToken' & 'Limit' parameters + + response = [] + for event_bus in self.events_backend.list_event_buses(name_prefix): + event_bus_response = { + "Name": event_bus.name, + "Arn": event_bus.arn, + } + + if event_bus.policy: + event_bus_response["Policy"] = event_bus.policy + + response.append(event_bus_response) + + return json.dumps({"EventBuses": response}), self.response_headers + + def delete_event_bus(self): + name = self._get_param("Name") + + self.events_backend.delete_event_bus(name) + + return "", self.response_headers diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py index d5bfdf782..5f81e2cf6 100644 --- a/tests/test_events/test_events.py +++ b/tests/test_events/test_events.py @@ -1,6 +1,7 @@ import random import boto3 import json +import sure # noqa from moto.events import mock_events from botocore.exceptions import ClientError @@ -204,6 +205,53 @@ def test_permissions(): assert resp_policy["Statement"][0]["Sid"] == "Account1" +@mock_events +def test_put_permission_errors(): + client = boto3.client("events", "us-east-1") + client.create_event_bus(Name="test-bus") + + client.put_permission.when.called_with( + EventBusName="non-existing", + Action="events:PutEvents", + Principal="111111111111", + StatementId="test", + ).should.throw(ClientError, "Event bus non-existing does not exist.") + + client.put_permission.when.called_with( + EventBusName="test-bus", + Action="events:PutPermission", + Principal="111111111111", + StatementId="test", + ).should.throw( + ClientError, "Provided value in parameter 'action' is not supported." + ) + + +@mock_events +def test_remove_permission_errors(): + client = boto3.client("events", "us-east-1") + client.create_event_bus(Name="test-bus") + + client.remove_permission.when.called_with( + EventBusName="non-existing", StatementId="test" + ).should.throw(ClientError, "Event bus non-existing does not exist.") + + client.remove_permission.when.called_with( + EventBusName="test-bus", StatementId="test" + ).should.throw(ClientError, "EventBus does not have a policy.") + + client.put_permission( + EventBusName="test-bus", + Action="events:PutEvents", + Principal="111111111111", + StatementId="test", + ) + + client.remove_permission.when.called_with( + EventBusName="test-bus", StatementId="non-existing" + ).should.throw(ClientError, "Statement with the provided id does not exist.") + + @mock_events def test_put_events(): client = boto3.client("events", "eu-central-1") @@ -220,3 +268,177 @@ def test_put_events(): with assert_raises(ClientError): client.put_events(Entries=[event] * 20) + + +@mock_events +def test_create_event_bus(): + client = boto3.client("events", "us-east-1") + response = client.create_event_bus(Name="test-bus") + + response["EventBusArn"].should.equal( + "arn:aws:events:us-east-1:123456789012:event-bus/test-bus" + ) + + +@mock_events +def test_create_event_bus_errors(): + client = boto3.client("events", "us-east-1") + client.create_event_bus(Name="test-bus") + + client.create_event_bus.when.called_with(Name="test-bus").should.throw( + ClientError, "Event bus test-bus already exists." + ) + + # the 'default' name is already used for the account's default event bus. + client.create_event_bus.when.called_with(Name="default").should.throw( + ClientError, "Event bus default already exists." + ) + + # non partner event buses can't contain the '/' character + client.create_event_bus.when.called_with(Name="test/test-bus").should.throw( + ClientError, "Event bus name must not contain '/'." + ) + + client.create_event_bus.when.called_with( + Name="aws.partner/test/test-bus", EventSourceName="aws.partner/test/test-bus" + ).should.throw( + ClientError, "Event source aws.partner/test/test-bus does not exist." + ) + + +@mock_events +def test_describe_event_bus(): + client = boto3.client("events", "us-east-1") + + response = client.describe_event_bus() + + response["Name"].should.equal("default") + response["Arn"].should.equal( + "arn:aws:events:us-east-1:123456789012:event-bus/default" + ) + response.should_not.have.key("Policy") + + client.create_event_bus(Name="test-bus") + client.put_permission( + EventBusName="test-bus", + Action="events:PutEvents", + Principal="111111111111", + StatementId="test", + ) + + response = client.describe_event_bus(Name="test-bus") + + response["Name"].should.equal("test-bus") + response["Arn"].should.equal( + "arn:aws:events:us-east-1:123456789012:event-bus/test-bus" + ) + json.loads(response["Policy"]).should.equal( + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "test", + "Effect": "Allow", + "Principal": {"AWS": "arn:aws:iam::111111111111:root"}, + "Action": "events:PutEvents", + "Resource": "arn:aws:events:us-east-1:123456789012:event-bus/test-bus", + } + ], + } + ) + + +@mock_events +def test_describe_event_bus_errors(): + client = boto3.client("events", "us-east-1") + + client.describe_event_bus.when.called_with(Name="non-existing").should.throw( + ClientError, "Event bus non-existing does not exist." + ) + + +@mock_events +def test_list_event_buses(): + client = boto3.client("events", "us-east-1") + client.create_event_bus(Name="test-bus-1") + client.create_event_bus(Name="test-bus-2") + client.create_event_bus(Name="other-bus-1") + client.create_event_bus(Name="other-bus-2") + + response = client.list_event_buses() + + response["EventBuses"].should.have.length_of(5) + sorted(response["EventBuses"], key=lambda i: i["Name"]).should.equal( + [ + { + "Name": "default", + "Arn": "arn:aws:events:us-east-1:123456789012:event-bus/default", + }, + { + "Name": "other-bus-1", + "Arn": "arn:aws:events:us-east-1:123456789012:event-bus/other-bus-1", + }, + { + "Name": "other-bus-2", + "Arn": "arn:aws:events:us-east-1:123456789012:event-bus/other-bus-2", + }, + { + "Name": "test-bus-1", + "Arn": "arn:aws:events:us-east-1:123456789012:event-bus/test-bus-1", + }, + { + "Name": "test-bus-2", + "Arn": "arn:aws:events:us-east-1:123456789012:event-bus/test-bus-2", + }, + ] + ) + + response = client.list_event_buses(NamePrefix="other-bus") + + response["EventBuses"].should.have.length_of(2) + sorted(response["EventBuses"], key=lambda i: i["Name"]).should.equal( + [ + { + "Name": "other-bus-1", + "Arn": "arn:aws:events:us-east-1:123456789012:event-bus/other-bus-1", + }, + { + "Name": "other-bus-2", + "Arn": "arn:aws:events:us-east-1:123456789012:event-bus/other-bus-2", + }, + ] + ) + + +@mock_events +def test_delete_event_bus(): + client = boto3.client("events", "us-east-1") + client.create_event_bus(Name="test-bus") + + response = client.list_event_buses() + response["EventBuses"].should.have.length_of(2) + + client.delete_event_bus(Name="test-bus") + + response = client.list_event_buses() + response["EventBuses"].should.have.length_of(1) + response["EventBuses"].should.equal( + [ + { + "Name": "default", + "Arn": "arn:aws:events:us-east-1:123456789012:event-bus/default", + } + ] + ) + + # deleting non existing event bus should be successful + client.delete_event_bus(Name="non-existing") + + +@mock_events +def test_delete_event_bus_errors(): + client = boto3.client("events", "us-east-1") + + client.delete_event_bus.when.called_with(Name="default").should.throw( + ClientError, "Cannot delete event bus default." + )