Browse Source

Fixed #30226 -- Added BaseBackend for authentication.

Tobias Bengfort 6 years ago
parent
commit
75337a6050

+ 19 - 2
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):
         """

+ 21 - 0
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

+ 3 - 0
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`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

+ 13 - 11
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:
 

+ 23 - 1
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."""