[PATCH 3/5] REST: Enable token auth support

Andrew Donnellan andrew.donnellan at au1.ibm.com
Tue Jun 13 21:38:35 AEST 2017


On 10/06/17 03:25, Stephen Finucane wrote:
> Token authentication is generally viewed as a more secure option for API
> authentication than storing a username and password.
>
> Django REST Framework gives us a TokenAuthentication class and an authtoken
> app that we can use to generate random tokens and authenticate to API
> endpoints. Enable this support and add some tests to validate correct
> behavior.
>
> Signed-off-by: Andrew Donnellan <andrew.donnellan at au1.ibm.com>
> Signed-off-by: Stephen Finucane <stephen at that.guru>

Changes from my RFC (that I spotted on first reading):

* add token property on Person
* add tests
* split out regenerate_token()

All looks good to me!


> ---
>  patchwork/models.py             | 13 +++++++++++++
>  patchwork/settings/base.py      |  6 ++++++
>  patchwork/tests/test_bundles.py | 24 ++++++++++++++++++++++++
>  patchwork/views/bundle.py       | 18 +++++++++++-------
>  patchwork/views/utils.py        | 14 ++++++++++++++
>  5 files changed, 68 insertions(+), 7 deletions(-)
>
> diff --git a/patchwork/models.py b/patchwork/models.py
> index 943a601..dcb4c55 100644
> --- a/patchwork/models.py
> +++ b/patchwork/models.py
> @@ -37,6 +37,9 @@ from patchwork.compat import is_authenticated
>  from patchwork.fields import HashField
>  from patchwork.hasher import hash_diff
>
> +if settings.ENABLE_REST_API:
> +    from rest_framework.authtoken.models import Token
> +
>
>  @python_2_unicode_compatible
>  class Person(models.Model):
> @@ -162,6 +165,16 @@ class UserProfile(models.Model):
>      def n_todo_patches(self):
>          return self.todo_patches().count()
>
> +    @property
> +    def token(self):
> +        if not settings.ENABLE_REST_API:
> +            return
> +
> +        try:
> +            return Token.objects.get(user=self.user)
> +        except Token.DoesNotExist:
> +            return
> +
>      def todo_patches(self, project=None):
>          # filter on project, if necessary
>          if project:
> diff --git a/patchwork/settings/base.py b/patchwork/settings/base.py
> index 26c75c9..6fd98a7 100644
> --- a/patchwork/settings/base.py
> +++ b/patchwork/settings/base.py
> @@ -143,6 +143,7 @@ try:
>
>      INSTALLED_APPS += [
>          'rest_framework',
> +        'rest_framework.authtoken',
>          'django_filters',
>      ]
>  except ImportError:
> @@ -158,6 +159,11 @@ REST_FRAMEWORK = {
>          'rest_framework.filters.SearchFilter',
>          'rest_framework.filters.OrderingFilter',
>      ),
> +    'DEFAULT_AUTHENTICATION_CLASSES': (
> +        'rest_framework.authentication.SessionAuthentication',
> +        'rest_framework.authentication.BasicAuthentication',
> +        'rest_framework.authentication.TokenAuthentication',
> +    ),
>      'SEARCH_PARAM': 'q',
>      'ORDERING_PARAM': 'order',
>  }
> diff --git a/patchwork/tests/test_bundles.py b/patchwork/tests/test_bundles.py
> index cdc7ee0..4461e32 100644
> --- a/patchwork/tests/test_bundles.py
> +++ b/patchwork/tests/test_bundles.py
> @@ -37,6 +37,7 @@ from patchwork.tests.utils import create_bundle
>  from patchwork.tests.utils import create_patches
>  from patchwork.tests.utils import create_project
>  from patchwork.tests.utils import create_user
> +from patchwork.views import utils as view_utils
>
>
>  def bundle_url(bundle):
> @@ -311,6 +312,7 @@ class BundlePrivateViewTest(BundleTestBase):
>          self.assertEqual(response.status_code, 404)
>
>
> + at unittest.skipUnless(settings.ENABLE_REST_API, 'requires ENABLE_REST_API')
>  class BundlePrivateViewMboxTest(BundlePrivateViewTest):
>
>      """Ensure that non-owners can't view private bundle mboxes"""
> @@ -342,6 +344,28 @@ class BundlePrivateViewMboxTest(BundlePrivateViewTest):
>          response = self.client.get(self.url, HTTP_AUTHORIZATION=auth_string)
>          self.assertEqual(response.status_code, 404)
>
> +    def test_private_bundle_mbox_token_auth(self):
> +        self.client.logout()
> +
> +        # create tokens for both users
> +        for user in [self.user, self.other_user]:
> +            view_utils.regenerate_token(user)
> +
> +        def _get_auth_string(user):
> +            return 'Token {}'.format(str(user.profile.token))
> +
> +        # Check we can view as owner
> +        auth_string = _get_auth_string(self.user)
> +        response = self.client.get(self.url, HTTP_AUTHORIZATION=auth_string)
> +
> +        self.assertEqual(response.status_code, 200)
> +        self.assertContains(response, self.patches[0].name)
> +
> +        # Check we can't view as another user
> +        auth_string = _get_auth_string(self.other_user)
> +        response = self.client.get(self.url, HTTP_AUTHORIZATION=auth_string)
> +        self.assertEqual(response.status_code, 404)
> +
>
>  class BundleCreateFromListTest(BundleTestBase):
>
> diff --git a/patchwork/views/bundle.py b/patchwork/views/bundle.py
> index 387b7c6..2d18571 100644
> --- a/patchwork/views/bundle.py
> +++ b/patchwork/views/bundle.py
> @@ -36,19 +36,23 @@ from patchwork.views import generic_list
>  from patchwork.views.utils import bundle_to_mbox
>
>  if settings.ENABLE_REST_API:
> -    from rest_framework.authentication import BasicAuthentication  # noqa
> +    from rest_framework.authentication import SessionAuthentication
>      from rest_framework.exceptions import AuthenticationFailed
> +    from rest_framework.settings import api_settings
>
>
>  def rest_auth(request):
>      if not settings.ENABLE_REST_API:
>          return request.user
> -    try:
> -        auth_result = BasicAuthentication().authenticate(request)
> -        if auth_result:
> -            return auth_result[0]
> -    except AuthenticationFailed:
> -        pass
> +    for auth in api_settings.DEFAULT_AUTHENTICATION_CLASSES:
> +        if auth == SessionAuthentication:
> +            continue
> +        try:
> +            auth_result = auth().authenticate(request)
> +            if auth_result:
> +                return auth_result[0]
> +        except AuthenticationFailed:
> +            pass
>      return request.user
>
>
> diff --git a/patchwork/views/utils.py b/patchwork/views/utils.py
> index 5528ee7..84682b8 100644
> --- a/patchwork/views/utils.py
> +++ b/patchwork/views/utils.py
> @@ -26,6 +26,7 @@ from email.parser import HeaderParser
>  import email.utils
>  import re
>
> +from django.conf import settings
>  from django.http import Http404
>  from django.utils import six
>
> @@ -33,6 +34,9 @@ from patchwork.models import Comment
>  from patchwork.models import Patch
>  from patchwork.models import Series
>
> +if settings.ENABLE_REST_API:
> +    from rest_framework.authtoken.models import Token
> +
>
>  class PatchMbox(MIMENonMultipart):
>      patch_charset = 'utf-8'
> @@ -181,3 +185,13 @@ def series_to_mbox(series):
>          mbox.append(patch_to_mbox(dep.patch))
>
>      return '\n'.join(mbox)
> +
> +
> +def regenerate_token(user):
> +    """Generate (or regenerate) user API tokens.
> +
> +    Arguments:
> +        user: The User object to generate a token for.
> +    """
> +    Token.objects.filter(user=user).delete()
> +    Token.objects.create(user=user)
>

-- 
Andrew Donnellan              OzLabs, ADL Canberra
andrew.donnellan at au1.ibm.com  IBM Australia Limited



More information about the Patchwork mailing list