From a11eaaa97dabd2a9f4c769b0e831203052b15afa Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sat, 14 Aug 2021 16:56:16 -0700 Subject: [PATCH 01/13] feat(event-sources): AppSync lambda authorizer event --- .../logging/correlation_paths.py | 1 + .../data_classes/appsync_authorizer_event.py | 57 +++++++++++++++++++ tests/events/appSyncAuthorizerEvent.json | 13 +++++ tests/events/appSyncAuthorizerResponse.json | 9 +++ tests/functional/test_data_classes.py | 14 +++++ 5 files changed, 94 insertions(+) create mode 100644 aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py create mode 100644 tests/events/appSyncAuthorizerEvent.json create mode 100644 tests/events/appSyncAuthorizerResponse.json diff --git a/aws_lambda_powertools/logging/correlation_paths.py b/aws_lambda_powertools/logging/correlation_paths.py index 004aa2a59a3..b6926f08591 100644 --- a/aws_lambda_powertools/logging/correlation_paths.py +++ b/aws_lambda_powertools/logging/correlation_paths.py @@ -2,6 +2,7 @@ API_GATEWAY_REST = "requestContext.requestId" API_GATEWAY_HTTP = API_GATEWAY_REST +APPSYNC_AUTHORIZER = "requestContext.requestId" APPSYNC_RESOLVER = 'request.headers."x-amzn-trace-id"' APPLICATION_LOAD_BALANCER = 'headers."x-amzn-trace-id"' EVENT_BRIDGE = "id" diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py new file mode 100644 index 00000000000..e5de600732a --- /dev/null +++ b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py @@ -0,0 +1,57 @@ +from typing import Optional + +from aws_lambda_powertools.utilities.data_classes.common import DictWrapper + + +class AppSyncAuthorizerEventRequestContext(DictWrapper): + """Request context""" + + @property + def api_id(self) -> str: + """AppSync api id""" + return self["requestContext"]["apiId"] + + @property + def account_id(self) -> str: + """AWS Account ID""" + return self["requestContext"]["accountId"] + + @property + def request_id(self) -> str: + """Requestt ID""" + return self["requestContext"]["requestId"] + + @property + def query_string(self) -> str: + """Graphql query string""" + return self["requestContext"]["queryString"] + + @property + def operation_name(self) -> Optional[str]: + """Graphql operation name, optional""" + return self["requestContext"].get("operationName") + + @property + def variables(self) -> dict: + """Graphql variables""" + return self["requestContext"]["variables"] + + +class AppSyncAuthorizerEvent(DictWrapper): + """AppSync lambda authorizer event + + Documentation: + ------------- + - https://aws.amazon.com/blogs/mobile/appsync-lambda-auth/ + - https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#aws-lambda-authorization + """ + + @property + def authorization_token(self) -> str: + """Authorization token""" + return self["authorizationToken"] + + @property + def request_context(self) -> AppSyncAuthorizerEventRequestContext: + """Request context""" + return AppSyncAuthorizerEventRequestContext(self._data) diff --git a/tests/events/appSyncAuthorizerEvent.json b/tests/events/appSyncAuthorizerEvent.json new file mode 100644 index 00000000000..a8264569bfc --- /dev/null +++ b/tests/events/appSyncAuthorizerEvent.json @@ -0,0 +1,13 @@ +{ + "authorizationToken": "BE9DC5E3-D410-4733-AF76-70178092E681", + "requestContext": { + "apiId": "giy7kumfmvcqvbedntjwjvagii", + "accountId": "254688921111", + "requestId": "b80ed838-14c6-4500-b4c3-b694c7bef086", + "queryString": "mutation MyNewTask($desc: String!) {\n createTask(description: $desc, owner: \"ccc\", taskStatus: \"cc\", title: \"ccc\") {\n id\n }\n}\n", + "operationName": "MyNewTask", + "variables": { + "desc": "Foo" + } + } +} diff --git a/tests/events/appSyncAuthorizerResponse.json b/tests/events/appSyncAuthorizerResponse.json new file mode 100644 index 00000000000..7dd8234d2ef --- /dev/null +++ b/tests/events/appSyncAuthorizerResponse.json @@ -0,0 +1,9 @@ +{ + "isAuthorized": true, + "resolverContext": { + "name": "Foo Man", + "balance": 100 + }, + "deniedFields": ["Mutation.createEvent"], + "ttlOverride": 15 +} diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py index f9bb1fdef73..a840f77752f 100644 --- a/tests/functional/test_data_classes.py +++ b/tests/functional/test_data_classes.py @@ -30,6 +30,7 @@ aws_timestamp, make_id, ) +from aws_lambda_powertools.utilities.data_classes.appsync_authorizer_event import AppSyncAuthorizerEvent from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import ( AppSyncIdentityCognito, AppSyncIdentityIAM, @@ -1419,3 +1420,16 @@ def lambda_handler(event: APIGatewayProxyEventV2, _): # WHEN calling the lambda handler lambda_handler({"headers": {"X-Foo": "Foo"}}, None) + + +def test_appsync_authorizer_event(): + event = AppSyncAuthorizerEvent(load_event("appSyncAuthorizerEvent.json")) + + assert event.authorization_token == "BE9DC5E3-D410-4733-AF76-70178092E681" + assert event.authorization_token == event["authorizationToken"] + assert event.request_context.api_id == event["requestContext"]["apiId"] + assert event.request_context.account_id == event["requestContext"]["accountId"] + assert event.request_context.request_id == event["requestContext"]["requestId"] + assert event.request_context.query_string == event["requestContext"]["queryString"] + assert event.request_context.operation_name == event["requestContext"]["operationName"] + assert event.request_context.variables == event["requestContext"]["variables"] From 5152ea94166fc94ab535f315375fa4670efda2c9 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sat, 14 Aug 2021 18:09:47 -0700 Subject: [PATCH 02/13] feat(event-sources): AppSync lambda authorize response helpers --- .../data_classes/appsync_authorizer_event.py | 58 ++++++++++++++++++- tests/functional/test_data_classes.py | 24 +++++++- 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py index e5de600732a..fda8656ab80 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Dict, List, Optional from aws_lambda_powertools.utilities.data_classes.common import DictWrapper @@ -32,7 +32,7 @@ def operation_name(self) -> Optional[str]: return self["requestContext"].get("operationName") @property - def variables(self) -> dict: + def variables(self) -> Dict: """Graphql variables""" return self["requestContext"]["variables"] @@ -55,3 +55,57 @@ def authorization_token(self) -> str: def request_context(self) -> AppSyncAuthorizerEventRequestContext: """Request context""" return AppSyncAuthorizerEventRequestContext(self._data) + + +class AppSyncAuthorizerResponse: + """AppSync Lambda authorizer response helper""" + + def __init__(self): + self._data: Dict = {"isAuthorized": False} + + def authorize(self) -> "AppSyncAuthorizerResponse": + """Authorize the authorizationToken by setting isAuthorized to True + + "isAuthorized" is a boolean value indicating if the value in authorizationToken + is authorized to make calls to the GraphQL API. If this value is + true, execution of the GraphQL API continues. If this value is false, + an UnauthorizedException is raised + """ + self._data["isAuthorized"] = True + return self + + def ttl(self, ttl_orderride: Optional[int] = 0) -> "AppSyncAuthorizerResponse": + """Set the ttlOverride + + The number of seconds that the response should be cached for. If no value is + returned, the value from the API (if configured) or the default of 300 seconds + (five minutes) is used. + If this is 0, the response is not cached. + """ + if ttl_orderride is not None: + self._data["ttlOverride"] = ttl_orderride + return self + + def resolver_context(self, resolver_context: Dict) -> "AppSyncAuthorizerResponse": + """A JSON object visible as $ctx.identity.resolverContext in resolver templates + + Warning: The total size of this JSON object must not exceed 5MB. + """ + self._data["resolverContext"] = resolver_context + return self + + def denied_fields(self, denied_fields: List[str]) -> "AppSyncAuthorizerResponse": + """A list of which are forcibly changed to null, even if a value was returned from a resolver. + + Each item is either a fully qualified field ARN in the form of + arn:aws:appsync:us-east-1:111122223333:apis/GraphQLApiId/types/TypeName/fields/FieldName + or a short form of TypeName.FieldName. The full ARN form should be used when two APIs + share a lambda function authorizer and there might be ambiguity between common types + and fields between the two APIs. + """ + self._data["deniedFields"] = denied_fields + return self + + def asdict(self) -> dict: + """Return the response as a dict""" + return self._data diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py index a840f77752f..6b42227499b 100644 --- a/tests/functional/test_data_classes.py +++ b/tests/functional/test_data_classes.py @@ -30,7 +30,10 @@ aws_timestamp, make_id, ) -from aws_lambda_powertools.utilities.data_classes.appsync_authorizer_event import AppSyncAuthorizerEvent +from aws_lambda_powertools.utilities.data_classes.appsync_authorizer_event import ( + AppSyncAuthorizerEvent, + AppSyncAuthorizerResponse, +) from aws_lambda_powertools.utilities.data_classes.appsync_resolver_event import ( AppSyncIdentityCognito, AppSyncIdentityIAM, @@ -1433,3 +1436,22 @@ def test_appsync_authorizer_event(): assert event.request_context.query_string == event["requestContext"]["queryString"] assert event.request_context.operation_name == event["requestContext"]["operationName"] assert event.request_context.variables == event["requestContext"]["variables"] + + +def test_appsync_authorizer_response(): + """Check various helper functions for AppSync authorizer response""" + expected = load_event("appSyncAuthorizerResponse.json") + + response = ( + AppSyncAuthorizerResponse() + .authorize() + .denied_fields(["Mutation.createEvent"]) + .resolver_context({"balance": 100, "name": "Foo Man"}) + .ttl(15) + ) + + assert expected == response.asdict() + + assert {"isAuthorized": False} == AppSyncAuthorizerResponse().asdict() + assert {"isAuthorized": True} == AppSyncAuthorizerResponse().authorize().ttl(None).asdict() + assert {"isAuthorized": True, "ttlOverride": 0} == AppSyncAuthorizerResponse().authorize().ttl().asdict() From 23e52bf800030c1728b4c10b9a80ecd797b4c264 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sat, 14 Aug 2021 18:24:48 -0700 Subject: [PATCH 03/13] feat(event-sources): rename to to_dict --- .../utilities/data_classes/appsync_authorizer_event.py | 2 +- tests/functional/test_data_classes.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py index fda8656ab80..b64e1a4afc4 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py @@ -106,6 +106,6 @@ def denied_fields(self, denied_fields: List[str]) -> "AppSyncAuthorizerResponse" self._data["deniedFields"] = denied_fields return self - def asdict(self) -> dict: + def to_dict(self) -> dict: """Return the response as a dict""" return self._data diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py index 6b42227499b..2fa37b40b87 100644 --- a/tests/functional/test_data_classes.py +++ b/tests/functional/test_data_classes.py @@ -1450,8 +1450,8 @@ def test_appsync_authorizer_response(): .ttl(15) ) - assert expected == response.asdict() + assert expected == response.to_dict() - assert {"isAuthorized": False} == AppSyncAuthorizerResponse().asdict() - assert {"isAuthorized": True} == AppSyncAuthorizerResponse().authorize().ttl(None).asdict() - assert {"isAuthorized": True, "ttlOverride": 0} == AppSyncAuthorizerResponse().authorize().ttl().asdict() + assert {"isAuthorized": False} == AppSyncAuthorizerResponse().to_dict() + assert {"isAuthorized": True} == AppSyncAuthorizerResponse().authorize().ttl(None).to_dict() + assert {"isAuthorized": True, "ttlOverride": 0} == AppSyncAuthorizerResponse().authorize().ttl().to_dict() From 490072996496ecd85d0be5156df8171349c819ad Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sat, 14 Aug 2021 22:32:31 -0700 Subject: [PATCH 04/13] refactor: opted for optionals instead --- .../data_classes/appsync_authorizer_event.py | 74 +++++++++---------- tests/functional/test_data_classes.py | 19 +++-- 2 files changed, 44 insertions(+), 49 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py index b64e1a4afc4..5a75c4535e2 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional from aws_lambda_powertools.utilities.data_classes.common import DictWrapper @@ -58,53 +58,49 @@ def request_context(self) -> AppSyncAuthorizerEventRequestContext: class AppSyncAuthorizerResponse: - """AppSync Lambda authorizer response helper""" + """AppSync Lambda authorizer response helper - def __init__(self): - self._data: Dict = {"isAuthorized": False} - - def authorize(self) -> "AppSyncAuthorizerResponse": - """Authorize the authorizationToken by setting isAuthorized to True - - "isAuthorized" is a boolean value indicating if the value in authorizationToken + Parameters + ---------- + authorize: bool + authorize is a boolean value indicating if the value in authorizationToken is authorized to make calls to the GraphQL API. If this value is true, execution of the GraphQL API continues. If this value is false, an UnauthorizedException is raised - """ - self._data["isAuthorized"] = True - return self - - def ttl(self, ttl_orderride: Optional[int] = 0) -> "AppSyncAuthorizerResponse": - """Set the ttlOverride - - The number of seconds that the response should be cached for. If no value is - returned, the value from the API (if configured) or the default of 300 seconds - (five minutes) is used. - If this is 0, the response is not cached. - """ - if ttl_orderride is not None: - self._data["ttlOverride"] = ttl_orderride - return self - - def resolver_context(self, resolver_context: Dict) -> "AppSyncAuthorizerResponse": - """A JSON object visible as $ctx.identity.resolverContext in resolver templates - + ttl: Optional[int] + Set the ttlOverride. The number of seconds that the response should be + cached for. If no value is returned, the value from the API (if configured) + or the default of 300 seconds (five minutes) is used. If this is 0, the response + is not cached. + resolver_context: Optional[Dict[str, Any]] + A JSON object visible as `$ctx.identity.resolverContext` in resolver templates Warning: The total size of this JSON object must not exceed 5MB. - """ - self._data["resolverContext"] = resolver_context - return self - - def denied_fields(self, denied_fields: List[str]) -> "AppSyncAuthorizerResponse": - """A list of which are forcibly changed to null, even if a value was returned from a resolver. - + denied_fields: Optional[List[str]] + A list of which are forcibly changed to null, even if a value was returned from a resolver. Each item is either a fully qualified field ARN in the form of - arn:aws:appsync:us-east-1:111122223333:apis/GraphQLApiId/types/TypeName/fields/FieldName + `arn:aws:appsync:us-east-1:111122223333:apis/GraphQLApiId/types/TypeName/fields/FieldName` or a short form of TypeName.FieldName. The full ARN form should be used when two APIs share a lambda function authorizer and there might be ambiguity between common types and fields between the two APIs. - """ - self._data["deniedFields"] = denied_fields - return self + """ + + def __init__( + self, + authorize: bool = False, + ttl: Optional[int] = None, + resolver_context: Optional[Dict[str, Any]] = None, + denied_fields: Optional[List[str]] = None, + ): + self._data: Dict = {"isAuthorized": authorize} + + if ttl is not None: + self._data["ttlOverride"] = ttl + + if denied_fields: + self._data["deniedFields"] = denied_fields + + if resolver_context: + self._data["resolverContext"] = resolver_context def to_dict(self) -> dict: """Return the response as a dict""" diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py index 2fa37b40b87..e35cc609316 100644 --- a/tests/functional/test_data_classes.py +++ b/tests/functional/test_data_classes.py @@ -1441,17 +1441,16 @@ def test_appsync_authorizer_event(): def test_appsync_authorizer_response(): """Check various helper functions for AppSync authorizer response""" expected = load_event("appSyncAuthorizerResponse.json") - - response = ( - AppSyncAuthorizerResponse() - .authorize() - .denied_fields(["Mutation.createEvent"]) - .resolver_context({"balance": 100, "name": "Foo Man"}) - .ttl(15) + response = AppSyncAuthorizerResponse( + authorize=True, + ttl=15, + resolver_context={"balance": 100, "name": "Foo Man"}, + denied_fields=["Mutation.createEvent"], ) - assert expected == response.to_dict() assert {"isAuthorized": False} == AppSyncAuthorizerResponse().to_dict() - assert {"isAuthorized": True} == AppSyncAuthorizerResponse().authorize().ttl(None).to_dict() - assert {"isAuthorized": True, "ttlOverride": 0} == AppSyncAuthorizerResponse().authorize().ttl().to_dict() + assert {"isAuthorized": False} == AppSyncAuthorizerResponse(denied_fields=[]).to_dict() + assert {"isAuthorized": False} == AppSyncAuthorizerResponse(resolver_context={}).to_dict() + assert {"isAuthorized": True} == AppSyncAuthorizerResponse(authorize=True).to_dict() + assert {"isAuthorized": True, "ttlOverride": 0} == AppSyncAuthorizerResponse(authorize=True, ttl=0).to_dict() From a6d61254cddc117409292d12414a0950e60b0def Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 15 Aug 2021 11:02:10 -0700 Subject: [PATCH 05/13] refactor: based on code review --- .../data_classes/appsync_authorizer_event.py | 29 +++++++++++-------- tests/functional/test_data_classes.py | 18 +++++++----- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py index 5a75c4535e2..9c7a4d1570a 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py @@ -67,7 +67,7 @@ class AppSyncAuthorizerResponse: is authorized to make calls to the GraphQL API. If this value is true, execution of the GraphQL API continues. If this value is false, an UnauthorizedException is raised - ttl: Optional[int] + ttl_override: Optional[int] Set the ttlOverride. The number of seconds that the response should be cached for. If no value is returned, the value from the API (if configured) or the default of 300 seconds (five minutes) is used. If this is 0, the response @@ -87,21 +87,26 @@ class AppSyncAuthorizerResponse: def __init__( self, authorize: bool = False, - ttl: Optional[int] = None, + ttl_override: Optional[int] = None, resolver_context: Optional[Dict[str, Any]] = None, denied_fields: Optional[List[str]] = None, ): - self._data: Dict = {"isAuthorized": authorize} + self.authorize = authorize + self.ttl_override = ttl_override + self.denied_fields = denied_fields + self.resolver_context = resolver_context - if ttl is not None: - self._data["ttlOverride"] = ttl + def asdict(self) -> dict: + """Return the response as a dict""" + response: Dict = {"isAuthorized": self.authorize} - if denied_fields: - self._data["deniedFields"] = denied_fields + if self.ttl_override is not None: + response["ttlOverride"] = self.ttl_override - if resolver_context: - self._data["resolverContext"] = resolver_context + if self.denied_fields: + response["deniedFields"] = self.denied_fields - def to_dict(self) -> dict: - """Return the response as a dict""" - return self._data + if self.resolver_context: + response["resolverContext"] = self.resolver_context + + return response diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py index e35cc609316..d8e194ed713 100644 --- a/tests/functional/test_data_classes.py +++ b/tests/functional/test_data_classes.py @@ -1443,14 +1443,16 @@ def test_appsync_authorizer_response(): expected = load_event("appSyncAuthorizerResponse.json") response = AppSyncAuthorizerResponse( authorize=True, - ttl=15, + ttl_override=15, resolver_context={"balance": 100, "name": "Foo Man"}, denied_fields=["Mutation.createEvent"], ) - assert expected == response.to_dict() - - assert {"isAuthorized": False} == AppSyncAuthorizerResponse().to_dict() - assert {"isAuthorized": False} == AppSyncAuthorizerResponse(denied_fields=[]).to_dict() - assert {"isAuthorized": False} == AppSyncAuthorizerResponse(resolver_context={}).to_dict() - assert {"isAuthorized": True} == AppSyncAuthorizerResponse(authorize=True).to_dict() - assert {"isAuthorized": True, "ttlOverride": 0} == AppSyncAuthorizerResponse(authorize=True, ttl=0).to_dict() + assert expected == response.asdict() + + assert {"isAuthorized": False} == AppSyncAuthorizerResponse().asdict() + assert {"isAuthorized": False} == AppSyncAuthorizerResponse(denied_fields=[]).asdict() + assert {"isAuthorized": False} == AppSyncAuthorizerResponse(resolver_context={}).asdict() + assert {"isAuthorized": True} == AppSyncAuthorizerResponse(authorize=True).asdict() + assert {"isAuthorized": True, "ttlOverride": 0} == AppSyncAuthorizerResponse( + authorize=True, ttl_override=0 + ).asdict() From 2e9d1ec28b35e6fe92c4e91eda66ab55bbc8f3eb Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 15 Aug 2021 11:07:45 -0700 Subject: [PATCH 06/13] refactor: based on code review --- .../data_classes/appsync_authorizer_event.py | 22 +++++++++---------- tests/functional/test_data_classes.py | 10 ++++----- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py index 9c7a4d1570a..5f2a9d98c55 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py @@ -8,7 +8,7 @@ class AppSyncAuthorizerEventRequestContext(DictWrapper): @property def api_id(self) -> str: - """AppSync api id""" + """AppSync API ID""" return self["requestContext"]["apiId"] @property @@ -67,7 +67,7 @@ class AppSyncAuthorizerResponse: is authorized to make calls to the GraphQL API. If this value is true, execution of the GraphQL API continues. If this value is false, an UnauthorizedException is raised - ttl_override: Optional[int] + max_age: Optional[int] Set the ttlOverride. The number of seconds that the response should be cached for. If no value is returned, the value from the API (if configured) or the default of 300 seconds (five minutes) is used. If this is 0, the response @@ -75,7 +75,7 @@ class AppSyncAuthorizerResponse: resolver_context: Optional[Dict[str, Any]] A JSON object visible as `$ctx.identity.resolverContext` in resolver templates Warning: The total size of this JSON object must not exceed 5MB. - denied_fields: Optional[List[str]] + deny_fields: Optional[List[str]] A list of which are forcibly changed to null, even if a value was returned from a resolver. Each item is either a fully qualified field ARN in the form of `arn:aws:appsync:us-east-1:111122223333:apis/GraphQLApiId/types/TypeName/fields/FieldName` @@ -87,24 +87,24 @@ class AppSyncAuthorizerResponse: def __init__( self, authorize: bool = False, - ttl_override: Optional[int] = None, + max_age: Optional[int] = None, resolver_context: Optional[Dict[str, Any]] = None, - denied_fields: Optional[List[str]] = None, + deny_fields: Optional[List[str]] = None, ): self.authorize = authorize - self.ttl_override = ttl_override - self.denied_fields = denied_fields + self.max_age = max_age + self.deny_fields = deny_fields self.resolver_context = resolver_context def asdict(self) -> dict: """Return the response as a dict""" response: Dict = {"isAuthorized": self.authorize} - if self.ttl_override is not None: - response["ttlOverride"] = self.ttl_override + if self.max_age is not None: + response["ttlOverride"] = self.max_age - if self.denied_fields: - response["deniedFields"] = self.denied_fields + if self.deny_fields: + response["deniedFields"] = self.deny_fields if self.resolver_context: response["resolverContext"] = self.resolver_context diff --git a/tests/functional/test_data_classes.py b/tests/functional/test_data_classes.py index d8e194ed713..72db667041f 100644 --- a/tests/functional/test_data_classes.py +++ b/tests/functional/test_data_classes.py @@ -1443,16 +1443,14 @@ def test_appsync_authorizer_response(): expected = load_event("appSyncAuthorizerResponse.json") response = AppSyncAuthorizerResponse( authorize=True, - ttl_override=15, + max_age=15, resolver_context={"balance": 100, "name": "Foo Man"}, - denied_fields=["Mutation.createEvent"], + deny_fields=["Mutation.createEvent"], ) assert expected == response.asdict() assert {"isAuthorized": False} == AppSyncAuthorizerResponse().asdict() - assert {"isAuthorized": False} == AppSyncAuthorizerResponse(denied_fields=[]).asdict() + assert {"isAuthorized": False} == AppSyncAuthorizerResponse(deny_fields=[]).asdict() assert {"isAuthorized": False} == AppSyncAuthorizerResponse(resolver_context={}).asdict() assert {"isAuthorized": True} == AppSyncAuthorizerResponse(authorize=True).asdict() - assert {"isAuthorized": True, "ttlOverride": 0} == AppSyncAuthorizerResponse( - authorize=True, ttl_override=0 - ).asdict() + assert {"isAuthorized": False, "ttlOverride": 0} == AppSyncAuthorizerResponse(max_age=0).asdict() From 12a50f4a88488d4cd462d4ef9dcbb934a7304830 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 15 Aug 2021 11:09:34 -0700 Subject: [PATCH 07/13] refactor: based on code review --- .../utilities/data_classes/appsync_authorizer_event.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py index 5f2a9d98c55..4fc590bf53a 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py @@ -23,17 +23,17 @@ def request_id(self) -> str: @property def query_string(self) -> str: - """Graphql query string""" + """GraphQL query string""" return self["requestContext"]["queryString"] @property def operation_name(self) -> Optional[str]: - """Graphql operation name, optional""" + """GraphQL operation name, optional""" return self["requestContext"].get("operationName") @property def variables(self) -> Dict: - """Graphql variables""" + """GraphQL variables""" return self["requestContext"]["variables"] From b8fe2dfcef6adbb4b73bc8d44024a5e4d143d633 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 15 Aug 2021 11:18:22 -0700 Subject: [PATCH 08/13] refactor: clean up docs --- .../utilities/data_classes/appsync_authorizer_event.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py index 4fc590bf53a..b9c0827a9e8 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py @@ -76,12 +76,12 @@ class AppSyncAuthorizerResponse: A JSON object visible as `$ctx.identity.resolverContext` in resolver templates Warning: The total size of this JSON object must not exceed 5MB. deny_fields: Optional[List[str]] - A list of which are forcibly changed to null, even if a value was returned from a resolver. - Each item is either a fully qualified field ARN in the form of + A list of fields that will be set to `null` regardless of the resolver's return. + + A field is either `TypeName.FieldName`, or an ARN such as `arn:aws:appsync:us-east-1:111122223333:apis/GraphQLApiId/types/TypeName/fields/FieldName` - or a short form of TypeName.FieldName. The full ARN form should be used when two APIs - share a lambda function authorizer and there might be ambiguity between common types - and fields between the two APIs. + + Use the full ARN for correctness when sharing a Lambda function authorizer between APIs. """ def __init__( From b698dd553ac39ea30dfa25c6cf255ce19ce1c15d Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 15 Aug 2021 11:25:50 -0700 Subject: [PATCH 09/13] chore: add link for amplify docs --- .../utilities/data_classes/appsync_authorizer_event.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py index b9c0827a9e8..d2fb4ca2f91 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py @@ -44,6 +44,7 @@ class AppSyncAuthorizerEvent(DictWrapper): ------------- - https://aws.amazon.com/blogs/mobile/appsync-lambda-auth/ - https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#aws-lambda-authorization + - https://docs.amplify.aws/lib/graphqlapi/authz/q/platform/js#aws-lambda """ @property From 599ffa6ac6772ef5d88868a0b3fb4b623ad1bb15 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sun, 15 Aug 2021 11:40:24 -0700 Subject: [PATCH 10/13] chore: update docs --- .../utilities/data_classes/appsync_authorizer_event.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py index d2fb4ca2f91..be2e2f38429 100644 --- a/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py +++ b/aws_lambda_powertools/utilities/data_classes/appsync_authorizer_event.py @@ -75,6 +75,9 @@ class AppSyncAuthorizerResponse: is not cached. resolver_context: Optional[Dict[str, Any]] A JSON object visible as `$ctx.identity.resolverContext` in resolver templates + + The resolverContext object only supports key-value pairs. Nested keys are not supported. + Warning: The total size of this JSON object must not exceed 5MB. deny_fields: Optional[List[str]] A list of fields that will be set to `null` regardless of the resolver's return. From 7a225f8a76531bdb023f728fe7d5a16244835a2f Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Mon, 16 Aug 2021 05:58:10 +0000 Subject: [PATCH 11/13] docs: add sample usage for lamda authorizer --- docs/utilities/data_classes.md | 49 ++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index 9616b2b75fd..5c1f0a93062 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -63,6 +63,7 @@ Event Source | Data_class [API Gateway Proxy V2](#api-gateway-proxy-v2) | `APIGatewayProxyEventV2` [Application Load Balancer](#application-load-balancer) | `ALBEvent` [AppSync Resolver](#appsync-resolver) | `AppSyncResolverEvent` +[AppSync Authorizer](#appsync-authorizer) | `AppSyncAuthorizerEvent` [CloudWatch Logs](#cloudwatch-logs) | `CloudWatchLogsEvent` [CodePipeline Job Event](#codepipeline-job) | `CodePipelineJobEvent` [Cognito User Pool](#cognito-user-pool) | Multiple available under `cognito_user_pool_event` @@ -128,6 +129,54 @@ Is it used for Application load balancer event. do_something_with(event.json_body, event.query_string_parameters) ``` +## AppSync Authorizer + +> New in 1.20.0 + +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. +See blog post [Introducing Lambda authorization for AWS AppSync GraphQL APIs](https://aws.amazon.com/blogs/mobile/appsync-lambda-auth/){target="_blank"} +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. + +In this example extract the `requestId` as the `correlation_id` for logging and builds the AppSync authorizer +using the `AppSyncAuthorizerResponse` helper. + +=== "app.py" + + ```python + from typing import Dict + + from aws_lambda_powertools.logging import correlation_paths + from aws_lambda_powertools.logging.logger import Logger + from aws_lambda_powertools.utilities.data_classes.appsync_authorizer_event import ( + AppSyncAuthorizerEvent, + AppSyncAuthorizerResponse, + ) + from aws_lambda_powertools.utilities.data_classes.event_source import event_source + + logger = Logger() + + + def get_user_by_token(token: str): + """Look a user by token""" + + + @logger.inject_lambda_context(correlation_id_path=correlation_paths.APPSYNC_AUTHORIZER) + @event_source(data_class=AppSyncAuthorizerEvent) + def lambda_handler(event: AppSyncAuthorizerEvent, context) -> Dict: + user = get_user_by_token(event.authorization_token) + + if not user: + # No user found, return not authorized + return AppSyncAuthorizerResponse().to_dict() + + return AppSyncAuthorizerResponse( + authorize=True, + resolver_context={"id": user.id}, + # Only allow admins to delete events + deny_fields=None if user.is_admin else ["Mutation.deleteEvent"], + ).asdict() + ``` + ### AppSync Resolver > New in 1.12.0 From 521d48285863ea0f526e71e15e56307ffe112d17 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Mon, 16 Aug 2021 06:05:44 +0000 Subject: [PATCH 12/13] chore: bump ci From 18c09aae15e77e0dbd7b098dbc7b0903b823b12c Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Mon, 16 Aug 2021 06:09:45 +0000 Subject: [PATCH 13/13] chore: bump ci --- docs/utilities/data_classes.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/utilities/data_classes.md b/docs/utilities/data_classes.md index 5c1f0a93062..7591a26288e 100644 --- a/docs/utilities/data_classes.md +++ b/docs/utilities/data_classes.md @@ -137,8 +137,7 @@ Used when building an [AWS_LAMBDA Authorization](https://docs.aws.amazon.com/app See blog post [Introducing Lambda authorization for AWS AppSync GraphQL APIs](https://aws.amazon.com/blogs/mobile/appsync-lambda-auth/){target="_blank"} 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. -In this example extract the `requestId` as the `correlation_id` for logging and builds the AppSync authorizer -using the `AppSyncAuthorizerResponse` helper. +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. === "app.py"