Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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
  • Loading branch information...
commit 8daec78cfde2b4a4451050472bedb12cb1706b9b 1 parent 3f50119
@spookylukey spookylukey authored
View
6 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():
View
3  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 @@ def get_group_permissions(self, user_obj):
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
87 django/contrib/auth/models.py
@@ -128,6 +128,49 @@ def make_random_password(self, length=10, allowed_chars='abcdefghjkmnpqrstuvwxyz
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 @@ def get_group_permissions(self, obj=None):
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 @@ def has_perm(self, perm, obj=None):
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 @@ def has_module_perms(self, app_label):
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 @@ def _get_user_permissions(self):
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 []
View
2  django/contrib/auth/tests/__init__.py
@@ -4,7 +4,7 @@
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
94 django/contrib/auth/tests/auth_backends.py
@@ -88,8 +88,6 @@ def test_has_no_object_perm(self):
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 @@ def has_perm(self, user, perm, obj=None):
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 @@ def get_all_permissions(self, user, obj=None):
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 @@ def get_group_permissions(self, user, obj=None):
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 @@ def setUp(self):
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 @@ def test_get_all_permissions(self):
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
11 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
View
10 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
<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
32 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
---------------------------
Please sign in to comment.
Something went wrong with that request. Please try again.