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 back custom auth #322

Merged
merged 9 commits into from
May 2, 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
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[run]
branch = True
[report]
exclude_lines =
raise NotImplementedError.*
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Next Release (TBD)
(`#311 <https://github.com/awslabs/chalice/issues/310>`__)
* Fix content type validation when charset is provided
(`#306 <https://github.com/awslabs/chalice/issues/306>`__)
* Add back custom authorizer support
(`#322 <https://github.com/awslabs/chalice/pull/322>`__)


0.8.0
Expand Down
43 changes: 34 additions & 9 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,7 @@ Tutorial: Using Custom Authentication
AWS API Gateway routes can be authenticated in multiple ways:

- API Key
- Cognito User Pools
- Custom Auth Handler

API Key
Expand All @@ -960,21 +961,45 @@ Only requests sent with a valid `X-Api-Key` header will be accepted.
Using Amazon Cognito User Pools
-------------------------------

To integrate with cognito user pools, you can use the ``define_authorizer`` method
on the ``app`` object.
To integrate with cognito user pools, you can use the
``CognitoUserPoolAuthorizer`` object:

.. code-block:: python

@app.route('/user-pools', methods=['GET'], authorizer_name='MyPool')
authorizer = CognitoUserPoolAuthorizer(
'MyPool', header='Authorization',
provider_arns=['arn:aws:cognito:...:userpool/name'])

@app.route('/user-pools', methods=['GET'], authorizer=authorizer)
def authenticated():
return {"secure": True}

app.define_authorizer(
name='MyPool',
header='Authorization',
auth_type='cognito_user_pools',
provider_arns=['arn:aws:cognito:...:userpool/name']
)

Note, earlier versions of chalice also have an ``app.define_authorizer``
method as well as an ``authorizer_name`` argument on the ``@app.route(...)``
method. This approach is deprecated in favor of ``CognitoUserPoolAuthorizer``
and the ``authorizer`` argument in the ``@app.route(...)`` method.
``app.define_authorizer`` will be removed in future versions of chalice.


Using Custom Authorizers
------------------------

To integrate with custom authorizers, you can use the ``CustomAuthorizer`` method
on the ``app`` object. You'll need to set the ``authorizer_uri``
to the URI of your lambda function.

.. code-block:: python

authorizer = CustomAuthorizer(
'MyCustomAuth', header='Authorization',
authorizer_uri=('arn:aws:apigateway:region:lambda:path/2015-03-01'
'/functions/arn:aws:lambda:region:account-id:'
'function:FunctionName/invocations'))

@app.route('/custom-auth', methods=['GET'], authorizer=authorizer)
def authenticated():
return {"secure": True}


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

__version__ = '0.8.0'
76 changes: 71 additions & 5 deletions chalice/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import json
import traceback
import decimal
import warnings
from collections import Mapping

# Implementation note: This file is intended to be a standalone file
Expand Down Expand Up @@ -100,6 +101,67 @@ def __repr__(self):
return 'CaseInsensitiveMapping(%s)' % repr(self._dict)


class Authorizer(object):
name = ''

def to_swagger(self):
raise NotImplementedError("to_swagger")


class CognitoUserPoolAuthorizer(Authorizer):

_AUTH_TYPE = 'cognito_user_pools'

def __init__(self, name, provider_arns, header='Authorization'):
self.name = name
self._header = header
if not isinstance(provider_arns, list):
# This class is used directly by users so we're
# adding some validation to help them troubleshoot
# potential issues.
raise TypeError(
"provider_arns should be a list of ARNs, received: %s"
% provider_arns)
self._provider_arns = provider_arns

def to_swagger(self):
return {
'in': 'header',
'type': 'apiKey',
'name': self._header,
'x-amazon-apigateway-authtype': self._AUTH_TYPE,
'x-amazon-apigateway-authorizer': {
'type': self._AUTH_TYPE,
'providerARNs': self._provider_arns,
}
}


class CustomAuthorizer(Authorizer):

_AUTH_TYPE = 'custom'

def __init__(self, name, authorizer_uri, ttl_seconds=300,
header='Authorization'):
self.name = name
self._header = header
self._authorizer_uri = authorizer_uri
self._ttl_seconds = ttl_seconds

def to_swagger(self):
return {
'in': 'header',
'type': 'apiKey',
'name': self._header,
'x-amazon-apigateway-authtype': self._AUTH_TYPE,
'x-amazon-apigateway-authorizer': {
'type': 'token',
'authorizerUri': self._authorizer_uri,
'authorizerResultTtlInSeconds': self._ttl_seconds,
}
}


class CORSConfig(object):
"""A cors configuration to attach to a route."""

Expand Down Expand Up @@ -204,7 +266,7 @@ class RouteEntry(object):
def __init__(self, view_function, view_name, path, methods,
authorizer_name=None,
api_key_required=None, content_types=None,
cors=False):
cors=False, authorizer=None):
self.view_function = view_function
self.view_name = view_name
self.uri_pattern = path
Expand All @@ -225,6 +287,7 @@ def __init__(self, view_function, view_name, path, methods,
elif cors is False:
cors = None
self.cors = cors
self.authorizer = authorizer

def _parse_view_args(self):
if '{' not in self.uri_pattern:
Expand Down Expand Up @@ -284,12 +347,14 @@ def authorizers(self):
return self._authorizers.copy()

def define_authorizer(self, name, header, auth_type, provider_arns=None):
# TODO: double check remaining authorizers. This only handles
# cognito_user_pools.
warnings.warn(
"define_authorizer() is deprecated and will be removed in future "
"versions of chalice. Please use CognitoUserPoolAuthorizer(...) "
"instead", PendingDeprecationWarning)
self._authorizers[name] = {
'header': header,
'auth_type': auth_type,
'provider_arns': provider_arns
'provider_arns': provider_arns,
}

def route(self, path, **kwargs):
Expand All @@ -302,6 +367,7 @@ def _add_route(self, path, view_func, **kwargs):
name = kwargs.pop('name', view_func.__name__)
methods = kwargs.pop('methods', ['GET'])
authorizer_name = kwargs.pop('authorizer_name', None)
authorizer = kwargs.pop('authorizer', None)
api_key_required = kwargs.pop('api_key_required', None)
content_types = kwargs.pop('content_types', ['application/json'])
cors = kwargs.pop('cors', False)
Expand All @@ -319,7 +385,7 @@ def _add_route(self, path, view_func, **kwargs):
"URL paths must be unique." % path)
entry = RouteEntry(view_func, name, path, methods,
authorizer_name, api_key_required,
content_types, cors)
content_types, cors, authorizer)
self.routes[path] = entry

def __call__(self, event, context):
Expand Down
15 changes: 14 additions & 1 deletion chalice/app.pyi
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, List, Any, Callable, Union
from typing import Dict, List, Any, Callable, Union, Optional

class ChaliceError(Exception): ...
class ChaliceViewError(ChaliceError):
Expand All @@ -14,6 +14,18 @@ class TooManyRequestsError(ChaliceViewError): ...

ALL_ERRORS = ... # type: List[ChaliceViewError]


class Authorizer:
name = ... # type: str
def to_swagger(self) -> Dict[str, Any]: ...


class CognitoUserPoolAuthorizer(Authorizer): ...


class CustomAuthorizer(Authorizer): ...


class CORSConfig:
allow_origin = ... # type: str
allow_headers = ... # type: str
Expand Down Expand Up @@ -63,6 +75,7 @@ class RouteEntry(object):
methods = ... # type: List[str]
uri_pattern = ... # type: str
authorizer_name = ... # type: str
authorizer = ... # type: Optional[Authorizer]
api_key_required = ... # type: bool
content_types = ... # type: List[str]
view_args = ... # type: List[str]
Expand Down
42 changes: 37 additions & 5 deletions chalice/deploy/swagger.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import Any, List, Dict # noqa

from chalice.app import Chalice, RouteEntry # noqa
from chalice.app import Chalice, RouteEntry, Authorizer # noqa


class SwaggerGenerator(object):
Expand Down Expand Up @@ -44,15 +44,24 @@ def _add_route_paths(self, api, app):
current = self._generate_route_method(view)
if 'security' in current:
self._add_to_security_definition(
current['security'], api, app.authorizers)
current['security'], api, app.authorizers, view)
swagger_for_path[http_method.lower()] = current
if view.cors is not None:
self._add_preflight_request(view, swagger_for_path)

def _add_to_security_definition(self, security, api_config, authorizers):
# type: (Any, Dict[str, Any], Dict[str, Any]) -> None
def _generate_security_from_auth_obj(self, api_config, authorizer):
# type: (Dict[str, Any], Authorizer) -> None
config = authorizer.to_swagger()
api_config.setdefault(
'securityDefinitions', {})[authorizer.name] = config

def _add_to_security_definition(self, security,
api_config, authorizers, view):
# type: (Any, Dict[str, Any], Dict[str, Any], RouteEntry) -> None
if view.authorizer is not None:
self._generate_security_from_auth_obj(api_config, view.authorizer)
return
for auth in security:
# TODO: Add validation checks for unknown auth references.
name = list(auth.keys())[0]
if name == 'api_key':
# This is just the api_key_required=True config
Expand All @@ -62,8 +71,29 @@ def _add_to_security_definition(self, security, api_config, authorizers):
'in': 'header',
} # type: Dict[str, Any]
else:
# This whole section is deprecated and will
# eventually be removed. This handles the
# authorizers that come in via app.define_authorizer(...)
# The only supported type in this method is
# 'cognito_user_pools'. Everything else goes through the
# preferred ``view.authorizer``.
if name not in authorizers:
error_msg = (
"The authorizer '%s' is not defined. "
"Use app.define_authorizer(...) to define an "
"authorizer." % (name)
)
if authorizers:
error_msg += (
' Defined authorizers in this app: %s' %
', '.join(authorizers))
raise ValueError(error_msg)
authorizer_config = authorizers[name]
auth_type = authorizer_config['auth_type']
if auth_type != 'cognito_user_pools':
raise ValueError(
"Unknown auth type: '%s', must be "
"'cognito_user_pools'" % (auth_type,))
swagger_snippet = {
'in': 'header',
'type': 'apiKey',
Expand Down Expand Up @@ -94,6 +124,8 @@ def _generate_route_method(self, view):
current['security'] = [{'api_key': []}]
if view.authorizer_name:
current['security'] = [{view.authorizer_name: []}]
if view.authorizer:
current['security'] = [{view.authorizer.name: []}]
return current

def _generate_precanned_responses(self):
Expand Down
59 changes: 59 additions & 0 deletions docs/source/upgrading.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,65 @@ interested in the high level changes, see the
`CHANGELOG.rst <https://github.com/awslabs/chalice/blob/master/CHANGELOG.rst>`__)
file.

.. _v0-8-1:

0.8.1
-----

The 0.8.1 changed the preferred way of specifying authorizers for view
functions. You now specify either an instance of
``chalice.CognitoUserPoolAuthorizer`` or ``chalice.CustomAuthorizer``
to an ``@app.route()`` function using the ``authorizer`` argument.

Deprecated:

.. code-block:: python

@app.route('/user-pools', methods=['GET'], authorizer_name='MyPool')
def authenticated():
return {"secure": True}

app.define_authorizer(
name='MyPool',
header='Authorization',
auth_type='cognito_user_pools',
provider_arns=['arn:aws:cognito:...:userpool/name']
)

Equivalent, and preferred way

.. code-block:: python

from chalice import CognitoUserPoolAuthorizer

authorizer = CognitoUserPoolAuthorizer(
'MyPool', header='Authorization',
provider_arns=['arn:aws:cognito:...:userpool/name'])

@app.route('/user-pools', methods=['GET'], authorizer=authorizer)
def authenticated():
return {"secure": True}


The ``define_authorizer`` is still available, but is now deprecated and will
be removed in future versions of chalice. You can also use the new
``authorizer`` argument to provider a ``CustomAuthorizer``:


.. code-block:: python

from chalice import CustomAuthorizer

authorizer = CustomAuthorizer(
'MyCustomAuth', header='Authorization',
authorizer_uri=('arn:aws:apigateway:region:lambda:path/2015-03-01'
'/functions/arn:aws:lambda:region:account-id:'
'function:FunctionName/invocations'))

@app.route('/custom-auth', methods=['GET'], authorizer=authorizer)
def authenticated():
return {"secure": True}


.. _v0-7-0:

Expand Down
Loading