]> git.ipfire.org Git - thirdparty/patchwork.git/commitdiff
REST: Enable token auth support
authorStephen Finucane <stephen@that.guru>
Fri, 9 Jun 2017 17:18:23 +0000 (18:18 +0100)
committerStephen Finucane <stephen@that.guru>
Wed, 14 Jun 2017 08:38:25 +0000 (09:38 +0100)
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@au1.ibm.com>
Signed-off-by: Stephen Finucane <stephen@that.guru>
patchwork/models.py
patchwork/settings/base.py
patchwork/tests/test_bundles.py
patchwork/views/bundle.py
patchwork/views/utils.py

index 943a6013fba6a4e1c4de6dd262ff70118269cace..dcb4c555b420bfda69a9a5ad7d482232f32f3c60 100644 (file)
@@ -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:
index 26c75c957b13fe408a4a0505e7e06c4a933ec0a3..6fd98a71bc3c142d14afc85e7216bc3bf7287f0c 100644 (file)
@@ -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',
 }
index cdc7ee0816ddfe49d3a8b30a8838918c31a6b638..4461e32fc930c58ebb0b513530a37986cb0e3f29 100644 (file)
@@ -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)
 
 
+@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):
 
index 387b7c67413d700db4222a3d0fe3999a5c5a8d71..2d18571d82d71ed03e9d6ed23d7e360e9bee22bd 100644 (file)
@@ -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
 
 
index 5528ee744049b113a219a664fc163c530c1e3f92..84682b8cc3bdcbb90cfee0762c8ad15718ef37d3 100644 (file)
@@ -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)