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 import get_user_model
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
|
from django.db.models import Exists, OuterRef, Q
|
||||||
from django.utils.deprecation import RemovedInDjango31Warning
|
from django.utils.deprecation import RemovedInDjango31Warning
|
||||||
|
|
||||||
UserModel = get_user_model()
|
UserModel = get_user_model()
|
||||||
|
@ -119,6 +120,42 @@ class ModelBackend(BaseBackend):
|
||||||
for perm in self.get_all_permissions(user_obj)
|
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):
|
def get_user(self, user_id):
|
||||||
try:
|
try:
|
||||||
user = UserModel._default_manager.get(pk=user_id)
|
user = UserModel._default_manager.get(pk=user_id)
|
||||||
|
|
|
@ -157,6 +157,32 @@ class UserManager(BaseUserManager):
|
||||||
|
|
||||||
return self._create_user(username, email, password, **extra_fields)
|
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.
|
# A few helper functions for common logic between User and AnonymousUser.
|
||||||
def _user_get_permissions(user, obj, from_name):
|
def _user_get_permissions(user, obj, from_name):
|
||||||
|
|
|
@ -291,6 +291,28 @@ Manager methods
|
||||||
|
|
||||||
The ``email`` and ``password`` parameters were made optional.
|
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
|
``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
|
implement them other than returning an empty set of permissions if
|
||||||
``obj is not None``.
|
``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)
|
.. method:: authenticate(request, username=None, password=None, **kwargs)
|
||||||
|
|
||||||
Tries to authenticate ``username`` with ``password`` by calling
|
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`
|
don't have an :attr:`~django.contrib.auth.models.CustomUser.is_active`
|
||||||
field are allowed.
|
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
|
.. class:: AllowAllUsersModelBackend
|
||||||
|
|
||||||
Same as :class:`ModelBackend` except that it doesn't reject inactive users
|
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
|
* :attr:`~django.contrib.auth.models.CustomUser.REQUIRED_FIELDS` now supports
|
||||||
:class:`~django.db.models.ManyToManyField`\s.
|
:class:`~django.db.models.ManyToManyField`\s.
|
||||||
|
|
||||||
|
* The new :meth:`.UserManager.with_perm` method returns users that have the
|
||||||
|
specified permission.
|
||||||
|
|
||||||
:mod:`django.contrib.contenttypes`
|
:mod:`django.contrib.contenttypes`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -179,12 +179,13 @@ Handling authorization in custom backends
|
||||||
|
|
||||||
Custom auth backends can provide their own permissions.
|
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_user_permissions()`,
|
||||||
:meth:`~django.contrib.auth.models.User.get_group_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.get_all_permissions()`,
|
||||||
:meth:`~django.contrib.auth.models.User.has_perm()`, and
|
:meth:`~django.contrib.auth.models.User.has_perm()`,
|
||||||
:meth:`~django.contrib.auth.models.User.has_module_perms()`) to any
|
: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.
|
authentication backend that implements these functions.
|
||||||
|
|
||||||
The permissions given to the user will be the superset of all permissions
|
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.conf.global_settings import PASSWORD_HASHERS
|
||||||
from django.contrib.auth import get_user_model
|
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.base_user import AbstractBaseUser
|
||||||
from django.contrib.auth.hashers import get_hasher
|
from django.contrib.auth.hashers import get_hasher
|
||||||
from django.contrib.auth.models import (
|
from django.contrib.auth.models import (
|
||||||
|
@ -261,6 +262,142 @@ class AbstractUserTestCase(TestCase):
|
||||||
hasher.iterations = old_iterations
|
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):
|
class IsActiveTestCase(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests the behavior of the guaranteed is_active attribute
|
Tests the behavior of the guaranteed is_active attribute
|
||||||
|
|
Loading…
Reference in New Issue