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, warn("Authentication backends without a `supports_object_permissions` attribute are deprecated. Please define it in %s." % cls,
PendingDeprecationWarning) PendingDeprecationWarning)
cls.supports_object_permissions = False 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() return cls()
def get_backends(): def get_backends():

View File

@ -12,6 +12,7 @@ class ModelBackend(object):
Authenticates against django.contrib.auth.models.User. Authenticates against django.contrib.auth.models.User.
""" """
supports_object_permissions = False supports_object_permissions = False
supports_anonymous_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.
@ -58,6 +59,8 @@ class ModelBackend(object):
return user_obj._group_perm_cache return user_obj._group_perm_cache
def get_all_permissions(self, user_obj): def get_all_permissions(self, user_obj):
if user_obj.is_anonymous():
return set()
if not hasattr(user_obj, '_perm_cache'): 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 = 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)) 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 from random import choice
return ''.join([choice(allowed_chars) for i in range(length)]) 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): class User(models.Model):
""" """
Users within the Django authentication system are represented by this model. Users within the Django authentication system are represented by this model.
@ -228,17 +271,7 @@ class User(models.Model):
return permissions return permissions
def get_all_permissions(self, obj=None): def get_all_permissions(self, obj=None):
permissions = set() return _user_get_all_permissions(self, obj)
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
def has_perm(self, perm, obj=None): def has_perm(self, perm, obj=None):
""" """
@ -257,16 +290,7 @@ class User(models.Model):
return True return True
# Otherwise we need to check the backends. # Otherwise we need to check the backends.
for backend in auth.get_backends(): return _user_has_perm(self, perm, obj)
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
def has_perms(self, perm_list, obj=None): def has_perms(self, perm_list, obj=None):
""" """
@ -290,11 +314,7 @@ class User(models.Model):
if self.is_superuser: if self.is_superuser:
return True return True
for backend in auth.get_backends(): return _user_has_module_perms(self, app_label)
if hasattr(backend, "has_module_perms"):
if backend.has_module_perms(self, app_label):
return True
return False
def get_and_delete_messages(self): def get_and_delete_messages(self):
messages = [] messages = []
@ -396,14 +416,23 @@ class AnonymousUser(object):
return self._user_permissions return self._user_permissions
user_permissions = property(_get_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): 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): def has_perms(self, perm_list, obj=None):
for perm in perm_list:
if not self.has_perm(perm, obj):
return False return False
return True
def has_module_perms(self, module): def has_module_perms(self, module):
return False return _user_has_module_perms(self, module)
def get_and_delete_messages(self): def get_and_delete_messages(self):
return [] 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.forms import FORM_TESTS
from django.contrib.auth.tests.remote_user \ from django.contrib.auth.tests.remote_user \
import RemoteUserTest, RemoteUserNoCreateTest, RemoteUserCustomTest 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 from django.contrib.auth.tests.tokens import TOKEN_GENERATOR_TESTS
# The password for the fixture data users is 'password' # 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'])) self.assertEqual(user.get_all_permissions(), set(['auth.test']))
class TestObj(object): class TestObj(object):
pass pass
@ -97,6 +95,9 @@ class TestObj(object):
class SimpleRowlevelBackend(object): class SimpleRowlevelBackend(object):
supports_object_permissions = True 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): def has_perm(self, user, perm, obj=None):
if not obj: if not obj:
return # We only support row level perms return # We only support row level perms
@ -104,10 +105,14 @@ class SimpleRowlevelBackend(object):
if isinstance(obj, TestObj): if isinstance(obj, TestObj):
if user.username == 'test2': if user.username == 'test2':
return True 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 True
return False return False
def has_module_perms(self, user, app_label):
return app_label == "app1"
def get_all_permissions(self, user, obj=None): def get_all_permissions(self, user, obj=None):
if not obj: if not obj:
return [] # We only support row level perms return [] # We only support row level perms
@ -115,6 +120,8 @@ class SimpleRowlevelBackend(object):
if not isinstance(obj, TestObj): if not isinstance(obj, TestObj):
return ['none'] return ['none']
if user.is_anonymous():
return ['anon']
if user.username == 'test2': if user.username == 'test2':
return ['simple', 'advanced'] return ['simple', 'advanced']
else: else:
@ -134,7 +141,9 @@ class SimpleRowlevelBackend(object):
class RowlevelBackendTest(TestCase): class RowlevelBackendTest(TestCase):
"""
Tests for auth backend that supports object level permissions
"""
backend = 'django.contrib.auth.tests.auth_backends.SimpleRowlevelBackend' backend = 'django.contrib.auth.tests.auth_backends.SimpleRowlevelBackend'
def setUp(self): def setUp(self):
@ -142,8 +151,7 @@ class RowlevelBackendTest(TestCase):
settings.AUTHENTICATION_BACKENDS = self.curr_auth + (self.backend,) settings.AUTHENTICATION_BACKENDS = self.curr_auth + (self.backend,)
self.user1 = User.objects.create_user('test', 'test@example.com', 'test') self.user1 = User.objects.create_user('test', 'test@example.com', 'test')
self.user2 = User.objects.create_user('test2', 'test2@example.com', 'test') self.user2 = User.objects.create_user('test2', 'test2@example.com', 'test')
self.user3 = AnonymousUser() self.user3 = User.objects.create_user('test3', 'test3@example.com', 'test')
self.user4 = User.objects.create_user('test4', 'test4@example.com', 'test')
def tearDown(self): def tearDown(self):
settings.AUTHENTICATION_BACKENDS = self.curr_auth settings.AUTHENTICATION_BACKENDS = self.curr_auth
@ -165,5 +173,75 @@ class RowlevelBackendTest(TestCase):
def test_get_group_permissions(self): def test_get_group_permissions(self):
content_type=ContentType.objects.get_for_model(Group) content_type=ContentType.objects.get_for_model(Group)
group = Group.objects.create(name='test_group') group = Group.objects.create(name='test_group')
self.user4.groups.add(group) self.user3.groups.add(group)
self.assertEqual(self.user4.get_group_permissions(TestObj()), set(['group_perm'])) 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 hooking up admin URLs. This has been deprecated since the 1.1
release. release.
* Authentication backends need to define the boolean attribute * Authentication backends need to define the boolean attributes
``supports_object_permissions``. The old backend style is deprecated ``supports_object_permissions`` and ``supports_anonymous_user``.
since the 1.2 release. The old backend style is deprecated since the 1.2 release.
* 1.4 * 1.4
* ``CsrfResponseMiddleware``. This has been deprecated since the 1.2 * ``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 permission checking. The ``supports_object_permissions`` variable
is not checked any longer and can be removed. 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 * The ability to specify a callable template loader rather than a
``Loader`` class will be removed, as will the ``load_template_source`` ``Loader`` class will be removed, as will the ``load_template_source``
functions that are included with the built in template loaders for 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 backend can provide this implementation and it will be used by
:class:`django.contrib.auth.models.User`. See the :ref:`authentication docs :class:`django.contrib.auth.models.User`. See the :ref:`authentication docs
<topics-auth>` for more information. <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 .. _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 Handling object permissions
--------------------------- ---------------------------