Skip to content

Commit 972b610

Browse files
horike37brettstack
authored andcommitted
feat: add API Gateway IAM (AWS_IAM) Authorizers (#827)
1 parent 4339ff1 commit 972b610

21 files changed

+4139
-50
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
exports.handler = async (event) => {
2+
return {
3+
statusCode: 200,
4+
body: JSON.stringify(event),
5+
headers: {}
6+
}
7+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
Transform: AWS::Serverless-2016-10-31
3+
Description: API Gateway with AWS IAM Authorizer
4+
Resources:
5+
MyApi:
6+
Type: AWS::Serverless::Api
7+
Properties:
8+
StageName: Prod
9+
Auth:
10+
DefaultAuthorizer: AWS_IAM
11+
InvokeRole: CALLER_CREDENTIALS
12+
13+
MyFunction:
14+
Type: AWS::Serverless::Function
15+
Properties:
16+
CodeUri: .
17+
Handler: index.handler
18+
Runtime: nodejs8.10
19+
Events:
20+
GetRoot:
21+
Type: Api
22+
Properties:
23+
RestApiId: !Ref MyApi
24+
Path: /
25+
Method: get
26+
27+
Outputs:
28+
ApiURL:
29+
Description: "API URL"
30+
Value: !Sub 'https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/'

samtranslator/model/api/api_generator.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
# Default the Cors Properties to '*' wildcard and False AllowCredentials. Other properties are actually Optional
1919
CorsProperties.__new__.__defaults__ = (None, None, _CORS_WILDCARD, None, False)
2020

21-
AuthProperties = namedtuple("_AuthProperties", ["Authorizers", "DefaultAuthorizer"])
22-
AuthProperties.__new__.__defaults__ = (None, None)
21+
AuthProperties = namedtuple("_AuthProperties", ["Authorizers", "DefaultAuthorizer", "InvokeRole"])
22+
AuthProperties.__new__.__defaults__ = (None, None, None)
2323

2424

2525
class ApiGenerator(object):
@@ -266,7 +266,7 @@ def _add_auth(self):
266266
"'DefinitionBody' does not contain a valid Swagger")
267267
swagger_editor = SwaggerEditor(self.definition_body)
268268
auth_properties = AuthProperties(**self.auth)
269-
authorizers = self._get_authorizers(auth_properties.Authorizers)
269+
authorizers = self._get_authorizers(auth_properties.Authorizers, auth_properties.DefaultAuthorizer)
270270

271271
if authorizers:
272272
swagger_editor.add_authorizers(authorizers)
@@ -275,14 +275,23 @@ def _add_auth(self):
275275
# Assign the Swagger back to template
276276
self.definition_body = swagger_editor.swagger
277277

278-
def _get_authorizers(self, authorizers_config):
278+
def _get_authorizers(self, authorizers_config, default_authorizer=None):
279+
authorizers = {}
280+
if default_authorizer == 'AWS_IAM':
281+
authorizers[default_authorizer] = ApiGatewayAuthorizer(
282+
api_logical_id=self.logical_id,
283+
name=default_authorizer,
284+
is_aws_iam_authorizer=True
285+
)
286+
279287
if not authorizers_config:
288+
if 'AWS_IAM' in authorizers:
289+
return authorizers
280290
return None
281291

282292
if not isinstance(authorizers_config, dict):
283293
raise InvalidResourceException(self.logical_id,
284294
"Authorizers must be a dictionary")
285-
authorizers = {}
286295

287296
for authorizer_name, authorizer in authorizers_config.items():
288297
authorizers[authorizer_name] = ApiGatewayAuthorizer(
@@ -294,7 +303,6 @@ def _get_authorizers(self, authorizers_config):
294303
function_payload_type=authorizer.get('FunctionPayloadType'),
295304
function_invoke_role=authorizer.get('FunctionInvokeRole')
296305
)
297-
298306
return authorizers
299307

300308
def _get_permission(self, authorizer_name, authorizer_lambda_function_arn):
@@ -346,7 +354,7 @@ def _set_default_authorizer(self, swagger_editor, authorizers, default_authorize
346354
if not default_authorizer:
347355
return
348356

349-
if not authorizers.get(default_authorizer):
357+
if not authorizers.get(default_authorizer) and default_authorizer != 'AWS_IAM':
350358
raise InvalidResourceException(self.logical_id, "Unable to set DefaultAuthorizer because '" +
351359
default_authorizer + "' was not defined in 'Authorizers'")
352360

samtranslator/model/apigateway.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class ApiGatewayAuthorizer(object):
9797
_VALID_FUNCTION_PAYLOAD_TYPES = [None, 'TOKEN', 'REQUEST']
9898

9999
def __init__(self, api_logical_id=None, name=None, user_pool_arn=None, function_arn=None, identity=None,
100-
function_payload_type=None, function_invoke_role=None):
100+
function_payload_type=None, function_invoke_role=None, is_aws_iam_authorizer=False):
101101
if function_payload_type not in ApiGatewayAuthorizer._VALID_FUNCTION_PAYLOAD_TYPES:
102102
raise InvalidResourceException(api_logical_id, name + " Authorizer has invalid "
103103
"'FunctionPayloadType': " + function_payload_type)
@@ -113,6 +113,7 @@ def __init__(self, api_logical_id=None, name=None, user_pool_arn=None, function_
113113
self.identity = identity
114114
self.function_payload_type = function_payload_type
115115
self.function_invoke_role = function_invoke_role
116+
self.is_aws_iam_authorizer = is_aws_iam_authorizer
116117

117118
def _is_missing_identity_source(self, identity):
118119
if not identity:
@@ -135,16 +136,19 @@ def generate_swagger(self):
135136
"type": "apiKey",
136137
"name": self._get_swagger_header_name(),
137138
"in": "header",
138-
"x-amazon-apigateway-authtype": self._get_swagger_authtype(),
139-
"x-amazon-apigateway-authorizer": {
140-
"type": self._get_swagger_authorizer_type()
141-
}
139+
"x-amazon-apigateway-authtype": self._get_swagger_authtype()
142140
}
143141

144142
if authorizer_type == 'COGNITO_USER_POOLS':
145-
swagger[APIGATEWAY_AUTHORIZER_KEY]['providerARNs'] = self._get_user_pool_arn_array()
143+
swagger[APIGATEWAY_AUTHORIZER_KEY] = {
144+
'type': self._get_swagger_authorizer_type(),
145+
'providerARNs': self._get_user_pool_arn_array()
146+
}
146147

147148
elif authorizer_type == 'LAMBDA':
149+
swagger[APIGATEWAY_AUTHORIZER_KEY] = {
150+
'type': self._get_swagger_authorizer_type()
151+
}
148152
partition = ArnGenerator.get_partition_name()
149153
resource = 'lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations'
150154
authorizer_uri = fnSub(ArnGenerator.generate_arn(partition=partition, service='apigateway',
@@ -217,6 +221,9 @@ def _get_swagger_header_name(self):
217221
return self._get_identity_header()
218222

219223
def _get_type(self):
224+
if self.is_aws_iam_authorizer:
225+
return 'AWS_IAM'
226+
220227
if self.user_pool_arn:
221228
return 'COGNITO_USER_POOLS'
222229

@@ -242,8 +249,13 @@ def _get_function_invoke_role(self):
242249

243250
def _get_swagger_authtype(self):
244251
authorizer_type = self._get_type()
252+
if authorizer_type == 'AWS_IAM':
253+
return 'awsSigv4'
254+
255+
if authorizer_type == 'COGNITO_USER_POOLS':
256+
return 'cognito_user_pools'
245257

246-
return 'cognito_user_pools' if authorizer_type == 'COGNITO_USER_POOLS' else 'custom'
258+
return 'custom'
247259

248260
def _get_function_payload_type(self):
249261
return 'TOKEN' if not self.function_payload_type else self.function_payload_type

samtranslator/model/eventsources/push.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,7 @@ def _add_swagger_integration(self, api, function):
529529
if CONDITION in function.resource_attributes:
530530
condition = function.resource_attributes[CONDITION]
531531

532-
editor.add_lambda_integration(self.Path, self.Method, uri, condition=condition)
532+
editor.add_lambda_integration(self.Path, self.Method, uri, self.Auth, api.get('Auth'), condition=condition)
533533

534534
if self.Auth:
535535
method_authorizer = self.Auth.get('Authorizer')
@@ -538,28 +538,29 @@ def _add_swagger_integration(self, api, function):
538538
api_auth = api.get('Auth')
539539
api_authorizers = api_auth and api_auth.get('Authorizers')
540540

541-
if not api_authorizers:
542-
raise InvalidEventException(
543-
self.relative_id,
544-
'Unable to set Authorizer [{authorizer}] on API method [{method}] for path [{path}] because '
545-
'the related API does not define any Authorizers.'.format(
546-
authorizer=method_authorizer, method=self.Method, path=self.Path))
547-
548-
if method_authorizer != 'NONE' and not api_authorizers.get(method_authorizer):
549-
raise InvalidEventException(
550-
self.relative_id,
551-
'Unable to set Authorizer [{authorizer}] on API method [{method}] for path [{path}] because it '
552-
'wasn\'t defined in the API\'s Authorizers.'.format(
553-
authorizer=method_authorizer, method=self.Method, path=self.Path))
554-
555-
if method_authorizer == 'NONE' and not api_auth.get('DefaultAuthorizer'):
556-
raise InvalidEventException(
557-
self.relative_id,
558-
'Unable to set Authorizer on API method [{method}] for path [{path}] because \'NONE\' '
559-
'is only a valid value when a DefaultAuthorizer on the API is specified.'.format(
560-
method=self.Method, path=self.Path))
561-
562-
editor.add_auth_to_method(api=api, path=self.Path, method_name=self.Method, auth=self.Auth)
541+
if method_authorizer != 'AWS_IAM':
542+
if not api_authorizers:
543+
raise InvalidEventException(
544+
self.relative_id,
545+
'Unable to set Authorizer [{authorizer}] on API method [{method}] for path [{path}] '
546+
'because the related API does not define any Authorizers.'.format(
547+
authorizer=method_authorizer, method=self.Method, path=self.Path))
548+
549+
if method_authorizer != 'NONE' and not api_authorizers.get(method_authorizer):
550+
raise InvalidEventException(
551+
self.relative_id,
552+
'Unable to set Authorizer [{authorizer}] on API method [{method}] for path [{path}] '
553+
'because it wasn\'t defined in the API\'s Authorizers.'.format(
554+
authorizer=method_authorizer, method=self.Method, path=self.Path))
555+
556+
if method_authorizer == 'NONE' and not api_auth.get('DefaultAuthorizer'):
557+
raise InvalidEventException(
558+
self.relative_id,
559+
'Unable to set Authorizer on API method [{method}] for path [{path}] because \'NONE\' '
560+
'is only a valid value when a DefaultAuthorizer on the API is specified.'.format(
561+
method=self.Method, path=self.Path))
562+
563+
editor.add_auth_to_method(api=api, path=self.Path, method_name=self.Method, auth=self.Auth)
563564

564565
api["DefinitionBody"] = editor.swagger
565566

samtranslator/swagger/swagger.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ def add_path(self, path, method=None):
129129

130130
path_dict.setdefault(method, {})
131131

132-
def add_lambda_integration(self, path, method, integration_uri, condition=None):
132+
def add_lambda_integration(self, path, method, integration_uri,
133+
method_auth_config=None, api_auth_config=None, condition=None):
133134
"""
134135
Adds aws_proxy APIGW integration to the given path+method.
135136
@@ -156,6 +157,15 @@ def add_lambda_integration(self, path, method, integration_uri, condition=None):
156157
'uri': integration_uri
157158
}
158159

160+
method_auth_config = method_auth_config or {}
161+
api_auth_config = api_auth_config or {}
162+
if method_auth_config.get('Authorizer') == 'AWS_IAM' \
163+
or api_auth_config.get('DefaultAuthorizer') == 'AWS_IAM' and not method_auth_config:
164+
self.paths[path][method][self._X_APIGW_INTEGRATION]['credentials'] = self._generate_integration_credentials(
165+
method_invoke_role=method_auth_config.get('InvokeRole'),
166+
api_invoke_role=api_auth_config.get('InvokeRole')
167+
)
168+
159169
# If 'responses' key is *not* present, add it with an empty dict as value
160170
path_dict[method].setdefault('responses', {})
161171

@@ -169,6 +179,13 @@ def make_path_conditional(self, path, condition):
169179
"""
170180
self.paths[path] = make_conditional(condition, self.paths[path])
171181

182+
def _generate_integration_credentials(self, method_invoke_role=None, api_invoke_role=None):
183+
return self._get_invoke_role(method_invoke_role or api_invoke_role)
184+
185+
def _get_invoke_role(self, invoke_role):
186+
CALLER_CREDENTIALS_ARN = 'arn:aws:iam::*:user/*'
187+
return invoke_role if invoke_role and invoke_role != 'CALLER_CREDENTIALS' else CALLER_CREDENTIALS_ARN
188+
172189
def iter_on_path(self):
173190
"""
174191
Yields all the paths available in the Swagger. As a caller, if you add new paths to Swagger while iterating,
@@ -409,7 +426,6 @@ def add_auth_to_method(self, path, method_name, auth, api):
409426
def set_method_authorizer(self, path, method_name, authorizer_name, authorizers, default_authorizer,
410427
is_default=False):
411428
normalized_method_name = self._normalize_method_name(method_name)
412-
413429
# It is possible that the method could have two definitions in a Fn::If block.
414430
for method_definition in self.get_method_contents(self.get_path(path)[normalized_method_name]):
415431

@@ -418,7 +434,10 @@ def set_method_authorizer(self, path, method_name, authorizer_name, authorizers,
418434
continue
419435
existing_security = method_definition.get('security', [])
420436
# TEST: [{'sigv4': []}, {'api_key': []}])
421-
authorizer_names = set(authorizers.keys())
437+
authorizer_list = ['AWS_IAM']
438+
if authorizers:
439+
authorizer_list.extend(authorizers.keys())
440+
authorizer_names = set(authorizer_list)
422441
existing_non_authorizer_security = []
423442
existing_authorizer_security = []
424443

@@ -473,6 +492,22 @@ def set_method_authorizer(self, path, method_name, authorizer_name, authorizers,
473492
if security:
474493
method_definition['security'] = security
475494

495+
# The first element of the method_definition['security'] should be AWS_IAM
496+
# because authorizer_list = ['AWS_IAM'] is hardcoded above
497+
if 'AWS_IAM' in method_definition['security'][0]:
498+
aws_iam_security_definition = {
499+
'AWS_IAM': {
500+
'x-amazon-apigateway-authtype': 'awsSigv4',
501+
'type': 'apiKey',
502+
'name': 'Authorization',
503+
'in': 'header'
504+
}
505+
}
506+
if not self.security_definitions:
507+
self.security_definitions = aws_iam_security_definition
508+
elif 'AWS_IAM' not in self.security_definitions:
509+
self.security_definitions.update(aws_iam_security_definition)
510+
476511
@property
477512
def swagger(self):
478513
"""

tests/swagger/test_swagger.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,37 @@ def test_must_raise_on_existing_integration(self):
327327
with self.assertRaises(ValueError):
328328
self.editor.add_lambda_integration("/bar", "get", "integrationUri")
329329

330+
def test_must_add_credentials_to_the_integration(self):
331+
path = "/newpath"
332+
method = "get"
333+
integration_uri = "something"
334+
expected = 'arn:aws:iam::*:user/*'
335+
api_auth_config = {
336+
"DefaultAuthorizer": "AWS_IAM",
337+
"InvokeRole": "CALLER_CREDENTIALS"
338+
}
339+
340+
self.editor.add_lambda_integration(path, method, integration_uri, None, api_auth_config)
341+
actual = self.editor.swagger["paths"][path][method][_X_INTEGRATION]['credentials']
342+
self.assertEquals(expected, actual)
343+
344+
def test_must_add_credentials_to_the_integration_overrides(self):
345+
path = "/newpath"
346+
method = "get"
347+
integration_uri = "something"
348+
expected = 'arn:aws:iam::*:role/xxxxxx'
349+
api_auth_config = {
350+
"DefaultAuthorizer": "MyAuth",
351+
}
352+
method_auth_config = {
353+
"Authorizer": "AWS_IAM",
354+
"InvokeRole": "arn:aws:iam::*:role/xxxxxx"
355+
}
356+
357+
self.editor.add_lambda_integration(path, method, integration_uri, method_auth_config, api_auth_config)
358+
actual = self.editor.swagger["paths"][path][method][_X_INTEGRATION]['credentials']
359+
self.assertEquals(expected, actual)
360+
330361

331362
class TestSwaggerEditor_iter_on_path(TestCase):
332363

0 commit comments

Comments
 (0)