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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Contributors

Abhishek Patel
Alessandro De Angelis
Aleksander Vaskevich
Alan Crosswell
Anvesh Agarwal
Asif Saif Uddin
Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

<!--
<!--
## [unreleased]
### Added
### Changed
Expand All @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

* #898 Added the ability to customize classes for django admin

### Added
* #884 Added support for Python 3.9

Expand Down
24 changes: 24 additions & 0 deletions docs/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,30 @@ The import string of the class (model) representing your grants. Overwrite
this value if you wrote your own implementation (subclass of
``oauth2_provider.models.Grant``).

APPLICATION_ADMIN_CLASS
~~~~~~~~~~~~~~~~~
The import string of the class (model) representing your application admin class.
Overwrite this value if you wrote your own implementation (subclass of
``oauth2_provider.admin.ApplicationAdmin``).

ACCESS_TOKEN_ADMIN_CLASS
~~~~~~~~~~~~~~~~~
The import string of the class (model) representing your access token admin class.
Overwrite this value if you wrote your own implementation (subclass of
``oauth2_provider.admin.AccessTokenAdmin``).

GRANT_ADMIN_CLASS
~~~~~~~~~~~~~~~~~
The import string of the class (model) representing your grant admin class.
Overwrite this value if you wrote your own implementation (subclass of
``oauth2_provider.admin.GrantAdmin``).

REFRESH_TOKEN_ADMIN_CLASS
~~~~~~~~~~~~~~~~~
The import string of the class (model) representing your refresh token admin class.
Overwrite this value if you wrote your own implementation (subclass of
``oauth2_provider.admin.RefreshTokenAdmin``).

OAUTH2_SERVER_CLASS
~~~~~~~~~~~~~~~~~~~
The import string for the ``server_class`` (or ``oauthlib.oauth2.Server`` subclass)
Expand Down
42 changes: 28 additions & 14 deletions oauth2_provider/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
from django.contrib import admin

from .models import get_access_token_model, get_application_model, get_grant_model, get_refresh_token_model
from oauth2_provider.models import (
get_access_token_admin_class,
get_access_token_model,
get_application_admin_class,
get_application_model,
get_grant_admin_class,
get_grant_model,
get_refresh_token_admin_class,
get_refresh_token_model,
)


class ApplicationAdmin(admin.ModelAdmin):
Expand All @@ -13,27 +22,32 @@ class ApplicationAdmin(admin.ModelAdmin):
raw_id_fields = ("user",)


class GrantAdmin(admin.ModelAdmin):
list_display = ("code", "application", "user", "expires")
raw_id_fields = ("user",)


class AccessTokenAdmin(admin.ModelAdmin):
list_display = ("token", "user", "application", "expires")
raw_id_fields = ("user", "source_refresh_token")


class GrantAdmin(admin.ModelAdmin):
list_display = ("code", "application", "user", "expires")
raw_id_fields = ("user",)


class RefreshTokenAdmin(admin.ModelAdmin):
list_display = ("token", "user", "application")
raw_id_fields = ("user", "access_token")


Application = get_application_model()
Grant = get_grant_model()
AccessToken = get_access_token_model()
RefreshToken = get_refresh_token_model()
application_model = get_application_model()
access_token_model = get_access_token_model()
grant_model = get_grant_model()
refresh_token_model = get_refresh_token_model()

application_admin_class = get_application_admin_class()
access_token_admin_class = get_access_token_admin_class()
grant_admin_class = get_grant_admin_class()
refresh_token_admin_class = get_refresh_token_admin_class()

admin.site.register(Application, ApplicationAdmin)
admin.site.register(Grant, GrantAdmin)
admin.site.register(AccessToken, AccessTokenAdmin)
admin.site.register(RefreshToken, RefreshTokenAdmin)
admin.site.register(application_model, application_admin_class)
admin.site.register(access_token_model, access_token_admin_class)
admin.site.register(grant_model, grant_admin_class)
admin.site.register(refresh_token_model, refresh_token_admin_class)
24 changes: 24 additions & 0 deletions oauth2_provider/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,30 @@ def get_refresh_token_model():
return apps.get_model(oauth2_settings.REFRESH_TOKEN_MODEL)


def get_application_admin_class():
""" Return the Application admin class that is active in this project. """
application_admin_class = oauth2_settings.APPLICATION_ADMIN_CLASS
return application_admin_class


def get_access_token_admin_class():
""" Return the AccessToken admin class that is active in this project. """
access_token_admin_class = oauth2_settings.ACCESS_TOKEN_ADMIN_CLASS
return access_token_admin_class


def get_grant_admin_class():
""" Return the Grant admin class that is active in this project. """
grant_admin_class = oauth2_settings.GRANT_ADMIN_CLASS
return grant_admin_class


def get_refresh_token_admin_class():
""" Return the RefreshToken admin class that is active in this project. """
refresh_token_admin_class = oauth2_settings.REFRESH_TOKEN_ADMIN_CLASS
return refresh_token_admin_class


def clear_expired():
now = timezone.now()
refresh_expire_at = None
Expand Down
63 changes: 47 additions & 16 deletions oauth2_provider/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
OAuth2 Provider settings, checking for user settings first, then falling
back to the defaults.
"""
import importlib

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.test.signals import setting_changed
from django.utils.module_loading import import_string


USER_SETTINGS = getattr(settings, "OAUTH2_PROVIDER", None)
Expand Down Expand Up @@ -53,6 +54,10 @@
"ACCESS_TOKEN_MODEL": ACCESS_TOKEN_MODEL,
"GRANT_MODEL": GRANT_MODEL,
"REFRESH_TOKEN_MODEL": REFRESH_TOKEN_MODEL,
"APPLICATION_ADMIN_CLASS": "oauth2_provider.admin.ApplicationAdmin",
"ACCESS_TOKEN_ADMIN_CLASS": "oauth2_provider.admin.AccessTokenAdmin",
"GRANT_ADMIN_CLASS": "oauth2_provider.admin.GrantAdmin",
"REFRESH_TOKEN_ADMIN_CLASS": "oauth2_provider.admin.RefreshTokenAdmin",
"REQUEST_APPROVAL_PROMPT": "force",
"ALLOWED_REDIRECT_URI_SCHEMES": ["http", "https"],
# Special settings that will be evaluated at runtime
Expand Down Expand Up @@ -88,6 +93,10 @@
"OAUTH2_VALIDATOR_CLASS",
"OAUTH2_BACKEND_CLASS",
"SCOPES_BACKEND_CLASS",
"APPLICATION_ADMIN_CLASS",
"ACCESS_TOKEN_ADMIN_CLASS",
"GRANT_ADMIN_CLASS",
"REFRESH_TOKEN_ADMIN_CLASS",
)


Expand All @@ -96,23 +105,21 @@ def perform_import(val, setting_name):
If the given setting is a string import notation,
then perform the necessary import or imports.
"""
if isinstance(val, (list, tuple)):
return [import_from_string(item, setting_name) for item in val]
elif "." in val:
if val is None:
return None
elif isinstance(val, str):
return import_from_string(val, setting_name)
else:
raise ImproperlyConfigured("Bad value for %r: %r" % (setting_name, val))
elif isinstance(val, (list, tuple)):
return [import_from_string(item, setting_name) for item in val]
return val


def import_from_string(val, setting_name):
"""
Attempt to import a class from a string representation.
"""
try:
parts = val.split(".")
module_path, class_name = ".".join(parts[:-1]), parts[-1]
module = importlib.import_module(module_path)
return getattr(module, class_name)
return import_string(val)
except ImportError as e:
msg = "Could not import %r for setting %r. %s: %s." % (val, setting_name, e.__class__.__name__, e)
raise ImportError(msg)
Expand All @@ -127,14 +134,21 @@ class OAuth2ProviderSettings:
"""

def __init__(self, user_settings=None, defaults=None, import_strings=None, mandatory=None):
self.user_settings = user_settings or {}
self.defaults = defaults or {}
self.import_strings = import_strings or ()
self._user_settings = user_settings or {}
self.defaults = defaults or DEFAULTS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why you decided to pass DEFAULTS if we're not passing anything? This looks legit to me, tho I am wondering why it was {} and not DEFAULTS before

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that the library settings are very similar to the django-rest-framework settings.
My changes are inspired from there

This will allow tests to cover configuration errors. Previously, there were no tests for settings at all.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense, thanks!

self.import_strings = import_strings or IMPORT_STRINGS
self.mandatory = mandatory or ()
self._cached_attrs = set()

@property
def user_settings(self):
if not hasattr(self, "_user_settings"):
self._user_settings = getattr(settings, "OAUTH2_PROVIDER", {})
return self._user_settings

def __getattr__(self, attr):
if attr not in self.defaults.keys():
raise AttributeError("Invalid OAuth2Provider setting: %r" % (attr))
if attr not in self.defaults:
raise AttributeError("Invalid OAuth2Provider setting: %s" % attr)

try:
# Check if present in user settings
Expand Down Expand Up @@ -166,12 +180,13 @@ def __getattr__(self, attr):
self.validate_setting(attr, val)

# Cache the result
self._cached_attrs.add(attr)
setattr(self, attr, val)
return val

def validate_setting(self, attr, val):
if not val and attr in self.mandatory:
raise AttributeError("OAuth2Provider setting: %r is mandatory" % (attr))
raise AttributeError("OAuth2Provider setting: %s is mandatory" % attr)

@property
def server_kwargs(self):
Expand Down Expand Up @@ -199,5 +214,21 @@ def server_kwargs(self):
kwargs.update(self.EXTRA_SERVER_KWARGS)
return kwargs

def reload(self):
for attr in self._cached_attrs:
delattr(self, attr)
self._cached_attrs.clear()
if hasattr(self, "_user_settings"):
delattr(self, "_user_settings")


oauth2_settings = OAuth2ProviderSettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS, MANDATORY)


def reload_oauth2_settings(*args, **kwargs):
setting = kwargs["setting"]
if setting == "OAUTH2_PROVIDER":
oauth2_settings.reload()


setting_changed.connect(reload_oauth2_settings)
17 changes: 17 additions & 0 deletions tests/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.contrib import admin


class CustomApplicationAdmin(admin.ModelAdmin):
list_display = ("id",)


class CustomAccessTokenAdmin(admin.ModelAdmin):
list_display = ("id",)


class CustomGrantAdmin(admin.ModelAdmin):
list_display = ("id",)


class CustomRefreshTokenAdmin(admin.ModelAdmin):
list_display = ("id",)
90 changes: 90 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from django.test import TestCase
from django.test.utils import override_settings

from oauth2_provider.admin import (
get_access_token_admin_class,
get_application_admin_class,
get_grant_admin_class,
get_refresh_token_admin_class,
)
from oauth2_provider.settings import OAuth2ProviderSettings, oauth2_settings
from tests.admin import (
CustomAccessTokenAdmin,
CustomApplicationAdmin,
CustomGrantAdmin,
CustomRefreshTokenAdmin,
)


class TestAdminClass(TestCase):
def test_import_error_message_maintained(self):
"""
Make sure import errors are captured and raised sensibly.
"""
settings = OAuth2ProviderSettings({"CLIENT_ID_GENERATOR_CLASS": "invalid_module.InvalidClassName"})
with self.assertRaises(ImportError):
settings.CLIENT_ID_GENERATOR_CLASS

def test_get_application_admin_class(self):
"""
Test for getting class for application admin.
"""
application_admin_class = get_application_admin_class()
default_application_admin_class = oauth2_settings.APPLICATION_ADMIN_CLASS
assert application_admin_class == default_application_admin_class

def test_get_access_token_admin_class(self):
"""
Test for getting class for access token admin.
"""
access_token_admin_class = get_access_token_admin_class()
default_access_token_admin_class = oauth2_settings.ACCESS_TOKEN_ADMIN_CLASS
assert access_token_admin_class == default_access_token_admin_class

def test_get_grant_admin_class(self):
"""
Test for getting class for grant admin.
"""
grant_admin_class = get_grant_admin_class()
default_grant_admin_class = oauth2_settings.GRANT_ADMIN_CLASS
assert grant_admin_class, default_grant_admin_class

def test_get_refresh_token_admin_class(self):
"""
Test for getting class for refresh token admin.
"""
refresh_token_admin_class = get_refresh_token_admin_class()
default_refresh_token_admin_class = oauth2_settings.REFRESH_TOKEN_ADMIN_CLASS
assert refresh_token_admin_class == default_refresh_token_admin_class

@override_settings(OAUTH2_PROVIDER={"APPLICATION_ADMIN_CLASS": "tests.admin.CustomApplicationAdmin"})
def test_get_custom_application_admin_class(self):
"""
Test for getting custom class for application admin.
"""
application_admin_class = get_application_admin_class()
assert application_admin_class == CustomApplicationAdmin

@override_settings(OAUTH2_PROVIDER={"ACCESS_TOKEN_ADMIN_CLASS": "tests.admin.CustomAccessTokenAdmin"})
def test_get_custom_access_token_admin_class(self):
"""
Test for getting custom class for access token admin.
"""
access_token_admin_class = get_access_token_admin_class()
assert access_token_admin_class == CustomAccessTokenAdmin

@override_settings(OAUTH2_PROVIDER={"GRANT_ADMIN_CLASS": "tests.admin.CustomGrantAdmin"})
def test_get_custom_grant_admin_class(self):
"""
Test for getting custom class for grant admin.
"""
grant_admin_class = get_grant_admin_class()
assert grant_admin_class == CustomGrantAdmin

@override_settings(OAUTH2_PROVIDER={"REFRESH_TOKEN_ADMIN_CLASS": "tests.admin.CustomRefreshTokenAdmin"})
def test_get_custom_refresh_token_admin_class(self):
"""
Test for getting custom class for refresh token admin.
"""
refresh_token_admin_class = get_refresh_token_admin_class()
assert refresh_token_admin_class == CustomRefreshTokenAdmin