Skip to content

Commit 89fa1ba

Browse files
committed
fix(data_classes): add AppSyncIdentityLambda for AWS_LAMBDA authorization
1 parent 4c43e9c commit 89fa1ba

4 files changed

Lines changed: 82 additions & 2 deletions

File tree

‎aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ def get_identity_object(identity: dict | None) -> Any:
2222
if "sub" in identity:
2323
return AppSyncIdentityCognito(identity)
2424

25+
# AWS_LAMBDA authorization - the identity is whatever the authorizer returned in resolverContext
26+
if "resolverContext" in identity:
27+
return AppSyncIdentityLambda(identity)
28+
2529
# AWS_IAM authorization
2630
return AppSyncIdentityIAM(identity)
2731

@@ -54,13 +58,13 @@ def stash(self) -> dict:
5458
return self.get("stash") or {}
5559

5660
@property
57-
def identity(self) -> AppSyncIdentityIAM | AppSyncIdentityCognito | None:
61+
def identity(self) -> AppSyncIdentityIAM | AppSyncIdentityCognito | AppSyncIdentityLambda | None:
5862
"""An object that contains information about the caller.
5963
Depending on the type of identify found:
6064
- API_KEY authorization - returns None
6165
- AWS_IAM authorization - returns AppSyncIdentityIAM
6266
- AMAZON_COGNITO_USER_POOLS authorization - returns AppSyncIdentityCognito
63-
- AWS_LAMBDA authorization - returns None - NEED TO TEST
67+
- AWS_LAMBDA authorization - returns AppSyncIdentityLambda
6468
- OPENID_CONNECT authorization - returns None - NEED TO TEST
6569
"""
6670
return get_identity_object(self.get("identity"))
@@ -150,6 +154,15 @@ def issuer(self) -> str:
150154
return self["issuer"]
151155

152156

157+
class AppSyncIdentityLambda(DictWrapper):
158+
"""AWS_LAMBDA authorization"""
159+
160+
@property
161+
def resolver_context(self) -> dict[str, Any]:
162+
"""The context that the Lambda authorizer returned under `resolverContext`."""
163+
return self["resolverContext"]
164+
165+
153166
class AppSyncResolverEventInfo(DictWrapper):
154167
"""The info section contains information about the GraphQL request"""
155168

‎docs/utilities/data_classes.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ and [AppSync Direct Lambda Resolvers](https://aws.amazon.com/blogs/mobile/appsyn
259259

260260
The example serves as an AppSync resolver for the `locations` field of the `Merchant` type. It uses the `@event_source` decorator to parse the AppSync event, handles pagination and filtering for locations, and demonstrates `AppSyncIdentityCognito`.
261261

262+
With [AWS_LAMBDA authorization](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#aws-lambda-authorization){target="_blank"}, the caller identity is whatever the Lambda authorizer returned in `resolverContext`. In that case, `AppSyncResolverEvent.identity` returns an `AppSyncIdentityLambda`, and the authorizer context is available via `identity.resolver_context`.
263+
262264
=== "app.py"
263265

264266
```python hl_lines="2-4 9"
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"typeName": "Query",
3+
"fieldName": "getUser",
4+
"arguments": {
5+
"userId": "u-1"
6+
},
7+
"identity": {
8+
"resolverContext": {
9+
"userId": "u-1",
10+
"tier": "gold"
11+
}
12+
},
13+
"source": null,
14+
"request": {
15+
"headers": {
16+
"x-forwarded-for": "11.215.2.22",
17+
"host": "SOMETHING.appsync-api.us-east-1.amazonaws.com"
18+
},
19+
"domainName": "SOMETHING.appsync-api.us-east-1.amazonaws.com"
20+
},
21+
"prev": null,
22+
"info": {
23+
"selectionSetList": ["userId", "tier"],
24+
"selectionSetGraphQL": "{\n userId\n tier\n}",
25+
"parentTypeName": "Query",
26+
"fieldName": "getUser",
27+
"variables": {}
28+
},
29+
"stash": {}
30+
}

‎tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import (
77
AppSyncIdentityCognito,
88
AppSyncIdentityIAM,
9+
AppSyncIdentityLambda,
910
AppSyncResolverEventInfo,
1011
get_identity_object,
1112
)
@@ -143,3 +144,37 @@ def test_appsync_resolver_event_empty():
143144

144145
assert event.info.field_name is None
145146
assert event.info.parent_type_name is None
147+
148+
149+
def test_get_identity_object_lambda_authorizer():
150+
# GIVEN an AWS_LAMBDA authorization identity, whose only field is the authorizer's resolverContext
151+
identity = {"resolverContext": {"userId": "u-1", "tier": "gold"}}
152+
153+
# WHEN dispatched to a type
154+
identity_object = get_identity_object(identity)
155+
156+
# THEN it resolves to AppSyncIdentityLambda, exposing the authorizer context
157+
assert isinstance(identity_object, AppSyncIdentityLambda)
158+
assert identity_object.resolver_context == identity["resolverContext"]
159+
160+
161+
def test_get_identity_object_prefers_sub_over_resolver_context():
162+
# GIVEN an identity carrying both `sub` and `resolverContext` (authorization modes are exclusive in practice)
163+
identity = {"sub": "string", "resolverContext": {"userId": "u-1"}}
164+
165+
# THEN the existing Cognito dispatch wins, keeping the old behaviour
166+
assert isinstance(get_identity_object(identity), AppSyncIdentityCognito)
167+
168+
169+
def test_appsync_resolver_event_lambda_authorizer():
170+
raw_event = load_event("appSyncLambdaAuthorizerResolverEvent.json")
171+
parsed_event = AppSyncResolverEvent(raw_event)
172+
173+
assert parsed_event.type_name == raw_event["typeName"]
174+
assert parsed_event.field_name == raw_event["fieldName"]
175+
assert parsed_event.arguments == raw_event["arguments"]
176+
177+
assert isinstance(parsed_event.identity, AppSyncIdentityLambda)
178+
identity: AppSyncIdentityLambda = parsed_event.identity
179+
assert identity.resolver_context == raw_event["identity"]["resolverContext"]
180+
assert identity.resolver_context["userId"] == "u-1"

0 commit comments

Comments
 (0)