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.
This commit is contained in:
Jorge C. Leitão 2014-05-08 22:06:46 +02:00 committed by Tim Graham
parent ebd70d4d00
commit 2e364a0aac
4 changed files with 57 additions and 4 deletions

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.exceptions import PermissionDenied
from django.core.mail import send_mail from django.core.mail import send_mail
from django.core import validators from django.core import validators
from django.db import models from django.db import models
@ -267,18 +268,32 @@ def _user_get_all_permissions(user, obj):
def _user_has_perm(user, perm, obj): def _user_has_perm(user, perm, obj):
"""
A backend can raise `PermissionDenied` to short-circuit permission checking.
"""
for backend in auth.get_backends(): 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): if backend.has_perm(user, perm, obj):
return True return True
except PermissionDenied:
return False
return False return False
def _user_has_module_perms(user, app_label): def _user_has_module_perms(user, app_label):
"""
A backend can raise `PermissionDenied` to short-circuit permission checking.
"""
for backend in auth.get_backends(): 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): if backend.has_module_perms(user, app_label):
return True return True
except PermissionDenied:
return False
return False return False

View File

@ -398,7 +398,7 @@ class InActiveUserBackendTest(TestCase):
class PermissionDeniedBackend(object): class PermissionDeniedBackend(object):
""" """
Always raises PermissionDenied. Always raises PermissionDenied in `authenticate`, `has_perm` and `has_module_perms`.
""" """
supports_object_permissions = True supports_object_permissions = True
supports_anonymous_user = True supports_anonymous_user = True
@ -407,6 +407,12 @@ class PermissionDeniedBackend(object):
def authenticate(self, username=None, password=None): def authenticate(self, username=None, password=None):
raise PermissionDenied 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 @skipIfCustomUser
class PermissionDeniedBackendTest(TestCase): class PermissionDeniedBackendTest(TestCase):
@ -430,6 +436,26 @@ class PermissionDeniedBackendTest(TestCase):
def test_authenticates(self): def test_authenticates(self):
self.assertEqual(authenticate(username='test', password='test'), self.user1) 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): class NewModelBackend(ModelBackend):
pass pass

View File

@ -36,7 +36,11 @@ Minor features
:mod:`django.contrib.auth` :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` :mod:`django.contrib.formtools`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -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 returned by all backends. That is, Django grants a permission to a user that
any one backend grants. 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 The simple backend above could implement permissions for the magic admin
fairly simply:: fairly simply::