Skip to content

Commit

Permalink
feat(http-api): Add a built-in AWS_IAM authorizer for HTTP APIs (#1924)
Browse files Browse the repository at this point in the history
  • Loading branch information
harrisonhjones authored Apr 13, 2022
1 parent c1a6690 commit e4ffc6e
Show file tree
Hide file tree
Showing 36 changed files with 2,497 additions and 132 deletions.
4 changes: 3 additions & 1 deletion DEVELOPMENT_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,9 @@ Running Tests

### Unit testing with one Python version

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

### Unit testing with multiple Python versions

Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
target:
$(info ${HELP_MESSAGE})
@exit 0

init:
pip install -e '.[dev]'

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

test-fast:
pytest -x --cov samtranslator --cov-report term-missing --cov-fail-under 95 -n auto tests/*

test-cov-report:
pytest --cov samtranslator --cov-report term-missing --cov-report html --cov-fail-under 95 tests/*

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
[
{
"LogicalResourceId": "MyDefaultIamAuthHttpApi",
"ResourceType": "AWS::ApiGatewayV2::Api"
},
{
"LogicalResourceId": "MyDefaultIamAuthHttpApiApiGatewayDefaultStage",
"ResourceType": "AWS::ApiGatewayV2::Stage"
},
{
"LogicalResourceId": "MyIamAuthEnabledHttpApi",
"ResourceType": "AWS::ApiGatewayV2::Api"
},
{
"LogicalResourceId": "MyIamAuthEnabledHttpApiApiGatewayDefaultStage",
"ResourceType": "AWS::ApiGatewayV2::Stage"
},
{
"LogicalResourceId": "MyLambdaFunction",
"ResourceType": "AWS::Lambda::Function"
},
{
"LogicalResourceId": "MyLambdaFunctionImplicitApiDefaultAuthEventPermission",
"ResourceType": "AWS::Lambda::Permission"
},
{
"LogicalResourceId": "MyLambdaFunctionImplicitApiIamAuthEventPermission",
"ResourceType": "AWS::Lambda::Permission"
},
{
"LogicalResourceId": "MyLambdaFunctionMyDefaultIamAuthHttpApiDefaultAuthEventPermission",
"ResourceType": "AWS::Lambda::Permission"
},
{
"LogicalResourceId": "MyLambdaFunctionMyDefaultIamAuthHttpApiIamAuthEventPermission",
"ResourceType": "AWS::Lambda::Permission"
},
{
"LogicalResourceId": "MyLambdaFunctionMyDefaultIamAuthHttpApiNoAuthEventPermission",
"ResourceType": "AWS::Lambda::Permission"
},
{
"LogicalResourceId": "MyLambdaFunctionMyIamAuthEnabledHttpApiDefaultAuthEventPermission",
"ResourceType": "AWS::Lambda::Permission"
},
{
"LogicalResourceId": "MyLambdaFunctionMyIamAuthEnabledHttpApiIamAuthEventPermission",
"ResourceType": "AWS::Lambda::Permission"
},
{
"LogicalResourceId": "MyLambdaFunctionRole",
"ResourceType": "AWS::IAM::Role"
},
{
"LogicalResourceId": "ServerlessHttpApi",
"ResourceType": "AWS::ApiGatewayV2::Api"
},
{
"LogicalResourceId": "ServerlessHttpApiApiGatewayDefaultStage",
"ResourceType": "AWS::ApiGatewayV2::Stage"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
Globals:
HttpApi:
Auth:
EnableIamAuthorizer: true
Resources:
#######
# Serverless function that use the implicit AWS::Serverless::HttpApi called "ServerlessHttpApi".
# IAM Authorizer of the implicit AWS::Serverless::HttpApi is enabled using the global above.
#######
MyLambdaFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs12.x
CodeUri: ${codeuri}
Events:
# The following events use the implicit AWS::Serverless::HttpApi called "ServerlessHttpApi".
# The Iam Authorizer of the implicit AWS::Serverless::HttpApi is enabled using the global above.
# Should not have any auth enabled because there is no one set as the default.
ImplicitApiDefaultAuthEvent:
Type: HttpApi
Properties:
Path: /default-auth
Method: GET
# Should have Iam auth as it is set here.
ImplicitApiIamAuthEvent:
Type: HttpApi
Properties:
Auth:
Authorizer: AWS_IAM
Path: /iam-auth
Method: GET

# The following events use the defined AWS::Serverless::HttpApi further down.
# Should not have any auth enabled.
MyDefaultIamAuthHttpApiNoAuthEvent:
Type: HttpApi
Properties:
ApiId:
Ref: MyDefaultIamAuthHttpApi
Auth:
Authorizer: NONE
Path: /no-auth
Method: GET
# Should have Iam auth as it is set as the default for the Api.
MyDefaultIamAuthHttpApiDefaultAuthEvent:
Type: HttpApi
Properties:
ApiId:
Ref: MyDefaultIamAuthHttpApi
Path: /default-auth
Method: GET
# Should have Iam auth as it is set here.
MyDefaultIamAuthHttpApiIamAuthEvent:
Type: HttpApi
Properties:
ApiId:
Ref: MyDefaultIamAuthHttpApi
Auth:
Authorizer: AWS_IAM
Path: /iam-auth
Method: GET
# The following events use the defined AWS::Serverless::HttpApi further down.
# Should not have any auth enabled because there is no one set as the default.
MyIamAuthEnabledHttpApiDefaultAuthEvent:
Type: HttpApi
Properties:
ApiId:
Ref: MyIamAuthEnabledHttpApi
Path: /default-auth
Method: GET
# Should have Iam auth as it is set here.
MyIamAuthEnabledHttpApiIamAuthEvent:
Type: HttpApi
Properties:
ApiId:
Ref: MyIamAuthEnabledHttpApi
Auth:
Authorizer: AWS_IAM
Path: /iam-auth
Method: GET

# HTTP API resource with the Iam authorizer enabled and set to the default.
MyDefaultIamAuthHttpApi:
Type: AWS::Serverless::HttpApi
Properties:
Auth:
EnableIamAuthorizer: true
DefaultAuthorizer: AWS_IAM

# HTTP API resource with the Iam authorizer enabled and NOT set to the default.
MyIamAuthEnabledHttpApi:
Type: AWS::Serverless::HttpApi
Properties:
Auth:
EnableIamAuthorizer: true
29 changes: 29 additions & 0 deletions integration/single/test_function_with_http_api_and_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import requests
from parameterized import parameterized
from integration.helpers.base_test import BaseTest


class TestFunctionWithHttpApiAndAuth(BaseTest):
"""
AWS::Lambda::Function tests with http api events and auth
"""

def test_function_with_http_api_and_auth(self):
# If the request is not signed, which none of the below are, IAM will respond with a "Forbidden" message.
# We are not testing that IAM auth works here, we are simply testing if it was applied.
IAM_AUTH_OUTPUT = '{"message":"Forbidden"}'

self.create_and_verify_stack("function_with_http_api_events_and_auth")

implicitEndpoint = self.get_api_v2_endpoint("ServerlessHttpApi")
self.assertEqual(requests.get(implicitEndpoint + "/default-auth").text, self.FUNCTION_OUTPUT)
self.assertEqual(requests.get(implicitEndpoint + "/iam-auth").text, IAM_AUTH_OUTPUT)

defaultIamEndpoint = self.get_api_v2_endpoint("MyDefaultIamAuthHttpApi")
self.assertEqual(requests.get(defaultIamEndpoint + "/no-auth").text, self.FUNCTION_OUTPUT)
self.assertEqual(requests.get(defaultIamEndpoint + "/default-auth").text, IAM_AUTH_OUTPUT)
self.assertEqual(requests.get(defaultIamEndpoint + "/iam-auth").text, IAM_AUTH_OUTPUT)

iamEnabledEndpoint = self.get_api_v2_endpoint("MyIamAuthEnabledHttpApi")
self.assertEqual(requests.get(iamEnabledEndpoint + "/default-auth").text, self.FUNCTION_OUTPUT)
self.assertEqual(requests.get(iamEnabledEndpoint + "/iam-auth").text, IAM_AUTH_OUTPUT)
17 changes: 12 additions & 5 deletions samtranslator/model/api/http_api_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@
)
CorsProperties.__new__.__defaults__ = (None, None, None, None, None, False)

AuthProperties = namedtuple("_AuthProperties", ["Authorizers", "DefaultAuthorizer"])
AuthProperties.__new__.__defaults__ = (None, None)
AuthProperties = namedtuple("_AuthProperties", ["Authorizers", "DefaultAuthorizer", "EnableIamAuthorizer"])
AuthProperties.__new__.__defaults__ = (None, None, False)
DefaultStageName = "$default"
HttpApiTagName = "httpapi:createdBy"

Expand Down Expand Up @@ -422,7 +422,7 @@ def _add_auth(self):
)
open_api_editor = OpenApiEditor(self.definition_body)
auth_properties = AuthProperties(**self.auth)
authorizers = self._get_authorizers(auth_properties.Authorizers, auth_properties.DefaultAuthorizer)
authorizers = self._get_authorizers(auth_properties.Authorizers, auth_properties.EnableIamAuthorizer)

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

def _get_authorizers(self, authorizers_config, default_authorizer=None):
def _get_authorizers(self, authorizers_config, enable_iam_authorizer=False):
"""
Returns all authorizers for an API as an ApiGatewayV2Authorizer object
:param authorizers_config: authorizer configuration from the API Auth section
:param default_authorizer: name of the default authorizer
:param enable_iam_authorizer: if True add an "AWS_IAM" authorizer
"""
authorizers = {}

if enable_iam_authorizer is True:
authorizers["AWS_IAM"] = ApiGatewayV2Authorizer(is_aws_iam_authorizer=True)

# If all the customer wants to do is enable the IAM authorizer the authorizers_config will be None.
if not authorizers_config:
return authorizers

if not isinstance(authorizers_config, dict):
raise InvalidResourceException(self.logical_id, "Authorizers must be a dictionary.")

Expand Down
12 changes: 12 additions & 0 deletions samtranslator/model/apigatewayv2.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def __init__(
identity=None,
authorizer_payload_format_version=None,
enable_simple_responses=None,
is_aws_iam_authorizer=False,
):
"""
Creates an authorizer for use in V2 Http Apis
Expand All @@ -87,6 +88,7 @@ def __init__(
self.identity = identity
self.authorizer_payload_format_version = authorizer_payload_format_version
self.enable_simple_responses = enable_simple_responses
self.is_aws_iam_authorizer = is_aws_iam_authorizer

self._validate_input_parameters()

Expand All @@ -100,6 +102,8 @@ def __init__(
self._validate_lambda_authorizer()

def _get_auth_type(self):
if self.is_aws_iam_authorizer:
return "AWS_IAM"
if self.jwt_configuration:
return "JWT"
return "REQUEST"
Expand Down Expand Up @@ -179,6 +183,14 @@ def generate_openapi(self):
"""
authorizer_type = self._get_auth_type()

if authorizer_type == "AWS_IAM":
openapi = {
"type": "apiKey",
"name": "Authorization",
"in": "header",
"x-amazon-apigateway-authtype": "awsSigv4",
}

if authorizer_type == "JWT":
openapi = {"type": "oauth2"}
openapi[APIGATEWAY_AUTHORIZER_KEY] = {
Expand Down
Loading

0 comments on commit e4ffc6e

Please sign in to comment.