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:
parent
3f50119868
commit
8daec78cfd
|
@ -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():
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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 []
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue