Skip to content
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

feat(parser): add support for API Gateway HTTP API #434 #441

Merged
merged 3 commits into from
May 28, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
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
22 changes: 11 additions & 11 deletions docs/utilities/parser.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,17 +295,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