From f40f7d75e9931919bc78e3a771bb35201caae785 Mon Sep 17 00:00:00 2001 From: James Saryerwinnie Date: Wed, 21 Jun 2017 15:23:35 -0700 Subject: [PATCH] Add documentation for authorizers Includes api doc updates as well as a topic guide. Also, fix APIGateway documentation to include class definition. That way refs to the :class: work as expected. --- CHANGELOG.rst | 2 + docs/source/api.rst | 150 ++++++++++++++++---- docs/source/index.rst | 1 + docs/source/topics/authorizers.rst | 220 +++++++++++++++++++++++++++++ 4 files changed, 346 insertions(+), 27 deletions(-) create mode 100644 docs/source/topics/authorizers.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b94997be3..542229835 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -19,6 +19,8 @@ Next Release (TBD) (`#246 `__, `#330 `__, `#380 `__) +* Add support for built-in authorizers + (`#356 `__) 0.9.0 diff --git a/docs/source/api.rst b/docs/source/api.rst index f0d3b59c3..ae7d707f8 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -92,6 +92,32 @@ Chalice you would like more control over how CORS is configured, you can provide an instance of :class:`CORSConfig`. + .. method:: authorizer(name, \*\*options) + + Register a built-in authorizer. + + .. code-block:: python + + from chalice import Chalice, AuthResponse + + app = Chalice(app_name="appname") + + @app.authorizer(ttl_seconds=30) + def my_auth(auth_request): + # Validate auth_request.token, and then: + return AuthResponse(routes=['/'], principal_id='username') + + @app.route('/', authorizer=my_auth) + def viewfunction(value): + pass + + :param ttl_seconds: The number of seconds to cache this response. + Subsequent requests that require this authorizer will use a + cached response if available. The default is 300 seconds. + + :param execution_role: An optional IAM role to specify when invoking + the Lambda function associated with the built-in authorizer. + Request ======= @@ -257,45 +283,115 @@ for an ``@app.route(authorizer=...)`` call: The header where the auth token will be specified. +Built-in Authorizers +-------------------- + +These classes are used when defining built-in authoriers in Chalice. + +.. class:: AuthRequest(auth_type, token, method_arn) + + An instance of this class is passed as the first argument + to an authorizer defined via ``@app.authorizer()``. You + generally do not instantiate this class directly. + + .. attribute:: auth_type + + The type of authentication + + .. attribute:: token + + The authorization token. This is usually the value of the + ``Authorization`` header. + + .. attribute:: method_arn + + The ARN of the API gateway being authorized. + +.. class:: AuthResponse(routes, principal_id, context=None) + + .. attribute:: routes + + A list of authorized routes. Each element in the list + can either by a string route such as `"/foo/bar"` or + an instance of ``AuthRoute``. If you specify the URL as + a string, then all supported HTTP methods will be authorized. + If you want to specify which HTTP methods are allowed, you + can use ``AuthRoute``. + + .. attribute:: principal_id + + The principal id of the user. + + .. attribute:: context + + An optional dictionary of key value pairs. This dictionary + will be accessible in the ``app.current_request.context`` + in all subsequent authorized requests for this user. + +.. class:: AuthRoute(path, methods) + + This class be used in the ``routes`` attribute of a + :class:`AuthResponse` instance to get fine grained control + over which HTTP methods are allowed for a given route. + + .. attribute:: path + + The allowed route specified as a string + + .. attribute:: methods + + A list of allowed HTTP methods. + + APIGateway ========== -There is a single instance of :class:`APIGateway` attached to each -:class:`Chalice` object under the ``api`` attribute. +.. class:: APIGateway() + + This class is used to control + how API Gateway interprets ``Content-Type`` headers in both requests and + responses. + + There is a single instance of this class attached to each + :class:`Chalice` object under the ``api`` attribute. -.. attribute:: default_binary_types + .. attribute:: default_binary_types - The value of ``default_binary_types`` are the ``Content-Types`` that are - considered binary by default. This value should not be changed, instead you - should modify the ``binary_types`` list to change the behavior of a content - type. Its value is: ``application/octet-stream``, ``application/x-tar``, ``application/zip``, ``audio/basic``, ``audio/ogg``, ``audio/mp4``, ``audio/mpeg``, ``audio/wav``, ``audio/webm``, ``image/png``, ``image/jpg``, ``image/gif``, ``video/ogg``, ``video/mpeg``, ``video/webm``. + The value of ``default_binary_types`` are the ``Content-Types`` that are + considered binary by default. This value should not be changed, instead you + should modify the ``binary_types`` list to change the behavior of a content + type. Its value is: ``application/octet-stream``, ``application/x-tar``, + ``application/zip``, ``audio/basic``, ``audio/ogg``, ``audio/mp4``, + ``audio/mpeg``, ``audio/wav``, ``audio/webm``, ``image/png``, + ``image/jpg``, ``image/gif``, ``video/ogg``, ``video/mpeg``, + ``video/webm``. -.. attribute:: binary_types + .. attribute:: binary_types - The value of ``binary_types`` controls how API Gateway interprets requests - and responses as detailed below. + The value of ``binary_types`` controls how API Gateway interprets requests + and responses as detailed below. - If an incoming request has a ``Content-Type`` header value that is present - in the ``binary_types`` list it will be assumed that its body is a sequence - of raw bytes. You can access these bytes by accessing the - ``app.current_request.raw_body`` property. + If an incoming request has a ``Content-Type`` header value that is present + in the ``binary_types`` list it will be assumed that its body is a sequence + of raw bytes. You can access these bytes by accessing the + ``app.current_request.raw_body`` property. - If an outgoing response from ``Chalice`` has a header ``Content-Type`` that - matches one of the ``binary_types`` its body must be a ``bytes`` type object. - It is important to note that originating request must have the ``Accept`` - header for the same type as the ``Content-Type`` on the response. Otherwise - a ``400`` error will be returned. + If an outgoing response from ``Chalice`` has a header ``Content-Type`` that + matches one of the ``binary_types`` its body must be a ``bytes`` type object. + It is important to note that originating request must have the ``Accept`` + header for the same type as the ``Content-Type`` on the response. Otherwise + a ``400`` error will be returned. - Implementation note: API Gateway and Lambda communicate through a JSON event - which is encoded using ``UTF-8``. The raw bytes are temporarily encoded - using + Implementation note: API Gateway and Lambda communicate through a JSON event + which is encoded using ``UTF-8``. The raw bytes are temporarily encoded + using - base64 when being passed between API Gateway and Labmda. In the worst case - this encoding can cause the binary body to be inflated up to ``4/3`` its - original size. Lambda only accepts an event up to ``6mb``, which means even - if your binary data was not quite at that limit, with the base64 encoding it - may exceed that limit. This will manifest as a ``502`` Bad Gateway error. + base64 when being passed between API Gateway and Labmda. In the worst case + this encoding can cause the binary body to be inflated up to ``4/3`` its + original size. Lambda only accepts an event up to ``6mb``, which means even + if your binary data was not quite at that limit, with the base64 encoding it + may exceed that limit. This will manifest as a ``502`` Bad Gateway error. CORS diff --git a/docs/source/index.rst b/docs/source/index.rst index 2e466f2e1..efee2d207 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -64,6 +64,7 @@ Topics topics/packaging topics/pyversion topics/cfn + topics/authorizers API Reference diff --git a/docs/source/topics/authorizers.rst b/docs/source/topics/authorizers.rst new file mode 100644 index 000000000..78a2aa77c --- /dev/null +++ b/docs/source/topics/authorizers.rst @@ -0,0 +1,220 @@ +Authorization +============= + +Chalice supports multiple mechanisms for authorization. This topic +covers how you can integrate authorization into your Chalice applications. + +In Chalice, all the authorizers are configured per-route and specified +using the ``authorizer`` kwarg to an ``@app.route()`` call. You +control which type of authorizer to use based on what's passed as the +``authorizer`` kwarg. You can use the same authorizer instance for +multiple routes. + +The first set of authorizers chalice supports cover the scenario where +you have some existing authorization mechanism that you just want your +Chalice app to use. + +Chalice also supports built-in authorizers, which allows Chalice to +manage your custom authorizers as part of ``chalice deploy``. This is +covered in the Built-in Authorizers section. + + +AWS IAM Authorizer +------------------ + +The IAM Authorizer allows you to control access to API Gateway with +`IAM permissions`_ + +To associate an IAM authorizer with a route in chalice, you use the +:class:`IAMAUthorizer` class: + +.. code-block:: python + + authorizer = IAMAuthorizer() + + @app.route('/iam-auth', methods=['GET'], authorizer=authorizer) + def authenticated(): + return {"success": True} + + +See the `API Gateway documentation +`__ +for more information on controlling access to API Gateway with IAM permissions. + +Amazon Cognito User Pools +------------------------- + +In addition to using IAM roles and policies with the :class:`IAMAuthorizer` you +can also use a `Cognito user pools`_ to control who can access your Chalice +app. A cognito user pool serves as your own identity provider to maintain a +user directory. + +To integrate Cognito user pools with Chalice, you'll need to have an existing +cognito user pool configured. + + +.. code-block:: python + + authorizer = CognitoUserPoolAuthorizer( + 'MyPool', provider_arns=['arn:aws:cognito:...:userpool/name']) + + @app.route('/user-pools', methods=['GET'], authorizer=authorizer) + def authenticated(): + return {"sucecss": True} + + +For more information about using Cognito user pools with API Gateway, +see the `Use Amazon Cognito User Pools documentation +`__. + + +Custom Authorizers +------------------ + +API Gateway also lets you write custom authorizers using a Lambda function. +You can configure a Chalice route to use a pre-existing Lambda function as +a custom authorizer. If you also want to write and manage your Lambda +authorizer using Chalice, see the next section, Built-in Authorizers. + +To connect an existing Lambda function as a custom authorizer in chalice, +you use the ``CustomAuthorizer`` class: + +.. 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 {"success": True} + + +Built-in Authorizers +-------------------- + +The ``IAMAuthorizer``, ``CognitoUserPoolAuthorizer``, and the +``CustomAuthorizer`` classes are all for cases where you have existing +resources for managing authorization and you want to wire them together with +your Chalice app. A Built-in authorizer is used when you'd like to write your +custom authorizer in Chalice, and have the additional Lambda functions managed +when you run ``chalice deploy/delete``. This section will cover how to use the +built-in authorizers in chalice. + +Creating an authorizer in chalice requires you use the ``@app.authorizer`` +decorator to a function. The function must accept a single arg, which will be +an instance of :class:`AuthRequest`. The function must return a +:class:`AuthResponse`. As an example, we'll port the example from the `API +Gateway documentation`_. First, we'll show the code and then walk through it: + +.. code-block:: python + + from chalice import Chalice, AuthResponse + + app = Chalice(app_name='demoauth1') + + + @app.authorizer() + def demo_auth(auth_request): + token = auth_request.token + # This is just for demo purposes as shown in the API Gateway docs. + # Normally you'd call an oauth provider, validate the + # jwt token, etc. + # In this exampe, the token is treated as the status for demo + # purposes. + if token == 'allow': + return AuthResponse(routes=['/'], principal_id='user') + else: + # By specifying an empty list of routes, + # we're saying this user is not authorized + # for any URLs, which will result in an + # Unauthorized response. + return AuthResponse(routes=[], principal_id='user') + + + @app.route('/', authorizer=demo_auth) + def index(): + return {'context': app.current_request.context} + + +In the example above we define a built-in authorizer by decorating +the ``demo_auth`` function with the ``@app.authorizer()`` decorator. +Note you must use ``@app.authorizer()`` and not ``@app.authorizer``. +A built-in authorizer function has this type signature:: + + def auth_handler(auth_request: AuthRequest) -> AuthResponse: ... + +Within the auth handler you must determine if the request is +authorized or not. The ``AuthResponse`` contains the allowed +URLs as well as the principal id of the user. You can optionally +return a dictionary of key value pairs (as the ``context`` kwarg). +This dictionary will be passed through on subsequent requests. +In our example above we're not using the context dictionary. + +Now let's deploy our app. As usual, we just need to run +``chalice deploy`` and chalice will automatically deploy all the +necessary Lambda functions for us. + +Now when we try to make a request, we'll get an Unauthorized error:: + + $ http https://api.us-west-2.amazonaws.com/dev/ + HTTP/1.1 401 Unauthorized + + { + "message": "Unauthorized" + } + +If we add the appropriate authorization header, we'll see the call succeed:: + + $ http https://api.us-west-2.amazonaws.com/dev/ 'Authorization: allow' + HTTP/1.1 200 OK + + { + "context": { + "accountId": "12345", + "apiId": "api", + "authorizer": { + "principalId": "user" + }, + "httpMethod": "GET", + "identity": { + "accessKey": null, + "accountId": null, + "apiKey": "", + "caller": null, + "cognitoAuthenticationProvider": null, + "cognitoAuthenticationType": null, + "cognitoIdentityId": null, + "cognitoIdentityPoolId": null, + "sourceIp": "1.1.1.1", + "user": null, + "userAgent": "HTTPie/0.9.9", + "userArn": null + }, + "path": "/dev/", + "requestId": "d35d2063-56be-11e7-9ce1-dd61c24a3668", + "resourceId": "id", + "resourcePath": "/", + "stage": "dev" + } + } + +The low level API for API Gateway's custom authorizer feature requires +that an IAM policy must be returned. The :class:`AuthResponse` class we're +using is a wrapper over building the IAM policy ourself. If you want +low level control and would prefer to contruct the IAM policy yourself +you can return a dictionary of the IAM policy instead of an instance of +:class:`AuthResponse`. If you do that, the dictionary is returned +without modification back to API Gateway. + +For more information on custom authorizers, see the +`Use API Gateway Custom Authorizers +`__ +page in the API Gateway user guide. + + +.. _IAM permissions: http://docs.aws.amazon.com/IAM/latest/UserGuide/access_permissions.html +.. _Cognito User Pools: http://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html +.. _API Gateway documentation: http://docs.aws.amazon.com/apigateway/latest/developerguide/use-custom-authorizer.html#api-gateway-custom-authorizer-lambda-function-create