Skip to content

Commit 28afbe4

Browse files
authored
feat(parser): Add S3 Object Lambda Event (#362)
1 parent 3bd0e46 commit 28afbe4

File tree

4 files changed

+185
-22
lines changed

4 files changed

+185
-22
lines changed

aws_lambda_powertools/utilities/parser/models/__init__.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,29 @@
44
from .event_bridge import EventBridgeModel
55
from .kinesis import KinesisDataStreamModel, KinesisDataStreamRecord, KinesisDataStreamRecordPayload
66
from .s3 import S3Model, S3RecordModel
7-
from .ses import SesModel, SesRecordModel
7+
from .s3_object_event import (
8+
S3ObjectConfiguration,
9+
S3ObjectContext,
10+
S3ObjectLambdaEvent,
11+
S3ObjectSessionAttributes,
12+
S3ObjectSessionContext,
13+
S3ObjectSessionIssuer,
14+
S3ObjectUserIdentity,
15+
S3ObjectUserRequest,
16+
)
17+
from .ses import (
18+
SesMail,
19+
SesMailCommonHeaders,
20+
SesMailHeaders,
21+
SesMessage,
22+
SesModel,
23+
SesReceipt,
24+
SesReceiptAction,
25+
SesReceiptVerdict,
26+
SesRecordModel,
27+
)
828
from .sns import SnsModel, SnsNotificationModel, SnsRecordModel
9-
from .sqs import SqsModel, SqsRecordModel
29+
from .sqs import SqsAttributesModel, SqsModel, SqsMsgAttributeModel, SqsRecordModel
1030

1131
__all__ = [
1232
"CloudWatchLogsData",
@@ -20,16 +40,34 @@
2040
"EventBridgeModel",
2141
"DynamoDBStreamChangedRecordModel",
2242
"DynamoDBStreamRecordModel",
43+
"DynamoDBStreamChangedRecordModel",
2344
"KinesisDataStreamModel",
2445
"KinesisDataStreamRecord",
2546
"KinesisDataStreamRecordPayload",
2647
"S3Model",
2748
"S3RecordModel",
49+
"S3ObjectLambdaEvent",
50+
"S3ObjectUserIdentity",
51+
"S3ObjectSessionContext",
52+
"S3ObjectSessionAttributes",
53+
"S3ObjectSessionIssuer",
54+
"S3ObjectUserRequest",
55+
"S3ObjectConfiguration",
56+
"S3ObjectContext",
2857
"SesModel",
2958
"SesRecordModel",
59+
"SesMessage",
60+
"SesMail",
61+
"SesMailCommonHeaders",
62+
"SesMailHeaders",
63+
"SesReceipt",
64+
"SesReceiptAction",
65+
"SesReceiptVerdict",
3066
"SnsModel",
3167
"SnsNotificationModel",
3268
"SnsRecordModel",
3369
"SqsModel",
3470
"SqsRecordModel",
71+
"SqsMsgAttributeModel",
72+
"SqsAttributesModel",
3573
]
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from typing import Dict, Optional
2+
3+
from pydantic import BaseModel, HttpUrl
4+
5+
6+
class S3ObjectContext(BaseModel):
7+
inputS3Url: HttpUrl
8+
outputRoute: str
9+
outputToken: str
10+
11+
12+
class S3ObjectConfiguration(BaseModel):
13+
accessPointArn: str
14+
supportingAccessPointArn: str
15+
payload: str
16+
17+
18+
class S3ObjectUserRequest(BaseModel):
19+
url: str
20+
headers: Dict[str, str]
21+
22+
23+
class S3ObjectSessionIssuer(BaseModel):
24+
type: str # noqa: A003, VNE003
25+
userName: Optional[str]
26+
principalId: str
27+
arn: str
28+
accountId: str
29+
30+
31+
class S3ObjectSessionAttributes(BaseModel):
32+
creationDate: str
33+
mfaAuthenticated: bool
34+
35+
36+
class S3ObjectSessionContext(BaseModel):
37+
sessionIssuer: S3ObjectSessionIssuer
38+
attributes: S3ObjectSessionAttributes
39+
40+
41+
class S3ObjectUserIdentity(BaseModel):
42+
type: str # noqa003
43+
accountId: str
44+
accessKeyId: str
45+
userName: Optional[str]
46+
principalId: str
47+
arn: str
48+
sessionContext: Optional[S3ObjectSessionContext]
49+
50+
51+
class S3ObjectLambdaEvent(BaseModel):
52+
xAmzRequestId: str
53+
getObjectContext: S3ObjectContext
54+
configuration: S3ObjectConfiguration
55+
userRequest: S3ObjectUserRequest
56+
userIdentity: S3ObjectUserIdentity
57+
protocolVersion: str

docs/utilities/parser.md

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -149,17 +149,18 @@ Use this standalone function when you want more control over the data validation
149149

150150
Parser comes with the following built-in models:
151151

152-
Model name | Description
153-
------------------------------------------------- | ----------------------------------------------------------------------------------------------------------
154-
**DynamoDBStreamModel** | Lambda Event Source payload for Amazon DynamoDB Streams
155-
**EventBridgeModel** | Lambda Event Source payload for Amazon EventBridge
156-
**SqsModel** | Lambda Event Source payload for Amazon SQS
157-
**AlbModel** | Lambda Event Source payload for Amazon Application Load Balancer
158-
**CloudwatchLogsModel** | Lambda Event Source payload for Amazon CloudWatch Logs
159-
**S3Model** | Lambda Event Source payload for Amazon S3
160-
**KinesisDataStreamModel** | Lambda Event Source payload for Amazon Kinesis Data Streams
161-
**SesModel** | Lambda Event Source payload for Amazon Simple Email Service
162-
**SnsModel** | Lambda Event Source payload for Amazon Simple Notification Service
152+
| Model name | Description |
153+
| -------------------------- | ------------------------------------------------------------------ |
154+
| **DynamoDBStreamModel** | Lambda Event Source payload for Amazon DynamoDB Streams |
155+
| **EventBridgeModel** | Lambda Event Source payload for Amazon EventBridge |
156+
| **SqsModel** | Lambda Event Source payload for Amazon SQS |
157+
| **AlbModel** | Lambda Event Source payload for Amazon Application Load Balancer |
158+
| **CloudwatchLogsModel** | Lambda Event Source payload for Amazon CloudWatch Logs |
159+
| **S3Model** | Lambda Event Source payload for Amazon S3 |
160+
| **S3ObjectLambdaEvent** | Lambda Event Source payload for Amazon S3 Object Lambda |
161+
| **KinesisDataStreamModel** | Lambda Event Source payload for Amazon Kinesis Data Streams |
162+
| **SesModel** | Lambda Event Source payload for Amazon Simple Email Service |
163+
| **SnsModel** | Lambda Event Source payload for Amazon Simple Notification Service |
163164

164165
### extending built-in models
165166

@@ -293,15 +294,15 @@ Here's an example of parsing a model found in an event coming from EventBridge,
293294

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

296-
Envelope name | Behaviour | Return
297-
------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------
298-
**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]]]`
299-
**EventBridgeEnvelope** | 1. Parses data using `EventBridgeModel`. <br/> 2. Parses `detail` key using your model and returns it. | `Model`
300-
**SqsEnvelope** | 1. Parses data using `SqsModel`. <br/> 2. Parses records in `body` key using your model and return them in a list. | `List[Model]`
301-
**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]`
302-
**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]`
303-
**SnsEnvelope** | 1. Parses data using `SnsModel`. <br/> 2. Parses records in `body` key using your model and return them in a list. | `List[Model]`
304-
**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]`
297+
| Envelope name | Behaviour | Return |
298+
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
299+
| **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]]]` |
300+
| **EventBridgeEnvelope** | 1. Parses data using `EventBridgeModel`. <br/> 2. Parses `detail` key using your model and returns it. | `Model` |
301+
| **SqsEnvelope** | 1. Parses data using `SqsModel`. <br/> 2. Parses records in `body` key using your model and return them in a list. | `List[Model]` |
302+
| **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]` |
303+
| **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]` |
304+
| **SnsEnvelope** | 1. Parses data using `SnsModel`. <br/> 2. Parses records in `body` key using your model and return them in a list. | `List[Model]` |
305+
| **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]` |
305306

306307
### bringing your own envelope
307308

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from aws_lambda_powertools.utilities.parser import event_parser
2+
from aws_lambda_powertools.utilities.parser.models import S3ObjectLambdaEvent
3+
from aws_lambda_powertools.utilities.typing import LambdaContext
4+
from tests.functional.parser.utils import load_event
5+
6+
7+
@event_parser(model=S3ObjectLambdaEvent)
8+
def handle_s3_object_event_iam(event: S3ObjectLambdaEvent, _: LambdaContext):
9+
return event
10+
11+
12+
def test_s3_object_event():
13+
event = load_event("s3ObjectEventIAMUser.json")
14+
parsed_event: S3ObjectLambdaEvent = handle_s3_object_event_iam(event, LambdaContext())
15+
assert parsed_event.xAmzRequestId == event["xAmzRequestId"]
16+
assert parsed_event.getObjectContext is not None
17+
object_context = parsed_event.getObjectContext
18+
assert str(object_context.inputS3Url) == event["getObjectContext"]["inputS3Url"]
19+
assert object_context.outputRoute == event["getObjectContext"]["outputRoute"]
20+
assert object_context.outputToken == event["getObjectContext"]["outputToken"]
21+
assert parsed_event.configuration is not None
22+
configuration = parsed_event.configuration
23+
assert configuration.accessPointArn == event["configuration"]["accessPointArn"]
24+
assert configuration.supportingAccessPointArn == event["configuration"]["supportingAccessPointArn"]
25+
assert configuration.payload == event["configuration"]["payload"]
26+
assert parsed_event.userRequest is not None
27+
user_request = parsed_event.userRequest
28+
assert user_request.url == event["userRequest"]["url"]
29+
assert user_request.headers == event["userRequest"]["headers"]
30+
assert user_request.headers["Accept-Encoding"] == "identity"
31+
assert parsed_event.userIdentity is not None
32+
user_identity = parsed_event.userIdentity
33+
assert user_identity.type == event["userIdentity"]["type"]
34+
assert user_identity.principalId == event["userIdentity"]["principalId"]
35+
assert user_identity.arn == event["userIdentity"]["arn"]
36+
assert user_identity.accountId == event["userIdentity"]["accountId"]
37+
assert user_identity.accessKeyId == event["userIdentity"]["accessKeyId"]
38+
assert user_identity.userName == event["userIdentity"]["userName"]
39+
assert user_identity.sessionContext is None
40+
assert parsed_event.protocolVersion == event["protocolVersion"]
41+
42+
43+
@event_parser(model=S3ObjectLambdaEvent)
44+
def handle_s3_object_event_temp_creds(event: S3ObjectLambdaEvent, _: LambdaContext):
45+
return event
46+
47+
48+
def test_s3_object_event_temp_credentials():
49+
event = load_event("s3ObjectEventTempCredentials.json")
50+
parsed_event: S3ObjectLambdaEvent = handle_s3_object_event_temp_creds(event, LambdaContext())
51+
assert parsed_event.xAmzRequestId == event["xAmzRequestId"]
52+
session_context = parsed_event.userIdentity.sessionContext
53+
assert session_context is not None
54+
session_issuer = session_context.sessionIssuer
55+
assert session_issuer is not None
56+
assert session_issuer.type == event["userIdentity"]["sessionContext"]["sessionIssuer"]["type"]
57+
assert session_issuer.userName == event["userIdentity"]["sessionContext"]["sessionIssuer"]["userName"]
58+
assert session_issuer.principalId == event["userIdentity"]["sessionContext"]["sessionIssuer"]["principalId"]
59+
assert session_issuer.arn == event["userIdentity"]["sessionContext"]["sessionIssuer"]["arn"]
60+
assert session_issuer.accountId == event["userIdentity"]["sessionContext"]["sessionIssuer"]["accountId"]
61+
session_attributes = session_context.attributes
62+
assert session_attributes is not None
63+
assert (
64+
str(session_attributes.mfaAuthenticated).lower()
65+
== event["userIdentity"]["sessionContext"]["attributes"]["mfaAuthenticated"]
66+
)
67+
assert session_attributes.creationDate == event["userIdentity"]["sessionContext"]["attributes"]["creationDate"]

0 commit comments

Comments
 (0)