Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/rest-framework/permissions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,18 @@ For example:

When a request is performed both the `READ_SCOPE` \\ `WRITE_SCOPE` and 'music' scopes are required to be authorized for the current access token.

TokenHasResourceScope
----------------------
The `TokenHasResourceScope` permission class allows the access only when the current access token has been authorized for **all** the scopes listed in the `required_scopes` field of the view but according of request's method.

When the current request's method is one of the "safe" methods, the access is allowed only if the access token has been authorized for the `scope:read` scope (for example `music:read`).
When the request's method is one of "non safe" methods, the access is allowed only if the access token has been authorizes for the `scope:write` scope (for example `music:write`).

.. code-block:: python

class SongView(views.APIView):
authentication_classes = [OAuth2Authentication]
permission_classes = [TokenHasResourceScope]
required_scopes = ['music']

The `required_scopes` attribute is mandatory (you just need inform the resource scope).
2 changes: 1 addition & 1 deletion oauth2_provider/ext/rest_framework/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .authentication import OAuth2Authentication
from .permissions import TokenHasScope, TokenHasReadWriteScope
from .permissions import TokenHasScope, TokenHasReadWriteScope, TokenHasResourceScope
25 changes: 25 additions & 0 deletions oauth2_provider/ext/rest_framework/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,28 @@ def get_scopes(self, request, view):
read_write_scope = oauth2_settings.WRITE_SCOPE

return required_scopes + [read_write_scope]


class TokenHasResourceScope(TokenHasScope):
"""
The request is authenticated as a user and the token used has the right scope
"""

def get_scopes(self, request, view):
try:
view_scopes = (
super(TokenHasResourceScope, self).get_scopes(request, view)
)
except ImproperlyConfigured:
view_scopes = []

if request.method.upper() in SAFE_HTTP_METHODS:
scope_type = oauth2_settings.READ_SCOPE
else:
scope_type = oauth2_settings.WRITE_SCOPE

required_scopes = [
'{0}:{1}'.format(scope, scope_type) for scope in view_scopes
]

return required_scopes
45 changes: 43 additions & 2 deletions oauth2_provider/tests/test_rest_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
try:
from rest_framework import permissions
from rest_framework.views import APIView
from ..ext.rest_framework import OAuth2Authentication, TokenHasScope, TokenHasReadWriteScope
from ..ext.rest_framework import OAuth2Authentication, TokenHasScope, TokenHasReadWriteScope, TokenHasResourceScope

class MockView(APIView):
permission_classes = (permissions.IsAuthenticated,)
Expand All @@ -40,12 +40,17 @@ class ScopedView(OAuth2View):
class ReadWriteScopedView(OAuth2View):
permission_classes = [permissions.IsAuthenticated, TokenHasReadWriteScope]

class ResourceScopedView(OAuth2View):
permission_classes = [permissions.IsAuthenticated, TokenHasResourceScope]
required_scopes = ['resource1']

urlpatterns = patterns(
'',
url(r'^oauth2/', include('oauth2_provider.urls')),
url(r'^oauth2-test/$', OAuth2View.as_view()),
url(r'^oauth2-scoped-test/$', ScopedView.as_view()),
url(r'^oauth2-read-write-test/$', ReadWriteScopedView.as_view()),
url(r'^oauth2-resource-scoped-test/$', ResourceScopedView.as_view()),
)

rest_framework_installed = True
Expand All @@ -64,7 +69,7 @@ class TestOAuth2Authentication(BaseTest):
urls = 'oauth2_provider.tests.test_rest_framework'

def setUp(self):
oauth2_settings._SCOPES = ['read', 'write', 'scope1', 'scope2']
oauth2_settings._SCOPES = ['read', 'write', 'scope1', 'scope2', 'resource1']

self.test_user = UserModel.objects.create_user("test_user", "test@user.com", "123456")
self.dev_user = UserModel.objects.create_user("dev_user", "dev@user.com", "123456")
Expand Down Expand Up @@ -153,3 +158,39 @@ def test_read_write_permission_post_deny(self):
auth = self._create_authorization_header(self.access_token.token)
response = self.client.post("/oauth2-read-write-test/", HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 403)

@unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed')
def test_resource_scoped_permission_get_allow(self):
self.access_token.scope = 'resource1:read'
self.access_token.save()

auth = self._create_authorization_header(self.access_token.token)
response = self.client.get("/oauth2-resource-scoped-test/", HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)

@unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed')
def test_resource_scoped_permission_post_allow(self):
self.access_token.scope = 'resource1:write'
self.access_token.save()

auth = self._create_authorization_header(self.access_token.token)
response = self.client.post("/oauth2-resource-scoped-test/", HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)

@unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed')
def test_resource_scoped_permission_get_denied(self):
self.access_token.scope = 'resource1:write'
self.access_token.save()

auth = self._create_authorization_header(self.access_token.token)
response = self.client.get("/oauth2-resource-scoped-test/", HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 403)

@unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed')
def test_resource_scoped_permission_post_denied(self):
self.access_token.scope = 'resource1:read'
self.access_token.save()

auth = self._create_authorization_header(self.access_token.token)
response = self.client.post("/oauth2-resource-scoped-test/", HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 403)
2 changes: 1 addition & 1 deletion requirements/testing.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
-r optional.txt
coverage
mock
mock==1.0.1