Skip to content

Commit

Permalink
Edits.
Browse files Browse the repository at this point in the history
  • Loading branch information
felixxm committed Aug 29, 2019
1 parent a1703f2 commit 34ddd01
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 45 deletions.
29 changes: 18 additions & 11 deletions django/contrib/auth/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,33 +121,40 @@ def has_module_perms(self, user_obj, app_label):
)

def with_perm(self, perm, is_active=True, include_superusers=True, obj=None):
"""
Return users that have permission "perm". By default, include active
users and all superusers.
"""
if not isinstance(perm, (str, Permission)):
raise TypeError('The `perm` argument must be a string or a permission instance.')
raise TypeError(
'The `perm` argument must be a string or a permission instance.'
)
elif isinstance(perm, str):
try:
app_label, codename = perm.split('.')
except ValueError:
raise ValueError("Permission name should be in the form 'app_label.perm_name'.") from None
raise ValueError(
'Permission name should be in the form '
'app_label.permission_codename.'
)

UserModel = get_user_model()

if obj is not None:
return UserModel._default_manager.none()

q0 = Q(group__user=OuterRef('pk')) | Q(user=OuterRef('pk'))
permission_q = Q(group__user=OuterRef('pk')) | Q(user=OuterRef('pk'))
if isinstance(perm, Permission):
q0 &= Q(pk=perm.pk)
permission_q &= Q(pk=perm.pk)
else:
q0 &= Q(codename=codename, content_type__app_label=app_label)
permission_q &= Q(codename=codename, content_type__app_label=app_label)

q1 = Q(has_permission=True)
user_q = Exists(Permission.objects.filter(permission_q))
if include_superusers:
q1 |= Q(is_superuser=True)
user_q |= Q(is_superuser=True)
if is_active is not None:
q1 &= Q(is_active=is_active)
user_q &= Q(is_active=is_active)

has_permission = Exists(Permission.objects.filter(q0))
return UserModel._default_manager.annotate(has_permission=has_permission).filter(q1)
return UserModel._default_manager.filter(user_q)

def get_user(self, user_id):
try:
Expand Down
22 changes: 16 additions & 6 deletions django/contrib/auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,18 +160,28 @@ def create_superuser(self, username, email=None, password=None, **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:
if len(backends) == 1:
backend, _ = backends[0]
else:
raise ValueError(
'You have multiple authentication backends configured and '
'therefore must provide the `backend` argument.'
)
_, backend = backends[0]
elif not isinstance(backend, str):
raise TypeError('The `backend` argument must be a string.')
backend = auth.load_backend(backend)
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.get_queryset().none()
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.
Expand Down
37 changes: 24 additions & 13 deletions docs/ref/contrib/auth.txt
Original file line number Diff line number Diff line change
Expand Up @@ -295,18 +295,23 @@ Manager methods

.. versionadded:: 3.0

Returns a queryset containing :class:`~django.contrib.auth.models.User`
objects that have the given permission ``perm`` either in the form of
``"<app label>.<permission codename>"`` or a
:class:`~django.contrib.auth.models.Permission` instance, including
superusers (if ``include_superusers`` is ``True``.)
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 ``is_active`` is ``True``, only active users will be included, or if
``False``, only inactive users will be included. Use ``None`` to return
all users irrespective of active state.
If ``include_superusers`` is ``True`` (default), the result will
include superusers.

If ``backend`` is given, it will be used if it's defined in
the :setting:`AUTHENTICATION_BACKENDS` setting.
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
========================
Expand Down Expand Up @@ -537,9 +542,8 @@ 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 allow an object to be passed as a parameter for
object-specific permissions, but it returns an empty queryset 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)

Expand Down Expand Up @@ -607,6 +611,13 @@ The following backends are available in :mod:`django.contrib.auth.backends`:
: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
Expand Down
6 changes: 3 additions & 3 deletions docs/releases/3.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,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() <django.contrib.auth.models.UserManager.with_perm>`
method returns all users that have the specified permission.

* The new :meth:`.UserManager.with_perm` method returns users that have the
specified permission.

:mod:`django.contrib.contenttypes`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
2 changes: 1 addition & 1 deletion docs/topics/auth/customizing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ 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()`,
Expand Down
17 changes: 6 additions & 11 deletions tests/auth_tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,17 +353,14 @@ def test_custom_backend_with_permission_instance_and_obj(self):
)

def test_invalid_permission_name(self):
msg = "Permission name should be in the form 'app_label.perm_name'."
msg = 'Permission name should be in the form app_label.permission_codename.'
for perm in ('nodots', 'too.many.dots', '...', ''):
with self.subTest(perm=perm), self.assertRaisesMessage(ValueError, msg):
User.objects.with_perm(perm)

def test_invalid_permission_type(self):
class FakePermission:
pass

msg = 'The `perm` argument must be a string or a permission instance.'
for perm in (b'auth.test', FakePermission()):
for perm in (b'auth.test', object(), None):
with self.subTest(perm=perm), self.assertRaisesMessage(TypeError, msg):
User.objects.with_perm(perm)

Expand Down Expand Up @@ -401,12 +398,10 @@ def test_custom_backend_with_obj(self):
)

def test_invalid_backend_type(self):
msg = 'The `backend` argument must be a string.'
with self.assertRaisesMessage(TypeError, msg):
User.objects.with_perm(
'auth.test',
backend=b'auth_tests.test_auth_backends.CustomModelBackend',
)
backend = b'auth_tests.test_auth_backends.CustomModelBackend'
msg = 'backend must be a dotted import path string (got %r).'
with self.assertRaisesMessage(TypeError, msg % backend):
User.objects.with_perm('auth.test', backend=backend)

def test_nonexistent_backend(self):
with self.assertRaises(ImportError):
Expand Down

0 comments on commit 34ddd01

Please sign in to comment.