Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #11010 - Add a foundation for object permissions to authenticat…

…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...
commit 9bf652dfd6a738fd841471f6abd71cba1b206d9f 1 parent 2c2f5ae
Jannis Leidel authored December 10, 2009
7  django/contrib/auth/__init__.py
... ...
@@ -1,4 +1,5 @@
1 1
 import datetime
  2
+from warnings import warn
2 3
 from django.core.exceptions import ImproperlyConfigured
3 4
 from django.utils.importlib import import_module
4 5
 
@@ -19,6 +20,12 @@ def load_backend(path):
19 20
         cls = getattr(mod, attr)
20 21
     except AttributeError:
21 22
         raise ImproperlyConfigured, 'Module "%s" does not define a "%s" authentication backend' % (module, attr)
  23
+    try:
  24
+        getattr(cls, 'supports_object_permissions')
  25
+    except AttributeError:
  26
+        warn("Authentication backends without a `supports_object_permissions` attribute are deprecated. Please define it in %s." % cls,
  27
+             PendingDeprecationWarning)
  28
+        cls.supports_object_permissions = False
22 29
     return cls()
23 30
 
24 31
 def get_backends():
2  django/contrib/auth/backends.py
@@ -11,6 +11,8 @@ class ModelBackend(object):
11 11
     """
12 12
     Authenticates against django.contrib.auth.models.User.
13 13
     """
  14
+    supports_object_permissions = False
  15
+
14 16
     # TODO: Model, login attribute name and password attribute name should be
15 17
     # configurable.
16 18
     def authenticate(self, username=None, password=None):
57  django/contrib/auth/models.py
@@ -121,7 +121,8 @@ def make_random_password(self, length=10, allowed_chars='abcdefghjkmnpqrstuvwxyz
121 121
         return ''.join([choice(allowed_chars) for i in range(length)])
122 122
 
123 123
 class User(models.Model):
124  
-    """Users within the Django authentication system are represented by this model.
  124
+    """
  125
+    Users within the Django authentication system are represented by this model.
125 126
 
126 127
     Username and password are required. Other fields are optional.
127 128
     """
@@ -151,11 +152,16 @@ def get_absolute_url(self):
151 152
         return "/users/%s/" % urllib.quote(smart_str(self.username))
152 153
 
153 154
     def is_anonymous(self):
154  
-        "Always returns False. This is a way of comparing User objects to anonymous users."
  155
+        """
  156
+        Always returns False. This is a way of comparing User objects to
  157
+        anonymous users.
  158
+        """
155 159
         return False
156 160
 
157 161
     def is_authenticated(self):
158  
-        """Always return True. This is a way to tell if the user has been authenticated in templates.
  162
+        """
  163
+        Always return True. This is a way to tell if the user has been
  164
+        authenticated in templates.
159 165
         """
160 166
         return True
161 167
 
@@ -194,30 +200,41 @@ def set_unusable_password(self):
194 200
     def has_usable_password(self):
195 201
         return self.password != UNUSABLE_PASSWORD
196 202
 
197  
-    def get_group_permissions(self):
  203
+    def get_group_permissions(self, obj=None):
198 204
         """
199 205
         Returns a list of permission strings that this user has through
200 206
         his/her groups. This method queries all available auth backends.
  207
+        If an object is passed in, only permissions matching this object
  208
+        are returned.
201 209
         """
202 210
         permissions = set()
203 211
         for backend in auth.get_backends():
204 212
             if hasattr(backend, "get_group_permissions"):
205  
-                permissions.update(backend.get_group_permissions(self))
  213
+                if obj is not None and backend.supports_object_permissions:
  214
+                    group_permissions = backend.get_group_permissions(self, obj)
  215
+                else:
  216
+                    group_permissions = backend.get_group_permissions(self)
  217
+                permissions.update(group_permissions)
206 218
         return permissions
207 219
 
208  
-    def get_all_permissions(self):
  220
+    def get_all_permissions(self, obj=None):
209 221
         permissions = set()
210 222
         for backend in auth.get_backends():
211 223
             if hasattr(backend, "get_all_permissions"):
212  
-                permissions.update(backend.get_all_permissions(self))
  224
+                if obj is not None and backend.supports_object_permissions:
  225
+                    all_permissions = backend.get_all_permissions(self, obj)
  226
+                else:
  227
+                    all_permissions = backend.get_all_permissions(self)
  228
+                permissions.update(all_permissions)
213 229
         return permissions
214 230
 
215  
-    def has_perm(self, perm):
  231
+    def has_perm(self, perm, obj=None):
216 232
         """
217 233
         Returns True if the user has the specified permission. This method
218 234
         queries all available auth backends, but returns immediately if any
219 235
         backend returns True. Thus, a user who has permission from a single
220  
-        auth backend is assumed to have permission in general.
  236
+        auth backend is assumed to have permission in general. If an object
  237
+        is provided, permissions for this specific object are checked.
221 238
         """
222 239
         # Inactive users have no permissions.
223 240
         if not self.is_active:
@@ -230,14 +247,22 @@ def has_perm(self, perm):
230 247
         # Otherwise we need to check the backends.
231 248
         for backend in auth.get_backends():
232 249
             if hasattr(backend, "has_perm"):
233  
-                if backend.has_perm(self, perm):
234  
-                    return True
  250
+                if obj is not None and backend.supports_object_permissions:
  251
+                    if backend.has_perm(self, perm, obj):
  252
+                        return True
  253
+                else:
  254
+                    if backend.has_perm(self, perm):
  255
+                        return True
235 256
         return False
236 257
 
237  
-    def has_perms(self, perm_list):
238  
-        """Returns True if the user has each of the specified permissions."""
  258
+    def has_perms(self, perm_list, obj=None):
  259
+        """
  260
+        Returns True if the user has each of the specified permissions.
  261
+        If object is passed, it checks if the user has all required perms
  262
+        for this object.
  263
+        """
239 264
         for perm in perm_list:
240  
-            if not self.has_perm(perm):
  265
+            if not self.has_perm(perm, obj):
241 266
                 return False
242 267
         return True
243 268
 
@@ -358,10 +383,10 @@ def _get_user_permissions(self):
358 383
         return self._user_permissions
359 384
     user_permissions = property(_get_user_permissions)
360 385
 
361  
-    def has_perm(self, perm):
  386
+    def has_perm(self, perm, obj=None):
362 387
         return False
363 388
 
364  
-    def has_perms(self, perm_list):
  389
+    def has_perms(self, perm_list, obj=None):
365 390
         return False
366 391
 
367 392
     def has_module_perms(self, module):
1  django/contrib/auth/tests/__init__.py
@@ -4,6 +4,7 @@
4 4
 from django.contrib.auth.tests.forms import FORM_TESTS
5 5
 from django.contrib.auth.tests.remote_user \
6 6
         import RemoteUserTest, RemoteUserNoCreateTest, RemoteUserCustomTest
  7
+from django.contrib.auth.tests.auth_backends import BackendTest, RowlevelBackendTest
7 8
 from django.contrib.auth.tests.tokens import TOKEN_GENERATOR_TESTS
8 9
 
9 10
 # The password for the fixture data users is 'password'
149  django/contrib/auth/tests/auth_backends.py
... ...
@@ -0,0 +1,149 @@
  1
+from django.conf import settings
  2
+from django.contrib.auth.models import User, Group, Permission, AnonymousUser
  3
+from django.contrib.contenttypes.models import ContentType
  4
+from django.test import TestCase
  5
+
  6
+
  7
+class BackendTest(TestCase):
  8
+
  9
+    backend = 'django.contrib.auth.backends.ModelBackend'
  10
+
  11
+    def setUp(self):
  12
+        self.curr_auth = settings.AUTHENTICATION_BACKENDS
  13
+        settings.AUTHENTICATION_BACKENDS = (self.backend,)
  14
+        User.objects.create_user('test', 'test@example.com', 'test')
  15
+
  16
+    def tearDown(self):
  17
+        settings.AUTHENTICATION_BACKENDS = self.curr_auth
  18
+
  19
+    def test_has_perm(self):
  20
+        user = User.objects.get(username='test')
  21
+        self.assertEqual(user.has_perm('auth.test'), False)
  22
+        user.is_staff = True
  23
+        user.save()
  24
+        self.assertEqual(user.has_perm('auth.test'), False)
  25
+        user.is_superuser = True
  26
+        user.save()
  27
+        self.assertEqual(user.has_perm('auth.test'), True)
  28
+        user.is_staff = False
  29
+        user.is_superuser = False
  30
+        user.save()
  31
+        self.assertEqual(user.has_perm('auth.test'), False)
  32
+
  33
+    def test_custom_perms(self):
  34
+        user = User.objects.get(username='test')
  35
+        content_type=ContentType.objects.get_for_model(Group)
  36
+        perm = Permission.objects.create(name='test', content_type=content_type, codename='test')
  37
+        user.user_permissions.add(perm)
  38
+        user.save()
  39
+
  40
+        # reloading user to purge the _perm_cache
  41
+        user = User.objects.get(username='test')
  42
+        self.assertEqual(user.get_all_permissions() == set([u'auth.test']), True)
  43
+        self.assertEqual(user.get_group_permissions(), set([]))
  44
+        self.assertEqual(user.has_module_perms('Group'), False)
  45
+        self.assertEqual(user.has_module_perms('auth'), True)
  46
+        perm = Permission.objects.create(name='test2', content_type=content_type, codename='test2')
  47
+        user.user_permissions.add(perm)
  48
+        user.save()
  49
+        perm = Permission.objects.create(name='test3', content_type=content_type, codename='test3')
  50
+        user.user_permissions.add(perm)
  51
+        user.save()
  52
+        user = User.objects.get(username='test')
  53
+        self.assertEqual(user.get_all_permissions(), set([u'auth.test2', u'auth.test', u'auth.test3']))
  54
+        self.assertEqual(user.has_perm('test'), False)
  55
+        self.assertEqual(user.has_perm('auth.test'), True)
  56
+        self.assertEqual(user.has_perms(['auth.test2', 'auth.test3']), True)
  57
+        perm = Permission.objects.create(name='test_group', content_type=content_type, codename='test_group')
  58
+        group = Group.objects.create(name='test_group')
  59
+        group.permissions.add(perm)
  60
+        group.save()
  61
+        user.groups.add(group)
  62
+        user = User.objects.get(username='test')
  63
+        exp = set([u'auth.test2', u'auth.test', u'auth.test3', u'auth.test_group'])
  64
+        self.assertEqual(user.get_all_permissions(), exp)
  65
+        self.assertEqual(user.get_group_permissions(), set([u'auth.test_group']))
  66
+        self.assertEqual(user.has_perms(['auth.test3', 'auth.test_group']), True)
  67
+
  68
+        user = AnonymousUser()
  69
+        self.assertEqual(user.has_perm('test'), False)
  70
+        self.assertEqual(user.has_perms(['auth.test2', 'auth.test3']), False)
  71
+
  72
+
  73
+class TestObj(object):
  74
+    pass
  75
+
  76
+
  77
+class SimpleRowlevelBackend(object):
  78
+    supports_object_permissions = True
  79
+
  80
+    def has_perm(self, user, perm, obj=None):
  81
+        if not obj:
  82
+            return # We only support row level perms
  83
+
  84
+        if isinstance(obj, TestObj):
  85
+            if user.username == 'test2':
  86
+                return True
  87
+            elif isinstance(user, AnonymousUser) and perm == 'anon':
  88
+                return True
  89
+        return False
  90
+
  91
+    def get_all_permissions(self, user, obj=None):
  92
+        if not obj:
  93
+            return [] # We only support row level perms
  94
+
  95
+        if not isinstance(obj, TestObj):
  96
+            return ['none']
  97
+
  98
+        if user.username == 'test2':
  99
+            return ['simple', 'advanced']
  100
+        else:
  101
+            return ['simple']
  102
+
  103
+    def get_group_permissions(self, user, obj=None):
  104
+        if not obj:
  105
+            return # We only support row level perms
  106
+
  107
+        if not isinstance(obj, TestObj):
  108
+            return ['none']
  109
+
  110
+        if 'test_group' in [group.name for group in user.groups.all()]:
  111
+            return ['group_perm']
  112
+        else:
  113
+            return ['none']
  114
+
  115
+
  116
+class RowlevelBackendTest(TestCase):
  117
+
  118
+    backend = 'django.contrib.auth.tests.auth_backends.SimpleRowlevelBackend'
  119
+
  120
+    def setUp(self):
  121
+        self.curr_auth = settings.AUTHENTICATION_BACKENDS
  122
+        settings.AUTHENTICATION_BACKENDS = self.curr_auth + (self.backend,)
  123
+        self.user1 = User.objects.create_user('test', 'test@example.com', 'test')
  124
+        self.user2 = User.objects.create_user('test2', 'test2@example.com', 'test')
  125
+        self.user3 = AnonymousUser()
  126
+        self.user4 = User.objects.create_user('test4', 'test4@example.com', 'test')
  127
+
  128
+    def tearDown(self):
  129
+        settings.AUTHENTICATION_BACKENDS = self.curr_auth
  130
+
  131
+    def test_has_perm(self):
  132
+        self.assertEqual(self.user1.has_perm('perm', TestObj()), False)
  133
+        self.assertEqual(self.user2.has_perm('perm', TestObj()), True)
  134
+        self.assertEqual(self.user2.has_perm('perm'), False)
  135
+        self.assertEqual(self.user2.has_perms(['simple', 'advanced'], TestObj()), True)
  136
+        self.assertEqual(self.user3.has_perm('perm', TestObj()), False)
  137
+        self.assertEqual(self.user3.has_perm('anon', TestObj()), False)
  138
+        self.assertEqual(self.user3.has_perms(['simple', 'advanced'], TestObj()), False)
  139
+
  140
+    def test_get_all_permissions(self):
  141
+        self.assertEqual(self.user1.get_all_permissions(TestObj()), set(['simple']))
  142
+        self.assertEqual(self.user2.get_all_permissions(TestObj()), set(['simple', 'advanced']))
  143
+        self.assertEqual(self.user2.get_all_permissions(), set([]))
  144
+
  145
+    def test_get_group_permissions(self):
  146
+        content_type=ContentType.objects.get_for_model(Group)
  147
+        group = Group.objects.create(name='test_group')
  148
+        self.user4.groups.add(group)
  149
+        self.assertEqual(self.user4.get_group_permissions(TestObj()), set(['group_perm']))
8  docs/internals/deprecation.txt
@@ -13,6 +13,10 @@ their deprecation, as per the :ref:`Django deprecation policy
13 13
           hooking up admin URLs.  This has been deprecated since the 1.1
14 14
           release.
15 15
 
  16
+        * Authentication backends need to define the boolean attribute
  17
+          ``supports_object_permissions``. The old backend style is deprecated
  18
+          since the 1.2 release.
  19
+
16 20
     * 1.4
17 21
         * ``CsrfResponseMiddleware``.  This has been deprecated since the 1.2
18 22
           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
36 40
           :ref:`messages framework <ref-contrib-messages>` should be used 
37 41
           instead.
38 42
 
  43
+        * Authentication backends need to support the ``obj`` parameter for
  44
+          permission checking. The ``supports_object_permissions`` variable
  45
+          is not checked any longer and can be removed.
  46
+
39 47
     * 2.0
40 48
         * ``django.views.defaults.shortcut()``. This function has been moved
41 49
           to ``django.contrib.contenttypes.views.shortcut()`` as part of the
49  docs/topics/auth.txt
@@ -202,29 +202,49 @@ Methods
202 202
         :meth:`~django.contrib.auth.models.User.set_unusable_password()` has
203 203
         been called for this user.
204 204
 
205  
-    .. method:: models.User.get_group_permissions()
  205
+    .. method:: models.User.get_group_permissions(obj=None)
206 206
 
207 207
         Returns a list of permission strings that the user has, through his/her
208 208
         groups.
209 209
 
210  
-    .. method:: models.User.get_all_permissions()
  210
+        .. versionadded:: 1.2
  211
+
  212
+        If ``obj`` is passed in, only returns the group permissions for
  213
+        this specific object.
  214
+
  215
+    .. method:: models.User.get_all_permissions(obj=None)
211 216
 
212 217
         Returns a list of permission strings that the user has, both through
213 218
         group and user permissions.
214 219
 
215  
-    .. method:: models.User.has_perm(perm)
  220
+        .. versionadded:: 1.2
  221
+
  222
+        If ``obj`` is passed in, only returns the permissions for this
  223
+        specific object.
  224
+
  225
+    .. method:: models.User.has_perm(perm, obj=None)
216 226
 
217 227
         Returns ``True`` if the user has the specified permission, where perm is
218 228
         in the format ``"<app label>.<permission codename>"``.
219 229
         If the user is inactive, this method will always return ``False``.
220 230
 
221  
-    .. method:: models.User.has_perms(perm_list)
  231
+        .. versionadded:: 1.2
  232
+
  233
+        If ``obj`` is passed in, this method won't check for a permission for
  234
+        the model, but for this specific object.
  235
+
  236
+    .. method:: models.User.has_perms(perm_list, obj=None)
222 237
 
223 238
         Returns ``True`` if the user has each of the specified permissions,
224 239
         where each perm is in the format 
225 240
         ``"<app label>.<permission codename>"``. If the user is inactive,
226 241
         this method will always return ``False``.
227 242
 
  243
+        .. versionadded:: 1.2
  244
+
  245
+        If ``obj`` is passed in, this method won't check for permissions for
  246
+        the model, but for the specific object.
  247
+
228 248
     .. method:: models.User.has_module_perms(package_name)
229 249
 
230 250
         Returns ``True`` if the user has any permissions in the given package
@@ -1521,3 +1541,24 @@ A full authorization implementation can be found in
1521 1541
 the ``auth_permission`` table most of the time.
1522 1542
 
1523 1543
 .. _django/contrib/auth/backends.py: http://code.djangoproject.com/browser/django/trunk/django/contrib/auth/backends.py
  1544
+
  1545
+Handling object permissions
  1546
+---------------------------
  1547
+
  1548
+Django's permission framework has a foundation for object permissions, though
  1549
+there is no implementation for it in the core. That means that checking for
  1550
+object permissions will always return ``False`` or an empty list (depending on
  1551
+the check performed).
  1552
+
  1553
+To enable object permissions in your own
  1554
+:ref:`authentication backend <ref-authentication-backends>` you'll just have
  1555
+to allow passing an ``obj`` parameter to the permission methods and set the
  1556
+``supports_objects_permissions`` class attribute to ``True``.
  1557
+
  1558
+A nonexistent ``supports_objects_permissions`` will raise a hidden
  1559
+``PendingDeprecationWarning`` if used in Django 1.2. In Django 1.3, this
  1560
+warning will be upgraded to a ``DeprecationWarning``, which will be displayed
  1561
+loudly. Additionally ``supports_objects_permissions`` will be set to ``False``.
  1562
+Django 1.4 will assume that every backend supports object permissions and
  1563
+won't check for the existence of ``supports_objects_permissions``, which
  1564
+means not supporting ``obj`` as a parameter will raise a ``TypeError``.
0  tests/regressiontests/auth_backends/__init__.py
No changes.
0  tests/regressiontests/auth_backends/models.py
No changes.
78  tests/regressiontests/auth_backends/tests.py
... ...
@@ -1,78 +0,0 @@
1  
-try:
2  
-    set
3  
-except NameError:
4  
-    from sets import Set as set     # Python 2.3 fallback
5  
-
6  
-__test__ = {'API_TESTS': """
7  
->>> from django.contrib.auth.models import User, Group, Permission, AnonymousUser
8  
->>> from django.contrib.contenttypes.models import ContentType
9  
-
10  
-# No Permissions assigned yet, should return False except for superuser
11  
-
12  
->>> user = User.objects.create_user('test', 'test@example.com', 'test')
13  
->>> user.has_perm("auth.test")
14  
-False
15  
->>> user.is_staff=True
16  
->>> user.save()
17  
->>> user.has_perm("auth.test")
18  
-False
19  
->>> user.is_superuser=True
20  
->>> user.save()
21  
->>> user.has_perm("auth.test")
22  
-True
23  
->>> user.is_staff = False
24  
->>> user.is_superuser = False
25  
->>> user.save()
26  
->>> user.has_perm("auth.test")
27  
-False
28  
->>> content_type=ContentType.objects.get_for_model(Group)
29  
->>> perm = Permission.objects.create(name="test", content_type=content_type, codename="test")
30  
->>> user.user_permissions.add(perm)
31  
->>> user.save()
32  
-
33  
-# reloading user to purge the _perm_cache
34  
-
35  
->>> user = User.objects.get(username="test")
36  
->>> user.get_all_permissions() == set([u'auth.test'])
37  
-True
38  
->>> user.get_group_permissions() == set([])
39  
-True
40  
->>> user.has_module_perms("Group")
41  
-False
42  
->>> user.has_module_perms("auth")
43  
-True
44  
->>> perm = Permission.objects.create(name="test2", content_type=content_type, codename="test2")
45  
->>> user.user_permissions.add(perm)
46  
->>> user.save()
47  
->>> perm = Permission.objects.create(name="test3", content_type=content_type, codename="test3")
48  
->>> user.user_permissions.add(perm)
49  
->>> user.save()
50  
->>> user = User.objects.get(username="test")
51  
->>> user.get_all_permissions() == set([u'auth.test2', u'auth.test', u'auth.test3'])
52  
-True
53  
->>> user.has_perm('test')
54  
-False
55  
->>> user.has_perm('auth.test')
56  
-True
57  
->>> user.has_perms(['auth.test2', 'auth.test3'])
58  
-True
59  
->>> perm = Permission.objects.create(name="test_group", content_type=content_type, codename="test_group")
60  
->>> group = Group.objects.create(name='test_group')
61  
->>> group.permissions.add(perm)
62  
->>> group.save()
63  
->>> user.groups.add(group)
64  
->>> user = User.objects.get(username="test")
65  
->>> exp = set([u'auth.test2', u'auth.test', u'auth.test3', u'auth.test_group'])
66  
->>> user.get_all_permissions() == exp
67  
-True
68  
->>> user.get_group_permissions() == set([u'auth.test_group'])
69  
-True
70  
->>> user.has_perms(['auth.test3', 'auth.test_group'])
71  
-True
72  
-
73  
->>> user = AnonymousUser()
74  
->>> user.has_perm('test')
75  
-False
76  
->>> user.has_perms(['auth.test2', 'auth.test3'])
77  
-False
78  
-"""}

0 notes on commit 9bf652d

Please sign in to comment.
Something went wrong with that request. Please try again.