mirror of https://github.com/django/django.git
Fixed #18763 -- Added ModelBackend/UserManager.with_perm() methods.
Co-authored-by: Nick Pope <nick.pope@flightdataservices.com>
This commit is contained in:
parent
fa7ffc6cb3
commit
400ec5125e
|
@ -3,6 +3,7 @@ import warnings
|
|||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.utils.deprecation import RemovedInDjango31Warning
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
@ -119,6 +120,42 @@ class ModelBackend(BaseBackend):
|
|||
for perm in self.get_all_permissions(user_obj)
|
||||
)
|
||||
|
||||
def with_perm(self, perm, is_active=True, include_superusers=True, obj=None):
|
||||
"""
|
||||
Return users that have permission "perm". By default, filter out
|
||||
inactive users and include superusers.
|
||||
"""
|
||||
if isinstance(perm, str):
|
||||
try:
|
||||
app_label, codename = perm.split('.')
|
||||
except ValueError:
|
||||
raise ValueError(
|
||||
'Permission name should be in the form '
|
||||
'app_label.permission_codename.'
|
||||
)
|
||||
elif not isinstance(perm, Permission):
|
||||
raise TypeError(
|
||||
'The `perm` argument must be a string or a permission instance.'
|
||||
)
|
||||
|
||||
UserModel = get_user_model()
|
||||
if obj is not None:
|
||||
return UserModel._default_manager.none()
|
||||
|
||||
permission_q = Q(group__user=OuterRef('pk')) | Q(user=OuterRef('pk'))
|
||||
if isinstance(perm, Permission):
|
||||
permission_q &= Q(pk=perm.pk)
|
||||
else:
|
||||
permission_q &= Q(codename=codename, content_type__app_label=app_label)
|
||||
|
||||
user_q = Exists(Permission.objects.filter(permission_q))
|
||||
if include_superusers:
|
||||
user_q |= Q(is_superuser=True)
|
||||
if is_active is not None:
|
||||
user_q &= Q(is_active=is_active)
|
||||
|
||||
return UserModel._default_manager.filter(user_q)
|
||||
|
||||
def get_user(self, user_id):
|
||||
try:
|
||||
user = UserModel._default_manager.get(pk=user_id)
|
||||
|
|
|
@ -157,6 +157,32 @@ class UserManager(BaseUserManager):
|
|||
|
||||
return self._create_user(username, email, password, **extra_fields)
|
||||
|
||||
def with_perm(self, perm, is_active=True, include_superusers=True, backend=None, obj=None):
|
||||
if backend is None:
|
||||
backends = auth._get_backends(return_tuples=True)
|
||||
if len(backends) == 1:
|
||||
backend, _ = backends[0]
|
||||
else:
|
||||
raise ValueError(
|
||||
'You have multiple authentication backends configured and '
|
||||
'therefore must provide the `backend` argument.'
|
||||
)
|
||||
elif not isinstance(backend, str):
|
||||
raise TypeError(
|
||||
'backend must be a dotted import path string (got %r).'
|
||||
% backend
|
||||
)
|
||||
else:
|
||||
backend = auth.load_backend(backend)
|
||||
if hasattr(backend, 'with_perm'):
|
||||
return backend.with_perm(
|
||||
perm,
|
||||
is_active=is_active,
|
||||
include_superusers=include_superusers,
|
||||
obj=obj,
|
||||
)
|
||||
return self.none()
|
||||
|
||||
|
||||
# A few helper functions for common logic between User and AnonymousUser.
|
||||
def _user_get_permissions(user, obj, from_name):
|
||||
|
|
|
@ -291,6 +291,28 @@ Manager methods
|
|||
|
||||
The ``email`` and ``password`` parameters were made optional.
|
||||
|
||||
.. method:: with_perm(perm, is_active=True, include_superusers=True, backend=None, obj=None)
|
||||
|
||||
.. versionadded:: 3.0
|
||||
|
||||
Returns users that have the given permission ``perm`` either in the
|
||||
``"<app label>.<permission codename>"`` format or as a
|
||||
:class:`~django.contrib.auth.models.Permission` instance. Returns an
|
||||
empty queryset if no users who have the ``perm`` found.
|
||||
|
||||
If ``is_active`` is ``True`` (default), returns only active users, or
|
||||
if ``False``, returns only inactive users. Use ``None`` to return all
|
||||
users irrespective of active state.
|
||||
|
||||
If ``include_superusers`` is ``True`` (default), the result will
|
||||
include superusers.
|
||||
|
||||
If ``backend`` is passed in and it's defined in
|
||||
:setting:`AUTHENTICATION_BACKENDS`, then this method will use it.
|
||||
Otherwise, it will use the ``backend`` in
|
||||
:setting:`AUTHENTICATION_BACKENDS`, if there is only one, or raise an
|
||||
exception.
|
||||
|
||||
``AnonymousUser`` object
|
||||
========================
|
||||
|
||||
|
@ -520,6 +542,9 @@ The following backends are available in :mod:`django.contrib.auth.backends`:
|
|||
implement them other than returning an empty set of permissions if
|
||||
``obj is not None``.
|
||||
|
||||
:meth:`with_perm` also allows an object to be passed as a parameter, but
|
||||
unlike others methods it returns an empty queryset if ``obj is not None``.
|
||||
|
||||
.. method:: authenticate(request, username=None, password=None, **kwargs)
|
||||
|
||||
Tries to authenticate ``username`` with ``password`` by calling
|
||||
|
@ -577,6 +602,22 @@ The following backends are available in :mod:`django.contrib.auth.backends`:
|
|||
don't have an :attr:`~django.contrib.auth.models.CustomUser.is_active`
|
||||
field are allowed.
|
||||
|
||||
.. method:: with_perm(perm, is_active=True, include_superusers=True, obj=None)
|
||||
|
||||
.. versionadded:: 3.0
|
||||
|
||||
Returns all active users who have the permission ``perm`` either in
|
||||
the form of ``"<app label>.<permission codename>"`` or a
|
||||
:class:`~django.contrib.auth.models.Permission` instance. Returns an
|
||||
empty queryset if no users who have the ``perm`` found.
|
||||
|
||||
If ``is_active`` is ``True`` (default), returns only active users, or
|
||||
if ``False``, returns only inactive users. Use ``None`` to return all
|
||||
users irrespective of active state.
|
||||
|
||||
If ``include_superusers`` is ``True`` (default), the result will
|
||||
include superusers.
|
||||
|
||||
.. class:: AllowAllUsersModelBackend
|
||||
|
||||
Same as :class:`ModelBackend` except that it doesn't reject inactive users
|
||||
|
|
|
@ -128,6 +128,9 @@ Minor features
|
|||
* :attr:`~django.contrib.auth.models.CustomUser.REQUIRED_FIELDS` now supports
|
||||
:class:`~django.db.models.ManyToManyField`\s.
|
||||
|
||||
* The new :meth:`.UserManager.with_perm` method returns users that have the
|
||||
specified permission.
|
||||
|
||||
:mod:`django.contrib.contenttypes`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -179,12 +179,13 @@ Handling authorization in custom backends
|
|||
|
||||
Custom auth backends can provide their own permissions.
|
||||
|
||||
The user model will delegate permission lookup functions
|
||||
The user model and its manager will delegate permission lookup functions
|
||||
(:meth:`~django.contrib.auth.models.User.get_user_permissions()`,
|
||||
:meth:`~django.contrib.auth.models.User.get_group_permissions()`,
|
||||
:meth:`~django.contrib.auth.models.User.get_all_permissions()`,
|
||||
:meth:`~django.contrib.auth.models.User.has_perm()`, and
|
||||
:meth:`~django.contrib.auth.models.User.has_module_perms()`) to any
|
||||
:meth:`~django.contrib.auth.models.User.has_perm()`,
|
||||
:meth:`~django.contrib.auth.models.User.has_module_perms()`, and
|
||||
:meth:`~django.contrib.auth.models.UserManager.with_perm()`) to any
|
||||
authentication backend that implements these functions.
|
||||
|
||||
The permissions given to the user will be the superset of all permissions
|
||||
|
|
|
@ -2,6 +2,7 @@ from unittest import mock
|
|||
|
||||
from django.conf.global_settings import PASSWORD_HASHERS
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.contrib.auth.base_user import AbstractBaseUser
|
||||
from django.contrib.auth.hashers import get_hasher
|
||||
from django.contrib.auth.models import (
|
||||
|
@ -261,6 +262,142 @@ class AbstractUserTestCase(TestCase):
|
|||
hasher.iterations = old_iterations
|
||||
|
||||
|
||||
class CustomModelBackend(ModelBackend):
|
||||
def with_perm(self, perm, is_active=True, include_superusers=True, backend=None, obj=None):
|
||||
if obj is not None and obj.username == 'charliebrown':
|
||||
return User.objects.filter(pk=obj.pk)
|
||||
return User.objects.filter(username__startswith='charlie')
|
||||
|
||||
|
||||
class UserWithPermTestCase(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
content_type = ContentType.objects.get_for_model(Group)
|
||||
cls.permission = Permission.objects.create(
|
||||
name='test', content_type=content_type, codename='test',
|
||||
)
|
||||
# User with permission.
|
||||
cls.user1 = User.objects.create_user('user 1', 'foo@example.com')
|
||||
cls.user1.user_permissions.add(cls.permission)
|
||||
# User with group permission.
|
||||
group1 = Group.objects.create(name='group 1')
|
||||
group1.permissions.add(cls.permission)
|
||||
group2 = Group.objects.create(name='group 2')
|
||||
group2.permissions.add(cls.permission)
|
||||
cls.user2 = User.objects.create_user('user 2', 'bar@example.com')
|
||||
cls.user2.groups.add(group1, group2)
|
||||
# Users without permissions.
|
||||
cls.user_charlie = User.objects.create_user('charlie', 'charlie@example.com')
|
||||
cls.user_charlie_b = User.objects.create_user('charliebrown', 'charlie@brown.com')
|
||||
# Superuser.
|
||||
cls.superuser = User.objects.create_superuser(
|
||||
'superuser', 'superuser@example.com', 'superpassword',
|
||||
)
|
||||
# Inactive user with permission.
|
||||
cls.inactive_user = User.objects.create_user(
|
||||
'inactive_user', 'baz@example.com', is_active=False,
|
||||
)
|
||||
cls.inactive_user.user_permissions.add(cls.permission)
|
||||
|
||||
def test_invalid_permission_name(self):
|
||||
msg = 'Permission name should be in the form app_label.permission_codename.'
|
||||
for perm in ('nodots', 'too.many.dots', '...', ''):
|
||||
with self.subTest(perm), self.assertRaisesMessage(ValueError, msg):
|
||||
User.objects.with_perm(perm)
|
||||
|
||||
def test_invalid_permission_type(self):
|
||||
msg = 'The `perm` argument must be a string or a permission instance.'
|
||||
for perm in (b'auth.test', object(), None):
|
||||
with self.subTest(perm), self.assertRaisesMessage(TypeError, msg):
|
||||
User.objects.with_perm(perm)
|
||||
|
||||
def test_invalid_backend_type(self):
|
||||
msg = 'backend must be a dotted import path string (got %r).'
|
||||
for backend in (b'auth_tests.CustomModelBackend', object()):
|
||||
with self.subTest(backend):
|
||||
with self.assertRaisesMessage(TypeError, msg % backend):
|
||||
User.objects.with_perm('auth.test', backend=backend)
|
||||
|
||||
def test_basic(self):
|
||||
active_users = [self.user1, self.user2]
|
||||
tests = [
|
||||
({}, [*active_users, self.superuser]),
|
||||
({'obj': self.user1}, []),
|
||||
# Only inactive users.
|
||||
({'is_active': False}, [self.inactive_user]),
|
||||
# All users.
|
||||
({'is_active': None}, [*active_users, self.superuser, self.inactive_user]),
|
||||
# Exclude superusers.
|
||||
({'include_superusers': False}, active_users),
|
||||
(
|
||||
{'include_superusers': False, 'is_active': False},
|
||||
[self.inactive_user],
|
||||
),
|
||||
(
|
||||
{'include_superusers': False, 'is_active': None},
|
||||
[*active_users, self.inactive_user],
|
||||
),
|
||||
]
|
||||
for kwargs, expected_users in tests:
|
||||
for perm in ('auth.test', self.permission):
|
||||
with self.subTest(perm=perm, **kwargs):
|
||||
self.assertCountEqual(
|
||||
User.objects.with_perm(perm, **kwargs),
|
||||
expected_users,
|
||||
)
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=['django.contrib.auth.backends.BaseBackend'])
|
||||
def test_backend_without_with_perm(self):
|
||||
self.assertSequenceEqual(User.objects.with_perm('auth.test'), [])
|
||||
|
||||
def test_nonexistent_permission(self):
|
||||
self.assertSequenceEqual(User.objects.with_perm('auth.perm'), [self.superuser])
|
||||
|
||||
def test_nonexistent_backend(self):
|
||||
with self.assertRaises(ImportError):
|
||||
User.objects.with_perm(
|
||||
'auth.test',
|
||||
backend='invalid.backend.CustomModelBackend',
|
||||
)
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=['auth_tests.test_models.CustomModelBackend'])
|
||||
def test_custom_backend(self):
|
||||
for perm in ('auth.test', self.permission):
|
||||
with self.subTest(perm):
|
||||
self.assertCountEqual(
|
||||
User.objects.with_perm(perm),
|
||||
[self.user_charlie, self.user_charlie_b],
|
||||
)
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=['auth_tests.test_models.CustomModelBackend'])
|
||||
def test_custom_backend_pass_obj(self):
|
||||
for perm in ('auth.test', self.permission):
|
||||
with self.subTest(perm):
|
||||
self.assertSequenceEqual(
|
||||
User.objects.with_perm(perm, obj=self.user_charlie_b),
|
||||
[self.user_charlie_b],
|
||||
)
|
||||
|
||||
@override_settings(AUTHENTICATION_BACKENDS=[
|
||||
'auth_tests.test_models.CustomModelBackend',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
])
|
||||
def test_multiple_backends(self):
|
||||
msg = (
|
||||
'You have multiple authentication backends configured and '
|
||||
'therefore must provide the `backend` argument.'
|
||||
)
|
||||
with self.assertRaisesMessage(ValueError, msg):
|
||||
User.objects.with_perm('auth.test')
|
||||
|
||||
backend = 'auth_tests.test_models.CustomModelBackend'
|
||||
self.assertCountEqual(
|
||||
User.objects.with_perm('auth.test', backend=backend),
|
||||
[self.user_charlie, self.user_charlie_b],
|
||||
)
|
||||
|
||||
|
||||
class IsActiveTestCase(TestCase):
|
||||
"""
|
||||
Tests the behavior of the guaranteed is_active attribute
|
||||
|
|
Loading…
Reference in New Issue