From 8f062bd209d369a02ca41bcad4895884de98e9d5 Mon Sep 17 00:00:00 2001 From: Alexander Prinsier Date: Mon, 13 Apr 2020 23:16:33 +0200 Subject: [PATCH 01/11] Add support for scopes --- lambdarest/__init__.py | 23 +++- tests/test_lambdarest.py | 282 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 304 insertions(+), 1 deletion(-) diff --git a/lambdarest/__init__.py b/lambdarest/__init__.py index 459f671..93fc84e 100755 --- a/lambdarest/__init__.py +++ b/lambdarest/__init__.py @@ -63,6 +63,8 @@ def to_json(self, encoder=json.JSONEncoder, application_load_balancer=False): ) return response +class ScopeMissing(Exception): + pass def __float_cast(value): try: @@ -261,6 +263,13 @@ def inner_lambda_handler(event, context=None): ) error_tuple = ("Validation Error", 400) + except ScopeMissing as error: + error_description = "Permission denied" + logging.warning( + logging_message.format(status_code=403, message=error_description) + ) + error_tuple = (error_description, 403) + except Exception as error: if error_handler: error_handler(error, method_name) @@ -272,7 +281,7 @@ def inner_lambda_handler(event, context=None): application_load_balancer=application_load_balancer ) - def inner_handler(method_name, path="/", schema=None, load_json=True): + def inner_handler(method_name, path="/", schema=None, load_json=True, scopes=[]): if schema and not load_json: raise ValueError("if schema is supplied, load_json needs to be true") @@ -288,6 +297,18 @@ def inner(event, *args, **kwargs): if schema: # jsonschema.validate using given schema validate(json_data, schema, **__validate_kwargs) + + try: + provided_scopes = json.loads(event["requestContext"]["authorizer"]["scopes"]) + except KeyError: + provided_scopes = [] + except json.decoder.JSONDecodeError: + # Ignore passed scopes if it isn't properly json encoded + provided_scopes = [] + for s in scopes: + if s not in provided_scopes: + raise ScopeMissing("Scope " + s + " missing") + return func(event, *args, **kwargs) # if this is a catch all url, make sure that it's setup correctly diff --git a/tests/test_lambdarest.py b/tests/test_lambdarest.py index 2264227..c28d945 100755 --- a/tests/test_lambdarest.py +++ b/tests/test_lambdarest.py @@ -460,3 +460,285 @@ def test_that_json_encoding_body_list(self): self.assertEqual(json.loads(result["body"]), [{"foo": "bar"}]) self.assertEqual(result["headers"], {}) self.assertEqual(result["statusCode"], 200) + + # test matrix for scopes + # Scopes requested + # none | single | multiple + # Scopes provided: + # missing | | A + # none B | | + # single | C | + # multiple D | | E + # invalid F | G | + + def test_scopes_A(self): + post_mock = mock.Mock(return_value=[{"foo": "bar"}]) + self.lambda_handler.handle("post", scopes=['resource.method1', 'resource3.method2'])(post_mock) # decorate mock + result = self.lambda_handler(self.event, self.context) + self.assertEqual( + result, {"body": "Permission denied", "statusCode": 403, "headers": {}} + ) + + def test_scopes_B(self): + event_with_empty_scopes = { + "resource": "/", + "httpMethod": "POST", + "headers": None, + "queryStringParameters": None, + "pathParameters": None, + "stageVariables": None, + "requestContext": { + "accountId": "1234123542134", + "authorizer": { + "scopes": "[]" + }, + "resourceId": "erd49w", + "stage": "test-invoke-stage", + "requestId": "test-invoke-request", + "identity": { + "cognitoIdentityPoolId": None, + "accountId": "23424534543", + "cognitoIdentityId": None, + "caller": "asdfasdfasfdasfdas", + "apiKey": "asdfasdfasdfas", + "sourceIp": "127.0.0.1", + "accessKey": "asdfasdfasdfasfd", + "cognitoAuthenticationType": None, + "cognitoAuthenticationProvider": None, + "userArn": "arn:aws:iam::123214323", + "userAgent": "Apache-HttpClient/4.5.x (Java/1.8.0_102)", + "user": "asdfsadsfads", + }, + "resourcePath": "/test", + "httpMethod": "POST", + "apiId": "90o718c6bk", + }, + "body": None, + "isBase64Encoded": False, + } + post_mock = mock.Mock(return_value=[{"foo": "bar"}]) + self.lambda_handler.handle("post", scopes=['resource.method1', 'resource3.method2'])(post_mock) # decorate mock + result = self.lambda_handler(event_with_empty_scopes, self.context) + self.assertEqual( + result, {"body": "Permission denied", "statusCode": 403, "headers": {}} + ) + + def test_scopes_C(self): + event_with_single_scope = { + "resource": "/", + "httpMethod": "POST", + "headers": None, + "queryStringParameters": None, + "pathParameters": None, + "stageVariables": None, + "requestContext": { + "accountId": "1234123542134", + "authorizer": { + "scopes": '["resource1.method2"]' + }, + "resourceId": "erd49w", + "stage": "test-invoke-stage", + "requestId": "test-invoke-request", + "identity": { + "cognitoIdentityPoolId": None, + "accountId": "23424534543", + "cognitoIdentityId": None, + "caller": "asdfasdfasfdasfdas", + "apiKey": "asdfasdfasdfas", + "sourceIp": "127.0.0.1", + "accessKey": "asdfasdfasdfasfd", + "cognitoAuthenticationType": None, + "cognitoAuthenticationProvider": None, + "userArn": "arn:aws:iam::123214323", + "userAgent": "Apache-HttpClient/4.5.x (Java/1.8.0_102)", + "user": "asdfsadsfads", + }, + "resourcePath": "/test", + "httpMethod": "POST", + "apiId": "90o718c6bk", + }, + "body": None, + "isBase64Encoded": False, + } + post_mock = mock.Mock(return_value=[{"foo": "bar"}]) + self.lambda_handler.handle("post", scopes=['resource1.method2'])(post_mock) # decorate mock + result = self.lambda_handler(event_with_single_scope, self.context) + self.assertEqual( + result, {"body": '[{"foo": "bar"}]', "statusCode": 200, "headers": {}} + ) + + def test_scopes_D(self): + event_with_multiple_scopes = { + "resource": "/", + "httpMethod": "POST", + "headers": None, + "queryStringParameters": None, + "pathParameters": None, + "stageVariables": None, + "requestContext": { + "accountId": "1234123542134", + "authorizer": { + "scopes": '["resource1.method2", "resouce2.method3"]' + }, + "resourceId": "erd49w", + "stage": "test-invoke-stage", + "requestId": "test-invoke-request", + "identity": { + "cognitoIdentityPoolId": None, + "accountId": "23424534543", + "cognitoIdentityId": None, + "caller": "asdfasdfasfdasfdas", + "apiKey": "asdfasdfasdfas", + "sourceIp": "127.0.0.1", + "accessKey": "asdfasdfasdfasfd", + "cognitoAuthenticationType": None, + "cognitoAuthenticationProvider": None, + "userArn": "arn:aws:iam::123214323", + "userAgent": "Apache-HttpClient/4.5.x (Java/1.8.0_102)", + "user": "asdfsadsfads", + }, + "resourcePath": "/test", + "httpMethod": "POST", + "apiId": "90o718c6bk", + }, + "body": None, + "isBase64Encoded": False, + } + post_mock = mock.Mock(return_value=[{"foo": "bar"}]) + self.lambda_handler.handle("post", scopes=[])(post_mock) # decorate mock + result = self.lambda_handler(event_with_multiple_scopes, self.context) + self.assertEqual( + result, {"body": '[{"foo": "bar"}]', "statusCode": 200, "headers": {}} + ) + + def test_scopes_E(self): + event_with_multiple_scopes = { + "resource": "/", + "httpMethod": "POST", + "headers": None, + "queryStringParameters": None, + "pathParameters": None, + "stageVariables": None, + "requestContext": { + "accountId": "1234123542134", + "authorizer": { + "scopes": '["resource1.method2", "resouce2.method3"]' + }, + "resourceId": "erd49w", + "stage": "test-invoke-stage", + "requestId": "test-invoke-request", + "identity": { + "cognitoIdentityPoolId": None, + "accountId": "23424534543", + "cognitoIdentityId": None, + "caller": "asdfasdfasfdasfdas", + "apiKey": "asdfasdfasdfas", + "sourceIp": "127.0.0.1", + "accessKey": "asdfasdfasdfasfd", + "cognitoAuthenticationType": None, + "cognitoAuthenticationProvider": None, + "userArn": "arn:aws:iam::123214323", + "userAgent": "Apache-HttpClient/4.5.x (Java/1.8.0_102)", + "user": "asdfsadsfads", + }, + "resourcePath": "/test", + "httpMethod": "POST", + "apiId": "90o718c6bk", + }, + "body": None, + "isBase64Encoded": False, + } + post_mock = mock.Mock(return_value=[{"foo": "bar"}]) + self.lambda_handler.handle("post", scopes=["resouce2.method3", "resouce3.method1"])(post_mock) # decorate mock + result = self.lambda_handler(event_with_multiple_scopes, self.context) + self.assertEqual( + result, {"body": "Permission denied", "statusCode": 403, "headers": {}} + ) + + def test_scopes_F(self): + event_with_invalid_scopes = { + "resource": "/", + "httpMethod": "POST", + "headers": None, + "queryStringParameters": None, + "pathParameters": None, + "stageVariables": None, + "requestContext": { + "accountId": "1234123542134", + "authorizer": { + "scopes": "{[invalid json}" + }, + "resourceId": "erd49w", + "stage": "test-invoke-stage", + "requestId": "test-invoke-request", + "identity": { + "cognitoIdentityPoolId": None, + "accountId": "23424534543", + "cognitoIdentityId": None, + "caller": "asdfasdfasfdasfdas", + "apiKey": "asdfasdfasdfas", + "sourceIp": "127.0.0.1", + "accessKey": "asdfasdfasdfasfd", + "cognitoAuthenticationType": None, + "cognitoAuthenticationProvider": None, + "userArn": "arn:aws:iam::123214323", + "userAgent": "Apache-HttpClient/4.5.x (Java/1.8.0_102)", + "user": "asdfsadsfads", + }, + "resourcePath": "/test", + "httpMethod": "POST", + "apiId": "90o718c6bk", + }, + "body": None, + "isBase64Encoded": False, + } + post_mock = mock.Mock(return_value=[{"foo": "bar"}]) + self.lambda_handler.handle("post", scopes=[])(post_mock) # decorate mock + result = self.lambda_handler(event_with_invalid_scopes, self.context) + self.assertEqual( + result, {"body": '[{"foo": "bar"}]', "statusCode": 200, "headers": {}} + ) + + def test_scopes_G(self): + event_with_invalid_scopes = { + "resource": "/", + "httpMethod": "POST", + "headers": None, + "queryStringParameters": None, + "pathParameters": None, + "stageVariables": None, + "requestContext": { + "accountId": "1234123542134", + "authorizer": { + "scopes": "{[invalid json}" + }, + "resourceId": "erd49w", + "stage": "test-invoke-stage", + "requestId": "test-invoke-request", + "identity": { + "cognitoIdentityPoolId": None, + "accountId": "23424534543", + "cognitoIdentityId": None, + "caller": "asdfasdfasfdasfdas", + "apiKey": "asdfasdfasdfas", + "sourceIp": "127.0.0.1", + "accessKey": "asdfasdfasdfasfd", + "cognitoAuthenticationType": None, + "cognitoAuthenticationProvider": None, + "userArn": "arn:aws:iam::123214323", + "userAgent": "Apache-HttpClient/4.5.x (Java/1.8.0_102)", + "user": "asdfsadsfads", + }, + "resourcePath": "/test", + "httpMethod": "POST", + "apiId": "90o718c6bk", + }, + "body": None, + "isBase64Encoded": False, + } + post_mock = mock.Mock(return_value=[{"foo": "bar"}]) + self.lambda_handler.handle("post", scopes=["resource1.method2"])(post_mock) # decorate mock + result = self.lambda_handler(event_with_invalid_scopes, self.context) + self.assertEqual( + result, {"body": "Permission denied", "statusCode": 403, "headers": {}} + ) \ No newline at end of file From dc9061cbecb1da410c0e65ac1b4fe2107001b589 Mon Sep 17 00:00:00 2001 From: Alexander Prinsier Date: Mon, 13 Apr 2020 23:29:24 +0200 Subject: [PATCH 02/11] Update README.md for scopes --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index 230d913..0f43fb8 100755 --- a/README.md +++ b/README.md @@ -160,6 +160,7 @@ def my_own_get(event): return {"this": "will be json dumped"} + ##### TEST ##### @@ -243,6 +244,41 @@ result = lambda_handler(event=input_event) assert result == {"body": '{"path": "bar/baz"}', "statusCode": 200, "headers":{}} ``` +### Scopes + +If you're using a Lambda authorizer, you can pass authorization scopes as input into your Lambda function. + +This is useful when using the API Gateway with a Lambda authorizer and have the Lambda authorizer return in a scopes json object the permissions (scopes) the caller has access to. In your Lambda function you can specify what scopes the caller should have to call that function. If the requested scope was not provided by the Lambda authorizer, a 403 error code is given. + +```python +from lambdarest import lambda_handler + +@lambda_handler.handle("get", scopes="myresource.read") +def my_own_get(event): + return {"this": "will be json dumped"} + +##### TEST (permission granted) ##### + +input_event = { + "body": '{}', + "httpMethod": "GET", + "resource": "/" +} +result = lambda_handler(event=input_event) +assert result == {"body": '{"this": "will be json dumped"}', "statusCode": 200, "headers":{}} +``` + +##### TEST (permission denied) ##### + +input_event = { + "body": '{}', + "httpMethod": "GET", + "resource": "/" +} +result = lambda_handler(event=input_event) +assert result == {"body": "Permission denied", "statusCode": 403, "headers":{}} +``` + ## Use it with AWS Application Load Balancer In order to use it with Application Load Balancer you need to create your own lambda_handler and not use the singleton: From afd59b5e701b46c7acb4ec39c1a334c92d72b550 Mon Sep 17 00:00:00 2001 From: Alexander Prinsier Date: Mon, 13 Apr 2020 23:30:38 +0200 Subject: [PATCH 03/11] Correct README.md for scopes --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 0f43fb8..4cff2f5 100755 --- a/README.md +++ b/README.md @@ -266,7 +266,6 @@ input_event = { } result = lambda_handler(event=input_event) assert result == {"body": '{"this": "will be json dumped"}', "statusCode": 200, "headers":{}} -``` ##### TEST (permission denied) ##### From 423d58f625f6a91f316fdd55f046c99bafae031e Mon Sep 17 00:00:00 2001 From: Alexander Prinsier Date: Mon, 13 Apr 2020 23:34:32 +0200 Subject: [PATCH 04/11] Improve README.md for scopes --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 4cff2f5..f9e71d1 100755 --- a/README.md +++ b/README.md @@ -250,6 +250,8 @@ If you're using a Lambda authorizer, you can pass authorization scopes as input This is useful when using the API Gateway with a Lambda authorizer and have the Lambda authorizer return in a scopes json object the permissions (scopes) the caller has access to. In your Lambda function you can specify what scopes the caller should have to call that function. If the requested scope was not provided by the Lambda authorizer, a 403 error code is given. +The API gateway has the limitation it can only pass primitive data types from a Lambda authorizer function. The scopes list therefore needs to be json encoded by the authorizer function. + ```python from lambdarest import lambda_handler @@ -263,6 +265,11 @@ input_event = { "body": '{}', "httpMethod": "GET", "resource": "/" + "requestContext": { + "authorizer": { + "scopes": '["myresource.read"]' + } + } } result = lambda_handler(event=input_event) assert result == {"body": '{"this": "will be json dumped"}', "statusCode": 200, "headers":{}} From 05062463ff66c08188e3091985458510200d1a02 Mon Sep 17 00:00:00 2001 From: Alexander Prinsier Date: Mon, 13 Apr 2020 23:35:31 +0200 Subject: [PATCH 05/11] Improve README.md for scopes --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f9e71d1..8496f9c 100755 --- a/README.md +++ b/README.md @@ -264,8 +264,8 @@ def my_own_get(event): input_event = { "body": '{}', "httpMethod": "GET", - "resource": "/" - "requestContext": { + "resource": "/", + "requestContext": { "authorizer": { "scopes": '["myresource.read"]' } From 540d436564c420631d0faf38d324fd5b9b6a3d9f Mon Sep 17 00:00:00 2001 From: Alexander Prinsier Date: Mon, 13 Apr 2020 23:47:27 +0200 Subject: [PATCH 06/11] Improve README.md for scopes --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 8496f9c..bbc0c89 100755 --- a/README.md +++ b/README.md @@ -160,7 +160,6 @@ def my_own_get(event): return {"this": "will be json dumped"} - ##### TEST ##### From 4996df11acf0369f153df702578d101cdd23ee97 Mon Sep 17 00:00:00 2001 From: Alexander Prinsier Date: Mon, 13 Apr 2020 23:48:52 +0200 Subject: [PATCH 07/11] Add missing newline at end of file --- tests/test_lambdarest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_lambdarest.py b/tests/test_lambdarest.py index c28d945..7e6518d 100755 --- a/tests/test_lambdarest.py +++ b/tests/test_lambdarest.py @@ -741,4 +741,4 @@ def test_scopes_G(self): result = self.lambda_handler(event_with_invalid_scopes, self.context) self.assertEqual( result, {"body": "Permission denied", "statusCode": 403, "headers": {}} - ) \ No newline at end of file + ) From 9d04f71aa3a30d3520fbff5f8ec90ba87191942d Mon Sep 17 00:00:00 2001 From: Alexander Prinsier Date: Tue, 14 Apr 2020 00:33:07 +0200 Subject: [PATCH 08/11] Fix error in scopes test in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bbc0c89..b8d1b9d 100755 --- a/README.md +++ b/README.md @@ -254,7 +254,7 @@ The API gateway has the limitation it can only pass primitive data types from a ```python from lambdarest import lambda_handler -@lambda_handler.handle("get", scopes="myresource.read") +@lambda_handler.handle("get", scopes=["myresource.read"]) def my_own_get(event): return {"this": "will be json dumped"} From b43413f2b94abc86608f3ff39653386e7e23286b Mon Sep 17 00:00:00 2001 From: Alexander Prinsier Date: Tue, 14 Apr 2020 00:41:30 +0200 Subject: [PATCH 09/11] Code formatting improvements --- lambdarest/__init__.py | 6 ++++- tests/test_lambdarest.py | 50 ++++++++++++++++++++++------------------ 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/lambdarest/__init__.py b/lambdarest/__init__.py index 93fc84e..6ee4f17 100755 --- a/lambdarest/__init__.py +++ b/lambdarest/__init__.py @@ -63,9 +63,11 @@ def to_json(self, encoder=json.JSONEncoder, application_load_balancer=False): ) return response + class ScopeMissing(Exception): pass + def __float_cast(value): try: return float(value) @@ -299,7 +301,9 @@ def inner(event, *args, **kwargs): validate(json_data, schema, **__validate_kwargs) try: - provided_scopes = json.loads(event["requestContext"]["authorizer"]["scopes"]) + provided_scopes = json.loads( + event["requestContext"]["authorizer"]["scopes"] + ) except KeyError: provided_scopes = [] except json.decoder.JSONDecodeError: diff --git a/tests/test_lambdarest.py b/tests/test_lambdarest.py index 7e6518d..91501d7 100755 --- a/tests/test_lambdarest.py +++ b/tests/test_lambdarest.py @@ -473,7 +473,11 @@ def test_that_json_encoding_body_list(self): def test_scopes_A(self): post_mock = mock.Mock(return_value=[{"foo": "bar"}]) - self.lambda_handler.handle("post", scopes=['resource.method1', 'resource3.method2'])(post_mock) # decorate mock + self.lambda_handler.handle( + "post", scopes=["resource.method1", "resource3.method2"] + )( + post_mock + ) # decorate mock result = self.lambda_handler(self.event, self.context) self.assertEqual( result, {"body": "Permission denied", "statusCode": 403, "headers": {}} @@ -489,9 +493,7 @@ def test_scopes_B(self): "stageVariables": None, "requestContext": { "accountId": "1234123542134", - "authorizer": { - "scopes": "[]" - }, + "authorizer": {"scopes": "[]"}, "resourceId": "erd49w", "stage": "test-invoke-stage", "requestId": "test-invoke-request", @@ -517,7 +519,11 @@ def test_scopes_B(self): "isBase64Encoded": False, } post_mock = mock.Mock(return_value=[{"foo": "bar"}]) - self.lambda_handler.handle("post", scopes=['resource.method1', 'resource3.method2'])(post_mock) # decorate mock + self.lambda_handler.handle( + "post", scopes=["resource.method1", "resource3.method2"] + )( + post_mock + ) # decorate mock result = self.lambda_handler(event_with_empty_scopes, self.context) self.assertEqual( result, {"body": "Permission denied", "statusCode": 403, "headers": {}} @@ -533,9 +539,7 @@ def test_scopes_C(self): "stageVariables": None, "requestContext": { "accountId": "1234123542134", - "authorizer": { - "scopes": '["resource1.method2"]' - }, + "authorizer": {"scopes": '["resource1.method2"]'}, "resourceId": "erd49w", "stage": "test-invoke-stage", "requestId": "test-invoke-request", @@ -561,7 +565,9 @@ def test_scopes_C(self): "isBase64Encoded": False, } post_mock = mock.Mock(return_value=[{"foo": "bar"}]) - self.lambda_handler.handle("post", scopes=['resource1.method2'])(post_mock) # decorate mock + self.lambda_handler.handle("post", scopes=["resource1.method2"])( + post_mock + ) # decorate mock result = self.lambda_handler(event_with_single_scope, self.context) self.assertEqual( result, {"body": '[{"foo": "bar"}]', "statusCode": 200, "headers": {}} @@ -577,9 +583,7 @@ def test_scopes_D(self): "stageVariables": None, "requestContext": { "accountId": "1234123542134", - "authorizer": { - "scopes": '["resource1.method2", "resouce2.method3"]' - }, + "authorizer": {"scopes": '["resource1.method2", "resouce2.method3"]'}, "resourceId": "erd49w", "stage": "test-invoke-stage", "requestId": "test-invoke-request", @@ -621,9 +625,7 @@ def test_scopes_E(self): "stageVariables": None, "requestContext": { "accountId": "1234123542134", - "authorizer": { - "scopes": '["resource1.method2", "resouce2.method3"]' - }, + "authorizer": {"scopes": '["resource1.method2", "resouce2.method3"]'}, "resourceId": "erd49w", "stage": "test-invoke-stage", "requestId": "test-invoke-request", @@ -649,7 +651,11 @@ def test_scopes_E(self): "isBase64Encoded": False, } post_mock = mock.Mock(return_value=[{"foo": "bar"}]) - self.lambda_handler.handle("post", scopes=["resouce2.method3", "resouce3.method1"])(post_mock) # decorate mock + self.lambda_handler.handle( + "post", scopes=["resouce2.method3", "resouce3.method1"] + )( + post_mock + ) # decorate mock result = self.lambda_handler(event_with_multiple_scopes, self.context) self.assertEqual( result, {"body": "Permission denied", "statusCode": 403, "headers": {}} @@ -665,9 +671,7 @@ def test_scopes_F(self): "stageVariables": None, "requestContext": { "accountId": "1234123542134", - "authorizer": { - "scopes": "{[invalid json}" - }, + "authorizer": {"scopes": "{[invalid json}"}, "resourceId": "erd49w", "stage": "test-invoke-stage", "requestId": "test-invoke-request", @@ -709,9 +713,7 @@ def test_scopes_G(self): "stageVariables": None, "requestContext": { "accountId": "1234123542134", - "authorizer": { - "scopes": "{[invalid json}" - }, + "authorizer": {"scopes": "{[invalid json}"}, "resourceId": "erd49w", "stage": "test-invoke-stage", "requestId": "test-invoke-request", @@ -737,7 +739,9 @@ def test_scopes_G(self): "isBase64Encoded": False, } post_mock = mock.Mock(return_value=[{"foo": "bar"}]) - self.lambda_handler.handle("post", scopes=["resource1.method2"])(post_mock) # decorate mock + self.lambda_handler.handle("post", scopes=["resource1.method2"])( + post_mock + ) # decorate mock result = self.lambda_handler(event_with_invalid_scopes, self.context) self.assertEqual( result, {"body": "Permission denied", "statusCode": 403, "headers": {}} From 9799bac2d5ca4bf3db5cb702f4a287b7f12fbaa6 Mon Sep 17 00:00:00 2001 From: Alexander Prinsier Date: Tue, 14 Apr 2020 01:24:33 +0200 Subject: [PATCH 10/11] Correct scopes test in README.md --- README.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b8d1b9d..e214d30 100755 --- a/README.md +++ b/README.md @@ -251,19 +251,21 @@ This is useful when using the API Gateway with a Lambda authorizer and have the The API gateway has the limitation it can only pass primitive data types from a Lambda authorizer function. The scopes list therefore needs to be json encoded by the authorizer function. +To use this, add a scopes attribute to the handler with the list of scopes your function requires. They will be verified from the requestContext.authorizer.scopes attribute from the Lambda authorizer. + ```python from lambdarest import lambda_handler -@lambda_handler.handle("get", scopes=["myresource.read"]) +@lambda_handler.handle("get", path="/private1", scopes=["myresource.read"]) def my_own_get(event): return {"this": "will be json dumped"} -##### TEST (permission granted) ##### +##### TEST ##### input_event = { "body": '{}', "httpMethod": "GET", - "resource": "/", + "resource": "/private1", "requestContext": { "authorizer": { "scopes": '["myresource.read"]' @@ -272,15 +274,24 @@ input_event = { } result = lambda_handler(event=input_event) assert result == {"body": '{"this": "will be json dumped"}', "statusCode": 200, "headers":{}} - -##### TEST (permission denied) ##### +``` +When no scopes are provided by the authorizer but are still requested by your function, a permission denied error is returned. +```python +from lambdarest import lambda_handler + +@lambda_handler.handle("get", path="/private2", scopes=["myresource.read"]) +def my_own_get(event): + return {"this": "will be json dumped"} + +##### TEST ##### input_event = { "body": '{}', "httpMethod": "GET", - "resource": "/" + "resource": "/private2" } result = lambda_handler(event=input_event) +print(result) assert result == {"body": "Permission denied", "statusCode": 403, "headers":{}} ``` From d1b99910f78a52238c9fbea821706e051d679cd0 Mon Sep 17 00:00:00 2001 From: sloev Date: Tue, 14 Apr 2020 10:36:23 +0200 Subject: [PATCH 11/11] dont use lists in signatures as they are mutable --- lambdarest/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lambdarest/__init__.py b/lambdarest/__init__.py index 6ee4f17..e95ddfc 100755 --- a/lambdarest/__init__.py +++ b/lambdarest/__init__.py @@ -283,7 +283,7 @@ def inner_lambda_handler(event, context=None): application_load_balancer=application_load_balancer ) - def inner_handler(method_name, path="/", schema=None, load_json=True, scopes=[]): + def inner_handler(method_name, path="/", schema=None, load_json=True, scopes=None): if schema and not load_json: raise ValueError("if schema is supplied, load_json needs to be true") @@ -309,9 +309,10 @@ def inner(event, *args, **kwargs): except json.decoder.JSONDecodeError: # Ignore passed scopes if it isn't properly json encoded provided_scopes = [] - for s in scopes: - if s not in provided_scopes: - raise ScopeMissing("Scope " + s + " missing") + + for scope in scopes or []: + if scope not in provided_scopes: + raise ScopeMissing("Scope: '{}' is missing".format(scope)) return func(event, *args, **kwargs)