Skip to content

feat: Add sns notification support to Parser utility #206 #207

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 18, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .base import BaseEnvelope
from .dynamodb import DynamoDBStreamEnvelope
from .event_bridge import EventBridgeEnvelope
from .sns import SnsEnvelope
from .sqs import SqsEnvelope

__all__ = ["DynamoDBStreamEnvelope", "EventBridgeEnvelope", "SqsEnvelope", "BaseEnvelope"]
__all__ = ["DynamoDBStreamEnvelope", "EventBridgeEnvelope", "SnsEnvelope", "SqsEnvelope", "BaseEnvelope"]
42 changes: 42 additions & 0 deletions aws_lambda_powertools/utilities/parser/envelopes/sns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import logging
from typing import Any, Dict, List, Optional, Union

from ..models import SnsModel
from ..types import Model
from .base import BaseEnvelope

logger = logging.getLogger(__name__)


class SnsEnvelope(BaseEnvelope):
"""SNS Envelope to extract array of Records

The record's body parameter is a string, though it can also be a JSON encoded string.
Regardless of its type it'll be parsed into a BaseModel object.

Note: Records will be parsed the same way so if model is str,
all items in the list will be parsed as str and npt as JSON (and vice versa)
"""

def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Model) -> List[Optional[Model]]:
"""Parses records found with model provided

Parameters
----------
data : Dict
Lambda event to be parsed
model : Model
Data model provided to parse after extracting data using envelope

Returns
-------
List
List of records parsed with model provided
"""
logger.debug(f"Parsing incoming data with SNS model {SnsModel}")
parsed_envelope = SnsModel.parse_obj(data)
output = []
logger.debug(f"Parsing SNS records in `body` with {model}")
for record in parsed_envelope.Records:
output.append(self._parse(data=record.Sns.Message, model=model))
return output
4 changes: 4 additions & 0 deletions aws_lambda_powertools/utilities/parser/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
from .dynamodb import DynamoDBStreamChangedRecordModel, DynamoDBStreamModel, DynamoDBStreamRecordModel
from .event_bridge import EventBridgeModel
from .sns import SnsModel, SnsNotificationModel, SnsRecordModel
from .sqs import SqsModel, SqsRecordModel

__all__ = [
"DynamoDBStreamModel",
"EventBridgeModel",
"DynamoDBStreamChangedRecordModel",
"DynamoDBStreamRecordModel",
"SnsModel",
"SnsNotificationModel",
"SnsRecordModel",
"SqsModel",
"SqsRecordModel",
]
36 changes: 36 additions & 0 deletions aws_lambda_powertools/utilities/parser/models/sns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from datetime import datetime
from typing import Dict, List, Optional

from pydantic import BaseModel
from pydantic.networks import HttpUrl
from typing_extensions import Literal


class SqsMsgAttributeModel(BaseModel):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meant SNS instead of SQS?

Suggested change
class SqsMsgAttributeModel(BaseModel):
class SnsMsgAttributeModel(BaseModel):

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed!

Type: str
Value: str


class SnsNotificationModel(BaseModel):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need groupId and dedupId optional fields that might be added when FIFO is enabled - Haven't tried yet myself.

Publisher reference: https://docs.aws.amazon.com/sns/latest/dg/fifo-topic-code-examples.html#fifo-topic-java-publish

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolving it as @risenberg-cyberark confirmed Lambda cannot directly subscribe to SNS FIFO topics, it'd have to go through SQS FIFO, in which case our model already covers it.

Subject: Optional[str]
TopicArn: str
UnsubscribeUrl: HttpUrl
Type: Literal["Notification"]
MessageAttributes: Dict[str, SqsMsgAttributeModel]
Message: str
MessageId: str
SigningCertUrl: HttpUrl
Signature: str
Timestamp: datetime
SignatureVersion: str


class SnsRecordModel(BaseModel):
EventSource: Literal["aws:sns"]
EventVersion: str
EventSubscriptionArn: str
Sns: SnsNotificationModel


class SnsModel(BaseModel):
Records: List[SnsRecordModel]
6 changes: 3 additions & 3 deletions tests/events/snsEvent.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"SignatureVersion": "1",
"Timestamp": "2019-01-02T12:45:07.000Z",
"Signature": "tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r==",
"SigningCertUrl": "https://sns.us-east-2.amazonaws.com/SimpleNotificat ...",
"SigningCertUrl": "https://sns.us-east-2.amazonaws.com/SimpleNotification",
"MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
"Message": "Hello from SNS!",
"MessageAttributes": {
Expand All @@ -22,10 +22,10 @@
}
},
"Type": "Notification",
"UnsubscribeUrl": "https://sns.us-east-2.amazonaws.com/?Action=Unsubscri ...",
"UnsubscribeUrl": "https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe",
"TopicArn": "arn:aws:sns:us-east-2:123456789012:sns-lambda",
"Subject": "TestInvoke"
}
}
]
}
}
20 changes: 20 additions & 0 deletions tests/functional/parser/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
DynamoDBStreamModel,
DynamoDBStreamRecordModel,
EventBridgeModel,
SnsModel,
SnsNotificationModel,
SnsRecordModel,
SqsModel,
SqsRecordModel,
)
Expand Down Expand Up @@ -51,3 +54,20 @@ class MyAdvancedSqsRecordModel(SqsRecordModel):

class MyAdvancedSqsBusiness(SqsModel):
Records: List[MyAdvancedSqsRecordModel]


class MySnsBusiness(BaseModel):
message: str
username: str


class MySnsNotificationModel(SnsNotificationModel):
Message: str


class MyAdvancedSnsRecordModel(SnsRecordModel):
Sns: MySnsNotificationModel


class MyAdvancedSnsBusiness(SnsModel):
Records: List[MyAdvancedSnsRecordModel]
93 changes: 93 additions & 0 deletions tests/functional/parser/test_sns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from typing import Any, List

import pytest

from aws_lambda_powertools.utilities.parser import ValidationError, envelopes, event_parser
from aws_lambda_powertools.utilities.typing import LambdaContext
from tests.functional.parser.schemas import MyAdvancedSnsBusiness, MySnsBusiness
from tests.functional.parser.utils import load_event
from tests.functional.validator.conftest import sns_event # noqa: F401


@event_parser(model=MySnsBusiness, envelope=envelopes.SnsEnvelope)
def handle_sns_json_body(event: List[MySnsBusiness], _: LambdaContext):
assert len(event) == 1
assert event[0].message == "hello world"
assert event[0].username == "lessa"


def test_handle_sns_trigger_event_json_body(sns_event): # noqa: F811
handle_sns_json_body(sns_event, LambdaContext())


def test_validate_event_does_not_conform_with_model():
event: Any = {"invalid": "event"}

with pytest.raises(ValidationError):
handle_sns_json_body(event, LambdaContext())


def test_validate_event_does_not_conform_user_json_string_with_model():
event: Any = {
"Records": [
{
"EventVersion": "1.0",
"EventSubscriptionArn": "arn:aws:sns:us-east-2:123456789012:sns-la ...",
"EventSource": "aws:sns",
"Sns": {
"SignatureVersion": "1",
"Timestamp": "2019-01-02T12:45:07.000Z",
"Signature": "tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r==",
"SigningCertUrl": "https://sns.us-east-2.amazonaws.com/SimpleNotificat ...",
"MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
"Message": "not a valid JSON!",
"MessageAttributes": {"Test": {"Type": "String", "Value": "TestString"}},
"Type": "Notification",
"UnsubscribeUrl": "https://sns.us-east-2.amazonaws.com/?Action=Unsubscri ...",
"TopicArn": "arn:aws:sns:us-east-2:123456789012:sns-lambda",
"Subject": "TestInvoke",
},
}
]
}

with pytest.raises(ValidationError):
handle_sns_json_body(event, LambdaContext())


@event_parser(model=MyAdvancedSnsBusiness)
def handle_sns_no_envelope(event: MyAdvancedSnsBusiness, _: LambdaContext):
records = event.Records
record = records[0]

assert len(records) == 1
assert record.EventVersion == "1.0"
assert record.EventSubscriptionArn == "arn:aws:sns:us-east-2:123456789012:sns-la ..."
assert record.EventSource == "aws:sns"
assert record.Sns.Type == "Notification"
assert record.Sns.UnsubscribeUrl.scheme == "https"
assert record.Sns.UnsubscribeUrl.host == "sns.us-east-2.amazonaws.com"
assert record.Sns.UnsubscribeUrl.query == "Action=Unsubscribe"
assert record.Sns.TopicArn == "arn:aws:sns:us-east-2:123456789012:sns-lambda"
assert record.Sns.Subject == "TestInvoke"
assert record.Sns.SignatureVersion == "1"
convert_time = int(round(record.Sns.Timestamp.timestamp() * 1000))
assert convert_time == 1546433107000
assert record.Sns.Signature == "tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r=="
assert record.Sns.SigningCertUrl.host == "sns.us-east-2.amazonaws.com"
assert record.Sns.SigningCertUrl.scheme == "https"
assert record.Sns.SigningCertUrl.host == "sns.us-east-2.amazonaws.com"
assert record.Sns.SigningCertUrl.path == "/SimpleNotification"
assert record.Sns.MessageId == "95df01b4-ee98-5cb9-9903-4c221d41eb5e"
assert record.Sns.Message == "Hello from SNS!"
attrib_dict = record.Sns.MessageAttributes
assert len(attrib_dict) == 2
assert attrib_dict["Test"].Type == "String"
assert attrib_dict["Test"].Value == "TestString"
assert attrib_dict["TestBinary"].Type == "Binary"
assert attrib_dict["TestBinary"].Value == "TestBinary"


def test_handle_sns_trigger_event_no_envelope():
event_dict = load_event("snsEvent.json")
handle_sns_no_envelope(event_dict, LambdaContext())
4 changes: 2 additions & 2 deletions tests/functional/test_lambda_trigger_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,15 +487,15 @@ def test_sns_trigger_event():
assert sns.signature_version == "1"
assert sns.timestamp == "2019-01-02T12:45:07.000Z"
assert sns.signature == "tcc6faL2yUC6dgZdmrwh1Y4cGa/ebXEkAi6RibDsvpi+tE/1+82j...65r=="
assert sns.signing_cert_url == "https://sns.us-east-2.amazonaws.com/SimpleNotificat ..."
assert sns.signing_cert_url == "https://sns.us-east-2.amazonaws.com/SimpleNotification"
assert sns.message_id == "95df01b4-ee98-5cb9-9903-4c221d41eb5e"
assert sns.message == "Hello from SNS!"
message_attributes = sns.message_attributes
test_message_attribute = message_attributes["Test"]
assert test_message_attribute.get_type == "String"
assert test_message_attribute.value == "TestString"
assert sns.get_type == "Notification"
assert sns.unsubscribe_url == "https://sns.us-east-2.amazonaws.com/?Action=Unsubscri ..."
assert sns.unsubscribe_url == "https://sns.us-east-2.amazonaws.com/?Action=Unsubscribe"
assert sns.topic_arn == "arn:aws:sns:us-east-2:123456789012:sns-lambda"
assert sns.subject == "TestInvoke"
assert event.record._data == event["Records"][0]
Expand Down
4 changes: 2 additions & 2 deletions tests/functional/validator/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,8 @@ def sns_event():
"Timestamp": "1970-01-01T00:00:00.000Z",
"SignatureVersion": "1",
"Signature": "EXAMPLE",
"SigningCertUrl": "EXAMPLE",
"UnsubscribeUrl": "EXAMPLE",
"SigningCertUrl": "https://www.example.com",
"UnsubscribeUrl": "https://www.example.com",
"MessageAttributes": {
"Test": {"Type": "String", "Value": "TestString"},
"TestBinary": {"Type": "Binary", "Value": "TestBinary"},
Expand Down