Skip to content

Commit

Permalink
Fixed #11010 - Add a foundation for object permissions to authenticat…
Browse files Browse the repository at this point in the history
…ion 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
  • Loading branch information
jezdez committed Dec 10, 2009
1 parent 2c2f5ae commit 9bf652d
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 98 deletions.
7 changes: 7 additions & 0 deletions django/contrib/auth/__init__.py
@@ -1,4 +1,5 @@
import datetime
from warnings import warn
from django.core.exceptions import ImproperlyConfigured
from django.utils.importlib import import_module

Expand All @@ -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():
Expand Down
2 changes: 2 additions & 0 deletions django/contrib/auth/backends.py
Expand Up @@ -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):
Expand Down
57 changes: 41 additions & 16 deletions django/contrib/auth/models.py
Expand Up @@ -121,7 +121,8 @@ def make_random_password(self, length=10, allowed_chars='abcdefghjkmnpqrstuvwxyz
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.
"""
Expand Down Expand Up @@ -151,11 +152,16 @@ def get_absolute_url(self):
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

Expand Down Expand Up @@ -194,30 +200,41 @@ def set_unusable_password(self):
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:
Expand All @@ -230,14 +247,22 @@ def has_perm(self, perm):
# 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

Expand Down Expand Up @@ -358,10 +383,10 @@ def _get_user_permissions(self):
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):
Expand Down
1 change: 1 addition & 0 deletions django/contrib/auth/tests/__init__.py
Expand Up @@ -4,6 +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.tokens import TOKEN_GENERATOR_TESTS

# The password for the fixture data users is 'password'
Expand Down
149 changes: 149 additions & 0 deletions django/contrib/auth/tests/auth_backends.py
@@ -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']))
8 changes: 8 additions & 0 deletions docs/internals/deprecation.txt
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 9bf652d

Please sign in to comment.