Skip to content

Commit 12ac357

Browse files
committed
Merge pull request #721 from dulaccc/token-scope-permission
Token scope permission class
2 parents a34f45b + eec8efa commit 12ac357

File tree

3 files changed

+97
-2
lines changed

3 files changed

+97
-2
lines changed

rest_framework/compat.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,9 +453,11 @@ def apply_markdown(text):
453453
from provider.oauth2 import backends as oauth2_provider_backends
454454
from provider.oauth2 import models as oauth2_provider_models
455455
from provider.oauth2 import forms as oauth2_provider_forms
456+
from provider import scope as oauth2_provider_scope
456457

457458
except ImportError:
458459
oauth2_provider = None
459460
oauth2_provider_backends = None
460461
oauth2_provider_models = None
461462
oauth2_provider_forms = None
463+
oauth2_provider_scope = None

rest_framework/permissions.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
99

10+
from rest_framework.compat import oauth2_provider_scope
11+
1012

1113
class BasePermission(object):
1214
"""
@@ -125,3 +127,33 @@ def has_permission(self, request, view):
125127
request.user.has_perms(perms)):
126128
return True
127129
return False
130+
131+
132+
class TokenHasReadWriteScope(BasePermission):
133+
"""
134+
The request is authenticated as a user and the token used has the right scope
135+
"""
136+
137+
def has_permission(self, request, view):
138+
if not request.auth:
139+
return False
140+
141+
read_only = request.method in SAFE_METHODS
142+
if hasattr(request.auth, 'resource'): # oauth 1
143+
if read_only:
144+
return True
145+
elif request.auth.resource.is_readonly is False:
146+
return True
147+
return False
148+
elif hasattr(request.auth, 'scope'): # oauth 2
149+
scope_valid = lambda scope_wanted_key, scope_had: oauth2_provider_scope.check(
150+
oauth2_provider_scope.SCOPE_NAME_DICT[scope_wanted_key], scope_had)
151+
152+
if read_only and scope_valid('read', request.auth.scope):
153+
return True
154+
elif scope_valid('write', request.auth.scope):
155+
return True
156+
return False
157+
else:
158+
# Improperly configured!
159+
pass

rest_framework/tests/authentication.py

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
)
1818
from rest_framework.authtoken.models import Token
1919
from rest_framework.compat import patterns, url, include
20-
from rest_framework.compat import oauth2_provider, oauth2_provider_models
20+
from rest_framework.compat import oauth2_provider, oauth2_provider_models, oauth2_provider_scope
2121
from rest_framework.compat import oauth, oauth_provider
2222
from rest_framework.tests.utils import RequestFactory
2323
from rest_framework.views import APIView
@@ -47,13 +47,17 @@ def put(self, request):
4747
(r'^basic/$', MockView.as_view(authentication_classes=[BasicAuthentication])),
4848
(r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication])),
4949
(r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
50-
(r'^oauth/$', MockView.as_view(authentication_classes=[OAuthAuthentication]))
50+
(r'^oauth/$', MockView.as_view(authentication_classes=[OAuthAuthentication])),
51+
(r'^oauth-with-scope/$', MockView.as_view(authentication_classes=[OAuthAuthentication],
52+
permission_classes=[permissions.TokenHasReadWriteScope]))
5153
)
5254

5355
if oauth2_provider is not None:
5456
urlpatterns += patterns('',
5557
url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
5658
url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])),
59+
url(r'^oauth2-with-scope-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication],
60+
permission_classes=[permissions.TokenHasReadWriteScope])),
5761
)
5862

5963

@@ -389,6 +393,39 @@ def test_post_hmac_sha1_signature_passes(self):
389393
response = self.csrf_client.post('/oauth/', HTTP_AUTHORIZATION=auth)
390394
self.assertEqual(response.status_code, 200)
391395

396+
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
397+
@unittest.skipUnless(oauth, 'oauth2 not installed')
398+
def test_get_form_with_readonly_resource_passing_auth(self):
399+
"""Ensure POSTing with a readonly resource instead of a write scope fails"""
400+
read_only_access_token = self.token
401+
read_only_access_token.resource.is_readonly = True
402+
read_only_access_token.resource.save()
403+
params = self._create_authorization_url_parameters()
404+
response = self.csrf_client.get('/oauth-with-scope/', params)
405+
self.assertEqual(response.status_code, 200)
406+
407+
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
408+
@unittest.skipUnless(oauth, 'oauth2 not installed')
409+
def test_post_form_with_readonly_resource_failing_auth(self):
410+
"""Ensure POSTing with a readonly resource instead of a write scope fails"""
411+
read_only_access_token = self.token
412+
read_only_access_token.resource.is_readonly = True
413+
read_only_access_token.resource.save()
414+
params = self._create_authorization_url_parameters()
415+
response = self.csrf_client.post('/oauth-with-scope/', params)
416+
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
417+
418+
@unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
419+
@unittest.skipUnless(oauth, 'oauth2 not installed')
420+
def test_post_form_with_write_resource_passing_auth(self):
421+
"""Ensure POSTing with a write resource succeed"""
422+
read_write_access_token = self.token
423+
read_write_access_token.resource.is_readonly = False
424+
read_write_access_token.resource.save()
425+
params = self._create_authorization_url_parameters()
426+
response = self.csrf_client.post('/oauth-with-scope/', params)
427+
self.assertEqual(response.status_code, 200)
428+
392429

393430
class OAuth2Tests(TestCase):
394431
"""OAuth 2.0 authentication"""
@@ -514,3 +551,27 @@ def test_post_form_with_expired_access_token_failing_auth(self):
514551
response = self.csrf_client.post('/oauth2-test/', params, HTTP_AUTHORIZATION=auth)
515552
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
516553
self.assertIn('Invalid token', response.content)
554+
555+
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
556+
def test_post_form_with_invalid_scope_failing_auth(self):
557+
"""Ensure POSTing with a readonly scope instead of a write scope fails"""
558+
read_only_access_token = self.access_token
559+
read_only_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['read']
560+
read_only_access_token.save()
561+
auth = self._create_authorization_header(token=read_only_access_token.token)
562+
params = self._client_credentials_params()
563+
response = self.csrf_client.get('/oauth2-with-scope-test/', params, HTTP_AUTHORIZATION=auth)
564+
self.assertEqual(response.status_code, 200)
565+
response = self.csrf_client.post('/oauth2-with-scope-test/', params, HTTP_AUTHORIZATION=auth)
566+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
567+
568+
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
569+
def test_post_form_with_valid_scope_passing_auth(self):
570+
"""Ensure POSTing with a write scope succeed"""
571+
read_write_access_token = self.access_token
572+
read_write_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['write']
573+
read_write_access_token.save()
574+
auth = self._create_authorization_header(token=read_write_access_token.token)
575+
params = self._client_credentials_params()
576+
response = self.csrf_client.post('/oauth2-with-scope-test/', params, HTTP_AUTHORIZATION=auth)
577+
self.assertEqual(response.status_code, 200)

0 commit comments

Comments
 (0)