Enhancement/3837 (#3847)

* Move event pattern validation into EventPattern class and apply enhanced pattern logic to all Rules

* Fix exists filtering logic to only match leaf nodes in event

* Apply black formatting

* Replace JSONDecodeError with ValueError for Python2 compatibility

* Update unit test names

* Move event pattern tests into test_event_pattern.py

* Apply black formatting

Co-authored-by: TSNoble <tom.noble@bjss.com>
This commit is contained in:
Tom Noble 2021-04-10 14:27:38 +01:00 committed by GitHub
commit 3942613bf4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 124 additions and 135 deletions

View file

@ -33,7 +33,7 @@ class Rule(CloudFormationModel):
def __init__(self, name, region_name, **kwargs):
self.name = name
self.region_name = region_name
self.event_pattern = kwargs.get("EventPattern")
self.event_pattern = EventPattern(kwargs.get("EventPattern"))
self.schedule_exp = kwargs.get("ScheduleExpression")
self.state = kwargs.get("State") or "ENABLED"
self.description = kwargs.get("Description")
@ -100,7 +100,7 @@ class Rule(CloudFormationModel):
if event_bus_name != self.event_bus_name:
return
if not self._validate_event(event):
if not self.event_pattern.matches_event(event):
return
# supported targets
@ -123,23 +123,6 @@ class Rule(CloudFormationModel):
else:
raise NotImplementedError("Expr not defined for {0}".format(type(self)))
def _validate_event(self, event):
for field, pattern in json.loads(self.event_pattern).items():
if not isinstance(pattern, list):
# to keep it simple at the beginning only pattern with 1 level of depth are validated
continue
if isinstance(pattern[0], dict):
if "exists" in pattern[0]:
if pattern[0]["exists"] and field not in event:
return False
elif not pattern[0]["exists"] and field in event:
return False
elif event.get(field) not in pattern:
return False
return True
def _parse_arn(self, arn):
# http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html
# this method needs probably some more fine tuning,
@ -191,8 +174,7 @@ class Rule(CloudFormationModel):
archive_name, archive_uuid = resource_id.split(":")
archive = events_backends[self.region_name].archives.get(archive_name)
if archive.uuid == archive_uuid:
if archive.event_pattern.matches_event(event):
archive.events.append(event)
archive.events.append(event)
def _send_to_sqs_queue(self, resource_id, event, group_id=None):
from moto.sqs import sqs_backends
@ -413,7 +395,7 @@ class Archive(CloudFormationModel):
if description:
self.description = description
if event_pattern:
self.event_pattern = event_pattern
self.event_pattern = EventPattern(event_pattern)
if retention:
self.retention = retention
@ -562,11 +544,36 @@ class Replay(BaseModel):
class EventPattern:
def __init__(self, filter):
self._filter = json.loads(filter) if filter else None
self._filter = self._load_event_pattern(filter)
if not self._validate_event_pattern(self._filter):
raise InvalidEventPatternException
def __str__(self):
return json.dumps(self._filter)
def _load_event_pattern(self, pattern):
try:
return json.loads(pattern) if pattern else None
except ValueError:
raise InvalidEventPatternException
def _validate_event_pattern(self, pattern):
# values in the event pattern have to be either a dict or an array
if pattern is None:
return True
dicts_valid = [
self._validate_event_pattern(value)
for value in pattern.values()
if isinstance(value, dict)
]
non_dicts_valid = [
isinstance(value, list)
for value in pattern.values()
if not isinstance(value, dict)
]
return all(dicts_valid) and all(non_dicts_valid)
def matches_event(self, event):
if not self._filter:
return True
@ -600,9 +607,10 @@ class EventPattern:
def _does_item_match_named_filter(self, item, filter):
filter_name, filter_value = list(filter.items())[0]
if filter_name == "exists":
item_exists = item is not None
is_leaf_node = not isinstance(item, dict)
leaf_exists = is_leaf_node and item is not None
should_exist = filter_value
return item_exists if should_exist else not item_exists
return leaf_exists if should_exist else not leaf_exists
if filter_name == "prefix":
prefix = filter_value
return item.startswith(prefix)
@ -1052,9 +1060,6 @@ class EventsBackend(BaseBackend):
"Member must have length less than or equal to 48".format(name)
)
if event_pattern:
self._validate_event_pattern(event_pattern)
event_bus = self._get_event_bus(source_arn)
if name in self.archives:
@ -1104,26 +1109,6 @@ class EventsBackend(BaseBackend):
return archive
def _validate_event_pattern(self, pattern):
try:
json_pattern = json.loads(pattern)
except ValueError: # json.JSONDecodeError exists since Python 3.5
raise InvalidEventPatternException
if not self._is_event_value_an_array(json_pattern):
raise InvalidEventPatternException
def _is_event_value_an_array(self, pattern):
# the values of a key in the event pattern have to be either a dict or an array
for value in pattern.values():
if isinstance(value, dict):
if not self._is_event_value_an_array(value):
return False
elif not isinstance(value, list):
return False
return True
def describe_archive(self, name):
archive = self.archives.get(name)
@ -1168,9 +1153,6 @@ class EventsBackend(BaseBackend):
if not archive:
raise ResourceNotFoundException("Archive {} does not exist.".format(name))
if event_pattern:
self._validate_event_pattern(event_pattern)
archive.update(description, event_pattern, retention)
return {

View file

@ -20,7 +20,7 @@ class EventsHandler(BaseResponse):
return {
"Name": rule.name,
"Arn": rule.arn,
"EventPattern": rule.event_pattern,
"EventPattern": str(rule.event_pattern),
"State": rule.state,
"Description": rule.description,
"ScheduleExpression": rule.schedule_exp,