Skip to content

Commit 60d188e

Browse files
authored
Merge branch 'awslabs:develop' into user-agent
2 parents c53e384 + b3679c3 commit 60d188e

File tree

19 files changed

+456
-31
lines changed

19 files changed

+456
-31
lines changed

.github/workflows/on_push_docs.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ on:
1010
- "examples/**"
1111
- "CHANGELOG.md"
1212

13+
permissions:
14+
id-token: write
15+
1316
jobs:
1417
release-docs:
1518
permissions:
1619
contents: write
1720
pages: write
21+
id-token: write
1822
uses: ./.github/workflows/reusable_publish_docs.yml
1923
with:
2024
version: develop

.github/workflows/publish_v2_layer.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
permissions:
2929
# lower privilege propagated from parent workflow (release.yml)
3030
contents: read
31-
id-token: none
31+
id-token: write
3232
pages: none
3333
pull-requests: none
3434
runs-on: aws-lambda-powertools_ubuntu-latest_8-core
@@ -223,7 +223,7 @@ jobs:
223223
contents: write
224224
pages: write
225225
pull-requests: none
226-
id-token: none
226+
id-token: write
227227
uses: ./.github/workflows/reusable_publish_docs.yml
228228
with:
229229
version: ${{ inputs.latest_published_version }}

.github/workflows/rebuild_latest_docs.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,15 @@ on:
1414
default: "2.0.0"
1515
required: true
1616

17+
permissions:
18+
id-token: write
19+
1720
jobs:
1821
release-docs:
1922
permissions:
2023
contents: write
2124
pages: write
25+
id-token: write
2226
uses: ./.github/workflows/reusable_publish_docs.yml
2327
with:
2428
version: ${{ inputs.latest_published_version }}

.github/workflows/reusable_publish_docs.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ on:
2626
default: develop
2727

2828
permissions:
29+
id-token: write
2930
contents: write
3031
pages: write
3132

@@ -36,6 +37,7 @@ jobs:
3637
concurrency:
3738
group: on-docs-rebuild
3839
runs-on: ubuntu-latest
40+
environment: Docs
3941
steps:
4042
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
4143
with:
@@ -88,3 +90,27 @@ jobs:
8890
publish_dir: ./api
8991
keep_files: true
9092
destination_dir: latest/api
93+
- name: Configure AWS credentials
94+
uses: aws-actions/configure-aws-credentials@e1e17a757e536f70e52b5a12b2e8d1d1c60e04ef
95+
with:
96+
aws-region: us-east-1
97+
role-to-assume: ${{ secrets.AWS_DOCS_ROLE_ARN }}
98+
- name: Copy API Docs
99+
run: |
100+
cp -r api site/
101+
- name: Deploy Docs (Version)
102+
env:
103+
VERSION: ${{ inputs.version }}
104+
ALIAS: ${{ inputs.alias }}
105+
run: |
106+
aws s3 sync \
107+
site/ \
108+
s3://${{ secrets.AWS_DOCS_BUCKET }}/lambda-python/${{ env.VERSION }}/
109+
- name: Deploy Docs (Alias)
110+
env:
111+
VERSION: ${{ inputs.version }}
112+
ALIAS: ${{ inputs.alias }}
113+
run: |
114+
aws s3 sync \
115+
site/ \
116+
s3://${{ secrets.AWS_DOCS_BUCKET }}/lambda-python/${{ env.ALIAS }}/

aws_lambda_powertools/event_handler/api_gateway.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def with_cors():
8484
8585
cors_config = CORSConfig(
8686
allow_origin="https://wwww.example.com/",
87+
extra_origins=["https://dev.example.com/"],
8788
expose_headers=["x-exposed-response-header"],
8889
allow_headers=["x-custom-request-header"],
8990
max_age=100,
@@ -106,6 +107,7 @@ def without_cors():
106107
def __init__(
107108
self,
108109
allow_origin: str = "*",
110+
extra_origins: Optional[List[str]] = None,
109111
allow_headers: Optional[List[str]] = None,
110112
expose_headers: Optional[List[str]] = None,
111113
max_age: Optional[int] = None,
@@ -117,6 +119,8 @@ def __init__(
117119
allow_origin: str
118120
The value of the `Access-Control-Allow-Origin` to send in the response. Defaults to "*", but should
119121
only be used during development.
122+
extra_origins: Optional[List[str]]
123+
The list of additional allowed origins.
120124
allow_headers: Optional[List[str]]
121125
The list of additional allowed headers. This list is added to list of
122126
built-in allowed headers: `Authorization`, `Content-Type`, `X-Amz-Date`,
@@ -128,16 +132,29 @@ def __init__(
128132
allow_credentials: bool
129133
A boolean value that sets the value of `Access-Control-Allow-Credentials`
130134
"""
131-
self.allow_origin = allow_origin
135+
self._allowed_origins = [allow_origin]
136+
if extra_origins:
137+
self._allowed_origins.extend(extra_origins)
132138
self.allow_headers = set(self._REQUIRED_HEADERS + (allow_headers or []))
133139
self.expose_headers = expose_headers or []
134140
self.max_age = max_age
135141
self.allow_credentials = allow_credentials
136142

137-
def to_dict(self) -> Dict[str, str]:
143+
def to_dict(self, origin: Optional[str]) -> Dict[str, str]:
138144
"""Builds the configured Access-Control http headers"""
145+
146+
# If there's no Origin, don't add any CORS headers
147+
if not origin:
148+
return {}
149+
150+
# If the origin doesn't match any of the allowed origins, and we don't allow all origins ("*"),
151+
# don't add any CORS headers
152+
if origin not in self._allowed_origins and "*" not in self._allowed_origins:
153+
return {}
154+
155+
# The origin matched an allowed origin, so return the CORS headers
139156
headers: Dict[str, str] = {
140-
"Access-Control-Allow-Origin": self.allow_origin,
157+
"Access-Control-Allow-Origin": origin,
141158
"Access-Control-Allow-Headers": ",".join(sorted(self.allow_headers)),
142159
}
143160

@@ -207,9 +224,9 @@ def __init__(self, response: Response, route: Optional[Route] = None):
207224
self.response = response
208225
self.route = route
209226

210-
def _add_cors(self, cors: CORSConfig):
227+
def _add_cors(self, event: BaseProxyEvent, cors: CORSConfig):
211228
"""Update headers to include the configured Access-Control headers"""
212-
self.response.headers.update(cors.to_dict())
229+
self.response.headers.update(cors.to_dict(event.get_header_value("Origin")))
213230

214231
def _add_cache_control(self, cache_control: str):
215232
"""Set the specified cache control headers for 200 http responses. For non-200 `no-cache` is used."""
@@ -230,7 +247,7 @@ def _route(self, event: BaseProxyEvent, cors: Optional[CORSConfig]):
230247
if self.route is None:
231248
return
232249
if self.route.cors:
233-
self._add_cors(cors or CORSConfig())
250+
self._add_cors(event, cors or CORSConfig())
234251
if self.route.cache_control:
235252
self._add_cache_control(self.route.cache_control)
236253
if self.route.compress and "gzip" in (event.get_header_value("accept-encoding", "") or ""):
@@ -644,7 +661,7 @@ def _not_found(self, method: str) -> ResponseBuilder:
644661
headers: Dict[str, Union[str, List[str]]] = {}
645662
if self._cors:
646663
logger.debug("CORS is enabled, updating headers.")
647-
headers.update(self._cors.to_dict())
664+
headers.update(self._cors.to_dict(self.current_event.get_header_value("Origin")))
648665

649666
if method == "OPTIONS":
650667
logger.debug("Pre-flight request detected. Returning CORS with null response")

aws_lambda_powertools/utilities/data_classes/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def get_header_value(
113113
class BaseProxyEvent(DictWrapper):
114114
@property
115115
def headers(self) -> Dict[str, str]:
116-
return self["headers"]
116+
return self.get("headers") or {}
117117

118118
@property
119119
def query_string_parameters(self) -> Optional[Dict[str, str]]:

docs/core/event_handler/api_gateway.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,8 @@ To address this API Gateway behavior, we use `strip_prefixes` parameter to accou
280280

281281
You can configure CORS at the `APIGatewayRestResolver` constructor via `cors` parameter using the `CORSConfig` class.
282282

283-
This will ensure that CORS headers are always returned as part of the response when your functions match the path invoked.
283+
This will ensure that CORS headers are returned as part of the response when your functions match the path invoked and the `Origin`
284+
matches one of the allowed values.
284285

285286
???+ tip
286287
Optionally disable CORS on a per path basis with `cors=False` parameter.
@@ -297,6 +298,18 @@ This will ensure that CORS headers are always returned as part of the response w
297298
--8<-- "examples/event_handler_rest/src/setting_cors_output.json"
298299
```
299300

301+
=== "setting_cors_extra_origins.py"
302+
303+
```python hl_lines="5 11-12 34"
304+
--8<-- "examples/event_handler_rest/src/setting_cors_extra_origins.py"
305+
```
306+
307+
=== "setting_cors_extra_origins_output.json"
308+
309+
```json
310+
--8<-- "examples/event_handler_rest/src/setting_cors_extra_origins_output.json"
311+
```
312+
300313
#### Pre-flight
301314

302315
Pre-flight (OPTIONS) calls are typically handled at the API Gateway or Lambda Function URL level as per [our sample infrastructure](#required-resources), no Lambda integration is necessary. However, ALB expects you to handle pre-flight requests.
@@ -310,9 +323,13 @@ For convenience, these are the default values when using `CORSConfig` to enable
310323
???+ warning
311324
Always configure `allow_origin` when using in production.
312325

326+
???+ tip "Multiple origins?"
327+
If you need to allow multiple origins, pass the additional origins using the `extra_origins` key.
328+
313329
| Key | Value | Note |
314-
| -------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
330+
|----------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
315331
| **[allow_origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin){target="_blank"}**: `str` | `*` | Only use the default value for development. **Never use `*` for production** unless your use case requires it |
332+
| **[extra_origins](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin){target="_blank"}**: `List[str]` | `[]` | Additional origins to be allowed, in addition to the one specified in `allow_origin` |
316333
| **[allow_headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers){target="_blank"}**: `List[str]` | `[Authorization, Content-Type, X-Amz-Date, X-Api-Key, X-Amz-Security-Token]` | Additional headers will be appended to the default list for your convenience |
317334
| **[expose_headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers){target="_blank"}**: `List[str]` | `[]` | Any additional header beyond the [safe listed by CORS specification](https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header){target="_blank"}. |
318335
| **[max_age](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age){target="_blank"}**: `int` | `` | Only for pre-flight requests if you choose to have your function to handle it instead of API Gateway |
@@ -331,7 +348,7 @@ You can use the `Response` class to have full control over the response. For exa
331348

332349
=== "fine_grained_responses.py"
333350

334-
```python hl_lines="9 28-32"
351+
```python hl_lines="9 29-35"
335352
--8<-- "examples/event_handler_rest/src/fine_grained_responses.py"
336353
```
337354

examples/batch_processing/sam/kinesis_batch_processing.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
AWSTemplateFormatVersion: '2010-09-09'
1+
AWSTemplateFormatVersion: "2010-09-09"
22
Transform: AWS::Serverless-2016-10-31
33
Description: partial batch response sample
44

@@ -51,3 +51,6 @@ Resources:
5151
Type: AWS::Kinesis::Stream
5252
Properties:
5353
ShardCount: 1
54+
StreamEncryption:
55+
EncryptionType: KMS
56+
KeyId: alias/aws/kinesis

examples/batch_processing/sam/sqs_batch_processing.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
AWSTemplateFormatVersion: '2010-09-09'
1+
AWSTemplateFormatVersion: "2010-09-09"
22
Transform: AWS::Serverless-2016-10-31
33
Description: partial batch response sample
44

@@ -37,6 +37,7 @@ Resources:
3737
Type: AWS::SQS::Queue
3838
Properties:
3939
VisibilityTimeout: 30 # Fn timeout * 6
40+
SqsManagedSseEnabled: true
4041
RedrivePolicy:
4142
maxReceiveCount: 2
4243
deadLetterTargetArn: !GetAtt SampleDLQ.Arn

examples/event_handler_rest/src/setting_cors.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
tracer = Tracer()
1010
logger = Logger()
11-
cors_config = CORSConfig(allow_origin="https://example.com", max_age=300)
11+
# CORS will match when Origin is only https://www.example.com
12+
cors_config = CORSConfig(allow_origin="https://www.example.com", max_age=300)
1213
app = APIGatewayRestResolver(cors=cors_config)
1314

1415

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import requests
2+
from requests import Response
3+
4+
from aws_lambda_powertools import Logger, Tracer
5+
from aws_lambda_powertools.event_handler import APIGatewayRestResolver, CORSConfig
6+
from aws_lambda_powertools.logging import correlation_paths
7+
from aws_lambda_powertools.utilities.typing import LambdaContext
8+
9+
tracer = Tracer()
10+
logger = Logger()
11+
# CORS will match when Origin is https://www.example.com OR https://dev.example.com
12+
cors_config = CORSConfig(allow_origin="https://www.example.com", extra_origins=["https://dev.example.com"], max_age=300)
13+
app = APIGatewayRestResolver(cors=cors_config)
14+
15+
16+
@app.get("/todos")
17+
@tracer.capture_method
18+
def get_todos():
19+
todos: Response = requests.get("https://jsonplaceholder.typicode.com/todos")
20+
todos.raise_for_status()
21+
22+
# for brevity, we'll limit to the first 10 only
23+
return {"todos": todos.json()[:10]}
24+
25+
26+
@app.get("/todos/<todo_id>")
27+
@tracer.capture_method
28+
def get_todo_by_id(todo_id: str): # value come as str
29+
todos: Response = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}")
30+
todos.raise_for_status()
31+
32+
return {"todos": todos.json()}
33+
34+
35+
@app.get("/healthcheck", cors=False) # optionally removes CORS for a given route
36+
@tracer.capture_method
37+
def am_i_alive():
38+
return {"am_i_alive": "yes"}
39+
40+
41+
# You can continue to use other utilities just as before
42+
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
43+
@tracer.capture_lambda_handler
44+
def lambda_handler(event: dict, context: LambdaContext) -> dict:
45+
return app.resolve(event, context)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"statusCode": 200,
3+
"multiValueHeaders": {
4+
"Content-Type": ["application/json"],
5+
"Access-Control-Allow-Origin": ["https://www.example.com","https://dev.example.com"],
6+
"Access-Control-Allow-Headers": ["Authorization,Content-Type,X-Amz-Date,X-Amz-Security-Token,X-Api-Key"]
7+
},
8+
"body": "{\"todos\":[{\"userId\":1,\"id\":1,\"title\":\"delectus aut autem\",\"completed\":false},{\"userId\":1,\"id\":2,\"title\":\"quis ut nam facilis et officia qui\",\"completed\":false},{\"userId\":1,\"id\":3,\"title\":\"fugiat veniam minus\",\"completed\":false},{\"userId\":1,\"id\":4,\"title\":\"et porro tempora\",\"completed\":true},{\"userId\":1,\"id\":5,\"title\":\"laboriosam mollitia et enim quasi adipisci quia provident illum\",\"completed\":false},{\"userId\":1,\"id\":6,\"title\":\"qui ullam ratione quibusdam voluptatem quia omnis\",\"completed\":false},{\"userId\":1,\"id\":7,\"title\":\"illo expedita consequatur quia in\",\"completed\":false},{\"userId\":1,\"id\":8,\"title\":\"quo adipisci enim quam ut ab\",\"completed\":true},{\"userId\":1,\"id\":9,\"title\":\"molestiae perspiciatis ipsa\",\"completed\":false},{\"userId\":1,\"id\":10,\"title\":\"illo est ratione doloremque quia maiores aut\",\"completed\":true}]}",
9+
"isBase64Encoded": false
10+
}

tests/e2e/event_handler/handlers/alb_handler.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
from aws_lambda_powertools.event_handler import ALBResolver, Response, content_types
2-
3-
app = ALBResolver()
1+
from aws_lambda_powertools.event_handler import (
2+
ALBResolver,
3+
CORSConfig,
4+
Response,
5+
content_types,
6+
)
7+
8+
cors_config = CORSConfig(allow_origin="https://www.example.org", extra_origins=["https://dev.example.org"])
9+
app = ALBResolver(cors=cors_config)
410

511
# The reason we use post is that whoever is writing tests can easily assert on the
612
# content being sent (body, headers, cookies, content-type) to reduce cognitive load.

tests/e2e/event_handler/handlers/api_gateway_http_handler.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from aws_lambda_powertools.event_handler import (
22
APIGatewayHttpResolver,
3+
CORSConfig,
34
Response,
45
content_types,
56
)
67

7-
app = APIGatewayHttpResolver()
8+
cors_config = CORSConfig(allow_origin="https://www.example.org", extra_origins=["https://dev.example.org"])
9+
app = APIGatewayHttpResolver(cors=cors_config)
810

911
# The reason we use post is that whoever is writing tests can easily assert on the
1012
# content being sent (body, headers, cookies, content-type) to reduce cognitive load.

tests/e2e/event_handler/handlers/api_gateway_rest_handler.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from aws_lambda_powertools.event_handler import (
22
APIGatewayRestResolver,
3+
CORSConfig,
34
Response,
45
content_types,
56
)
67

7-
app = APIGatewayRestResolver()
8+
cors_config = CORSConfig(allow_origin="https://www.example.org", extra_origins=["https://dev.example.org"])
9+
app = APIGatewayRestResolver(cors=cors_config)
810

911
# The reason we use post is that whoever is writing tests can easily assert on the
1012
# content being sent (body, headers, cookies, content-type) to reduce cognitive load.

tests/e2e/event_handler/handlers/lambda_function_url_handler.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from aws_lambda_powertools.event_handler import (
2+
CORSConfig,
23
LambdaFunctionUrlResolver,
34
Response,
45
content_types,
56
)
67

7-
app = LambdaFunctionUrlResolver()
8+
cors_config = CORSConfig(allow_origin="https://www.example.org", extra_origins=["https://dev.example.org"])
9+
app = LambdaFunctionUrlResolver(cors=cors_config)
810

911
# The reason we use post is that whoever is writing tests can easily assert on the
1012
# content being sent (body, headers, cookies, content-type) to reduce cognitive load.

0 commit comments

Comments
 (0)