From 6a9e4581f650d199524e0a13982ea78998f1e385 Mon Sep 17 00:00:00 2001 From: James Saryerwinnie Date: Mon, 1 May 2017 13:35:21 -0700 Subject: [PATCH 1/9] Add back custom auth This needs to go through the `define_authorizer` in order to work with the swagger generation. --- CHANGELOG.rst | 2 ++ README.rst | 23 +++++++++++++ chalice/app.py | 13 ++++--- chalice/deploy/swagger.py | 31 +++++++++++++++-- tests/unit/deploy/test_swagger.py | 56 +++++++++++++++++++++++++++++-- 5 files changed, 116 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 66dfc9e72..1a8acdc57 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,8 @@ Next Release (TBD) (`#311 `__) * Fix content type validation when charset is provided (`#306 `__) +* Add back custom authorizer support + (`#322 `__) 0.8.0 diff --git a/README.rst b/README.rst index 637cde6b1..d20cde056 100644 --- a/README.rst +++ b/README.rst @@ -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 @@ -976,6 +977,28 @@ on the ``app`` object. provider_arns=['arn:aws:cognito:...:userpool/name'] ) +Using Custom Authorizers +------------------------ + +To integrate with custom authorizers, you can use the ``define_authorizer`` method +on the ``app`` object. You'll need to set the ``auth_type`` to ``custom`` and +the ``authorizer_uri`` to the URI of your lambda function. + +.. code-block:: python + + @app.route('/user-pools', methods=['GET'], authorizer_name='MyCustomAuth') + def authenticated(): + return {"secure": True} + + app.define_authorizer( + name='MyCustomAuth', + header='Authorization', + auth_type='custom', + authorizer_uri=('arn:aws:apigateway:region:lambda:path/2015-03-01' + '/functions/arn:aws:lambda:region:account-id:' + 'function:FunctionName/invocations') + ) + Tutorial: Local Mode ==================== diff --git a/chalice/app.py b/chalice/app.py index cae30375f..074b83872 100644 --- a/chalice/app.py +++ b/chalice/app.py @@ -283,14 +283,17 @@ def _already_configured(self, log): 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. - self._authorizers[name] = { + def define_authorizer(self, name, header, auth_type, + provider_arns=None, authorizer_uri=None): + auth = { 'header': header, 'auth_type': auth_type, - 'provider_arns': provider_arns } + if provider_arns is not None: + auth['provider_arns'] = provider_arns + elif authorizer_uri is not None: + auth['authorizer_uri'] = authorizer_uri + self._authorizers[name] = auth def route(self, path, **kwargs): def _register_view(view_func): diff --git a/chalice/deploy/swagger.py b/chalice/deploy/swagger.py index ccbe14ebb..f985f66e8 100644 --- a/chalice/deploy/swagger.py +++ b/chalice/deploy/swagger.py @@ -23,6 +23,8 @@ class SwaggerGenerator(object): } } # type: Dict[str, Any] + _KNOWN_AUTH_TYPES = ['cognito_user_pools', 'custom'] + def __init__(self, region, lambda_arn): # type: (str, str) -> None self._region = region @@ -52,7 +54,6 @@ def _add_route_paths(self, api, app): def _add_to_security_definition(self, security, api_config, authorizers): # type: (Any, Dict[str, Any], Dict[str, Any]) -> None 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 @@ -62,18 +63,44 @@ def _add_to_security_definition(self, security, api_config, authorizers): 'in': 'header', } # type: Dict[str, Any] else: + 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 not in self._KNOWN_AUTH_TYPES: + raise ValueError( + "Unknown auth type: '%s', must be one of: %s" % + (auth_type, ', '.join(self._KNOWN_AUTH_TYPES))) swagger_snippet = { 'in': 'header', 'type': 'apiKey', 'name': authorizer_config['header'], 'x-amazon-apigateway-authtype': auth_type, 'x-amazon-apigateway-authorizer': { + } + } + if auth_type == 'custom': + auth_config = { + 'type': auth_type, + 'authorizerUri': authorizer_config['authorizer_uri'], + 'authorizerResultTtlInSeconds': 300, + 'type': 'token', + } + elif auth_type == 'cognito_user_pools': + auth_config = { 'type': auth_type, 'providerARNs': authorizer_config['provider_arns'], } - } + swagger_snippet['x-amazon-apigateway-authorizer'] = auth_config api_config.setdefault( 'securityDefinitions', {})[name] = swagger_snippet diff --git a/tests/unit/deploy/test_swagger.py b/tests/unit/deploy/test_swagger.py index 8ffb94cfc..78afbe819 100644 --- a/tests/unit/deploy/test_swagger.py +++ b/tests/unit/deploy/test_swagger.py @@ -1,8 +1,10 @@ from chalice.deploy.swagger import SwaggerGenerator from chalice import CORSConfig +import pytest from pytest import fixture + @fixture def swagger_gen(): return SwaggerGenerator(region='us-west-2', @@ -210,10 +212,10 @@ def foo(name): } -def test_can_add_authorizers(sample_app, swagger_gen): +def test_can_add_cognito_authorizers(sample_app, swagger_gen): @sample_app.route('/api-key-required', authorizer_name='MyUserPool') - def foo(name): + def foo(): return {} # Doesn't matter if you define the authorizer before @@ -239,3 +241,53 @@ def foo(name): 'providerARNs': ['arn:aws:cog:r:1:userpool/name'] } } + + +def test_can_add_custom_authorizer(sample_app, swagger_gen): + @sample_app.route('/custom-auth', authorizer_name='MyAuth') + def foo(): + return {} + + sample_app.define_authorizer('MyAuth', + header='Authorization', + auth_type='custom', + authorizer_uri='arn:aws:...') + + doc = swagger_gen.generate_swagger(sample_app) + single_method = doc['paths']['/custom-auth']['get'] + assert single_method.get('security') == [{'MyAuth': []}] + security_definitions = doc['securityDefinitions'] + assert 'MyAuth' in security_definitions + assert security_definitions['MyAuth'] == { + 'type': 'apiKey', + 'name': 'Authorization', + 'in': 'header', + 'x-amazon-apigateway-authtype': 'custom', + 'x-amazon-apigateway-authorizer': { + 'authorizerUri': 'arn:aws:...', + 'type': 'token', + 'authorizerResultTtlInSeconds': 300 + } + } + + +def test_unknown_auth_raises_error(sample_app, swagger_gen): + @sample_app.route('/unknown', authorizer_name='Unknown') + def foo(): + return {} + + sample_app.define_authorizer( + 'Unknown', header='Authorization', + auth_type='unknown-type', authorizer_uri='arn:aws:...') + + with pytest.raises(ValueError): + swagger_gen.generate_swagger(sample_app) + + +def test_reference_auth_without_defining(sample_app, swagger_gen): + @sample_app.route('/unknown', authorizer_name='NeverDefined') + def foo(): + return {} + + with pytest.raises(ValueError): + swagger_gen.generate_swagger(sample_app) From a820d1fc918b53fb7a47085cce06799500745e23 Mon Sep 17 00:00:00 2001 From: James Saryerwinnie Date: Mon, 1 May 2017 16:40:22 -0700 Subject: [PATCH 2/9] Add auth objects for configuring authorizers --- chalice/app.py | 70 ++++++++++++++++++---- chalice/app.pyi | 9 ++- chalice/deploy/swagger.py | 47 ++++++++------- tests/unit/deploy/test_swagger.py | 97 ++++++++++++++++++++++++------- 4 files changed, 171 insertions(+), 52 deletions(-) diff --git a/chalice/app.py b/chalice/app.py index 074b83872..7caa1aec5 100644 --- a/chalice/app.py +++ b/chalice/app.py @@ -100,6 +100,59 @@ def __repr__(self): return 'CaseInsensitiveMapping(%s)' % repr(self._dict) +class Authorizer(object): + name = '' + + def to_swagger(self): + raise NotImplementedError("to_swagger") + + +class CognitoUserPoolAuthorizer(object): + + _AUTH_TYPE = 'cognito_user_pools' + + def __init__(self, name, header, provider_arns): + self.name = name + self._header = header + 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(object): + + _AUTH_TYPE = 'custom' + + def __init__(self, name, header, authorizer_uri, ttl_seconds=300): + 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.""" @@ -204,7 +257,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 @@ -225,6 +278,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: @@ -283,17 +337,12 @@ def _already_configured(self, log): def authorizers(self): return self._authorizers.copy() - def define_authorizer(self, name, header, auth_type, - provider_arns=None, authorizer_uri=None): - auth = { + def define_authorizer(self, name, header, auth_type, provider_arns=None): + self._authorizers[name] = { 'header': header, 'auth_type': auth_type, + 'provider_arns': provider_arns, } - if provider_arns is not None: - auth['provider_arns'] = provider_arns - elif authorizer_uri is not None: - auth['authorizer_uri'] = authorizer_uri - self._authorizers[name] = auth def route(self, path, **kwargs): def _register_view(view_func): @@ -305,6 +354,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) @@ -322,7 +372,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): diff --git a/chalice/app.pyi b/chalice/app.pyi index e3003e7e6..60d09bfb4 100644 --- a/chalice/app.pyi +++ b/chalice/app.pyi @@ -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): @@ -14,6 +14,12 @@ class TooManyRequestsError(ChaliceViewError): ... ALL_ERRORS = ... # type: List[ChaliceViewError] + +class Authorizer: + name = ... # type: str + def to_swagger(self) -> Dict[str, Any]: ... + + class CORSConfig: allow_origin = ... # type: str allow_headers = ... # type: str @@ -63,6 +69,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] diff --git a/chalice/deploy/swagger.py b/chalice/deploy/swagger.py index f985f66e8..80e84744a 100644 --- a/chalice/deploy/swagger.py +++ b/chalice/deploy/swagger.py @@ -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): @@ -23,8 +23,6 @@ class SwaggerGenerator(object): } } # type: Dict[str, Any] - _KNOWN_AUTH_TYPES = ['cognito_user_pools', 'custom'] - def __init__(self, region, lambda_arn): # type: (str, str) -> None self._region = region @@ -46,13 +44,23 @@ 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: name = list(auth.keys())[0] if name == 'api_key': @@ -63,6 +71,12 @@ 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. " @@ -76,31 +90,20 @@ def _add_to_security_definition(self, security, api_config, authorizers): raise ValueError(error_msg) authorizer_config = authorizers[name] auth_type = authorizer_config['auth_type'] - if auth_type not in self._KNOWN_AUTH_TYPES: + if auth_type != 'cognito_user_pools': raise ValueError( - "Unknown auth type: '%s', must be one of: %s" % - (auth_type, ', '.join(self._KNOWN_AUTH_TYPES))) + "Unknown auth type: '%s', must be " + "'cognito_user_pools'" % (auth_type,)) swagger_snippet = { 'in': 'header', 'type': 'apiKey', 'name': authorizer_config['header'], 'x-amazon-apigateway-authtype': auth_type, 'x-amazon-apigateway-authorizer': { - } - } - if auth_type == 'custom': - auth_config = { - 'type': auth_type, - 'authorizerUri': authorizer_config['authorizer_uri'], - 'authorizerResultTtlInSeconds': 300, - 'type': 'token', - } - elif auth_type == 'cognito_user_pools': - auth_config = { 'type': auth_type, 'providerARNs': authorizer_config['provider_arns'], } - swagger_snippet['x-amazon-apigateway-authorizer'] = auth_config + } api_config.setdefault( 'securityDefinitions', {})[name] = swagger_snippet @@ -121,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): diff --git a/tests/unit/deploy/test_swagger.py b/tests/unit/deploy/test_swagger.py index 78afbe819..ce9a6acab 100644 --- a/tests/unit/deploy/test_swagger.py +++ b/tests/unit/deploy/test_swagger.py @@ -1,5 +1,6 @@ from chalice.deploy.swagger import SwaggerGenerator from chalice import CORSConfig +from chalice.app import CustomAuthorizer, CognitoUserPoolAuthorizer import pytest from pytest import fixture @@ -243,18 +244,55 @@ def foo(): } -def test_can_add_custom_authorizer(sample_app, swagger_gen): - @sample_app.route('/custom-auth', authorizer_name='MyAuth') +def test_unknown_auth_raises_error(sample_app, swagger_gen): + @sample_app.route('/unknown', authorizer_name='Unknown') + def foo(): + return {} + + sample_app.define_authorizer( + 'Unknown', header='Authorization', + auth_type='unknown-type') + + with pytest.raises(ValueError): + swagger_gen.generate_swagger(sample_app) + + +def test_reference_auth_without_defining(sample_app, swagger_gen): + @sample_app.route('/unknown', authorizer_name='NeverDefined') + def foo(): + return {} + + with pytest.raises(ValueError): + swagger_gen.generate_swagger(sample_app) + + +def test_reference_auth_with_other_auth_defined(sample_app, swagger_gen): + @sample_app.route('/api-key-required', + authorizer_name='Unknown') def foo(): return {} - sample_app.define_authorizer('MyAuth', - header='Authorization', - auth_type='custom', - authorizer_uri='arn:aws:...') + # Doesn't matter if you define the authorizer before + # it's referenced. + sample_app.define_authorizer( + name='MyUserPool', + header='Authorization', + auth_type='cognito_user_pools', + provider_arns=['arn:aws:cog:r:1:userpool/name'] + ) + + with pytest.raises(ValueError): + swagger_gen.generate_swagger(sample_app) + + +def test_can_use_authorizer_object(sample_app, swagger_gen): + authorizer = CustomAuthorizer('MyAuth', 'Authorization', 'auth-uri') + @sample_app.route('/auth', authorizer=authorizer) + def auth(): + return {'foo': 'bar'} doc = swagger_gen.generate_swagger(sample_app) - single_method = doc['paths']['/custom-auth']['get'] + single_method = doc['paths']['/auth']['get'] assert single_method.get('security') == [{'MyAuth': []}] security_definitions = doc['securityDefinitions'] assert 'MyAuth' in security_definitions @@ -264,30 +302,49 @@ def foo(): 'in': 'header', 'x-amazon-apigateway-authtype': 'custom', 'x-amazon-apigateway-authorizer': { - 'authorizerUri': 'arn:aws:...', + 'authorizerUri': 'auth-uri', 'type': 'token', 'authorizerResultTtlInSeconds': 300 } } -def test_unknown_auth_raises_error(sample_app, swagger_gen): - @sample_app.route('/unknown', authorizer_name='Unknown') +def test_can_use_cognito_auth_object(sample_app, swagger_gen): + authorizer = CognitoUserPoolAuthorizer('MyUserPool', + header='Authorization', + provider_arns=['myarn']) + @sample_app.route('/api-key-required', authorizer=authorizer) def foo(): return {} - sample_app.define_authorizer( - 'Unknown', header='Authorization', - auth_type='unknown-type', authorizer_uri='arn:aws:...') - - with pytest.raises(ValueError): - swagger_gen.generate_swagger(sample_app) + doc = swagger_gen.generate_swagger(sample_app) + single_method = doc['paths']['/api-key-required']['get'] + assert single_method.get('security') == [{'MyUserPool': []}] + assert 'securityDefinitions' in doc + assert doc['securityDefinitions'].get('MyUserPool') == { + 'in': 'header', + 'type': 'apiKey', + 'name': 'Authorization', + 'x-amazon-apigateway-authtype': 'cognito_user_pools', + 'x-amazon-apigateway-authorizer': { + 'type': 'cognito_user_pools', + 'providerARNs': ['myarn'] + } + } -def test_reference_auth_without_defining(sample_app, swagger_gen): - @sample_app.route('/unknown', authorizer_name='NeverDefined') +def test_auth_defined_for_multiple_methods(sample_app, swagger_gen): + authorizer = CognitoUserPoolAuthorizer('MyUserPool', + header='Authorization', + provider_arns=['myarn']) + @sample_app.route('/pool1', authorizer=authorizer) def foo(): return {} - with pytest.raises(ValueError): - swagger_gen.generate_swagger(sample_app) + @sample_app.route('/pool2', authorizer=authorizer) + def bar(): + return {} + + doc = swagger_gen.generate_swagger(sample_app) + assert 'securityDefinitions' in doc + assert len(doc['securityDefinitions']) == 1 From d51825e66caba3c5044807816c9ad180b03b602b Mon Sep 17 00:00:00 2001 From: James Saryerwinnie Date: Mon, 1 May 2017 17:13:22 -0700 Subject: [PATCH 3/9] Add pending deprecation warning --- chalice/app.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/chalice/app.py b/chalice/app.py index 7caa1aec5..69fb20beb 100644 --- a/chalice/app.py +++ b/chalice/app.py @@ -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 @@ -338,6 +339,10 @@ def authorizers(self): return self._authorizers.copy() def define_authorizer(self, name, header, auth_type, provider_arns=None): + 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, From eb807462e52e6c88569ceecaa416563f528e680a Mon Sep 17 00:00:00 2001 From: James Saryerwinnie Date: Mon, 1 May 2017 17:38:39 -0700 Subject: [PATCH 4/9] Update README with new interface --- README.rst | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/README.rst b/README.rst index d20cde056..4f71938a4 100644 --- a/README.rst +++ b/README.rst @@ -961,43 +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 ``define_authorizer`` method -on the ``app`` object. You'll need to set the ``auth_type`` to ``custom`` and -the ``authorizer_uri`` to the URI of your lambda function. +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 - @app.route('/user-pools', methods=['GET'], authorizer_name='MyCustomAuth') - def authenticated(): - return {"secure": True} - - app.define_authorizer( - name='MyCustomAuth', - header='Authorization', - auth_type='custom', + 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') - ) + 'function:FunctionName/invocations')) + + @app.route('/user-pools', methods=['GET'], authorizer=authorizer) + def authenticated(): + return {"secure": True} Tutorial: Local Mode From ac9ee17f93b90a79b629d222d8c2846debed6f04 Mon Sep 17 00:00:00 2001 From: James Saryerwinnie Date: Mon, 1 May 2017 17:39:23 -0700 Subject: [PATCH 5/9] Add authorizers as top level import --- chalice/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chalice/__init__.py b/chalice/__init__.py index db3836562..335a6912b 100644 --- a/chalice/__init__.py +++ b/chalice/__init__.py @@ -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' From 80392553aa4a5d348256892885f394a73ca68020 Mon Sep 17 00:00:00 2001 From: James Saryerwinnie Date: Tue, 2 May 2017 09:33:45 -0700 Subject: [PATCH 6/9] Incorporate review feedback Also added extra validation to help users. --- README.rst | 2 +- chalice/app.py | 16 +++++++++---- tests/unit/deploy/test_swagger.py | 3 ++- tests/unit/test_app.py | 38 +++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 4f71938a4..791c16010 100644 --- a/README.rst +++ b/README.rst @@ -997,7 +997,7 @@ to the URI of your lambda function. '/functions/arn:aws:lambda:region:account-id:' 'function:FunctionName/invocations')) - @app.route('/user-pools', methods=['GET'], authorizer=authorizer) + @app.route('/custom-auth', methods=['GET'], authorizer=authorizer) def authenticated(): return {"secure": True} diff --git a/chalice/app.py b/chalice/app.py index 69fb20beb..586f51534 100644 --- a/chalice/app.py +++ b/chalice/app.py @@ -108,13 +108,20 @@ def to_swagger(self): raise NotImplementedError("to_swagger") -class CognitoUserPoolAuthorizer(object): +class CognitoUserPoolAuthorizer(Authorizer): _AUTH_TYPE = 'cognito_user_pools' - def __init__(self, name, header, provider_arns): + 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): @@ -130,11 +137,12 @@ def to_swagger(self): } -class CustomAuthorizer(object): +class CustomAuthorizer(Authorizer): _AUTH_TYPE = 'custom' - def __init__(self, name, header, authorizer_uri, ttl_seconds=300): + def __init__(self, name, authorizer_uri, ttl_seconds=300, + header='Authorization'): self.name = name self._header = header self._authorizer_uri = authorizer_uri diff --git a/tests/unit/deploy/test_swagger.py b/tests/unit/deploy/test_swagger.py index ce9a6acab..3378fd8a6 100644 --- a/tests/unit/deploy/test_swagger.py +++ b/tests/unit/deploy/test_swagger.py @@ -286,7 +286,8 @@ def foo(): def test_can_use_authorizer_object(sample_app, swagger_gen): - authorizer = CustomAuthorizer('MyAuth', 'Authorization', 'auth-uri') + authorizer = CustomAuthorizer( + 'MyAuth', authorizer_uri='auth-uri', header='Authorization') @sample_app.route('/auth', authorizer=authorizer) def auth(): return {'foo': 'bar'} diff --git a/tests/unit/test_app.py b/tests/unit/test_app.py index 9344d447d..90e4a1fcc 100644 --- a/tests/unit/test_app.py +++ b/tests/unit/test_app.py @@ -543,3 +543,41 @@ def test_json_body_available_when_content_type_matches(content_type, is_json): assert request.json_body == {'json': 'body'} else: assert request.json_body is None + + +def test_can_serialize_cognito_auth(): + auth = app.CognitoUserPoolAuthorizer( + 'Name', provider_arns=['Foo'], header='Authorization') + assert auth.to_swagger() == { + 'in': 'header', + 'type': 'apiKey', + 'name': 'Authorization', + 'x-amazon-apigateway-authtype': 'cognito_user_pools', + 'x-amazon-apigateway-authorizer': { + 'type': 'cognito_user_pools', + 'providerARNs': ['Foo'], + } + } + + +def test_typecheck_list_type(): + with pytest.raises(TypeError): + app.CognitoUserPoolAuthorizer('Name', 'Authorization', + provider_arns='foo') + + +def test_can_serialize_custom_authorizer(): + auth = app.CustomAuthorizer( + 'Name', 'myuri', ttl_seconds=10, header='NotAuth' + ) + assert auth.to_swagger() == { + 'in': 'header', + 'type': 'apiKey', + 'name': 'NotAuth', + 'x-amazon-apigateway-authtype': 'custom', + 'x-amazon-apigateway-authorizer': { + 'type': 'token', + 'authorizerUri': 'myuri', + 'authorizerResultTtlInSeconds': 10, + } + } From aae3fb288de3e3cddd3dcb6947ce2fe6041eacb8 Mon Sep 17 00:00:00 2001 From: James Saryerwinnie Date: Tue, 2 May 2017 09:38:56 -0700 Subject: [PATCH 7/9] Add upgrading notes --- docs/source/upgrading.rst | 59 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/docs/source/upgrading.rst b/docs/source/upgrading.rst index 0798da249..bf61c7fa3 100644 --- a/docs/source/upgrading.rst +++ b/docs/source/upgrading.rst @@ -7,6 +7,65 @@ interested in the high level changes, see the `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: From 79dd7527dfcbe3df1e119352d729b28a78cae345 Mon Sep 17 00:00:00 2001 From: James Saryerwinnie Date: Tue, 2 May 2017 09:41:40 -0700 Subject: [PATCH 8/9] Add missing typedef --- chalice/app.pyi | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/chalice/app.pyi b/chalice/app.pyi index 60d09bfb4..693f76f8c 100644 --- a/chalice/app.pyi +++ b/chalice/app.pyi @@ -20,6 +20,12 @@ class Authorizer: def to_swagger(self) -> Dict[str, Any]: ... +class CognitoUserPoolAuthorizer(Authorizer): ... + + +class CustomAuthorizer(Authorizer): ... + + class CORSConfig: allow_origin = ... # type: str allow_headers = ... # type: str From 1b223a7e1d1afb50bc4a793a1c86bb69ac68722f Mon Sep 17 00:00:00 2001 From: James Saryerwinnie Date: Tue, 2 May 2017 10:14:13 -0700 Subject: [PATCH 9/9] Ignore not implementederror in coverage These aren't intended to be tested. --- .coveragerc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.coveragerc b/.coveragerc index 398ff08af..c6d6ba42b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,2 +1,5 @@ [run] branch = True +[report] +exclude_lines = + raise NotImplementedError.*