diff --git a/oauth2_provider/__init__.py b/oauth2_provider/__init__.py index f6c203077..42c87e16c 100644 --- a/oauth2_provider/__init__.py +++ b/oauth2_provider/__init__.py @@ -3,3 +3,5 @@ __author__ = "Massimiliano Pippi & Federico Frenguelli" VERSION = __version__ # synonym + +default_app_config = 'oauth2_provider.apps.OAuth2ProviderConfig' diff --git a/oauth2_provider/apps.py b/oauth2_provider/apps.py new file mode 100644 index 000000000..58b00ed4f --- /dev/null +++ b/oauth2_provider/apps.py @@ -0,0 +1,30 @@ +from django.apps import AppConfig + + +class OAuth2ProviderConfig(AppConfig): + name = 'oauth2_provider' + verbose_name = "OAuth2 provider" + + def ready(self): + # Monkey-patch Meta.model and other root objects + from .models import get_application_model + Application = get_application_model() + + # monkey-patch views/application.ApplicationOwnerIsUserMixin model + from .views.application import ApplicationOwnerIsUserMixin + ApplicationOwnerIsUserMixin.model = Application + + # monkey-patch forms.RegistrationForm model + from .forms import RegistrationForm + RegistrationForm.Meta.model = Application + + # monkey-patch oauth2_validators.Appliacation and GRANT_TYPE_MAPPING + from . import oauth2_validators + oauth2_validators.Application = Application + oauth2_validators.GRANT_TYPE_MAPPING = { + 'authorization_code': (Application.GRANT_AUTHORIZATION_CODE,), + 'password': (Application.GRANT_PASSWORD,), + 'client_credentials': (Application.GRANT_CLIENT_CREDENTIALS,), + 'refresh_token': (Application.GRANT_AUTHORIZATION_CODE, Application.GRANT_PASSWORD, + Application.GRANT_CLIENT_CREDENTIALS) + } diff --git a/oauth2_provider/compat.py b/oauth2_provider/compat.py index e82e4ef97..658a53112 100644 --- a/oauth2_provider/compat.py +++ b/oauth2_provider/compat.py @@ -25,6 +25,13 @@ else: AUTH_USER_MODEL = 'auth.User' +try: + # Django's new application loading system + from django.apps import apps + get_model = apps.get_model +except ImportError: + from django.db.models import get_model + try: from django.contrib.auth import get_user_model except ImportError: diff --git a/oauth2_provider/forms.py b/oauth2_provider/forms.py index 8f7b3ab34..cd5e64239 100644 --- a/oauth2_provider/forms.py +++ b/oauth2_provider/forms.py @@ -1,7 +1,5 @@ from django import forms -from .models import get_application_model - class AllowForm(forms.Form): allow = forms.BooleanField(required=False) @@ -24,5 +22,6 @@ class RegistrationForm(forms.ModelForm): TODO: add docstring """ class Meta: - model = get_application_model() + # FIXME: monkey-patched in apps.py + model = None fields = ('name', 'client_id', 'client_secret', 'client_type', 'authorization_grant_type', 'redirect_uris') diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py index 688d64834..5eadfa15b 100644 --- a/oauth2_provider/models.py +++ b/oauth2_provider/models.py @@ -3,18 +3,13 @@ from django.core.urlresolvers import reverse from django.db import models from django.utils import timezone -try: - # Django's new application loading system - from django.apps import apps - get_model = apps.get_model -except ImportError: - from django.db.models import get_model from django.utils.translation import ugettext_lazy as _ from django.utils.encoding import python_2_unicode_compatible from django.core.exceptions import ImproperlyConfigured +from django.utils.six.moves.urllib.parse import urlparse from .settings import oauth2_settings -from .compat import AUTH_USER_MODEL +from .compat import AUTH_USER_MODEL, get_model from .generators import generate_client_secret, generate_client_id from .validators import validate_uris @@ -96,6 +91,18 @@ def redirect_uri_allowed(self, uri): """ return uri in self.redirect_uris.split() + @property + def redirect_uri_schemes(self): + """ + Returns the set of schemes used by the :attr:`redirect_uris`. + """ + schemes = set() + for uri in self.redirect_uris.split(): + parsed = urlparse(uri) + if parsed.scheme: + schemes.add(parsed.scheme) + return schemes + def clean(self): from django.core.exceptions import ValidationError if not self.redirect_uris \ @@ -239,7 +246,17 @@ def get_application_model(): except ValueError: e = "APPLICATION_MODEL must be of the form 'app_label.model_name'" raise ImproperlyConfigured(e) - app_model = get_model(app_label, model_name) + + try: + # Django >=1.7 + from django.apps import apps + try: + app_model = apps.get_model(app_label, model_name) + except LookupError: + app_model = None + except ImportError: + app_model = get_model(app_label, model_name) + if app_model is None: e = "APPLICATION_MODEL refers to model {0} that has not been installed" raise ImproperlyConfigured(e.format(oauth2_settings.APPLICATION_MODEL)) diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py index 6a79f3f7d..ebf450531 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -10,21 +10,16 @@ from oauthlib.oauth2 import RequestValidator from .compat import unquote_plus -from .models import Grant, AccessToken, RefreshToken, get_application_model +from .models import Grant, AccessToken, RefreshToken from .settings import oauth2_settings -Application = get_application_model() +# FIXME: monkey-patched in apps.py +Application = None +# FIXME: monkey-patched in apps.py +GRANT_TYPE_MAPPING = {} log = logging.getLogger('oauth2_provider') -GRANT_TYPE_MAPPING = { - 'authorization_code': (Application.GRANT_AUTHORIZATION_CODE,), - 'password': (Application.GRANT_PASSWORD,), - 'client_credentials': (Application.GRANT_CLIENT_CREDENTIALS,), - 'refresh_token': (Application.GRANT_AUTHORIZATION_CODE, Application.GRANT_PASSWORD, - Application.GRANT_CLIENT_CREDENTIALS) -} - class OAuth2Validator(RequestValidator): def _extract_basic_auth(self, request): diff --git a/oauth2_provider/views/application.py b/oauth2_provider/views/application.py index 2d6ef617f..2cf039aa5 100644 --- a/oauth2_provider/views/application.py +++ b/oauth2_provider/views/application.py @@ -4,14 +4,14 @@ from braces.views import LoginRequiredMixin from ..forms import RegistrationForm -from ..models import get_application_model class ApplicationOwnerIsUserMixin(LoginRequiredMixin): """ This mixin is used to provide an Application queryset filtered by the current request.user. """ - model = get_application_model() + # FIXME: monkey-patched in apps.py + model = None fields = '__all__' def get_queryset(self): diff --git a/oauth2_provider/views/base.py b/oauth2_provider/views/base.py index b7a5309af..57b9e05fe 100644 --- a/oauth2_provider/views/base.py +++ b/oauth2_provider/views/base.py @@ -15,8 +15,7 @@ from ..forms import AllowForm from ..models import get_application_model from .mixins import OAuthLibMixin - -Application = get_application_model() +from .util import SchemedHttpResponseRedirect log = logging.getLogger('oauth2_provider') @@ -102,9 +101,9 @@ def form_valid(self, form): allow = form.cleaned_data.get('allow') uri, headers, body, status = self.create_authorization_response( request=self.request, scopes=scopes, credentials=credentials, allow=allow) - self.success_url = uri - log.debug("Success url for the request: {0}".format(self.success_url)) - return super(AuthorizationView, self).form_valid(form) + log.debug("Redirect uri for the request: {0}".format(uri)) + application = get_application_model().objects.get(client_id=credentials['client_id']) # TODO: cache it! + return SchemedHttpResponseRedirect(uri, allowed_schemes=application.redirect_uri_schemes) except OAuthToolkitError as error: return self.error_response(error) @@ -115,7 +114,7 @@ def get(self, request, *args, **kwargs): kwargs['scopes_descriptions'] = [oauth2_settings.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 = Application.objects.get(client_id=credentials['client_id']) # TODO: cache it! + application = get_application_model().objects.get(client_id=credentials['client_id']) # TODO: cache it! kwargs['application'] = application kwargs.update(credentials) self.oauth2_data = kwargs @@ -127,15 +126,19 @@ def get(self, request, *args, **kwargs): # a successful response depending on 'approval_prompt' url parameter require_approval = request.GET.get('approval_prompt', oauth2_settings.REQUEST_APPROVAL_PROMPT) + def build_authorization_response(): + uri, headers, body, status = self.create_authorization_response( + request=self.request, scopes=" ".join(scopes), + credentials=credentials, allow=True) + redirect_schemes = application.redirect_uri_schemes + return SchemedHttpResponseRedirect(uri, allowed_schemes=redirect_schemes) + # If skip_authorization field is True, skip the authorization screen even # if this is the first use of the application and there was no previous authorization. # This is useful for in-house applications-> assume an in-house applications # are already approved. if application.skip_authorization: - uri, headers, body, status = self.create_authorization_response( - request=self.request, scopes=" ".join(scopes), - credentials=credentials, allow=True) - return HttpResponseRedirect(uri) + return build_authorization_response() elif require_approval == 'auto': tokens = request.user.accesstoken_set.filter(application=kwargs['application'], @@ -143,10 +146,7 @@ def get(self, request, *args, **kwargs): # check past authorizations regarded the same scopes as the current one for token in tokens: if token.allow_scopes(scopes): - uri, headers, body, status = self.create_authorization_response( - request=self.request, scopes=" ".join(scopes), - credentials=credentials, allow=True) - return HttpResponseRedirect(uri) + return build_authorization_response() return self.render_to_response(self.get_context_data(**kwargs)) diff --git a/oauth2_provider/views/util.py b/oauth2_provider/views/util.py new file mode 100644 index 000000000..10fe5cc21 --- /dev/null +++ b/oauth2_provider/views/util.py @@ -0,0 +1,24 @@ +from django.http.response import HttpResponseRedirectBase + + +class SchemedHttpResponseRedirectBase(HttpResponseRedirectBase): + """ + HttpResponseRedirectBase-like class that accepts an `allowed_schemes` + positional argument to overwrite the default set of schemes. + Warning: if `allowed_schemes` is empty, no scheme is allowed. + """ + + def __init__(self, redirect_to, *args, **kwargs): + try: + self.allowed_schemes = kwargs.pop('allowed_schemes') + except KeyError: + pass + super(SchemedHttpResponseRedirectBase, self).__init__(redirect_to, *args, **kwargs) + + +class SchemedHttpResponseRedirect(SchemedHttpResponseRedirectBase): + status_code = 302 + + +class SchemedHttpResponsePermanentRedirect(SchemedHttpResponseRedirectBase): + status_code = 301