Skip to content

Commit

Permalink
Merge branch 'multi-lambda-auth'
Browse files Browse the repository at this point in the history
* multi-lambda-auth:
  Add documentation for authorizers
  Add support for builtin API Gateway authorizers
  • Loading branch information
jamesls committed Jun 22, 2017
2 parents 9f6705a + f40f7d7 commit 4d1751d
Show file tree
Hide file tree
Showing 24 changed files with 1,670 additions and 148 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Next Release (TBD)
(`#246 <https://github.com/awslabs/chalice/issues/246>`__,
`#330 <https://github.com/awslabs/chalice/issues/330>`__,
`#380 <https://github.com/awslabs/chalice/pull/380>`__)
* Add support for built-in authorizers
(`#356 <https://github.com/awslabs/chalice/issues/356>`__)


0.9.0
Expand Down
3 changes: 2 additions & 1 deletion chalice/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
from chalice.app import (
ChaliceViewError, BadRequestError, UnauthorizedError, ForbiddenError,
NotFoundError, ConflictError, TooManyRequestsError, Response, CORSConfig,
CustomAuthorizer, CognitoUserPoolAuthorizer, IAMAuthorizer
CustomAuthorizer, CognitoUserPoolAuthorizer, IAMAuthorizer,
AuthResponse, AuthRoute
)

__version__ = '0.9.0'
139 changes: 139 additions & 0 deletions chalice/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ def __init__(self, app_name, configure_logs=True):
self.configure_logs = configure_logs
self.log = logging.getLogger(self.app_name)
self._authorizers = {}
self.builtin_auth_handlers = []
if self.configure_logs:
self._configure_logging()

Expand Down Expand Up @@ -461,6 +462,27 @@ def define_authorizer(self, name, header, auth_type, provider_arns=None):
'provider_arns': provider_arns,
}

def authorizer(self, name=None, **kwargs):
def _register_authorizer(auth_func):
auth_name = name
if auth_name is None:
auth_name = auth_func.__name__
ttl_seconds = kwargs.pop('ttl_seconds', None)
execution_role = kwargs.pop('execution_role', None)
if kwargs:
raise TypeError(
'TypeError: authorizer() got unexpected keyword '
'arguments: %s' % ', '.join(list(kwargs)))
auth_config = BuiltinAuthConfig(
name=auth_name,
handler_string='app.%s' % auth_func.__name__,
ttl_seconds=ttl_seconds,
execution_role=execution_role,
)
self.builtin_auth_handlers.append(auth_config)
return ChaliceAuthorizer(name, auth_func, auth_config)
return _register_authorizer

def route(self, path, **kwargs):
def _register_view(view_func):
self._add_route(path, view_func, **kwargs)
Expand Down Expand Up @@ -626,3 +648,120 @@ def _add_cors_headers(self, response, cors):
for name, value in cors.get_access_control_headers().items():
if name not in response.headers:
response.headers[name] = value


class BuiltinAuthConfig(object):
def __init__(self, name, handler_string, ttl_seconds=None,
execution_role=None):
# We'd also support all the misc config options you can set.
self.name = name
self.handler_string = handler_string
self.ttl_seconds = ttl_seconds
self.execution_role = execution_role


class ChaliceAuthorizer(object):
def __init__(self, name, func, config):
self.name = name
self.func = func
self.config = config

def __call__(self, event, content):
auth_request = self._transform_event(event)
result = self.func(auth_request)
if isinstance(result, AuthResponse):
return result.to_dict(auth_request)
return result

def _transform_event(self, event):
return AuthRequest(event['type'],
event['authorizationToken'],
event['methodArn'])


class AuthRequest(object):
def __init__(self, auth_type, token, method_arn):
self.auth_type = auth_type
self.token = token
self.method_arn = method_arn


class AuthResponse(object):
ALL_HTTP_METHODS = ['DELETE', 'HEAD', 'OPTIONS',
'PATCH', 'POST', 'PUT', 'GET']

def __init__(self, routes, principal_id, context=None):
self.routes = routes
self.principal_id = principal_id
# The request is used to generate full qualified ARNs
# that we need for the resource portion of the returned
# policy.
if context is None:
context = {}
self.context = context

def to_dict(self, request):
return {
'context': self.context,
'principalId': self.principal_id,
'policyDocument': self._generate_policy(request),
}

def _generate_policy(self, request):
allowed_resources = self._generate_allowed_resources(request)
return {
'Version': '2012-10-17',
'Statement': [
{
'Action': 'execute-api:Invoke',
'Effect': 'Allow',
'Resource': allowed_resources,
}
]
}

def _generate_allowed_resources(self, request):
allowed_resources = []
for route in self.routes:
if isinstance(route, AuthRoute):
methods = route.methods
path = route.path
else:
# If 'route' is just a string, then they've
# opted not to use the AuthRoute(), so we'll
# generate a policy that allows all HTTP methods.
methods = self.ALL_HTTP_METHODS
path = route
for method in methods:
allowed_resources.append(
self._generate_arn(path, request, method))
return allowed_resources

def _generate_arn(self, route, request, method='*'):
incoming_arn = request.method_arn
parts = incoming_arn.rsplit(':', 1)
# "arn:aws:execute-api:us-west-2:123:rest-api-id/dev/GET/needs/auth"
# Then we pull out the rest-api-id and stage, such that:
# base = ['rest-api-id', 'stage']
base = parts[-1].split('/')[:2]
# Now we add in the path components and rejoin everything
# back together to make a full arn.
# We're also assuming all HTTP methods (via '*') for now.
# To support per HTTP method routes the API will need to be updated.
# We also need to strip off the leading ``/`` so it can be
# '/'.join(...)'d properly.
base.extend([method, route[1:]])
last_arn_segment = '/'.join(base)
if route == '/':
# We have to special case the '/' case. For whatever
# reason, API gateway adds an extra '/' to the method_arn
# of the auth request, so we need to do the same thing.
last_arn_segment += '/'
final_arn = '%s:%s' % (parts[0], last_arn_segment)
return final_arn


class AuthRoute(object):
def __init__(self, path, methods):
self.path = path
self.methods = methods
32 changes: 32 additions & 0 deletions chalice/app.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class TooManyRequestsError(ChaliceViewError): ...


ALL_ERRORS = ... # type: List[ChaliceViewError]
_BUILTIN_AUTH_FUNC = Callable[
[AuthRequest], Union[AuthResponse, Dict[str, Any]]]


class Authorizer:
Expand Down Expand Up @@ -107,6 +109,7 @@ class Chalice(object):
current_request = ... # type: Request
debug = ... # type: bool
authorizers = ... # type: Dict[str, Dict[str, Any]]
builtin_auth_handlers = ... # type: List[BuiltinAuthConfig]

def __init__(self, app_name: str) -> None: ...

Expand All @@ -116,3 +119,32 @@ class Chalice(object):
def _get_view_function_response(self,
view_function: Callable[..., Any],
function_args: List[Any]) -> Response: ...


class ChaliceAuthorizer(object):
name = ... # type: str
func = ... # type: _BUILTIN_AUTH_FUNC
config = ... # type: BuiltinAuthConfig


class BuiltinAuthConfig(object):
name = ... # type: str
handler_string = ... # type: str


class AuthRequest(object):
auth_type = ... # type: str
token = ... # type: str
method_arn = ... # type: str


class AuthRoute(object):
path = ... # type: str
methods = ... # type: List[str]


class AuthResponse(object):
ALL_HTTP_METHODS = ... # type: List[str]
routes = ... # type: Union[str, AuthRoute]
principal_id = ... # type: str
context = ... # type: Optional[Dict[str, str]]
46 changes: 43 additions & 3 deletions chalice/awsclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import shutil
import json
import re
import uuid

import botocore.session # noqa
from botocore.exceptions import ClientError
Expand Down Expand Up @@ -275,7 +276,7 @@ def get_role_arn_for_name(self, name):
try:
role = client.get_role(RoleName=name)
except client.exceptions.NoSuchEntityException:
raise ValueError("No role ARN found for: %s" % name)
raise ResourceDoesNotExistError("No role ARN found for: %s" % name)
return role['Role']['Arn']

def delete_role_policy(self, role_name, policy_name):
Expand Down Expand Up @@ -507,12 +508,14 @@ def get_sdk_download_stream(self, rest_api_id,
return response['body']

def add_permission_for_apigateway(self, function_name, region_name,
account_id, rest_api_id, random_id):
# type: (str, str, str, str, str) -> None
account_id, rest_api_id, random_id=None):
# type: (str, str, str, str, Optional[str]) -> None
"""Authorize API gateway to invoke a lambda function."""
client = self._client('lambda')
source_arn = self._build_source_arn_str(region_name, account_id,
rest_api_id)
if random_id is None:
random_id = self._random_id()
client.add_permission(
Action='lambda:InvokeFunction',
FunctionName=function_name,
Expand Down Expand Up @@ -564,3 +567,40 @@ def _client(self, service_name):
self._client_cache[service_name] = self._session.create_client(
service_name)
return self._client_cache[service_name]

def add_permission_for_authorizer(self, rest_api_id, function_arn,
random_id=None):
# type: (str, str, Optional[str]) -> None
client = self._client('apigateway')
# This is actually a paginated operation, but botocore does not
# support this style of pagination right now. The max authorizers
# for an API is 10, so we're ok for now. We will need to circle
# back on this eventually.
authorizers = client.get_authorizers(restApiId=rest_api_id)
for authorizer in authorizers['items']:
if function_arn in authorizer['authorizerUri']:
authorizer_id = authorizer['id']
break
else:
raise ResourceDoesNotExistError(
"Unable to find authorizer associated "
"with function ARN: %s" % function_arn)
parts = function_arn.split(':')
region_name = parts[3]
account_id = parts[4]
function_name = parts[-1]
source_arn = ("arn:aws:execute-api:%s:%s:%s/authorizers/%s" %
(region_name, account_id, rest_api_id, authorizer_id))
if random_id is None:
random_id = self._random_id()
self._client('lambda').add_permission(
Action='lambda:InvokeFunction',
FunctionName=function_name,
StatementId=random_id,
Principal='apigateway.amazonaws.com',
SourceArn=source_arn,
)

def _random_id(self):
# type: () -> str
return str(uuid.uuid4())
6 changes: 4 additions & 2 deletions chalice/cli/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,10 @@ def create_config_obj(self, chalice_stage_name=DEFAULT_STAGE_NAME,
user_provided_params['profile'] = self.profile
if api_gateway_stage is not None:
user_provided_params['api_gateway_stage'] = api_gateway_stage
config = Config(chalice_stage_name, user_provided_params,
config_from_disk, default_params)
config = Config(chalice_stage=chalice_stage_name,
user_provided_params=user_provided_params,
config_from_disk=config_from_disk,
default_params=default_params)
return config

def _validate_config_from_disk(self, config):
Expand Down
Loading

0 comments on commit 4d1751d

Please sign in to comment.