diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index 95251752d4c..633baa52653 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -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, DeprecationWarning) 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() def get_backends(): diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py index 2f608043cb7..d8c81406b3e 100644 --- a/django/contrib/auth/backends.py +++ b/django/contrib/auth/backends.py @@ -8,6 +8,7 @@ class ModelBackend(object): """ supports_object_permissions = False supports_anonymous_user = True + supports_inactive_user = True # TODO: Model, login attribute name and password attribute name should be # configurable. @@ -42,12 +43,16 @@ class ModelBackend(object): return user_obj._perm_cache def has_perm(self, user_obj, perm): + if not user_obj.is_active: + return False return perm in self.get_all_permissions(user_obj) def has_module_perms(self, user_obj, 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): if perm[:perm.index('.')] == app_label: return True diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index ebf9a8d2164..ec3af633bdb 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -170,8 +170,10 @@ def _user_get_all_permissions(user, obj): def _user_has_perm(user, perm, obj): anon = user.is_anonymous() + active = user.is_active 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 obj is not None: 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): anon = user.is_anonymous() + active = user.is_active 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 backend.has_module_perms(user, app_label): return True @@ -310,12 +314,9 @@ class User(models.Model): auth backend is assumed to have permission in general. If an object 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. - if self.is_superuser: + # Active superusers have all permissions. + if self.is_active and self.is_superuser: return True # 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 label. Uses pretty much the same logic as has_perm, above. """ - if not self.is_active: - return False - - if self.is_superuser: + # Active superusers have all permissions. + if self.is_active and self.is_superuser: return True return _user_has_module_perms(self, app_label) diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py index 64d30372b1c..3a8f55b6e72 100644 --- a/django/contrib/auth/tests/__init__.py +++ b/django/contrib/auth/tests/__init__.py @@ -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.decorators import LoginRequiredTestCase -from django.contrib.auth.tests.forms import UserCreationFormTest, AuthenticationFormTest, SetPasswordFormTest, PasswordChangeFormTest, UserChangeFormTest, PasswordResetFormTest -from django.contrib.auth.tests.remote_user \ - import RemoteUserTest, RemoteUserNoCreateTest, RemoteUserCustomTest +from django.contrib.auth.tests.forms import (UserCreationFormTest, + AuthenticationFormTest, SetPasswordFormTest, PasswordChangeFormTest, + 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.signals import SignalTestCase from django.contrib.auth.tests.tokens import TokenGeneratorTest -from django.contrib.auth.tests.views import PasswordResetTest, \ - ChangePasswordTest, LoginTest, LogoutTest, LoginURLSettings +from django.contrib.auth.tests.views import (PasswordResetTest, + ChangePasswordTest, LoginTest, LogoutTest, LoginURLSettings) from django.contrib.auth.tests.permissions import TestAuthPermissions # The password for the fixture data users is 'password' diff --git a/django/contrib/auth/tests/auth_backends.py b/django/contrib/auth/tests/auth_backends.py index e931152f77a..6a99757921d 100644 --- a/django/contrib/auth/tests/auth_backends.py +++ b/django/contrib/auth/tests/auth_backends.py @@ -102,9 +102,12 @@ class TestObj(object): class SimpleRowlevelBackend(object): 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): if not obj: @@ -116,9 +119,13 @@ class SimpleRowlevelBackend(object): elif user.is_anonymous() and perm == 'anon': # not reached due to supports_anonymous_user = False return True + elif not user.is_active and perm == 'inactive': + return True return False def has_module_perms(self, user, app_label): + if not user.is_anonymous() and not user.is_active: + return False return app_label == "app1" def get_all_permissions(self, user, obj=None): @@ -192,11 +199,13 @@ class RowlevelBackendTest(TestCase): class AnonymousUserBackend(SimpleRowlevelBackend): supports_anonymous_user = True + supports_inactive_user = False class NoAnonymousUserBackend(SimpleRowlevelBackend): supports_anonymous_user = False + supports_inactive_user = False class AnonymousUserBackendTest(TestCase): @@ -258,6 +267,7 @@ class NoAnonymousUserBackendTest(TestCase): def test_get_all_permissions(self): self.assertEqual(self.user1.get_all_permissions(TestObj()), set()) + class NoBackendsTest(TestCase): """ 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): 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) + diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 4cd48071887..54f40d027ad 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -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`` language code. + * Authentication backends need to define the boolean attribute + ``supports_inactive_user``. + * 1.5 * The ``mod_python`` request handler has been deprecated since the 1.3 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 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 * ``django.views.defaults.shortcut()``. This function has been moved to ``django.contrib.contenttypes.views.shortcut()`` as part of the diff --git a/docs/releases/1.3-beta-1.txt b/docs/releases/1.3-beta-1.txt index 1fa6bf2e444..342136cd2fe 100644 --- a/docs/releases/1.3-beta-1.txt +++ b/docs/releases/1.3-beta-1.txt @@ -55,6 +55,14 @@ displayed by most translation tools. 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 ` for more details. + Backwards-incompatible changes in 1.3 alpha 2 ============================================= diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt index d111eaeffb2..bd68f02aad1 100644 --- a/docs/releases/1.3.txt +++ b/docs/releases/1.3.txt @@ -177,6 +177,14 @@ caching in Django`. .. _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 ` for more details. + Everything else ~~~~~~~~~~~~~~~ diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index 04b6d39964b..641db3a8b20 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -741,7 +741,7 @@ The login_required decorator @login_required def my_view(request): ... - + :func:`~django.contrib.auth.decorators.login_required` does the following: * 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 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 ---------------------------