Skip to content

Commit 8e83fdb

Browse files
Add a built-in AWS_IAM authorizer for HTTP APIs
- Added tests - Added test-fast target - Updated documentation
1 parent 979825f commit 8e83fdb

File tree

36 files changed

+2497
-132
lines changed

36 files changed

+2497
-132
lines changed

DEVELOPMENT_GUIDE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,9 @@ Running Tests
114114

115115
### Unit testing with one Python version
116116

117-
If you're trying to do a quick run, it's ok to use the current python version. Run `make pr`.
117+
If you're trying to do a quick run, it's ok to use the current python version.
118+
Run `make test` or `make test-fast`. Once all tests pass make sure to run
119+
`make pr` before sending out your PR.
118120

119121
### Unit testing with multiple Python versions
120122

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
target:
22
$(info ${HELP_MESSAGE})
33
@exit 0
4-
4+
55
init:
66
pip install -e '.[dev]'
77

88
test:
99
pytest --cov samtranslator --cov-report term-missing --cov-fail-under 95 -n auto tests/*
1010

11+
test-fast:
12+
pytest -x --cov samtranslator --cov-report term-missing --cov-fail-under 95 -n auto tests/*
13+
1114
test-cov-report:
1215
pytest --cov samtranslator --cov-report term-missing --cov-report html --cov-fail-under 95 tests/*
1316

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
[
2+
{
3+
"LogicalResourceId": "MyDefaultIamAuthHttpApi",
4+
"ResourceType": "AWS::ApiGatewayV2::Api"
5+
},
6+
{
7+
"LogicalResourceId": "MyDefaultIamAuthHttpApiApiGatewayDefaultStage",
8+
"ResourceType": "AWS::ApiGatewayV2::Stage"
9+
},
10+
{
11+
"LogicalResourceId": "MyIamAuthEnabledHttpApi",
12+
"ResourceType": "AWS::ApiGatewayV2::Api"
13+
},
14+
{
15+
"LogicalResourceId": "MyIamAuthEnabledHttpApiApiGatewayDefaultStage",
16+
"ResourceType": "AWS::ApiGatewayV2::Stage"
17+
},
18+
{
19+
"LogicalResourceId": "MyLambdaFunction",
20+
"ResourceType": "AWS::Lambda::Function"
21+
},
22+
{
23+
"LogicalResourceId": "MyLambdaFunctionImplicitApiDefaultAuthEventPermission",
24+
"ResourceType": "AWS::Lambda::Permission"
25+
},
26+
{
27+
"LogicalResourceId": "MyLambdaFunctionImplicitApiIamAuthEventPermission",
28+
"ResourceType": "AWS::Lambda::Permission"
29+
},
30+
{
31+
"LogicalResourceId": "MyLambdaFunctionMyDefaultIamAuthHttpApiDefaultAuthEventPermission",
32+
"ResourceType": "AWS::Lambda::Permission"
33+
},
34+
{
35+
"LogicalResourceId": "MyLambdaFunctionMyDefaultIamAuthHttpApiIamAuthEventPermission",
36+
"ResourceType": "AWS::Lambda::Permission"
37+
},
38+
{
39+
"LogicalResourceId": "MyLambdaFunctionMyDefaultIamAuthHttpApiNoAuthEventPermission",
40+
"ResourceType": "AWS::Lambda::Permission"
41+
},
42+
{
43+
"LogicalResourceId": "MyLambdaFunctionMyIamAuthEnabledHttpApiDefaultAuthEventPermission",
44+
"ResourceType": "AWS::Lambda::Permission"
45+
},
46+
{
47+
"LogicalResourceId": "MyLambdaFunctionMyIamAuthEnabledHttpApiIamAuthEventPermission",
48+
"ResourceType": "AWS::Lambda::Permission"
49+
},
50+
{
51+
"LogicalResourceId": "MyLambdaFunctionRole",
52+
"ResourceType": "AWS::IAM::Role"
53+
},
54+
{
55+
"LogicalResourceId": "ServerlessHttpApi",
56+
"ResourceType": "AWS::ApiGatewayV2::Api"
57+
},
58+
{
59+
"LogicalResourceId": "ServerlessHttpApiApiGatewayDefaultStage",
60+
"ResourceType": "AWS::ApiGatewayV2::Stage"
61+
}
62+
]
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
Globals:
2+
HttpApi:
3+
Auth:
4+
EnableIamAuthorizer: true
5+
Resources:
6+
#######
7+
# Serverless function that use the implicit AWS::Serverless::HttpApi called "ServerlessHttpApi".
8+
# IAM Authorizer of the implicit AWS::Serverless::HttpApi is enabled using the global above.
9+
#######
10+
MyLambdaFunction:
11+
Type: AWS::Serverless::Function
12+
Properties:
13+
Handler: index.handler
14+
Runtime: nodejs12.x
15+
CodeUri: ${codeuri}
16+
Events:
17+
# The following events use the implicit AWS::Serverless::HttpApi called "ServerlessHttpApi".
18+
# The Iam Authorizer of the implicit AWS::Serverless::HttpApi is enabled using the global above.
19+
# Should not have any auth enabled because there is no one set as the default.
20+
ImplicitApiDefaultAuthEvent:
21+
Type: HttpApi
22+
Properties:
23+
Path: /default-auth
24+
Method: GET
25+
# Should have Iam auth as it is set here.
26+
ImplicitApiIamAuthEvent:
27+
Type: HttpApi
28+
Properties:
29+
Auth:
30+
Authorizer: AWS_IAM
31+
Path: /iam-auth
32+
Method: GET
33+
34+
# The following events use the defined AWS::Serverless::HttpApi further down.
35+
# Should not have any auth enabled.
36+
MyDefaultIamAuthHttpApiNoAuthEvent:
37+
Type: HttpApi
38+
Properties:
39+
ApiId:
40+
Ref: MyDefaultIamAuthHttpApi
41+
Auth:
42+
Authorizer: NONE
43+
Path: /no-auth
44+
Method: GET
45+
# Should have Iam auth as it is set as the default for the Api.
46+
MyDefaultIamAuthHttpApiDefaultAuthEvent:
47+
Type: HttpApi
48+
Properties:
49+
ApiId:
50+
Ref: MyDefaultIamAuthHttpApi
51+
Path: /default-auth
52+
Method: GET
53+
# Should have Iam auth as it is set here.
54+
MyDefaultIamAuthHttpApiIamAuthEvent:
55+
Type: HttpApi
56+
Properties:
57+
ApiId:
58+
Ref: MyDefaultIamAuthHttpApi
59+
Auth:
60+
Authorizer: AWS_IAM
61+
Path: /iam-auth
62+
Method: GET
63+
# The following events use the defined AWS::Serverless::HttpApi further down.
64+
# Should not have any auth enabled because there is no one set as the default.
65+
MyIamAuthEnabledHttpApiDefaultAuthEvent:
66+
Type: HttpApi
67+
Properties:
68+
ApiId:
69+
Ref: MyIamAuthEnabledHttpApi
70+
Path: /default-auth
71+
Method: GET
72+
# Should have Iam auth as it is set here.
73+
MyIamAuthEnabledHttpApiIamAuthEvent:
74+
Type: HttpApi
75+
Properties:
76+
ApiId:
77+
Ref: MyIamAuthEnabledHttpApi
78+
Auth:
79+
Authorizer: AWS_IAM
80+
Path: /iam-auth
81+
Method: GET
82+
83+
# HTTP API resource with the Iam authorizer enabled and set to the default.
84+
MyDefaultIamAuthHttpApi:
85+
Type: AWS::Serverless::HttpApi
86+
Properties:
87+
Auth:
88+
EnableIamAuthorizer: true
89+
DefaultAuthorizer: AWS_IAM
90+
91+
# HTTP API resource with the Iam authorizer enabled and NOT set to the default.
92+
MyIamAuthEnabledHttpApi:
93+
Type: AWS::Serverless::HttpApi
94+
Properties:
95+
Auth:
96+
EnableIamAuthorizer: true
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import requests
2+
from parameterized import parameterized
3+
from integration.helpers.base_test import BaseTest
4+
5+
6+
class TestFunctionWithHttpApiAndAuth(BaseTest):
7+
"""
8+
AWS::Lambda::Function tests with http api events and auth
9+
"""
10+
11+
def test_function_with_http_api_and_auth(self):
12+
# If the request is not signed, which none of the below are, IAM will respond with a "Forbidden" message.
13+
# We are not testing that IAM auth works here, we are simply testing if it was applied.
14+
IAM_AUTH_OUTPUT = '{"message":"Forbidden"}'
15+
16+
self.create_and_verify_stack("function_with_http_api_events_and_auth")
17+
18+
implicitEndpoint = self.get_api_v2_endpoint("ServerlessHttpApi")
19+
self.assertEqual(requests.get(implicitEndpoint + "/default-auth").text, self.FUNCTION_OUTPUT)
20+
self.assertEqual(requests.get(implicitEndpoint + "/iam-auth").text, IAM_AUTH_OUTPUT)
21+
22+
defaultIamEndpoint = self.get_api_v2_endpoint("MyDefaultIamAuthHttpApi")
23+
self.assertEqual(requests.get(defaultIamEndpoint + "/no-auth").text, self.FUNCTION_OUTPUT)
24+
self.assertEqual(requests.get(defaultIamEndpoint + "/default-auth").text, IAM_AUTH_OUTPUT)
25+
self.assertEqual(requests.get(defaultIamEndpoint + "/iam-auth").text, IAM_AUTH_OUTPUT)
26+
27+
iamEnabledEndpoint = self.get_api_v2_endpoint("MyIamAuthEnabledHttpApi")
28+
self.assertEqual(requests.get(iamEnabledEndpoint + "/default-auth").text, self.FUNCTION_OUTPUT)
29+
self.assertEqual(requests.get(iamEnabledEndpoint + "/iam-auth").text, IAM_AUTH_OUTPUT)

samtranslator/model/api/http_api_generator.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
)
2525
CorsProperties.__new__.__defaults__ = (None, None, None, None, None, False)
2626

27-
AuthProperties = namedtuple("_AuthProperties", ["Authorizers", "DefaultAuthorizer"])
28-
AuthProperties.__new__.__defaults__ = (None, None)
27+
AuthProperties = namedtuple("_AuthProperties", ["Authorizers", "DefaultAuthorizer", "EnableIamAuthorizer"])
28+
AuthProperties.__new__.__defaults__ = (None, None, False)
2929
DefaultStageName = "$default"
3030
HttpApiTagName = "httpapi:createdBy"
3131

@@ -422,7 +422,7 @@ def _add_auth(self):
422422
)
423423
open_api_editor = OpenApiEditor(self.definition_body)
424424
auth_properties = AuthProperties(**self.auth)
425-
authorizers = self._get_authorizers(auth_properties.Authorizers, auth_properties.DefaultAuthorizer)
425+
authorizers = self._get_authorizers(auth_properties.Authorizers, auth_properties.EnableIamAuthorizer)
426426

427427
# authorizers is guaranteed to return a value or raise an exception
428428
open_api_editor.add_authorizers_security_definitions(authorizers)
@@ -494,14 +494,21 @@ def _set_default_authorizer(self, open_api_editor, authorizers, default_authoriz
494494
path, default_authorizer, authorizers=authorizers, api_authorizers=api_authorizers
495495
)
496496

497-
def _get_authorizers(self, authorizers_config, default_authorizer=None):
497+
def _get_authorizers(self, authorizers_config, enable_iam_authorizer=False):
498498
"""
499499
Returns all authorizers for an API as an ApiGatewayV2Authorizer object
500500
:param authorizers_config: authorizer configuration from the API Auth section
501-
:param default_authorizer: name of the default authorizer
501+
:param enable_iam_authorizer: if True add an "AWS_IAM" authorizer
502502
"""
503503
authorizers = {}
504504

505+
if enable_iam_authorizer is True:
506+
authorizers["AWS_IAM"] = ApiGatewayV2Authorizer(is_aws_iam_authorizer=True)
507+
508+
# If all the customer wants to do is enable the IAM authorizer the authorizers_config will be None.
509+
if not authorizers_config:
510+
return authorizers
511+
505512
if not isinstance(authorizers_config, dict):
506513
raise InvalidResourceException(self.logical_id, "Authorizers must be a dictionary.")
507514

samtranslator/model/apigatewayv2.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def __init__(
7373
identity=None,
7474
authorizer_payload_format_version=None,
7575
enable_simple_responses=None,
76+
is_aws_iam_authorizer=False,
7677
):
7778
"""
7879
Creates an authorizer for use in V2 Http Apis
@@ -87,6 +88,7 @@ def __init__(
8788
self.identity = identity
8889
self.authorizer_payload_format_version = authorizer_payload_format_version
8990
self.enable_simple_responses = enable_simple_responses
91+
self.is_aws_iam_authorizer = is_aws_iam_authorizer
9092

9193
self._validate_input_parameters()
9294

@@ -100,6 +102,8 @@ def __init__(
100102
self._validate_lambda_authorizer()
101103

102104
def _get_auth_type(self):
105+
if self.is_aws_iam_authorizer:
106+
return "AWS_IAM"
103107
if self.jwt_configuration:
104108
return "JWT"
105109
return "REQUEST"
@@ -174,6 +178,14 @@ def generate_openapi(self):
174178
"""
175179
authorizer_type = self._get_auth_type()
176180

181+
if authorizer_type == "AWS_IAM":
182+
openapi = {
183+
"type": "apiKey",
184+
"name": "Authorization",
185+
"in": "header",
186+
"x-amazon-apigateway-authtype": "awsSigv4",
187+
}
188+
177189
if authorizer_type == "JWT":
178190
openapi = {"type": "oauth2"}
179191
openapi[APIGATEWAY_AUTHORIZER_KEY] = {

0 commit comments

Comments
 (0)