-
Notifications
You must be signed in to change notification settings - Fork 421
feat(parser): Support for API GW v1 proxy schema & envelope #403
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
Changes from all commits
462a0be
efd429c
1eba236
a88fb89
ee58a5c
5c67f40
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 APIGatewayProxyEventModel | ||
from ..types import Model | ||
from .base import BaseEnvelope | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class ApiGatewayEnvelope(BaseEnvelope): | ||
"""API Gateway 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 {APIGatewayProxyEventModel}") | ||
parsed_envelope = APIGatewayProxyEventModel.parse_obj(data) | ||
logger.debug(f"Parsing event payload in `detail` with {model}") | ||
return self._parse(data=parsed_envelope.body, model=model) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
from datetime import datetime | ||
from typing import Any, Dict, List, Optional | ||
|
||
from pydantic import BaseModel, root_validator | ||
from pydantic.networks import IPvAnyNetwork | ||
|
||
from ..types import Literal | ||
|
||
|
||
class ApiGatewayUserCertValidity(BaseModel): | ||
notBefore: str | ||
notAfter: str | ||
|
||
|
||
class ApiGatewayUserCert(BaseModel): | ||
clientCertPem: str | ||
subjectDN: str | ||
issuerDN: str | ||
serialNumber: str | ||
validity: ApiGatewayUserCertValidity | ||
|
||
|
||
class APIGatewayEventIdentity(BaseModel): | ||
accessKey: Optional[str] | ||
accountId: Optional[str] | ||
apiKey: Optional[str] | ||
apiKeyId: Optional[str] | ||
caller: Optional[str] | ||
cognitoAuthenticationProvider: Optional[str] | ||
cognitoAuthenticationType: Optional[str] | ||
cognitoIdentityId: Optional[str] | ||
cognitoIdentityPoolId: Optional[str] | ||
principalOrgId: Optional[str] | ||
sourceIp: IPvAnyNetwork | ||
user: Optional[str] | ||
userAgent: Optional[str] | ||
userArn: Optional[str] | ||
clientCert: Optional[ApiGatewayUserCert] | ||
|
||
|
||
class APIGatewayEventAuthorizer(BaseModel): | ||
claims: Optional[Dict[str, Any]] | ||
scopes: Optional[List[str]] | ||
|
||
|
||
class APIGatewayEventRequestContext(BaseModel): | ||
accountId: str | ||
apiId: str | ||
authorizer: APIGatewayEventAuthorizer | ||
stage: str | ||
protocol: str | ||
identity: APIGatewayEventIdentity | ||
requestId: str | ||
requestTime: str | ||
requestTimeEpoch: datetime | ||
resourceId: Optional[str] | ||
resourcePath: str | ||
domainName: Optional[str] | ||
domainPrefix: Optional[str] | ||
extendedRequestId: Optional[str] | ||
httpMethod: Literal["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] | ||
path: str | ||
connectedAt: Optional[datetime] | ||
connectionId: Optional[str] | ||
eventType: Optional[Literal["CONNECT", "MESSAGE", "DISCONNECT"]] | ||
messageDirection: Optional[str] | ||
messageId: Optional[str] | ||
routeKey: Optional[str] | ||
operationName: Optional[str] | ||
|
||
|
||
class APIGatewayProxyEventModel(BaseModel): | ||
version: str | ||
resource: str | ||
path: str | ||
httpMethod: Literal["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] | ||
headers: Dict[str, str] | ||
multiValueHeaders: Dict[str, List[str]] | ||
queryStringParameters: Optional[Dict[str, str]] | ||
multiValueQueryStringParameters: Optional[Dict[str, List[str]]] | ||
requestContext: APIGatewayEventRequestContext | ||
pathParameters: Optional[Dict[str, str]] | ||
stageVariables: Optional[Dict[str, str]] | ||
isBase64Encoded: bool | ||
body: str | ||
|
||
@root_validator() | ||
def check_message_id(cls, values): | ||
message_id, event_type = values.get("messageId"), values.get("eventType") | ||
if message_id is not None and event_type != "MESSAGE": | ||
raise TypeError("messageId is available only when the `eventType` is `MESSAGE`") | ||
return values |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -161,6 +161,7 @@ Parser comes with the following built-in models: | |
| **KinesisDataStreamModel** | Lambda Event Source payload for Amazon Kinesis Data Streams | | ||
| **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 | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. API Gateway v1, v2, or both? e.g. REST vs HTTP API. By my first look I'd guess REST API There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. V1 |
||
|
||
### extending built-in models | ||
|
||
|
@@ -294,16 +295,16 @@ 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]` | | ||
|
||
| 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` | | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. API Gateway v1, v2, or both? e.g. REST vs HTTP API. By my first look I'd guess REST API There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. V1 |
||
### bringing your own envelope | ||
|
||
You can create your own Envelope model and logic by inheriting from `BaseEnvelope`, and implementing the `parse` method. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
from aws_lambda_powertools.utilities.parser import envelopes, event_parser | ||
from aws_lambda_powertools.utilities.parser.models import APIGatewayProxyEventModel | ||
from aws_lambda_powertools.utilities.typing import LambdaContext | ||
from tests.functional.parser.schemas import MyApiGatewayBusiness | ||
from tests.functional.parser.utils import load_event | ||
|
||
|
||
@event_parser(model=MyApiGatewayBusiness, envelope=envelopes.ApiGatewayEnvelope) | ||
def handle_apigw_with_envelope(event: MyApiGatewayBusiness, _: LambdaContext): | ||
assert event.message == "Hello" | ||
assert event.username == "Ran" | ||
|
||
|
||
@event_parser(model=APIGatewayProxyEventModel) | ||
def handle_apigw_event(event: APIGatewayProxyEventModel, _: LambdaContext): | ||
assert event.body == "Hello from Lambda!" | ||
return event | ||
|
||
|
||
def test_apigw_event_with_envelope(): | ||
event = load_event("apiGatewayProxyEvent.json") | ||
event["body"] = '{"message": "Hello", "username": "Ran"}' | ||
handle_apigw_with_envelope(event, LambdaContext()) | ||
|
||
|
||
def test_apigw_event(): | ||
event = load_event("apiGatewayProxyEvent.json") | ||
parsed_event: APIGatewayProxyEventModel = handle_apigw_event(event, LambdaContext()) | ||
assert parsed_event.version == event["version"] | ||
assert parsed_event.resource == event["resource"] | ||
assert parsed_event.path == event["path"] | ||
assert parsed_event.headers == event["headers"] | ||
assert parsed_event.multiValueHeaders == event["multiValueHeaders"] | ||
assert parsed_event.queryStringParameters == event["queryStringParameters"] | ||
assert parsed_event.multiValueQueryStringParameters == event["multiValueQueryStringParameters"] | ||
|
||
request_context = parsed_event.requestContext | ||
assert request_context.accountId == event["requestContext"]["accountId"] | ||
assert request_context.apiId == event["requestContext"]["apiId"] | ||
|
||
authorizer = request_context.authorizer | ||
assert authorizer.claims is None | ||
assert authorizer.scopes is None | ||
|
||
assert request_context.domainName == event["requestContext"]["domainName"] | ||
assert request_context.domainPrefix == event["requestContext"]["domainPrefix"] | ||
assert request_context.extendedRequestId == event["requestContext"]["extendedRequestId"] | ||
assert request_context.httpMethod == event["requestContext"]["httpMethod"] | ||
|
||
identity = request_context.identity | ||
assert identity.accessKey == event["requestContext"]["identity"]["accessKey"] | ||
assert identity.accountId == event["requestContext"]["identity"]["accountId"] | ||
assert identity.caller == event["requestContext"]["identity"]["caller"] | ||
assert ( | ||
identity.cognitoAuthenticationProvider == event["requestContext"]["identity"]["cognitoAuthenticationProvider"] | ||
) | ||
assert identity.cognitoAuthenticationType == event["requestContext"]["identity"]["cognitoAuthenticationType"] | ||
assert identity.cognitoIdentityId == event["requestContext"]["identity"]["cognitoIdentityId"] | ||
assert identity.cognitoIdentityPoolId == event["requestContext"]["identity"]["cognitoIdentityPoolId"] | ||
assert identity.principalOrgId == event["requestContext"]["identity"]["principalOrgId"] | ||
assert str(identity.sourceIp) == event["requestContext"]["identity"]["sourceIp"] | ||
assert identity.user == event["requestContext"]["identity"]["user"] | ||
assert identity.userAgent == event["requestContext"]["identity"]["userAgent"] | ||
assert identity.userArn == event["requestContext"]["identity"]["userArn"] | ||
assert identity.clientCert is not None | ||
assert identity.clientCert.clientCertPem == event["requestContext"]["identity"]["clientCert"]["clientCertPem"] | ||
assert identity.clientCert.subjectDN == event["requestContext"]["identity"]["clientCert"]["subjectDN"] | ||
assert identity.clientCert.issuerDN == event["requestContext"]["identity"]["clientCert"]["issuerDN"] | ||
assert identity.clientCert.serialNumber == event["requestContext"]["identity"]["clientCert"]["serialNumber"] | ||
assert ( | ||
identity.clientCert.validity.notBefore | ||
== event["requestContext"]["identity"]["clientCert"]["validity"]["notBefore"] | ||
) | ||
assert ( | ||
identity.clientCert.validity.notAfter | ||
== event["requestContext"]["identity"]["clientCert"]["validity"]["notAfter"] | ||
) | ||
|
||
assert request_context.path == event["requestContext"]["path"] | ||
assert request_context.protocol == event["requestContext"]["protocol"] | ||
assert request_context.requestId == event["requestContext"]["requestId"] | ||
assert request_context.requestTime == event["requestContext"]["requestTime"] | ||
convert_time = int(round(request_context.requestTimeEpoch.timestamp() * 1000)) | ||
assert convert_time == 1583349317135 | ||
assert request_context.resourceId == event["requestContext"]["resourceId"] | ||
assert request_context.resourcePath == event["requestContext"]["resourcePath"] | ||
assert request_context.stage == event["requestContext"]["stage"] | ||
|
||
assert parsed_event.pathParameters == event["pathParameters"] | ||
assert parsed_event.stageVariables == event["stageVariables"] | ||
assert parsed_event.body == event["body"] | ||
assert parsed_event.isBase64Encoded == event["isBase64Encoded"] | ||
|
||
assert request_context.connectedAt is None | ||
assert request_context.connectionId is None | ||
assert request_context.eventType is None | ||
assert request_context.messageDirection is None | ||
assert request_context.messageId is None | ||
assert request_context.routeKey is None | ||
assert request_context.operationName is None | ||
assert identity.apiKey is None | ||
assert identity.apiKeyId is None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you add a note why this check is needed? e.g. spoofing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what would you like to write here? basically what i saw that in the v1 version there's a duplication 3 fields, so i made sure that all values are actually the same. V2 removes this duplication