From 18e234a4f7fe374780781744ea3b556c0f57af64 Mon Sep 17 00:00:00 2001 From: ibrahim Date: Sat, 31 Oct 2026 15:25:28 -0300 Subject: [PATCH 1/5] test(data_classes): add fixture and AppSyncIdentityLambda resolver_context test --- .../appSyncResolverEventLambdaAuth.json | 59 +++++++++++++++++++ .../test_appsync_resolver_event.py | 9 +++ 2 files changed, 68 insertions(+) create mode 100644 tests/events/appSyncResolverEventLambdaAuth.json diff --git a/tests/events/appSyncResolverEventLambdaAuth.json b/tests/events/appSyncResolverEventLambdaAuth.json new file mode 100644 index 00000000000..f5145e7b281 --- /dev/null +++ b/tests/events/appSyncResolverEventLambdaAuth.json @@ -0,0 +1,59 @@ +{ + "typeName": "Merchant", + "fieldName": "locations", + "arguments": { + "page": 2, + "size": 1, + "name": "value" + }, + "identity": { + "resolverContext": { + "userId": "u-1", + "tier": "gold", + "nested": { + "team": "eng" + } + } + }, + "source": { + "name": "Value", + "nested": { + "name": "value", + "list": [] + } + }, + "request": { + "headers": { + "x-forwarded-for": "11.215.2.22, 64.44.173.11", + "cloudfront-viewer-country": "US", + "cloudfront-is-tablet-viewer": "false", + "via": "2.0 SOMETHING.cloudfront.net (CloudFront)", + "cloudfront-forwarded-proto": "https", + "origin": "https://console.aws.amazon.com", + "content-length": "156", + "accept-language": "en-US,en;q=0.9", + "host": "SOMETHING.appsync-api.us-east-1.amazonaws.com", + "x-forwarded-proto": "https", + "sec-gpc": "1", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) etc.", + "accept": "*/*", + "cloudfront-is-mobile-viewer": "false", + "cloudfront-is-smarttv-viewer": "false", + "accept-encoding": "gzip, deflate, br", + "referer": "https://console.aws.amazon.com/", + "content-type": "application/json", + "sec-fetch-mode": "cors", + "x-amz-cf-id": "Fo5VIuvP6V6anIEt62WzFDCK45mzM4yEdpt5BYxOl9OFqafd-WR0cA==", + "x-amzn-trace-id": "Root=1-60488877-0b0c4e6727ab2a1c545babd0", + "authorization": "AUTH-HEADER", + "sec-fetch-dest": "empty", + "x-amz-user-agent": "AWS-Console-AppSync/", + "cloudfront-is-desktop-viewer": "true", + "sec-fetch-site": "cross-site", + "x-forwarded-port": "443" + } + }, + "prev": { + "result": {} + } +} diff --git a/tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py b/tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py index 1d16d5402ca..df046a6462f 100644 --- a/tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py +++ b/tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py @@ -143,3 +143,12 @@ def test_appsync_resolver_event_empty(): assert event.info.field_name is None assert event.info.parent_type_name is None + + +def test_appsync_lambda_identity_class_exposes_resolver_context(): + # AWS_LAMBDA authorization: the identity carries only what the authorizer returned + from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import AppSyncIdentityLambda + + identity = AppSyncIdentityLambda({"resolverContext": {"userId": "u-1", "tier": "gold"}}) + + assert identity.resolver_context == {"userId": "u-1", "tier": "gold"} From 0a3c1664c4082e76021933d89246800293562659 Mon Sep 17 00:00:00 2001 From: ibrahim Date: Sat, 31 Oct 2026 15:25:48 -0300 Subject: [PATCH 2/5] test(data_classes): AppSyncResolverEvent dispatches resolverContext identity to AppSyncIdentityLambda --- .../test_appsync_resolver_event.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py b/tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py index df046a6462f..31f3b881143 100644 --- a/tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py +++ b/tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py @@ -152,3 +152,15 @@ def test_appsync_lambda_identity_class_exposes_resolver_context(): identity = AppSyncIdentityLambda({"resolverContext": {"userId": "u-1", "tier": "gold"}}) assert identity.resolver_context == {"userId": "u-1", "tier": "gold"} + + +def test_appsync_resolver_event_lambda_identity(): + # AWS_LAMBDA authorization: identity with resolverContext must dispatch to AppSyncIdentityLambda + from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import AppSyncIdentityLambda + + raw_event = load_event("appSyncResolverEventLambdaAuth.json") + parsed_event = AppSyncResolverEvent(raw_event) + + assert isinstance(parsed_event.identity, AppSyncIdentityLambda) + assert not isinstance(parsed_event.identity, AppSyncIdentityIAM) + assert parsed_event.identity.resolver_context == raw_event["identity"]["resolverContext"] From 6d766930d201a9e1b81d2b049a6e8f953701402b Mon Sep 17 00:00:00 2001 From: ibrahim Date: Sat, 31 Oct 2026 15:25:48 -0300 Subject: [PATCH 3/5] test(data_classes): regression - IAM identity fallback unchanged without resolverContext --- .../test_appsync_resolver_event.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py b/tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py index 31f3b881143..793cbf21492 100644 --- a/tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py +++ b/tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py @@ -164,3 +164,22 @@ def test_appsync_resolver_event_lambda_identity(): assert isinstance(parsed_event.identity, AppSyncIdentityLambda) assert not isinstance(parsed_event.identity, AppSyncIdentityIAM) assert parsed_event.identity.resolver_context == raw_event["identity"]["resolverContext"] + + +def test_get_identity_object_iam_unchanged_without_resolver_context(): + # AWS_IAM authorization must keep falling through when no resolverContext is present + identity = { + "accountId": "123456789012", + "cognitoIdentityAuthProvider": "cognitoIdentityAuthProvider", + "cognitoIdentityAuthType": "cognitoIdentityAuthType", + "cognitoIdentityId": "cognitoIdentityId", + "cognitoIdentityPoolId": "cognitoIdentityPoolId", + "sourceIp": ["11.215.2.22"], + "userArn": "userArn", + "username": "username", + } + + identity_object = get_identity_object(identity) + + assert isinstance(identity_object, AppSyncIdentityIAM) + assert identity_object.account_id == identity["accountId"] From 660ee9d0168ba1f274f265ad12a2adc339dde2ca Mon Sep 17 00:00:00 2001 From: ibrahim Date: Sat, 31 Oct 2026 15:25:55 -0300 Subject: [PATCH 4/5] test(data_classes): resolver_context passes nested structures through untouched --- .../test_appsync_resolver_event.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py b/tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py index 793cbf21492..c0a633a2ef3 100644 --- a/tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py +++ b/tests/unit/data_classes/required_dependencies/test_appsync_resolver_event.py @@ -183,3 +183,15 @@ def test_get_identity_object_iam_unchanged_without_resolver_context(): assert isinstance(identity_object, AppSyncIdentityIAM) assert identity_object.account_id == identity["accountId"] + + +def test_get_identity_object_lambda_resolver_context_passthrough(): + # AWS_LAMBDA authorization: resolver_context must pass through nested structures untouched + from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import AppSyncIdentityLambda + + resolver_context = {"userId": "u-1", "nested": {"team": "eng", "roles": ["admin"]}} + identity_object = get_identity_object({"resolverContext": resolver_context}) + + assert isinstance(identity_object, AppSyncIdentityLambda) + assert identity_object.resolver_context == resolver_context + assert identity_object.resolver_context["nested"]["roles"] == ["admin"] From 54ec63aaea4561e7b842457e3a3473f25ada7f75 Mon Sep 17 00:00:00 2001 From: ibrahim Date: Sat, 31 Oct 2026 15:27:17 -0300 Subject: [PATCH 5/5] fix(data_classes): add AppSyncIdentityLambda for AWS_LAMBDA AppSync authorization --- .../data_classes/appsync_resolver_event.py | 21 +++++++++++++++++-- docs/utilities/data_classes.md | 2 ++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py index 1571ece94c7..9a9de0f22b7 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_resolver_event.py @@ -22,6 +22,10 @@ def get_identity_object(identity: dict | None) -> Any: if "sub" in identity: return AppSyncIdentityCognito(identity) + # AWS_LAMBDA authorization + if "resolverContext" in identity: + return AppSyncIdentityLambda(identity) + # AWS_IAM authorization return AppSyncIdentityIAM(identity) @@ -54,13 +58,13 @@ def stash(self) -> dict: return self.get("stash") or {} @property - def identity(self) -> AppSyncIdentityIAM | AppSyncIdentityCognito | None: + def identity(self) -> AppSyncIdentityIAM | AppSyncIdentityCognito | AppSyncIdentityLambda | None: """An object that contains information about the caller. Depending on the type of identify found: - API_KEY authorization - returns None - AWS_IAM authorization - returns AppSyncIdentityIAM - AMAZON_COGNITO_USER_POOLS authorization - returns AppSyncIdentityCognito - - AWS_LAMBDA authorization - returns None - NEED TO TEST + - AWS_LAMBDA authorization - returns AppSyncIdentityLambda - OPENID_CONNECT authorization - returns None - NEED TO TEST """ return get_identity_object(self.get("identity")) @@ -150,6 +154,19 @@ def issuer(self) -> str: return self["issuer"] +class AppSyncIdentityLambda(DictWrapper): + """AWS_LAMBDA authorization + + When using a Lambda authorizer, the identity object contains only the + ``resolverContext`` returned by the authorizer. + """ + + @property + def resolver_context(self) -> dict[str, Any]: + """The resolver context returned by the Lambda authorizer.""" + return self["resolverContext"] + + class AppSyncResolverEventInfo(DictWrapper): """The info section contains information about the GraphQL request""" diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index d57bdcd7509..631b9f968ca 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -259,6 +259,8 @@ and [AppSync Direct Lambda Resolvers](https://aws.amazon.com/blogs/mobile/appsyn 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`. +Depending on the authorization type, `AppSyncResolverEvent.identity` returns `AppSyncIdentityCognito` (Cognito User Pools / OpenID Connect), `AppSyncIdentityIAM` (AWS IAM), `AppSyncIdentityLambda` (Lambda authorizer, exposing the authorizer's `resolver_context`), or `None` (API key). + === "app.py" ```python hl_lines="2-4 9"