From 75337a60509fdfdd321a5caf8e30d57fff6b9518 Mon Sep 17 00:00:00 2001 From: Tobias Bengfort Date: Fri, 8 Mar 2019 17:48:50 +0100 Subject: [PATCH] Fixed #30226 -- Added BaseBackend for authentication. --- django/contrib/auth/backends.py | 21 +++++++++++++++++++-- docs/ref/contrib/auth.txt | 21 +++++++++++++++++++++ docs/releases/3.0.txt | 3 +++ docs/topics/auth/customizing.txt | 24 +++++++++++++----------- tests/auth_tests/test_auth_backends.py | 24 +++++++++++++++++++++++- 5 files changed, 79 insertions(+), 14 deletions(-) diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py index 7549d71273..d20987f8e1 100644 --- a/django/contrib/auth/backends.py +++ b/django/contrib/auth/backends.py @@ -8,7 +8,24 @@ from django.utils.deprecation import RemovedInDjango31Warning UserModel = get_user_model() -class ModelBackend: +class BaseBackend: + def authenticate(self, request, **kwargs): + return None + + def get_user(self, user_id): + return None + + def get_group_permissions(self, user_obj, obj=None): + return set() + + def get_all_permissions(self, user_obj, obj=None): + return self.get_group_permissions(user_obj, obj=obj) + + def has_perm(self, user_obj, perm, obj=None): + return perm in self.get_all_permissions(user_obj, obj=obj) + + +class ModelBackend(BaseBackend): """ Authenticates against settings.AUTH_USER_MODEL. """ @@ -86,7 +103,7 @@ class ModelBackend: return user_obj._perm_cache def has_perm(self, user_obj, perm, obj=None): - return user_obj.is_active and perm in self.get_all_permissions(user_obj, obj) + return user_obj.is_active and super().has_perm(user_obj, perm, obj=obj) def has_module_perms(self, user_obj, app_label): """ diff --git a/docs/ref/contrib/auth.txt b/docs/ref/contrib/auth.txt index 12186f19b7..17197e9fc7 100644 --- a/docs/ref/contrib/auth.txt +++ b/docs/ref/contrib/auth.txt @@ -460,6 +460,27 @@ Available authentication backends The following backends are available in :mod:`django.contrib.auth.backends`: +.. class:: BaseBackend + + .. versionadded:: 3.0 + + A base class that provides default implementations for all required + methods. By default, it will reject any user and provide no permissions. + + .. method:: get_group_permissions(user_obj, obj=None) + + Returns an empty set. + + .. method:: get_all_permissions(user_obj, obj=None) + + Uses :meth:`get_group_permissions` to get the set of permission strings + the ``user_obj`` has. + + .. method:: has_perm(user_obj, perm, obj=None) + + Uses :meth:`get_all_permissions` to check if ``user_obj`` has the + permission string ``perm``. + .. class:: ModelBackend This is the default authentication backend used by Django. It diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt index 14156af459..43eb07c78c 100644 --- a/docs/releases/3.0.txt +++ b/docs/releases/3.0.txt @@ -69,6 +69,9 @@ Minor features :class:`~django.contrib.auth.views.PasswordResetConfirmView` allows specifying a token parameter displayed as a component of password reset URLs. +* Added :class:`~django.contrib.auth.backends.BaseBackend` class to ease + customization of authentication backends. + :mod:`django.contrib.contenttypes` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index fd723cbb94..becf30e7e6 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -100,14 +100,18 @@ and returns a user object or ``None``. The ``authenticate`` method takes a ``request`` argument and credentials as keyword arguments. Most of the time, it'll just look like this:: - class MyBackend: + from django.contrib.auth.backends import BaseBackend + + class MyBackend(BaseBackend): def authenticate(self, request, username=None, password=None): # Check the username/password and return a user. ... But it could also authenticate a token, like so:: - class MyBackend: + from django.contrib.auth.backends import BaseBackend + + class MyBackend(BaseBackend): def authenticate(self, request, token=None): # Check the token and return a user. ... @@ -132,10 +136,11 @@ variable defined in your ``settings.py`` file and creates a Django ``User`` object the first time a user authenticates:: from django.conf import settings + from django.contrib.auth.backends import BaseBackend from django.contrib.auth.hashers import check_password from django.contrib.auth.models import User - class SettingsBackend: + class SettingsBackend(BaseBackend): """ Authenticate against the settings ADMIN_LOGIN and ADMIN_PASSWORD. @@ -190,11 +195,11 @@ exception in :meth:`~django.contrib.auth.models.User.has_perm()` or :meth:`~django.contrib.auth.models.User.has_module_perms()`, the authorization will immediately fail and Django won't check the backends that follow. -The simple backend above could implement permissions for the magic admin -fairly simply:: +A backend could implement permissions for the magic admin fairly simply:: - class SettingsBackend: - ... + from django.contrib.auth.backends import BaseBackend + + class MagicAdminBackend(BaseBackend): def has_perm(self, user_obj, perm, obj=None): return user_obj.username == settings.ADMIN_LOGIN @@ -205,10 +210,7 @@ all take the user object, which may be an anonymous user, as an argument. A full authorization implementation can be found in the ``ModelBackend`` class in :source:`django/contrib/auth/backends.py`, which is the default backend and -queries the ``auth_permission`` table most of the time. If you wish to provide -custom behavior for only part of the backend API, you can take advantage of -Python inheritance and subclass ``ModelBackend`` instead of implementing the -complete API in a custom backend. +queries the ``auth_permission`` table most of the time. .. _anonymous_auth: diff --git a/tests/auth_tests/test_auth_backends.py b/tests/auth_tests/test_auth_backends.py index 02f7d3ef27..84facd6c3a 100644 --- a/tests/auth_tests/test_auth_backends.py +++ b/tests/auth_tests/test_auth_backends.py @@ -4,7 +4,7 @@ from unittest import mock from django.contrib.auth import ( BACKEND_SESSION_KEY, SESSION_KEY, authenticate, get_user, signals, ) -from django.contrib.auth.backends import ModelBackend +from django.contrib.auth.backends import BaseBackend, ModelBackend from django.contrib.auth.hashers import MD5PasswordHasher from django.contrib.auth.models import AnonymousUser, Group, Permission, User from django.contrib.contenttypes.models import ContentType @@ -20,6 +20,28 @@ from .models import ( ) +class SimpleBackend(BaseBackend): + def get_group_permissions(self, user_obj, obj=None): + return ['group_perm'] + + +@override_settings(AUTHENTICATION_BACKENDS=['auth_tests.test_auth_backends.SimpleBackend']) +class BaseBackendTest(TestCase): + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user('test', 'test@example.com', 'test') + + def test_get_group_permissions(self): + self.assertEqual(self.user.get_group_permissions(), {'group_perm'}) + + def test_get_all_permissions(self): + self.assertEqual(self.user.get_all_permissions(), {'group_perm'}) + + def test_has_perm(self): + self.assertIs(self.user.has_perm('group_perm'), True) + self.assertIs(self.user.has_perm('other_perm', TestObj()), False) + + class CountingMD5PasswordHasher(MD5PasswordHasher): """Hasher that counts how many times it computes a hash."""