diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index dd4a8484f5..5dbda44501 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -1,6 +1,6 @@ import re -from django.core.exceptions import ImproperlyConfigured +from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.utils.importlib import import_module from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed @@ -60,6 +60,9 @@ def authenticate(**credentials): except TypeError: # This backend doesn't accept these credentials as arguments. Try the next one. continue + except PermissionDenied: + # This backend says to stop in our tracks - this user should not be allowed in at all. + return None if user is None: continue # Annotate the user object with the path of the backend. diff --git a/django/contrib/auth/tests/auth_backends.py b/django/contrib/auth/tests/auth_backends.py index e92f159ff9..2ab0bd0efa 100644 --- a/django/contrib/auth/tests/auth_backends.py +++ b/django/contrib/auth/tests/auth_backends.py @@ -6,7 +6,8 @@ from django.contrib.auth.models import User, Group, Permission, AnonymousUser from django.contrib.auth.tests.utils import skipIfCustomUser from django.contrib.auth.tests.custom_user import ExtensionUser from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ImproperlyConfigured +from django.core.exceptions import ImproperlyConfigured, PermissionDenied +from django.contrib.auth import authenticate from django.test import TestCase from django.test.utils import override_settings @@ -323,3 +324,37 @@ class InActiveUserBackendTest(TestCase): def test_has_module_perms(self): self.assertEqual(self.user1.has_module_perms("app1"), False) self.assertEqual(self.user1.has_module_perms("app2"), False) + + +class PermissionDeniedBackend(object): + """ + Always raises PermissionDenied. + """ + supports_object_permissions = True + supports_anonymous_user = True + supports_inactive_user = True + + def authenticate(self, username=None, password=None): + raise PermissionDenied + + +class PermissionDeniedBackendTest(TestCase): + """ + Tests that other backends are not checked once a backend raises PermissionDenied + """ + backend = 'django.contrib.auth.tests.auth_backends.PermissionDeniedBackend' + + def setUp(self): + self.user1 = User.objects.create_user('test', 'test@example.com', 'test') + self.user1.save() + + @override_settings(AUTHENTICATION_BACKENDS=(backend, ) + + tuple(settings.AUTHENTICATION_BACKENDS)) + def test_permission_denied(self): + "user is not authenticated after a backend raises permission denied #2550" + self.assertEqual(authenticate(username='test', password='test'), None) + + @override_settings(AUTHENTICATION_BACKENDS=tuple( + settings.AUTHENTICATION_BACKENDS) + (backend, )) + def test_authenticates(self): + self.assertEqual(authenticate(username='test', password='test'), self.user1) diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index ef162a8de3..49649bc6b8 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -17,6 +17,12 @@ deprecation process for some features`_. What's new in Django 1.6 ======================== +Minor features +~~~~~~~~~~~~~~ + +* Authentication backends can raise ``PermissionDenied`` to immediately fail + the authentication chain. + Backwards incompatible changes in 1.6 ===================================== diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index aed482f710..6d8e3c66c3 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -2391,6 +2391,12 @@ processing at the first positive match. you need to force users to re-authenticate using different methods. A simple way to do that is simply to execute ``Session.objects.all().delete()``. +.. versionadded:: 1.6 + +If a backend raises a :class:`~django.core.exceptions.PermissionDenied` +exception, authentication will immediately fail. Django won't check the +backends that follow. + Writing an authentication backend ---------------------------------