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 diff --git a/.travis.yml b/.travis.yml index bfbfb947d..8e318a6dc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,27 +1,25 @@ language: python -python: "2.7" +python: + - "3.5" + +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=py33-django15 - - TOX_ENV=py33-django16 - - TOX_ENV=py33-django17 + - TOX_ENV=py27-django19 + - TOX_ENV=py32-django18 - 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: + fast_finish: true + install: - - pip install tox + - pip install tox "virtualenv<14" - pip install coveralls script: diff --git a/AUTHORS b/AUTHORS index 8fa585f0d..be13a5925 100644 --- a/AUTHORS +++ b/AUTHORS @@ -13,3 +13,8 @@ David Fischer Ash Christopher Rodney Richardson Hiroki Kiyohara +Diego Garcia +Bas van Oostveen +Bart Merenda +Paul Oswald +Jens Timmerman 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 42dec11b6..8a204b4a4 100644 --- a/README.rst +++ b/README.rst @@ -32,13 +32,23 @@ 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 +------------------------- + +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 ------------ -* Python 2.6, 2.7, 3.3, 3.4 -* Django 1.4, 1.5, 1.6, 1.7, 1.8 +* Python 2.7, 3.2, 3.3, 3.4, 3.5 +* Django 1.7, 1.8, 1.9 Installation ------------ @@ -70,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 ------- @@ -87,6 +97,28 @@ Roadmap / Todo list (help wanted) 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] +~~~~~~~~~~~~~~~~~~~ + +* **#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 +* #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] ~~~~~~~~~~~~~~~~~~ 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/changelog.rst b/docs/changelog.rst index 5dc0cf9d7..95e5e3789 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,31 @@ 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] +------------------ + +* **#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 +* #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] ------------------ 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 ----------------------------------------------------- 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/index.rst b/docs/index.rst index 4aa62fffa..de2c0f8b1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,8 +21,8 @@ If you need support please send a message to the `Django OAuth Toolkit Google Gr Requirements ------------ -* Python 2.6, 2.7, 3.3, 3.4 -* Django 1.4, 1.5, 1.6, 1.7 +* Python 2.7, 3.2, 3.3, 3.4, 3.5 +* Django 1.7, 1.8, 1.9 Index ===== @@ -38,6 +38,7 @@ Index models advanced_topics settings + management_commands glossary .. toctree:: 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/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/rest-framework/permissions.rst b/docs/rest-framework/permissions.rst index 092d5809f..629bf50d0 100644 --- a/docs/rest-framework/permissions.rst +++ b/docs/rest-framework/permissions.rst @@ -48,3 +48,36 @@ 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). + + +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. diff --git a/docs/settings.rst b/docs/settings.rst index 6fc46da46..4999c1c77 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 @@ -71,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 ~~~~~~~~~~ @@ -81,6 +95,11 @@ 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. Check :ref:`cleartokens` management command for further info. + REQUEST_APPROVAL_PROMPT ~~~~~~~~~~~~~~~~~~~~~~~ Can be ``'force'`` or ``'auto'``. diff --git a/docs/tutorial/tutorial_01.rst b/docs/tutorial/tutorial_01.rst index c7c66873c..23304eb82 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 `_. @@ -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 the 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: +`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 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/` @@ -101,25 +101,25 @@ 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 +++++++++++++++++++++++++ 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 `. diff --git a/docs/tutorial/tutorial_02.rst b/docs/tutorial/tutorial_02.rst index 98fa08314..7b82e5264 100644 --- a/docs/tutorial/tutorial_02.rst +++ b/docs/tutorial/tutorial_02.rst @@ -34,14 +34,44 @@ URL this view will respond to: .. code-block:: python + from django.conf.urls import patterns, url + import oauth2_provider.views as oauth2_views + from django.conf import settings from .views import ApiEndpoint - urlpatterns = patterns( - '', + # 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 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'^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! - ) + 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. Since we inherit from `ProtectedResourceView`, we're done and our API is OAuth2 protected - for the sake of the lazy programmer. @@ -51,7 +81,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 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 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: :: 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 diff --git a/oauth2_provider/__init__.py b/oauth2_provider/__init__.py index 9ffed34b1..a13af8376 100644 --- a/oauth2_provider/__init__.py +++ b/oauth2_provider/__init__.py @@ -1,5 +1,7 @@ -__version__ = '0.9.0' +__version__ = '0.10.0' __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" 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 4266c341e..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,22 +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 diff --git a/oauth2_provider/compat_handlers.py b/oauth2_provider/compat_handlers.py new file mode 100644 index 000000000..ce95a02eb --- /dev/null +++ b/oauth2_provider/compat_handlers.py @@ -0,0 +1,6 @@ +# flake8: noqa +# 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/ext/rest_framework/__init__.py b/oauth2_provider/ext/rest_framework/__init__.py index 24cbda12b..4b826720c 100644 --- a/oauth2_provider/ext/rest_framework/__init__.py +++ b/oauth2_provider/ext/rest_framework/__init__.py @@ -1,2 +1,4 @@ +# flake8: noqa from .authentication import OAuth2Authentication -from .permissions import TokenHasScope, TokenHasReadWriteScope +from .permissions import TokenHasScope, TokenHasReadWriteScope, TokenHasResourceScope +from .permissions import IsAuthenticatedOrTokenHasScope diff --git a/oauth2_provider/ext/rest_framework/permissions.py b/oauth2_provider/ext/rest_framework/permissions.py index e60415db0..71b2ac91d 100644 --- a/oauth2_provider/ext/rest_framework/permissions.py +++ b/oauth2_provider/ext/rest_framework/permissions.py @@ -2,7 +2,8 @@ from django.core.exceptions import ImproperlyConfigured -from rest_framework.permissions import BasePermission +from rest_framework.permissions import BasePermission, IsAuthenticated +from .authentication import OAuth2Authentication from ...settings import oauth2_settings @@ -29,7 +30,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.') @@ -59,3 +60,48 @@ 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 + + +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().has_permission(request, view) + oauth2authenticated = False + if is_authenticated: + oauth2authenticated = isinstance(request.successful_authenticator, OAuth2Authentication) + + token_has_scope = TokenHasScope() + return (is_authenticated and not oauth2authenticated) or token_has_scope.has_permission(request, view) diff --git a/oauth2_provider/forms.py b/oauth2_provider/forms.py index a8b398554..a2b4d8f1c 100644 --- a/oauth2_provider/forms.py +++ b/oauth2_provider/forms.py @@ -1,12 +1,10 @@ from django import forms -from .models import Application - 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()) @@ -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/south_migrations/__init__.py b/oauth2_provider/management/__init__.py similarity index 100% rename from oauth2_provider/south_migrations/__init__.py rename to oauth2_provider/management/__init__.py 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..48f70b822 --- /dev/null +++ b/oauth2_provider/management/commands/cleartokens.py @@ -0,0 +1,9 @@ +from django.core.management.base import BaseCommand +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/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( diff --git a/oauth2_provider/migrations/0003_auto_20150909_1131.py b/oauth2_provider/migrations/0003_auto_20150909_1131.py deleted file mode 100644 index 75a2db3e1..000000000 --- a/oauth2_provider/migrations/0003_auto_20150909_1131.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -from django.conf import settings - - -class Migration(migrations.Migration): - - dependencies = [ - ('oauth2_provider', '0002_08_updates'), - ] - - operations = [ - migrations.AlterField( - model_name='accesstoken', - name='application', - field=models.ForeignKey(related_name='accesstoken_set', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL), - ), - migrations.AlterField( - model_name='accesstoken', - name='user', - field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, null=True, related_name='accesstoken_set'), - ), - migrations.AlterField( - model_name='refreshtoken', - name='access_token', - field=models.OneToOneField(to=settings.OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL, related_name='refresh_token'), - ), - migrations.AlterField( - model_name='refreshtoken', - name='application', - field=models.ForeignKey(related_name='refreshtoken_set', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL), - ), - migrations.AlterField( - model_name='refreshtoken', - name='user', - field=models.ForeignKey(related_name='refreshtoken_set', to=settings.AUTH_USER_MODEL), - ), - ] 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), + ), + ] diff --git a/oauth2_provider/migrations/0004_auto_20161006_0253.py b/oauth2_provider/migrations/0004_auto_20161006_0253.py new file mode 100644 index 000000000..7fc6f8fc4 --- /dev/null +++ b/oauth2_provider/migrations/0004_auto_20161006_0253.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + +from ..settings import oauth2_settings + + +class Migration(migrations.Migration): + dependencies = [ + ('oauth2_provider', '0003_auto_20160316_1503'), + ] + + operations = [ + migrations.AlterField( + model_name='AccessToken', + name='application', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accesstoken_set', + to=oauth2_settings.APPLICATION_MODEL), + ), + migrations.AlterField( + model_name='AccessToken', + name='user', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, + related_name='accesstoken_set', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='RefreshToken', + name='application', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='refreshtoken_set', + to=oauth2_settings.APPLICATION_MODEL), + ), + migrations.AlterField( + model_name='RefreshToken', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='refreshtoken_set', + to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='RefreshToken', + name='access_token', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='refresh_token', + to=oauth2_settings.ACCESS_TOKEN_MODEL), + ), + ] diff --git a/oauth2_provider/models.py b/oauth2_provider/models.py index beb972099..1513b9005 100644 --- a/oauth2_provider/models.py +++ b/oauth2_provider/models.py @@ -1,7 +1,11 @@ from __future__ import unicode_literals +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 +from django.db import models, transaction from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -9,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 @@ -55,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(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) @@ -122,10 +128,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 @@ -144,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() @@ -155,6 +159,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): @@ -165,7 +172,7 @@ def __str__(self): class AbstractAccessToken(models.Model): - user = models.ForeignKey(AUTH_USER_MODEL, related_name='accesstoken_set', blank=True, null=True) + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='accesstoken_set', blank=True, null=True) application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, related_name='accesstoken_set') expires = models.DateTimeField() scope = models.TextField(blank=True) @@ -185,6 +192,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): @@ -208,6 +218,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()} + @python_2_unicode_compatible class AccessToken(AbstractAccessToken): @@ -220,8 +237,7 @@ class AccessToken(AbstractAccessToken): * :attr:`user` The Django user representing resources' owner * :attr:`token` Access token * :attr:`application` Application instance - * :attr:`expires` Expire time in seconds, defaults to - :data:`settings.ACCESS_TOKEN_EXPIRE_SECONDS` + * :attr:`expires` Date and time of token expiration, in DateTime format * :attr:`scope` Allowed scopes """ token = models.CharField(max_length=255, db_index=True) @@ -229,12 +245,12 @@ class AccessToken(AbstractAccessToken): def __str__(self): return self.token -# Add swappable like this to not break django 1.4 compatibility -AccessToken._meta.swappable = 'OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL' + class Meta(AbstractAccessToken.Meta): + swappable = 'OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL' class AbstractRefreshToken(models.Model): - user = models.ForeignKey(AUTH_USER_MODEL, related_name='refreshtoken_set') + user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='refreshtoken_set') application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, related_name='refreshtoken_set') access_token = models.OneToOneField(oauth2_settings.ACCESS_TOKEN_MODEL, related_name='refresh_token') @@ -269,8 +285,8 @@ class RefreshToken(AbstractRefreshToken): def __str__(self): return self.token -# Add swappable like this to not break django 1.4 compatibility -RefreshToken._meta.swappable = 'OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL' + class Meta(AbstractRefreshToken.Meta): + swappable = 'OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL' def get_application_model(): @@ -280,7 +296,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)) @@ -294,7 +310,7 @@ def get_access_token_model(): except ValueError: e = "ACCESS_TOKEN_MODEL must be of the form 'app_label.model_name'" raise ImproperlyConfigured(e) - access_token_model = get_model(app_label, model_name) + access_token_model = apps.get_model(app_label, model_name) if access_token_model is None: e = "ACCESS_TOKEN_MODEL refers to model {0} that has not been installed" raise ImproperlyConfigured(e.format(oauth2_settings.ACCESS_TOKEN_MODEL)) @@ -308,8 +324,29 @@ def get_refresh_token_model(): except ValueError: e = "REFRESH_TOKEN_MODEL must be of the form 'app_label.model_name'" raise ImproperlyConfigured(e) - refresh_token_model = get_model(app_label, model_name) + refresh_token_model = apps.get_model(app_label, model_name) if refresh_token_model is None: e = "REFRESH_TOKEN_MODEL refers to model {0} that has not been installed" raise ImproperlyConfigured(e.format(oauth2_settings.REFRESH_TOKEN_MODEL)) return refresh_token_model + + +def clear_expired(): + 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_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() diff --git a/oauth2_provider/oauth2_backends.py b/oauth2_provider/oauth2_backends.py index 4c7e40a84..6fbdc82cd 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): """ @@ -32,6 +32,17 @@ 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 +132,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 @@ -179,7 +191,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/oauth2_validators.py b/oauth2_provider/oauth2_validators.py index 97a6c04f9..725573b64 100644 --- a/oauth2_provider/oauth2_validators.py +++ b/oauth2_provider/oauth2_validators.py @@ -1,11 +1,13 @@ from __future__ import unicode_literals +import six import base64 import binascii import logging 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 @@ -56,10 +58,15 @@ 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' + # 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): @@ -275,7 +282,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 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 e0a37b937..240d00860 100644 --- a/oauth2_provider/settings.py +++ b/oauth2_provider/settings.py @@ -20,26 +20,29 @@ import six from django.conf import settings +from django.core.exceptions import ImproperlyConfigured try: # Available in Python 2.7+ import importlib except ImportError: from django.utils import importlib - USER_SETTINGS = getattr(settings, 'OAUTH2_PROVIDER', None) DEFAULTS = { '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"}, + 'DEFAULT_SCOPES': ['__all__'], 'READ_SCOPE': 'read', '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'), 'ACCESS_TOKEN_MODEL': getattr(settings, 'OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL', 'oauth2_provider.AccessToken'), 'REFRESH_TOKEN_MODEL': getattr(settings, 'OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL', 'oauth2_provider.RefreshToken'), @@ -48,12 +51,14 @@ # Special settings that will be evaluated at runtime '_SCOPES': [], + '_DEFAULT_SCOPES': [], } # List of settings that cannot be empty MANDATORY = ( 'CLIENT_ID_GENERATOR_CLASS', 'CLIENT_SECRET_GENERATOR_CLASS', + 'OAUTH2_SERVER_CLASS', 'OAUTH2_VALIDATOR_CLASS', 'OAUTH2_BACKEND_CLASS', 'SCOPES', @@ -64,6 +69,7 @@ IMPORT_STRINGS = ( 'CLIENT_ID_GENERATOR_CLASS', 'CLIENT_SECRET_GENERATOR_CLASS', + 'OAUTH2_SERVER_CLASS', 'OAUTH2_VALIDATOR_CLASS', 'OAUTH2_BACKEND_CLASS', ) @@ -127,6 +133,18 @@ def __getattr__(self, attr): # Overriding special settings if attr == '_SCOPES': val = list(six.iterkeys(self.SCOPES)) + if attr == '_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) 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/templates/oauth2_provider/application_confirm_delete.html b/oauth2_provider/templates/oauth2_provider/application_confirm_delete.html index 1651f5177..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 future %} {% 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 36eb583a1..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 future %} {% 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 baa81a1b5..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 future %} {% 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 c50049308..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 future %} {% 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 077e2315d..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 future %} {% 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-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..2c6a028a8 --- /dev/null +++ b/oauth2_provider/templates/oauth2_provider/authorized-tokens.html @@ -0,0 +1,23 @@ +{% extends "oauth2_provider/base.html" %} + +{% load i18n %} +{% 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/settings.py b/oauth2_provider/tests/settings.py index 47ffae0a2..a615ea818 100644 --- a/oauth2_provider/tests/settings.py +++ b/oauth2_provider/tests/settings.py @@ -1,7 +1,4 @@ -import os - DEBUG = True -TEMPLATE_DEBUG = DEBUG ADMINS = () @@ -42,11 +39,6 @@ # Make this unique, and don't share it with anybody. SECRET_KEY = "1234567890evonove" -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -) - MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -57,7 +49,29 @@ ROOT_URLCONF = 'oauth2_provider.tests.urls' -TEMPLATE_DIRS = () +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ + # insert your TEMPLATE_DIRS here + ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + # Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this + # list if you haven't customized them: + 'django.contrib.auth.context_processors.auth', + 'django.template.context_processors.debug', + 'django.template.context_processors.i18n', + 'django.template.context_processors.media', + 'django.template.context_processors.static', + 'django.template.context_processors.tz', + 'django.contrib.messages.context_processors.messages', + ], + 'debug': True, + }, + }, +] INSTALLED_APPS = ( 'django.contrib.auth', @@ -100,7 +114,7 @@ }, 'null': { 'level': 'DEBUG', - 'class': 'django.utils.log.NullHandler', + 'class': 'oauth2_provider.compat_handlers.NullHandler', }, }, 'loggers': { @@ -120,11 +134,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' diff --git a/oauth2_provider/tests/test_application_views.py b/oauth2_provider/tests/test_application_views.py index 32495ca27..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() @@ -21,6 +22,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/tests/test_auth_backends.py b/oauth2_provider/tests/test_auth_backends.py index d44f3af41..41a628b9d 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 get_access_token_model from ..backends import OAuth2Backend @@ -15,6 +15,7 @@ ApplicationModel = get_application_model() AccessToken = get_access_token_model() + class BaseTest(TestCase): """ Base class for cases in this module @@ -76,7 +77,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): diff --git a/oauth2_provider/tests/test_authorization_code.py b/oauth2_provider/tests/test_authorization_code.py index a25a38e58..58ec7eb67 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, get_access_token_model, get_refresh_token_model from ..settings import oauth2_settings from ..views import ProtectedResourceView @@ -19,7 +20,7 @@ Application = get_application_model() AccessToken = get_access_token_model() -RefreshToken = get_access_token_model() +RefreshToken = get_refresh_token_model() UserModel = get_user_model() @@ -47,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() @@ -1005,3 +1007,33 @@ 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_default_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) diff --git a/oauth2_provider/tests/test_client_credential.py b/oauth2_provider/tests/test_client_credential.py index 7a8d00a0f..2eb8c8c24 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 @@ -49,6 +49,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_decorators.py b/oauth2_provider/tests/test_decorators.py index 5a0339c1d..30c03788a 100644 --- a/oauth2_provider/tests/test_decorators.py +++ b/oauth2_provider/tests/test_decorators.py @@ -1,13 +1,12 @@ -import json 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, get_access_token_model -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 9293616d7..25493ec61 100644 --- a/oauth2_provider/tests/test_implicit.py +++ b/oauth2_provider/tests/test_implicit.py @@ -1,14 +1,13 @@ from __future__ import unicode_literals -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 ..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, AuthorizationView +from ..views import ProtectedResourceView Application = get_application_model() @@ -37,6 +36,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 +45,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 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 05538b649..3a1f93da2 100644 --- a/oauth2_provider/tests/test_models.py +++ b/oauth2_provider/tests/test_models.py @@ -1,18 +1,13 @@ from __future__ import unicode_literals -try: - from unittest import skipIf -except ImportError: - from django.utils.unittest.case 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, get_access_token_model, get_refresh_token_model -from ..compat import get_user_model Application = get_application_model() @@ -83,8 +78,37 @@ 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') class TestCustomApplicationModel(TestCase): def setUp(self): @@ -100,7 +124,12 @@ def test_related_objects(self): # Django internals caches the related objects. if django.VERSION < (1, 8): del UserModel._meta._related_objects_cache - related_object_names = [ro.name for ro in UserModel._meta.get_all_related_objects()] + all_related_objects = UserModel._meta.get_all_related_objects() if django.VERSION < (1, 10) else [ + f for f in UserModel._meta.get_fields() + if (f.one_to_many or f.one_to_one) + and f.auto_created and not f.concrete + ] + related_object_names = [ro.name for ro in all_related_objects] self.assertNotIn('oauth2_provider:application', related_object_names) self.assertIn('tests%stestapplication' % (':' if django.VERSION < (1, 8) else '_'), related_object_names) @@ -112,6 +141,11 @@ 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.assertTrue(grant.is_expired()) + class TestAccessTokenModel(TestCase): def setUp(self): @@ -132,6 +166,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.assertTrue(access_token.is_expired()) + class TestRefreshTokenModel(TestCase): diff --git a/oauth2_provider/tests/test_oauth2_backends.py b/oauth2_provider/tests/test_oauth2_backends.py index bab4891eb..5203e09cc 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 @@ -7,10 +8,16 @@ 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") @@ -34,6 +41,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() diff --git a/oauth2_provider/tests/test_oauth2_validators.py b/oauth2_provider/tests/test_oauth2_validators.py index b18e4decf..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() @@ -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 diff --git a/oauth2_provider/tests/test_password.py b/oauth2_provider/tests/test_password.py index e1d250f6f..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 @@ -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_rest_framework.py b/oauth2_provider/tests/test_rest_framework.py index d2767611c..951f17b36 100644 --- a/oauth2_provider/tests/test_rest_framework.py +++ b/oauth2_provider/tests/test_rest_framework.py @@ -1,15 +1,16 @@ +import unittest from datetime import timedelta -from django.conf.urls import patterns, url, include +from django.conf.urls import 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, unittest - +from django.test.utils import override_settings +from django.utils import timezone from .test_utils import TestCaseUtils from ..models import get_access_token_model, get_application_model from ..settings import oauth2_settings -from ..compat import get_user_model Application = get_application_model() @@ -20,7 +21,9 @@ try: from rest_framework import permissions from rest_framework.views import APIView - from ..ext.rest_framework import OAuth2Authentication, TokenHasScope, TokenHasReadWriteScope + 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,) @@ -38,16 +41,25 @@ 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] - urlpatterns = patterns( - '', + class ResourceScopedView(OAuth2View): + permission_classes = [permissions.IsAuthenticated, TokenHasResourceScope] + required_scopes = ['resource1'] + + urlpatterns = [ 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()), + url(r'^oauth2-authenticated-or-scoped-test/$', AuthenticatedOrScopedView.as_view()), + ] rest_framework_installed = True except ImportError: @@ -61,11 +73,11 @@ class BaseTest(TestCaseUtils, TestCase): pass +@override_settings(ROOT_URLCONF=__name__) 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") @@ -101,6 +113,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' @@ -110,6 +140,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' @@ -154,3 +211,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) diff --git a/oauth2_provider/tests/test_scopes.py b/oauth2_provider/tests/test_scopes.py index 6fd7ff109..8bbb0c2a8 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, get_access_token_model from ..settings import oauth2_settings from ..views import ScopedProtectedResourceView, ReadWriteScopedResourceView + Application = get_application_model() AccessToken = get_access_token_model() UserModel = get_user_model() @@ -69,6 +71,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): """ diff --git a/oauth2_provider/tests/test_token_revocation.py b/oauth2_provider/tests/test_token_revocation.py index c9e7c2b4f..6d64082cc 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, get_access_token_model, get_refresh_token_model from ..settings import oauth2_settings diff --git a/oauth2_provider/tests/test_token_view.py b/oauth2_provider/tests/test_token_view.py new file mode 100644 index 000000000..30c3fa020 --- /dev/null +++ b/oauth2_provider/tests/test_token_view.py @@ -0,0 +1,179 @@ +from __future__ import unicode_literals + +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 + + +Application = get_application_model() +UserModel = get_user_model() + + +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") + + 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() + + +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. + """ + 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) + 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) + + +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. + """ + 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/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')), ) diff --git a/oauth2_provider/urls.py b/oauth2_provider/urls.py index 2fff96c60..ebcb9e0b6 100644 --- a/oauth2_provider/urls.py +++ b/oauth2_provider/urls.py @@ -1,21 +1,25 @@ 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"), 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..4f444f55d 100644 --- a/oauth2_provider/views/__init__.py +++ b/oauth2_provider/views/__init__.py @@ -1,4 +1,6 @@ +# flake8: noqa from .base import AuthorizationView, TokenView, RevokeTokenView 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 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) 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 diff --git a/oauth2_provider/views/token.py b/oauth2_provider/views/token.py new file mode 100644 index 000000000..f7e4562e9 --- /dev/null +++ b/oauth2_provider/views/token.py @@ -0,0 +1,36 @@ +from __future__ import absolute_import, unicode_literals + +from django.core.urlresolvers import reverse_lazy +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' + success_url = reverse_lazy('oauth2_provider:authorized-token-list') + model = AccessToken + + def get_queryset(self): + return super(AuthorizedTokenDeleteView, self).get_queryset().filter(user=self.request.user) diff --git a/requirements/base.txt b/requirements/base.txt index 1dd139d74..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 -django-braces==1.4.0 +Sphinx==1.3.3 +oauthlib==1.0.3 +django-braces==1.8.1 six 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 f95801893..d444c6358 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -1,3 +1,6 @@ -r optional.txt -coverage -mock +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..852de9c22 --- a/runtests.py +++ b/runtests.py @@ -1,10 +1,8 @@ #!/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]) +# sys.exit() is required otherwise the wrapper exits +# with exit code 0, regardless the pytest.main() execution +sys.exit(pytest.main()) 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, diff --git a/tox.ini b/tox.ini index 38ad106e5..cd372938f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,119 +1,24 @@ +[pytest] +DJANGO_SETTINGS_MODULE=oauth2_provider.tests.settings +testpaths=oauth2_provider + [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}-django{18,19}, + {py32}-django{18}, + {py33}-django{18}, + {py34}-django{18,19}, + {py35}-django{18,19}, docs, flake8 [testenv] -downloadcache = {toxworkdir}/cache/ -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 = - Django<1.8 - {[testenv]deps} - -[testenv:py27-django18] -basepython = python2.7 -deps = - Django<1.9 - {[testenv]deps} - -[testenv:py33-django15] -basepython = python3.3 -deps = - Django<1.6 - django-discover-runner - {[testenv]deps} - -[testenv:py33-django16] -basepython = python3.3 -deps = - Django<1.7 - {[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-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 = - Django<1.8 - {[testenv]deps} - -[testenv:py34-django18] -basepython = python3.4 +commands=python runtests.py -q --cov oauth2_provider --cov-report= --cov-append deps = - Django<1.9 - {[testenv]deps} + django18: Django==1.8.11 + django19: Django==1.9.4 + coverage<4 + -rrequirements/testing.txt [testenv:docs] basepython=python @@ -131,5 +36,4 @@ commands = [flake8] max-line-length = 120 -ignore = F403,F401 exclude = docs,migrations,south_migrations,.tox