Skip to content

Commit 274763e

Browse files
committed
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 <[email protected]> Signed-off-by: Stephen Finucane <[email protected]>
1 parent 02f8c28 commit 274763e

File tree

5 files changed

+68
-7
lines changed

5 files changed

+68
-7
lines changed

patchwork/models.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
from patchwork.fields import HashField
3838
from patchwork.hasher import hash_diff
3939

40+
if settings.ENABLE_REST_API:
41+
from rest_framework.authtoken.models import Token
42+
4043

4144
@python_2_unicode_compatible
4245
class Person(models.Model):
@@ -162,6 +165,16 @@ def contributor_projects(self):
162165
def n_todo_patches(self):
163166
return self.todo_patches().count()
164167

168+
@property
169+
def token(self):
170+
if not settings.ENABLE_REST_API:
171+
return
172+
173+
try:
174+
return Token.objects.get(user=self.user)
175+
except Token.DoesNotExist:
176+
return
177+
165178
def todo_patches(self, project=None):
166179
# filter on project, if necessary
167180
if project:

patchwork/settings/base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143

144144
INSTALLED_APPS += [
145145
'rest_framework',
146+
'rest_framework.authtoken',
146147
'django_filters',
147148
]
148149
except ImportError:
@@ -158,6 +159,11 @@
158159
'rest_framework.filters.SearchFilter',
159160
'rest_framework.filters.OrderingFilter',
160161
),
162+
'DEFAULT_AUTHENTICATION_CLASSES': (
163+
'rest_framework.authentication.SessionAuthentication',
164+
'rest_framework.authentication.BasicAuthentication',
165+
'rest_framework.authentication.TokenAuthentication',
166+
),
161167
'SEARCH_PARAM': 'q',
162168
'ORDERING_PARAM': 'order',
163169
}

patchwork/tests/test_bundles.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from patchwork.tests.utils import create_patches
3838
from patchwork.tests.utils import create_project
3939
from patchwork.tests.utils import create_user
40+
from patchwork.views import utils as view_utils
4041

4142

4243
def bundle_url(bundle):
@@ -311,6 +312,7 @@ def test_private_bundle(self):
311312
self.assertEqual(response.status_code, 404)
312313

313314

315+
@unittest.skipUnless(settings.ENABLE_REST_API, 'requires ENABLE_REST_API')
314316
class BundlePrivateViewMboxTest(BundlePrivateViewTest):
315317

316318
"""Ensure that non-owners can't view private bundle mboxes"""
@@ -342,6 +344,28 @@ def _get_auth_string(user):
342344
response = self.client.get(self.url, HTTP_AUTHORIZATION=auth_string)
343345
self.assertEqual(response.status_code, 404)
344346

347+
def test_private_bundle_mbox_token_auth(self):
348+
self.client.logout()
349+
350+
# create tokens for both users
351+
for user in [self.user, self.other_user]:
352+
view_utils.regenerate_token(user)
353+
354+
def _get_auth_string(user):
355+
return 'Token {}'.format(str(user.profile.token))
356+
357+
# Check we can view as owner
358+
auth_string = _get_auth_string(self.user)
359+
response = self.client.get(self.url, HTTP_AUTHORIZATION=auth_string)
360+
361+
self.assertEqual(response.status_code, 200)
362+
self.assertContains(response, self.patches[0].name)
363+
364+
# Check we can't view as another user
365+
auth_string = _get_auth_string(self.other_user)
366+
response = self.client.get(self.url, HTTP_AUTHORIZATION=auth_string)
367+
self.assertEqual(response.status_code, 404)
368+
345369

346370
class BundleCreateFromListTest(BundleTestBase):
347371

patchwork/views/bundle.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,23 @@
3636
from patchwork.views.utils import bundle_to_mbox
3737

3838
if settings.ENABLE_REST_API:
39-
from rest_framework.authentication import BasicAuthentication # noqa
39+
from rest_framework.authentication import SessionAuthentication
4040
from rest_framework.exceptions import AuthenticationFailed
41+
from rest_framework.settings import api_settings
4142

4243

4344
def rest_auth(request):
4445
if not settings.ENABLE_REST_API:
4546
return request.user
46-
try:
47-
auth_result = BasicAuthentication().authenticate(request)
48-
if auth_result:
49-
return auth_result[0]
50-
except AuthenticationFailed:
51-
pass
47+
for auth in api_settings.DEFAULT_AUTHENTICATION_CLASSES:
48+
if auth == SessionAuthentication:
49+
continue
50+
try:
51+
auth_result = auth().authenticate(request)
52+
if auth_result:
53+
return auth_result[0]
54+
except AuthenticationFailed:
55+
pass
5256
return request.user
5357

5458

patchwork/views/utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,17 @@
2626
import email.utils
2727
import re
2828

29+
from django.conf import settings
2930
from django.http import Http404
3031
from django.utils import six
3132

3233
from patchwork.models import Comment
3334
from patchwork.models import Patch
3435
from patchwork.models import Series
3536

37+
if settings.ENABLE_REST_API:
38+
from rest_framework.authtoken.models import Token
39+
3640

3741
class PatchMbox(MIMENonMultipart):
3842
patch_charset = 'utf-8'
@@ -181,3 +185,13 @@ def series_to_mbox(series):
181185
mbox.append(patch_to_mbox(dep.patch))
182186

183187
return '\n'.join(mbox)
188+
189+
190+
def regenerate_token(user):
191+
"""Generate (or regenerate) user API tokens.
192+
193+
Arguments:
194+
user: The User object to generate a token for.
195+
"""
196+
Token.objects.filter(user=user).delete()
197+
Token.objects.create(user=user)

0 commit comments

Comments
 (0)