From 1a66f683f1e8aa282d43e8da0bfb25bb3a74222f Mon Sep 17 00:00:00 2001 From: Diego Garcia Date: Tue, 7 Jul 2015 15:03:22 -0300 Subject: [PATCH 01/91] create TokenHasResourceScope a drf permission class --- docs/rest-framework/permissions.rst | 15 +++++++ .../ext/rest_framework/__init__.py | 2 +- .../ext/rest_framework/permissions.py | 25 +++++++++++ oauth2_provider/tests/test_rest_framework.py | 45 ++++++++++++++++++- 4 files changed, 84 insertions(+), 3 deletions(-) diff --git a/docs/rest-framework/permissions.rst b/docs/rest-framework/permissions.rst index 092d5809f..d22e8f4f7 100644 --- a/docs/rest-framework/permissions.rst +++ b/docs/rest-framework/permissions.rst @@ -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). diff --git a/oauth2_provider/ext/rest_framework/__init__.py b/oauth2_provider/ext/rest_framework/__init__.py index 24cbda12b..00da0a1ce 100644 --- a/oauth2_provider/ext/rest_framework/__init__.py +++ b/oauth2_provider/ext/rest_framework/__init__.py @@ -1,2 +1,2 @@ from .authentication import OAuth2Authentication -from .permissions import TokenHasScope, TokenHasReadWriteScope +from .permissions import TokenHasScope, TokenHasReadWriteScope, TokenHasResourceScope diff --git a/oauth2_provider/ext/rest_framework/permissions.py b/oauth2_provider/ext/rest_framework/permissions.py index e60415db0..559bbbc54 100644 --- a/oauth2_provider/ext/rest_framework/permissions.py +++ b/oauth2_provider/ext/rest_framework/permissions.py @@ -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 diff --git a/oauth2_provider/tests/test_rest_framework.py b/oauth2_provider/tests/test_rest_framework.py index ccf4e0b5a..28b7b00a2 100644 --- a/oauth2_provider/tests/test_rest_framework.py +++ b/oauth2_provider/tests/test_rest_framework.py @@ -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,) @@ -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 @@ -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") @@ -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) From 7a9ee1b7c20ab2eef4c79fa146f7d9ccc9085273 Mon Sep 17 00:00:00 2001 From: Diego Garcia Date: Fri, 10 Jul 2015 13:37:32 -0300 Subject: [PATCH 02/91] force version 1.0.1 of mock library in testing requirements (because new version [1.0.2] dont work with py2.6) --- requirements/testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/testing.txt b/requirements/testing.txt index f95801893..544875e36 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -1,3 +1,3 @@ -r optional.txt coverage -mock +mock==1.0.1 From b81cbe4844fe1e0e9618ff67e85d47be41364f68 Mon Sep 17 00:00:00 2001 From: Calvin Jeong Date: Mon, 17 Aug 2015 02:43:36 +0000 Subject: [PATCH 03/91] Put additional information when generating token responses. --- oauth2_provider/oauth2_backends.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/oauth2_provider/oauth2_backends.py b/oauth2_provider/oauth2_backends.py index 4c7e40a84..272dba0e1 100644 --- a/oauth2_provider/oauth2_backends.py +++ b/oauth2_provider/oauth2_backends.py @@ -32,6 +32,18 @@ def _get_escaped_full_path(self, request): return urlunparse(parsed) + def _get_extra_credentials(self, request): + """ + Produce extra credentials for token response. This dictionary will be + merged with the response. + See also: `oauthlib.oauth2.rfc6749.TokenEndpoint.create_token_response` + + :param request: The current django.http.HttpRequest object + :return: dictionary of extra credentials or None (default) + """ + return None + + def _extract_params(self, request): """ Extract parameters from the Django request object. Such parameters will then be passed to @@ -121,9 +133,10 @@ def create_token_response(self, request): :param request: The current django.http.HttpRequest object """ uri, http_method, body, headers = self._extract_params(request) + extra_credentials = self._get_extra_credentials(request) headers, body, status = self.server.create_token_response(uri, http_method, body, - headers) + headers, extra_credentials) uri = headers.get("Location", None) return uri, headers, body, status From b7078e825afd80e130921039a8663602d7a49bbf Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Thu, 17 Sep 2015 09:57:47 +0200 Subject: [PATCH 04/91] updated changelog with new features --- README.rst | 4 ++++ docs/changelog.rst | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/README.rst b/README.rst index 42dec11b6..a16702709 100644 --- a/README.rst +++ b/README.rst @@ -87,6 +87,10 @@ Roadmap / Todo list (help wanted) Changelog --------- +master +~~~~~~~~~~~~~~~~~~ + +* #273: Generic read write scope by resource 0.9.0 [2015-07-28] ~~~~~~~~~~~~~~~~~~ diff --git a/docs/changelog.rst b/docs/changelog.rst index 5dc0cf9d7..2619f0a95 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ Changelog ========= +master +------------------ + +* #273: Generic read write scope by resource + + 0.9.0 [2015-07-28] ------------------ From 2d896c1ba8db27d41830b8f779f9899d73330896 Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Thu, 17 Sep 2015 09:58:06 +0200 Subject: [PATCH 05/91] updated contributors list --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 8fa585f0d..34e66b331 100644 --- a/AUTHORS +++ b/AUTHORS @@ -13,3 +13,4 @@ David Fischer Ash Christopher Rodney Richardson Hiroki Kiyohara +Diego Garcia From 68d03c04c61a412b8ca96152979b5315e094104c Mon Sep 17 00:00:00 2001 From: Liang Zheng Date: Wed, 23 Sep 2015 14:42:30 -0700 Subject: [PATCH 06/91] Fixing doc about SessionAuthenticationMiddleware --- docs/tutorial/tutorial_03.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/tutorial/tutorial_03.rst b/docs/tutorial/tutorial_03.rst index 612414cb2..210cc24cc 100644 --- a/docs/tutorial/tutorial_03.rst +++ b/docs/tutorial/tutorial_03.rst @@ -24,7 +24,8 @@ which takes care of token verification. In your settings.py: MIDDLEWARE_CLASSES = ( '...', - # be sure following two appear in this order + # If you use SessionAuthenticationMiddleware, be sure it appears before OAuth2TokenMiddleware. + # SessionAuthenticationMiddleware is NOT required for using django-oauth-toolkit. 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'oauth2_provider.middleware.OAuth2TokenMiddleware', '...', @@ -43,6 +44,9 @@ not used at all, it will try to authenticate user with the OAuth2 access token a `request.user` and `request._cached_user` fields so that AuthenticationMiddleware (when active) will not try to get user from the session. +If you use SessionAuthenticationMiddleware, be sure it appears before OAuth2TokenMiddleware. +However SessionAuthenticationMiddleware is NOT required for using django-oauth-toolkit. + Protect your view ----------------- The authentication backend will run smoothly with, for example, `login_required` decorators, so From 07df6f6973da12ecab2690835ebba8f046f33771 Mon Sep 17 00:00:00 2001 From: Nikita Grishko Date: Sun, 27 Sep 2015 21:24:49 +0300 Subject: [PATCH 07/91] ``server_class`` is now pluggable through Django settings --- docs/settings.rst | 5 +++++ oauth2_provider/oauth2_backends.py | 7 +++---- oauth2_provider/settings.py | 3 +++ oauth2_provider/views/base.py | 8 +++----- oauth2_provider/views/generic.py | 4 +--- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 6fc46da46..4df3e148c 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -59,6 +59,11 @@ CLIENT_SECRET_GENERATOR_LENGTH The length of the generated secrets, in characters. If this value is too low, secrets may become subject to bruteforce guessing. +OAUTH2_SERVER_CLASS +~~~~~~~~~~~~~~~~~~~~ +The import string for the ``server_class`` (or ``oauthlib.oauth2.Server`` subclass) +used in the ``OAuthLibMixin`` that implements OAuth2 grant types. + OAUTH2_VALIDATOR_CLASS ~~~~~~~~~~~~~~~~~~~~~~ The import string of the ``oauthlib.oauth2.RequestValidator`` subclass that diff --git a/oauth2_provider/oauth2_backends.py b/oauth2_provider/oauth2_backends.py index 4c7e40a84..27984d2eb 100644 --- a/oauth2_provider/oauth2_backends.py +++ b/oauth2_provider/oauth2_backends.py @@ -18,7 +18,7 @@ def __init__(self, server=None): """ :params server: An instance of oauthlib.oauth2.Server class """ - self.server = server or oauth2.Server(oauth2_settings.OAUTH2_VALIDATOR_CLASS()) + self.server = server or oauth2_settings.OAUTH2_SERVER_CLASS(oauth2_settings.OAUTH2_VALIDATOR_CLASS()) def _get_escaped_full_path(self, request): """ @@ -179,7 +179,6 @@ def get_oauthlib_core(): Utility function that take a request and returns an instance of `oauth2_provider.backends.OAuthLibCore` """ - from oauthlib.oauth2 import Server - - server = Server(oauth2_settings.OAUTH2_VALIDATOR_CLASS()) + validator = oauth2_settings.OAUTH2_VALIDATOR_CLASS() + server = oauth2_settings.OAUTH2_SERVER_CLASS(validator) return oauth2_settings.OAUTH2_BACKEND_CLASS(server) diff --git a/oauth2_provider/settings.py b/oauth2_provider/settings.py index db5768686..d7903d61c 100644 --- a/oauth2_provider/settings.py +++ b/oauth2_provider/settings.py @@ -33,6 +33,7 @@ 'CLIENT_ID_GENERATOR_CLASS': 'oauth2_provider.generators.ClientIdGenerator', 'CLIENT_SECRET_GENERATOR_CLASS': 'oauth2_provider.generators.ClientSecretGenerator', 'CLIENT_SECRET_GENERATOR_LENGTH': 128, + 'OAUTH2_SERVER_CLASS': 'oauthlib.oauth2.Server', 'OAUTH2_VALIDATOR_CLASS': 'oauth2_provider.oauth2_validators.OAuth2Validator', 'OAUTH2_BACKEND_CLASS': 'oauth2_provider.oauth2_backends.OAuthLibCore', 'SCOPES': {"read": "Reading scope", "write": "Writing scope"}, @@ -52,6 +53,7 @@ MANDATORY = ( 'CLIENT_ID_GENERATOR_CLASS', 'CLIENT_SECRET_GENERATOR_CLASS', + 'OAUTH2_SERVER_CLASS', 'OAUTH2_VALIDATOR_CLASS', 'OAUTH2_BACKEND_CLASS', 'SCOPES', @@ -62,6 +64,7 @@ IMPORT_STRINGS = ( 'CLIENT_ID_GENERATOR_CLASS', 'CLIENT_SECRET_GENERATOR_CLASS', + 'OAUTH2_SERVER_CLASS', 'OAUTH2_VALIDATOR_CLASS', 'OAUTH2_BACKEND_CLASS', ) diff --git a/oauth2_provider/views/base.py b/oauth2_provider/views/base.py index 5d80f1d9a..ea19f319a 100644 --- a/oauth2_provider/views/base.py +++ b/oauth2_provider/views/base.py @@ -6,8 +6,6 @@ from django.utils import timezone from django.utils.decorators import method_decorator -from oauthlib.oauth2 import Server - from braces.views import LoginRequiredMixin, CsrfExemptMixin from ..settings import oauth2_settings @@ -71,7 +69,7 @@ class AuthorizationView(BaseAuthorizationView, FormView): template_name = 'oauth2_provider/authorize.html' form_class = AllowForm - server_class = Server + server_class = oauth2_settings.OAUTH2_SERVER_CLASS validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS oauthlib_backend_class = oauth2_settings.OAUTH2_BACKEND_CLASS @@ -163,7 +161,7 @@ class TokenView(CsrfExemptMixin, OAuthLibMixin, View): * Password * Client credentials """ - server_class = Server + server_class = oauth2_settings.OAUTH2_SERVER_CLASS validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS oauthlib_backend_class = oauth2_settings.OAUTH2_BACKEND_CLASS @@ -181,7 +179,7 @@ class RevokeTokenView(CsrfExemptMixin, OAuthLibMixin, View): """ Implements an endpoint to revoke access or refresh tokens """ - server_class = Server + server_class = oauth2_settings.OAUTH2_SERVER_CLASS validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS oauthlib_backend_class = oauth2_settings.OAUTH2_BACKEND_CLASS diff --git a/oauth2_provider/views/generic.py b/oauth2_provider/views/generic.py index 11fcbd041..77f1795df 100644 --- a/oauth2_provider/views/generic.py +++ b/oauth2_provider/views/generic.py @@ -1,7 +1,5 @@ from django.views.generic import View -from oauthlib.oauth2 import Server - from ..settings import oauth2_settings from .mixins import ProtectedResourceMixin, ScopedResourceMixin, ReadWriteScopedResourceMixin @@ -10,7 +8,7 @@ class ProtectedResourceView(ProtectedResourceMixin, View): """ Generic view protecting resources by providing OAuth2 authentication out of the box """ - server_class = Server + server_class = oauth2_settings.OAUTH2_SERVER_CLASS validator_class = oauth2_settings.OAUTH2_VALIDATOR_CLASS oauthlib_backend_class = oauth2_settings.OAUTH2_BACKEND_CLASS From 55035a586afc82b8fb3b65b7f703958e25e89b7e Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Thu, 15 Oct 2015 16:08:00 +0200 Subject: [PATCH 08/91] urlpatterns should be a plain list or `django.conf.urls.url` instances As stated by the Django documentation, and Django 1.9 removed the patterns method --- oauth2_provider/urls.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/oauth2_provider/urls.py b/oauth2_provider/urls.py index 2fff96c60..e098e4148 100644 --- a/oauth2_provider/urls.py +++ b/oauth2_provider/urls.py @@ -1,18 +1,16 @@ from __future__ import absolute_import -from django.conf.urls import patterns, url +from django.conf.urls import url from . import views -urlpatterns = patterns( - '', +urlpatterns = ( url(r'^authorize/$', views.AuthorizationView.as_view(), name="authorize"), url(r'^token/$', views.TokenView.as_view(), name="token"), url(r'^revoke_token/$', views.RevokeTokenView.as_view(), name="revoke-token"), ) # Application management views -urlpatterns += patterns( - '', +urlpatterns += ( url(r'^applications/$', views.ApplicationList.as_view(), name="list"), url(r'^applications/register/$', views.ApplicationRegistration.as_view(), name="register"), url(r'^applications/(?P\d+)/$', views.ApplicationDetail.as_view(), name="detail"), From d22958ebdcacc3cfae872660993e9ccf66cf3526 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Thu, 15 Oct 2015 17:06:19 +0200 Subject: [PATCH 09/91] Add Django 1.9 to the test matrix --- .travis.yml | 3 +++ tox.ini | 24 +++++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index bfbfb947d..0bf3413a7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,14 +10,17 @@ env: - TOX_ENV=py27-django16 - TOX_ENV=py27-django17 - TOX_ENV=py27-django18 + - TOX_ENV=py27-django19 - TOX_ENV=py33-django15 - TOX_ENV=py33-django16 - TOX_ENV=py33-django17 - TOX_ENV=py33-django18 + - TOX_ENV=py33-django19 - TOX_ENV=py34-django15 - TOX_ENV=py34-django16 - TOX_ENV=py34-django17 - TOX_ENV=py34-django18 + - TOX_ENV=py34-django19 - TOX_ENV=docs install: diff --git a/tox.ini b/tox.ini index 38ad106e5..c7442fed0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,9 @@ [tox] envlist = py26-django14, py26-django15, py26-django16, - py27-django14, py27-django15, py27-django16, py27-django17, py27-django18, - py33-django15, py33-django16, py33-django17, py33-django18, - py34-django15, py34-django16, py34-django17, py34-django18, + py27-django14, py27-django15, py27-django16, py27-django17, py27-django18, py27-django19, + py33-django15, py33-django16, py33-django17, py33-django18, py33-django19, + py34-django15, py34-django16, py34-django17, py34-django18, py34-django19, docs, flake8 @@ -65,6 +65,12 @@ deps = Django<1.9 {[testenv]deps} +[testenv:py27-django19] +basepython = python2.7 +deps = + git+https://github.com/django/django.git@stable/1.9.x#egg=Django + {[testenv]deps} + [testenv:py33-django15] basepython = python3.3 deps = @@ -90,6 +96,12 @@ deps = Django<1.9 {[testenv]deps} +[testenv:py33-django19] +basepython = python3.3 +deps = + git+https://github.com/django/django.git@stable/1.9.x#egg=Django + {[testenv]deps} + [testenv:py34-django15] basepython = python3.4 deps = @@ -115,6 +127,12 @@ deps = Django<1.9 {[testenv]deps} +[testenv:py34-django19] +basepython = python3.4 +deps = + git+https://github.com/django/django.git@stable/1.9.x#egg=Django + {[testenv]deps} + [testenv:docs] basepython=python changedir=docs From 6c7fdb29909d279e36c12474be2639148d7c55fd Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Thu, 15 Oct 2015 18:04:21 +0200 Subject: [PATCH 10/91] Use the native NullHandler --- oauth2_provider/tests/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2_provider/tests/settings.py b/oauth2_provider/tests/settings.py index 47ffae0a2..990b797cd 100644 --- a/oauth2_provider/tests/settings.py +++ b/oauth2_provider/tests/settings.py @@ -100,7 +100,7 @@ }, 'null': { 'level': 'DEBUG', - 'class': 'django.utils.log.NullHandler', + 'class': 'logging.NullHandler', }, }, 'loggers': { From f9f50b14813a2aeb19fd63fb48d22e1689e7482d Mon Sep 17 00:00:00 2001 From: Bart Merenda Date: Thu, 15 Oct 2015 18:23:28 +0200 Subject: [PATCH 11/91] tested OAuthLibCoreBackend's _get_extra_credentials method overriding --- oauth2_provider/oauth2_backends.py | 1 - oauth2_provider/tests/test_oauth2_backends.py | 28 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/oauth2_provider/oauth2_backends.py b/oauth2_provider/oauth2_backends.py index 272dba0e1..dac0b8d0a 100644 --- a/oauth2_provider/oauth2_backends.py +++ b/oauth2_provider/oauth2_backends.py @@ -43,7 +43,6 @@ def _get_extra_credentials(self, request): """ return None - def _extract_params(self, request): """ Extract parameters from the Django request object. Such parameters will then be passed to diff --git a/oauth2_provider/tests/test_oauth2_backends.py b/oauth2_provider/tests/test_oauth2_backends.py index bab4891eb..27495675d 100644 --- a/oauth2_provider/tests/test_oauth2_backends.py +++ b/oauth2_provider/tests/test_oauth2_backends.py @@ -1,4 +1,5 @@ import json +import mock from django.test import TestCase, RequestFactory @@ -34,6 +35,33 @@ def test_application_json_extract_params(self): self.assertNotIn("password=123456", body) +class TestCustomOAuthLibCoreBackend(TestCase): + """ + Tests that the public API behaves as expected when we override + the OAuthLibCoreBackend core methods. + """ + class MyOAuthLibCore(OAuthLibCore): + def _get_extra_credentials(self, request): + return 1 + + def setUp(self): + self.factory = RequestFactory() + + def test_create_token_response_gets_extra_credentials(self): + """ + Make sures that extra_credentials parameter is passed to oauthlib + """ + payload = "grant_type=password&username=john&password=123456" + request = self.factory.post("/o/token/", payload, content_type="application/x-www-form-urlencoded") + + with mock.patch('oauthlib.oauth2.Server.create_token_response') as create_token_response: + mocked = mock.MagicMock() + create_token_response.return_value = mocked, mocked, mocked + core = self.MyOAuthLibCore() + core.create_token_response(request) + self.assertTrue(create_token_response.call_args[0][4] == 1) + + class TestJSONOAuthLibCoreBackend(TestCase): def setUp(self): self.factory = RequestFactory() From a2b6cef1fc6dee679407d61312a23f96c3e99cda Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Thu, 15 Oct 2015 19:04:50 +0200 Subject: [PATCH 12/91] Use a plain list of url instances for the tests urls too --- oauth2_provider/tests/urls.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/oauth2_provider/tests/urls.py b/oauth2_provider/tests/urls.py index 2548f5995..aa925826a 100644 --- a/oauth2_provider/tests/urls.py +++ b/oauth2_provider/tests/urls.py @@ -1,11 +1,10 @@ -from django.conf.urls import patterns, include, url +from django.conf.urls import include, url from django.contrib import admin admin.autodiscover() -urlpatterns = patterns( - '', +urlpatterns = ( url(r'^admin/', include(admin.site.urls)), url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')), ) From 65f22bbc961a335a2031994b7860169a24fb6b73 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Thu, 15 Oct 2015 19:09:49 +0200 Subject: [PATCH 13/91] Fix the unittest import --- oauth2_provider/tests/test_rest_framework.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/oauth2_provider/tests/test_rest_framework.py b/oauth2_provider/tests/test_rest_framework.py index 28b7b00a2..7aa8ffed9 100644 --- a/oauth2_provider/tests/test_rest_framework.py +++ b/oauth2_provider/tests/test_rest_framework.py @@ -3,8 +3,12 @@ from django.conf.urls import patterns, url, include from django.http import HttpResponse from django.test import TestCase -from django.utils import timezone, unittest +from django.utils import timezone +try: + import unittest +except ImportError: + from django.utils import unittest from .test_utils import TestCaseUtils from ..models import AccessToken, get_application_model From a72e5bd8286db9eddf278d53fe95293b9b82326d Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Thu, 15 Oct 2015 19:11:50 +0200 Subject: [PATCH 14/91] Ensure the MIDDLEWARE_CLASSES content is a tuple for concatenation --- oauth2_provider/tests/test_auth_backends.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2_provider/tests/test_auth_backends.py b/oauth2_provider/tests/test_auth_backends.py index 24944664e..53efc224d 100644 --- a/oauth2_provider/tests/test_auth_backends.py +++ b/oauth2_provider/tests/test_auth_backends.py @@ -76,7 +76,7 @@ def test_get_user(self): 'oauth2_provider.backends.OAuth2Backend', 'django.contrib.auth.backends.ModelBackend', ), - MIDDLEWARE_CLASSES=MIDDLEWARE_CLASSES + ('oauth2_provider.middleware.OAuth2TokenMiddleware',) + MIDDLEWARE_CLASSES=tuple(MIDDLEWARE_CLASSES) + ('oauth2_provider.middleware.OAuth2TokenMiddleware',) ) class TestOAuth2Middleware(BaseTest): From 4ac449c18d2b7f424f80243d0022fbece99e2f2d Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Thu, 15 Oct 2015 19:35:29 +0200 Subject: [PATCH 15/91] Implement a compatibility for the url templatetag --- oauth2_provider/compat.py | 6 ++++++ .../oauth2_provider/application_confirm_delete.html | 2 +- .../templates/oauth2_provider/application_detail.html | 2 +- .../templates/oauth2_provider/application_form.html | 2 +- .../templates/oauth2_provider/application_list.html | 2 +- .../oauth2_provider/application_registration_form.html | 2 +- oauth2_provider/templatetags/__init__.py | 0 oauth2_provider/templatetags/compat.py | 10 ++++++++++ 8 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 oauth2_provider/templatetags/__init__.py create mode 100644 oauth2_provider/templatetags/compat.py diff --git a/oauth2_provider/compat.py b/oauth2_provider/compat.py index 4266c341e..3fca93610 100644 --- a/oauth2_provider/compat.py +++ b/oauth2_provider/compat.py @@ -37,3 +37,9 @@ get_model = apps.get_model except ImportError: from django.db.models import get_model + +# Django 1.5 add the support of context variables for the url template tag +if django.VERSION >= (1, 5): + from django.template.defaulttags import url +else: + from django.templatetags.future import url diff --git a/oauth2_provider/templates/oauth2_provider/application_confirm_delete.html b/oauth2_provider/templates/oauth2_provider/application_confirm_delete.html index 1651f5177..b1d944f9e 100644 --- a/oauth2_provider/templates/oauth2_provider/application_confirm_delete.html +++ b/oauth2_provider/templates/oauth2_provider/application_confirm_delete.html @@ -1,7 +1,7 @@ {% extends "oauth2_provider/base.html" %} {% load i18n %} -{% load url from future %} +{% load url from compat %} {% block content %}

{% trans "Are you sure to delete the application" %} {{ application.name }}?

diff --git a/oauth2_provider/templates/oauth2_provider/application_detail.html b/oauth2_provider/templates/oauth2_provider/application_detail.html index 36eb583a1..833f9a581 100644 --- a/oauth2_provider/templates/oauth2_provider/application_detail.html +++ b/oauth2_provider/templates/oauth2_provider/application_detail.html @@ -1,7 +1,7 @@ {% extends "oauth2_provider/base.html" %} {% load i18n %} -{% load url from future %} +{% load url from compat %} {% block content %}

{{ application.name }}

diff --git a/oauth2_provider/templates/oauth2_provider/application_form.html b/oauth2_provider/templates/oauth2_provider/application_form.html index baa81a1b5..5c08ff0aa 100644 --- a/oauth2_provider/templates/oauth2_provider/application_form.html +++ b/oauth2_provider/templates/oauth2_provider/application_form.html @@ -1,7 +1,7 @@ {% extends "oauth2_provider/base.html" %} {% load i18n %} -{% load url from future %} +{% load url from compat %} {% block content %}
diff --git a/oauth2_provider/templates/oauth2_provider/application_list.html b/oauth2_provider/templates/oauth2_provider/application_list.html index c50049308..cb7c7c4eb 100644 --- a/oauth2_provider/templates/oauth2_provider/application_list.html +++ b/oauth2_provider/templates/oauth2_provider/application_list.html @@ -1,7 +1,7 @@ {% extends "oauth2_provider/base.html" %} {% load i18n %} -{% load url from future %} +{% load url from compat %} {% block content %}

{% trans "Your applications" %}

diff --git a/oauth2_provider/templates/oauth2_provider/application_registration_form.html b/oauth2_provider/templates/oauth2_provider/application_registration_form.html index 077e2315d..69bebb283 100644 --- a/oauth2_provider/templates/oauth2_provider/application_registration_form.html +++ b/oauth2_provider/templates/oauth2_provider/application_registration_form.html @@ -1,7 +1,7 @@ {% extends "oauth2_provider/application_form.html" %} {% load i18n %} -{% load url from future %} +{% load url from compat %} {% block app-form-title %}{% trans "Register a new application" %}{% endblock app-form-title %} diff --git a/oauth2_provider/templatetags/__init__.py b/oauth2_provider/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/oauth2_provider/templatetags/compat.py b/oauth2_provider/templatetags/compat.py new file mode 100644 index 000000000..8fbc8b0c4 --- /dev/null +++ b/oauth2_provider/templatetags/compat.py @@ -0,0 +1,10 @@ +from django import template + +from ..compat import url as url_compat + +register = template.Library() + + +@register.tag +def url(parser, token): + return url_compat(parser, token) From a573eee4f8197007cd8689e0be72d0c2c8b49939 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Thu, 15 Oct 2015 19:45:32 +0200 Subject: [PATCH 16/91] Use a compat import for the NullHandler --- oauth2_provider/compat.py | 6 ++++++ oauth2_provider/tests/settings.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/oauth2_provider/compat.py b/oauth2_provider/compat.py index 3fca93610..2a6f9ebd0 100644 --- a/oauth2_provider/compat.py +++ b/oauth2_provider/compat.py @@ -43,3 +43,9 @@ from django.template.defaulttags import url else: from django.templatetags.future import url + +# Django 1.9 drops the NullHandler since Python 2.7 includes it +try: + from logging import NullHandler +except ImportError: + from django.utils.log import NullHandler \ No newline at end of file diff --git a/oauth2_provider/tests/settings.py b/oauth2_provider/tests/settings.py index 990b797cd..528f34070 100644 --- a/oauth2_provider/tests/settings.py +++ b/oauth2_provider/tests/settings.py @@ -100,7 +100,7 @@ }, 'null': { 'level': 'DEBUG', - 'class': 'logging.NullHandler', + 'class': 'oauth2_provider.compat.NullHandler', }, }, 'loggers': { From 489cf135508130aea2c4e530fa72f66b20966584 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Thu, 15 Oct 2015 20:02:36 +0200 Subject: [PATCH 17/91] Drop support for the py3.3 under Django 1.9 but add support for py3.5 --- .travis.yml | 2 +- tox.ini | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0bf3413a7..8e5c9a0fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,12 +15,12 @@ env: - TOX_ENV=py33-django16 - TOX_ENV=py33-django17 - TOX_ENV=py33-django18 - - TOX_ENV=py33-django19 - TOX_ENV=py34-django15 - TOX_ENV=py34-django16 - TOX_ENV=py34-django17 - TOX_ENV=py34-django18 - TOX_ENV=py34-django19 + - TOX_ENV=py35-django19 - TOX_ENV=docs install: diff --git a/tox.ini b/tox.ini index c7442fed0..b3d004ecb 100644 --- a/tox.ini +++ b/tox.ini @@ -2,8 +2,9 @@ envlist = py26-django14, py26-django15, py26-django16, py27-django14, py27-django15, py27-django16, py27-django17, py27-django18, py27-django19, - py33-django15, py33-django16, py33-django17, py33-django18, py33-django19, + py33-django15, py33-django16, py33-django17, py33-django18, py34-django15, py34-django16, py34-django17, py34-django18, py34-django19, + py35-django19, docs, flake8 @@ -133,6 +134,12 @@ deps = git+https://github.com/django/django.git@stable/1.9.x#egg=Django {[testenv]deps} +[testenv:py35-django19] +basepython = python3.5 +deps = + git+https://github.com/django/django.git@stable/1.9.x#egg=Django + {[testenv]deps} + [testenv:docs] basepython=python changedir=docs From c2b9b5fcbe716a696be57d6faefb615d4b9cbae1 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Thu, 15 Oct 2015 20:23:59 +0200 Subject: [PATCH 18/91] Extract the compat for the null handler since it causes an import loop --- oauth2_provider/compat.py | 6 ------ oauth2_provider/compat_handlers.py | 5 +++++ oauth2_provider/tests/settings.py | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) create mode 100644 oauth2_provider/compat_handlers.py diff --git a/oauth2_provider/compat.py b/oauth2_provider/compat.py index 2a6f9ebd0..3fca93610 100644 --- a/oauth2_provider/compat.py +++ b/oauth2_provider/compat.py @@ -43,9 +43,3 @@ from django.template.defaulttags import url else: from django.templatetags.future import url - -# Django 1.9 drops the NullHandler since Python 2.7 includes it -try: - from logging import NullHandler -except ImportError: - from django.utils.log import NullHandler \ No newline at end of file diff --git a/oauth2_provider/compat_handlers.py b/oauth2_provider/compat_handlers.py new file mode 100644 index 000000000..21859e80e --- /dev/null +++ b/oauth2_provider/compat_handlers.py @@ -0,0 +1,5 @@ +# Django 1.9 drops the NullHandler since Python 2.7 includes it +try: + from logging import NullHandler +except ImportError: + from django.utils.log import NullHandler diff --git a/oauth2_provider/tests/settings.py b/oauth2_provider/tests/settings.py index 528f34070..e144e8006 100644 --- a/oauth2_provider/tests/settings.py +++ b/oauth2_provider/tests/settings.py @@ -100,7 +100,7 @@ }, 'null': { 'level': 'DEBUG', - 'class': 'oauth2_provider.compat.NullHandler', + 'class': 'oauth2_provider.compat_handlers.NullHandler', }, }, 'loggers': { From 8043ffb64604eb6324cf5cb08929070f9c812086 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Thu, 15 Oct 2015 20:27:04 +0200 Subject: [PATCH 19/91] Flip the try/except import for the unittest module --- oauth2_provider/tests/test_rest_framework.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauth2_provider/tests/test_rest_framework.py b/oauth2_provider/tests/test_rest_framework.py index 7aa8ffed9..83c9dbe12 100644 --- a/oauth2_provider/tests/test_rest_framework.py +++ b/oauth2_provider/tests/test_rest_framework.py @@ -6,9 +6,9 @@ from django.utils import timezone try: - import unittest -except ImportError: from django.utils import unittest +except ImportError: + import unittest from .test_utils import TestCaseUtils from ..models import AccessToken, get_application_model From 24d70a0e74dddedeb94a85ea5d99371a4e034a2e Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Thu, 15 Oct 2015 20:43:56 +0200 Subject: [PATCH 20/91] Remove the python3.5 interpreter for now since Travis is not ready --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8e5c9a0fc..f26365704 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,6 @@ env: - TOX_ENV=py34-django17 - TOX_ENV=py34-django18 - TOX_ENV=py34-django19 - - TOX_ENV=py35-django19 - TOX_ENV=docs install: From 89d6e3ff4f86f6853a91b88339760abae90ad0e6 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Thu, 15 Oct 2015 23:47:31 +0200 Subject: [PATCH 21/91] Use the new travis architecture for better stability --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index f26365704..58801694c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: python python: "2.7" +sudo: false env: - TOX_ENV=py26-django14 From 042ca54cff930f9004733b7487bbe903a86212a4 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 16 Oct 2015 09:54:02 +0200 Subject: [PATCH 22/91] Remove an unsupported environment, Django 1.9 drops Python 3.3 support --- tox.ini | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tox.ini b/tox.ini index b3d004ecb..9069304fe 100644 --- a/tox.ini +++ b/tox.ini @@ -97,12 +97,6 @@ deps = Django<1.9 {[testenv]deps} -[testenv:py33-django19] -basepython = python3.3 -deps = - git+https://github.com/django/django.git@stable/1.9.x#egg=Django - {[testenv]deps} - [testenv:py34-django15] basepython = python3.4 deps = From 82fe41cebbb1c5e8fdc291225a24a32e50a7d8a1 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 16 Oct 2015 16:03:50 +0200 Subject: [PATCH 23/91] Use compact syntax for tox envs --- tox.ini | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index 9069304fe..def45b189 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,10 @@ [tox] envlist = - py26-django14, py26-django15, py26-django16, - py27-django14, py27-django15, py27-django16, py27-django17, py27-django18, py27-django19, - py33-django15, py33-django16, py33-django17, py33-django18, - py34-django15, py34-django16, py34-django17, py34-django18, py34-django19, - py35-django19, + {py26}-django{14,15,16}, + {py27}-django{14,15,16,17,18,19}, + {py33}-django{15,16,17,18}, + {py34}-django{15,16,17,18,19}, + {py35}-django{19}, docs, flake8 From ba798e4246748941b30e4a79b1c749bd612be947 Mon Sep 17 00:00:00 2001 From: Pierre Dulac Date: Fri, 16 Oct 2015 16:27:00 +0200 Subject: [PATCH 24/91] Add the py35-django19 env to travis but allow it to fail --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index 58801694c..31ffd3fce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,8 +21,15 @@ env: - TOX_ENV=py34-django17 - TOX_ENV=py34-django18 - TOX_ENV=py34-django19 + - TOX_ENV=py35-django19 - TOX_ENV=docs +matrix: + # Python 3.5 not yet available on travis, watch this to see when it is. + fast_finish: true + allow_failures: + - env: TOX_ENV=py35-django19 + install: - pip install tox - pip install coveralls From f4d27cac53f0ca98fe512da456c407aea9af91f1 Mon Sep 17 00:00:00 2001 From: Federico Dolce Date: Thu, 22 Oct 2015 16:42:21 +0200 Subject: [PATCH 25/91] Added test for pluggable serverclass --- oauth2_provider/tests/test_oauth2_backends.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/oauth2_provider/tests/test_oauth2_backends.py b/oauth2_provider/tests/test_oauth2_backends.py index bab4891eb..127aeea7c 100644 --- a/oauth2_provider/tests/test_oauth2_backends.py +++ b/oauth2_provider/tests/test_oauth2_backends.py @@ -1,16 +1,24 @@ import json +import mock from django.test import TestCase, RequestFactory +from django.test.utils import override_settings from ..backends import get_oauthlib_core from ..oauth2_backends import OAuthLibCore, JSONOAuthLibCore class TestOAuthLibCoreBackend(TestCase): + def setUp(self): self.factory = RequestFactory() self.oauthlib_core = OAuthLibCore() + def test_swappable_serer_class(self): + with mock.patch('oauth2_provider.oauth2_backends.oauth2_settings.OAUTH2_SERVER_CLASS'): + oauthlib_core = OAuthLibCore() + self.assertTrue(isinstance(oauthlib_core.server, mock.MagicMock)) + def test_form_urlencoded_extract_params(self): payload = "grant_type=password&username=john&password=123456" request = self.factory.post("/o/token/", payload, content_type="application/x-www-form-urlencoded") From a5d9e689751d32369132a39d9517a758406a9790 Mon Sep 17 00:00:00 2001 From: Federico Dolce Date: Thu, 22 Oct 2015 17:40:53 +0200 Subject: [PATCH 26/91] Changelog updated --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index a16702709..54f2b3f97 100644 --- a/README.rst +++ b/README.rst @@ -90,6 +90,12 @@ Changelog master ~~~~~~~~~~~~~~~~~~ +* #299: 'server_class' is now pluggable through Django settings +* #309: Add the py35-django19 env to travis +* #308: Use compact syntax for tox envs +* #306: Django 1.9 compatibility +* #288: Put additional information when generating token responses +* #297: Fixed doc about SessionAuthenticationMiddleware * #273: Generic read write scope by resource 0.9.0 [2015-07-28] From 71e62feb8d22fbb7dee054689ea63ca962ebe2bc Mon Sep 17 00:00:00 2001 From: Federico Dolce Date: Thu, 22 Oct 2015 17:41:48 +0200 Subject: [PATCH 27/91] Added python 3.5 and Django 1.9 to requirements --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 54f2b3f97..3a25ffad4 100644 --- a/README.rst +++ b/README.rst @@ -37,8 +37,8 @@ guidelines Date: Thu, 22 Oct 2015 17:53:54 +0200 Subject: [PATCH 28/91] Docs changelog updated --- docs/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2619f0a95..2b79621a0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,12 @@ Changelog master ------------------ +* #299: 'server_class' is now pluggable through Django settings +* #309: Add the py35-django19 env to travis +* #308: Use compact syntax for tox envs +* #306: Django 1.9 compatibility +* #288: Put additional information when generating token responses +* #297: Fixed doc about SessionAuthenticationMiddleware * #273: Generic read write scope by resource From 58f02a1c76f9bee37ebb22a022d69a370b670262 Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Thu, 19 Nov 2015 17:37:07 +0100 Subject: [PATCH 29/91] updated support for django and python --- .travis.yml | 19 ++----------- tox.ini | 82 +++++++++++------------------------------------------ 2 files changed, 20 insertions(+), 81 deletions(-) diff --git a/.travis.yml b/.travis.yml index 31ffd3fce..d2a58cd1a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,33 +3,20 @@ python: "2.7" sudo: false env: - - TOX_ENV=py26-django14 - - TOX_ENV=py26-django15 - - TOX_ENV=py26-django16 - - TOX_ENV=py27-django14 - - TOX_ENV=py27-django15 - - TOX_ENV=py27-django16 - TOX_ENV=py27-django17 - TOX_ENV=py27-django18 - TOX_ENV=py27-django19 - - TOX_ENV=py33-django15 - - TOX_ENV=py33-django16 + - TOX_ENV=py32-django17 + - TOX_ENV=py32-django18 - TOX_ENV=py33-django17 - TOX_ENV=py33-django18 - - TOX_ENV=py34-django15 - - TOX_ENV=py34-django16 - TOX_ENV=py34-django17 - TOX_ENV=py34-django18 - TOX_ENV=py34-django19 + - TOX_ENV=py35-django18 - TOX_ENV=py35-django19 - TOX_ENV=docs -matrix: - # Python 3.5 not yet available on travis, watch this to see when it is. - fast_finish: true - allow_failures: - - env: TOX_ENV=py35-django19 - install: - pip install tox - pip install coveralls diff --git a/tox.ini b/tox.ini index def45b189..fc6af1271 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,10 @@ [tox] envlist = - {py26}-django{14,15,16}, - {py27}-django{14,15,16,17,18,19}, - {py33}-django{15,16,17,18}, - {py34}-django{15,16,17,18,19}, - {py35}-django{19}, + {py27}-django{17,18,19}, + {py32}-django{17,18} + {py33}-django{17,18}, + {py34}-django{17,18,19}, + {py35}-django{18,19}, docs, flake8 @@ -14,46 +14,6 @@ commands=coverage run -a runtests.py deps = -r{toxinidir}/requirements/testing.txt -[testenv:py26-django14] -basepython = python2.6 -deps = - Django<1.5 - django-discover-runner - {[testenv]deps} - -[testenv:py26-django15] -basepython = python2.6 -deps = - Django<1.6 - django-discover-runner - {[testenv]deps} - -[testenv:py26-django16] -basepython = python2.6 -deps = - Django<1.7 - {[testenv]deps} - -[testenv:py27-django14] -basepython = python2.7 -deps = - Django<1.5 - django-discover-runner - {[testenv]deps} - -[testenv:py27-django15] -basepython = python2.7 -deps = - Django<1.6 - django-discover-runner - {[testenv]deps} - -[testenv:py27-django16] -basepython = python2.7 -deps = - Django<1.7 - {[testenv]deps} - [testenv:py27-django17] basepython = python2.7 deps = @@ -72,17 +32,16 @@ deps = git+https://github.com/django/django.git@stable/1.9.x#egg=Django {[testenv]deps} -[testenv:py33-django15] -basepython = python3.3 +[testenv:py32-django17] +basepython = python3.2 deps = - Django<1.6 - django-discover-runner + Django<1.8 {[testenv]deps} -[testenv:py33-django16] -basepython = python3.3 +[testenv:py32-django18] +basepython = python3.2 deps = - Django<1.7 + Django<1.9 {[testenv]deps} [testenv:py33-django17] @@ -97,19 +56,6 @@ deps = Django<1.9 {[testenv]deps} -[testenv:py34-django15] -basepython = python3.4 -deps = - Django<1.6 - django-discover-runner - {[testenv]deps} - -[testenv:py34-django16] -basepython = python3.4 -deps = - Django<1.7 - {[testenv]deps} - [testenv:py34-django17] basepython = python3.4 deps = @@ -128,6 +74,12 @@ deps = git+https://github.com/django/django.git@stable/1.9.x#egg=Django {[testenv]deps} +[testenv:py35-django18] +basepython = python3.5 +deps = + Django<1.9 + {[testenv]deps} + [testenv:py35-django19] basepython = python3.5 deps = From 9d5b939c0cec0aead8eb8c5cad6955718ab854d5 Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Fri, 20 Nov 2015 11:56:27 +0100 Subject: [PATCH 30/91] improved test matrix --- .travis.yml | 7 ++++ requirements/testing.txt | 1 - tox.ini | 80 +++------------------------------------- 3 files changed, 13 insertions(+), 75 deletions(-) diff --git a/.travis.yml b/.travis.yml index d2a58cd1a..064545683 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,13 @@ env: - TOX_ENV=py35-django19 - TOX_ENV=docs +matrix: + include: + - python: 3.5 + env: + - TOX_ENV=py35-django18 + - TOX_ENV=py35-django19 + install: - pip install tox - pip install coveralls diff --git a/requirements/testing.txt b/requirements/testing.txt index 544875e36..fff8bc1f1 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -1,3 +1,2 @@ -r optional.txt -coverage mock==1.0.1 diff --git a/tox.ini b/tox.ini index fc6af1271..305121091 100644 --- a/tox.ini +++ b/tox.ini @@ -9,82 +9,14 @@ envlist = flake8 [testenv] -downloadcache = {toxworkdir}/cache/ commands=coverage run -a runtests.py deps = - -r{toxinidir}/requirements/testing.txt - -[testenv:py27-django17] -basepython = python2.7 -deps = - Django<1.8 - {[testenv]deps} - -[testenv:py27-django18] -basepython = python2.7 -deps = - Django<1.9 - {[testenv]deps} - -[testenv:py27-django19] -basepython = python2.7 -deps = - git+https://github.com/django/django.git@stable/1.9.x#egg=Django - {[testenv]deps} - -[testenv:py32-django17] -basepython = python3.2 -deps = - Django<1.8 - {[testenv]deps} - -[testenv:py32-django18] -basepython = python3.2 -deps = - Django<1.9 - {[testenv]deps} - -[testenv:py33-django17] -basepython = python3.3 -deps = - Django<1.8 - {[testenv]deps} - -[testenv:py33-django18] -basepython = python3.3 -deps = - Django<1.9 - {[testenv]deps} - -[testenv:py34-django17] -basepython = python3.4 -deps = - Django<1.8 - {[testenv]deps} - -[testenv:py34-django18] -basepython = python3.4 -deps = - Django<1.9 - {[testenv]deps} - -[testenv:py34-django19] -basepython = python3.4 -deps = - git+https://github.com/django/django.git@stable/1.9.x#egg=Django - {[testenv]deps} - -[testenv:py35-django18] -basepython = python3.5 -deps = - Django<1.9 - {[testenv]deps} - -[testenv:py35-django19] -basepython = python3.5 -deps = - git+https://github.com/django/django.git@stable/1.9.x#egg=Django - {[testenv]deps} + django17: Django==1.7.10 + django18: Django==1.8.6 + django19: https://www.djangoproject.com/download/1.9rc1/tarball/ + py32: coverage<4 + py{27,33,34,35}: coverage + -rrequirements/testing.txt [testenv:docs] basepython=python From 3d3a75c6292f823b35fb728e05e40a84eb12a998 Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Fri, 20 Nov 2015 11:56:42 +0100 Subject: [PATCH 31/91] added support for python3.2 --- oauth2_provider/oauth2_validators.py | 6 ++++++ requirements/base.txt | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py index cc669f04f..3b42dad37 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import six import base64 import binascii import logging @@ -60,6 +61,11 @@ def _authenticate_basic_auth(self, request): except AttributeError: encoding = 'utf-8' + # Encode auth_string to bytes. This is needed for python3.2 compatibility + # because b64decode function only supports bytes type in input. + if isinstance(auth_string, six.string_types): + auth_string = auth_string.encode(encoding) + try: b64_decoded = base64.b64decode(auth_string) except (TypeError, binascii.Error): diff --git a/requirements/base.txt b/requirements/base.txt index 1dd139d74..2a471eb19 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,5 +1,5 @@ Sphinx==1.3.1 South==1.0 oauthlib==1.0.1 -django-braces==1.4.0 +django-braces==1.8.1 six From 38992befe08174bcd013c667ffecebbfca84400a Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Fri, 20 Nov 2015 12:20:09 +0100 Subject: [PATCH 32/91] python3.5 envs allowed to fail --- .travis.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 064545683..70b52233a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,9 +18,10 @@ env: - TOX_ENV=docs matrix: - include: - - python: 3.5 - env: + # Python 3.5 not yet available on travis, watch this to see when it is. + fast_finish: true + allow_failures: + - env: - TOX_ENV=py35-django18 - TOX_ENV=py35-django19 From 772dbed1ccc6a828fc85466a1bf9043f78b18385 Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Fri, 20 Nov 2015 12:30:40 +0100 Subject: [PATCH 33/91] updated travis config for python3.5 ...again --- .travis.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 70b52233a..b344a7538 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,12 +18,11 @@ env: - TOX_ENV=docs matrix: - # Python 3.5 not yet available on travis, watch this to see when it is. + # Python 3.5 not yet available on travis, watch this to see when it is. fast_finish: true allow_failures: - - env: - - TOX_ENV=py35-django18 - - TOX_ENV=py35-django19 + - env: TOX_ENV=py35-django18 + - env: TOX_ENV=py35-django19 install: - pip install tox From ca513fe30a98585854523aee9c73fcc779d07a3d Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Fri, 20 Nov 2015 12:51:24 +0100 Subject: [PATCH 34/91] updated changelog and the list of supported python and django versions --- README.rst | 5 +++-- docs/changelog.rst | 1 + docs/index.rst | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 3a25ffad4..2044e9b56 100644 --- a/README.rst +++ b/README.rst @@ -37,8 +37,8 @@ guidelines Date: Fri, 20 Nov 2015 12:51:50 +0100 Subject: [PATCH 35/91] removed references to syncdb --- docs/install.rst | 1 - docs/tutorial/tutorial_01.rst | 22 +++++++++++----------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/docs/install.rst b/docs/install.rst index efc21e90e..adaf95f34 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -29,7 +29,6 @@ Sync your database .. sourcecode:: sh - $ python manage.py syncdb $ python manage.py migrate oauth2_provider Next step is our :doc:`first tutorial `. diff --git a/docs/tutorial/tutorial_01.rst b/docs/tutorial/tutorial_01.rst index c7c66873c..fdb1c3edc 100644 --- a/docs/tutorial/tutorial_01.rst +++ b/docs/tutorial/tutorial_01.rst @@ -8,7 +8,7 @@ You want to make your own :term:`Authorization Server` to issue access tokens to Start Your App -------------- During this tutorial you will make an XHR POST from a Heroku deployed app to your localhost instance. -Since the domain that will originate the request (the app on Heroku) is different than the destination domain (your local instance), +Since the domain that will originate the request (the app on Heroku) is different than the destination domain (your local instance), you will need to install the `django-cors-headers `_ app. These "cross-domain" requests are by default forbidden by web browsers unless you use `CORS `_. @@ -60,29 +60,29 @@ Allow CORS requests from all domains (just for the scope of this tutorial): Include the required hidden input in your login template, `registration/login.html`. The ``{{ next }}`` template context variable will be populated with the correct -redirect value. See the `Django documentation `_ +redirect value. See the `Django documentation `_ for details on using login templates. .. code-block:: html -As a final step, execute syncdb, start the internal server, and login with your credentials. +As a final step, execute migrate command, start the internal server, and login with your credentials. Create an OAuth2 Client Application ----------------------------------- -Before your :term:`Application` can use the :term:`Authorization Server` for user login, -you must first register the app (also known as the :term:`Client`.) Once registered, your app will be granted access to +Before your :term:`Application` can use the :term:`Authorization Server` for user login, +you must first register the app (also known as the :term:`Client`.) Once registered, your app will be granted access to the API, subject to approval by its users. -Let's register your application. +Let's register your application. Point your browser to http://localhost:8000/o/applications/ and add an Application instance. `Client id` and `Client Secret` are automatically generated, you have to provide the rest of the informations: * `User`: the owner of the Application (e.g. a developer, or the currently logged in user.) - * `Redirect uris`: Applications must register at least one redirection endpoint prior to utilizing the + * `Redirect uris`: Applications must register at least one redirection endpoint prior to utilizing the authorization endpoint. The :term:`Authorization Server` will deliver the access token to the client only if the client specifies one of the verified redirection uris. For this tutorial, paste verbatim the value `http://django-oauth-toolkit.herokuapp.com/consumer/exchange/` @@ -101,16 +101,16 @@ process we'll explain shortly) Test Your Authorization Server ------------------------------ Your authorization server is ready and can begin issuing access tokens. To test the process you need an OAuth2 -consumer; if you are familiar enough with OAuth2, you can use curl, requests, or anything that speaks http. For the rest -of us, there is a `consumer service `_ deployed on Heroku to test +consumer; if you are familiar enough with OAuth2, you can use curl, requests, or anything that speaks http. For the rest +of us, there is a `consumer service `_ deployed on Heroku to test your provider. Build an Authorization Link for Your Users ++++++++++++++++++++++++++++++++++++++++++ Authorizing an application to access OAuth2 protected data in an :term:`Authorization Code` flow is always initiated -by the user. Your application can prompt users to click a special link to start the process. Go to the +by the user. Your application can prompt users to click a special link to start the process. Go to the `Consumer `_ page and complete the form by filling in your -application's details obtained from the steps in this tutorial. Submit the form, and you'll receive a link your users can +application's details obtained from the steps in this tutorial. Submit the form, and you'll receive a link your users can use to access the authorization page. Authorize the Application From 95442d293afa95b2e66f433424f3ddd52e8e16cf Mon Sep 17 00:00:00 2001 From: Anton Burnashev Date: Sun, 15 Nov 2015 02:19:39 +0300 Subject: [PATCH 36/91] Fix typo --- docs/tutorial/tutorial_04.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/tutorial_04.rst b/docs/tutorial/tutorial_04.rst index e062c1a76..e115f827f 100644 --- a/docs/tutorial/tutorial_04.rst +++ b/docs/tutorial/tutorial_04.rst @@ -18,7 +18,7 @@ Note that these revocation-specific parameters are in addition to the authentica Setup a Request ---------------- -Depending on the client type you're using, the token revocation request you may submit to the authentication server mayy vary. A `Public` client, for example, will not have access to your `Client Secret`. A revoke request from a public client would omit that secret, and take the form: +Depending on the client type you're using, the token revocation request you may submit to the authentication server may vary. A `Public` client, for example, will not have access to your `Client Secret`. A revoke request from a public client would omit that secret, and take the form: :: From 43c1b5b451bf993ec9a527d836c3e179cecafb2e Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Fri, 20 Nov 2015 18:04:46 +0100 Subject: [PATCH 37/91] application registration view uses custom application model in form class --- oauth2_provider/forms.py | 11 ---------- .../tests/test_application_views.py | 20 +++++++++++++++++++ oauth2_provider/views/application.py | 13 ++++++++++-- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/oauth2_provider/forms.py b/oauth2_provider/forms.py index a8b398554..8cd8e4fef 100644 --- a/oauth2_provider/forms.py +++ b/oauth2_provider/forms.py @@ -1,7 +1,5 @@ from django import forms -from .models import Application - class AllowForm(forms.Form): allow = forms.BooleanField(required=False) @@ -17,12 +15,3 @@ def __init__(self, *args, **kwargs): if data and 'scopes' in data: data['scope'] = data['scopes'] return super(AllowForm, self).__init__(*args, **kwargs) - - -class RegistrationForm(forms.ModelForm): - """ - TODO: add docstring - """ - class Meta: - model = Application - fields = ('name', 'client_id', 'client_secret', 'client_type', 'authorization_grant_type', 'redirect_uris') diff --git a/oauth2_provider/tests/test_application_views.py b/oauth2_provider/tests/test_application_views.py index 32495ca27..ec94f6ab9 100644 --- a/oauth2_provider/tests/test_application_views.py +++ b/oauth2_provider/tests/test_application_views.py @@ -1,7 +1,9 @@ from __future__ import unicode_literals +import mock from django.core.urlresolvers import reverse from django.test import TestCase +from django.test.utils import override_settings from ..models import get_application_model from ..compat import get_user_model @@ -21,6 +23,24 @@ def tearDown(self): class TestApplicationRegistrationView(BaseTest): + def test_get_form_class(self): + """ + Tests that the form class returned by the 'get_form_class' method is + bound to custom application model defined in the + 'OAUTH2_PROVIDER_APPLICATION_MODEL' setting. + """ + from ..views.application import ApplicationRegistration + from .models import TestApplication + from ..settings import oauth2_settings + # Patch oauth2 settings to use a custom Application model + oauth2_settings.APPLICATION_MODEL = 'tests.TestApplication' + # Create a registration view and tests that the model form is bound + # to the custom Application model + application_form_class = ApplicationRegistration().get_form_class() + self.assertEqual(TestApplication, application_form_class._meta.model) + # Revert oauth2 settings + oauth2_settings.APPLICATION_MODEL = 'oauth2_provider.Application' + def test_application_registration_user(self): self.client.login(username="foo_user", password="123456") diff --git a/oauth2_provider/views/application.py b/oauth2_provider/views/application.py index 0a3598709..777ccf801 100644 --- a/oauth2_provider/views/application.py +++ b/oauth2_provider/views/application.py @@ -1,9 +1,9 @@ from django.core.urlresolvers import reverse_lazy +from django.forms.models import modelform_factory from django.views.generic import CreateView, DetailView, DeleteView, ListView, UpdateView from braces.views import LoginRequiredMixin -from ..forms import RegistrationForm from ..models import get_application_model @@ -21,9 +21,18 @@ class ApplicationRegistration(LoginRequiredMixin, CreateView): """ View used to register a new Application for the request.user """ - form_class = RegistrationForm template_name = "oauth2_provider/application_registration_form.html" + def get_form_class(self): + """ + Returns the form class for the application model + """ + return modelform_factory( + get_application_model(), + fields=('name', 'client_id', 'client_secret', 'client_type', + 'authorization_grant_type', 'redirect_uris') + ) + def form_valid(self, form): form.instance.user = self.request.user return super(ApplicationRegistration, self).form_valid(form) From e8e3af21176f3454ae9eb4473252088f890f44fe Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Fri, 20 Nov 2015 18:30:51 +0100 Subject: [PATCH 38/91] updated changelog --- README.rst | 1 + docs/changelog.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 2044e9b56..b86eefefe 100644 --- a/README.rst +++ b/README.rst @@ -91,6 +91,7 @@ master ~~~~~~~~~~~~~~~~~~ * **#322: dropping support for python 2.6 and django 1.4, 1.5, 1.6** +* #323: Application registration view uses custom application model in form class * #299: 'server_class' is now pluggable through Django settings * #309: Add the py35-django19 env to travis * #308: Use compact syntax for tox envs diff --git a/docs/changelog.rst b/docs/changelog.rst index 0c8200fe4..92100fdfc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ master ------------------ * **#322: dropping support for python 2.6 and django 1.4, 1.5, 1.6** +* #323: Application registration view uses custom application model in form class * #299: 'server_class' is now pluggable through Django settings * #309: Add the py35-django19 env to travis * #308: Use compact syntax for tox envs From cd462990e132de1e50328b74a6f324ef77543805 Mon Sep 17 00:00:00 2001 From: trbs Date: Mon, 18 May 2015 19:55:47 +0200 Subject: [PATCH 39/91] Issue #148 cleanup of expired tokens Simple implementation of cleanup strategy for expired tokens. --- oauth2_provider/management/__init__.py | 0 oauth2_provider/management/commands/__init__.py | 0 .../management/commands/cleartokens.py | 9 +++++++++ oauth2_provider/models.py | 17 ++++++++++++++++- oauth2_provider/settings.py | 1 + 5 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 oauth2_provider/management/__init__.py create mode 100644 oauth2_provider/management/commands/__init__.py create mode 100644 oauth2_provider/management/commands/cleartokens.py diff --git a/oauth2_provider/management/__init__.py b/oauth2_provider/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/oauth2_provider/management/commands/__init__.py b/oauth2_provider/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/oauth2_provider/management/commands/cleartokens.py b/oauth2_provider/management/commands/cleartokens.py new file mode 100644 index 000000000..5b56d2bc1 --- /dev/null +++ b/oauth2_provider/management/commands/cleartokens.py @@ -0,0 +1,9 @@ +from django.core.management.base import BaseCommand, CommandError +from ...models import clear_expired + + +class Command(BaseCommand): + help = "Can be run as a cronjob or directly to clean out expired tokens" + + def handle(self, *args, **options): + clear_expired() diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py index 1d26726be..16416c983 100644 --- a/oauth2_provider/models.py +++ b/oauth2_provider/models.py @@ -1,7 +1,9 @@ from __future__ import unicode_literals +from datetime import timedelta + from django.core.urlresolvers import reverse -from django.db import models +from django.db import models, transaction from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -266,3 +268,16 @@ def get_application_model(): e = "APPLICATION_MODEL refers to model {0} that has not been installed" raise ImproperlyConfigured(e.format(oauth2_settings.APPLICATION_MODEL)) return app_model + + +def clear_expired(): + REFRESH_TOKEN_EXPIRE_SECONDS = oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS + if not isinstance(REFRESH_TOKEN_EXPIRE_SECONDS, timedelta): + REFRESH_TOKEN_EXPIRE_SECONDS = timedelta(seconds=REFRESH_TOKEN_EXPIRE_SECONDS) + now = timezone.now() + with transaction.atomic(): + if REFRESH_TOKEN_EXPIRE_SECONDS: + refresh_expire_date = now - REFRESH_TOKEN_EXPIRE_SECONDS + RefreshToken.objects.filter(access_token__expires__lt=refresh_expire_date).delete() + AccessToken.objects.filter(refresh_token__isnull=True, expires__lt=now).delete() + Grant.objects.filter(expires__lt=now).delete() diff --git a/oauth2_provider/settings.py b/oauth2_provider/settings.py index d7903d61c..977a80dd4 100644 --- a/oauth2_provider/settings.py +++ b/oauth2_provider/settings.py @@ -41,6 +41,7 @@ 'WRITE_SCOPE': 'write', 'AUTHORIZATION_CODE_EXPIRE_SECONDS': 60, 'ACCESS_TOKEN_EXPIRE_SECONDS': 36000, + 'REFRESH_TOKEN_EXPIRE_SECONDS': None, 'APPLICATION_MODEL': getattr(settings, 'OAUTH2_PROVIDER_APPLICATION_MODEL', 'oauth2_provider.Application'), 'REQUEST_APPROVAL_PROMPT': 'force', 'ALLOWED_REDIRECT_URI_SCHEMES': ['http', 'https'], From 32c8053ad3b8673ce57e575a61393e2b2a51ee2d Mon Sep 17 00:00:00 2001 From: trbs Date: Sun, 1 Nov 2015 23:08:48 +0100 Subject: [PATCH 40/91] improve variable checking in clear_expired --- oauth2_provider/models.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py index 16416c983..6ae1384fb 100644 --- a/oauth2_provider/models.py +++ b/oauth2_provider/models.py @@ -271,13 +271,21 @@ def get_application_model(): def clear_expired(): - REFRESH_TOKEN_EXPIRE_SECONDS = oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS - if not isinstance(REFRESH_TOKEN_EXPIRE_SECONDS, timedelta): - REFRESH_TOKEN_EXPIRE_SECONDS = timedelta(seconds=REFRESH_TOKEN_EXPIRE_SECONDS) now = timezone.now() + refresh_expire_at = None + + REFRESH_TOKEN_EXPIRE_SECONDS = oauth2_settings.REFRESH_TOKEN_EXPIRE_SECONDS + if REFRESH_TOKEN_EXPIRE_SECONDS: + if not isinstance(REFRESH_TOKEN_EXPIRE_SECONDS, timedelta): + try: + REFRESH_TOKEN_EXPIRE_SECONDS = timedelta(seconds=REFRESH_TOKEN_EXPIRE_SECONDS) + except TypeError: + e = "REFRESH_TOKEN_EXPIRE_SECONDS must be either a timedelta or seconds" + raise ImproperlyConfigured(e) + refresh_expire_at = now - REFRESH_TOKEN_EXPIRE_SECONDS + with transaction.atomic(): - if REFRESH_TOKEN_EXPIRE_SECONDS: - refresh_expire_date = now - REFRESH_TOKEN_EXPIRE_SECONDS - RefreshToken.objects.filter(access_token__expires__lt=refresh_expire_date).delete() + if refresh_expire_at: + RefreshToken.objects.filter(access_token__expires__lt=refresh_expire_at).delete() AccessToken.objects.filter(refresh_token__isnull=True, expires__lt=now).delete() Grant.objects.filter(expires__lt=now).delete() From d2c8b9690c609de373ae648833f21aa18ae61b04 Mon Sep 17 00:00:00 2001 From: trbs Date: Sun, 1 Nov 2015 23:09:15 +0100 Subject: [PATCH 41/91] add documentation for refresh_token_expire_seconds --- AUTHORS | 1 + docs/settings.rst | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/AUTHORS b/AUTHORS index 34e66b331..88c94b7f0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -14,3 +14,4 @@ Ash Christopher Rodney Richardson Hiroki Kiyohara Diego Garcia +Bas van Oostveen diff --git a/docs/settings.rst b/docs/settings.rst index 4df3e148c..cfbbe757e 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -86,6 +86,19 @@ WRITE_SCOPE ~~~~~~~~~~~ The name of the *write* scope. +REFRESH_TOKEN_EXPIRE_SECONDS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The number of seconds before a refresh token gets removed from the database by +the ``cleartokens``` management command. It's important that ``cleartokens`` +runs regularly (eg: via cron) in order for this setting to work. + +If ``cleartokens`` runs daily the maximum delay before a refresh token is +removed is ``REFRESH_TOKEN_EXPIRE_SECONDS`` + 1 day. This is normally not a +problem since refresh tokens are long lived. + +Note: Refresh tokens need to expire before AccessTokens can be removed from the +database. Using ``cleartokens`` without ``REFRESH_TOKEN_EXPIRE_SECONDS`` has limited effect. + REQUEST_APPROVAL_PROMPT ~~~~~~~~~~~~~~~~~~~~~~~ Can be ``'force'`` or ``'auto'``. From 96fbbf7ce2ce170117a7f3c892ef70162f4757f5 Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Mon, 23 Nov 2015 14:15:52 +0100 Subject: [PATCH 42/91] updated changelog --- README.rst | 1 + docs/changelog.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/README.rst b/README.rst index b86eefefe..ec111be52 100644 --- a/README.rst +++ b/README.rst @@ -91,6 +91,7 @@ master ~~~~~~~~~~~~~~~~~~ * **#322: dropping support for python 2.6 and django 1.4, 1.5, 1.6** +* #249: Added a command to clean expired tokens * #323: Application registration view uses custom application model in form class * #299: 'server_class' is now pluggable through Django settings * #309: Add the py35-django19 env to travis diff --git a/docs/changelog.rst b/docs/changelog.rst index 92100fdfc..b9745dbf5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ master ------------------ * **#322: dropping support for python 2.6 and django 1.4, 1.5, 1.6** +* #249: Added a command to clean expired tokens * #323: Application registration view uses custom application model in form class * #299: 'server_class' is now pluggable through Django settings * #309: Add the py35-django19 env to travis From 91d12c6db54bc4ca73f9a89bc53e889ec95970a4 Mon Sep 17 00:00:00 2001 From: Bart Merenda Date: Fri, 30 Oct 2015 11:02:26 +0100 Subject: [PATCH 43/91] Fixed issue 40 --- oauth2_provider/models.py | 7 + .../authorized-token-delete.html | 9 + .../oauth2_provider/authorized-tokens.html | 24 +++ .../tests/test_application_views.py | 7 + oauth2_provider/tests/test_models.py | 30 ++++ oauth2_provider/tests/test_token_view.py | 166 ++++++++++++++++++ oauth2_provider/urls.py | 6 + oauth2_provider/views/__init__.py | 1 + oauth2_provider/views/application.py | 6 +- oauth2_provider/views/token.py | 37 ++++ 10 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 oauth2_provider/templates/oauth2_provider/authorized-token-delete.html create mode 100644 oauth2_provider/templates/oauth2_provider/authorized-tokens.html create mode 100644 oauth2_provider/tests/test_token_view.py create mode 100644 oauth2_provider/views/token.py diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py index 6ae1384fb..90a5ade2a 100644 --- a/oauth2_provider/models.py +++ b/oauth2_provider/models.py @@ -221,6 +221,13 @@ def revoke(self): """ self.delete() + @property + 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()} + def __str__(self): return self.token diff --git a/oauth2_provider/templates/oauth2_provider/authorized-token-delete.html b/oauth2_provider/templates/oauth2_provider/authorized-token-delete.html new file mode 100644 index 000000000..e08233a70 --- /dev/null +++ b/oauth2_provider/templates/oauth2_provider/authorized-token-delete.html @@ -0,0 +1,9 @@ +{% extends "oauth2_provider/base.html" %} + +{% load i18n %} +{% block content %} + {% csrf_token %} +

{% trans "Are you sure you want to delete this token?" %}

+ + +{% endblock %} diff --git a/oauth2_provider/templates/oauth2_provider/authorized-tokens.html b/oauth2_provider/templates/oauth2_provider/authorized-tokens.html new file mode 100644 index 000000000..f25069e61 --- /dev/null +++ b/oauth2_provider/templates/oauth2_provider/authorized-tokens.html @@ -0,0 +1,24 @@ +{% extends "oauth2_provider/base.html" %} + +{% load i18n %} +{% load url from compat %} +{% block content %} +
+

{% trans "Tokens" %}

+
    + {% for authorized_token in authorized_tokens %} +
  • + {{ authorized_token.application }} + (revoke) +
  • +
      + {% for scope_name, scope_description in authorized_token.scopes.items %} +
    • {{ scope_name }}: {{ scope_description }}
    • + {% endfor %} +
    + {% empty %} +
  • {% trans "There are no authorized tokens yet." %}
  • + {% endfor %} +
+
+{% endblock %} diff --git a/oauth2_provider/tests/test_application_views.py b/oauth2_provider/tests/test_application_views.py index ec94f6ab9..66227b51c 100644 --- a/oauth2_provider/tests/test_application_views.py +++ b/oauth2_provider/tests/test_application_views.py @@ -99,3 +99,10 @@ def test_application_detail_not_owner(self): response = self.client.get(reverse('oauth2_provider:detail', args=(self.app_bar_1.pk,))) self.assertEqual(response.status_code, 404) + + def test_delete_view_deletes(self): + self.client.login(username="foo_user", password="123456") + response = self.client.post(reverse('oauth2_provider:delete', args=(self.app_foo_1.pk,))) + + self.assertFalse(Application.objects.filter(pk=self.app_foo_1.pk).exists()) + self.assertRedirects(response, reverse('oauth2_provider:list')) diff --git a/oauth2_provider/tests/test_models.py b/oauth2_provider/tests/test_models.py index d082ab8dc..d12ed30ef 100644 --- a/oauth2_provider/tests/test_models.py +++ b/oauth2_provider/tests/test_models.py @@ -81,6 +81,36 @@ def test_str(self): app.name = "test_app" self.assertEqual("%s" % app, "test_app") + def test_scopes_property(self): + self.client.login(username="test_user", password="123456") + + app = Application.objects.create( + name="test_app", + redirect_uris="http://localhost http://example.com http://example.it", + user=self.user, + client_type=Application.CLIENT_CONFIDENTIAL, + authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, + ) + + access_token = AccessToken( + user=self.user, + scope='read write', + expires=0, + token='', + application=app + ) + + access_token2 = AccessToken( + user=self.user, + scope='write', + expires=0, + token='', + application=app + ) + + self.assertEqual(access_token.scopes, {'read': 'Reading scope', 'write': 'Writing scope'}) + self.assertEqual(access_token2.scopes, {'write': 'Writing scope'}) + @skipIf(django.VERSION < (1, 5), "Behavior is broken on 1.4 and there is no solution") @override_settings(OAUTH2_PROVIDER_APPLICATION_MODEL='tests.TestApplication') diff --git a/oauth2_provider/tests/test_token_view.py b/oauth2_provider/tests/test_token_view.py new file mode 100644 index 000000000..2ee09f8ef --- /dev/null +++ b/oauth2_provider/tests/test_token_view.py @@ -0,0 +1,166 @@ +from __future__ import unicode_literals + +import datetime + +from django.core.urlresolvers import reverse +from django.test import TestCase +from django.utils import timezone + +from ..models import get_application_model, AccessToken +from ..compat import get_user_model + +Application = get_application_model() +UserModel = get_user_model() + + +class TestAuthorizedTokenViews(TestCase): + def setUp(self): + self.foo_user = UserModel.objects.create_user("foo_user", "test@user.com", "123456") + self.bar_user = UserModel.objects.create_user("bar_user", "dev@user.com", "123456") + + self.application = Application( + name="Test Application", + redirect_uris="http://localhost http://example.com http://example.it", + user=self.bar_user, + client_type=Application.CLIENT_CONFIDENTIAL, + authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE, + ) + self.application.save() + + def tearDown(self): + self.foo_user.delete() + self.bar_user.delete() + + def test_list_view_authorization_required(self): + """ + Test that the view redirects to login page if user is not logged-in. + """ + response = self.client.get(reverse('oauth2_provider:authorized-token-list')) + self.assertEqual(response.status_code, 302) + self.assertTrue('/accounts/login/?next=' in response['Location']) + + def test_empty_list_view(self): + """ + Test that when you have no tokens, an appropriate message is shown + """ + self.client.login(username="foo_user", password="123456") + + response = self.client.get(reverse('oauth2_provider:authorized-token-list')) + self.assertEqual(response.status_code, 200) + self.assertIn(b'There are no authorized tokens yet.', response.content) + + def test_list_view_one_token(self): + """ + Test that the view shows your token + """ + self.client.login(username="bar_user", password="123456") + AccessToken.objects.create(user=self.bar_user, token='1234567890', + application=self.application, + expires=timezone.now() + datetime.timedelta(days=1), + scope='read write') + + response = self.client.get(reverse('oauth2_provider:authorized-token-list')) + self.assertEqual(response.status_code, 200) + self.assertIn(b'read', response.content) + self.assertIn(b'write', response.content) + self.assertNotIn(b'There are no authorized tokens yet.', response.content) + + def test_list_view_two_tokens(self): + """ + Test that the view shows your tokens + """ + self.client.login(username="bar_user", password="123456") + AccessToken.objects.create(user=self.bar_user, token='1234567890', + application=self.application, + expires=timezone.now() + datetime.timedelta(days=1), + scope='read write') + AccessToken.objects.create(user=self.bar_user, token='0123456789', + application=self.application, + expires=timezone.now() + datetime.timedelta(days=1), + scope='read write') + + response = self.client.get(reverse('oauth2_provider:authorized-token-list')) + self.assertEqual(response.status_code, 200) + print(response.content.decode()) + self.assertNotIn(b'There are no authorized tokens yet.', response.content) + + def test_list_view_shows_correct_user_token(self): + """ + Test that only currently logged-in user's tokens are shown + """ + self.client.login(username="bar_user", password="123456") + AccessToken.objects.create(user=self.foo_user, token='1234567890', + application=self.application, + expires=timezone.now() + datetime.timedelta(days=1), + scope='read write') + + response = self.client.get(reverse('oauth2_provider:authorized-token-list')) + self.assertEqual(response.status_code, 200) + self.assertIn(b'There are no authorized tokens yet.', response.content) + + def test_delete_view_authorization_required(self): + """ + Test that the view redirects to login page if user is not logged-in. + """ + self.token = AccessToken.objects.create(user=self.foo_user, token='1234567890', + application=self.application, + expires=timezone.now() + datetime.timedelta(days=1), + scope='read write') + + response = self.client.get(reverse('oauth2_provider:authorized-token-delete', kwargs={'pk': self.token.pk})) + self.assertEqual(response.status_code, 302) + self.assertTrue('/accounts/login/?next=' in response['Location']) + + def test_delete_view_works(self): + """ + Test that a GET on this view returns 200 if the token belongs to the logged-in user. + """ + self.token = AccessToken.objects.create(user=self.foo_user, token='1234567890', + application=self.application, + expires=timezone.now() + datetime.timedelta(days=1), + scope='read write') + + self.client.login(username="foo_user", password="123456") + response = self.client.get(reverse('oauth2_provider:authorized-token-delete', kwargs={'pk': self.token.pk})) + self.assertEqual(response.status_code, 200) + + def test_delete_view_token_belongs_to_user(self): + """ + Test that a 404 is returned when trying to GET this view with someone else's tokens. + """ + self.token = AccessToken.objects.create(user=self.foo_user, token='1234567890', + application=self.application, + expires=timezone.now() + datetime.timedelta(days=1), + scope='read write') + + self.client.login(username="bar_user", password="123456") + response = self.client.get(reverse('oauth2_provider:authorized-token-delete', kwargs={'pk': self.token.pk})) + self.assertEqual(response.status_code, 404) + + def test_delete_view_post_actually_deletes(self): + """ + Test that a POST on this view works if the token belongs to the logged-in user. + """ + self.token = AccessToken.objects.create(user=self.foo_user, token='1234567890', + application=self.application, + expires=timezone.now() + datetime.timedelta(days=1), + scope='read write') + + self.client.login(username="foo_user", password="123456") + response = self.client.post(reverse('oauth2_provider:authorized-token-delete', kwargs={'pk': self.token.pk})) + self.assertFalse(AccessToken.objects.exists()) + self.assertRedirects(response, reverse('oauth2_provider:authorized-token-list')) + + def test_delete_view_only_deletes_user_own_token(self): + """ + Test that a 404 is returned when trying to POST on this view with someone else's tokens. + """ + self.token = AccessToken.objects.create(user=self.foo_user, token='1234567890', + application=self.application, + expires=timezone.now() + datetime.timedelta(days=1), + scope='read write') + + self.client.login(username="bar_user", password="123456") + response = self.client.post(reverse('oauth2_provider:authorized-token-delete', kwargs={'pk': self.token.pk})) + self.assertTrue(AccessToken.objects.exists()) + self.assertEqual(response.status_code, 404) diff --git a/oauth2_provider/urls.py b/oauth2_provider/urls.py index e098e4148..ebcb9e0b6 100644 --- a/oauth2_provider/urls.py +++ b/oauth2_provider/urls.py @@ -17,3 +17,9 @@ url(r'^applications/(?P\d+)/delete/$', views.ApplicationDelete.as_view(), name="delete"), url(r'^applications/(?P\d+)/update/$', views.ApplicationUpdate.as_view(), name="update"), ) + +urlpatterns += ( + url(r'^authorized_tokens/$', views.AuthorizedTokensListView.as_view(), name="authorized-token-list"), + url(r'^authorized_tokens/(?P\d+)/delete/$', views.AuthorizedTokenDeleteView.as_view(), + name="authorized-token-delete"), +) diff --git a/oauth2_provider/views/__init__.py b/oauth2_provider/views/__init__.py index e285d518b..257c86add 100644 --- a/oauth2_provider/views/__init__.py +++ b/oauth2_provider/views/__init__.py @@ -2,3 +2,4 @@ from .application import ApplicationRegistration, ApplicationDetail, ApplicationList, \ ApplicationDelete, ApplicationUpdate from .generic import ProtectedResourceView, ScopedProtectedResourceView, ReadWriteScopedResourceView +from .token import AuthorizedTokensListView, AuthorizedTokenDeleteView diff --git a/oauth2_provider/views/application.py b/oauth2_provider/views/application.py index 777ccf801..33fe35c34 100644 --- a/oauth2_provider/views/application.py +++ b/oauth2_provider/views/application.py @@ -1,4 +1,4 @@ -from django.core.urlresolvers import reverse_lazy +from django.core.urlresolvers import reverse from django.forms.models import modelform_factory from django.views.generic import CreateView, DetailView, DeleteView, ListView, UpdateView @@ -59,9 +59,11 @@ class ApplicationDelete(ApplicationOwnerIsUserMixin, DeleteView): View used to delete an application owned by the request.user """ context_object_name = 'application' - success_url = reverse_lazy('oauth2_provider:list') template_name = "oauth2_provider/application_confirm_delete.html" + def get_success_url(self): + return reverse('oauth2_provider:list') + class ApplicationUpdate(ApplicationOwnerIsUserMixin, UpdateView): """ diff --git a/oauth2_provider/views/token.py b/oauth2_provider/views/token.py new file mode 100644 index 000000000..a8b900413 --- /dev/null +++ b/oauth2_provider/views/token.py @@ -0,0 +1,37 @@ +from __future__ import absolute_import, unicode_literals + +from django.core.urlresolvers import reverse +from django.views.generic import ListView, DeleteView +from braces.views import LoginRequiredMixin + +from ..models import AccessToken + + +class AuthorizedTokensListView(LoginRequiredMixin, ListView): + """ + Show a page where the current logged-in user can see his tokens so they can revoke them + """ + context_object_name = 'authorized_tokens' + template_name = 'oauth2_provider/authorized-tokens.html' + model = AccessToken + + def get_queryset(self): + """ + Show only user's tokens + """ + return super(AuthorizedTokensListView, self).get_queryset()\ + .select_related('application').filter(user=self.request.user) + + +class AuthorizedTokenDeleteView(LoginRequiredMixin, DeleteView): + """ + View for revoking a specific token + """ + template_name = 'oauth2_provider/authorized-token-delete.html' + model = AccessToken + + def get_success_url(self): + return reverse('oauth2_provider:authorized-token-list') + + def get_queryset(self): + return super(AuthorizedTokenDeleteView, self).get_queryset().filter(user=self.request.user) From 88f5d014c7297f6199eaed6bcbe5cc86f85ae5af Mon Sep 17 00:00:00 2001 From: Bart Merenda Date: Fri, 30 Oct 2015 11:12:39 +0100 Subject: [PATCH 44/91] removed useless print --- oauth2_provider/tests/test_token_view.py | 1 - 1 file changed, 1 deletion(-) diff --git a/oauth2_provider/tests/test_token_view.py b/oauth2_provider/tests/test_token_view.py index 2ee09f8ef..f8393fb5b 100644 --- a/oauth2_provider/tests/test_token_view.py +++ b/oauth2_provider/tests/test_token_view.py @@ -81,7 +81,6 @@ def test_list_view_two_tokens(self): response = self.client.get(reverse('oauth2_provider:authorized-token-list')) self.assertEqual(response.status_code, 200) - print(response.content.decode()) self.assertNotIn(b'There are no authorized tokens yet.', response.content) def test_list_view_shows_correct_user_token(self): From 797cae27028998a0051b8d066f7867f3f567e386 Mon Sep 17 00:00:00 2001 From: Bart Merenda Date: Mon, 23 Nov 2015 21:22:14 +0100 Subject: [PATCH 45/91] Splitted AuthorizedToken TestCase in two Test cases --- oauth2_provider/tests/test_application_views.py | 7 ------- oauth2_provider/tests/test_token_view.py | 13 +++++++++++++ oauth2_provider/views/application.py | 6 ++---- oauth2_provider/views/token.py | 7 +++---- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/oauth2_provider/tests/test_application_views.py b/oauth2_provider/tests/test_application_views.py index 66227b51c..ec94f6ab9 100644 --- a/oauth2_provider/tests/test_application_views.py +++ b/oauth2_provider/tests/test_application_views.py @@ -99,10 +99,3 @@ def test_application_detail_not_owner(self): response = self.client.get(reverse('oauth2_provider:detail', args=(self.app_bar_1.pk,))) self.assertEqual(response.status_code, 404) - - def test_delete_view_deletes(self): - self.client.login(username="foo_user", password="123456") - response = self.client.post(reverse('oauth2_provider:delete', args=(self.app_foo_1.pk,))) - - self.assertFalse(Application.objects.filter(pk=self.app_foo_1.pk).exists()) - self.assertRedirects(response, reverse('oauth2_provider:list')) diff --git a/oauth2_provider/tests/test_token_view.py b/oauth2_provider/tests/test_token_view.py index f8393fb5b..7e02a32b2 100644 --- a/oauth2_provider/tests/test_token_view.py +++ b/oauth2_provider/tests/test_token_view.py @@ -14,6 +14,9 @@ class TestAuthorizedTokenViews(TestCase): + """ + TestCase superclass for Authorized Token Views' Test Cases + """ def setUp(self): self.foo_user = UserModel.objects.create_user("foo_user", "test@user.com", "123456") self.bar_user = UserModel.objects.create_user("bar_user", "dev@user.com", "123456") @@ -31,6 +34,11 @@ def tearDown(self): self.foo_user.delete() self.bar_user.delete() + +class TestAuthorizedTokenListView(TestAuthorizedTokenViews): + """ + Tests for the Authorized Token ListView + """ def test_list_view_authorization_required(self): """ Test that the view redirects to login page if user is not logged-in. @@ -97,6 +105,11 @@ def test_list_view_shows_correct_user_token(self): self.assertEqual(response.status_code, 200) self.assertIn(b'There are no authorized tokens yet.', response.content) + +class TestAuthorizedTokenDeleteView(TestAuthorizedTokenViews): + """ + Tests for the Authorized Token DeleteView + """ def test_delete_view_authorization_required(self): """ Test that the view redirects to login page if user is not logged-in. diff --git a/oauth2_provider/views/application.py b/oauth2_provider/views/application.py index 33fe35c34..777ccf801 100644 --- a/oauth2_provider/views/application.py +++ b/oauth2_provider/views/application.py @@ -1,4 +1,4 @@ -from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse_lazy from django.forms.models import modelform_factory from django.views.generic import CreateView, DetailView, DeleteView, ListView, UpdateView @@ -59,11 +59,9 @@ class ApplicationDelete(ApplicationOwnerIsUserMixin, DeleteView): View used to delete an application owned by the request.user """ context_object_name = 'application' + success_url = reverse_lazy('oauth2_provider:list') template_name = "oauth2_provider/application_confirm_delete.html" - def get_success_url(self): - return reverse('oauth2_provider:list') - class ApplicationUpdate(ApplicationOwnerIsUserMixin, UpdateView): """ diff --git a/oauth2_provider/views/token.py b/oauth2_provider/views/token.py index a8b900413..f7e4562e9 100644 --- a/oauth2_provider/views/token.py +++ b/oauth2_provider/views/token.py @@ -1,7 +1,8 @@ from __future__ import absolute_import, unicode_literals -from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse_lazy from django.views.generic import ListView, DeleteView + from braces.views import LoginRequiredMixin from ..models import AccessToken @@ -28,10 +29,8 @@ class AuthorizedTokenDeleteView(LoginRequiredMixin, DeleteView): View for revoking a specific token """ template_name = 'oauth2_provider/authorized-token-delete.html' + success_url = reverse_lazy('oauth2_provider:authorized-token-list') model = AccessToken - def get_success_url(self): - return reverse('oauth2_provider:authorized-token-list') - def get_queryset(self): return super(AuthorizedTokenDeleteView, self).get_queryset().filter(user=self.request.user) From d1e67990e34621bef2ef46cd80ffa3e58ff0dc54 Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Thu, 26 Nov 2015 14:31:14 +0100 Subject: [PATCH 46/91] moved to pytest lower the number of cpus used by xdist fixed typo foo --- requirements/optional.txt | 2 +- requirements/project.txt | 2 +- requirements/testing.txt | 4 ++++ runtests.py | 10 ++-------- tox.ini | 9 ++++++--- 5 files changed, 14 insertions(+), 13 deletions(-) mode change 100644 => 100755 runtests.py diff --git a/requirements/optional.txt b/requirements/optional.txt index f1cab8bdd..f37bc8ce0 100644 --- a/requirements/optional.txt +++ b/requirements/optional.txt @@ -1,2 +1,2 @@ -r base.txt -djangorestframework>=2.3 \ No newline at end of file +djangorestframework>=3.3 diff --git a/requirements/project.txt b/requirements/project.txt index b42ccf949..f89e28c15 100644 --- a/requirements/project.txt +++ b/requirements/project.txt @@ -1,2 +1,2 @@ -r optional.txt -Django>=1.4 \ No newline at end of file +Django>=1.7 diff --git a/requirements/testing.txt b/requirements/testing.txt index fff8bc1f1..d444c6358 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -1,2 +1,6 @@ -r optional.txt mock==1.0.1 +pytest==2.8.3 +pytest-django==2.9.1 +pytest-xdist==1.13.1 +pytest-cov==2.2.0 diff --git a/runtests.py b/runtests.py old mode 100644 new mode 100755 index 8f3bf4d41..b098d442c --- a/runtests.py +++ b/runtests.py @@ -1,10 +1,4 @@ #!/usr/bin/env python -import os -import sys +import pytest -if __name__ == "__main__": - app_to_test = "oauth2_provider" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "oauth2_provider.tests.settings") - - from django.core.management import execute_from_command_line - execute_from_command_line([sys.argv[0], "test", app_to_test]) +pytest.main() diff --git a/tox.ini b/tox.ini index 305121091..d840a5ae1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,3 +1,7 @@ +[pytest] +DJANGO_SETTINGS_MODULE=oauth2_provider.tests.settings +testpaths=oauth2_provider + [tox] envlist = {py27}-django{17,18,19}, @@ -9,13 +13,12 @@ envlist = flake8 [testenv] -commands=coverage run -a runtests.py +commands=python runtests.py -q --cov oauth2_provider --cov-report= --cov-append deps = django17: Django==1.7.10 django18: Django==1.8.6 django19: https://www.djangoproject.com/download/1.9rc1/tarball/ - py32: coverage<4 - py{27,33,34,35}: coverage + coverage<4 -rrequirements/testing.txt [testenv:docs] From 54fa0aab3e547100c349fc87f597fc140ff876fe Mon Sep 17 00:00:00 2001 From: Bart Merenda Date: Fri, 11 Dec 2015 17:46:05 +0100 Subject: [PATCH 47/91] added doc for token views --- docs/views/token.rst | 15 +++++++++++++++ docs/views/views.rst | 1 + 2 files changed, 16 insertions(+) create mode 100644 docs/views/token.rst diff --git a/docs/views/token.rst b/docs/views/token.rst new file mode 100644 index 000000000..02f6bf53e --- /dev/null +++ b/docs/views/token.rst @@ -0,0 +1,15 @@ +Granted Tokens Views +==================== + +A set of views is provided to let users handle tokens that have been granted to them, without needing to accessing Django Admin Site. +Every view provides access only to the tokens that have been granted to the user performing the request. + + +Granted Token views are listed at the url `authorized_tokens/`. + + +For each granted token there is a delete view that allows you to delete such token. You can override default templates `authorized-tokens.html` for the list view and `authorized-token-delete.html` for the delete view; they are located inside `templates/oauth2_provider` folder. + + +.. automodule:: oauth2_provider.views.token + :members: diff --git a/docs/views/views.rst b/docs/views/views.rst index 34afef9df..262f9d20a 100644 --- a/docs/views/views.rst +++ b/docs/views/views.rst @@ -9,4 +9,5 @@ Django OAuth Toolkit provides a set of pre-defined views for different purposes: function_based class_based application + token mixins From bee961f67bca3f0d7db6559cfff62db89d69dd5a Mon Sep 17 00:00:00 2001 From: teuneboon Date: Mon, 19 Oct 2015 10:28:22 +0200 Subject: [PATCH 48/91] If AccessToken/Grant had, for w/e reason, no "expires", is_expired check would fail with a TypeError(can't compare datetime.datetime to NoneType) --- oauth2_provider/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py index 90a5ade2a..f5d635639 100644 --- a/oauth2_provider/models.py +++ b/oauth2_provider/models.py @@ -157,6 +157,9 @@ def is_expired(self): """ Check token expiration with timezone awareness """ + if not self.expires: + return True + return timezone.now() >= self.expires def redirect_uri_allowed(self, uri): @@ -198,6 +201,9 @@ def is_expired(self): """ Check token expiration with timezone awareness """ + if not self.expires: + return True + return timezone.now() >= self.expires def allow_scopes(self, scopes): From f9bc2aa579716dc3ad02466e077cb6a8a37c189d Mon Sep 17 00:00:00 2001 From: teuneboon Date: Mon, 19 Oct 2015 10:28:22 +0200 Subject: [PATCH 49/91] Added unit tests for expires can be None change --- oauth2_provider/tests/test_models.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/oauth2_provider/tests/test_models.py b/oauth2_provider/tests/test_models.py index d12ed30ef..a9ce4375b 100644 --- a/oauth2_provider/tests/test_models.py +++ b/oauth2_provider/tests/test_models.py @@ -139,6 +139,11 @@ class TestGrantModel(TestCase): def test_str(self): grant = Grant(code="test_code") self.assertEqual("%s" % grant, grant.code) + + def test_expires_can_be_none(self): + grant = Grant(code="test_code") + self.assertIsNone(grant.expires) + self.assertFalse(grant.is_expired()) class TestAccessTokenModel(TestCase): @@ -159,6 +164,11 @@ def test_user_can_be_none(self): ) access_token = AccessToken.objects.create(token="test_token", application=app, expires=timezone.now()) self.assertIsNone(access_token.user) + + def test_expires_can_be_none(self): + access_token = AccessToken(token="test_token") + self.assertIsNone(access_token.expires) + self.assertFalse(access_token.is_expired()) class TestRefreshTokenModel(TestCase): From e4c40865787b6a757c6b899b6dbee3567763bdc1 Mon Sep 17 00:00:00 2001 From: teuneboon Date: Mon, 19 Oct 2015 10:28:22 +0200 Subject: [PATCH 50/91] Fixed reversed True/False logic in unit test for expires_can_be_none --- oauth2_provider/tests/test_models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/oauth2_provider/tests/test_models.py b/oauth2_provider/tests/test_models.py index a9ce4375b..47871878c 100644 --- a/oauth2_provider/tests/test_models.py +++ b/oauth2_provider/tests/test_models.py @@ -143,7 +143,7 @@ def test_str(self): def test_expires_can_be_none(self): grant = Grant(code="test_code") self.assertIsNone(grant.expires) - self.assertFalse(grant.is_expired()) + self.assertTrue(grant.is_expired()) class TestAccessTokenModel(TestCase): @@ -168,7 +168,7 @@ def test_user_can_be_none(self): def test_expires_can_be_none(self): access_token = AccessToken(token="test_token") self.assertIsNone(access_token.expires) - self.assertFalse(access_token.is_expired()) + self.assertTrue(access_token.is_expired()) class TestRefreshTokenModel(TestCase): From 6e4b0cb99ed89d81827f956b16961a61eed34567 Mon Sep 17 00:00:00 2001 From: Federico Dolce Date: Mon, 14 Dec 2015 15:34:05 +0100 Subject: [PATCH 51/91] Removed trailing whitespaces --- oauth2_provider/models.py | 4 ++-- oauth2_provider/tests/test_models.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py index f5d635639..fd3cdf40d 100644 --- a/oauth2_provider/models.py +++ b/oauth2_provider/models.py @@ -159,7 +159,7 @@ def is_expired(self): """ if not self.expires: return True - + return timezone.now() >= self.expires def redirect_uri_allowed(self, uri): @@ -203,7 +203,7 @@ def is_expired(self): """ if not self.expires: return True - + return timezone.now() >= self.expires def allow_scopes(self, scopes): diff --git a/oauth2_provider/tests/test_models.py b/oauth2_provider/tests/test_models.py index 47871878c..563000207 100644 --- a/oauth2_provider/tests/test_models.py +++ b/oauth2_provider/tests/test_models.py @@ -139,7 +139,7 @@ class TestGrantModel(TestCase): def test_str(self): grant = Grant(code="test_code") self.assertEqual("%s" % grant, grant.code) - + def test_expires_can_be_none(self): grant = Grant(code="test_code") self.assertIsNone(grant.expires) @@ -164,7 +164,7 @@ def test_user_can_be_none(self): ) access_token = AccessToken.objects.create(token="test_token", application=app, expires=timezone.now()) self.assertIsNone(access_token.user) - + def test_expires_can_be_none(self): access_token = AccessToken(token="test_token") self.assertIsNone(access_token.expires) From d0393b30b640aa096ac2a8eb6a29172bc6cb05d8 Mon Sep 17 00:00:00 2001 From: Federico Dolce Date: Thu, 29 Oct 2015 18:08:13 +0100 Subject: [PATCH 52/91] Added possibility to specify a default list of scopes --- oauth2_provider/forms.py | 2 +- oauth2_provider/oauth2_validators.py | 2 +- oauth2_provider/settings.py | 5 ++- .../tests/test_authorization_code.py | 32 +++++++++++++++++++ 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/oauth2_provider/forms.py b/oauth2_provider/forms.py index 8cd8e4fef..a2b4d8f1c 100644 --- a/oauth2_provider/forms.py +++ b/oauth2_provider/forms.py @@ -4,7 +4,7 @@ class AllowForm(forms.Form): allow = forms.BooleanField(required=False) redirect_uri = forms.CharField(widget=forms.HiddenInput()) - scope = forms.CharField(required=False, widget=forms.HiddenInput()) + scope = forms.CharField(widget=forms.HiddenInput()) client_id = forms.CharField(widget=forms.HiddenInput()) state = forms.CharField(required=False, widget=forms.HiddenInput()) response_type = forms.CharField(widget=forms.HiddenInput()) diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py index 3b42dad37..2cc526f1d 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -278,7 +278,7 @@ def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): return set(scopes).issubset(set(oauth2_settings._SCOPES)) def get_default_scopes(self, client_id, request, *args, **kwargs): - return oauth2_settings._SCOPES + return oauth2_settings._DEFAULT_SCOPES or oauth2_settings._SCOPES def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs): return request.client.redirect_uri_allowed(redirect_uri) diff --git a/oauth2_provider/settings.py b/oauth2_provider/settings.py index 977a80dd4..d1facfb76 100644 --- a/oauth2_provider/settings.py +++ b/oauth2_provider/settings.py @@ -26,7 +26,6 @@ except ImportError: from django.utils import importlib - USER_SETTINGS = getattr(settings, 'OAUTH2_PROVIDER', None) DEFAULTS = { @@ -37,6 +36,7 @@ 'OAUTH2_VALIDATOR_CLASS': 'oauth2_provider.oauth2_validators.OAuth2Validator', 'OAUTH2_BACKEND_CLASS': 'oauth2_provider.oauth2_backends.OAuthLibCore', 'SCOPES': {"read": "Reading scope", "write": "Writing scope"}, + 'DEFAULT_SCOPES': {}, 'READ_SCOPE': 'read', 'WRITE_SCOPE': 'write', 'AUTHORIZATION_CODE_EXPIRE_SECONDS': 60, @@ -48,6 +48,7 @@ # Special settings that will be evaluated at runtime '_SCOPES': [], + '_DEFAULT_SCOPES': [], } # List of settings that cannot be empty @@ -129,6 +130,8 @@ def __getattr__(self, attr): # Overriding special settings if attr == '_SCOPES': val = list(six.iterkeys(self.SCOPES)) + if attr == '_DEFAULT_SCOPES': + val = list(six.iterkeys(self.DEFAULT_SCOPES)) self.validate_setting(attr, val) diff --git a/oauth2_provider/tests/test_authorization_code.py b/oauth2_provider/tests/test_authorization_code.py index 5c51c35cf..ff095e5fb 100644 --- a/oauth2_provider/tests/test_authorization_code.py +++ b/oauth2_provider/tests/test_authorization_code.py @@ -7,6 +7,7 @@ from django.test import TestCase, RequestFactory from django.core.urlresolvers import reverse +from django.test.utils import override_settings from django.utils import timezone from ..compat import urlparse, parse_qs, urlencode, get_user_model @@ -1003,3 +1004,34 @@ def test_resource_access_deny(self): view = ResourceView.as_view() response = view(request) self.assertEqual(response.status_code, 403) + + +class TestDefaultScopes(BaseTest): + + def test_pre_auth_deafult_scopes(self): + """ + Test response for a valid client_id with response_type: code using default scopes + """ + self.client.login(username="test_user", password="123456") + oauth2_settings._DEFAULT_SCOPES = ['read'] + + query_string = urlencode({ + 'client_id': self.application.client_id, + 'response_type': 'code', + 'state': 'random_state_string', + 'redirect_uri': 'http://example.it', + }) + url = "{url}?{qs}".format(url=reverse('oauth2_provider:authorize'), qs=query_string) + + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + # check form is in context and form params are valid + self.assertIn("form", response.context) + + form = response.context["form"] + self.assertEqual(form['redirect_uri'].value(), "http://example.it") + 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 = [] From 9924b26a9f30e111cb5ae8183daf674ed6d869ea Mon Sep 17 00:00:00 2001 From: Federico Dolce Date: Thu, 19 Nov 2015 16:29:04 +0100 Subject: [PATCH 53/91] Added doc for DEFAULT_SCOPES setting --- docs/settings.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/settings.rst b/docs/settings.rst index cfbbe757e..fdbe71bfb 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -76,7 +76,16 @@ to get a ``Server`` instance. SCOPES ~~~~~~ -A dictionnary mapping each scope name to its human description. +A dictionary mapping each scope name to its human description. + +DEFAULT_SCOPES +~~~~~~~~~~~~~~ +A list of scopes that should be returned by default. +This is a subset of the keys of the SCOPES setting. +By default this is set to '__all__' meaning that the whole set of SCOPES will be returned. +.. code-block:: python + + DEFAULT_SCOPES = ['read', 'write'] READ_SCOPE ~~~~~~~~~~ From 8b66536aa4b3e98ae89904be8add5dfbf2c825f9 Mon Sep 17 00:00:00 2001 From: Federico Dolce Date: Fri, 20 Nov 2015 11:00:31 +0100 Subject: [PATCH 54/91] Fixed DEFAULT_SCOPES to be a list --- oauth2_provider/oauth2_validators.py | 2 +- oauth2_provider/settings.py | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py index 2cc526f1d..25908d979 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -278,7 +278,7 @@ def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): return set(scopes).issubset(set(oauth2_settings._SCOPES)) def get_default_scopes(self, client_id, request, *args, **kwargs): - return oauth2_settings._DEFAULT_SCOPES or oauth2_settings._SCOPES + return oauth2_settings._DEFAULT_SCOPES def validate_redirect_uri(self, client_id, redirect_uri, request, *args, **kwargs): return request.client.redirect_uri_allowed(redirect_uri) diff --git a/oauth2_provider/settings.py b/oauth2_provider/settings.py index d1facfb76..7fb78e211 100644 --- a/oauth2_provider/settings.py +++ b/oauth2_provider/settings.py @@ -20,6 +20,7 @@ import six from django.conf import settings +from django.core.exceptions import ImproperlyConfigured try: # Available in Python 2.7+ import importlib @@ -36,7 +37,7 @@ 'OAUTH2_VALIDATOR_CLASS': 'oauth2_provider.oauth2_validators.OAuth2Validator', 'OAUTH2_BACKEND_CLASS': 'oauth2_provider.oauth2_backends.OAuthLibCore', 'SCOPES': {"read": "Reading scope", "write": "Writing scope"}, - 'DEFAULT_SCOPES': {}, + 'DEFAULT_SCOPES': ['__all__'], 'READ_SCOPE': 'read', 'WRITE_SCOPE': 'write', 'AUTHORIZATION_CODE_EXPIRE_SECONDS': 60, @@ -131,7 +132,17 @@ def __getattr__(self, attr): if attr == '_SCOPES': val = list(six.iterkeys(self.SCOPES)) if attr == '_DEFAULT_SCOPES': - val = list(six.iterkeys(self.DEFAULT_SCOPES)) + if '__all__' in self.DEFAULT_SCOPES: + # If DEFAULT_SCOPES is set to ['__all__'] the whole set of scopes is returned + val = list(self._SCOPES) + else: + # Otherwise we return a subset (that can be void) of SCOPES + val = [] + for scope in self.DEFAULT_SCOPES: + if scope in self._SCOPES: + val.append(scope) + else: + raise ImproperlyConfigured("Defined DEFAULT_SCOPES not present in SCOPES") self.validate_setting(attr, val) From b65777267a4a61b5753f41e13e9600ae16406d01 Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Wed, 25 Nov 2015 15:43:26 +0100 Subject: [PATCH 55/91] fixed _DEFAULT_SCOPES for testing --- oauth2_provider/tests/test_authorization_code.py | 4 ++-- oauth2_provider/tests/test_client_credential.py | 1 + oauth2_provider/tests/test_password.py | 1 + oauth2_provider/tests/test_scopes.py | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/oauth2_provider/tests/test_authorization_code.py b/oauth2_provider/tests/test_authorization_code.py index ff095e5fb..cf87996a7 100644 --- a/oauth2_provider/tests/test_authorization_code.py +++ b/oauth2_provider/tests/test_authorization_code.py @@ -46,6 +46,7 @@ def setUp(self): self.application.save() oauth2_settings._SCOPES = ['read', 'write'] + oauth2_settings._DEFAULT_SCOPES = ['read', 'write'] def tearDown(self): self.application.delete() @@ -1008,7 +1009,7 @@ def test_resource_access_deny(self): class TestDefaultScopes(BaseTest): - def test_pre_auth_deafult_scopes(self): + def test_pre_auth_default_scopes(self): """ Test response for a valid client_id with response_type: code using default scopes """ @@ -1034,4 +1035,3 @@ def test_pre_auth_deafult_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 = [] diff --git a/oauth2_provider/tests/test_client_credential.py b/oauth2_provider/tests/test_client_credential.py index 8136f5d71..a0462ca8d 100644 --- a/oauth2_provider/tests/test_client_credential.py +++ b/oauth2_provider/tests/test_client_credential.py @@ -48,6 +48,7 @@ def setUp(self): self.application.save() oauth2_settings._SCOPES = ['read', 'write'] + oauth2_settings._DEFAULT_SCOPES = ['read', 'write'] def tearDown(self): self.application.delete() diff --git a/oauth2_provider/tests/test_password.py b/oauth2_provider/tests/test_password.py index e1d250f6f..a4fbdf424 100644 --- a/oauth2_provider/tests/test_password.py +++ b/oauth2_provider/tests/test_password.py @@ -37,6 +37,7 @@ def setUp(self): self.application.save() oauth2_settings._SCOPES = ['read', 'write'] + oauth2_settings._DEFAULT_SCOPES = ['read', 'write'] def tearDown(self): self.application.delete() diff --git a/oauth2_provider/tests/test_scopes.py b/oauth2_provider/tests/test_scopes.py index 76e8ea51a..36ef1032d 100644 --- a/oauth2_provider/tests/test_scopes.py +++ b/oauth2_provider/tests/test_scopes.py @@ -68,6 +68,7 @@ class TestScopesQueryParameterBackwardsCompatibility(BaseTest): def setUp(self): super(TestScopesQueryParameterBackwardsCompatibility, self).setUp() oauth2_settings._SCOPES = ['read', 'write'] + oauth2_settings._DEFAULT_SCOPES = ['read', 'write'] def test_scopes_query_parameter_is_supported_on_post(self): """ From bb4f0923f6e490716ca128ad5660f89810b6bd3f Mon Sep 17 00:00:00 2001 From: Federico Dolce Date: Fri, 20 Nov 2015 17:26:37 +0100 Subject: [PATCH 56/91] Fixed tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d840a5ae1..7ca4b4c0b 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ testpaths=oauth2_provider [tox] envlist = {py27}-django{17,18,19}, - {py32}-django{17,18} + {py32}-django{17,18}, {py33}-django{17,18}, {py34}-django{17,18,19}, {py35}-django{18,19}, From 2ee2b6a539aa75ee2e147f48a39ca31c72aae567 Mon Sep 17 00:00:00 2001 From: Federico Dolce Date: Mon, 14 Dec 2015 11:24:42 +0100 Subject: [PATCH 57/91] Added test for default scopes in implicit grant --- oauth2_provider/tests/test_implicit.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/oauth2_provider/tests/test_implicit.py b/oauth2_provider/tests/test_implicit.py index 9293616d7..df038fd58 100644 --- a/oauth2_provider/tests/test_implicit.py +++ b/oauth2_provider/tests/test_implicit.py @@ -37,6 +37,7 @@ def setUp(self): self.application.save() oauth2_settings._SCOPES = ['read', 'write'] + oauth2_settings._DEFAULT_SCOPES = ['read'] def tearDown(self): self.application.delete() @@ -45,6 +46,26 @@ def tearDown(self): class TestImplicitAuthorizationCodeView(BaseTest): + def test_pre_auth_valid_client_default_scopes(self): + """ + Test response for a valid client_id with response_type: token and default_scopes + """ + self.client.login(username="test_user", password="123456") + query_string = urlencode({ + 'client_id': self.application.client_id, + 'response_type': 'token', + 'state': 'random_state_string', + 'redirect_uri': 'http://example.it', + }) + + url = "{url}?{qs}".format(url=reverse('oauth2_provider:authorize'), qs=query_string) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + self.assertIn("form", response.context) + form = response.context["form"] + self.assertEqual(form['scope'].value(), 'read') + def test_pre_auth_valid_client(self): """ Test response for a valid client_id with response_type: token From 4d515035ea0b4454f39b1a478fa0e0155adc6eac Mon Sep 17 00:00:00 2001 From: Bart Merenda Date: Mon, 14 Dec 2015 12:00:55 +0100 Subject: [PATCH 58/91] Added doc for cleartokens command --- docs/index.rst | 1 + docs/management_commands.rst | 20 ++++++++++++++++++++ docs/settings.rst | 10 +--------- 3 files changed, 22 insertions(+), 9 deletions(-) create mode 100644 docs/management_commands.rst diff --git a/docs/index.rst b/docs/index.rst index e4e1d3332..de2c0f8b1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,6 +38,7 @@ Index models advanced_topics settings + management_commands glossary .. toctree:: diff --git a/docs/management_commands.rst b/docs/management_commands.rst new file mode 100644 index 000000000..3930062b6 --- /dev/null +++ b/docs/management_commands.rst @@ -0,0 +1,20 @@ +Management commands +=================== + +Django OAuth Toolkit exposes some useful management commands that can be run via shell or by other means (eg: cron) + +.. _cleartokens: + +cleartokens +~~~~~~~~~~~ + +The ``cleartokens`` management command allows the user to remove those refresh tokens whose lifetime is greater than the +amount specified by ``REFRESH_TOKEN_EXPIRE_SECONDS`` settings. It is important that this command is run regularly +(eg: via cron) to avoid cluttering the database with expired refresh tokens. + +If ``cleartokens`` runs daily the maximum delay before a refresh token is +removed is ``REFRESH_TOKEN_EXPIRE_SECONDS`` + 1 day. This is normally not a +problem since refresh tokens are long lived. + +Note: Refresh tokens need to expire before AccessTokens can be removed from the +database. Using ``cleartokens`` without ``REFRESH_TOKEN_EXPIRE_SECONDS`` has limited effect. diff --git a/docs/settings.rst b/docs/settings.rst index fdbe71bfb..4999c1c77 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -98,15 +98,7 @@ The name of the *write* scope. REFRESH_TOKEN_EXPIRE_SECONDS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The number of seconds before a refresh token gets removed from the database by -the ``cleartokens``` management command. It's important that ``cleartokens`` -runs regularly (eg: via cron) in order for this setting to work. - -If ``cleartokens`` runs daily the maximum delay before a refresh token is -removed is ``REFRESH_TOKEN_EXPIRE_SECONDS`` + 1 day. This is normally not a -problem since refresh tokens are long lived. - -Note: Refresh tokens need to expire before AccessTokens can be removed from the -database. Using ``cleartokens`` without ``REFRESH_TOKEN_EXPIRE_SECONDS`` has limited effect. +the ``cleartokens`` management command. Check :ref:`cleartokens` management command for further info. REQUEST_APPROVAL_PROMPT ~~~~~~~~~~~~~~~~~~~~~~~ From 970ccee42fadb1c7c158ec37de7cda8d0e02a619 Mon Sep 17 00:00:00 2001 From: Bart Merenda Date: Mon, 14 Dec 2015 12:55:49 +0100 Subject: [PATCH 59/91] Added AppConfig --- AUTHORS | 1 + oauth2_provider/__init__.py | 2 ++ oauth2_provider/apps.py | 6 ++++++ 3 files changed, 9 insertions(+) create mode 100644 oauth2_provider/apps.py diff --git a/AUTHORS b/AUTHORS index 88c94b7f0..7b2c6b83f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -15,3 +15,4 @@ Rodney Richardson Hiroki Kiyohara Diego Garcia Bas van Oostveen +Bart Merenda diff --git a/oauth2_provider/__init__.py b/oauth2_provider/__init__.py index 9ffed34b1..ea2f026c9 100644 --- a/oauth2_provider/__init__.py +++ b/oauth2_provider/__init__.py @@ -2,4 +2,6 @@ __author__ = "Massimiliano Pippi & Federico Frenguelli" +default_app_config = 'oauth2_provider.apps.DOTConfig' + VERSION = __version__ # synonym diff --git a/oauth2_provider/apps.py b/oauth2_provider/apps.py new file mode 100644 index 000000000..6f67f3871 --- /dev/null +++ b/oauth2_provider/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DOTConfig(AppConfig): + name = 'oauth2_provider' + verbose_name = "Django OAuth Toolkit" From a8187642472cf4411e5157352abbf84a761b66e4 Mon Sep 17 00:00:00 2001 From: Barthelemy Dagenais Date: Tue, 8 Dec 2015 13:46:28 -0500 Subject: [PATCH 60/91] added missing swappable migration option to prevent the creation of the swappable model if alternative model is provided. --- oauth2_provider/migrations/0001_initial.py | 1 + 1 file changed, 1 insertion(+) diff --git a/oauth2_provider/migrations/0001_initial.py b/oauth2_provider/migrations/0001_initial.py index bb1b51842..a1c59c709 100644 --- a/oauth2_provider/migrations/0001_initial.py +++ b/oauth2_provider/migrations/0001_initial.py @@ -30,6 +30,7 @@ class Migration(migrations.Migration): ], options={ 'abstract': False, + 'swappable': 'OAUTH2_PROVIDER_APPLICATION_MODEL', }, ), migrations.CreateModel( From c904ba85bf3be864bf1809f44965f3344ecdb9f1 Mon Sep 17 00:00:00 2001 From: Federico Dolce Date: Mon, 14 Dec 2015 17:13:52 +0100 Subject: [PATCH 61/91] Fixed django versions in tox.ini --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 7ca4b4c0b..7a4bc12fe 100644 --- a/tox.ini +++ b/tox.ini @@ -15,9 +15,9 @@ envlist = [testenv] commands=python runtests.py -q --cov oauth2_provider --cov-report= --cov-append deps = - django17: Django==1.7.10 - django18: Django==1.8.6 - django19: https://www.djangoproject.com/download/1.9rc1/tarball/ + django17: Django==1.7.11 + django18: Django==1.8.7 + django19: Django==1.9 coverage<4 -rrequirements/testing.txt From 53c47a90bef86cc3139e27f6fbd7f96fe7845963 Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Mon, 14 Dec 2015 17:05:27 +0100 Subject: [PATCH 62/91] ignored .cache folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 227bc5037..bf1a049e6 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ __pycache__ pip-log.txt # Unit test / coverage reports +.cache .coverage .tox nosetests.xml From 2bf3fb9643a82dda66fed3ef71869a35e3da8ff2 Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Mon, 14 Dec 2015 17:15:46 +0100 Subject: [PATCH 63/91] updated changelog --- README.rst | 7 +++++-- docs/changelog.rst | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index ec111be52..5eed1ba76 100644 --- a/README.rst +++ b/README.rst @@ -87,10 +87,13 @@ Roadmap / Todo list (help wanted) Changelog --------- -master -~~~~~~~~~~~~~~~~~~ +0.10.0 [2015-12-14] +~~~~~~~~~~~~~~~~~~~ * **#322: dropping support for python 2.6 and django 1.4, 1.5, 1.6** +* #310: Fixed error that could occur sometimes when checking validity of incomplete AccessToken/Grant +* #333: Added possibility to specify the default list of scopes returned when scope parameter is missing +* #325: Added management views of issued tokens * #249: Added a command to clean expired tokens * #323: Application registration view uses custom application model in form class * #299: 'server_class' is now pluggable through Django settings diff --git a/docs/changelog.rst b/docs/changelog.rst index b9745dbf5..a9a4e5a24 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,10 +1,13 @@ Changelog ========= -master +0.10.0 [2015-12-14] ------------------ * **#322: dropping support for python 2.6 and django 1.4, 1.5, 1.6** +* #310: Fixed error that could occur sometimes when checking validity of incomplete AccessToken/Grant +* #333: Added possibility to specify the default list of scopes returned when scope parameter is missing +* #325: Added management views of issued tokens * #249: Added a command to clean expired tokens * #323: Application registration view uses custom application model in form class * #299: 'server_class' is now pluggable through Django settings From 71675f80c27c311d3b42ceca3980d89edb8ace4c Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Mon, 14 Dec 2015 17:20:42 +0100 Subject: [PATCH 64/91] updated setup.py requirements and metadata --- setup.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index 77bd24f9d..2114ab47d 100644 --- a/setup.py +++ b/setup.py @@ -30,16 +30,16 @@ def get_version(package): "Framework :: Django", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", - "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", "Topic :: Software Development :: Libraries :: Python Modules", "Framework :: Django", - "Framework :: Django :: 1.4", - "Framework :: Django :: 1.5", - "Framework :: Django :: 1.6", "Framework :: Django :: 1.7", + "Framework :: Django :: 1.8", + "Framework :: Django :: 1.9", ], keywords='django oauth oauth2 oauthlib', author="Federico Frenguelli, Massimiliano Pippi", @@ -50,9 +50,9 @@ def get_version(package): include_package_data=True, test_suite='runtests', install_requires=[ - 'django>=1.4', - 'django-braces>=1.2.2', - 'oauthlib==1.0.1', + 'django>=1.7', + 'django-braces>=1.8.1', + 'oauthlib==1.0.3', 'six', ], zip_safe=False, From 6d64220a50200e1e6162c79d575da2e046e639c3 Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Mon, 14 Dec 2015 17:20:54 +0100 Subject: [PATCH 65/91] updated development requirements --- requirements/base.txt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 2a471eb19..79d7461e5 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,5 +1,4 @@ -Sphinx==1.3.1 -South==1.0 -oauthlib==1.0.1 +Sphinx==1.3.3 +oauthlib==1.0.3 django-braces==1.8.1 six From 21c6f25e1c4c9756ecd956c54e485c4cf1718326 Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Mon, 14 Dec 2015 17:22:04 +0100 Subject: [PATCH 66/91] bumped version to 0.10.0 --- oauth2_provider/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2_provider/__init__.py b/oauth2_provider/__init__.py index ea2f026c9..a13af8376 100644 --- a/oauth2_provider/__init__.py +++ b/oauth2_provider/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.9.0' +__version__ = '0.10.0' __author__ = "Massimiliano Pippi & Federico Frenguelli" From df5d5a1850159ef2fdbf825f30ea97effcd58dcf Mon Sep 17 00:00:00 2001 From: Emanuele Palazzetti Date: Tue, 15 Dec 2015 16:30:38 +0100 Subject: [PATCH 67/91] wrapping pytest.main() call in the runtests.py; fixing the script exit code --- runtests.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/runtests.py b/runtests.py index b098d442c..852de9c22 100755 --- a/runtests.py +++ b/runtests.py @@ -1,4 +1,8 @@ #!/usr/bin/env python +import sys import pytest -pytest.main() + +# sys.exit() is required otherwise the wrapper exits +# with exit code 0, regardless the pytest.main() execution +sys.exit(pytest.main()) From 6c88b25d25ffdd99e9451e56fe077fc781fe4429 Mon Sep 17 00:00:00 2001 From: Doug Keen Date: Tue, 5 Jan 2016 12:16:40 -0800 Subject: [PATCH 68/91] Fix #340 by defaulting encoding var when request.encoding is None (which is a valid value, as documented: https://docs.djangoproject.com/en/1.9/ref/request-response/#django.http.HttpRequest.encoding) --- oauth2_provider/oauth2_validators.py | 3 ++- oauth2_provider/tests/test_oauth2_validators.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/oauth2_provider/oauth2_validators.py b/oauth2_provider/oauth2_validators.py index 25908d979..7cc6f3e18 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -7,6 +7,7 @@ from datetime import timedelta from django.utils import timezone +from django.conf import settings from django.contrib.auth import authenticate from django.core.exceptions import ObjectDoesNotExist from oauthlib.oauth2 import RequestValidator @@ -57,7 +58,7 @@ def _authenticate_basic_auth(self, request): return False try: - encoding = request.encoding + encoding = request.encoding or settings.DEFAULT_CHARSET or 'utf-8' except AttributeError: encoding = 'utf-8' diff --git a/oauth2_provider/tests/test_oauth2_validators.py b/oauth2_provider/tests/test_oauth2_validators.py index b18e4decf..851d2e70e 100644 --- a/oauth2_provider/tests/test_oauth2_validators.py +++ b/oauth2_provider/tests/test_oauth2_validators.py @@ -53,6 +53,12 @@ def test_authenticate_basic_auth(self): self.request.headers = {'HTTP_AUTHORIZATION': 'Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=\n'} self.assertTrue(self.validator._authenticate_basic_auth(self.request)) + def test_authenticate_basic_auth_default_encoding(self): + self.request.encoding = None + # client_id:client_secret + self.request.headers = {'HTTP_AUTHORIZATION': 'Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=\n'} + self.assertTrue(self.validator._authenticate_basic_auth(self.request)) + def test_authenticate_basic_auth_wrong_client_id(self): self.request.encoding = 'utf-8' # wrong_id:client_secret From 13ed73b8d1a7aa217153d80c4aa8b77d8841aa99 Mon Sep 17 00:00:00 2001 From: Bart Merenda Date: Tue, 15 Dec 2015 17:45:33 +0100 Subject: [PATCH 69/91] Removed unused imports --- oauth2_provider/compat.py | 2 +- oauth2_provider/compat_handlers.py | 1 + oauth2_provider/ext/rest_framework/__init__.py | 1 + oauth2_provider/management/commands/cleartokens.py | 2 +- oauth2_provider/tests/settings.py | 2 -- oauth2_provider/tests/test_application_views.py | 2 -- oauth2_provider/tests/test_authorization_code.py | 1 - oauth2_provider/tests/test_decorators.py | 1 - oauth2_provider/tests/test_implicit.py | 3 +-- oauth2_provider/tests/test_oauth2_backends.py | 1 - oauth2_provider/views/__init__.py | 1 + tox.ini | 1 - 12 files changed, 6 insertions(+), 12 deletions(-) diff --git a/oauth2_provider/compat.py b/oauth2_provider/compat.py index 3fca93610..d4c3fc231 100644 --- a/oauth2_provider/compat.py +++ b/oauth2_provider/compat.py @@ -2,7 +2,7 @@ The `compat` module provides support for backwards compatibility with older versions of django and python.. """ - +# flake8: noqa from __future__ import unicode_literals import django diff --git a/oauth2_provider/compat_handlers.py b/oauth2_provider/compat_handlers.py index 21859e80e..ce95a02eb 100644 --- a/oauth2_provider/compat_handlers.py +++ b/oauth2_provider/compat_handlers.py @@ -1,3 +1,4 @@ +# flake8: noqa # Django 1.9 drops the NullHandler since Python 2.7 includes it try: from logging import NullHandler diff --git a/oauth2_provider/ext/rest_framework/__init__.py b/oauth2_provider/ext/rest_framework/__init__.py index 00da0a1ce..bdc638818 100644 --- a/oauth2_provider/ext/rest_framework/__init__.py +++ b/oauth2_provider/ext/rest_framework/__init__.py @@ -1,2 +1,3 @@ +# flake8: noqa from .authentication import OAuth2Authentication from .permissions import TokenHasScope, TokenHasReadWriteScope, TokenHasResourceScope diff --git a/oauth2_provider/management/commands/cleartokens.py b/oauth2_provider/management/commands/cleartokens.py index 5b56d2bc1..48f70b822 100644 --- a/oauth2_provider/management/commands/cleartokens.py +++ b/oauth2_provider/management/commands/cleartokens.py @@ -1,4 +1,4 @@ -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand from ...models import clear_expired diff --git a/oauth2_provider/tests/settings.py b/oauth2_provider/tests/settings.py index e144e8006..b7521bd4c 100644 --- a/oauth2_provider/tests/settings.py +++ b/oauth2_provider/tests/settings.py @@ -1,5 +1,3 @@ -import os - DEBUG = True TEMPLATE_DEBUG = DEBUG diff --git a/oauth2_provider/tests/test_application_views.py b/oauth2_provider/tests/test_application_views.py index ec94f6ab9..f5920c1ea 100644 --- a/oauth2_provider/tests/test_application_views.py +++ b/oauth2_provider/tests/test_application_views.py @@ -1,9 +1,7 @@ from __future__ import unicode_literals -import mock from django.core.urlresolvers import reverse from django.test import TestCase -from django.test.utils import override_settings from ..models import get_application_model from ..compat import get_user_model diff --git a/oauth2_provider/tests/test_authorization_code.py b/oauth2_provider/tests/test_authorization_code.py index cf87996a7..8ce8b8024 100644 --- a/oauth2_provider/tests/test_authorization_code.py +++ b/oauth2_provider/tests/test_authorization_code.py @@ -7,7 +7,6 @@ from django.test import TestCase, RequestFactory from django.core.urlresolvers import reverse -from django.test.utils import override_settings from django.utils import timezone from ..compat import urlparse, parse_qs, urlencode, get_user_model diff --git a/oauth2_provider/tests/test_decorators.py b/oauth2_provider/tests/test_decorators.py index b9e22bc93..babf574d1 100644 --- a/oauth2_provider/tests/test_decorators.py +++ b/oauth2_provider/tests/test_decorators.py @@ -1,4 +1,3 @@ -import json from datetime import timedelta from django.test import TestCase, RequestFactory diff --git a/oauth2_provider/tests/test_implicit.py b/oauth2_provider/tests/test_implicit.py index df038fd58..b3414ad7d 100644 --- a/oauth2_provider/tests/test_implicit.py +++ b/oauth2_provider/tests/test_implicit.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import mock from django.test import TestCase, RequestFactory from django.core.urlresolvers import reverse @@ -8,7 +7,7 @@ from ..compat import urlparse, parse_qs, urlencode, get_user_model from ..models import get_application_model from ..settings import oauth2_settings -from ..views import ProtectedResourceView, AuthorizationView +from ..views import ProtectedResourceView Application = get_application_model() diff --git a/oauth2_provider/tests/test_oauth2_backends.py b/oauth2_provider/tests/test_oauth2_backends.py index 399f9a4fd..5203e09cc 100644 --- a/oauth2_provider/tests/test_oauth2_backends.py +++ b/oauth2_provider/tests/test_oauth2_backends.py @@ -2,7 +2,6 @@ import mock from django.test import TestCase, RequestFactory -from django.test.utils import override_settings from ..backends import get_oauthlib_core from ..oauth2_backends import OAuthLibCore, JSONOAuthLibCore diff --git a/oauth2_provider/views/__init__.py b/oauth2_provider/views/__init__.py index 257c86add..4f444f55d 100644 --- a/oauth2_provider/views/__init__.py +++ b/oauth2_provider/views/__init__.py @@ -1,3 +1,4 @@ +# flake8: noqa from .base import AuthorizationView, TokenView, RevokeTokenView from .application import ApplicationRegistration, ApplicationDetail, ApplicationList, \ ApplicationDelete, ApplicationUpdate diff --git a/tox.ini b/tox.ini index 7a4bc12fe..e0560ef1f 100644 --- a/tox.ini +++ b/tox.ini @@ -37,5 +37,4 @@ commands = [flake8] max-line-length = 120 -ignore = F403,F401 exclude = docs,migrations,south_migrations,.tox From 53b80156333a5cc184f8d1be7d905fd352cc25cf Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Thu, 10 Mar 2016 15:26:07 +0100 Subject: [PATCH 70/91] removed django 1.6 compatibility settings --- oauth2_provider/tests/settings.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/oauth2_provider/tests/settings.py b/oauth2_provider/tests/settings.py index b7521bd4c..1e38f8cc2 100644 --- a/oauth2_provider/tests/settings.py +++ b/oauth2_provider/tests/settings.py @@ -118,11 +118,3 @@ OAUTH2_PROVIDER = { '_SCOPES': ['example'] } - -import django - -if django.VERSION[:2] < (1, 6): - TEST_RUNNER = 'discover_runner.DiscoverRunner' - INSTALLED_APPS += ('discover_runner',) -else: - TEST_RUNNER = 'django.test.runner.DiscoverRunner' From 8201b81afe7eca0ee80453ccfd5bdbf550c25e12 Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Thu, 10 Mar 2016 16:36:48 +0100 Subject: [PATCH 71/91] updated test matrix --- .travis.yml | 10 ++++------ tox.ini | 13 ++++++------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index b344a7538..ffc2ea3ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ language: python -python: "2.7" +python: + - "3.5" + sudo: false env: @@ -18,14 +20,10 @@ env: - TOX_ENV=docs matrix: - # Python 3.5 not yet available on travis, watch this to see when it is. fast_finish: true - allow_failures: - - env: TOX_ENV=py35-django18 - - env: TOX_ENV=py35-django19 install: - - pip install tox + - pip install tox "virtualenv<14" - pip install coveralls script: diff --git a/tox.ini b/tox.ini index e0560ef1f..cd372938f 100644 --- a/tox.ini +++ b/tox.ini @@ -4,10 +4,10 @@ testpaths=oauth2_provider [tox] envlist = - {py27}-django{17,18,19}, - {py32}-django{17,18}, - {py33}-django{17,18}, - {py34}-django{17,18,19}, + {py27}-django{18,19}, + {py32}-django{18}, + {py33}-django{18}, + {py34}-django{18,19}, {py35}-django{18,19}, docs, flake8 @@ -15,9 +15,8 @@ envlist = [testenv] commands=python runtests.py -q --cov oauth2_provider --cov-report= --cov-append deps = - django17: Django==1.7.11 - django18: Django==1.8.7 - django19: Django==1.9 + django18: Django==1.8.11 + django19: Django==1.9.4 coverage<4 -rrequirements/testing.txt From 6c5da583fd7c8fbf5e53beb8aa37dac1816e13d8 Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Thu, 10 Mar 2016 16:40:44 +0100 Subject: [PATCH 72/91] removed django17 from .travis.yml --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index ffc2ea3ee..8e318a6dc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,14 +5,10 @@ python: sudo: false env: - - TOX_ENV=py27-django17 - TOX_ENV=py27-django18 - TOX_ENV=py27-django19 - - TOX_ENV=py32-django17 - TOX_ENV=py32-django18 - - TOX_ENV=py33-django17 - TOX_ENV=py33-django18 - - TOX_ENV=py34-django17 - TOX_ENV=py34-django18 - TOX_ENV=py34-django19 - TOX_ENV=py35-django18 From e8e7980e491c51a524ab8a99897d644b05dd231a Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Fri, 11 Mar 2016 15:02:23 +0100 Subject: [PATCH 73/91] removed old compatibility code for django < 1.8 --- oauth2_provider/backends.py | 4 ++- oauth2_provider/compat.py | 30 +------------------ oauth2_provider/models.py | 14 +++++---- .../application_confirm_delete.html | 3 +- .../oauth2_provider/application_detail.html | 3 +- .../oauth2_provider/application_form.html | 3 +- .../oauth2_provider/application_list.html | 3 +- .../application_registration_form.html | 3 +- .../oauth2_provider/authorized-tokens.html | 1 - oauth2_provider/templatetags/__init__.py | 0 oauth2_provider/templatetags/compat.py | 10 ------- .../tests/test_application_views.py | 3 +- oauth2_provider/tests/test_auth_backends.py | 6 ++-- .../tests/test_authorization_code.py | 5 ++-- .../tests/test_client_credential.py | 2 +- oauth2_provider/tests/test_decorators.py | 2 +- oauth2_provider/tests/test_implicit.py | 6 ++-- oauth2_provider/tests/test_models.py | 9 ++---- .../tests/test_oauth2_validators.py | 2 +- oauth2_provider/tests/test_password.py | 4 +-- oauth2_provider/tests/test_rest_framework.py | 8 ++--- oauth2_provider/tests/test_scopes.py | 6 ++-- .../tests/test_token_revocation.py | 5 ++-- oauth2_provider/tests/test_token_view.py | 3 +- 24 files changed, 47 insertions(+), 88 deletions(-) delete mode 100644 oauth2_provider/templatetags/__init__.py delete mode 100644 oauth2_provider/templatetags/compat.py diff --git a/oauth2_provider/backends.py b/oauth2_provider/backends.py index 3578fa0c5..b2e706b54 100644 --- a/oauth2_provider/backends.py +++ b/oauth2_provider/backends.py @@ -1,6 +1,8 @@ -from .compat import get_user_model +from django.contrib.auth import get_user_model + from .oauth2_backends import get_oauthlib_core + UserModel = get_user_model() OAuthLibCore = get_oauthlib_core() diff --git a/oauth2_provider/compat.py b/oauth2_provider/compat.py index d4c3fc231..f8888505f 100644 --- a/oauth2_provider/compat.py +++ b/oauth2_provider/compat.py @@ -1,13 +1,10 @@ """ The `compat` module provides support for backwards compatibility with older -versions of django and python.. +versions of django and python. """ # flake8: noqa from __future__ import unicode_literals -import django -from django.conf import settings - # urlparse in python3 has been renamed to urllib.parse try: from urlparse import urlparse, parse_qs, parse_qsl, urlunparse @@ -18,28 +15,3 @@ from urllib import urlencode, unquote_plus except ImportError: from urllib.parse import urlencode, unquote_plus - -# Django 1.5 add support for custom auth user model -if django.VERSION >= (1, 5): - AUTH_USER_MODEL = settings.AUTH_USER_MODEL -else: - AUTH_USER_MODEL = 'auth.User' - -try: - from django.contrib.auth import get_user_model -except ImportError: - from django.contrib.auth.models import User - get_user_model = lambda: User - -# Django's new application loading system -try: - from django.apps import apps - get_model = apps.get_model -except ImportError: - from django.db.models import get_model - -# Django 1.5 add the support of context variables for the url template tag -if django.VERSION >= (1, 5): - from django.template.defaulttags import url -else: - from django.templatetags.future import url diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py index fd3cdf40d..f127f88cd 100644 --- a/oauth2_provider/models.py +++ b/oauth2_provider/models.py @@ -2,6 +2,8 @@ from datetime import timedelta +from django.apps import apps +from django.conf import settings from django.core.urlresolvers import reverse from django.db import models, transaction from django.utils import timezone @@ -11,7 +13,7 @@ from django.core.exceptions import ImproperlyConfigured from .settings import oauth2_settings -from .compat import AUTH_USER_MODEL, parse_qsl, urlparse, get_model +from .compat import parse_qsl, urlparse from .generators import generate_client_secret, generate_client_id from .validators import validate_uris @@ -57,7 +59,7 @@ class AbstractApplication(models.Model): client_id = models.CharField(max_length=100, unique=True, default=generate_client_id, db_index=True) - user = models.ForeignKey(AUTH_USER_MODEL, related_name="%(app_label)s_%(class)s") + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="%(app_label)s_%(class)s") help_text = _("Allowed URIs list, space separated") redirect_uris = models.TextField(help_text=help_text, validators=[validate_uris], blank=True) @@ -146,7 +148,7 @@ class Grant(models.Model): * :attr:`redirect_uri` Self explained * :attr:`scope` Required scopes, optional """ - user = models.ForeignKey(AUTH_USER_MODEL) + user = models.ForeignKey(settings.AUTH_USER_MODEL) code = models.CharField(max_length=255, db_index=True) # code comes from oauthlib application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL) expires = models.DateTimeField() @@ -183,7 +185,7 @@ class AccessToken(models.Model): * :attr:`expires` Date and time of token expiration, in DateTime format * :attr:`scope` Allowed scopes """ - user = models.ForeignKey(AUTH_USER_MODEL, blank=True, null=True) + user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True) token = models.CharField(max_length=255, db_index=True) application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL) expires = models.DateTimeField() @@ -252,7 +254,7 @@ class RefreshToken(models.Model): * :attr:`access_token` AccessToken instance this refresh token is bounded to """ - user = models.ForeignKey(AUTH_USER_MODEL) + user = models.ForeignKey(settings.AUTH_USER_MODEL) token = models.CharField(max_length=255, db_index=True) application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL) access_token = models.OneToOneField(AccessToken, @@ -276,7 +278,7 @@ 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) + app_model = apps.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/templates/oauth2_provider/application_confirm_delete.html b/oauth2_provider/templates/oauth2_provider/application_confirm_delete.html index b1d944f9e..35b961a0b 100644 --- a/oauth2_provider/templates/oauth2_provider/application_confirm_delete.html +++ b/oauth2_provider/templates/oauth2_provider/application_confirm_delete.html @@ -1,7 +1,6 @@ {% extends "oauth2_provider/base.html" %} {% load i18n %} -{% load url from compat %} {% block content %}

{% trans "Are you sure to delete the application" %} {{ application.name }}?

@@ -16,4 +15,4 @@

{% trans "Are you sure to delete the applicatio

-{% endblock content %} \ No newline at end of file +{% endblock content %} diff --git a/oauth2_provider/templates/oauth2_provider/application_detail.html b/oauth2_provider/templates/oauth2_provider/application_detail.html index 833f9a581..736dc4605 100644 --- a/oauth2_provider/templates/oauth2_provider/application_detail.html +++ b/oauth2_provider/templates/oauth2_provider/application_detail.html @@ -1,7 +1,6 @@ {% extends "oauth2_provider/base.html" %} {% load i18n %} -{% load url from compat %} {% block content %}

{{ application.name }}

@@ -39,4 +38,4 @@

{{ application.name }}

{% trans "Delete" %}
-{% endblock content %} \ No newline at end of file +{% endblock content %} diff --git a/oauth2_provider/templates/oauth2_provider/application_form.html b/oauth2_provider/templates/oauth2_provider/application_form.html index 5c08ff0aa..43926e134 100644 --- a/oauth2_provider/templates/oauth2_provider/application_form.html +++ b/oauth2_provider/templates/oauth2_provider/application_form.html @@ -1,7 +1,6 @@ {% extends "oauth2_provider/base.html" %} {% load i18n %} -{% load url from compat %} {% block content %}
@@ -40,4 +39,4 @@

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/oauth2_provider/templates/oauth2_provider/application_list.html b/oauth2_provider/templates/oauth2_provider/application_list.html index cb7c7c4eb..34b299a6c 100644 --- a/oauth2_provider/templates/oauth2_provider/application_list.html +++ b/oauth2_provider/templates/oauth2_provider/application_list.html @@ -1,7 +1,6 @@ {% extends "oauth2_provider/base.html" %} {% load i18n %} -{% load url from compat %} {% block content %}

{% trans "Your applications" %}

@@ -17,4 +16,4 @@

{% trans "Your applications" %}

{% trans "No applications defined" %}. {% trans "Click here" %} {% trans "if you want to register a new one" %}

{% endif %}
-{% endblock content %} \ No newline at end of file +{% endblock content %} diff --git a/oauth2_provider/templates/oauth2_provider/application_registration_form.html b/oauth2_provider/templates/oauth2_provider/application_registration_form.html index 69bebb283..c22eca9ef 100644 --- a/oauth2_provider/templates/oauth2_provider/application_registration_form.html +++ b/oauth2_provider/templates/oauth2_provider/application_registration_form.html @@ -1,10 +1,9 @@ {% extends "oauth2_provider/application_form.html" %} {% load i18n %} -{% load url from compat %} {% block app-form-title %}{% trans "Register a new application" %}{% endblock app-form-title %} {% block app-form-action-url %}{% url 'oauth2_provider:register' %}{% endblock app-form-action-url %} -{% block app-form-back-url %}{% url "oauth2_provider:list" %}"{% endblock app-form-back-url %} \ No newline at end of file +{% block app-form-back-url %}{% url "oauth2_provider:list" %}"{% endblock app-form-back-url %} diff --git a/oauth2_provider/templates/oauth2_provider/authorized-tokens.html b/oauth2_provider/templates/oauth2_provider/authorized-tokens.html index f25069e61..2c6a028a8 100644 --- a/oauth2_provider/templates/oauth2_provider/authorized-tokens.html +++ b/oauth2_provider/templates/oauth2_provider/authorized-tokens.html @@ -1,7 +1,6 @@ {% extends "oauth2_provider/base.html" %} {% load i18n %} -{% load url from compat %} {% block content %}

{% trans "Tokens" %}

diff --git a/oauth2_provider/templatetags/__init__.py b/oauth2_provider/templatetags/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/oauth2_provider/templatetags/compat.py b/oauth2_provider/templatetags/compat.py deleted file mode 100644 index 8fbc8b0c4..000000000 --- a/oauth2_provider/templatetags/compat.py +++ /dev/null @@ -1,10 +0,0 @@ -from django import template - -from ..compat import url as url_compat - -register = template.Library() - - -@register.tag -def url(parser, token): - return url_compat(parser, token) diff --git a/oauth2_provider/tests/test_application_views.py b/oauth2_provider/tests/test_application_views.py index f5920c1ea..8cf22b9a8 100644 --- a/oauth2_provider/tests/test_application_views.py +++ b/oauth2_provider/tests/test_application_views.py @@ -1,10 +1,11 @@ from __future__ import unicode_literals +from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse from django.test import TestCase from ..models import get_application_model -from ..compat import get_user_model + Application = get_application_model() UserModel = get_user_model() diff --git a/oauth2_provider/tests/test_auth_backends.py b/oauth2_provider/tests/test_auth_backends.py index 53efc224d..d5abb1935 100644 --- a/oauth2_provider/tests/test_auth_backends.py +++ b/oauth2_provider/tests/test_auth_backends.py @@ -1,11 +1,11 @@ +from django.conf.global_settings import MIDDLEWARE_CLASSES +from django.contrib.auth import get_user_model +from django.contrib.auth.models import AnonymousUser from django.test import TestCase, RequestFactory from django.test.utils import override_settings -from django.contrib.auth.models import AnonymousUser from django.utils.timezone import now, timedelta -from django.conf.global_settings import MIDDLEWARE_CLASSES from django.http import HttpResponse -from ..compat import get_user_model from ..models import get_application_model from ..models import AccessToken from ..backends import OAuth2Backend diff --git a/oauth2_provider/tests/test_authorization_code.py b/oauth2_provider/tests/test_authorization_code.py index 8ce8b8024..e9c7aae2f 100644 --- a/oauth2_provider/tests/test_authorization_code.py +++ b/oauth2_provider/tests/test_authorization_code.py @@ -5,11 +5,12 @@ import datetime import mock -from django.test import TestCase, RequestFactory +from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse +from django.test import TestCase, RequestFactory from django.utils import timezone -from ..compat import urlparse, parse_qs, urlencode, get_user_model +from ..compat import urlparse, parse_qs, urlencode from ..models import get_application_model, Grant, AccessToken, RefreshToken from ..settings import oauth2_settings from ..views import ProtectedResourceView diff --git a/oauth2_provider/tests/test_client_credential.py b/oauth2_provider/tests/test_client_credential.py index a0462ca8d..515cac59e 100644 --- a/oauth2_provider/tests/test_client_credential.py +++ b/oauth2_provider/tests/test_client_credential.py @@ -8,6 +8,7 @@ import urllib from django.core.urlresolvers import reverse +from django.contrib.auth import get_user_model from django.test import TestCase, RequestFactory from django.views.generic import View @@ -19,7 +20,6 @@ from ..settings import oauth2_settings from ..views import ProtectedResourceView from ..views.mixins import OAuthLibMixin -from ..compat import get_user_model from .test_utils import TestCaseUtils diff --git a/oauth2_provider/tests/test_decorators.py b/oauth2_provider/tests/test_decorators.py index babf574d1..294497e1f 100644 --- a/oauth2_provider/tests/test_decorators.py +++ b/oauth2_provider/tests/test_decorators.py @@ -1,12 +1,12 @@ from datetime import timedelta +from django.contrib.auth import get_user_model from django.test import TestCase, RequestFactory from django.utils import timezone from ..decorators import protected_resource, rw_protected_resource from ..settings import oauth2_settings from ..models import get_application_model, AccessToken -from ..compat import get_user_model from .test_utils import TestCaseUtils diff --git a/oauth2_provider/tests/test_implicit.py b/oauth2_provider/tests/test_implicit.py index b3414ad7d..25493ec61 100644 --- a/oauth2_provider/tests/test_implicit.py +++ b/oauth2_provider/tests/test_implicit.py @@ -1,10 +1,10 @@ from __future__ import unicode_literals - -from django.test import TestCase, RequestFactory +from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse +from django.test import TestCase, RequestFactory -from ..compat import urlparse, parse_qs, urlencode, get_user_model +from ..compat import urlparse, parse_qs, urlencode from ..models import get_application_model from ..settings import oauth2_settings from ..views import ProtectedResourceView diff --git a/oauth2_provider/tests/test_models.py b/oauth2_provider/tests/test_models.py index 563000207..89bad276f 100644 --- a/oauth2_provider/tests/test_models.py +++ b/oauth2_provider/tests/test_models.py @@ -1,18 +1,15 @@ from __future__ import unicode_literals -try: - from unittest import skipIf -except ImportError: - from django.utils.unittest.case import skipIf +from unittest import skipIf import django +from django.contrib.auth import get_user_model +from django.core.exceptions import ValidationError from django.test import TestCase from django.test.utils import override_settings -from django.core.exceptions import ValidationError from django.utils import timezone from ..models import get_application_model, Grant, AccessToken, RefreshToken -from ..compat import get_user_model Application = get_application_model() diff --git a/oauth2_provider/tests/test_oauth2_validators.py b/oauth2_provider/tests/test_oauth2_validators.py index 851d2e70e..e4f7e1a82 100644 --- a/oauth2_provider/tests/test_oauth2_validators.py +++ b/oauth2_provider/tests/test_oauth2_validators.py @@ -1,3 +1,4 @@ +from django.contrib.auth import get_user_model from django.test import TestCase import mock @@ -5,7 +6,6 @@ from ..oauth2_validators import OAuth2Validator from ..models import get_application_model -from ..compat import get_user_model UserModel = get_user_model() AppModel = get_application_model() diff --git a/oauth2_provider/tests/test_password.py b/oauth2_provider/tests/test_password.py index a4fbdf424..72db69f37 100644 --- a/oauth2_provider/tests/test_password.py +++ b/oauth2_provider/tests/test_password.py @@ -2,13 +2,13 @@ import json -from django.test import TestCase, RequestFactory +from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse +from django.test import TestCase, RequestFactory from ..models import get_application_model from ..settings import oauth2_settings from ..views import ProtectedResourceView -from ..compat import get_user_model from .test_utils import TestCaseUtils diff --git a/oauth2_provider/tests/test_rest_framework.py b/oauth2_provider/tests/test_rest_framework.py index 83c9dbe12..10ebdc403 100644 --- a/oauth2_provider/tests/test_rest_framework.py +++ b/oauth2_provider/tests/test_rest_framework.py @@ -1,19 +1,15 @@ +import unittest from datetime import timedelta from django.conf.urls import patterns, url, include +from django.contrib.auth import get_user_model from django.http import HttpResponse from django.test import TestCase from django.utils import timezone -try: - from django.utils import unittest -except ImportError: - import unittest - from .test_utils import TestCaseUtils from ..models import AccessToken, get_application_model from ..settings import oauth2_settings -from ..compat import get_user_model Application = get_application_model() diff --git a/oauth2_provider/tests/test_scopes.py b/oauth2_provider/tests/test_scopes.py index 36ef1032d..dfc1fdfd1 100644 --- a/oauth2_provider/tests/test_scopes.py +++ b/oauth2_provider/tests/test_scopes.py @@ -2,16 +2,18 @@ import json -from django.test import TestCase, RequestFactory +from django.contrib.auth import get_user_model from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse +from django.test import TestCase, RequestFactory from .test_utils import TestCaseUtils -from ..compat import urlparse, parse_qs, get_user_model, urlencode +from ..compat import urlparse, parse_qs, urlencode from ..models import get_application_model, Grant, AccessToken from ..settings import oauth2_settings from ..views import ScopedProtectedResourceView, ReadWriteScopedResourceView + Application = get_application_model() UserModel = get_user_model() diff --git a/oauth2_provider/tests/test_token_revocation.py b/oauth2_provider/tests/test_token_revocation.py index ce8024fa9..868a3aa3b 100644 --- a/oauth2_provider/tests/test_token_revocation.py +++ b/oauth2_provider/tests/test_token_revocation.py @@ -2,11 +2,12 @@ import datetime -from django.test import TestCase, RequestFactory +from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse +from django.test import TestCase, RequestFactory from django.utils import timezone -from ..compat import urlencode, get_user_model +from ..compat import urlencode from ..models import get_application_model, AccessToken, RefreshToken from ..settings import oauth2_settings diff --git a/oauth2_provider/tests/test_token_view.py b/oauth2_provider/tests/test_token_view.py index 7e02a32b2..30c3fa020 100644 --- a/oauth2_provider/tests/test_token_view.py +++ b/oauth2_provider/tests/test_token_view.py @@ -2,12 +2,13 @@ import datetime +from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse from django.test import TestCase from django.utils import timezone from ..models import get_application_model, AccessToken -from ..compat import get_user_model + Application = get_application_model() UserModel = get_user_model() From 98b078db1e8bbebb6054f5eaece26fe35f778309 Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Fri, 11 Mar 2016 15:29:26 +0100 Subject: [PATCH 74/91] removed south_migrations since support for django<1.8 was dropped --- .../south_migrations/0001_initial.py | 159 ------------------ .../south_migrations/0002_adding_indexes.py | 119 ------------- ...p_authorization__chg_field_accesstoken_.py | 121 ------------- oauth2_provider/south_migrations/__init__.py | 0 4 files changed, 399 deletions(-) delete mode 100644 oauth2_provider/south_migrations/0001_initial.py delete mode 100644 oauth2_provider/south_migrations/0002_adding_indexes.py delete mode 100644 oauth2_provider/south_migrations/0003_auto__add_field_application_skip_authorization__chg_field_accesstoken_.py delete mode 100644 oauth2_provider/south_migrations/__init__.py diff --git a/oauth2_provider/south_migrations/0001_initial.py b/oauth2_provider/south_migrations/0001_initial.py deleted file mode 100644 index e42b6b054..000000000 --- a/oauth2_provider/south_migrations/0001_initial.py +++ /dev/null @@ -1,159 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - -try: - from django.contrib.auth import get_user_model -except ImportError: # django < 1.5 - from django.contrib.auth.models import User -else: - User = get_user_model() - -from oauth2_provider.models import get_application_model -ApplicationModel = get_application_model() - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding model 'Application' - db.create_table(u'oauth2_provider_application', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('client_id', self.gf('django.db.models.fields.CharField')(default='284250a821f74df67cb50b6c2b7fc95d39d0e4a9', unique=True, max_length=100)), - ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm["%s.%s" % (User._meta.app_label, User._meta.object_name)])), - ('redirect_uris', self.gf('django.db.models.fields.TextField')(blank=True)), - ('client_type', self.gf('django.db.models.fields.CharField')(max_length=32)), - ('authorization_grant_type', self.gf('django.db.models.fields.CharField')(max_length=32)), - ('client_secret', self.gf('django.db.models.fields.CharField')(default='89288b8343edef095b5fee98b4def28409cf4e064fcd26b00c555f51d8fdabfcaedbae8b9d6739080cf27d216e13cc85133d794c9cc1018e0d116c951f0b865e', max_length=255, blank=True)), - ('name', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), - )) - db.send_create_signal(u'oauth2_provider', ['Application']) - - # Adding model 'Grant' - db.create_table(u'oauth2_provider_grant', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm["%s.%s" % (User._meta.app_label, User._meta.object_name)])), - ('code', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('application', self.gf('django.db.models.fields.related.ForeignKey')(to=orm["%s.%s" % (ApplicationModel._meta.app_label, ApplicationModel._meta.object_name)])), - ('expires', self.gf('django.db.models.fields.DateTimeField')()), - ('redirect_uri', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('scope', self.gf('django.db.models.fields.TextField')(blank=True)), - )) - db.send_create_signal(u'oauth2_provider', ['Grant']) - - # Adding model 'AccessToken' - db.create_table(u'oauth2_provider_accesstoken', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm["%s.%s" % (User._meta.app_label, User._meta.object_name)])), - ('token', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('application', self.gf('django.db.models.fields.related.ForeignKey')(to=orm["%s.%s" % (ApplicationModel._meta.app_label, ApplicationModel._meta.object_name)])), - ('expires', self.gf('django.db.models.fields.DateTimeField')()), - ('scope', self.gf('django.db.models.fields.TextField')(blank=True)), - )) - db.send_create_signal(u'oauth2_provider', ['AccessToken']) - - # Adding model 'RefreshToken' - db.create_table(u'oauth2_provider_refreshtoken', ( - (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), - ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm["%s.%s" % (User._meta.app_label, User._meta.object_name)])), - ('token', self.gf('django.db.models.fields.CharField')(max_length=255)), - ('application', self.gf('django.db.models.fields.related.ForeignKey')(to=orm["%s.%s" % (ApplicationModel._meta.app_label, ApplicationModel._meta.object_name)])), - ('access_token', self.gf('django.db.models.fields.related.OneToOneField')(related_name='refresh_token', unique=True, to=orm['oauth2_provider.AccessToken'])), - )) - db.send_create_signal(u'oauth2_provider', ['RefreshToken']) - - - def backwards(self, orm): - # Deleting model 'Application' - db.delete_table(u'oauth2_provider_application') - - # Deleting model 'Grant' - db.delete_table(u'oauth2_provider_grant') - - # Deleting model 'AccessToken' - db.delete_table(u'oauth2_provider_accesstoken') - - # Deleting model 'RefreshToken' - db.delete_table(u'oauth2_provider_refreshtoken') - - - models = { - u'auth.group': { - 'Meta': {'object_name': 'Group'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - u'auth.permission': { - 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - u"%s.%s" % (User._meta.app_label, User._meta.object_name): { - 'Meta': {'object_name': User.__name__}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - u'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - u'oauth2_provider.accesstoken': { - 'Meta': {'object_name': 'AccessToken'}, - 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']"% (ApplicationModel._meta.app_label, ApplicationModel._meta.object_name)}), - 'expires': ('django.db.models.fields.DateTimeField', [], {}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'scope': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'token': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']"% (User._meta.app_label, User._meta.object_name)}) - }, - u"%s.%s" % (ApplicationModel._meta.app_label, ApplicationModel._meta.object_name): { - 'Meta': {'object_name': ApplicationModel.__name__}, - 'authorization_grant_type': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - 'client_id': ('django.db.models.fields.CharField', [], {'default': "'30f17d266183cd455bc57ce8548a439db3491353'", 'unique': 'True', 'max_length': '100'}), - 'client_secret': ('django.db.models.fields.CharField', [], {'default': "'18e68df61ad8e1af355644ddf6a636b269b6309aafbd2a34d4f5ed6c5562e44c0792c5b2441571e85cbf8a85249dca5537dedb6fd6f60e134f4a60f3865c8395'", 'max_length': '255', 'blank': 'True'}), - 'client_type': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), - 'redirect_uris': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']"% (User._meta.app_label, User._meta.object_name)}) - }, - u'oauth2_provider.grant': { - 'Meta': {'object_name': 'Grant'}, - 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']"% (ApplicationModel._meta.app_label, ApplicationModel._meta.object_name)}), - 'code': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'expires': ('django.db.models.fields.DateTimeField', [], {}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'redirect_uri': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'scope': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']"% (User._meta.app_label, User._meta.object_name)}) - }, - u'oauth2_provider.refreshtoken': { - 'Meta': {'object_name': 'RefreshToken'}, - 'access_token': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'refresh_token'", 'unique': 'True', 'to': u"orm['oauth2_provider.AccessToken']"}), - 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']"% (ApplicationModel._meta.app_label, ApplicationModel._meta.object_name)}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'token': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']"% (User._meta.app_label, User._meta.object_name)}) - } - } - - complete_apps = ['oauth2_provider'] \ No newline at end of file diff --git a/oauth2_provider/south_migrations/0002_adding_indexes.py b/oauth2_provider/south_migrations/0002_adding_indexes.py deleted file mode 100644 index 6c65c86ab..000000000 --- a/oauth2_provider/south_migrations/0002_adding_indexes.py +++ /dev/null @@ -1,119 +0,0 @@ -# -*- coding: utf-8 -*- -import datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - -try: - from django.contrib.auth import get_user_model -except ImportError: # django < 1.5 - from django.contrib.auth.models import User -else: - User = get_user_model() - -from oauth2_provider.models import get_application_model -ApplicationModel = get_application_model() - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding index on 'RefreshToken', fields ['token'] - db.create_index(u'oauth2_provider_refreshtoken', ['token']) - - # Adding index on 'Grant', fields ['code'] - db.create_index(u'oauth2_provider_grant', ['code']) - - # Adding index on 'AccessToken', fields ['token'] - db.create_index(u'oauth2_provider_accesstoken', ['token']) - - - def backwards(self, orm): - # Removing index on 'AccessToken', fields ['token'] - db.delete_index(u'oauth2_provider_accesstoken', ['token']) - - # Removing index on 'Grant', fields ['code'] - db.delete_index(u'oauth2_provider_grant', ['code']) - - # Removing index on 'RefreshToken', fields ['token'] - db.delete_index(u'oauth2_provider_refreshtoken', ['token']) - - - models = { - u'auth.group': { - 'Meta': {'object_name': 'Group'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - u'auth.permission': { - 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - u"%s.%s" % (User._meta.app_label, User._meta.object_name): { - 'Meta': {'object_name': User.__name__}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - u'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - u'oauth2_provider.accesstoken': { - 'Meta': {'object_name': 'AccessToken'}, - 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']"% (ApplicationModel._meta.app_label, ApplicationModel._meta.object_name)}), - 'expires': ('django.db.models.fields.DateTimeField', [], {}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'scope': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'token': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']"% (User._meta.app_label, User._meta.object_name)}) - }, - u"%s.%s" % (ApplicationModel._meta.app_label, ApplicationModel._meta.object_name): { - 'Meta': {'object_name': ApplicationModel.__name__}, - 'authorization_grant_type': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - 'client_id': ('django.db.models.fields.CharField', [], {'default': "'30f17d266183cd455bc57ce8548a439db3491353'", 'unique': 'True', 'max_length': '100'}), - 'client_secret': ('django.db.models.fields.CharField', [], {'default': "'18e68df61ad8e1af355644ddf6a636b269b6309aafbd2a34d4f5ed6c5562e44c0792c5b2441571e85cbf8a85249dca5537dedb6fd6f60e134f4a60f3865c8395'", 'max_length': '255', 'blank': 'True'}), - 'client_type': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), - 'redirect_uris': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']"% (User._meta.app_label, User._meta.object_name)}) - }, - u'oauth2_provider.grant': { - 'Meta': {'object_name': 'Grant'}, - 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']"% (ApplicationModel._meta.app_label, ApplicationModel._meta.object_name)}), - 'code': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'expires': ('django.db.models.fields.DateTimeField', [], {}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'redirect_uri': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'scope': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']"% (User._meta.app_label, User._meta.object_name)}) - }, - u'oauth2_provider.refreshtoken': { - 'Meta': {'object_name': 'RefreshToken'}, - 'access_token': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'refresh_token'", 'unique': 'True', 'to': u"orm['oauth2_provider.AccessToken']"}), - 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']"% (ApplicationModel._meta.app_label, ApplicationModel._meta.object_name)}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'token': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']"% (User._meta.app_label, User._meta.object_name)}) - } - } - - complete_apps = ['oauth2_provider'] diff --git a/oauth2_provider/south_migrations/0003_auto__add_field_application_skip_authorization__chg_field_accesstoken_.py b/oauth2_provider/south_migrations/0003_auto__add_field_application_skip_authorization__chg_field_accesstoken_.py deleted file mode 100644 index 85f9d8a79..000000000 --- a/oauth2_provider/south_migrations/0003_auto__add_field_application_skip_authorization__chg_field_accesstoken_.py +++ /dev/null @@ -1,121 +0,0 @@ -# -*- coding: utf-8 -*- -from south.utils import datetime_utils as datetime -from south.db import db -from south.v2 import SchemaMigration -from django.db import models - - -try: - from django.contrib.auth import get_user_model -except ImportError: # django < 1.5 - from django.contrib.auth.models import User -else: - User = get_user_model() - -from oauth2_provider.models import get_application_model -ApplicationModel = get_application_model() - - -class Migration(SchemaMigration): - - def forwards(self, orm): - # Adding field 'Application.skip_authorization' - db.add_column(u'oauth2_provider_application', 'skip_authorization', - self.gf('django.db.models.fields.BooleanField')(default=False), - keep_default=False) - - - # Changing field 'AccessToken.user' - db.alter_column(u'oauth2_provider_accesstoken', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)], null=True)) - - def backwards(self, orm): - # Deleting field 'Application.skip_authorization' - db.delete_column(u'oauth2_provider_application', 'skip_authorization') - - - # User chose to not deal with backwards NULL issues for 'AccessToken.user' - raise RuntimeError("Cannot reverse this migration. 'AccessToken.user' and its values cannot be restored.") - - # The following code is provided here to aid in writing a correct migration - # Changing field 'AccessToken.user' - db.alter_column(u'oauth2_provider_accesstoken', 'user_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)])) - - models = { - u'auth.group': { - 'Meta': {'object_name': 'Group'}, - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), - 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) - }, - u'auth.permission': { - 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, - 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) - }, - u'%s.%s' % (User._meta.app_label, User._meta.object_name): { - 'Meta': {'object_name': User.__name__}, - 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), - 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), - 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), - 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), - 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), - 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), - 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) - }, - u'contenttypes.contenttype': { - 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, - 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) - }, - u'oauth2_provider.accesstoken': { - 'Meta': {'object_name': 'AccessToken'}, - 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']"% (ApplicationModel._meta.app_label, ApplicationModel._meta.object_name)}), - 'expires': ('django.db.models.fields.DateTimeField', [], {}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'scope': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']" % (User._meta.app_label, User._meta.object_name), 'null': 'True', 'blank': 'True'}) - }, - u"%s.%s" % (ApplicationModel._meta.app_label, ApplicationModel._meta.object_name): { - 'Meta': {'object_name': ApplicationModel.__name__}, - 'authorization_grant_type': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - 'client_id': ('django.db.models.fields.CharField', [], {'default': "u'amXbsy974anVL3xgzY2dczL8SRMSXA5awkXyjtsY'", 'unique': 'True', 'max_length': '100', 'db_index': 'True'}), - 'client_secret': ('django.db.models.fields.CharField', [], {'default': "u'trXjdJB8EO7HPsZcPswIT1l0Zdg3W3AWDxXvh5Jj9rON2MAoRT6YVDSHqKFB76rIgD9X9YBxoY7jjT4Mj12UHc2BjCCXJI4nzx4qwEwoyZ7l6N88xiHaM6J5qXeWJ6e3'", 'max_length': '255', 'db_index': 'True', 'blank': 'True'}), - 'client_type': ('django.db.models.fields.CharField', [], {'max_length': '32'}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), - 'redirect_uris': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'skip_authorization': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']" % (User._meta.app_label, User._meta.object_name)}) - }, - u'oauth2_provider.grant': { - 'Meta': {'object_name': 'Grant'}, - 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s.%s']"% (ApplicationModel._meta.app_label, ApplicationModel._meta.object_name)}), - 'code': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), - 'expires': ('django.db.models.fields.DateTimeField', [], {}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'redirect_uri': ('django.db.models.fields.CharField', [], {'max_length': '255'}), - 'scope': ('django.db.models.fields.TextField', [], {'blank': 'True'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']" % (User._meta.app_label, User._meta.object_name)}) - }, - u'oauth2_provider.refreshtoken': { - 'Meta': {'object_name': 'RefreshToken'}, - 'access_token': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "u'refresh_token'", 'unique': 'True', 'to': u"orm['oauth2_provider.AccessToken']"}), - 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']"% (ApplicationModel._meta.app_label, ApplicationModel._meta.object_name)}), - u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), - 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['%s.%s']" % (User._meta.app_label, User._meta.object_name)}) - } -} - -complete_apps = ['oauth2_provider'] diff --git a/oauth2_provider/south_migrations/__init__.py b/oauth2_provider/south_migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 From b751b080d7dc3d2e0d128b75a5a68c4a985d6e20 Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Fri, 11 Mar 2016 15:47:46 +0100 Subject: [PATCH 75/91] removed django1.4 specific code --- oauth2_provider/models.py | 6 ++---- oauth2_provider/tests/test_mixins.py | 5 ----- oauth2_provider/tests/test_models.py | 3 --- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py index f127f88cd..eaf73f9ae 100644 --- a/oauth2_provider/models.py +++ b/oauth2_provider/models.py @@ -126,10 +126,8 @@ def __str__(self): class Application(AbstractApplication): - pass - -# Add swappable like this to not break django 1.4 compatibility -Application._meta.swappable = 'OAUTH2_PROVIDER_APPLICATION_MODEL' + class Meta(AbstractApplication.Meta): + swappable = 'OAUTH2_PROVIDER_APPLICATION_MODEL' @python_2_unicode_compatible diff --git a/oauth2_provider/tests/test_mixins.py b/oauth2_provider/tests/test_mixins.py index 4cb6f203d..97695a526 100644 --- a/oauth2_provider/tests/test_mixins.py +++ b/oauth2_provider/tests/test_mixins.py @@ -3,7 +3,6 @@ from django.core.exceptions import ImproperlyConfigured from django.views.generic import View from django.test import TestCase, RequestFactory -from django.http import HttpResponse from oauthlib.oauth2 import Server @@ -100,10 +99,6 @@ class TestView(ProtectedResourceMixin, View): server_class = Server validator_class = OAuth2Validator - def options(self, request, *args, **kwargs): - """Django 1.4 doesn't provide a default options method""" - return HttpResponse() - request = self.request_factory.options("/fake-req") view = TestView.as_view() response = view(request) diff --git a/oauth2_provider/tests/test_models.py b/oauth2_provider/tests/test_models.py index 89bad276f..7bc191c2b 100644 --- a/oauth2_provider/tests/test_models.py +++ b/oauth2_provider/tests/test_models.py @@ -1,7 +1,5 @@ from __future__ import unicode_literals -from unittest import skipIf - import django from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError @@ -109,7 +107,6 @@ def test_scopes_property(self): self.assertEqual(access_token2.scopes, {'write': 'Writing scope'}) -@skipIf(django.VERSION < (1, 5), "Behavior is broken on 1.4 and there is no solution") @override_settings(OAUTH2_PROVIDER_APPLICATION_MODEL='tests.TestApplication') class TestCustomApplicationModel(TestCase): def setUp(self): From 9b28c9e20cedeb8bc93c02326535151f6e670111 Mon Sep 17 00:00:00 2001 From: Federico Frenguelli Date: Fri, 11 Mar 2016 15:48:40 +0100 Subject: [PATCH 76/91] fixed AppsRegistryNotReady errors in docs build --- docs/conf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 84880a19a..d9529ec62 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,9 +19,12 @@ here = os.path.abspath(os.path.dirname(__file__)) sys.path.insert(0, here) sys.path.insert(0, os.path.dirname(here)) -sys.path.insert(0, os.path.join(os.path.dirname(here), 'example')) os.environ['DJANGO_SETTINGS_MODULE'] = 'oauth2_provider.tests.settings' + +import django +django.setup() + import oauth2_provider # -- General configuration ----------------------------------------------------- From 167c6c3d54b06433ec6226171a781e475968b1c7 Mon Sep 17 00:00:00 2001 From: Paul Oswald Date: Thu, 17 Mar 2016 14:10:42 +0900 Subject: [PATCH 77/91] relax user constraint on AbstractApplication model --- oauth2_provider/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py index eaf73f9ae..f87395002 100644 --- a/oauth2_provider/models.py +++ b/oauth2_provider/models.py @@ -59,7 +59,9 @@ class AbstractApplication(models.Model): client_id = models.CharField(max_length=100, unique=True, default=generate_client_id, db_index=True) - user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="%(app_label)s_%(class)s") + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name="%(app_label)s_%(class)s", + null=True, blank=True) + help_text = _("Allowed URIs list, space separated") redirect_uris = models.TextField(help_text=help_text, validators=[validate_uris], blank=True) From b2cbf2916c5cffb9cb756f3824657b19d195ede9 Mon Sep 17 00:00:00 2001 From: Paul Oswald Date: Thu, 17 Mar 2016 14:11:01 +0900 Subject: [PATCH 78/91] relax user constraint on AbstractApplication model --- .../migrations/0003_auto_20160316_1503.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 oauth2_provider/migrations/0003_auto_20160316_1503.py diff --git a/oauth2_provider/migrations/0003_auto_20160316_1503.py b/oauth2_provider/migrations/0003_auto_20160316_1503.py new file mode 100644 index 000000000..5dd05ddff --- /dev/null +++ b/oauth2_provider/migrations/0003_auto_20160316_1503.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('oauth2_provider', '0002_08_updates'), + ] + + operations = [ + migrations.AlterField( + model_name='application', + name='user', + field=models.ForeignKey(related_name='oauth2_provider_application', blank=True, to=settings.AUTH_USER_MODEL, null=True), + ), + ] From 3074e8c3f62ab7a3ccc3db2bb426414402ba041a Mon Sep 17 00:00:00 2001 From: Paul Oswald Date: Fri, 18 Mar 2016 00:19:15 +0900 Subject: [PATCH 79/91] Update changelog --- AUTHORS | 1 + README.rst | 5 +++++ docs/changelog.rst | 6 ++++++ 3 files changed, 12 insertions(+) diff --git a/AUTHORS b/AUTHORS index 7b2c6b83f..3a91ba821 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,3 +16,4 @@ Hiroki Kiyohara Diego Garcia Bas van Oostveen Bart Merenda +Paul Oswald diff --git a/README.rst b/README.rst index 5eed1ba76..4b31264ed 100644 --- a/README.rst +++ b/README.rst @@ -87,6 +87,11 @@ Roadmap / Todo list (help wanted) Changelog --------- +Development +~~~~~~~~~~~ + +* #357: Support multiple-user clients by allowing User to be NULL for Applications + 0.10.0 [2015-12-14] ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/changelog.rst b/docs/changelog.rst index a9a4e5a24..1b24e4dfe 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ Changelog ========= +Development +~~~~~~~~~~~ + +* #357: Support multiple-user clients by allowing User to be NULL for Applications + + 0.10.0 [2015-12-14] ------------------ From 7cba4a5baa49005d449213b9738f427ceeb6af69 Mon Sep 17 00:00:00 2001 From: Emanuele Palazzetti Date: Mon, 21 Mar 2016 16:26:40 +0100 Subject: [PATCH 80/91] added more information about how to report security issues --- README.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.rst b/README.rst index 4b31264ed..f70ca4d15 100644 --- a/README.rst +++ b/README.rst @@ -34,6 +34,16 @@ Contributing We love contributions, so please feel free to fix bugs, improve things, provide documentation. Just `follow the guidelines `_ and submit a PR. +Reporting security issues +------------------------- + +If you believe you've found an issue with security implications, please send a detailed description via email to **security@evonove.it**. +Mail sent to that address reaches the Django OAuth Toolkit core team, who can solve (or forward) the security issue as soon as possible. After +our acknowledge, we may decide to open a public discussion in our mailing list or issues tracker. + +Once you’ve submitted an issue via email, you should receive a response from the core team within 48 hours, and depending on the action to be +taken, you may receive further followup emails. + Requirements ------------ From 5513f110dbd0515089b25ea039d7605c77da9fde Mon Sep 17 00:00:00 2001 From: David Richfield Date: Fri, 22 Apr 2016 09:42:52 +0200 Subject: [PATCH 81/91] Copy-edit Minor grammar and style edits. --- docs/tutorial/tutorial_01.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/tutorial/tutorial_01.rst b/docs/tutorial/tutorial_01.rst index fdb1c3edc..89edbfce5 100644 --- a/docs/tutorial/tutorial_01.rst +++ b/docs/tutorial/tutorial_01.rst @@ -8,7 +8,7 @@ You want to make your own :term:`Authorization Server` to issue access tokens to Start Your App -------------- During this tutorial you will make an XHR POST from a Heroku deployed app to your localhost instance. -Since the domain that will originate the request (the app on Heroku) is different than the destination domain (your local instance), +Since the domain that will originate the request (the app on Heroku) is different from the destination domain (your local instance), you will need to install the `django-cors-headers `_ app. These "cross-domain" requests are by default forbidden by web browsers unless you use `CORS `_. @@ -67,7 +67,7 @@ for details on using login templates. -As a final step, execute migrate command, start the internal server, and login with your credentials. +As a final step, execute the migrate command, start the internal server, and login with your credentials. Create an OAuth2 Client Application ----------------------------------- @@ -78,11 +78,11 @@ the API, subject to approval by its users. Let's register your application. Point your browser to http://localhost:8000/o/applications/ and add an Application instance. -`Client id` and `Client Secret` are automatically generated, you have to provide the rest of the informations: +`Client id` and `Client Secret` are automatically generated; you have to provide the rest of the informations: * `User`: the owner of the Application (e.g. a developer, or the currently logged in user.) - * `Redirect uris`: Applications must register at least one redirection endpoint prior to utilizing the + * `Redirect uris`: Applications must register at least one redirection endpoint beofre using the authorization endpoint. The :term:`Authorization Server` will deliver the access token to the client only if the client specifies one of the verified redirection uris. For this tutorial, paste verbatim the value `http://django-oauth-toolkit.herokuapp.com/consumer/exchange/` @@ -117,9 +117,9 @@ Authorize the Application +++++++++++++++++++++++++ When a user clicks the link, she is redirected to your (possibly local) :term:`Authorization Server`. If you're not logged in, you will be prompted for username and password. This is because the authorization -page is login protected by django-oauth-toolkit. Login, then you should see the (not so cute) form users can use to give +page is login protected by django-oauth-toolkit. Login, then you should see the (not so cute) form a user can use to give her authorization to the client application. Flag the *Allow* checkbox and click *Authorize*, you will be redirected -again on to the consumer service. +again to the consumer service. __ loginTemplate_ @@ -140,9 +140,9 @@ Refresh the token +++++++++++++++++ The page showing the access token retrieved from the :term:`Authorization Server` also let you make a POST request to the server itself to swap the refresh token for another, brand new access token. -Just fill in the missing form fields and click the Refresh button: if everything goes smooth you will see the access and +Just fill in the missing form fields and click the Refresh button: if everything goes smoothly you will see the access and refresh token change their values, otherwise you will likely see an error message. -When finished playing with your authorization server, take note of both the access and refresh tokens, we will use them +When you have finished playing with your authorization server, take note of both the access and refresh tokens, we will use them for the next part of the tutorial. So let's make an API and protect it with your OAuth2 tokens in the :doc:`part 2 of the tutorial `. From f28496c8b07086941909ad94cb22f1c5a89a7362 Mon Sep 17 00:00:00 2001 From: Paul Oswald Date: Fri, 13 May 2016 11:17:47 +0900 Subject: [PATCH 82/91] Small documentation fixes --- docs/advanced_topics.rst | 6 +++--- docs/contributing.rst | 4 ++-- docs/tutorial/tutorial_02.rst | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/advanced_topics.rst b/docs/advanced_topics.rst index 5579e0c69..dd0468f2f 100644 --- a/docs/advanced_topics.rst +++ b/docs/advanced_topics.rst @@ -55,9 +55,9 @@ That's all, now Django OAuth Toolkit will use your model wherever an Application Skip authorization form ======================= -Depending on the OAuth2 flow in use and the access token policy, users might be prompted for the -same authorization multiple times: sometimes this is acceptable or even desiderable but other it isn't. -To control DOT behaviour you can use `approval_prompt` parameter when hitting the authorization endpoint. +Depending on the OAuth2 flow in use and the access token policy, users might be prompted for the +same authorization multiple times: sometimes this is acceptable or even desirable but other times it isn't. +To control DOT behaviour you can use the `approval_prompt` parameter when hitting the authorization endpoint. Possible values are: * `force` - users are always prompted for authorization. diff --git a/docs/contributing.rst b/docs/contributing.rst index 5ebf257a3..6de828be3 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -47,7 +47,7 @@ of the pull request. Pull upstream changes into your fork regularly ============================================== -It's a good practice to pull upstream changes from master into your fork on a regular basis, infact if you work on +It's a good practice to pull upstream changes from master into your fork on a regular basis, in fact if you work on outdated code and your changes diverge too far from master, the pull request has to be rejected. To pull in upstream changes:: @@ -85,7 +85,7 @@ Add the tests! -------------- Whenever you add code, you have to add tests as well. We cannot accept untested code, so unless it is a peculiar -situation you previously discussed with the core commiters, if your pull request reduces the test coverage it will be +situation you previously discussed with the core committers, if your pull request reduces the test coverage it will be **immediately rejected**. Code conventions matter diff --git a/docs/tutorial/tutorial_02.rst b/docs/tutorial/tutorial_02.rst index 98fa08314..214abdb74 100644 --- a/docs/tutorial/tutorial_02.rst +++ b/docs/tutorial/tutorial_02.rst @@ -51,7 +51,7 @@ Testing your API Time to make requests to your API. For a quick test, try accessing your app at the url `/api/hello` with your browser -and verify that it reponds with a `403` (in fact no `HTTP_AUTHORIZATION` header was provided). +and verify that it responds with a `403` (in fact no `HTTP_AUTHORIZATION` header was provided). You can test your API with anything that can perform HTTP requests, but for this tutorial you can use the online `consumer client `_. Just fill the form with the URL of the API endpoint (i.e. http://localhost:8000/api/hello if you're on localhost) and From 06378b58f060e9977ff04f97414c5f60429f149b Mon Sep 17 00:00:00 2001 From: Paul Oswald Date: Fri, 13 May 2016 11:19:17 +0900 Subject: [PATCH 83/91] Don't encourage adding the application urls without dealing security restrictions --- docs/tutorial/tutorial_02.rst | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/tutorial_02.rst b/docs/tutorial/tutorial_02.rst index 214abdb74..7ea8d98dd 100644 --- a/docs/tutorial/tutorial_02.rst +++ b/docs/tutorial/tutorial_02.rst @@ -34,15 +34,37 @@ URL this view will respond to: .. code-block:: python + from django.conf.urls import patterns, url + from oauth2_provider import views + from django.conf import settings from .views import ApiEndpoint urlpatterns = patterns( '', url(r'^admin/', include(admin.site.urls)), - url(r'^o/', include('oauth2_provider.urls', namespace='oauth2_provider')), # look ma, I'm a provider! - url(r'^api/hello', ApiEndpoint.as_view()), # and also a resource server! + + # OAuth2 provider endpoints + url(r'^o/authorize/$', views.AuthorizationView.as_view(), name="authorize"), + url(r'^o/token/$', views.TokenView.as_view(), name="token"), + url(r'^o/revoke-token/$', views.RevokeTokenView.as_view(), name="revoke-token"), + + url(r'^api/hello', ApiEndpoint.as_view()), # a resource endpoint ) + if settings.DEBUG: + # OAuth2 Application management views + + urlpatterns += patterns( + '', + url(r'^o/applications/$', views.ApplicationList.as_view(), name="application-list"), + url(r'^o/applications/register/$', views.ApplicationRegistration.as_view(), name="application-register"), + url(r'^o/applications/(?P\d+)/$', views.ApplicationDetail.as_view(), name="application-detail"), + url(r'^o/applications/(?P\d+)/delete/$', views.ApplicationDelete.as_view(), name="application-delete"), + url(r'^o/applications/(?P\d+)/update/$', views.ApplicationUpdate.as_view(), name="application-update"), + ) + +You will probably want to write your own application views to deal with permissions and access control but the ones packaged with the library can get you started when developing the app. + Since we inherit from `ProtectedResourceView`, we're done and our API is OAuth2 protected - for the sake of the lazy programmer. From e6fc1a9637f08e4d6d5fa39e10b2fddcc346614a Mon Sep 17 00:00:00 2001 From: Adam Chainz Date: Tue, 31 May 2016 18:52:44 +0100 Subject: [PATCH 84/91] Convert readthedocs link for their .org -> .io migration for hosted projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As per [their blog post of the 27th April](https://blog.readthedocs.com/securing-subdomains/) ‘Securing subdomains’: > Starting today, Read the Docs will start hosting projects from subdomains on the domain readthedocs.io, instead of on readthedocs.org. This change addresses some security concerns around site cookies while hosting user generated data on the same domain as our dashboard. Test Plan: Manually visited all the links I’ve modified. --- CONTRIBUTING.rst | 2 +- README.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 69be21a75..61d13273e 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -2,4 +2,4 @@ Contributing ============ Thanks for your interest! We love contributions, so please feel free to fix bugs, improve things, provide documentation. Just `follow the -guidelines `_ and submit a PR. +guidelines `_ and submit a PR. diff --git a/README.rst b/README.rst index f70ca4d15..0affb7238 100644 --- a/README.rst +++ b/README.rst @@ -32,7 +32,7 @@ Contributing ------------ We love contributions, so please feel free to fix bugs, improve things, provide documentation. Just `follow the -guidelines `_ and submit a PR. +guidelines `_ and submit a PR. Reporting security issues ------------------------- @@ -80,7 +80,7 @@ Notice that `oauth2_provider` namespace is mandatory. Documentation -------------- -The `full documentation `_ is on *Read the Docs*. +The `full documentation `_ is on *Read the Docs*. License ------- From fb5cda00dcfffc726257196139dfc5feef0ea6ae Mon Sep 17 00:00:00 2001 From: Paul Oswald Date: Wed, 1 Jun 2016 14:57:58 +0900 Subject: [PATCH 85/91] Define urls such that they are namespaced properly and forward-compatible with newer Django standards --- docs/tutorial/tutorial_02.rst | 52 ++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/docs/tutorial/tutorial_02.rst b/docs/tutorial/tutorial_02.rst index 7ea8d98dd..7b82e5264 100644 --- a/docs/tutorial/tutorial_02.rst +++ b/docs/tutorial/tutorial_02.rst @@ -35,33 +35,41 @@ URL this view will respond to: .. code-block:: python from django.conf.urls import patterns, url - from oauth2_provider import views + import oauth2_provider.views as oauth2_views from django.conf import settings from .views import ApiEndpoint - urlpatterns = patterns( - '', - url(r'^admin/', include(admin.site.urls)), - - # OAuth2 provider endpoints - url(r'^o/authorize/$', views.AuthorizationView.as_view(), name="authorize"), - url(r'^o/token/$', views.TokenView.as_view(), name="token"), - url(r'^o/revoke-token/$', views.RevokeTokenView.as_view(), name="revoke-token"), - - url(r'^api/hello', ApiEndpoint.as_view()), # a resource endpoint - ) + # OAuth2 provider endpoints + oauth2_endpoint_views = [ + url(r'^authorize/$', oauth2_views.AuthorizationView.as_view(), name="authorize"), + url(r'^token/$', oauth2_views.TokenView.as_view(), name="token"), + url(r'^revoke-token/$', oauth2_views.RevokeTokenView.as_view(), name="revoke-token"), + ] if settings.DEBUG: - # OAuth2 Application management views - - urlpatterns += patterns( - '', - url(r'^o/applications/$', views.ApplicationList.as_view(), name="application-list"), - url(r'^o/applications/register/$', views.ApplicationRegistration.as_view(), name="application-register"), - url(r'^o/applications/(?P\d+)/$', views.ApplicationDetail.as_view(), name="application-detail"), - url(r'^o/applications/(?P\d+)/delete/$', views.ApplicationDelete.as_view(), name="application-delete"), - url(r'^o/applications/(?P\d+)/update/$', views.ApplicationUpdate.as_view(), name="application-update"), - ) + # OAuth2 Application Management endpoints + oauth2_endpoint_views += [ + url(r'^applications/$', oauth2_views.ApplicationList.as_view(), name="list"), + url(r'^applications/register/$', oauth2_views.ApplicationRegistration.as_view(), name="register"), + url(r'^applications/(?P\d+)/$', oauth2_views.ApplicationDetail.as_view(), name="detail"), + url(r'^applications/(?P\d+)/delete/$', oauth2_views.ApplicationDelete.as_view(), name="delete"), + url(r'^applications/(?P\d+)/update/$', oauth2_views.ApplicationUpdate.as_view(), name="update"), + ] + + # OAuth2 Token Management endpoints + oauth2_endpoint_views += [ + url(r'^authorized-tokens/$', oauth2_views.AuthorizedTokensListView.as_view(), name="authorized-token-list"), + url(r'^authorized-tokens/(?P\d+)/delete/$', oauth2_views.AuthorizedTokenDeleteView.as_view(), + name="authorized-token-delete"), + ] + + urlpatterns = [ + # OAuth 2 endpoints: + url(r'^o/', include(oauth2_endpoint_views, namespace="oauth2_provider")), + + url(r'^admin/', include(admin.site.urls)), + url(r'^api/hello', ApiEndpoint.as_view()), # an example resource endpoint + ] You will probably want to write your own application views to deal with permissions and access control but the ones packaged with the library can get you started when developing the app. From da7e6ba0bde6d684b801343cfd6d9264104fd05c Mon Sep 17 00:00:00 2001 From: David Richfield Date: Sun, 5 Jun 2016 12:56:22 +0200 Subject: [PATCH 86/91] Typo: "beofre" -> "before" --- docs/tutorial/tutorial_01.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/tutorial_01.rst b/docs/tutorial/tutorial_01.rst index 89edbfce5..23304eb82 100644 --- a/docs/tutorial/tutorial_01.rst +++ b/docs/tutorial/tutorial_01.rst @@ -82,7 +82,7 @@ Point your browser to http://localhost:8000/o/applications/ and add an Applicati * `User`: the owner of the Application (e.g. a developer, or the currently logged in user.) - * `Redirect uris`: Applications must register at least one redirection endpoint beofre using the + * `Redirect uris`: Applications must register at least one redirection endpoint before using the authorization endpoint. The :term:`Authorization Server` will deliver the access token to the client only if the client specifies one of the verified redirection uris. For this tutorial, paste verbatim the value `http://django-oauth-toolkit.herokuapp.com/consumer/exchange/` From 6fcb8b52a5861440af55a28cfa07d1654562f99a Mon Sep 17 00:00:00 2001 From: Jens Timmerman Date: Wed, 29 Jun 2016 18:29:56 +0200 Subject: [PATCH 87/91] added an IsAuthenticatedOrTokenHasScope Permission --- .../ext/rest_framework/permissions.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/oauth2_provider/ext/rest_framework/permissions.py b/oauth2_provider/ext/rest_framework/permissions.py index 559bbbc54..5a0982d60 100644 --- a/oauth2_provider/ext/rest_framework/permissions.py +++ b/oauth2_provider/ext/rest_framework/permissions.py @@ -2,7 +2,7 @@ from django.core.exceptions import ImproperlyConfigured -from rest_framework.permissions import BasePermission +from rest_framework.permissions import BasePermission, IsAuthenticated from ...settings import oauth2_settings @@ -29,7 +29,7 @@ def has_permission(self, request, view): return token.is_valid(required_scopes) - assert False, ('TokenHasScope requires either the' + assert False, ('TokenHasScope requires the' '`oauth2_provider.rest_framework.OAuth2Authentication` authentication ' 'class to be used.') @@ -84,3 +84,16 @@ def get_scopes(self, request, view): ] return required_scopes + + +class IsAuthenticatedOrTokenHasScope(BasePermission): + """ + The user is authenticated using some backend or the token has the right scope + This is usefull when combined with the DjangoModelPermissions to allow people browse the browsable api's + if they log in using the a non token bassed middleware, + and let them access the api's using a rest client with a token + """ + def has_permission(self, request, view): + is_authenticated = IsAuthenticated() + token_has_scope = TokenHasScope() + return is_authenticated.has_permission(request, view) or token_has_scope.has_permission(request, view) From 9c86cfac3426bc17e0aac41e3bbcb08f1f711e6d Mon Sep 17 00:00:00 2001 From: Jens Timmerman Date: Wed, 29 Jun 2016 18:40:38 +0200 Subject: [PATCH 88/91] import permission into rest_framework --- oauth2_provider/ext/rest_framework/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/oauth2_provider/ext/rest_framework/__init__.py b/oauth2_provider/ext/rest_framework/__init__.py index bdc638818..4b826720c 100644 --- a/oauth2_provider/ext/rest_framework/__init__.py +++ b/oauth2_provider/ext/rest_framework/__init__.py @@ -1,3 +1,4 @@ # flake8: noqa from .authentication import OAuth2Authentication from .permissions import TokenHasScope, TokenHasReadWriteScope, TokenHasResourceScope +from .permissions import IsAuthenticatedOrTokenHasScope From 233498faaacd446aa169983488e4e83c99c90ccf Mon Sep 17 00:00:00 2001 From: Jens Timmerman Date: Thu, 30 Jun 2016 12:01:03 +0200 Subject: [PATCH 89/91] added tests, fixed an error the tests revealed --- .../ext/rest_framework/permissions.py | 12 ++++- oauth2_provider/tests/test_rest_framework.py | 52 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/oauth2_provider/ext/rest_framework/permissions.py b/oauth2_provider/ext/rest_framework/permissions.py index 5a0982d60..71b2ac91d 100644 --- a/oauth2_provider/ext/rest_framework/permissions.py +++ b/oauth2_provider/ext/rest_framework/permissions.py @@ -3,6 +3,7 @@ from django.core.exceptions import ImproperlyConfigured from rest_framework.permissions import BasePermission, IsAuthenticated +from .authentication import OAuth2Authentication from ...settings import oauth2_settings @@ -89,11 +90,18 @@ def get_scopes(self, request, view): class IsAuthenticatedOrTokenHasScope(BasePermission): """ The user is authenticated using some backend or the token has the right scope + This only returns True if the user is authenticated, but not using a token + or using a token, and the token has the correct scope. + This is usefull when combined with the DjangoModelPermissions to allow people browse the browsable api's if they log in using the a non token bassed middleware, and let them access the api's using a rest client with a token """ def has_permission(self, request, view): - is_authenticated = IsAuthenticated() + is_authenticated = IsAuthenticated().has_permission(request, view) + oauth2authenticated = False + if is_authenticated: + oauth2authenticated = isinstance(request.successful_authenticator, OAuth2Authentication) + token_has_scope = TokenHasScope() - return is_authenticated.has_permission(request, view) or token_has_scope.has_permission(request, view) + return (is_authenticated and not oauth2authenticated) or token_has_scope.has_permission(request, view) diff --git a/oauth2_provider/tests/test_rest_framework.py b/oauth2_provider/tests/test_rest_framework.py index 10ebdc403..a64f58cd1 100644 --- a/oauth2_provider/tests/test_rest_framework.py +++ b/oauth2_provider/tests/test_rest_framework.py @@ -19,7 +19,9 @@ try: from rest_framework import permissions from rest_framework.views import APIView + from rest_framework.test import force_authenticate, APIRequestFactory from ..ext.rest_framework import OAuth2Authentication, TokenHasScope, TokenHasReadWriteScope, TokenHasResourceScope + from ..ext.rest_framework import IsAuthenticatedOrTokenHasScope class MockView(APIView): permission_classes = (permissions.IsAuthenticated,) @@ -37,6 +39,10 @@ class ScopedView(OAuth2View): permission_classes = [permissions.IsAuthenticated, TokenHasScope] required_scopes = ['scope1'] + class AuthenticatedOrScopedView(OAuth2View): + permission_classes = [IsAuthenticatedOrTokenHasScope] + required_scopes = ['scope1'] + class ReadWriteScopedView(OAuth2View): permission_classes = [permissions.IsAuthenticated, TokenHasReadWriteScope] @@ -51,6 +57,7 @@ class ResourceScopedView(OAuth2View): 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()), + url(r'^oauth2-authenticated-or-scoped-test/$', AuthenticatedOrScopedView.as_view()), ) rest_framework_installed = True @@ -105,6 +112,24 @@ def test_authentication_denied(self): response = self.client.get("/oauth2-test/", HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 401) + @unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed') + def test_authentication_or_scope_denied(self): + # user is not authenticated + # not a correct token + auth = self._create_authorization_header("fake-token") + response = self.client.get("/oauth2-authenticated-or-scoped-test/", HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 401) + # token doesn't have correct scope + auth = self._create_authorization_header(self.access_token.token) + + factory = APIRequestFactory() + request = factory.get("/oauth2-authenticated-or-scoped-test/") + request.auth = auth + force_authenticate(request, token=self.access_token) + response = AuthenticatedOrScopedView.as_view()(request) + # authenticated but wrong scope, this is 403, not 401 + self.assertEqual(response.status_code, 403) + @unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed') def test_scoped_permission_allow(self): self.access_token.scope = 'scope1' @@ -114,6 +139,33 @@ def test_scoped_permission_allow(self): response = self.client.get("/oauth2-scoped-test/", HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) + @unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed') + def test_authenticated_or_scoped_permission_allow(self): + self.access_token.scope = 'scope1' + self.access_token.save() + # correct token and correct scope + auth = self._create_authorization_header(self.access_token.token) + response = self.client.get("/oauth2-authenticated-or-scoped-test/", HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 200) + + auth = self._create_authorization_header("fake-token") + # incorrect token but authenticated + factory = APIRequestFactory() + request = factory.get("/oauth2-authenticated-or-scoped-test/") + request.auth = auth + force_authenticate(request, self.test_user) + response = AuthenticatedOrScopedView.as_view()(request) + self.assertEqual(response.status_code, 200) + + # correct token but not authenticated + request = factory.get("/oauth2-authenticated-or-scoped-test/") + request.auth = auth + self.access_token.scope = 'scope1' + self.access_token.save() + force_authenticate(request, token=self.access_token) + response = AuthenticatedOrScopedView.as_view()(request) + self.assertEqual(response.status_code, 200) + @unittest.skipUnless(rest_framework_installed, 'djangorestframework not installed') def test_scoped_permission_deny(self): self.access_token.scope = 'scope2' From 46d136fb0d7529c45f915dbc465f0c50ab81c4c0 Mon Sep 17 00:00:00 2001 From: Jens Timmerman Date: Thu, 30 Jun 2016 18:05:36 +0200 Subject: [PATCH 90/91] added documentation, info to the readme, changelog and added myself to contributors --- AUTHORS | 1 + README.rst | 1 + docs/changelog.rst | 1 + docs/rest-framework/permissions.rst | 18 ++++++++++++++++++ 4 files changed, 21 insertions(+) diff --git a/AUTHORS b/AUTHORS index 3a91ba821..be13a5925 100644 --- a/AUTHORS +++ b/AUTHORS @@ -17,3 +17,4 @@ Diego Garcia Bas van Oostveen Bart Merenda Paul Oswald +Jens Timmerman diff --git a/README.rst b/README.rst index 0affb7238..8a204b4a4 100644 --- a/README.rst +++ b/README.rst @@ -100,6 +100,7 @@ Changelog Development ~~~~~~~~~~~ +* #396: added an IsAuthenticatedOrTokenHasScope Permission * #357: Support multiple-user clients by allowing User to be NULL for Applications 0.10.0 [2015-12-14] diff --git a/docs/changelog.rst b/docs/changelog.rst index 1b24e4dfe..95e5e3789 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Development ~~~~~~~~~~~ +* #396: added an IsAuthenticatedOrTokenHasScope Permission * #357: Support multiple-user clients by allowing User to be NULL for Applications diff --git a/docs/rest-framework/permissions.rst b/docs/rest-framework/permissions.rst index d22e8f4f7..629bf50d0 100644 --- a/docs/rest-framework/permissions.rst +++ b/docs/rest-framework/permissions.rst @@ -63,3 +63,21 @@ When the request's method is one of "non safe" methods, the access is allowed on required_scopes = ['music'] The `required_scopes` attribute is mandatory (you just need inform the resource scope). + + +IsAuthenticatedOrTokenHasScope +------------------------------ +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. +And also allows access to Authenticated users who are authenticated in django, but were not authenticated trought the OAuth2Authentication class. +This allows for protection of the api using scopes, but still let's users browse the full browseable API. +To restrict users to only browse the parts of the browseable API they should be allowed to see, you can combine this wwith the DjangoModelPermission or the DjangoObjectPermission. + +For example: + +.. code-block:: python + + class SongView(views.APIView): + permission_classes = [IsAuthenticatedOrTokenHasScope, DjangoModelPermission] + required_scopes = ['music'] + +The `required_scopes` attribute is mandatory. From 95ed1de39b5b29e515d100ea4beed22322f047dd Mon Sep 17 00:00:00 2001 From: Arief Bastian HB Date: Thu, 6 Oct 2016 16:24:51 +0700 Subject: [PATCH 91/91] missing on merge with upstream/master - mark compat_handlers to be unchecked by flex8 --- oauth2_provider/compat_handlers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/oauth2_provider/compat_handlers.py b/oauth2_provider/compat_handlers.py index 21859e80e..ce95a02eb 100644 --- a/oauth2_provider/compat_handlers.py +++ b/oauth2_provider/compat_handlers.py @@ -1,3 +1,4 @@ +# flake8: noqa # Django 1.9 drops the NullHandler since Python 2.7 includes it try: from logging import NullHandler