Skip to content

Commit

Permalink
Merge pull request #580 from kyleknap/auth-sam
Browse files Browse the repository at this point in the history
Add support for custom authorizers with SAM
  • Loading branch information
kyleknap authored Oct 25, 2017
2 parents 91ff9d2 + dbbca7b commit f342b33
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 86 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Next Release (TBA)

* Fix issue deploying some packages in Windows with utf-8 characters
(`#560 <https://github.com/aws/chalice/pull/560>`__)
* Add support for custom authorizers with ``chalice package``
(`#580 <https://github.com/aws/chalice/pull/580>`__)


1.0.3
Expand Down
45 changes: 32 additions & 13 deletions chalice/deploy/swagger.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from chalice.app import Chalice, RouteEntry, Authorizer, CORSConfig # noqa
from chalice.app import ChaliceAuthorizer
from chalice.utils import to_cfn_resource_name


class SwaggerGenerator(object):
Expand Down Expand Up @@ -69,30 +70,38 @@ def _add_route_paths(self, api, app):
def _generate_security_from_auth_obj(self, api_config, authorizer):
# type: (Dict[str, Any], Authorizer) -> None
if isinstance(authorizer, ChaliceAuthorizer):
function_name = '%s-%s' % (
self._deployed_resources['api_handler_name'],
authorizer.config.name
)
arn = self._deployed_resources[
'lambda_functions'][function_name]['arn']
auth_config = authorizer.config
config = {
'in': 'header',
'type': 'apiKey',
'name': 'Authorization',
'x-amazon-apigateway-authtype': 'custom',
'x-amazon-apigateway-authorizer': {
'type': 'token',
'authorizerCredentials': auth_config.execution_role,
'authorizerUri': self._uri(arn),
'authorizerResultTtlInSeconds': auth_config.ttl_seconds,
}
'x-amazon-apigateway-authtype': 'custom'
}
api_gateway_authorizer = {
'type': 'token',
'authorizerUri': self._auth_uri(authorizer)
}
if auth_config.execution_role is not None:
api_gateway_authorizer['authorizerCredentials'] = \
auth_config.execution_role
if auth_config.ttl_seconds is not None:
api_gateway_authorizer['authorizerResultTtlInSeconds'] = \
auth_config.ttl_seconds
config['x-amazon-apigateway-authorizer'] = api_gateway_authorizer
else:
config = authorizer.to_swagger()
api_config.setdefault(
'securityDefinitions', {})[authorizer.name] = config

def _auth_uri(self, authorizer):
# type: (ChaliceAuthorizer) -> str
function_name = '%s-%s' % (
self._deployed_resources['api_handler_name'],
authorizer.config.name
)
return self._uri(
self._deployed_resources['lambda_functions'][function_name]['arn'])

def _add_to_security_definition(self, security,
api_config, view):
# type: (Any, Dict[str, Any], RouteEntry) -> None
Expand Down Expand Up @@ -226,3 +235,13 @@ def _uri(self, lambda_arn=None):
'/functions/${APIHandler.Arn}/invocations'
)
}

def _auth_uri(self, authorizer):
# type: (ChaliceAuthorizer) -> Any
return {
'Fn::Sub': (
'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31'
'/functions/${%s.Arn}/invocations' % to_cfn_resource_name(
authorizer.name)
)
}
101 changes: 50 additions & 51 deletions chalice/package.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import os
import copy
import hashlib
import re

from typing import Any, Dict # noqa

Expand All @@ -12,7 +10,7 @@
from chalice.deploy.deployer import ApplicationPolicyHandler
from chalice.constants import DEFAULT_LAMBDA_TIMEOUT
from chalice.constants import DEFAULT_LAMBDA_MEMORY_SIZE
from chalice.utils import OSUtils, UI, serialize_to_json
from chalice.utils import OSUtils, UI, serialize_to_json, to_cfn_resource_name
from chalice.config import Config # noqa
from chalice.app import Chalice # noqa
from chalice.policy import AppPolicyGenerator
Expand All @@ -29,10 +27,8 @@ def create_app_packager(config):
# lambda function is deployed.
SAMTemplateGenerator(
CFNSwaggerGenerator('{region}', {}),
PreconfiguredPolicyGenerator(
config,
ApplicationPolicyHandler(
osutils, AppPolicyGenerator(osutils)))),
ApplicationPolicyHandler(
osutils, AppPolicyGenerator(osutils))),
LambdaDeploymentPackager(
osutils=osutils,
dependency_builder=DependencyBuilder(osutils),
Expand All @@ -45,18 +41,6 @@ class UnsupportedFeatureError(Exception):
pass


class PreconfiguredPolicyGenerator(object):
def __init__(self, config, policy_gen):
# type: (Config, ApplicationPolicyHandler) -> None
self._config = config
self._policy_gen = policy_gen

def generate_policy_from_app_source(self):
# type: () -> Dict[str, Any]
return self._policy_gen.generate_policy_from_app_source(
self._config)


class SAMTemplateGenerator(object):
_BASE_TEMPLATE = {
'AWSTemplateFormatVersion': '2010-09-09',
Expand Down Expand Up @@ -85,51 +69,61 @@ class SAMTemplateGenerator(object):
} # type: Dict[str, Any]

def __init__(self, swagger_generator, policy_generator):
# type: (SwaggerGenerator, PreconfiguredPolicyGenerator) -> None
# type: (SwaggerGenerator, ApplicationPolicyHandler) -> None
self._swagger_generator = swagger_generator
self._policy_generator = policy_generator

def generate_sam_template(self, config, code_uri='<placeholder>'):
# type: (Config, str) -> Dict[str, Any]
self._check_for_unsupported_features(config)
template = copy.deepcopy(self._BASE_TEMPLATE)
resources = {
'APIHandler': self._generate_serverless_function(config, code_uri),
'APIHandler': self._generate_serverless_function(
config, code_uri, 'app.app', 'api'),
'RestAPI': self._generate_rest_api(
config.chalice_app, config.api_gateway_stage),
}
self._add_auth_handlers(resources, config, code_uri)
template['Resources'] = resources
self._update_endpoint_url_output(template, config)
return template

def _check_for_unsupported_features(self, config):
# type: (Config) -> None
if config.chalice_app.builtin_auth_handlers:
# It doesn't look like SAM templates support everything
# we need to fully support built in authorizers.
# See: awslabs/serverless-application-model#49
# and: https://forums.aws.amazon.com/thread.jspa?messageID=787920
#
# We might need to switch to low level cfn to fix this.
raise UnsupportedFeatureError(
"SAM templates do not currently support these "
"built-in auth handlers: %s" % ', '.join(
[c.name for c in
config.chalice_app.builtin_auth_handlers]))
def _add_auth_handlers(self, resources, config, code_uri):
# type: (Dict[str, Any], Config, str) -> None
for auth_config in config.chalice_app.builtin_auth_handlers:
auth_resource_name = to_cfn_resource_name(auth_config.name)
new_config = config.scope(chalice_stage=config.chalice_stage,
function_name=auth_config.name)
resources[auth_resource_name] = self._generate_serverless_function(
new_config, code_uri, auth_config.handler_string, 'authorizer')
resources[auth_resource_name + 'InvokePermission'] = \
self._generate_lambda_permission(auth_resource_name)

def _generate_lambda_permission(self, lambda_ref):
# type: (str) -> Dict[str, Any]
return {
'Type': 'AWS::Lambda::Permission',
'Properties': {
'FunctionName': {'Fn::GetAtt': [lambda_ref, 'Arn']},
'Action': 'lambda:InvokeFunction',
'Principal': 'apigateway.amazonaws.com'
}
}

def _update_endpoint_url_output(self, template, config):
# type: (Dict[str, Any], Config) -> None
url = template['Outputs']['EndpointURL']['Value']['Fn::Sub']
template['Outputs']['EndpointURL']['Value']['Fn::Sub'] = (
url % config.api_gateway_stage)

def _generate_serverless_function(self, config, code_uri):
# type: (Config, str) -> Dict[str, Any]
def _generate_serverless_function(self, config, code_uri, handler_string,
function_type):
# type: (Config, str, str, str) -> Dict[str, Any]
properties = {
'Runtime': config.lambda_python_version,
'Handler': 'app.app',
'Handler': handler_string,
'CodeUri': code_uri,
'Events': self._generate_function_events(config.chalice_app),
'Events': self._generate_function_events(
config.chalice_app, function_type),
'Tags': config.tags,
'Timeout': DEFAULT_LAMBDA_TIMEOUT,
'MemorySize': DEFAULT_LAMBDA_MEMORY_SIZE
Expand All @@ -145,23 +139,24 @@ def _generate_serverless_function(self, config, code_uri):
if not config.manage_iam_role:
properties['Role'] = config.iam_role_arn
else:
properties['Policies'] = [self._generate_iam_policy()]
properties['Policies'] = [self._generate_iam_policy(config)]
return {
'Type': 'AWS::Serverless::Function',
'Properties': properties,
}

def _generate_function_events(self, app):
def _generate_function_events(self, app, function_type):
# type: (Chalice, str) -> Dict[str, Any]
return getattr(
self, '_generate_' + function_type + '_function_events')(app)

def _generate_api_function_events(self, app):
# type: (Chalice) -> Dict[str, Any]
events = {}
for methods in app.routes.values():
for http_method, view in methods.items():
mod_view_name = re.sub(r'[^A-Za-z0-9]+', '', view.view_name)
key_name = ''.join([
mod_view_name, http_method.lower(),
hashlib.md5(
view.view_name.encode('utf-8')).hexdigest()[:4],
])
key_name = to_cfn_resource_name(
view.view_name + http_method.lower())
events[key_name] = {
'Type': 'Api',
'Properties': {
Expand All @@ -172,6 +167,10 @@ def _generate_function_events(self, app):
}
return events

def _generate_authorizer_function_events(self, app):
# type: (Chalice) -> Dict[str, Any]
return {}

def _generate_rest_api(self, app, api_gateway_stage):
# type: (Chalice, str) -> Dict[str, Any]
swagger_definition = self._swagger_generator.generate_swagger(app)
Expand All @@ -184,9 +183,9 @@ def _generate_rest_api(self, app, api_gateway_stage):
'Properties': properties,
}

def _generate_iam_policy(self):
# type: () -> Dict[str, Any]
return self._policy_generator.generate_policy_from_app_source()
def _generate_iam_policy(self, config):
# type: (Config) -> Dict[str, Any]
return self._policy_generator.generate_policy_from_app_source(config)


class AppPackager(object):
Expand Down
15 changes: 15 additions & 0 deletions chalice/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import zipfile
import json
import contextlib
import hashlib
import tempfile
import re
import shutil
import sys
import tarfile
Expand All @@ -20,6 +22,19 @@ class AbortedError(Exception):
pass


def to_cfn_resource_name(name):
# type: (str) -> str
"""Transform a name to a valid cfn name.
This transform ensures that only alphanumeric characters are used
and prevent collisions by appending the hash of the original name.
"""
alphanumeric_only_name = re.sub(r'[^A-Za-z0-9]+', '', name)
return ''.join([
alphanumeric_only_name, hashlib.md5(
name.encode('utf-8')).hexdigest()[:4]])


def remove_stage_from_deployed_values(key, filename):
# type: (str, str) -> None
"""Delete a top level key from the deployed JSON file."""
Expand Down
39 changes: 38 additions & 1 deletion tests/unit/deploy/test_swagger.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from chalice.deploy.swagger import SwaggerGenerator
from chalice.deploy.swagger import SwaggerGenerator, CFNSwaggerGenerator
from chalice import CORSConfig
from chalice.app import CustomAuthorizer, CognitoUserPoolAuthorizer
from chalice.app import IAMAuthorizer, Chalice
Expand Down Expand Up @@ -473,3 +473,40 @@ def foo():
'/2015-03-31/functions/auth_arn/invocations'),
}
}


def test_will_custom_auth_with_cfn(sample_app):
swagger_gen = CFNSwaggerGenerator(
region='us-west-2',
deployed_resources={}
)

# No "name=" kwarg provided should default
# to a name of "auth".
@sample_app.authorizer(ttl_seconds=10, execution_role='arn:role')
def auth(auth_request):
pass

@sample_app.route('/auth', authorizer=auth)
def foo():
pass

doc = swagger_gen.generate_swagger(sample_app)
assert 'securityDefinitions' in doc
assert doc['securityDefinitions']['auth'] == {
'in': 'header',
'name': 'Authorization',
'type': 'apiKey',
'x-amazon-apigateway-authtype': 'custom',
'x-amazon-apigateway-authorizer': {
'type': 'token',
'authorizerCredentials': 'arn:role',
'authorizerResultTtlInSeconds': 10,
'authorizerUri': {
'Fn::Sub': (
'arn:aws:apigateway:${AWS::Region}:lambda:path'
'/2015-03-31/functions/${authfa53.Arn}/invocations'
)
}
}
}
Loading

0 comments on commit f342b33

Please sign in to comment.