Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 25 additions & 10 deletions localstack-core/localstack/services/events/event_rule_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,18 @@ def _evaluate_nested_event_pattern_on_dict(self, event_pattern, payload: dict) -

# TODO: maybe save/cache the flattened/expanded pattern?
flat_pattern_conditions = self.flatten_pattern(event_pattern)
flat_payloads = self.flatten_payload(payload)
flat_payloads = self.flatten_payload(payload, flat_pattern_conditions)

return any(
all(
any(
self._evaluate_condition(
flat_payload.get(key), condition, field_exists=key in flat_payload
)
for condition in values
for condition in conditions
for flat_payload in flat_payloads
)
for key, values in flat_pattern.items()
for key, conditions in flat_pattern.items()
)
for flat_pattern in flat_pattern_conditions
)
Expand Down Expand Up @@ -147,7 +147,7 @@ def _evaluate_cidr(condition: str, value: str) -> bool:

@staticmethod
def _evaluate_wildcard(condition: str, value: str) -> bool:
return re.match(re.escape(condition).replace("\\*", ".+") + "$", value)
return bool(re.match(re.escape(condition).replace("\\*", ".+") + "$", value))

@staticmethod
def _evaluate_numeric_condition(conditions: list, value: t.Any) -> bool:
Expand Down Expand Up @@ -237,18 +237,24 @@ def _traverse_event_pattern(obj, array=None, parent_key=None) -> list:
return _traverse_event_pattern(nested_dict)

@staticmethod
def flatten_payload(nested_dict: dict) -> list[dict]:
def flatten_payload(payload: dict, patterns: list[dict]) -> list[dict]:
"""
Takes a dictionary as input and will output the dictionary on a single level.
The dictionary can have lists containing other dictionaries, and one root level entry will be created for every
item in a list.
item in a list if it corresponds to the entries of the patterns.
Input:
payload:
`{"field1": {
"field2: [
{"field3: "val1", "field4": "val2"},
{"field3: "val3", "field4": "val4"},
}
]}`
patterns:
`[
"field1.field2.field3": <condition>,
"field1.field2.field4": <condition>,
]`
Output:
`[
{
Expand All @@ -260,16 +266,25 @@ def flatten_payload(nested_dict: dict) -> list[dict]:
"field1.field2.field4": "val4"
},
]`
:param nested_dict: a (nested) dictionary
:param payload: a (nested) dictionary, the event payload
:param patterns: the flattened patterns from the EventPattern (see flatten_pattern)
:return: flatten_dict: a dictionary with no nested dict inside, flattened to a single level
"""
patterns_keys = {key for keys in patterns for key in keys}

def _is_key_in_patterns(key: str) -> bool:
return key is None or any(pattern_key.startswith(key) for pattern_key in patterns_keys)

def _traverse(_object: dict, array=None, parent_key=None) -> list:
if isinstance(_object, dict):
for key, values in _object.items():
# We update the parent key do that {"key1": {"key2": ""}} becomes "key1.key2"
# We update the parent key so that {"key1": {"key2": ""}} becomes "key1.key2"
_parent_key = f"{parent_key}.{key}" if parent_key else key
array = _traverse(values, array, _parent_key)

# we make sure that we are building only the relevant parts of the payload related to the pattern
# the payload could be very complex, and the pattern only applies to part of it
if _is_key_in_patterns(_parent_key):
array = _traverse(values, array, _parent_key)

elif isinstance(_object, list):
if not _object:
Expand All @@ -279,7 +294,7 @@ def _traverse(_object: dict, array=None, parent_key=None) -> list:
array = [{**item, parent_key: _object} for item in array]
return array

return _traverse(nested_dict, array=[{}], parent_key=None)
return _traverse(payload, array=[{}], parent_key=None)


class EventPatternCompiler:
Expand Down
23 changes: 23 additions & 0 deletions tests/aws/services/events/test_events_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from localstack.testing.pytest import markers
from localstack.utils.common import short_uid
from localstack.utils.files import load_file
from tests.aws.services.events.helper_functions import (
is_old_provider,
sqs_collect_messages,
Expand All @@ -22,6 +23,7 @@
REQUEST_TEMPLATE_DIR, "complex_multi_key_event_pattern.json"
)
COMPLEX_MULTI_KEY_EVENT = os.path.join(REQUEST_TEMPLATE_DIR, "complex_multi_key_event.json")
TEST_PAYLOAD_DIR = os.path.join(THIS_FOLDER, "test_payloads")


def load_request_templates(directory_path: str) -> List[Tuple[dict, str]]:
Expand Down Expand Up @@ -262,6 +264,27 @@ def test_invalid_event_payload(self, aws_client, snapshot):
)
snapshot.match("plain-string-payload-exc", e.value.response)

@markers.aws.validated
def test_event_with_large_and_complex_payload(self, aws_client, snapshot):
event_file_path = os.path.join(TEST_PAYLOAD_DIR, "large_complex_payload.json")
event = load_file(event_file_path)

simple_pattern = {"detail-type": ["cmd.documents.generate"]}
response = aws_client.events.test_event_pattern(
Event=event,
EventPattern=json.dumps(simple_pattern),
)
snapshot.match("complex-event-simple-pattern", response)

complex_pattern = {
"detail": {"payload.nested.another-level.deep": {"inside-list": [{"prefix": "q-test"}]}}
}
response = aws_client.events.test_event_pattern(
Event=event,
EventPattern=json.dumps(complex_pattern),
)
snapshot.match("complex-event-complex-pattern", response)


class TestRuleWithPattern:
@markers.aws.validated
Expand Down
19 changes: 19 additions & 0 deletions tests/aws/services/events/test_events_patterns.snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -1296,5 +1296,24 @@
"exception_type": "<class 'botocore.errorfactory.InvalidEventPatternException'>"
}
}
},
"tests/aws/services/events/test_events_patterns.py::TestEventPattern::test_event_with_large_and_complex_payload": {
"recorded-date": "17-03-2025, 10:58:02",
"recorded-content": {
"complex-event-simple-pattern": {
"Result": true,
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
},
"complex-event-complex-pattern": {
"Result": true,
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 200
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,9 @@
"tests/aws/services/events/test_events_patterns.py::TestEventPattern::test_event_pattern_with_multi_key": {
"last_validated_date": "2024-07-11T13:55:38+00:00"
},
"tests/aws/services/events/test_events_patterns.py::TestEventPattern::test_event_with_large_and_complex_payload": {
"last_validated_date": "2025-03-17T10:58:02+00:00"
},
"tests/aws/services/events/test_events_patterns.py::TestEventPattern::test_invalid_event_payload": {
"last_validated_date": "2024-12-05T17:44:17+00:00"
},
Expand Down
Loading
Loading