Fixed #18763 -- Added ModelBackend/UserManager.with_perm() methods.

Co-authored-by: Nick Pope <nick.pope@flightdataservices.com>
This commit is contained in:
Berker Peksag 2016-08-25 19:26:18 +03:00 committed by Mariusz Felisiak
parent fa7ffc6cb3
commit 400ec5125e
6 changed files with 248 additions and 3 deletions

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -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

View File

@ -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