Skip to content

Commit 125b9c7

Browse files
author
Michael Brewer
committed
Merge branch 'develop' into fix-api-docs-parser
2 parents e0e2249 + 61e7f2d commit 125b9c7

File tree

15 files changed

+257
-26
lines changed

15 files changed

+257
-26
lines changed

Diff for: aws_lambda_powertools/event_handler/api_gateway.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -525,10 +525,10 @@ def _resolve(self) -> ResponseBuilder:
525525
for route in self._routes:
526526
if method != route.method:
527527
continue
528-
match: Optional[re.Match] = route.rule.match(path)
529-
if match:
528+
match_results: Optional[re.Match] = route.rule.match(path)
529+
if match_results:
530530
logger.debug("Found a registered route. Calling function")
531-
return self._call_route(route, match.groupdict()) # pass fn args
531+
return self._call_route(route, match_results.groupdict()) # pass fn args
532532

533533
logger.debug(f"No match found for path {path} and method {method}")
534534
return self._not_found(method)

Diff for: aws_lambda_powertools/exceptions/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Shared exceptions that don't belong to a single utility"""
2+
3+
4+
class InvalidEnvelopeExpressionError(Exception):
5+
"""When JMESPath fails to parse expression"""

Diff for: aws_lambda_powertools/logging/correlation_paths.py

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
API_GATEWAY_REST = "requestContext.requestId"
44
API_GATEWAY_HTTP = API_GATEWAY_REST
5+
APPSYNC_AUTHORIZER = "requestContext.requestId"
56
APPSYNC_RESOLVER = 'request.headers."x-amzn-trace-id"'
67
APPLICATION_LOAD_BALANCER = 'headers."x-amzn-trace-id"'
78
EVENT_BRIDGE = "id"

Diff for: aws_lambda_powertools/shared/jmespath_utils.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import base64
22
import gzip
33
import json
4+
import logging
45
from typing import Any, Dict, Optional, Union
56

67
import jmespath
78
from jmespath.exceptions import LexerError
89

9-
from aws_lambda_powertools.utilities.validation import InvalidEnvelopeExpressionError
10-
from aws_lambda_powertools.utilities.validation.base import logger
10+
from aws_lambda_powertools.exceptions import InvalidEnvelopeExpressionError
11+
12+
logger = logging.getLogger(__name__)
1113

1214

1315
class PowertoolsFunctions(jmespath.functions.Functions):
@@ -27,7 +29,7 @@ def _func_powertools_base64_gzip(self, value):
2729
return uncompressed.decode()
2830

2931

30-
def unwrap_event_from_envelope(data: Union[Dict, str], envelope: str, jmespath_options: Optional[Dict]) -> Any:
32+
def extract_data_from_envelope(data: Union[Dict, str], envelope: str, jmespath_options: Optional[Dict]) -> Any:
3133
"""Searches data using JMESPath expression
3234
3335
Parameters
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from typing import Any, Dict, List, Optional
2+
3+
from aws_lambda_powertools.utilities.data_classes.common import DictWrapper
4+
5+
6+
class AppSyncAuthorizerEventRequestContext(DictWrapper):
7+
"""Request context"""
8+
9+
@property
10+
def api_id(self) -> str:
11+
"""AppSync API ID"""
12+
return self["requestContext"]["apiId"]
13+
14+
@property
15+
def account_id(self) -> str:
16+
"""AWS Account ID"""
17+
return self["requestContext"]["accountId"]
18+
19+
@property
20+
def request_id(self) -> str:
21+
"""Requestt ID"""
22+
return self["requestContext"]["requestId"]
23+
24+
@property
25+
def query_string(self) -> str:
26+
"""GraphQL query string"""
27+
return self["requestContext"]["queryString"]
28+
29+
@property
30+
def operation_name(self) -> Optional[str]:
31+
"""GraphQL operation name, optional"""
32+
return self["requestContext"].get("operationName")
33+
34+
@property
35+
def variables(self) -> Dict:
36+
"""GraphQL variables"""
37+
return self["requestContext"]["variables"]
38+
39+
40+
class AppSyncAuthorizerEvent(DictWrapper):
41+
"""AppSync lambda authorizer event
42+
43+
Documentation:
44+
-------------
45+
- https://aws.amazon.com/blogs/mobile/appsync-lambda-auth/
46+
- https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#aws-lambda-authorization
47+
- https://docs.amplify.aws/lib/graphqlapi/authz/q/platform/js#aws-lambda
48+
"""
49+
50+
@property
51+
def authorization_token(self) -> str:
52+
"""Authorization token"""
53+
return self["authorizationToken"]
54+
55+
@property
56+
def request_context(self) -> AppSyncAuthorizerEventRequestContext:
57+
"""Request context"""
58+
return AppSyncAuthorizerEventRequestContext(self._data)
59+
60+
61+
class AppSyncAuthorizerResponse:
62+
"""AppSync Lambda authorizer response helper
63+
64+
Parameters
65+
----------
66+
authorize: bool
67+
authorize is a boolean value indicating if the value in authorizationToken
68+
is authorized to make calls to the GraphQL API. If this value is
69+
true, execution of the GraphQL API continues. If this value is false,
70+
an UnauthorizedException is raised
71+
max_age: int, optional
72+
Set the ttlOverride. The number of seconds that the response should be
73+
cached for. If no value is returned, the value from the API (if configured)
74+
or the default of 300 seconds (five minutes) is used. If this is 0, the response
75+
is not cached.
76+
resolver_context: Dict[str, Any], optional
77+
A JSON object visible as `$ctx.identity.resolverContext` in resolver templates
78+
79+
The resolverContext object only supports key-value pairs. Nested keys are not supported.
80+
81+
Warning: The total size of this JSON object must not exceed 5MB.
82+
deny_fields: List[str], optional
83+
A list of fields that will be set to `null` regardless of the resolver's return.
84+
85+
A field is either `TypeName.FieldName`, or an ARN such as
86+
`arn:aws:appsync:us-east-1:111122223333:apis/GraphQLApiId/types/TypeName/fields/FieldName`
87+
88+
Use the full ARN for correctness when sharing a Lambda function authorizer between APIs.
89+
"""
90+
91+
def __init__(
92+
self,
93+
authorize: bool = False,
94+
max_age: Optional[int] = None,
95+
resolver_context: Optional[Dict[str, Any]] = None,
96+
deny_fields: Optional[List[str]] = None,
97+
):
98+
self.authorize = authorize
99+
self.max_age = max_age
100+
self.deny_fields = deny_fields
101+
self.resolver_context = resolver_context
102+
103+
def asdict(self) -> dict:
104+
"""Return the response as a dict"""
105+
response: Dict = {"isAuthorized": self.authorize}
106+
107+
if self.max_age is not None:
108+
response["ttlOverride"] = self.max_age
109+
110+
if self.deny_fields:
111+
response["deniedFields"] = self.deny_fields
112+
113+
if self.resolver_context:
114+
response["resolverContext"] = self.resolver_context
115+
116+
return response

Diff for: aws_lambda_powertools/utilities/feature_flags/appconfig.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def get_configuration(self) -> Dict[str, Any]:
8080
)
8181

8282
if self.envelope:
83-
config = jmespath_utils.unwrap_event_from_envelope(
83+
config = jmespath_utils.extract_data_from_envelope(
8484
data=config, envelope=self.envelope, jmespath_options=self.jmespath_options
8585
)
8686

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from ...exceptions import InvalidEnvelopeExpressionError
2+
3+
14
class SchemaValidationError(Exception):
25
"""When serialization fail schema validation"""
36

@@ -6,5 +9,4 @@ class InvalidSchemaFormatError(Exception):
69
"""When JSON Schema is in invalid format"""
710

811

9-
class InvalidEnvelopeExpressionError(Exception):
10-
"""When JMESPath fails to parse expression"""
12+
__all__ = ["SchemaValidationError", "InvalidSchemaFormatError", "InvalidEnvelopeExpressionError"]

Diff for: aws_lambda_powertools/utilities/validation/validator.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def handler(event, context):
117117
When JMESPath expression to unwrap event is invalid
118118
"""
119119
if envelope:
120-
event = jmespath_utils.unwrap_event_from_envelope(
120+
event = jmespath_utils.extract_data_from_envelope(
121121
data=event, envelope=envelope, jmespath_options=jmespath_options
122122
)
123123

@@ -219,7 +219,7 @@ def handler(event, context):
219219
When JMESPath expression to unwrap event is invalid
220220
"""
221221
if envelope:
222-
event = jmespath_utils.unwrap_event_from_envelope(
222+
event = jmespath_utils.extract_data_from_envelope(
223223
data=event, envelope=envelope, jmespath_options=jmespath_options
224224
)
225225

Diff for: docs/utilities/data_classes.md

+48
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Event Source | Data_class
6363
[API Gateway Proxy V2](#api-gateway-proxy-v2) | `APIGatewayProxyEventV2`
6464
[Application Load Balancer](#application-load-balancer) | `ALBEvent`
6565
[AppSync Resolver](#appsync-resolver) | `AppSyncResolverEvent`
66+
[AppSync Authorizer](#appsync-authorizer) | `AppSyncAuthorizerEvent`
6667
[CloudWatch Logs](#cloudwatch-logs) | `CloudWatchLogsEvent`
6768
[CodePipeline Job Event](#codepipeline-job) | `CodePipelineJobEvent`
6869
[Cognito User Pool](#cognito-user-pool) | Multiple available under `cognito_user_pool_event`
@@ -128,6 +129,53 @@ Is it used for Application load balancer event.
128129
do_something_with(event.json_body, event.query_string_parameters)
129130
```
130131

132+
## AppSync Authorizer
133+
134+
> New in 1.20.0
135+
136+
Used when building an [AWS_LAMBDA Authorization](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#aws-lambda-authorization){target="_blank"} with AppSync.
137+
See blog post [Introducing Lambda authorization for AWS AppSync GraphQL APIs](https://aws.amazon.com/blogs/mobile/appsync-lambda-auth/){target="_blank"}
138+
or read the Amplify documentation on using [AWS Lambda for authorization](https://docs.amplify.aws/lib/graphqlapi/authz/q/platform/js#aws-lambda){target="_blank"} with AppSync.
139+
140+
In this example extract the `requestId` as the `correlation_id` for logging, used `@event_source` decorator and builds the AppSync authorizer using the `AppSyncAuthorizerResponse` helper.
141+
142+
=== "app.py"
143+
144+
```python
145+
from typing import Dict
146+
147+
from aws_lambda_powertools.logging import correlation_paths
148+
from aws_lambda_powertools.logging.logger import Logger
149+
from aws_lambda_powertools.utilities.data_classes.appsync_authorizer_event import (
150+
AppSyncAuthorizerEvent,
151+
AppSyncAuthorizerResponse,
152+
)
153+
from aws_lambda_powertools.utilities.data_classes.event_source import event_source
154+
155+
logger = Logger()
156+
157+
158+
def get_user_by_token(token: str):
159+
"""Look a user by token"""
160+
161+
162+
@logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_AUTHORIZER)
163+
@event_source(data_class=AppSyncAuthorizerEvent)
164+
def lambda_handler(event: AppSyncAuthorizerEvent, context) -> Dict:
165+
user = get_user_by_token(event.authorization_token)
166+
167+
if not user:
168+
# No user found, return not authorized
169+
return AppSyncAuthorizerResponse().to_dict()
170+
171+
return AppSyncAuthorizerResponse(
172+
authorize=True,
173+
resolver_context={"id": user.id},
174+
# Only allow admins to delete events
175+
deny_fields=None if user.is_admin else ["Mutation.deleteEvent"],
176+
).asdict()
177+
```
178+
131179
### AppSync Resolver
132180

133181
> New in 1.12.0

Diff for: poetry.lock

+11-11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: pyproject.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ black = "^20.8b1"
3434
flake8 = "^3.9.0"
3535
flake8-black = "^0.2.3"
3636
flake8-builtins = "^1.5.3"
37-
flake8-comprehensions = "^3.6.0"
37+
flake8-comprehensions = "^3.6.1"
3838
flake8-debugger = "^4.0.0"
3939
flake8-fixme = "^1.1.1"
4040
flake8-isort = "^4.0.0"
@@ -60,7 +60,7 @@ pydantic = ["pydantic", "email-validator"]
6060

6161
[tool.coverage.run]
6262
source = ["aws_lambda_powertools"]
63-
omit = ["tests/*"]
63+
omit = ["tests/*", "aws_lambda_powertools/exceptions/*"]
6464
branch = true
6565

6666
[tool.coverage.html]

Diff for: tests/events/appSyncAuthorizerEvent.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"authorizationToken": "BE9DC5E3-D410-4733-AF76-70178092E681",
3+
"requestContext": {
4+
"apiId": "giy7kumfmvcqvbedntjwjvagii",
5+
"accountId": "254688921111",
6+
"requestId": "b80ed838-14c6-4500-b4c3-b694c7bef086",
7+
"queryString": "mutation MyNewTask($desc: String!) {\n createTask(description: $desc, owner: \"ccc\", taskStatus: \"cc\", title: \"ccc\") {\n id\n }\n}\n",
8+
"operationName": "MyNewTask",
9+
"variables": {
10+
"desc": "Foo"
11+
}
12+
}
13+
}

Diff for: tests/events/appSyncAuthorizerResponse.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"isAuthorized": true,
3+
"resolverContext": {
4+
"name": "Foo Man",
5+
"balance": 100
6+
},
7+
"deniedFields": ["Mutation.createEvent"],
8+
"ttlOverride": 15
9+
}

0 commit comments

Comments
 (0)