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

Add support for custom authorizers with SAM #580

Merged
merged 4 commits into from
Oct 25, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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