From 8daec78cfde2b4a4451050472bedb12cb1706b9b Mon Sep 17 00:00:00 2001 From: Luke Plant Date: Thu, 28 Jan 2010 01:47:23 +0000 Subject: [PATCH] Fixed #12557 - AnonymousUser should check auth backends for permissions Thanks to hvdklauw for the idea and work on the patch. git-svn-id: http://code.djangoproject.com/svn/django/trunk@12316 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/auth/__init__.py | 6 ++ django/contrib/auth/backends.py | 3 + django/contrib/auth/models.py | 87 +++++++++++++------- django/contrib/auth/tests/__init__.py | 2 +- django/contrib/auth/tests/auth_backends.py | 94 ++++++++++++++++++++-- docs/internals/deprecation.txt | 11 ++- docs/releases/1.2.txt | 10 +++ docs/topics/auth.txt | 32 ++++++++ 8 files changed, 204 insertions(+), 41 deletions(-) diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index 0e0bb88327..169ae01070 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -26,6 +26,12 @@ def load_backend(path): warn("Authentication backends without a `supports_object_permissions` attribute are deprecated. Please define it in %s." % cls, PendingDeprecationWarning) cls.supports_object_permissions = False + try: + getattr(cls, 'supports_anonymous_user') + except AttributeError: + warn("Authentication backends without a `supports_anonymous_user` attribute are deprecated. Please define it in %s." % cls, + PendingDeprecationWarning) + cls.supports_anonymous_user = False return cls() def get_backends(): diff --git a/django/contrib/auth/backends.py b/django/contrib/auth/backends.py index 80a6bef136..038ba2bba3 100644 --- a/django/contrib/auth/backends.py +++ b/django/contrib/auth/backends.py @@ -12,6 +12,7 @@ class ModelBackend(object): Authenticates against django.contrib.auth.models.User. """ supports_object_permissions = False + supports_anonymous_user = True # TODO: Model, login attribute name and password attribute name should be # configurable. @@ -58,6 +59,8 @@ class ModelBackend(object): return user_obj._group_perm_cache def get_all_permissions(self, user_obj): + if user_obj.is_anonymous(): + return set() if not hasattr(user_obj, '_perm_cache'): user_obj._perm_cache = set([u"%s.%s" % (p.content_type.app_label, p.codename) for p in user_obj.user_permissions.select_related()]) user_obj._perm_cache.update(self.get_group_permissions(user_obj)) diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 5e76935603..fd9301deaa 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -128,6 +128,49 @@ class UserManager(models.Manager): from random import choice return ''.join([choice(allowed_chars) for i in range(length)]) + +# A few helper functions for common logic between User and AnonymousUser. +def _user_get_all_permissions(user, obj): + permissions = set() + anon = user.is_anonymous() + for backend in auth.get_backends(): + if not anon or backend.supports_anonymous_user: + if hasattr(backend, "get_all_permissions"): + if obj is not None: + if backend.supports_object_permissions: + permissions.update( + backend.get_all_permissions(user, obj) + ) + else: + permissions.update(backend.get_all_permissions(user)) + return permissions + + +def _user_has_perm(user, perm, obj): + anon = user.is_anonymous() + for backend in auth.get_backends(): + if not anon or backend.supports_anonymous_user: + if hasattr(backend, "has_perm"): + if obj is not None: + if (backend.supports_object_permissions and + backend.has_perm(user, perm, obj)): + return True + else: + if backend.has_perm(user, perm): + return True + return False + + +def _user_has_module_perms(user, app_label): + anon = user.is_anonymous() + for backend in auth.get_backends(): + if not anon or backend.supports_anonymous_user: + if hasattr(backend, "has_module_perms"): + if backend.has_module_perms(user, app_label): + return True + return False + + class User(models.Model): """ Users within the Django authentication system are represented by this model. @@ -228,17 +271,7 @@ class User(models.Model): return permissions def get_all_permissions(self, obj=None): - permissions = set() - for backend in auth.get_backends(): - if hasattr(backend, "get_all_permissions"): - if obj is not None: - if backend.supports_object_permissions: - permissions.update( - backend.get_all_permissions(self, obj) - ) - else: - permissions.update(backend.get_all_permissions(self)) - return permissions + return _user_get_all_permissions(self, obj) def has_perm(self, perm, obj=None): """ @@ -257,16 +290,7 @@ class User(models.Model): return True # Otherwise we need to check the backends. - for backend in auth.get_backends(): - if hasattr(backend, "has_perm"): - if obj is not None: - if (backend.supports_object_permissions and - backend.has_perm(self, perm, obj)): - return True - else: - if backend.has_perm(self, perm): - return True - return False + return _user_has_perm(self, perm, obj) def has_perms(self, perm_list, obj=None): """ @@ -290,11 +314,7 @@ class User(models.Model): if self.is_superuser: return True - for backend in auth.get_backends(): - if hasattr(backend, "has_module_perms"): - if backend.has_module_perms(self, app_label): - return True - return False + return _user_has_module_perms(self, app_label) def get_and_delete_messages(self): messages = [] @@ -396,14 +416,23 @@ class AnonymousUser(object): return self._user_permissions user_permissions = property(_get_user_permissions) + def get_group_permissions(self, obj=None): + return set() + + def get_all_permissions(self, obj=None): + return _user_get_all_permissions(self, obj=obj) + def has_perm(self, perm, obj=None): - return False + return _user_has_perm(self, perm, obj=obj) def has_perms(self, perm_list, obj=None): - return False + for perm in perm_list: + if not self.has_perm(perm, obj): + return False + return True def has_module_perms(self, module): - return False + return _user_has_module_perms(self, module) def get_and_delete_messages(self): return [] diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py index 9a078cf643..3d7c562c00 100644 --- a/django/contrib/auth/tests/__init__.py +++ b/django/contrib/auth/tests/__init__.py @@ -4,7 +4,7 @@ from django.contrib.auth.tests.views \ from django.contrib.auth.tests.forms import FORM_TESTS from django.contrib.auth.tests.remote_user \ import RemoteUserTest, RemoteUserNoCreateTest, RemoteUserCustomTest -from django.contrib.auth.tests.auth_backends import BackendTest, RowlevelBackendTest +from django.contrib.auth.tests.auth_backends import BackendTest, RowlevelBackendTest, AnonymousUserBackendTest, NoAnonymousUserBackendTest from django.contrib.auth.tests.tokens import TOKEN_GENERATOR_TESTS # 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 3ab80f623c..8eaf2cb3e7 100644 --- a/django/contrib/auth/tests/auth_backends.py +++ b/django/contrib/auth/tests/auth_backends.py @@ -88,8 +88,6 @@ class BackendTest(TestCase): self.assertEqual(user.get_all_permissions(), set(['auth.test'])) - - class TestObj(object): pass @@ -97,6 +95,9 @@ class TestObj(object): class SimpleRowlevelBackend(object): supports_object_permissions = True + # 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: return # We only support row level perms @@ -104,10 +105,14 @@ class SimpleRowlevelBackend(object): if isinstance(obj, TestObj): if user.username == 'test2': return True - elif isinstance(user, AnonymousUser) and perm == 'anon': + elif user.is_anonymous() and perm == 'anon': + # not reached due to supports_anonymous_user = False return True return False + def has_module_perms(self, user, app_label): + return app_label == "app1" + def get_all_permissions(self, user, obj=None): if not obj: return [] # We only support row level perms @@ -115,6 +120,8 @@ class SimpleRowlevelBackend(object): if not isinstance(obj, TestObj): return ['none'] + if user.is_anonymous(): + return ['anon'] if user.username == 'test2': return ['simple', 'advanced'] else: @@ -134,7 +141,9 @@ class SimpleRowlevelBackend(object): class RowlevelBackendTest(TestCase): - + """ + Tests for auth backend that supports object level permissions + """ backend = 'django.contrib.auth.tests.auth_backends.SimpleRowlevelBackend' def setUp(self): @@ -142,8 +151,7 @@ class RowlevelBackendTest(TestCase): settings.AUTHENTICATION_BACKENDS = self.curr_auth + (self.backend,) self.user1 = User.objects.create_user('test', 'test@example.com', 'test') self.user2 = User.objects.create_user('test2', 'test2@example.com', 'test') - self.user3 = AnonymousUser() - self.user4 = User.objects.create_user('test4', 'test4@example.com', 'test') + self.user3 = User.objects.create_user('test3', 'test3@example.com', 'test') def tearDown(self): settings.AUTHENTICATION_BACKENDS = self.curr_auth @@ -165,5 +173,75 @@ class RowlevelBackendTest(TestCase): def test_get_group_permissions(self): content_type=ContentType.objects.get_for_model(Group) group = Group.objects.create(name='test_group') - self.user4.groups.add(group) - self.assertEqual(self.user4.get_group_permissions(TestObj()), set(['group_perm'])) + self.user3.groups.add(group) + self.assertEqual(self.user3.get_group_permissions(TestObj()), set(['group_perm'])) + + +class AnonymousUserBackend(SimpleRowlevelBackend): + + supports_anonymous_user = True + + +class NoAnonymousUserBackend(SimpleRowlevelBackend): + + supports_anonymous_user = False + + +class AnonymousUserBackendTest(TestCase): + """ + Tests for AnonymousUser delegating to backend if it has 'supports_anonymous_user' = True + """ + + backend = 'django.contrib.auth.tests.auth_backends.AnonymousUserBackend' + + def setUp(self): + self.curr_auth = settings.AUTHENTICATION_BACKENDS + settings.AUTHENTICATION_BACKENDS = (self.backend,) + self.user1 = AnonymousUser() + + 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('anon', TestObj()), True) + + def test_has_perms(self): + self.assertEqual(self.user1.has_perms(['anon'], TestObj()), True) + self.assertEqual(self.user1.has_perms(['anon', 'perm'], TestObj()), False) + + def test_has_module_perms(self): + self.assertEqual(self.user1.has_module_perms("app1"), True) + self.assertEqual(self.user1.has_module_perms("app2"), False) + + def test_get_all_permissions(self): + self.assertEqual(self.user1.get_all_permissions(TestObj()), set(['anon'])) + + +class NoAnonymousUserBackendTest(TestCase): + """ + Tests that AnonymousUser does not delegate to backend if it has 'supports_anonymous_user' = False + """ + backend = 'django.contrib.auth.tests.auth_backends.NoAnonymousUserBackend' + + def setUp(self): + self.curr_auth = settings.AUTHENTICATION_BACKENDS + settings.AUTHENTICATION_BACKENDS = self.curr_auth + (self.backend,) + self.user1 = AnonymousUser() + + 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('anon', TestObj()), False) + + def test_has_perms(self): + self.assertEqual(self.user1.has_perms(['anon'], TestObj()), False) + + def test_has_module_perms(self): + self.assertEqual(self.user1.has_module_perms("app1"), False) + self.assertEqual(self.user1.has_module_perms("app2"), False) + + def test_get_all_permissions(self): + self.assertEqual(self.user1.get_all_permissions(TestObj()), set()) diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index e2d4b6c427..c3aa55e9d8 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -13,9 +13,9 @@ their deprecation, as per the :ref:`Django deprecation policy hooking up admin URLs. This has been deprecated since the 1.1 release. - * Authentication backends need to define the boolean attribute - ``supports_object_permissions``. The old backend style is deprecated - since the 1.2 release. + * Authentication backends need to define the boolean attributes + ``supports_object_permissions`` and ``supports_anonymous_user``. + The old backend style is deprecated since the 1.2 release. * 1.4 * ``CsrfResponseMiddleware``. This has been deprecated since the 1.2 @@ -56,6 +56,11 @@ their deprecation, as per the :ref:`Django deprecation policy permission checking. The ``supports_object_permissions`` variable is not checked any longer and can be removed. + * Authentication backends need to support the ``AnonymousUser`` + being passed to all methods dealing with permissions. + The ``supports_anonymous_user`` variable is not checked any + longer and can be removed. + * The ability to specify a callable template loader rather than a ``Loader`` class will be removed, as will the ``load_template_source`` functions that are included with the built in template loaders for diff --git a/docs/releases/1.2.txt b/docs/releases/1.2.txt index 7c8992bd42..a7660ae922 100644 --- a/docs/releases/1.2.txt +++ b/docs/releases/1.2.txt @@ -558,3 +558,13 @@ Although there is no implementation of this in core, a custom authentication backend can provide this implementation and it will be used by :class:`django.contrib.auth.models.User`. See the :ref:`authentication docs ` for more information. + +Permissions for anonymous users +------------------------------- + +If you provide a custom auth backend with ``supports_anonymous_user`` set to +``True``, AnonymousUser will check the backend for permissions, just like +User already did. This is useful for centralizing permission handling - apps +can always delegate the question of whether something is allowed or not to +the authorization/authentication backend. See the :ref:`authentication +docs ` for more details. diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index 6707208283..ab9d268fe1 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -1559,6 +1559,38 @@ the ``auth_permission`` table most of the time. .. _django/contrib/auth/backends.py: http://code.djangoproject.com/browser/django/trunk/django/contrib/auth/backends.py +Authorization for anonymous users +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionchanged:: 1.2 + +An anonymous user is one that is not authenticated i.e. they have provided no +valid authentication details. However, that does not necessarily mean they are +not authorized to do anything. At the most basic level, most Web sites +authorize anonymous users to browse most of the site, and many allow anonymous +posting of comments etc. + +Django's permission framework does not have a place to store permissions for +anonymous users. However, it has a foundation that allows custom authentication +backends to specify authorization for anonymous users. This is especially useful +for the authors of re-usable apps, who can delegate all questions of authorization +to the auth backend, rather than needing settings, for example, to control +anonymous access. + +To enable this in your own backend, you must set the class attribute +``supports_anonymous_user`` to ``True``. (This precaution is to maintain +compatibility with backends that assume that all user objects are actual +instances of the :class:`django.contrib.auth.models.User` class). With this +in place, :class:`django.contrib.auth.models.AnonymousUser` will delegate all +the relevant permission methods to the authentication backends. + +A nonexistent ``supports_anonymous_user`` attribute will raise a hidden +``PendingDeprecationWarning`` if used in Django 1.2. In Django 1.3, this +warning will be upgraded to a ``DeprecationWarning``, which will be displayed +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. + Handling object permissions ---------------------------