Fixed #11010 - Add a foundation for object permissions to authentication backends. Thanks to Florian Apolloner for writing the initial patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@11807 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jannis Leidel 2009-12-10 01:05:35 +00:00
parent 2c2f5aee4d
commit 9bf652dfd6
10 changed files with 253 additions and 98 deletions

View File

@ -1,4 +1,5 @@
import datetime
from warnings import warn
from django.core.exceptions import ImproperlyConfigured
from django.utils.importlib import import_module
@ -19,6 +20,12 @@ def load_backend(path):
cls = getattr(mod, attr)
except AttributeError:
raise ImproperlyConfigured, 'Module "%s" does not define a "%s" authentication backend' % (module, attr)
try:
getattr(cls, 'supports_object_permissions')
except AttributeError:
warn("Authentication backends without a `supports_object_permissions` attribute are deprecated. Please define it in %s." % cls,
PendingDeprecationWarning)
cls.supports_object_permissions = False
return cls()
def get_backends():

View File

@ -11,6 +11,8 @@ class ModelBackend(object):
"""
Authenticates against django.contrib.auth.models.User.
"""
supports_object_permissions = False
# TODO: Model, login attribute name and password attribute name should be
# configurable.
def authenticate(self, username=None, password=None):

View File

@ -121,7 +121,8 @@ class UserManager(models.Manager):
return ''.join([choice(allowed_chars) for i in range(length)])
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.
Username and password are required. Other fields are optional.
"""
@ -151,11 +152,16 @@ class User(models.Model):
return "/users/%s/" % urllib.quote(smart_str(self.username))
def is_anonymous(self):
"Always returns False. This is a way of comparing User objects to anonymous users."
"""
Always returns False. This is a way of comparing User objects to
anonymous users.
"""
return False
def is_authenticated(self):
"""Always return True. This is a way to tell if the user has been authenticated in templates.
"""
Always return True. This is a way to tell if the user has been
authenticated in templates.
"""
return True
@ -194,30 +200,41 @@ class User(models.Model):
def has_usable_password(self):
return self.password != UNUSABLE_PASSWORD
def get_group_permissions(self):
def get_group_permissions(self, obj=None):
"""
Returns a list of permission strings that this user has through
his/her groups. This method queries all available auth backends.
If an object is passed in, only permissions matching this object
are returned.
"""
permissions = set()
for backend in auth.get_backends():
if hasattr(backend, "get_group_permissions"):
permissions.update(backend.get_group_permissions(self))
if obj is not None and backend.supports_object_permissions:
group_permissions = backend.get_group_permissions(self, obj)
else:
group_permissions = backend.get_group_permissions(self)
permissions.update(group_permissions)
return permissions
def get_all_permissions(self):
def get_all_permissions(self, obj=None):
permissions = set()
for backend in auth.get_backends():
if hasattr(backend, "get_all_permissions"):
permissions.update(backend.get_all_permissions(self))
if obj is not None and backend.supports_object_permissions:
all_permissions = backend.get_all_permissions(self, obj)
else:
all_permissions = backend.get_all_permissions(self)
permissions.update(all_permissions)
return permissions
def has_perm(self, perm):
def has_perm(self, perm, obj=None):
"""
Returns True if the user has the specified permission. This method
queries all available auth backends, but returns immediately if any
backend returns True. Thus, a user who has permission from a single
auth backend is assumed to have permission in general.
auth backend is assumed to have permission in general. If an object
is provided, permissions for this specific object are checked.
"""
# Inactive users have no permissions.
if not self.is_active:
@ -230,14 +247,22 @@ class User(models.Model):
# Otherwise we need to check the backends.
for backend in auth.get_backends():
if hasattr(backend, "has_perm"):
if backend.has_perm(self, perm):
return True
if obj is not None and backend.supports_object_permissions:
if 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):
"""Returns True if the user has each of the specified permissions."""
def has_perms(self, perm_list, obj=None):
"""
Returns True if the user has each of the specified permissions.
If object is passed, it checks if the user has all required perms
for this object.
"""
for perm in perm_list:
if not self.has_perm(perm):
if not self.has_perm(perm, obj):
return False
return True
@ -358,10 +383,10 @@ class AnonymousUser(object):
return self._user_permissions
user_permissions = property(_get_user_permissions)
def has_perm(self, perm):
def has_perm(self, perm, obj=None):
return False
def has_perms(self, perm_list):
def has_perms(self, perm_list, obj=None):
return False
def has_module_perms(self, module):

View File

@ -4,6 +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.tokens import TOKEN_GENERATOR_TESTS
# The password for the fixture data users is 'password'

View File

@ -0,0 +1,149 @@
from django.conf import settings
from django.contrib.auth.models import User, Group, Permission, AnonymousUser
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
class BackendTest(TestCase):
backend = 'django.contrib.auth.backends.ModelBackend'
def setUp(self):
self.curr_auth = settings.AUTHENTICATION_BACKENDS
settings.AUTHENTICATION_BACKENDS = (self.backend,)
User.objects.create_user('test', 'test@example.com', 'test')
def tearDown(self):
settings.AUTHENTICATION_BACKENDS = self.curr_auth
def test_has_perm(self):
user = User.objects.get(username='test')
self.assertEqual(user.has_perm('auth.test'), False)
user.is_staff = True
user.save()
self.assertEqual(user.has_perm('auth.test'), False)
user.is_superuser = True
user.save()
self.assertEqual(user.has_perm('auth.test'), True)
user.is_staff = False
user.is_superuser = False
user.save()
self.assertEqual(user.has_perm('auth.test'), False)
def test_custom_perms(self):
user = User.objects.get(username='test')
content_type=ContentType.objects.get_for_model(Group)
perm = Permission.objects.create(name='test', content_type=content_type, codename='test')
user.user_permissions.add(perm)
user.save()
# reloading user to purge the _perm_cache
user = User.objects.get(username='test')
self.assertEqual(user.get_all_permissions() == set([u'auth.test']), True)
self.assertEqual(user.get_group_permissions(), set([]))
self.assertEqual(user.has_module_perms('Group'), False)
self.assertEqual(user.has_module_perms('auth'), True)
perm = Permission.objects.create(name='test2', content_type=content_type, codename='test2')
user.user_permissions.add(perm)
user.save()
perm = Permission.objects.create(name='test3', content_type=content_type, codename='test3')
user.user_permissions.add(perm)
user.save()
user = User.objects.get(username='test')
self.assertEqual(user.get_all_permissions(), set([u'auth.test2', u'auth.test', u'auth.test3']))
self.assertEqual(user.has_perm('test'), False)
self.assertEqual(user.has_perm('auth.test'), True)
self.assertEqual(user.has_perms(['auth.test2', 'auth.test3']), True)
perm = Permission.objects.create(name='test_group', content_type=content_type, codename='test_group')
group = Group.objects.create(name='test_group')
group.permissions.add(perm)
group.save()
user.groups.add(group)
user = User.objects.get(username='test')
exp = set([u'auth.test2', u'auth.test', u'auth.test3', u'auth.test_group'])
self.assertEqual(user.get_all_permissions(), exp)
self.assertEqual(user.get_group_permissions(), set([u'auth.test_group']))
self.assertEqual(user.has_perms(['auth.test3', 'auth.test_group']), True)
user = AnonymousUser()
self.assertEqual(user.has_perm('test'), False)
self.assertEqual(user.has_perms(['auth.test2', 'auth.test3']), False)
class TestObj(object):
pass
class SimpleRowlevelBackend(object):
supports_object_permissions = True
def has_perm(self, user, perm, obj=None):
if not obj:
return # We only support row level perms
if isinstance(obj, TestObj):
if user.username == 'test2':
return True
elif isinstance(user, AnonymousUser) and perm == 'anon':
return True
return False
def get_all_permissions(self, user, obj=None):
if not obj:
return [] # We only support row level perms
if not isinstance(obj, TestObj):
return ['none']
if user.username == 'test2':
return ['simple', 'advanced']
else:
return ['simple']
def get_group_permissions(self, user, obj=None):
if not obj:
return # We only support row level perms
if not isinstance(obj, TestObj):
return ['none']
if 'test_group' in [group.name for group in user.groups.all()]:
return ['group_perm']
else:
return ['none']
class RowlevelBackendTest(TestCase):
backend = 'django.contrib.auth.tests.auth_backends.SimpleRowlevelBackend'
def setUp(self):
self.curr_auth = settings.AUTHENTICATION_BACKENDS
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')
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.user2.has_perm('perm', TestObj()), True)
self.assertEqual(self.user2.has_perm('perm'), False)
self.assertEqual(self.user2.has_perms(['simple', 'advanced'], TestObj()), True)
self.assertEqual(self.user3.has_perm('perm', TestObj()), False)
self.assertEqual(self.user3.has_perm('anon', TestObj()), False)
self.assertEqual(self.user3.has_perms(['simple', 'advanced'], TestObj()), False)
def test_get_all_permissions(self):
self.assertEqual(self.user1.get_all_permissions(TestObj()), set(['simple']))
self.assertEqual(self.user2.get_all_permissions(TestObj()), set(['simple', 'advanced']))
self.assertEqual(self.user2.get_all_permissions(), set([]))
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']))

View File

@ -13,6 +13,10 @@ 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.
* 1.4
* ``CsrfResponseMiddleware``. This has been deprecated since the 1.2
release, in favour of the template tag method for inserting the CSRF
@ -36,6 +40,10 @@ their deprecation, as per the :ref:`Django deprecation policy
:ref:`messages framework <ref-contrib-messages>` should be used
instead.
* Authentication backends need to support the ``obj`` parameter for
permission checking. The ``supports_object_permissions`` variable
is not checked any longer and can be removed.
* 2.0
* ``django.views.defaults.shortcut()``. This function has been moved
to ``django.contrib.contenttypes.views.shortcut()`` as part of the

View File

@ -202,29 +202,49 @@ Methods
:meth:`~django.contrib.auth.models.User.set_unusable_password()` has
been called for this user.
.. method:: models.User.get_group_permissions()
.. method:: models.User.get_group_permissions(obj=None)
Returns a list of permission strings that the user has, through his/her
groups.
.. method:: models.User.get_all_permissions()
.. versionadded:: 1.2
If ``obj`` is passed in, only returns the group permissions for
this specific object.
.. method:: models.User.get_all_permissions(obj=None)
Returns a list of permission strings that the user has, both through
group and user permissions.
.. method:: models.User.has_perm(perm)
.. versionadded:: 1.2
If ``obj`` is passed in, only returns the permissions for this
specific object.
.. method:: models.User.has_perm(perm, obj=None)
Returns ``True`` if the user has the specified permission, where perm is
in the format ``"<app label>.<permission codename>"``.
If the user is inactive, this method will always return ``False``.
.. method:: models.User.has_perms(perm_list)
.. versionadded:: 1.2
If ``obj`` is passed in, this method won't check for a permission for
the model, but for this specific object.
.. method:: models.User.has_perms(perm_list, obj=None)
Returns ``True`` if the user has each of the specified permissions,
where each perm is in the format
``"<app label>.<permission codename>"``. If the user is inactive,
this method will always return ``False``.
.. versionadded:: 1.2
If ``obj`` is passed in, this method won't check for permissions for
the model, but for the specific object.
.. method:: models.User.has_module_perms(package_name)
Returns ``True`` if the user has any permissions in the given package
@ -1521,3 +1541,24 @@ A full authorization implementation can be found in
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
Handling object permissions
---------------------------
Django's permission framework has a foundation for object permissions, though
there is no implementation for it in the core. That means that checking for
object permissions will always return ``False`` or an empty list (depending on
the check performed).
To enable object permissions in your own
:ref:`authentication backend <ref-authentication-backends>` you'll just have
to allow passing an ``obj`` parameter to the permission methods and set the
``supports_objects_permissions`` class attribute to ``True``.
A nonexistent ``supports_objects_permissions`` 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_objects_permissions`` will be set to ``False``.
Django 1.4 will assume that every backend supports object permissions and
won't check for the existence of ``supports_objects_permissions``, which
means not supporting ``obj`` as a parameter will raise a ``TypeError``.

View File

@ -1,78 +0,0 @@
try:
set
except NameError:
from sets import Set as set # Python 2.3 fallback
__test__ = {'API_TESTS': """
>>> from django.contrib.auth.models import User, Group, Permission, AnonymousUser
>>> from django.contrib.contenttypes.models import ContentType
# No Permissions assigned yet, should return False except for superuser
>>> user = User.objects.create_user('test', 'test@example.com', 'test')
>>> user.has_perm("auth.test")
False
>>> user.is_staff=True
>>> user.save()
>>> user.has_perm("auth.test")
False
>>> user.is_superuser=True
>>> user.save()
>>> user.has_perm("auth.test")
True
>>> user.is_staff = False
>>> user.is_superuser = False
>>> user.save()
>>> user.has_perm("auth.test")
False
>>> content_type=ContentType.objects.get_for_model(Group)
>>> perm = Permission.objects.create(name="test", content_type=content_type, codename="test")
>>> user.user_permissions.add(perm)
>>> user.save()
# reloading user to purge the _perm_cache
>>> user = User.objects.get(username="test")
>>> user.get_all_permissions() == set([u'auth.test'])
True
>>> user.get_group_permissions() == set([])
True
>>> user.has_module_perms("Group")
False
>>> user.has_module_perms("auth")
True
>>> perm = Permission.objects.create(name="test2", content_type=content_type, codename="test2")
>>> user.user_permissions.add(perm)
>>> user.save()
>>> perm = Permission.objects.create(name="test3", content_type=content_type, codename="test3")
>>> user.user_permissions.add(perm)
>>> user.save()
>>> user = User.objects.get(username="test")
>>> user.get_all_permissions() == set([u'auth.test2', u'auth.test', u'auth.test3'])
True
>>> user.has_perm('test')
False
>>> user.has_perm('auth.test')
True
>>> user.has_perms(['auth.test2', 'auth.test3'])
True
>>> perm = Permission.objects.create(name="test_group", content_type=content_type, codename="test_group")
>>> group = Group.objects.create(name='test_group')
>>> group.permissions.add(perm)
>>> group.save()
>>> user.groups.add(group)
>>> user = User.objects.get(username="test")
>>> exp = set([u'auth.test2', u'auth.test', u'auth.test3', u'auth.test_group'])
>>> user.get_all_permissions() == exp
True
>>> user.get_group_permissions() == set([u'auth.test_group'])
True
>>> user.has_perms(['auth.test3', 'auth.test_group'])
True
>>> user = AnonymousUser()
>>> user.has_perm('test')
False
>>> user.has_perms(['auth.test2', 'auth.test3'])
False
"""}