Skip to content

Commit 0258400

Browse files
author
Michael Brewer
authored
feat(data-classes): AppSync Lambda authorizer event (#610)
1 parent f7cd398 commit 0258400

File tree

6 files changed

+222
-0
lines changed

6 files changed

+222
-0
lines changed

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"
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: Optional[int]
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: Optional[Dict[str, Any]]
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: Optional[List[str]]
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

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
+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+
}
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+
}

tests/functional/test_data_classes.py

+35
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
aws_timestamp,
3131
make_id,
3232
)
33+
from aws_lambda_powertools.utilities.data_classes.appsync_authorizer_event import (
34+
AppSyncAuthorizerEvent,
35+
AppSyncAuthorizerResponse,
36+
)
3337
from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import (
3438
AppSyncIdentityCognito,
3539
AppSyncIdentityIAM,
@@ -1419,3 +1423,34 @@ def lambda_handler(event: APIGatewayProxyEventV2, _):
14191423

14201424
# WHEN calling the lambda handler
14211425
lambda_handler({"headers": {"X-Foo": "Foo"}}, None)
1426+
1427+
1428+
def test_appsync_authorizer_event():
1429+
event = AppSyncAuthorizerEvent(load_event("appSyncAuthorizerEvent.json"))
1430+
1431+
assert event.authorization_token == "BE9DC5E3-D410-4733-AF76-70178092E681"
1432+
assert event.authorization_token == event["authorizationToken"]
1433+
assert event.request_context.api_id == event["requestContext"]["apiId"]
1434+
assert event.request_context.account_id == event["requestContext"]["accountId"]
1435+
assert event.request_context.request_id == event["requestContext"]["requestId"]
1436+
assert event.request_context.query_string == event["requestContext"]["queryString"]
1437+
assert event.request_context.operation_name == event["requestContext"]["operationName"]
1438+
assert event.request_context.variables == event["requestContext"]["variables"]
1439+
1440+
1441+
def test_appsync_authorizer_response():
1442+
"""Check various helper functions for AppSync authorizer response"""
1443+
expected = load_event("appSyncAuthorizerResponse.json")
1444+
response = AppSyncAuthorizerResponse(
1445+
authorize=True,
1446+
max_age=15,
1447+
resolver_context={"balance": 100, "name": "Foo Man"},
1448+
deny_fields=["Mutation.createEvent"],
1449+
)
1450+
assert expected == response.asdict()
1451+
1452+
assert {"isAuthorized": False} == AppSyncAuthorizerResponse().asdict()
1453+
assert {"isAuthorized": False} == AppSyncAuthorizerResponse(deny_fields=[]).asdict()
1454+
assert {"isAuthorized": False} == AppSyncAuthorizerResponse(resolver_context={}).asdict()
1455+
assert {"isAuthorized": True} == AppSyncAuthorizerResponse(authorize=True).asdict()
1456+
assert {"isAuthorized": False, "ttlOverride": 0} == AppSyncAuthorizerResponse(max_age=0).asdict()

0 commit comments

Comments
 (0)