From 4724886dbb11fd4e01bda7ef0ad83129a7d3aa6d Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sat, 31 Oct 2020 01:10:14 -0700 Subject: [PATCH 1/2] feat(data_classes): API Gateway V2 IAM and Lambda Update to support IAM and Lambda authorizer for API Gateway V2 trigger events --- .../data_classes/api_gateway_proxy_event.py | 55 +++++++++++++++++ tests/events/apiGatewayProxyV2IamEvent.json | 60 +++++++++++++++++++ ...piGatewayProxyV2LambdaAuthorizerEvent.json | 50 ++++++++++++++++ .../functional/test_lambda_trigger_events.py | 26 ++++++++ 4 files changed, 191 insertions(+) create mode 100644 tests/events/apiGatewayProxyV2IamEvent.json create mode 100644 tests/events/apiGatewayProxyV2LambdaAuthorizerEvent.json diff --git a/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py b/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py index a7d75eadaa6..2adf401f33b 100644 --- a/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py +++ b/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py @@ -267,6 +267,52 @@ def user_agent(self) -> str: return self["requestContext"]["http"]["userAgent"] +class RequestContextV2AuthorizerIam(DictWrapper): + @property + def access_key(self) -> Optional[str]: + return self.get("accessKey") + + @property + def account_id(self) -> Optional[str]: + """The AWS account ID associated with the request.""" + return self.get("accountId") + + @property + def caller_id(self) -> Optional[str]: + """The principal identifier of the caller making the request.""" + return self.get("callerId") + + @property + def cognito_amr(self) -> Optional[List[str]]: + return self["cognitoIdentity"].get("amr") + + @property + def cognito_identity_id(self) -> Optional[str]: + """The Amazon Cognito identity ID of the caller making the request. + Available only if the request was signed with Amazon Cognito credentials.""" + return self["cognitoIdentity"].get("identityId") + + @property + def cognito_identity_pool_id(self) -> Optional[str]: + """The Amazon Cognito identity pool ID of the caller making the request. + Available only if the request was signed with Amazon Cognito credentials.""" + return self["cognitoIdentity"].get("identityPoolId") + + @property + def principal_org_id(self) -> Optional[str]: + """The AWS organization ID.""" + return self.get("principalOrgId") + + @property + def user_arn(self) -> Optional[str]: + """The Amazon Resource Name (ARN) of the effective user identified after authentication.""" + return self.get("userArn") + + @property + def user_id(self) -> Optional[str]: + return self.get("userId") + + class RequestContextV2Authorizer(DictWrapper): @property def jwt_claim(self) -> Dict[str, Any]: @@ -276,6 +322,15 @@ def jwt_claim(self) -> Dict[str, Any]: def jwt_scopes(self) -> List[str]: return self["jwt"]["scopes"] + @property + def get_lambda(self) -> Optional[Dict[str, Any]]: + return self.get("lambda") + + @property + def iam(self) -> Optional[RequestContextV2AuthorizerIam]: + iam = self.get("iam") + return None if iam is None else RequestContextV2AuthorizerIam(iam) + class RequestContextV2(DictWrapper): @property diff --git a/tests/events/apiGatewayProxyV2IamEvent.json b/tests/events/apiGatewayProxyV2IamEvent.json new file mode 100644 index 00000000000..73d50d78a4a --- /dev/null +++ b/tests/events/apiGatewayProxyV2IamEvent.json @@ -0,0 +1,60 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "pathParameters": { + "proxy": "hello/world" + }, + "requestContext": { + "routeKey": "$default", + "accountId": "123456789012", + "stage": "$default", + "requestId": "id", + "authorizer": { + "iam": { + "accessKey": "ARIA2ZJZYVUEREEIHAKY", + "accountId": "1234567890", + "callerId": "AROA7ZJZYVRE7C3DUXHH6:CognitoIdentityCredentials", + "cognitoIdentity": { + "amr" : ["foo"], + "identityId": "us-east-1:3f291106-8703-466b-8f2b-3ecee1ca56ce", + "identityPoolId": "us-east-1:4f291106-8703-466b-8f2b-3ecee1ca56ce" + }, + "principalOrgId": "AwsOrgId", + "userArn": "arn:aws:iam::1234567890:user/Admin", + "userId": "AROA2ZJZYVRE7Y3TUXHH6" + } + }, + "apiId": "api-id", + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "time": "12/Mar/2020:19:03:58+0000", + "timeEpoch": 1583348638390, + "http": { + "method": "GET", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "IP", + "userAgent": "agent" + } + }, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + }, + "body": "{\r\n\t\"a\": 1\r\n}", + "isBase64Encoded": false +} diff --git a/tests/events/apiGatewayProxyV2LambdaAuthorizerEvent.json b/tests/events/apiGatewayProxyV2LambdaAuthorizerEvent.json new file mode 100644 index 00000000000..75d1574f854 --- /dev/null +++ b/tests/events/apiGatewayProxyV2LambdaAuthorizerEvent.json @@ -0,0 +1,50 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "pathParameters": { + "proxy": "hello/world" + }, + "requestContext": { + "routeKey": "$default", + "accountId": "123456789012", + "stage": "$default", + "requestId": "id", + "authorizer": { + "lambda": { + "key": "value" + } + }, + "apiId": "api-id", + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "time": "12/Mar/2020:19:03:58+0000", + "timeEpoch": 1583348638390, + "http": { + "method": "GET", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "IP", + "userAgent": "agent" + } + }, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + }, + "body": "{\r\n\t\"a\": 1\r\n}", + "isBase64Encoded": false +} diff --git a/tests/functional/test_lambda_trigger_events.py b/tests/functional/test_lambda_trigger_events.py index cc104ec8f04..6cfdbc765b1 100644 --- a/tests/functional/test_lambda_trigger_events.py +++ b/tests/functional/test_lambda_trigger_events.py @@ -639,6 +639,32 @@ def test_api_gateway_proxy_v2_event(): assert event.stage_variables == event["stageVariables"] +def test_api_gateway_proxy_v2_lambda_authorizer_event(): + event = APIGatewayProxyEventV2(load_event("apiGatewayProxyV2LambdaAuthorizerEvent.json")) + + request_context = event.request_context + assert request_context is not None + lambda_props = request_context.authorizer.get_lambda + assert lambda_props is not None + assert lambda_props["key"] == "value" + + +def test_api_gateway_proxy_v2_iam_event(): + event = APIGatewayProxyEventV2(load_event("apiGatewayProxyV2IamEvent.json")) + + iam = event.request_context.authorizer.iam + assert iam is not None + assert iam.access_key == "ARIA2ZJZYVUEREEIHAKY" + assert iam.account_id == "1234567890" + assert iam.caller_id == "AROA7ZJZYVRE7C3DUXHH6:CognitoIdentityCredentials" + assert iam.cognito_amr == ["foo"] + assert iam.cognito_identity_id == "us-east-1:3f291106-8703-466b-8f2b-3ecee1ca56ce" + assert iam.cognito_identity_pool_id == "us-east-1:4f291106-8703-466b-8f2b-3ecee1ca56ce" + assert iam.principal_org_id == "AwsOrgId" + assert iam.user_arn == "arn:aws:iam::1234567890:user/Admin" + assert iam.user_id == "AROA2ZJZYVRE7Y3TUXHH6" + + def test_base_proxy_event_get_query_string_value(): default_value = "default" set_value = "value" From 992683294b26ce761c6870c243b5f56303760f9a Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Mon, 2 Nov 2020 02:44:21 -0800 Subject: [PATCH 2/2] chore(docs): Add some of the missing docstrings For , this refers to the context parameters returned by the lambda authorizer (optional) For , this refers to the IAM authorization details --- .../utilities/data_classes/api_gateway_proxy_event.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py b/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py index 2adf401f33b..a0f2b8be8a1 100644 --- a/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py +++ b/aws_lambda_powertools/utilities/data_classes/api_gateway_proxy_event.py @@ -270,6 +270,7 @@ def user_agent(self) -> str: class RequestContextV2AuthorizerIam(DictWrapper): @property def access_key(self) -> Optional[str]: + """The IAM user access key associated with the request.""" return self.get("accessKey") @property @@ -284,6 +285,8 @@ def caller_id(self) -> Optional[str]: @property def cognito_amr(self) -> Optional[List[str]]: + """This represents how the user was authenticated. + AMR stands for Authentication Methods References as per the openid spec""" return self["cognitoIdentity"].get("amr") @property @@ -310,6 +313,7 @@ def user_arn(self) -> Optional[str]: @property def user_id(self) -> Optional[str]: + """The IAM user ID of the effective user identified after authentication.""" return self.get("userId") @@ -324,10 +328,12 @@ def jwt_scopes(self) -> List[str]: @property def get_lambda(self) -> Optional[Dict[str, Any]]: + """Lambda authorization context details""" return self.get("lambda") @property def iam(self) -> Optional[RequestContextV2AuthorizerIam]: + """IAM authorization details used for making the request.""" iam = self.get("iam") return None if iam is None else RequestContextV2AuthorizerIam(iam)