Commit 2e364a0a authored by Jorge C. Leitão's avatar Jorge C. Leitão Committed by Tim Graham
Browse files

Fixed #15716 - Authentication backends can short-circuit authorization.

Authorization backends can now raise PermissionDenied in "has_perm"
and "has_module_perms" to short-circuit authorization process.
parent ebd70d4d
Loading
Loading
Loading
Loading
+17 −2
Original line number Diff line number Diff line
from __future__ import unicode_literals

from django.core.exceptions import PermissionDenied
from django.core.mail import send_mail
from django.core import validators
from django.db import models
@@ -267,18 +268,32 @@ def _user_get_all_permissions(user, obj):


def _user_has_perm(user, perm, obj):
    """
    A backend can raise `PermissionDenied` to short-circuit permission checking.
    """
    for backend in auth.get_backends():
        if hasattr(backend, "has_perm"):
        if not hasattr(backend, 'has_perm'):
            continue
        try:
            if backend.has_perm(user, perm, obj):
                return True
        except PermissionDenied:
            return False
    return False


def _user_has_module_perms(user, app_label):
    """
    A backend can raise `PermissionDenied` to short-circuit permission checking.
    """
    for backend in auth.get_backends():
        if hasattr(backend, "has_module_perms"):
        if not hasattr(backend, 'has_module_perms'):
            continue
        try:
            if backend.has_module_perms(user, app_label):
                return True
        except PermissionDenied:
            return False
    return False


+27 −1
Original line number Diff line number Diff line
@@ -398,7 +398,7 @@ class InActiveUserBackendTest(TestCase):

class PermissionDeniedBackend(object):
    """
    Always raises PermissionDenied.
    Always raises PermissionDenied in `authenticate`, `has_perm` and `has_module_perms`.
    """
    supports_object_permissions = True
    supports_anonymous_user = True
@@ -407,6 +407,12 @@ class PermissionDeniedBackend(object):
    def authenticate(self, username=None, password=None):
        raise PermissionDenied

    def has_perm(self, user_obj, perm, obj=None):
        raise PermissionDenied

    def has_module_perms(self, user_obj, app_label):
        raise PermissionDenied


@skipIfCustomUser
class PermissionDeniedBackendTest(TestCase):
@@ -430,6 +436,26 @@ class PermissionDeniedBackendTest(TestCase):
    def test_authenticates(self):
        self.assertEqual(authenticate(username='test', password='test'), self.user1)

    @override_settings(AUTHENTICATION_BACKENDS=(backend, ) +
            tuple(settings.AUTHENTICATION_BACKENDS))
    def test_has_perm_denied(self):
        content_type = ContentType.objects.get_for_model(Group)
        perm = Permission.objects.create(name='test', content_type=content_type, codename='test')
        self.user1.user_permissions.add(perm)

        self.assertIs(self.user1.has_perm('auth.test'), False)
        self.assertIs(self.user1.has_module_perms('auth'), False)

    @override_settings(AUTHENTICATION_BACKENDS=tuple(
        settings.AUTHENTICATION_BACKENDS) + (backend, ))
    def test_has_perm(self):
        content_type = ContentType.objects.get_for_model(Group)
        perm = Permission.objects.create(name='test', content_type=content_type, codename='test')
        self.user1.user_permissions.add(perm)

        self.assertIs(self.user1.has_perm('auth.test'), True)
        self.assertIs(self.user1.has_module_perms('auth'), True)


class NewModelBackend(ModelBackend):
    pass
+5 −1
Original line number Diff line number Diff line
@@ -36,7 +36,11 @@ Minor features
:mod:`django.contrib.auth`
^^^^^^^^^^^^^^^^^^^^^^^^^^

* ...
* Authorization backends can now raise
  :class:`~django.core.exceptions.PermissionDenied` in
  :meth:`~django.contrib.auth.models.User.has_perm`
  and :meth:`~django.contrib.auth.models.User.has_module_perms`
  to short-circuit permission checking.

:mod:`django.contrib.formtools`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+8 −0
Original line number Diff line number Diff line
@@ -180,6 +180,14 @@ The permissions given to the user will be the superset of all permissions
returned by all backends. That is, Django grants a permission to a user that
any one backend grants.

.. versionadded:: 1.8

    If a backend raises a :class:`~django.core.exceptions.PermissionDenied`
    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::