From 2e364a0aacb49a5160896b1ca5a2619baa3f4d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20C=2E=20Leit=C3=A3o?= Date: Thu, 8 May 2014 22:06:46 +0200 Subject: [PATCH] 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. --- django/contrib/auth/models.py | 19 +++++++++++-- .../contrib/auth/tests/test_auth_backends.py | 28 ++++++++++++++++++- docs/releases/1.8.txt | 6 +++- docs/topics/auth/customizing.txt | 8 ++++++ 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 8cc28bc85d..14f0a5ecd8 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -1,5 +1,6 @@ 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 diff --git a/django/contrib/auth/tests/test_auth_backends.py b/django/contrib/auth/tests/test_auth_backends.py index 4331b8496c..2b115e113b 100644 --- a/django/contrib/auth/tests/test_auth_backends.py +++ b/django/contrib/auth/tests/test_auth_backends.py @@ -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 diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 1c78627ecf..aa39fb2b7c 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -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` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index 388ebdd1ac..8b9db396cd 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -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::