Skip to content

Commit

Permalink
Add support for scopes backends to allow easier customization of scopes
Browse files Browse the repository at this point in the history
  • Loading branch information
synasius authored and jleclanche committed Feb 7, 2017
1 parent 50e02b4 commit 4553923
Show file tree
Hide file tree
Showing 12 changed files with 94 additions and 14 deletions.
3 changes: 2 additions & 1 deletion oauth2_provider/decorators.py
Expand Up @@ -6,6 +6,7 @@

from .oauth2_validators import OAuth2Validator
from .oauth2_backends import OAuthLibCore
from .scopes import get_scopes_backend
from .settings import oauth2_settings


Expand Down Expand Up @@ -55,7 +56,7 @@ def decorator(view_func):
@wraps(view_func)
def _validate(request, *args, **kwargs):
# Check if provided scopes are acceptable
provided_scopes = oauth2_settings._SCOPES
provided_scopes = get_scopes_backend().get_all_scopes()
read_write_scopes = [oauth2_settings.READ_SCOPE, oauth2_settings.WRITE_SCOPE]

if not set(read_write_scopes).issubset(set(provided_scopes)):
Expand Down
5 changes: 4 additions & 1 deletion oauth2_provider/models.py
Expand Up @@ -11,6 +11,7 @@
from django.utils.encoding import python_2_unicode_compatible
from django.core.exceptions import ImproperlyConfigured

from .scopes import get_scopes_backend
from .settings import oauth2_settings
from .compat import parse_qsl, reverse, urlparse
from .generators import generate_client_secret, generate_client_id
Expand Down Expand Up @@ -236,7 +237,9 @@ def scopes(self):
"""
Returns a dictionary of allowed scope names (as keys) with their descriptions (as values)
"""
return {name: desc for name, desc in oauth2_settings.SCOPES.items() if name in self.scope.split()}
all_scopes = get_scopes_backend().get_all_scopes()
token_scopes = self.scope.split()
return {name: desc for name, desc in all_scopes.items() if name in token_scopes}

def __str__(self):
return self.token
Expand Down
7 changes: 5 additions & 2 deletions oauth2_provider/oauth2_validators.py
Expand Up @@ -16,6 +16,7 @@
from .compat import unquote_plus
from .exceptions import FatalClientError
from .models import Grant, AccessToken, RefreshToken, get_application_model, AbstractApplication
from .scopes import get_scopes_backend
from .settings import oauth2_settings

log = logging.getLogger('oauth2_provider')
Expand Down Expand Up @@ -281,10 +282,12 @@ def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
"""
Ensure required scopes are permitted (as specified in the settings file)
"""
return set(scopes).issubset(set(oauth2_settings._SCOPES))
available_scopes = get_scopes_backend().get_available_scopes(application=client, request=request)
return set(scopes).issubset(set(available_scopes))

def get_default_scopes(self, client_id, request, *args, **kwargs):
return oauth2_settings._DEFAULT_SCOPES
default_scopes = get_scopes_backend().get_default_scopes(application=request.client, request=request)
return default_scopes

def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs):
return request.client.redirect_uri_allowed(redirect_uri)
Expand Down
53 changes: 53 additions & 0 deletions oauth2_provider/scopes.py
@@ -0,0 +1,53 @@
from __future__ import absolute_import
from __future__ import unicode_literals

from .settings import oauth2_settings


class BaseScopes(object):
def get_all_scopes(self):
"""
Return a dict-like object with all the scopes available in the
system. The key should be the scope name and the value should be
the description.
ex: {'read': 'A read scope', 'write': 'A write scope'}
"""
raise NotImplementedError("")

def get_available_scopes(self, application=None, request=None, *args, **kwargs):
"""
Return a list of scopes available for the current application/request.
TODO: add info on where and why this method is called.
ex: ['read', 'write']
"""
raise NotImplementedError("")

def get_default_scopes(self, application=None, request=None, *args, **kwargs):
"""
Return a list of the default scopes for the current application/request.
This MUST be a subset of the scopes returned by `get_available_scopes`.
TODO: add info on where and why this method is called.
ex: ['read']
"""
raise NotImplementedError("")


class SettingsScopes(BaseScopes):
def get_all_scopes(self):
return oauth2_settings.SCOPES

def get_available_scopes(self, application=None, request=None, *args, **kwargs):
return oauth2_settings._SCOPES

def get_default_scopes(self, application=None, request=None, *args, **kwargs):
return oauth2_settings._DEFAULT_SCOPES


def get_scopes_backend():
scopes_class = oauth2_settings.SCOPES_BACKEND_CLASS
return scopes_class()
2 changes: 2 additions & 0 deletions oauth2_provider/settings.py
Expand Up @@ -35,6 +35,7 @@
'OAUTH2_BACKEND_CLASS': 'oauth2_provider.oauth2_backends.OAuthLibCore',
'SCOPES': {"read": "Reading scope", "write": "Writing scope"},
'DEFAULT_SCOPES': ['__all__'],
'SCOPES_BACKEND_CLASS': 'oauth2_provider.scopes.SettingsScopes',
'READ_SCOPE': 'read',
'WRITE_SCOPE': 'write',
'AUTHORIZATION_CODE_EXPIRE_SECONDS': 60,
Expand Down Expand Up @@ -68,6 +69,7 @@
'OAUTH2_SERVER_CLASS',
'OAUTH2_VALIDATOR_CLASS',
'OAUTH2_BACKEND_CLASS',
'SCOPES_BACKEND_CLASS',
)


Expand Down
4 changes: 0 additions & 4 deletions oauth2_provider/tests/settings.py
Expand Up @@ -124,7 +124,3 @@
},
}
}

OAUTH2_PROVIDER = {
'_SCOPES': ['example']
}
1 change: 1 addition & 0 deletions oauth2_provider/tests/test_authorization_code.py
Expand Up @@ -1056,3 +1056,4 @@ def test_pre_auth_default_scopes(self):
self.assertEqual(form['state'].value(), "random_state_string")
self.assertEqual(form['scope'].value(), 'read')
self.assertEqual(form['client_id'].value(), self.application.client_id)
oauth2_settings._DEFAULT_SCOPES = ['read', 'write']
3 changes: 3 additions & 0 deletions oauth2_provider/tests/test_rest_framework.py
Expand Up @@ -96,6 +96,9 @@ def setUp(self):
application=self.application
)

def tearDown(self):
oauth2_settings._SCOPES = ['read', 'write']

def _create_authorization_header(self, token):
return "Bearer {0}".format(token)

Expand Down
9 changes: 5 additions & 4 deletions oauth2_provider/tests/test_scopes.py
Expand Up @@ -60,6 +60,7 @@ def setUp(self):
oauth2_settings.WRITE_SCOPE = 'write'

def tearDown(self):
oauth2_settings._SCOPES = ['read', 'write']
self.application.delete()
self.test_user.delete()
self.dev_user.delete()
Expand Down Expand Up @@ -323,26 +324,26 @@ def get_access_token(self, scopes):
return content['access_token']

def test_improperly_configured(self):
oauth2_settings._SCOPES = ['scope1']
oauth2_settings.SCOPES = {'scope1': 'Scope 1'}

request = self.factory.get("/fake")
view = ReadWriteResourceView.as_view()
self.assertRaises(ImproperlyConfigured, view, request)

oauth2_settings._SCOPES = ['read', 'write']
oauth2_settings.SCOPES = {'read': 'Read Scope', 'write': 'Write Scope'}
oauth2_settings.READ_SCOPE = 'ciccia'

view = ReadWriteResourceView.as_view()
self.assertRaises(ImproperlyConfigured, view, request)

def test_properly_configured(self):
oauth2_settings._SCOPES = ['scope1']
oauth2_settings.SCOPES = {'scope1': 'Scope 1'}

request = self.factory.get("/fake")
view = ReadWriteResourceView.as_view()
self.assertRaises(ImproperlyConfigured, view, request)

oauth2_settings._SCOPES = ['read', 'write']
oauth2_settings.SCOPES = {'read': 'Read Scope', 'write': 'Write Scope'}
oauth2_settings.READ_SCOPE = 'ciccia'

view = ReadWriteResourceView.as_view()
Expand Down
14 changes: 14 additions & 0 deletions oauth2_provider/tests/test_scopes_backend.py
@@ -0,0 +1,14 @@
from __future__ import absolute_import
from __future__ import unicode_literals

from oauth2_provider.scopes import SettingsScopes


def test_settings_scopes_get_available_scopes():
scopes = SettingsScopes()
assert scopes.get_available_scopes() == ['read', 'write']


def test_settings_scopes_get_default_scopes():
scopes = SettingsScopes()
assert scopes.get_default_scopes() == ['read', 'write']
4 changes: 3 additions & 1 deletion oauth2_provider/views/base.py
Expand Up @@ -8,6 +8,7 @@

from braces.views import LoginRequiredMixin, CsrfExemptMixin

from ..scopes import get_scopes_backend
from ..settings import oauth2_settings
from ..exceptions import OAuthToolkitError
from ..forms import AllowForm
Expand Down Expand Up @@ -110,7 +111,8 @@ def form_valid(self, form):
def get(self, request, *args, **kwargs):
try:
scopes, credentials = self.validate_authorization_request(request)
kwargs['scopes_descriptions'] = [oauth2_settings.SCOPES[scope] for scope in scopes]
all_scopes = get_scopes_backend().get_all_scopes()
kwargs['scopes_descriptions'] = [all_scopes[scope] for scope in scopes]
kwargs['scopes'] = scopes
# at this point we know an Application instance with such client_id exists in the database
application = get_application_model().objects.get(client_id=credentials['client_id']) # TODO: cache it!
Expand Down
3 changes: 2 additions & 1 deletion oauth2_provider/views/mixins.py
Expand Up @@ -6,6 +6,7 @@
from django.http import HttpResponseForbidden

from ..exceptions import FatalClientError
from ..scopes import get_scopes_backend
from ..settings import oauth2_settings


Expand Down Expand Up @@ -221,7 +222,7 @@ class ReadWriteScopedResourceMixin(ScopedResourceMixin, OAuthLibMixin):
read_write_scope = None

def __new__(cls, *args, **kwargs):
provided_scopes = oauth2_settings._SCOPES
provided_scopes = get_scopes_backend().get_all_scopes()
read_write_scopes = [oauth2_settings.READ_SCOPE, oauth2_settings.WRITE_SCOPE]

if not set(read_write_scopes).issubset(set(provided_scopes)):
Expand Down

0 comments on commit 4553923

Please sign in to comment.