Skip to content

Commit 4179ac1

Browse files
author
Michael Brewer
authored
feat(data-classes): Add S3 Object Lambda Event (#353)
1 parent 961b25e commit 4179ac1

File tree

6 files changed

+498
-0
lines changed

6 files changed

+498
-0
lines changed

aws_lambda_powertools/logging/correlation_paths.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
APPSYNC_RESOLVER = 'request.headers."x-amzn-trace-id"'
66
APPLICATION_LOAD_BALANCER = 'headers."x-amzn-trace-id"'
77
EVENT_BRIDGE = "id"
8+
S3_OBJECT_LAMBDA = "xAmzRequestId"
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
from typing import Dict, Optional
2+
3+
from aws_lambda_powertools.utilities.data_classes.common import DictWrapper, get_header_value
4+
5+
6+
class S3ObjectContext(DictWrapper):
7+
"""The input and output details for connections to Amazon S3 and S3 Object Lambda."""
8+
9+
@property
10+
def input_s3_url(self) -> str:
11+
"""A pre-signed URL that can be used to fetch the original object from Amazon S3.
12+
13+
The URL is signed using the original caller’s identity, and their permissions
14+
will apply when the URL is used. If there are signed headers in the URL, the
15+
Lambda function must include these in the call to Amazon S3, except for the Host."""
16+
return self["inputS3Url"]
17+
18+
@property
19+
def output_route(self) -> str:
20+
"""A routing token that is added to the S3 Object Lambda URL when the Lambda function
21+
calls `WriteGetObjectResponse`."""
22+
return self["outputRoute"]
23+
24+
@property
25+
def output_token(self) -> str:
26+
"""An opaque token used by S3 Object Lambda to match the WriteGetObjectResponse call
27+
with the original caller."""
28+
return self["outputToken"]
29+
30+
31+
class S3ObjectConfiguration(DictWrapper):
32+
"""Configuration information about the S3 Object Lambda access point."""
33+
34+
@property
35+
def access_point_arn(self) -> str:
36+
"""The Amazon Resource Name (ARN) of the S3 Object Lambda access point that received
37+
this request."""
38+
return self["accessPointArn"]
39+
40+
@property
41+
def supporting_access_point_arn(self) -> str:
42+
"""The ARN of the supporting access point that is specified in the S3 Object Lambda
43+
access point configuration."""
44+
return self["supportingAccessPointArn"]
45+
46+
@property
47+
def payload(self) -> str:
48+
"""Custom data that is applied to the S3 Object Lambda access point configuration.
49+
50+
S3 Object Lambda treats this as an opaque string, so it might need to be decoded
51+
before use."""
52+
return self["payload"]
53+
54+
55+
class S3ObjectUserRequest(DictWrapper):
56+
""" Information about the original call to S3 Object Lambda."""
57+
58+
@property
59+
def url(self) -> str:
60+
"""The decoded URL of the request as received by S3 Object Lambda, excluding any
61+
authorization-related query parameters."""
62+
return self["url"]
63+
64+
@property
65+
def headers(self) -> Dict[str, str]:
66+
"""A map of string to strings containing the HTTP headers and their values from the original call,
67+
excluding any authorization-related headers.
68+
69+
If the same header appears multiple times, their values are combined into a comma-delimited list.
70+
The case of the original headers is retained in this map."""
71+
return self["headers"]
72+
73+
def get_header_value(
74+
self, name: str, default_value: Optional[str] = None, case_sensitive: Optional[bool] = False
75+
) -> Optional[str]:
76+
"""Get header value by name
77+
78+
Parameters
79+
----------
80+
name: str
81+
Header name
82+
default_value: str, optional
83+
Default value if no value was found by name
84+
case_sensitive: bool
85+
Whether to use a case sensitive look up
86+
Returns
87+
-------
88+
str, optional
89+
Header value
90+
"""
91+
return get_header_value(self.headers, name, default_value, case_sensitive)
92+
93+
94+
class S3ObjectSessionIssuer(DictWrapper):
95+
@property
96+
def get_type(self) -> str:
97+
"""The source of the temporary security credentials, such as Root, IAMUser, or Role."""
98+
return self["type"]
99+
100+
@property
101+
def user_name(self) -> str:
102+
"""The friendly name of the user or role that issued the session."""
103+
return self["userName"]
104+
105+
@property
106+
def principal_id(self) -> str:
107+
"""The internal ID of the entity that was used to get credentials."""
108+
return self["principalId"]
109+
110+
@property
111+
def arn(self) -> str:
112+
"""The ARN of the source (account, IAM user, or role) that was used to get temporary security credentials."""
113+
return self["arn"]
114+
115+
@property
116+
def account_id(self) -> str:
117+
"""The account that owns the entity that was used to get credentials."""
118+
return self["accountId"]
119+
120+
121+
class S3ObjectSessionAttributes(DictWrapper):
122+
@property
123+
def creation_date(self) -> str:
124+
"""The date and time when the temporary security credentials were issued.
125+
Represented in ISO 8601 basic notation."""
126+
return self["creationDate"]
127+
128+
@property
129+
def mfa_authenticated(self) -> str:
130+
"""The value is true if the root user or IAM user whose credentials were used for the request also was
131+
authenticated with an MFA device; otherwise, false.."""
132+
return self["mfaAuthenticated"]
133+
134+
135+
class S3ObjectSessionContext(DictWrapper):
136+
@property
137+
def session_issuer(self) -> S3ObjectSessionIssuer:
138+
"""If the request was made with temporary security credentials, an element that provides information
139+
about how the credentials were obtained."""
140+
return S3ObjectSessionIssuer(self["sessionIssuer"])
141+
142+
@property
143+
def attributes(self) -> S3ObjectSessionAttributes:
144+
"""Session attributes."""
145+
return S3ObjectSessionAttributes(self["attributes"])
146+
147+
148+
class S3ObjectUserIdentity(DictWrapper):
149+
"""Details about the identity that made the call to S3 Object Lambda.
150+
151+
Documentation:
152+
-------------
153+
- https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference-user-identity.html
154+
"""
155+
156+
@property
157+
def get_type(self) -> str:
158+
"""The type of identity.
159+
160+
The following values are possible:
161+
162+
- Root – The request was made with your AWS account credentials. If the userIdentity
163+
type is Root and you set an alias for your account, the userName field contains your account alias.
164+
For more information, see Your AWS Account ID and Its Alias.
165+
- IAMUser – The request was made with the credentials of an IAM user.
166+
- AssumedRole – The request was made with temporary security credentials that were obtained
167+
with a role via a call to the AWS Security Token Service (AWS STS) AssumeRole API. This can include
168+
roles for Amazon EC2 and cross-account API access.
169+
- FederatedUser – The request was made with temporary security credentials that were obtained via a
170+
call to the AWS STS GetFederationToken API. The sessionIssuer element indicates if the API was
171+
called with root or IAM user credentials.
172+
- AWSAccount – The request was made by another AWS account.
173+
- AWSService – The request was made by an AWS account that belongs to an AWS service.
174+
For example, AWS Elastic Beanstalk assumes an IAM role in your account to call other AWS services
175+
on your behalf.
176+
"""
177+
return self["type"]
178+
179+
@property
180+
def account_id(self) -> str:
181+
"""The account that owns the entity that granted permissions for the request.
182+
183+
If the request was made with temporary security credentials, this is the account that owns the IAM
184+
user or role that was used to obtain credentials."""
185+
return self["accountId"]
186+
187+
@property
188+
def access_key_id(self) -> str:
189+
"""The access key ID that was used to sign the request.
190+
191+
If the request was made with temporary security credentials, this is the access key ID of
192+
the temporary credentials. For security reasons, accessKeyId might not be present, or might
193+
be displayed as an empty string."""
194+
return self["accessKeyId"]
195+
196+
@property
197+
def user_name(self) -> str:
198+
"""The friendly name of the identity that made the call."""
199+
return self["userName"]
200+
201+
@property
202+
def principal_id(self) -> str:
203+
"""The unique identifier for the identity that made the call.
204+
205+
For requests made with temporary security credentials, this value includes
206+
the session name that is passed to the AssumeRole, AssumeRoleWithWebIdentity,
207+
or GetFederationToken API call."""
208+
return self["principalId"]
209+
210+
@property
211+
def arn(self) -> str:
212+
"""The ARN of the principal that made the call.
213+
The last section of the ARN contains the user or role that made the call."""
214+
return self["arn"]
215+
216+
@property
217+
def session_context(self) -> Optional[S3ObjectSessionContext]:
218+
"""If the request was made with temporary security credentials,
219+
this element provides information about the session that was created for those credentials."""
220+
session_context = self.get("sessionContext")
221+
222+
if session_context is None:
223+
return None
224+
225+
return S3ObjectSessionContext(session_context)
226+
227+
228+
class S3ObjectLambdaEvent(DictWrapper):
229+
"""S3 object lambda event
230+
231+
Documentation:
232+
-------------
233+
- https://docs.aws.amazon.com/AmazonS3/latest/userguide/olap-writing-lambda.html
234+
235+
Example
236+
-------
237+
**Fetch and transform original object from Amazon S3**
238+
239+
import boto3
240+
import requests
241+
from aws_lambda_powertools.utilities.data_classes.s3_object_event import S3ObjectLambdaEvent
242+
243+
session = boto3.Session()
244+
s3 = session.client("s3")
245+
246+
def lambda_handler(event, context):
247+
event = S3ObjectLambdaEvent(event)
248+
249+
# Get object from S3
250+
response = requests.get(event.input_s3_url)
251+
original_object = response.content.decode("utf-8")
252+
253+
# Make changes to the object about to be returned
254+
transformed_object = original_object.upper()
255+
256+
# Write object back to S3 Object Lambda
257+
s3.write_get_object_response(
258+
Body=transformed_object, RequestRoute=event.request_route, RequestToken=event.request_token
259+
)
260+
"""
261+
262+
@property
263+
def request_id(self) -> str:
264+
"""The Amazon S3 request ID for this request. We recommend that you log this value to help with debugging."""
265+
return self["xAmzRequestId"]
266+
267+
@property
268+
def object_context(self) -> S3ObjectContext:
269+
"""The input and output details for connections to Amazon S3 and S3 Object Lambda."""
270+
return S3ObjectContext(self["getObjectContext"])
271+
272+
@property
273+
def configuration(self) -> S3ObjectConfiguration:
274+
"""Configuration information about the S3 Object Lambda access point."""
275+
return S3ObjectConfiguration(self["configuration"])
276+
277+
@property
278+
def user_request(self) -> S3ObjectUserRequest:
279+
"""Information about the original call to S3 Object Lambda."""
280+
return S3ObjectUserRequest(self["userRequest"])
281+
282+
@property
283+
def user_identity(self) -> S3ObjectUserIdentity:
284+
"""Details about the identity that made the call to S3 Object Lambda."""
285+
return S3ObjectUserIdentity(self["userIdentity"])
286+
287+
@property
288+
def request_route(self) -> str:
289+
"""A routing token that is added to the S3 Object Lambda URL when the Lambda function
290+
calls `WriteGetObjectResponse`."""
291+
return self.object_context.output_route
292+
293+
@property
294+
def request_token(self) -> str:
295+
"""An opaque token used by S3 Object Lambda to match the WriteGetObjectResponse call
296+
with the original caller."""
297+
return self.object_context.output_token
298+
299+
@property
300+
def input_s3_url(self) -> str:
301+
"""A pre-signed URL that can be used to fetch the original object from Amazon S3.
302+
303+
The URL is signed using the original caller’s identity, and their permissions
304+
will apply when the URL is used. If there are signed headers in the URL, the
305+
Lambda function must include these in the call to Amazon S3, except for the Host.
306+
307+
Example
308+
-------
309+
**Fetch original object from Amazon S3**
310+
311+
import requests
312+
from aws_lambda_powertools.utilities.data_classes.s3_object_event import S3ObjectLambdaEvent
313+
314+
def lambda_handler(event, context):
315+
event = S3ObjectLambdaEvent(event)
316+
317+
response = requests.get(event.input_s3_url)
318+
original_object = response.content.decode("utf-8")
319+
...
320+
"""
321+
return self.object_context.input_s3_url
322+
323+
@property
324+
def protocol_version(self) -> str:
325+
"""The version ID of the context provided.
326+
327+
The format of this field is `{Major Version}`.`{Minor Version}`.
328+
The minor version numbers are always two-digit numbers. Any removal or change to the semantics of a
329+
field will necessitate a major version bump and will require active opt-in. Amazon S3 can add new
330+
fields at any time, at which point you might experience a minor version bump. Due to the nature of
331+
software rollouts, it is possible that you might see multiple minor versions in use at once.
332+
"""
333+
return self["protocolVersion"]

docs/utilities/data_classes.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ Event Source | Data_class
5959
[EventBridge](#eventbridge) | `EventBridgeEvent`
6060
[Kinesis Data Stream](#kinesis-streams) | `KinesisStreamEvent`
6161
[S3](#s3) | `S3Event`
62+
[S3 Object Lambda](#s3-object-lambda) | `S3ObjectLambdaEvent`
6263
[SES](#ses) | `SESEvent`
6364
[SNS](#sns) | `SNSEvent`
6465
[SQS](#sqs) | `SQSEvent`
@@ -547,6 +548,43 @@ or plain text, depending on the original payload.
547548
do_something_with(f'{bucket_name}/{object_key}')
548549
```
549550

551+
### S3 Object Lambda
552+
553+
This example is based on the AWS Blog post [Introducing Amazon S3 Object Lambda – Use Your Code to Process Data as It Is Being Retrieved from S3](https://aws.amazon.com/blogs/aws/introducing-amazon-s3-object-lambda-use-your-code-to-process-data-as-it-is-being-retrieved-from-s3/){target="_blank"}.
554+
555+
=== "app.py"
556+
557+
```python hl_lines="4-5 10 12"
558+
import boto3
559+
import requests
560+
561+
from aws_lambda_powertools import Logger
562+
from aws_lambda_powertools.logging.correlation_paths import S3_OBJECT_LAMBDA
563+
from aws_lambda_powertools.utilities.data_classes.s3_object_event import S3ObjectLambdaEvent
564+
565+
logger = Logger()
566+
session = boto3.Session()
567+
s3 = session.client("s3")
568+
569+
@logger.inject_lambda_context(correlation_id_path=S3_OBJECT_LAMBDA, log_event=True)
570+
def lambda_handler(event, context):
571+
event = S3ObjectLambdaEvent(event)
572+
573+
# Get object from S3
574+
response = requests.get(event.input_s3_url)
575+
original_object = response.content.decode("utf-8")
576+
577+
# Make changes to the object about to be returned
578+
transformed_object = original_object.upper()
579+
580+
# Write object back to S3 Object Lambda
581+
s3.write_get_object_response(
582+
Body=transformed_object, RequestRoute=event.request_route, RequestToken=event.request_token
583+
)
584+
585+
return {"status_code": 200}
586+
```
587+
550588
### SES
551589

552590
=== "app.py"

0 commit comments

Comments
 (0)