Fixed #14249 -- Added support for inactive users to the auth backend system. Thanks, Harro van der Klauw.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@15010 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jannis Leidel 2010-12-21 19:18:12 +00:00
parent 5830477e46
commit 745c255a19
9 changed files with 156 additions and 20 deletions

View File

@ -30,6 +30,11 @@ def load_backend(path):
warn("Authentication backends without a `supports_anonymous_user` attribute are deprecated. Please define it in %s." % cls, warn("Authentication backends without a `supports_anonymous_user` attribute are deprecated. Please define it in %s." % cls,
DeprecationWarning) DeprecationWarning)
cls.supports_anonymous_user = False cls.supports_anonymous_user = False
if not hasattr(cls, 'supports_inactive_user'):
warn("Authentication backends without a `supports_inactive_user` attribute are deprecated. Please define it in %s." % cls,
DeprecationWarning)
cls.supports_inactive_user = False
return cls() return cls()
def get_backends(): def get_backends():

View File

@ -8,6 +8,7 @@ class ModelBackend(object):
""" """
supports_object_permissions = False supports_object_permissions = False
supports_anonymous_user = True supports_anonymous_user = True
supports_inactive_user = True
# TODO: Model, login attribute name and password attribute name should be # TODO: Model, login attribute name and password attribute name should be
# configurable. # configurable.
@ -42,12 +43,16 @@ class ModelBackend(object):
return user_obj._perm_cache return user_obj._perm_cache
def has_perm(self, user_obj, perm): def has_perm(self, user_obj, perm):
if not user_obj.is_active:
return False
return perm in self.get_all_permissions(user_obj) return perm in self.get_all_permissions(user_obj)
def has_module_perms(self, user_obj, app_label): def has_module_perms(self, user_obj, app_label):
""" """
Returns True if user_obj has any permissions in the given app_label. Returns True if user_obj has any permissions in the given app_label.
""" """
if not user_obj.is_active:
return False
for perm in self.get_all_permissions(user_obj): for perm in self.get_all_permissions(user_obj):
if perm[:perm.index('.')] == app_label: if perm[:perm.index('.')] == app_label:
return True return True

View File

@ -170,8 +170,10 @@ def _user_get_all_permissions(user, obj):
def _user_has_perm(user, perm, obj): def _user_has_perm(user, perm, obj):
anon = user.is_anonymous() anon = user.is_anonymous()
active = user.is_active
for backend in auth.get_backends(): for backend in auth.get_backends():
if not anon or backend.supports_anonymous_user: if (not active and not anon and backend.supports_inactive_user) or \
(not anon or backend.supports_anonymous_user):
if hasattr(backend, "has_perm"): if hasattr(backend, "has_perm"):
if obj is not None: if obj is not None:
if (backend.supports_object_permissions and if (backend.supports_object_permissions and
@ -185,8 +187,10 @@ def _user_has_perm(user, perm, obj):
def _user_has_module_perms(user, app_label): def _user_has_module_perms(user, app_label):
anon = user.is_anonymous() anon = user.is_anonymous()
active = user.is_active
for backend in auth.get_backends(): for backend in auth.get_backends():
if not anon or backend.supports_anonymous_user: if (not active and not anon and backend.supports_inactive_user) or \
(not anon or backend.supports_anonymous_user):
if hasattr(backend, "has_module_perms"): if hasattr(backend, "has_module_perms"):
if backend.has_module_perms(user, app_label): if backend.has_module_perms(user, app_label):
return True return True
@ -310,12 +314,9 @@ class User(models.Model):
auth backend is assumed to have permission in general. If an object auth backend is assumed to have permission in general. If an object
is provided, permissions for this specific object are checked. is provided, permissions for this specific object are checked.
""" """
# Inactive users have no permissions.
if not self.is_active:
return False
# Superusers have all permissions. # Active superusers have all permissions.
if self.is_superuser: if self.is_active and self.is_superuser:
return True return True
# Otherwise we need to check the backends. # Otherwise we need to check the backends.
@ -337,10 +338,8 @@ class User(models.Model):
Returns True if the user has any permissions in the given app Returns True if the user has any permissions in the given app
label. Uses pretty much the same logic as has_perm, above. label. Uses pretty much the same logic as has_perm, above.
""" """
if not self.is_active: # Active superusers have all permissions.
return False if self.is_active and self.is_superuser:
if self.is_superuser:
return True return True
return _user_has_module_perms(self, app_label) return _user_has_module_perms(self, app_label)

View File

@ -1,14 +1,18 @@
from django.contrib.auth.tests.auth_backends import BackendTest, RowlevelBackendTest, AnonymousUserBackendTest, NoAnonymousUserBackendTest, NoBackendsTest from django.contrib.auth.tests.auth_backends import (BackendTest,
RowlevelBackendTest, AnonymousUserBackendTest, NoAnonymousUserBackendTest,
NoBackendsTest, InActiveUserBackendTest, NoInActiveUserBackendTest)
from django.contrib.auth.tests.basic import BasicTestCase from django.contrib.auth.tests.basic import BasicTestCase
from django.contrib.auth.tests.decorators import LoginRequiredTestCase from django.contrib.auth.tests.decorators import LoginRequiredTestCase
from django.contrib.auth.tests.forms import UserCreationFormTest, AuthenticationFormTest, SetPasswordFormTest, PasswordChangeFormTest, UserChangeFormTest, PasswordResetFormTest from django.contrib.auth.tests.forms import (UserCreationFormTest,
from django.contrib.auth.tests.remote_user \ AuthenticationFormTest, SetPasswordFormTest, PasswordChangeFormTest,
import RemoteUserTest, RemoteUserNoCreateTest, RemoteUserCustomTest UserChangeFormTest, PasswordResetFormTest)
from django.contrib.auth.tests.remote_user import (RemoteUserTest,
RemoteUserNoCreateTest, RemoteUserCustomTest)
from django.contrib.auth.tests.models import ProfileTestCase from django.contrib.auth.tests.models import ProfileTestCase
from django.contrib.auth.tests.signals import SignalTestCase from django.contrib.auth.tests.signals import SignalTestCase
from django.contrib.auth.tests.tokens import TokenGeneratorTest from django.contrib.auth.tests.tokens import TokenGeneratorTest
from django.contrib.auth.tests.views import PasswordResetTest, \ from django.contrib.auth.tests.views import (PasswordResetTest,
ChangePasswordTest, LoginTest, LogoutTest, LoginURLSettings ChangePasswordTest, LoginTest, LogoutTest, LoginURLSettings)
from django.contrib.auth.tests.permissions import TestAuthPermissions from django.contrib.auth.tests.permissions import TestAuthPermissions
# The password for the fixture data users is 'password' # The password for the fixture data users is 'password'

View File

@ -102,9 +102,12 @@ class TestObj(object):
class SimpleRowlevelBackend(object): class SimpleRowlevelBackend(object):
supports_object_permissions = True supports_object_permissions = True
supports_inactive_user = False
# This class also supports tests for anonymous user permissions, and
# inactive user permissions via subclasses which just set the
# 'supports_anonymous_user' or 'supports_inactive_user' attribute.
# This class also supports tests for anonymous user permissions,
# via subclasses which just set the 'supports_anonymous_user' attribute.
def has_perm(self, user, perm, obj=None): def has_perm(self, user, perm, obj=None):
if not obj: if not obj:
@ -116,9 +119,13 @@ class SimpleRowlevelBackend(object):
elif user.is_anonymous() and perm == 'anon': elif user.is_anonymous() and perm == 'anon':
# not reached due to supports_anonymous_user = False # not reached due to supports_anonymous_user = False
return True return True
elif not user.is_active and perm == 'inactive':
return True
return False return False
def has_module_perms(self, user, app_label): def has_module_perms(self, user, app_label):
if not user.is_anonymous() and not user.is_active:
return False
return app_label == "app1" return app_label == "app1"
def get_all_permissions(self, user, obj=None): def get_all_permissions(self, user, obj=None):
@ -192,11 +199,13 @@ class RowlevelBackendTest(TestCase):
class AnonymousUserBackend(SimpleRowlevelBackend): class AnonymousUserBackend(SimpleRowlevelBackend):
supports_anonymous_user = True supports_anonymous_user = True
supports_inactive_user = False
class NoAnonymousUserBackend(SimpleRowlevelBackend): class NoAnonymousUserBackend(SimpleRowlevelBackend):
supports_anonymous_user = False supports_anonymous_user = False
supports_inactive_user = False
class AnonymousUserBackendTest(TestCase): class AnonymousUserBackendTest(TestCase):
@ -258,6 +267,7 @@ class NoAnonymousUserBackendTest(TestCase):
def test_get_all_permissions(self): def test_get_all_permissions(self):
self.assertEqual(self.user1.get_all_permissions(TestObj()), set()) self.assertEqual(self.user1.get_all_permissions(TestObj()), set())
class NoBackendsTest(TestCase): class NoBackendsTest(TestCase):
""" """
Tests that an appropriate error is raised if no auth backends are provided. Tests that an appropriate error is raised if no auth backends are provided.
@ -272,3 +282,67 @@ class NoBackendsTest(TestCase):
def test_raises_exception(self): def test_raises_exception(self):
self.assertRaises(ImproperlyConfigured, self.user.has_perm, ('perm', TestObj(),)) self.assertRaises(ImproperlyConfigured, self.user.has_perm, ('perm', TestObj(),))
class InActiveUserBackend(SimpleRowlevelBackend):
supports_anonymous_user = False
supports_inactive_user = True
class NoInActiveUserBackend(SimpleRowlevelBackend):
supports_anonymous_user = False
supports_inactive_user = False
class InActiveUserBackendTest(TestCase):
"""
Tests for a inactive user delegating to backend if it has 'supports_inactive_user' = True
"""
backend = 'django.contrib.auth.tests.auth_backends.InActiveUserBackend'
def setUp(self):
self.curr_auth = settings.AUTHENTICATION_BACKENDS
settings.AUTHENTICATION_BACKENDS = (self.backend,)
self.user1 = User.objects.create_user('test', 'test@example.com', 'test')
self.user1.is_active = False
self.user1.save()
def tearDown(self):
settings.AUTHENTICATION_BACKENDS = self.curr_auth
def test_has_perm(self):
self.assertEqual(self.user1.has_perm('perm', TestObj()), False)
self.assertEqual(self.user1.has_perm('inactive', TestObj()), True)
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 NoInActiveUserBackendTest(TestCase):
"""
Tests that an inactive user does not delegate to backend if it has 'supports_inactive_user' = False
"""
backend = 'django.contrib.auth.tests.auth_backends.NoInActiveUserBackend'
def setUp(self):
self.curr_auth = settings.AUTHENTICATION_BACKENDS
settings.AUTHENTICATION_BACKENDS = tuple(self.curr_auth) + (self.backend,)
self.user1 = User.objects.create_user('test', 'test@example.com', 'test')
self.user1.is_active = False
self.user1.save()
def tearDown(self):
settings.AUTHENTICATION_BACKENDS = self.curr_auth
def test_has_perm(self):
self.assertEqual(self.user1.has_perm('perm', TestObj()), False)
self.assertEqual(self.user1.has_perm('inactive', TestObj()), True)
def test_has_module_perms(self):
self.assertEqual(self.user1.has_module_perms("app1"), False)
self.assertEqual(self.user1.has_module_perms("app2"), False)

View File

@ -98,6 +98,9 @@ their deprecation, as per the :ref:`Django deprecation policy
* The ``no`` language code has been deprecated in favor of the ``nb`` * The ``no`` language code has been deprecated in favor of the ``nb``
language code. language code.
* Authentication backends need to define the boolean attribute
``supports_inactive_user``.
* 1.5 * 1.5
* The ``mod_python`` request handler has been deprecated since the 1.3 * The ``mod_python`` request handler has been deprecated since the 1.3
release. The ``mod_wsgi`` handler should be used instead. release. The ``mod_wsgi`` handler should be used instead.
@ -139,6 +142,11 @@ their deprecation, as per the :ref:`Django deprecation policy
* The :djadmin:`reset` and :djadmin:`sqlreset` management commands * The :djadmin:`reset` and :djadmin:`sqlreset` management commands
are deprecated. are deprecated.
* Authentication backends need to support a inactive user
being passed to all methods dealing with permissions.
The ``supports_inactive_user`` variable is not checked any
longer and can be removed.
* 2.0 * 2.0
* ``django.views.defaults.shortcut()``. This function has been moved * ``django.views.defaults.shortcut()``. This function has been moved
to ``django.contrib.contenttypes.views.shortcut()`` as part of the to ``django.contrib.contenttypes.views.shortcut()`` as part of the

View File

@ -55,6 +55,14 @@ displayed by most translation tools.
For more information, see :ref:`translator-comments`. For more information, see :ref:`translator-comments`.
Permissions for inactive users
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you provide a custom auth backend with ``supports_inactive_user`` set to
``True``, an inactive user model will check the backend for permissions.
This is useful for further centralizing the permission handling. See the
:ref:`authentication docs <topics-auth>` for more details.
Backwards-incompatible changes in 1.3 alpha 2 Backwards-incompatible changes in 1.3 alpha 2
============================================= =============================================

View File

@ -177,6 +177,14 @@ caching in Django<topics/cache>`.
.. _pylibmc: http://sendapatch.se/projects/pylibmc/ .. _pylibmc: http://sendapatch.se/projects/pylibmc/
Permissions for inactive users
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you provide a custom auth backend with ``supports_inactive_user`` set to
``True``, an inactive user model will check the backend for permissions.
This is useful for further centralizing the permission handling. See the
:ref:`authentication docs <topics-auth>` for more details.
Everything else Everything else
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~

View File

@ -741,7 +741,7 @@ The login_required decorator
@login_required @login_required
def my_view(request): def my_view(request):
... ...
:func:`~django.contrib.auth.decorators.login_required` does the following: :func:`~django.contrib.auth.decorators.login_required` does the following:
* If the user isn't logged in, redirect to * If the user isn't logged in, redirect to
@ -1645,6 +1645,31 @@ loudly. Additionally ``supports_anonymous_user`` will be set to ``False``.
Django 1.4 will assume that every backend supports anonymous users being Django 1.4 will assume that every backend supports anonymous users being
passed to the authorization methods. passed to the authorization methods.
Authorization for inactive users
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionadded:: 1.3
An inactive user is a one that is authenticated but has its attribute
``is_active`` set to ``False``. However this does not mean they are not
authrozied to do anything. For example they are allowed to activate their
account.
The support for anonymous users in the permission system allows for
anonymous users to have permissions to do something while inactive
authenticated users do not.
To enable this on your own backend, you must set the class attribute
``supports_inactive_user`` to ``True``.
A nonexisting ``supports_inactive_user`` attribute will raise a
``PendingDeprecationWarning`` if used in Django 1.3. In Django 1.4, this
warning will be updated to a ``DeprecationWarning`` which will be displayed
loudly. Additionally ``supports_inactive_user`` will be set to ``False``.
Django 1.5 will assume that every backend supports inactive users being
passed to the authorization methods.
Handling object permissions Handling object permissions
--------------------------- ---------------------------