Skip to content

Commit d50b959

Browse files
klmzpraneetap
authored andcommitted
feat: add authorizationscopes (#917)
1 parent 0976434 commit d50b959

15 files changed

+2724
-9
lines changed

samtranslator/model/api/api_generator.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,8 @@ def _add_auth(self):
444444
if authorizers:
445445
swagger_editor.add_authorizers_security_definitions(authorizers)
446446
self._set_default_authorizer(swagger_editor, authorizers, auth_properties.DefaultAuthorizer,
447-
auth_properties.AddDefaultAuthorizerToCorsPreflight)
447+
auth_properties.AddDefaultAuthorizerToCorsPreflight,
448+
auth_properties.Authorizers)
448449

449450
if auth_properties.ApiKeyRequired:
450451
swagger_editor.add_apikey_security_definition()
@@ -586,7 +587,8 @@ def _get_authorizers(self, authorizers_config, default_authorizer=None):
586587
function_arn=authorizer.get('FunctionArn'),
587588
identity=authorizer.get('Identity'),
588589
function_payload_type=authorizer.get('FunctionPayloadType'),
589-
function_invoke_role=authorizer.get('FunctionInvokeRole')
590+
function_invoke_role=authorizer.get('FunctionInvokeRole'),
591+
authorization_scopes=authorizer.get("AuthorizationScopes")
590592
)
591593
return authorizers
592594

@@ -636,7 +638,7 @@ def _construct_authorizer_lambda_permission(self):
636638
return permissions
637639

638640
def _set_default_authorizer(self, swagger_editor, authorizers, default_authorizer,
639-
add_default_auth_to_preflight=True):
641+
add_default_auth_to_preflight=True, api_authorizers=None):
640642
if not default_authorizer:
641643
return
642644

@@ -646,7 +648,8 @@ def _set_default_authorizer(self, swagger_editor, authorizers, default_authorize
646648

647649
for path in swagger_editor.iter_on_path():
648650
swagger_editor.set_path_default_authorizer(path, default_authorizer, authorizers=authorizers,
649-
add_default_auth_to_preflight=add_default_auth_to_preflight)
651+
add_default_auth_to_preflight=add_default_auth_to_preflight,
652+
api_authorizers=api_authorizers)
650653

651654
def _set_default_apikey_required(self, swagger_editor):
652655
for path in swagger_editor.iter_on_path():

samtranslator/model/apigateway.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ class ApiGatewayAuthorizer(object):
181181
_VALID_FUNCTION_PAYLOAD_TYPES = [None, 'TOKEN', 'REQUEST']
182182

183183
def __init__(self, api_logical_id=None, name=None, user_pool_arn=None, function_arn=None, identity=None,
184-
function_payload_type=None, function_invoke_role=None, is_aws_iam_authorizer=False):
184+
function_payload_type=None, function_invoke_role=None, is_aws_iam_authorizer=False,
185+
authorization_scopes=[]):
185186
if function_payload_type not in ApiGatewayAuthorizer._VALID_FUNCTION_PAYLOAD_TYPES:
186187
raise InvalidResourceException(api_logical_id, name + " Authorizer has invalid "
187188
"'FunctionPayloadType': " + function_payload_type)
@@ -198,6 +199,7 @@ def __init__(self, api_logical_id=None, name=None, user_pool_arn=None, function_
198199
self.function_payload_type = function_payload_type
199200
self.function_invoke_role = function_invoke_role
200201
self.is_aws_iam_authorizer = is_aws_iam_authorizer
202+
self.authorization_scopes = authorization_scopes
201203

202204
def _is_missing_identity_source(self, identity):
203205
if not identity:

samtranslator/model/eventsources/push.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,13 @@ def _add_swagger_integration(self, api, function):
635635
'is only a valid value when a DefaultAuthorizer on the API is specified.'.format(
636636
method=self.Method, path=self.Path))
637637

638+
if self.Auth.get("AuthorizationScopes") and not isinstance(self.Auth.get("AuthorizationScopes"), list):
639+
raise InvalidEventException(
640+
self.relative_id,
641+
'Unable to set Authorizer on API method [{method}] for path [{path}] because '
642+
'\'AuthorizationScopes\' must be a list of strings.'.format(method=self.Method,
643+
path=self.Path))
644+
638645
apikey_required_setting = self.Auth.get('ApiKeyRequired')
639646
apikey_required_setting_is_false = apikey_required_setting is not None and not apikey_required_setting
640647
if apikey_required_setting_is_false and not api_auth.get('ApiKeyRequired'):

samtranslator/swagger/swagger.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ def add_apikey_security_definition(self):
459459
self.security_definitions.update(api_key_security_definition)
460460

461461
def set_path_default_authorizer(self, path, default_authorizer, authorizers,
462-
add_default_auth_to_preflight=True):
462+
add_default_auth_to_preflight=True, api_authorizers=None):
463463
"""
464464
Adds the default_authorizer to the security block for each method on this path unless an Authorizer
465465
was defined at the Function/Path/Method level. This is intended to be used to set the
@@ -531,7 +531,8 @@ def set_path_default_authorizer(self, path, default_authorizer, authorizers,
531531
# No existing Authorizer found; use default
532532
else:
533533
security_dict = {}
534-
security_dict[default_authorizer] = []
534+
security_dict[default_authorizer] = self._get_authorization_scopes(api_authorizers,
535+
default_authorizer)
535536
authorizer_security = [security_dict]
536537

537538
security = existing_non_authorizer_security + authorizer_security
@@ -622,14 +623,17 @@ def add_auth_to_method(self, path, method_name, auth, api):
622623
:param dict api: Reference to the related Api's properties as defined in the template.
623624
"""
624625
method_authorizer = auth and auth.get('Authorizer')
626+
method_scopes = auth and auth.get('AuthorizationScopes')
627+
api_auth = api and api.get('Auth')
628+
authorizers = api_auth and api_auth.get('Authorizers')
625629
if method_authorizer:
626-
self._set_method_authorizer(path, method_name, method_authorizer)
630+
self._set_method_authorizer(path, method_name, method_authorizer, authorizers, method_scopes)
627631

628632
method_apikey_required = auth and auth.get('ApiKeyRequired')
629633
if method_apikey_required is not None:
630634
self._set_method_apikey_handling(path, method_name, method_apikey_required)
631635

632-
def _set_method_authorizer(self, path, method_name, authorizer_name):
636+
def _set_method_authorizer(self, path, method_name, authorizer_name, authorizers={}, method_scopes=None):
633637
"""
634638
Adds the authorizer_name to the security block for each method on this path.
635639
This is used to configure the authorizer for individual functions.
@@ -656,6 +660,13 @@ def _set_method_authorizer(self, path, method_name, authorizer_name):
656660
# This assumes there are no autorizers already configured in the existing security block
657661
security = existing_security + authorizer_security
658662

663+
if authorizer_name != 'NONE' and authorizers:
664+
method_auth_scopes = authorizers.get(authorizer_name, {}).get("AuthorizationScopes")
665+
if method_scopes is not None:
666+
method_auth_scopes = method_scopes
667+
if authorizers.get(authorizer_name) is not None and method_auth_scopes is not None:
668+
security_dict[authorizer_name] = method_auth_scopes
669+
659670
if security:
660671
method_definition['security'] = security
661672

@@ -1100,6 +1111,19 @@ def gen_skeleton():
11001111
}
11011112
}
11021113

1114+
@staticmethod
1115+
def _get_authorization_scopes(authorizers, default_authorizer):
1116+
"""
1117+
Returns auth scopes for an authorizer if present
1118+
:param authorizers: authorizer definitions
1119+
:param default_authorizer: name of the default authorizer
1120+
"""
1121+
if authorizers is not None:
1122+
if authorizers.get(default_authorizer) \
1123+
and authorizers[default_authorizer].get("AuthorizationScopes") is not None:
1124+
return authorizers[default_authorizer].get("AuthorizationScopes")
1125+
return []
1126+
11031127
@staticmethod
11041128
def _normalize_method_name(method):
11051129
"""

tests/swagger/test_swagger.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1791,3 +1791,69 @@ def test_must_add_iam_allow_and_custom(self):
17911791
}
17921792

17931793
self.assertEqual(deep_sort_lists(expected), deep_sort_lists(self.editor.swagger[_X_POLICY]))
1794+
1795+
class TestSwaggerEditor_add_authorization_scopes(TestCase):
1796+
def setUp(self):
1797+
self.api = api = {
1798+
'Auth':{
1799+
'Authorizers': {
1800+
'MyOtherCognitoAuth':{},
1801+
'MyCognitoAuth': {}
1802+
},
1803+
'DefaultAuthorizer': "MyCognitoAuth"
1804+
}
1805+
}
1806+
self.editor = SwaggerEditor({
1807+
"swagger": "2.0",
1808+
"paths": {
1809+
"/cognito": {
1810+
"get": {
1811+
"x-amazon-apigateway-integration": {
1812+
"httpMethod": "POST",
1813+
"type": "aws_proxy",
1814+
"uri": {
1815+
"Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations"
1816+
}
1817+
},
1818+
"security": [],
1819+
"responses": {}
1820+
}
1821+
},
1822+
}
1823+
})
1824+
1825+
def test_should_include_auth_scopes_if_defined_with_authorizer(self):
1826+
auth = {
1827+
'AuthorizationScopes': ["ResourceName/method.scope"],
1828+
'Authorizer':"MyOtherCognitoAuth"
1829+
}
1830+
self.editor.add_auth_to_method("/cognito", "get", auth, self.api)
1831+
self.assertEqual([{"MyOtherCognitoAuth": ["ResourceName/method.scope"]}],
1832+
self.editor.swagger["paths"]["/cognito"]["get"]["security"])
1833+
1834+
def test_should_include_auth_scopes_with_default_authorizer(self):
1835+
auth = {
1836+
'AuthorizationScopes': ["ResourceName/method.scope"],
1837+
'Authorizer': 'MyCognitoAuth'
1838+
}
1839+
self.editor.add_auth_to_method("/cognito", "get", auth, self.api)
1840+
self.assertEqual([{"MyCognitoAuth": ["ResourceName/method.scope"]}],
1841+
self.editor.swagger["paths"]["/cognito"]["get"]["security"])
1842+
1843+
def test_should_include_only_specified_authorizer_auth_if_no_scopes_defined(self):
1844+
auth = {
1845+
'Authorizer':"MyOtherCognitoAuth"
1846+
}
1847+
self.editor.add_auth_to_method("/cognito", "get", auth, self.api)
1848+
self.assertEqual([{"MyOtherCognitoAuth": []}],
1849+
self.editor.swagger["paths"]["/cognito"]["get"]["security"])
1850+
1851+
def test_should_include_none_if_default_is_overwritte(self):
1852+
auth = {
1853+
'Authorizer':"NONE"
1854+
}
1855+
1856+
self.editor.add_auth_to_method("/cognito", "get", auth, self.api)
1857+
self.assertEqual([{"NONE": []}],
1858+
self.editor.swagger["paths"]["/cognito"]["get"]["security"])
1859+
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
Resources:
2+
MyApiWithCognitoAuth:
3+
Type: "AWS::Serverless::Api"
4+
Properties:
5+
StageName: Prod
6+
Auth:
7+
DefaultAuthorizer: MyDefaultCognitoAuth
8+
Authorizers:
9+
MyDefaultCognitoAuth:
10+
UserPoolArn: arn:aws:1
11+
AuthorizationScopes:
12+
- default.write
13+
- default.read
14+
MyCognitoAuthWithDefaultScopes:
15+
UserPoolArn: arn:aws:2
16+
AuthorizationScopes:
17+
- default.delete
18+
- default.update
19+
20+
MyFn:
21+
Type: AWS::Serverless::Function
22+
Properties:
23+
CodeUri: s3://bucket/key
24+
Handler: index.handler
25+
Runtime: nodejs8.10
26+
Events:
27+
CognitoAuthorizerWithDefaultScopes:
28+
Type: Api
29+
Properties:
30+
RestApiId: !Ref MyApiWithCognitoAuth
31+
Method: get
32+
Path: /cognitoauthorizerwithdefaultscopes
33+
Auth:
34+
Authorizer: MyCognitoAuthWithDefaultScopes
35+
CognitoDefaultScopesDefaultAuthorizer:
36+
Type: Api
37+
Properties:
38+
RestApiId: !Ref MyApiWithCognitoAuth
39+
Method: get
40+
Path: /cognitodefaultscopesdefaultauthorizer
41+
CognitoWithAuthNone:
42+
Type: Api
43+
Properties:
44+
RestApiId: !Ref MyApiWithCognitoAuth
45+
Method: get
46+
Path: /cognitowithauthnone
47+
Auth:
48+
Authorizer: NONE
49+
CognitoDefaultScopesWithOverwritten:
50+
Type: Api
51+
Properties:
52+
RestApiId: !Ref MyApiWithCognitoAuth
53+
Method: get
54+
Path: /cognitodefaultscopesoverwritten
55+
Auth:
56+
Authorizer: MyDefaultCognitoAuth
57+
AuthorizationScopes:
58+
- overwritten.read
59+
- overwritten.write
60+
CognitoAuthorizerScopesOverwritten:
61+
Type: Api
62+
Properties:
63+
RestApiId: !Ref MyApiWithCognitoAuth
64+
Method: get
65+
Path: /cognitoauthorizercopesoverwritten
66+
Auth:
67+
Authorizer: MyCognitoAuthWithDefaultScopes
68+
AuthorizationScopes:
69+
- overwritten.read
70+
- overwritten.write
71+
CognitoDefaultScopesNone:
72+
Type: Api
73+
Properties:
74+
RestApiId: !Ref MyApiWithCognitoAuth
75+
Method: get
76+
Path: /cognitodefaultscopesnone
77+
Auth:
78+
Authorizer: MyDefaultCognitoAuth
79+
AuthorizationScopes: []
80+
CognitoDefaultAuthDefaultScopesNone:
81+
Type: Api
82+
Properties:
83+
RestApiId: !Ref MyApiWithCognitoAuth
84+
Method: get
85+
Path: /cognitodefaultauthdefaultscopesnone
86+
Auth:
87+
Authorizer: MyCognitoAuthWithDefaultScopes
88+
AuthorizationScopes: []
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
Resources:
2+
MyApiWithCognitoAuth:
3+
Type: "AWS::Serverless::Api"
4+
Properties:
5+
StageName: Prod
6+
OpenApiVersion: '3.0.1'
7+
Auth:
8+
DefaultAuthorizer: MyDefaultCognitoAuth
9+
Authorizers:
10+
MyDefaultCognitoAuth:
11+
UserPoolArn: arn:aws:1
12+
AuthorizationScopes:
13+
- default.write
14+
- default.read
15+
MyCognitoAuthWithDefaultScopes:
16+
UserPoolArn: arn:aws:2
17+
AuthorizationScopes:
18+
- default.delete
19+
- default.update
20+
21+
MyFn:
22+
Type: AWS::Serverless::Function
23+
Properties:
24+
CodeUri: s3://bucket/key
25+
Handler: index.handler
26+
Runtime: nodejs8.10
27+
Events:
28+
CognitoAuthorizerWithDefaultScopes:
29+
Type: Api
30+
Properties:
31+
RestApiId: !Ref MyApiWithCognitoAuth
32+
Method: get
33+
Path: /cognitoauthorizerwithdefaultscopes
34+
Auth:
35+
Authorizer: MyCognitoAuthWithDefaultScopes
36+
CognitoDefaultScopesDefaultAuthorizer:
37+
Type: Api
38+
Properties:
39+
RestApiId: !Ref MyApiWithCognitoAuth
40+
Method: get
41+
Path: /cognitodefaultscopesdefaultauthorizer
42+
CognitoWithAuthNone:
43+
Type: Api
44+
Properties:
45+
RestApiId: !Ref MyApiWithCognitoAuth
46+
Method: get
47+
Path: /cognitowithauthnone
48+
Auth:
49+
Authorizer: NONE
50+
CognitoDefaultScopesWithOverwritten:
51+
Type: Api
52+
Properties:
53+
RestApiId: !Ref MyApiWithCognitoAuth
54+
Method: get
55+
Path: /cognitodefaultscopesoverwritten
56+
Auth:
57+
Authorizer: MyDefaultCognitoAuth
58+
AuthorizationScopes:
59+
- overwritten.read
60+
- overwritten.write
61+
CognitoAuthorizerScopesOverwritten:
62+
Type: Api
63+
Properties:
64+
RestApiId: !Ref MyApiWithCognitoAuth
65+
Method: get
66+
Path: /cognitoauthorizercopesoverwritten
67+
Auth:
68+
Authorizer: MyCognitoAuthWithDefaultScopes
69+
AuthorizationScopes:
70+
- overwritten.read
71+
- overwritten.write
72+
CognitoDefaultScopesNone:
73+
Type: Api
74+
Properties:
75+
RestApiId: !Ref MyApiWithCognitoAuth
76+
Method: get
77+
Path: /cognitodefaultscopesnone
78+
Auth:
79+
Authorizer: MyDefaultCognitoAuth
80+
AuthorizationScopes: []
81+
CognitoDefaultAuthDefaultScopesNone:
82+
Type: Api
83+
Properties:
84+
RestApiId: !Ref MyApiWithCognitoAuth
85+
Method: get
86+
Path: /cognitodefaultauthdefaultscopesnone
87+
Auth:
88+
Authorizer: MyCognitoAuthWithDefaultScopes
89+
AuthorizationScopes: []

0 commit comments

Comments
 (0)