aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStephen Finucane <stephen@that.guru>2017-06-09 18:18:23 +0100
committerStephen Finucane <stephen@that.guru>2017-06-14 09:38:25 +0100
commit274763e7b66db5e128994e817876bf534692470e (patch)
treea382557925db4be040aaa73f655fb77a10e901f9
parent02f8c2818bfd33bd02e8b6a64809a114f91c5000 (diff)
downloadpatchwork-274763e7b66db5e128994e817876bf534692470e.tar
patchwork-274763e7b66db5e128994e817876bf534692470e.tar.gz
REST: Enable token auth support
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>
-rw-r--r--patchwork/models.py13
-rw-r--r--patchwork/settings/base.py6
-rw-r--r--patchwork/tests/test_bundles.py24
-rw-r--r--patchwork/views/bundle.py18
-rw-r--r--patchwork/views/utils.py14
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)
+@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)