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
This commit is contained in:
Luke Plant 2010-01-28 01:47:23 +00:00
parent 3f50119868
commit 8daec78cfd
8 changed files with 204 additions and 41 deletions

View File

@ -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():

View File

@ -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))

View File

@ -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):
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 []

View File

@ -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'

View File

@ -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())

View File

@ -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

View File

@ -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
<topics-auth>` 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 <topics-auth>` for more details.

View File

@ -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
---------------------------