Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(auth): add support for API Gateway Authorizers #546

Merged
merged 20 commits into from
Sep 21, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cd28c89
feat(auth): add support for API Gateway Authorizers
brettstack Aug 9, 2018
8cb9f63
addressed my own feedback in GitHub comments
brettstack Aug 9, 2018
915760c
improvement: remove check for Auth dict since it's verified by proper…
brettstack Aug 10, 2018
0ea0ce1
test: add error tests for authorizers
brettstack Aug 10, 2018
f4ecc38
test: fix missing DefaultAuthorizer in Authorizers test
brettstack Aug 10, 2018
e451739
fix: add permissions for API to invoke Authorizer Lambda Function
brettstack Aug 15, 2018
18ecefc
fix: fix error when no Auth defined
brettstack Aug 15, 2018
ea7efc8
docs: add Lambda REQUEST Authorizer example
brettstack Aug 16, 2018
e0b412f
fix: add error handling when no identity source provided for Lambda R…
brettstack Aug 17, 2018
da1867e
docs(examples): improve README commands for api_lambda_request_auth
brettstack Aug 24, 2018
3998447
add API Gateway + Cognito Authorizer example
brettstack Sep 7, 2018
60671cc
simplify setup by using npm scripts
brettstack Sep 11, 2018
e9d6c25
update to use authorization_code flow
brettstack Sep 13, 2018
9872796
add tests for cognito authorizers
brettstack Sep 14, 2018
1d1be1c
merge upstream/develop
brettstack Sep 17, 2018
90b3a39
convert result of map() to list for py3 support
brettstack Sep 17, 2018
6dca4c7
update spec to include API Auth and Function Auth
brettstack Sep 18, 2018
c28ae1a
add TOC for Data Types in spec
brettstack Sep 18, 2018
847646d
address documentation change requests
brettstack Sep 20, 2018
f6ac94b
address PR change requests
brettstack Sep 21, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions DEVELOPMENT_GUIDE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,23 @@ Install snakeviz `pip install snakeviz`
```
python -m cProfile -o sam_profile_results bin/sam-translate.py translate --input-file=tests/translator/input/alexa_skill.yaml --output-file=cfn-template.json
snakeviz sam_profile_results
```

Verifying transforms
--------------------

If you make changes to the transformer and want to verify the resulting CloudFormation template works as expected, you can transform your SAM template into a CloudFormation template using the following process:

```shell
# Optional: You only need to run the package command in certain cases; e.g. when your CodeUri specifies a local path
# Replace MY_TEMPLATE_PATH with the path to your template and MY_S3_BUCKET with an existing S3 bucket
aws cloudformation package --template-file MY_TEMPLATE_PATH/template.yaml --output-template-file output-template.yaml --s3-bucket MY_S3_BUCKET

# Transform your SAM template into a CloudFormation template
# Replace "output-template.yaml" if you didn't run the package command above or specified a different path for --output-template-file
bin/sam-translate.py --input-file=output-template.yaml

# Deploy your transformed CloudFormation template
# Replace MY_STACK_NAME with a unique name each time you deploy
aws cloudformation deploy --template-file cfn-template.json --capabilities CAPABILITY_NAMED_IAM --stack-name MY_STACK_NAME
```
2 changes: 1 addition & 1 deletion bin/sam-translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def main():
except InvalidDocumentException as e:
errorMessage = reduce(lambda message, error: message + ' ' + error.message, e.causes, e.message)
print(errorMessage)
errors = map(lambda cause: {'errorMessage': cause.message}, e.causes)
errors = map(lambda cause: cause.message, e.causes)
print(errors)


Expand Down
3 changes: 3 additions & 0 deletions examples/2016-10-31/api_cognito_auth/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = async (event) => {
return event
}
135 changes: 135 additions & 0 deletions examples/2016-10-31/api_cognito_auth/template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: API Gateway + Cognito User Pools Auth
Resources:
MyApi:
Type: AWS::Serverless::Api
Properties:
StageName: TestStage
Auth:
DefaultAuthorizer: MyLambdaAuthorizer
Authorizers:
MyCognitoAuthorizer:
UserPoolArn: !GetAtt MyCognitoUserPool.Arn

MyLambdaAuthorizer:
FunctionArn: !GetAtt MyAuthFunction.Arn
FunctionInvokeRole: NONE
Identity: # Optional
Header: Authn # Optional; Default: Authorization
ValidationExpression: myexpresso # Optional
ReauthorizeEvery: 33

MyLambdaRequestAuthorizer:
FunctionPayloadType: REQUEST
FunctionArn: !GetAtt MyAuthFunction.Arn
FunctionInvokeRole: !Sub arn:aws:iam::${AWS::AccountId}:role/admin
Identity:
Headers:
- Authorization1
QueryStrings:
- Authorization2
StageVariables:
- Authorization3
Context:
- Authorization4
ReauthorizeEvery: 100

MyFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./src
Handler: index.handler
Runtime: nodejs8.10
Events:
WithNoAuthorizer:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /
Method: get
Auth:
Authorizer: NONE
WithLambdaTokenAuthorizer:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /users
Method: get
Auth:
Authorizer: MyLambdaAuthorizer
WithCognitoAuthorizer:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /users
Method: post
Auth:
Authorizer: MyCognitoAuthorizer
WithLambdaRequestAuthorizer:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /users
Method: delete
Auth:
Authorizer: MyLambdaRequestAuthorizer
WithDefaultAuthorizer:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /users
Method: put

MyAuthFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./src
Handler: index.handler
Runtime: nodejs8.10

MyCognitoUserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: MyUserPool
LambdaConfig:
PreSignUp: !GetAtt PreSignupLambdaFunction.Arn
Policies:
PasswordPolicy:
MinimumLength: 8
Schema:
- AttributeDataType: String
Name: email
Required: false

PreSignupLambdaFunction:
Type: AWS::Serverless::Function
Properties:
InlineCode: |
exports.handler = async (event, context, callback) => {
event.response = { autoConfirmUser: true }
return event
}
Handler: index.handler
MemorySize: 128
Runtime: nodejs8.10
Timeout: 3
# TODO:
# Events:
# CognitoUserPoolPreSignup:
# Type: CognitoUserPool
# Properties:
# UserPool: !Ref MyCognitoUserPool

LambdaCognitoUserPoolExecutionPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt PreSignupLambdaFunction.Arn
Principal: cognito-idp.amazonaws.com
SourceArn: !Sub 'arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${MyCognitoUserPool}'

Outputs:
ApiURL:
Description: "API endpoint URL for Prod environment"
Value: !Sub 'https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/'
78 changes: 75 additions & 3 deletions samtranslator/model/api/api_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from samtranslator.model.intrinsics import ref
from samtranslator.model.apigateway import (ApiGatewayDeployment, ApiGatewayRestApi,
ApiGatewayStage)
ApiGatewayStage, ApiGatewayAuthorizer)
from samtranslator.model.exceptions import InvalidResourceException
from samtranslator.model.s3_utils.uri_parser import parse_s3_uri
from samtranslator.region_configuration import RegionConfiguration
Expand All @@ -15,9 +15,13 @@
# Default the Cors Properties to '*' wildcard. Other properties are actually Optional
CorsProperties.__new__.__defaults__ = (None, None, _CORS_WILDCARD, None)

AuthProperties = namedtuple("_AuthProperties", ["Authorizers", "DefaultAuthorizer"])
AuthProperties.__new__.__defaults__ = (None, None)


class ApiGenerator(object):

def __init__(self, logical_id, cache_cluster_enabled, cache_cluster_size, variables, depends_on, definition_body, definition_uri, name, stage_name, endpoint_configuration=None, method_settings=None, binary_media=None, cors=None):
def __init__(self, logical_id, cache_cluster_enabled, cache_cluster_size, variables, depends_on, definition_body, definition_uri, name, stage_name, endpoint_configuration=None, method_settings=None, binary_media=None, cors=None, auth=None):
"""Constructs an API Generator class that generates API Gateway resources

:param logical_id: Logical id of the SAM API Resource
Expand All @@ -43,6 +47,7 @@ def __init__(self, logical_id, cache_cluster_enabled, cache_cluster_size, variab
self.method_settings = method_settings
self.binary_media = binary_media
self.cors = cors
self.auth = auth

def _construct_rest_api(self):
"""Constructs and returns the ApiGateway RestApi.
Expand All @@ -61,12 +66,12 @@ def _construct_rest_api(self):
# to Regional which is the only supported config.
self._set_endpoint_configuration(rest_api, "REGIONAL")


if self.definition_uri and self.definition_body:
raise InvalidResourceException(self.logical_id,
"Specify either 'DefinitionUri' or 'DefinitionBody' property and not both")

self._add_cors()
self._add_auth()

if self.definition_uri:
rest_api.BodyS3Location = self._construct_body_s3_dict()
Expand Down Expand Up @@ -208,6 +213,73 @@ def _add_cors(self):
# Assign the Swagger back to template
self.definition_body = editor.swagger

def _add_auth(self):
keetonian marked this conversation as resolved.
Show resolved Hide resolved
"""
Add Auth configuration to the Swagger file, if necessary
"""

if not self.auth:
return

INVALID_ERROR = "Invalid value for 'Auth' property"
keetonian marked this conversation as resolved.
Show resolved Hide resolved

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

if self.auth and not self.definition_body:
raise InvalidResourceException(self.logical_id,
"Auth works only with inline Swagger specified in "
"'DefinitionBody' property")

# Make sure keys in the dict are recognized
if not all(key in AuthProperties._fields for key in self.auth.keys()):
raise InvalidResourceException(self.logical_id, INVALID_ERROR)

if not SwaggerEditor.is_valid(self.definition_body):
raise InvalidResourceException(self.logical_id, "Unable to add Auth configuration because "
"'DefinitionBody' does not contain a valid Swagger")
swagger_editor = SwaggerEditor(self.definition_body)
auth_properties = AuthProperties(**self.auth)
keetonian marked this conversation as resolved.
Show resolved Hide resolved
authorizers = self._get_authorizers(auth_properties.Authorizers)

if authorizers:
keetonian marked this conversation as resolved.
Show resolved Hide resolved
swagger_editor.add_authorizers(authorizers)
self._set_default_authorizer(swagger_editor, authorizers, auth_properties.DefaultAuthorizer)

# Assign the Swagger back to template
self.definition_body = swagger_editor.swagger

def _get_authorizers(self, authorizers_config):
if not authorizers_config:
return None

authorizers = {}

for authorizerName, authorizer in authorizers_config.items():
authorizers[authorizerName] = ApiGatewayAuthorizer(
api_logical_id=self.logical_id,
name=authorizer.get('Name'),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this and other instances. Name is no longer in the spec.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

user_pool_arn=authorizer.get('UserPoolArn'),
function_arn=authorizer.get('FunctionArn'),
identity=authorizer.get('Identity'),
function_payload_type=authorizer.get('FunctionPayloadType'),
function_invoke_role=authorizer.get('FunctionInvokeRole')
)

return authorizers

def _set_default_authorizer(self, swagger_editor, authorizers, default_authorizer):
if not default_authorizer:
return

if not authorizers.get(default_authorizer):
raise InvalidResourceException(self.logical_id, "Unable to set DefaultAuthorizer because '" +
default_authorizer + "' was not defined in 'Authorizers'")

for path in swagger_editor.iter_on_path():
swagger_editor.set_path_default_authorizer(path, default_authorizer, authorizers=authorizers)

def _set_endpoint_configuration(self, rest_api, value):
"""
Sets endpoint configuration property of AWS::ApiGateway::RestApi resource
Expand Down
Loading