Skip to content

Commit

Permalink
feat(parser): add support for API Gateway HTTP API #434 (#441)
Browse files Browse the repository at this point in the history
Co-authored-by: Heitor Lessa <[email protected]>
Co-authored-by: heitorlessa <[email protected]>
  • Loading branch information
3 people authored May 28, 2021
1 parent bfb67e7 commit 995c56e
Show file tree
Hide file tree
Showing 10 changed files with 235 additions and 19 deletions.
2 changes: 2 additions & 0 deletions aws_lambda_powertools/utilities/parser/envelopes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .apigw import ApiGatewayEnvelope
from .apigwv2 import ApiGatewayV2Envelope
from .base import BaseEnvelope
from .cloudwatch import CloudWatchLogsEnvelope
from .dynamodb import DynamoDBStreamEnvelope
Expand All @@ -9,6 +10,7 @@

__all__ = [
"ApiGatewayEnvelope",
"ApiGatewayV2Envelope",
"CloudWatchLogsEnvelope",
"DynamoDBStreamEnvelope",
"EventBridgeEnvelope",
Expand Down
32 changes: 32 additions & 0 deletions aws_lambda_powertools/utilities/parser/envelopes/apigwv2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import logging
from typing import Any, Dict, Optional, Type, Union

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

logger = logging.getLogger(__name__)


class ApiGatewayV2Envelope(BaseEnvelope):
"""API Gateway V2 envelope to extract data within body key"""

def parse(self, data: Optional[Union[Dict[str, Any], Any]], model: Type[Model]) -> Optional[Model]:
"""Parses data found with model provided
Parameters
----------
data : Dict
Lambda event to be parsed
model : Type[Model]
Data model provided to parse after extracting data using envelope
Returns
-------
Any
Parsed detail payload with model provided
"""
logger.debug(f"Parsing incoming data with Api Gateway model V2 {APIGatewayProxyEventV2Model}")
parsed_envelope = APIGatewayProxyEventV2Model.parse_obj(data)
logger.debug(f"Parsing event payload in `detail` with {model}")
return self._parse(data=parsed_envelope.body, model=model)
16 changes: 16 additions & 0 deletions aws_lambda_powertools/utilities/parser/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
APIGatewayEventRequestContext,
APIGatewayProxyEventModel,
)
from .apigwv2 import (
APIGatewayProxyEventV2Model,
RequestContextV2,
RequestContextV2Authorizer,
RequestContextV2AuthorizerIam,
RequestContextV2AuthorizerIamCognito,
RequestContextV2AuthorizerJwt,
RequestContextV2Http,
)
from .cloudwatch import CloudWatchLogsData, CloudWatchLogsDecode, CloudWatchLogsLogEvent, CloudWatchLogsModel
from .dynamodb import DynamoDBStreamChangedRecordModel, DynamoDBStreamModel, DynamoDBStreamRecordModel
from .event_bridge import EventBridgeModel
Expand Down Expand Up @@ -35,6 +44,13 @@
from .sqs import SqsAttributesModel, SqsModel, SqsMsgAttributeModel, SqsRecordModel

__all__ = [
"APIGatewayProxyEventV2Model",
"RequestContextV2",
"RequestContextV2Http",
"RequestContextV2Authorizer",
"RequestContextV2AuthorizerJwt",
"RequestContextV2AuthorizerIam",
"RequestContextV2AuthorizerIamCognito",
"CloudWatchLogsData",
"CloudWatchLogsDecode",
"CloudWatchLogsLogEvent",
Expand Down
71 changes: 71 additions & 0 deletions aws_lambda_powertools/utilities/parser/models/apigwv2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from datetime import datetime
from typing import Any, Dict, List, Optional

from pydantic import BaseModel, Field
from pydantic.networks import IPvAnyNetwork

from ..types import Literal


class RequestContextV2AuthorizerIamCognito(BaseModel):
amr: List[str]
identityId: str
identityPoolId: str


class RequestContextV2AuthorizerIam(BaseModel):
accessKey: Optional[str]
accountId: Optional[str]
callerId: Optional[str]
principalOrgId: Optional[str]
userArn: Optional[str]
userId: Optional[str]
cognitoIdentity: RequestContextV2AuthorizerIamCognito


class RequestContextV2AuthorizerJwt(BaseModel):
claims: Dict[str, Any]
scopes: List[str]


class RequestContextV2Authorizer(BaseModel):
jwt: Optional[RequestContextV2AuthorizerJwt]
iam: Optional[RequestContextV2AuthorizerIam]
lambda_value: Optional[Dict[str, Any]] = Field(None, alias="lambda")


class RequestContextV2Http(BaseModel):
method: Literal["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
path: str
protocol: str
sourceIp: IPvAnyNetwork
userAgent: str


class RequestContextV2(BaseModel):
accountId: str
apiId: str
authorizer: Optional[RequestContextV2Authorizer]
domainName: str
domainPrefix: str
requestId: str
routeKey: str
stage: str
time: str
timeEpoch: datetime
http: RequestContextV2Http


class APIGatewayProxyEventV2Model(BaseModel):
version: str
routeKey: str
rawPath: str
rawQueryString: str
cookies: Optional[List[str]]
headers: Dict[str, str]
queryStringParameters: Dict[str, str]
pathParameters: Optional[Dict[str, str]]
stageVariables: Optional[Dict[str, str]]
requestContext: RequestContextV2
body: str
isBase64Encoded: bool
23 changes: 12 additions & 11 deletions docs/utilities/parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ Parser comes with the following built-in models:
| **SesModel** | Lambda Event Source payload for Amazon Simple Email Service |
| **SnsModel** | Lambda Event Source payload for Amazon Simple Notification Service |
| **APIGatewayProxyEvent** | Lambda Event Source payload for Amazon API Gateway |
| **APIGatewayProxyEventV2Model** | Lambda Event Source payload for Amazon API Gateway v2 payload |

### extending built-in models

Expand Down Expand Up @@ -295,17 +296,17 @@ Here's an example of parsing a model found in an event coming from EventBridge,

Parser comes with the following built-in envelopes, where `Model` in the return section is your given model.

| Envelope name | Behaviour | Return |
| -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| **DynamoDBStreamEnvelope** | 1. Parses data using `DynamoDBStreamModel`. <br/> 2. Parses records in `NewImage` and `OldImage` keys using your model. <br/> 3. Returns a list with a dictionary containing `NewImage` and `OldImage` keys | `List[Dict[str, Optional[Model]]]` |
| **EventBridgeEnvelope** | 1. Parses data using `EventBridgeModel`. <br/> 2. Parses `detail` key using your model and returns it. | `Model` |
| **SqsEnvelope** | 1. Parses data using `SqsModel`. <br/> 2. Parses records in `body` key using your model and return them in a list. | `List[Model]` |
| **CloudWatchLogsEnvelope** | 1. Parses data using `CloudwatchLogsModel` which will base64 decode and decompress it. <br/> 2. Parses records in `message` key using your model and return them in a list. | `List[Model]` |
| **KinesisDataStreamEnvelope** | 1. Parses data using `KinesisDataStreamModel` which will base64 decode it. <br/> 2. Parses records in in `Records` key using your model and returns them in a list. | `List[Model]` |
| **SnsEnvelope** | 1. Parses data using `SnsModel`. <br/> 2. Parses records in `body` key using your model and return them in a list. | `List[Model]` |
| **SnsSqsEnvelope** | 1. Parses data using `SqsModel`. <br/> 2. Parses SNS records in `body` key using `SnsNotificationModel`. <br/> 3. Parses data in `Message` key using your model and return them in a list. | `List[Model]` |
| **ApiGatewayEnvelope** | 1. Parses data using `APIGatewayProxyEventModel`. <br/> 2. Parses `body` key using your model and returns it. | `Model` |

| Envelope name | Behaviour | Return |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| **DynamoDBStreamEnvelope** | 1. Parses data using `DynamoDBStreamModel`. <br/> 2. Parses records in `NewImage` and `OldImage` keys using your model. <br/> 3. Returns a list with a dictionary containing `NewImage` and `OldImage` keys | `List[Dict[str, Optional[Model]]]` |
| **EventBridgeEnvelope** | 1. Parses data using `EventBridgeModel`. <br/> 2. Parses `detail` key using your model and returns it. | `Model` |
| **SqsEnvelope** | 1. Parses data using `SqsModel`. <br/> 2. Parses records in `body` key using your model and return them in a list. | `List[Model]` |
| **CloudWatchLogsEnvelope** | 1. Parses data using `CloudwatchLogsModel` which will base64 decode and decompress it. <br/> 2. Parses records in `message` key using your model and return them in a list. | `List[Model]` |
| **KinesisDataStreamEnvelope** | 1. Parses data using `KinesisDataStreamModel` which will base64 decode it. <br/> 2. Parses records in in `Records` key using your model and returns them in a list. | `List[Model]` |
| **SnsEnvelope** | 1. Parses data using `SnsModel`. <br/> 2. Parses records in `body` key using your model and return them in a list. | `List[Model]` |
| **SnsSqsEnvelope** | 1. Parses data using `SqsModel`. <br/> 2. Parses SNS records in `body` key using `SnsNotificationModel`. <br/> 3. Parses data in `Message` key using your model and return them in a list. | `List[Model]` |
| **ApiGatewayEnvelope** | 1. Parses data using `APIGatewayProxyEventModel`. <br/> 2. Parses `body` key using your model and returns it. | `Model` |
| **ApiGatewayV2Envelope** | 1. Parses data using `APIGatewayProxyEventV2Model`. <br/> 2. Parses `body` key using your model and returns it. | `Model` |
### Bringing your own envelope

You can create your own Envelope model and logic by inheriting from `BaseEnvelope`, and implementing the `parse` method.
Expand Down
4 changes: 2 additions & 2 deletions tests/events/apiGatewayProxyV2Event.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"method": "POST",
"path": "/my/path",
"protocol": "HTTP/1.1",
"sourceIp": "IP",
"sourceIp": "192.168.0.1/32",
"userAgent": "agent"
},
"requestId": "id",
Expand All @@ -54,4 +54,4 @@
"stageVariable1": "value1",
"stageVariable2": "value2"
}
}
}
8 changes: 5 additions & 3 deletions tests/events/apiGatewayProxyV2IamEvent.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
"accountId": "1234567890",
"callerId": "AROA7ZJZYVRE7C3DUXHH6:CognitoIdentityCredentials",
"cognitoIdentity": {
"amr" : ["foo"],
"amr": [
"foo"
],
"identityId": "us-east-1:3f291106-8703-466b-8f2b-3ecee1ca56ce",
"identityPoolId": "us-east-1:4f291106-8703-466b-8f2b-3ecee1ca56ce"
},
Expand All @@ -47,7 +49,7 @@
"method": "GET",
"path": "/my/path",
"protocol": "HTTP/1.1",
"sourceIp": "IP",
"sourceIp": "192.168.0.1/32",
"userAgent": "agent"
}
},
Expand All @@ -57,4 +59,4 @@
},
"body": "{\r\n\t\"a\": 1\r\n}",
"isBase64Encoded": false
}
}
4 changes: 2 additions & 2 deletions tests/events/apiGatewayProxyV2LambdaAuthorizerEvent.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"method": "GET",
"path": "/my/path",
"protocol": "HTTP/1.1",
"sourceIp": "IP",
"sourceIp": "192.168.0.1/32",
"userAgent": "agent"
}
},
Expand All @@ -47,4 +47,4 @@
},
"body": "{\r\n\t\"a\": 1\r\n}",
"isBase64Encoded": false
}
}
92 changes: 92 additions & 0 deletions tests/functional/parser/test_apigwv2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from aws_lambda_powertools.utilities.parser import envelopes, event_parser
from aws_lambda_powertools.utilities.parser.models import (
APIGatewayProxyEventV2Model,
RequestContextV2,
RequestContextV2Authorizer,
)
from aws_lambda_powertools.utilities.typing import LambdaContext
from tests.functional.parser.schemas import MyApiGatewayBusiness
from tests.functional.utils import load_event


@event_parser(model=MyApiGatewayBusiness, envelope=envelopes.ApiGatewayV2Envelope)
def handle_apigw_with_envelope(event: MyApiGatewayBusiness, _: LambdaContext):
assert event.message == "Hello"
assert event.username == "Ran"


@event_parser(model=APIGatewayProxyEventV2Model)
def handle_apigw_event(event: APIGatewayProxyEventV2Model, _: LambdaContext):
return event


def test_apigw_v2_event_with_envelope():
event = load_event("apiGatewayProxyV2Event.json")
event["body"] = '{"message": "Hello", "username": "Ran"}'
handle_apigw_with_envelope(event, LambdaContext())


def test_apigw_v2_event_jwt_authorizer():
event = load_event("apiGatewayProxyV2Event.json")
parsed_event: APIGatewayProxyEventV2Model = handle_apigw_event(event, LambdaContext())
assert parsed_event.version == event["version"]
assert parsed_event.routeKey == event["routeKey"]
assert parsed_event.rawPath == event["rawPath"]
assert parsed_event.rawQueryString == event["rawQueryString"]
assert parsed_event.cookies == event["cookies"]
assert parsed_event.cookies[0] == "cookie1"
assert parsed_event.headers == event["headers"]
assert parsed_event.queryStringParameters == event["queryStringParameters"]
assert parsed_event.queryStringParameters["parameter2"] == "value"

request_context = parsed_event.requestContext
assert request_context.accountId == event["requestContext"]["accountId"]
assert request_context.apiId == event["requestContext"]["apiId"]
assert request_context.authorizer.jwt.claims == event["requestContext"]["authorizer"]["jwt"]["claims"]
assert request_context.authorizer.jwt.scopes == event["requestContext"]["authorizer"]["jwt"]["scopes"]
assert request_context.domainName == event["requestContext"]["domainName"]
assert request_context.domainPrefix == event["requestContext"]["domainPrefix"]

http = request_context.http
assert http.method == "POST"
assert http.path == "/my/path"
assert http.protocol == "HTTP/1.1"
assert str(http.sourceIp) == "192.168.0.1/32"
assert http.userAgent == "agent"

assert request_context.requestId == event["requestContext"]["requestId"]
assert request_context.routeKey == event["requestContext"]["routeKey"]
assert request_context.stage == event["requestContext"]["stage"]
assert request_context.time == event["requestContext"]["time"]
convert_time = int(round(request_context.timeEpoch.timestamp() * 1000))
assert convert_time == event["requestContext"]["timeEpoch"]
assert parsed_event.body == event["body"]
assert parsed_event.pathParameters == event["pathParameters"]
assert parsed_event.isBase64Encoded == event["isBase64Encoded"]
assert parsed_event.stageVariables == event["stageVariables"]


def test_api_gateway_proxy_v2_event_lambda_authorizer():
event = load_event("apiGatewayProxyV2LambdaAuthorizerEvent.json")
parsed_event: APIGatewayProxyEventV2Model = handle_apigw_event(event, LambdaContext())
request_context: RequestContextV2 = parsed_event.requestContext
assert request_context is not None
lambda_props: RequestContextV2Authorizer = request_context.authorizer.lambda_value
assert lambda_props is not None
assert lambda_props["key"] == "value"


def test_api_gateway_proxy_v2_event_iam_authorizer():
event = load_event("apiGatewayProxyV2IamEvent.json")
parsed_event: APIGatewayProxyEventV2Model = handle_apigw_event(event, LambdaContext())
iam = parsed_event.requestContext.authorizer.iam
assert iam is not None
assert iam.accessKey == "ARIA2ZJZYVUEREEIHAKY"
assert iam.accountId == "1234567890"
assert iam.callerId == "AROA7ZJZYVRE7C3DUXHH6:CognitoIdentityCredentials"
assert iam.cognitoIdentity.amr == ["foo"]
assert iam.cognitoIdentity.identityId == "us-east-1:3f291106-8703-466b-8f2b-3ecee1ca56ce"
assert iam.cognitoIdentity.identityPoolId == "us-east-1:4f291106-8703-466b-8f2b-3ecee1ca56ce"
assert iam.principalOrgId == "AwsOrgId"
assert iam.userArn == "arn:aws:iam::1234567890:user/Admin"
assert iam.userId == "AROA2ZJZYVRE7Y3TUXHH6"
2 changes: 1 addition & 1 deletion tests/functional/test_data_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ def test_api_gateway_proxy_v2_event():
assert http.method == "POST"
assert http.path == "/my/path"
assert http.protocol == "HTTP/1.1"
assert http.source_ip == "IP"
assert http.source_ip == "192.168.0.1/32"
assert http.user_agent == "agent"

assert request_context.request_id == event["requestContext"]["requestId"]
Expand Down

0 comments on commit 995c56e

Please sign in to comment.