Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

[1.5.X} Fixed #19412 -- Added PermissionsMixin to the auth.User heira…

…rchy.

This makes it easier to make a ModelBackend-compliant (with regards to
permissions) User model.

Thanks to cdestigter for the report about the relationship between
ModelBackend and permissions, and to the many users on django-dev that
contributed to the discussion about mixins.

Backport of 47e1df8 from master.
  • Loading branch information...
commit 311bd0055d6302ce42d3831f95d8b255986ddc40 1 parent a7465ee
Russell Keith-Magee authored December 15, 2012
158  django/contrib/auth/models.py
@@ -195,38 +195,6 @@ def create_superuser(self, username, email, password, **extra_fields):
195 195
         return u
196 196
 
197 197
 
198  
-# A few helper functions for common logic between User and AnonymousUser.
199  
-def _user_get_all_permissions(user, obj):
200  
-    permissions = set()
201  
-    for backend in auth.get_backends():
202  
-        if hasattr(backend, "get_all_permissions"):
203  
-            if obj is not None:
204  
-                permissions.update(backend.get_all_permissions(user, obj))
205  
-            else:
206  
-                permissions.update(backend.get_all_permissions(user))
207  
-    return permissions
208  
-
209  
-
210  
-def _user_has_perm(user, perm, obj):
211  
-    for backend in auth.get_backends():
212  
-        if hasattr(backend, "has_perm"):
213  
-            if obj is not None:
214  
-                if backend.has_perm(user, perm, obj):
215  
-                    return True
216  
-            else:
217  
-                if backend.has_perm(user, perm):
218  
-                    return True
219  
-    return False
220  
-
221  
-
222  
-def _user_has_module_perms(user, app_label):
223  
-    for backend in auth.get_backends():
224  
-        if hasattr(backend, "has_module_perms"):
225  
-            if backend.has_module_perms(user, app_label):
226  
-                return True
227  
-    return False
228  
-
229  
-
230 198
 @python_2_unicode_compatible
231 199
 class AbstractBaseUser(models.Model):
232 200
     password = models.CharField(_('password'), max_length=128)
@@ -290,32 +258,46 @@ def get_short_name(self):
290 258
         raise NotImplementedError()
291 259
 
292 260
 
293  
-class AbstractUser(AbstractBaseUser):
294  
-    """
295  
-    An abstract base class implementing a fully featured User model with
296  
-    admin-compliant permissions.
  261
+# A few helper functions for common logic between User and AnonymousUser.
  262
+def _user_get_all_permissions(user, obj):
  263
+    permissions = set()
  264
+    for backend in auth.get_backends():
  265
+        if hasattr(backend, "get_all_permissions"):
  266
+            if obj is not None:
  267
+                permissions.update(backend.get_all_permissions(user, obj))
  268
+            else:
  269
+                permissions.update(backend.get_all_permissions(user))
  270
+    return permissions
297 271
 
298  
-    Username, password and email are required. Other fields are optional.
  272
+
  273
+def _user_has_perm(user, perm, obj):
  274
+    for backend in auth.get_backends():
  275
+        if hasattr(backend, "has_perm"):
  276
+            if obj is not None:
  277
+                if backend.has_perm(user, perm, obj):
  278
+                    return True
  279
+            else:
  280
+                if backend.has_perm(user, perm):
  281
+                    return True
  282
+    return False
  283
+
  284
+
  285
+def _user_has_module_perms(user, app_label):
  286
+    for backend in auth.get_backends():
  287
+        if hasattr(backend, "has_module_perms"):
  288
+            if backend.has_module_perms(user, app_label):
  289
+                return True
  290
+    return False
  291
+
  292
+
  293
+class PermissionsMixin(models.Model):
  294
+    """
  295
+    A mixin class that adds the fields and methods necessary to support
  296
+    Django's Group and Permission model using the ModelBackend.
299 297
     """
300  
-    username = models.CharField(_('username'), max_length=30, unique=True,
301  
-        help_text=_('Required. 30 characters or fewer. Letters, numbers and '
302  
-                    '@/./+/-/_ characters'),
303  
-        validators=[
304  
-            validators.RegexValidator(re.compile('^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid')
305  
-        ])
306  
-    first_name = models.CharField(_('first name'), max_length=30, blank=True)
307  
-    last_name = models.CharField(_('last name'), max_length=30, blank=True)
308  
-    email = models.EmailField(_('email address'), blank=True)
309  
-    is_staff = models.BooleanField(_('staff status'), default=False,
310  
-        help_text=_('Designates whether the user can log into this admin '
311  
-                    'site.'))
312  
-    is_active = models.BooleanField(_('active'), default=True,
313  
-        help_text=_('Designates whether this user should be treated as '
314  
-                    'active. Unselect this instead of deleting accounts.'))
315 298
     is_superuser = models.BooleanField(_('superuser status'), default=False,
316 299
         help_text=_('Designates that this user has all permissions without '
317 300
                     'explicitly assigning them.'))
318  
-    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
319 301
     groups = models.ManyToManyField(Group, verbose_name=_('groups'),
320 302
         blank=True, help_text=_('The groups this user belongs to. A user will '
321 303
                                 'get all permissions granted to each of '
@@ -324,30 +306,9 @@ class AbstractUser(AbstractBaseUser):
324 306
         verbose_name=_('user permissions'), blank=True,
325 307
         help_text='Specific permissions for this user.')
326 308
 
327  
-    objects = UserManager()
328  
-
329  
-    USERNAME_FIELD = 'username'
330  
-    REQUIRED_FIELDS = ['email']
331  
-
332 309
     class Meta:
333  
-        verbose_name = _('user')
334  
-        verbose_name_plural = _('users')
335 310
         abstract = True
336 311
 
337  
-    def get_absolute_url(self):
338  
-        return "/users/%s/" % urlquote(self.username)
339  
-
340  
-    def get_full_name(self):
341  
-        """
342  
-        Returns the first_name plus the last_name, with a space in between.
343  
-        """
344  
-        full_name = '%s %s' % (self.first_name, self.last_name)
345  
-        return full_name.strip()
346  
-
347  
-    def get_short_name(self):
348  
-        "Returns the short name for the user."
349  
-        return self.first_name
350  
-
351 312
     def get_group_permissions(self, obj=None):
352 313
         """
353 314
         Returns a list of permission strings that this user has through his/her
@@ -405,6 +366,55 @@ def has_module_perms(self, app_label):
405 366
 
406 367
         return _user_has_module_perms(self, app_label)
407 368
 
  369
+
  370
+class AbstractUser(AbstractBaseUser, PermissionsMixin):
  371
+    """
  372
+    An abstract base class implementing a fully featured User model with
  373
+    admin-compliant permissions.
  374
+
  375
+    Username, password and email are required. Other fields are optional.
  376
+    """
  377
+    username = models.CharField(_('username'), max_length=30, unique=True,
  378
+        help_text=_('Required. 30 characters or fewer. Letters, numbers and '
  379
+                    '@/./+/-/_ characters'),
  380
+        validators=[
  381
+            validators.RegexValidator(re.compile('^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid')
  382
+        ])
  383
+    first_name = models.CharField(_('first name'), max_length=30, blank=True)
  384
+    last_name = models.CharField(_('last name'), max_length=30, blank=True)
  385
+    email = models.EmailField(_('email address'), blank=True)
  386
+    is_staff = models.BooleanField(_('staff status'), default=False,
  387
+        help_text=_('Designates whether the user can log into this admin '
  388
+                    'site.'))
  389
+    is_active = models.BooleanField(_('active'), default=True,
  390
+        help_text=_('Designates whether this user should be treated as '
  391
+                    'active. Unselect this instead of deleting accounts.'))
  392
+    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
  393
+
  394
+    objects = UserManager()
  395
+
  396
+    USERNAME_FIELD = 'username'
  397
+    REQUIRED_FIELDS = ['email']
  398
+
  399
+    class Meta:
  400
+        verbose_name = _('user')
  401
+        verbose_name_plural = _('users')
  402
+        abstract = True
  403
+
  404
+    def get_absolute_url(self):
  405
+        return "/users/%s/" % urlquote(self.username)
  406
+
  407
+    def get_full_name(self):
  408
+        """
  409
+        Returns the first_name plus the last_name, with a space in between.
  410
+        """
  411
+        full_name = '%s %s' % (self.first_name, self.last_name)
  412
+        return full_name.strip()
  413
+
  414
+    def get_short_name(self):
  415
+        "Returns the short name for the user."
  416
+        return self.first_name
  417
+
408 418
     def email_user(self, subject, message, from_email=None):
409 419
         """
410 420
         Sends an email to this User.
49  django/contrib/auth/tests/auth_backends.py
@@ -4,7 +4,7 @@
4 4
 from django.conf import settings
5 5
 from django.contrib.auth.models import User, Group, Permission, AnonymousUser
6 6
 from django.contrib.auth.tests.utils import skipIfCustomUser
7  
-from django.contrib.auth.tests.custom_user import ExtensionUser
  7
+from django.contrib.auth.tests.custom_user import ExtensionUser, CustomPermissionsUser
8 8
 from django.contrib.contenttypes.models import ContentType
9 9
 from django.core.exceptions import ImproperlyConfigured
10 10
 from django.test import TestCase
@@ -33,7 +33,7 @@ def tearDown(self):
33 33
         ContentType.objects.clear_cache()
34 34
 
35 35
     def test_has_perm(self):
36  
-        user = self.UserModel.objects.get(username='test')
  36
+        user = self.UserModel.objects.get(pk=self.user.pk)
37 37
         self.assertEqual(user.has_perm('auth.test'), False)
38 38
         user.is_staff = True
39 39
         user.save()
@@ -52,14 +52,14 @@ def test_has_perm(self):
52 52
         self.assertEqual(user.has_perm('auth.test'), False)
53 53
 
54 54
     def test_custom_perms(self):
55  
-        user = self.UserModel.objects.get(username='test')
  55
+        user = self.UserModel.objects.get(pk=self.user.pk)
56 56
         content_type = ContentType.objects.get_for_model(Group)
57 57
         perm = Permission.objects.create(name='test', content_type=content_type, codename='test')
58 58
         user.user_permissions.add(perm)
59 59
         user.save()
60 60
 
61 61
         # reloading user to purge the _perm_cache
62  
-        user = self.UserModel.objects.get(username='test')
  62
+        user = self.UserModel.objects.get(pk=self.user.pk)
63 63
         self.assertEqual(user.get_all_permissions() == set(['auth.test']), True)
64 64
         self.assertEqual(user.get_group_permissions(), set([]))
65 65
         self.assertEqual(user.has_module_perms('Group'), False)
@@ -70,7 +70,7 @@ def test_custom_perms(self):
70 70
         perm = Permission.objects.create(name='test3', content_type=content_type, codename='test3')
71 71
         user.user_permissions.add(perm)
72 72
         user.save()
73  
-        user = self.UserModel.objects.get(username='test')
  73
+        user = self.UserModel.objects.get(pk=self.user.pk)
74 74
         self.assertEqual(user.get_all_permissions(), set(['auth.test2', 'auth.test', 'auth.test3']))
75 75
         self.assertEqual(user.has_perm('test'), False)
76 76
         self.assertEqual(user.has_perm('auth.test'), True)
@@ -80,7 +80,7 @@ def test_custom_perms(self):
80 80
         group.permissions.add(perm)
81 81
         group.save()
82 82
         user.groups.add(group)
83  
-        user = self.UserModel.objects.get(username='test')
  83
+        user = self.UserModel.objects.get(pk=self.user.pk)
84 84
         exp = set(['auth.test2', 'auth.test', 'auth.test3', 'auth.test_group'])
85 85
         self.assertEqual(user.get_all_permissions(), exp)
86 86
         self.assertEqual(user.get_group_permissions(), set(['auth.test_group']))
@@ -92,7 +92,7 @@ def test_custom_perms(self):
92 92
 
93 93
     def test_has_no_object_perm(self):
94 94
         """Regressiontest for #12462"""
95  
-        user = self.UserModel.objects.get(username='test')
  95
+        user = self.UserModel.objects.get(pk=self.user.pk)
96 96
         content_type = ContentType.objects.get_for_model(Group)
97 97
         perm = Permission.objects.create(name='test', content_type=content_type, codename='test')
98 98
         user.user_permissions.add(perm)
@@ -105,7 +105,7 @@ def test_has_no_object_perm(self):
105 105
 
106 106
     def test_get_all_superuser_permissions(self):
107 107
         "A superuser has all permissions. Refs #14795"
108  
-        user = self.UserModel.objects.get(username='test2')
  108
+        user = self.UserModel.objects.get(pk=self.superuser.pk)
109 109
         self.assertEqual(len(user.get_all_permissions()), len(Permission.objects.all()))
110 110
 
111 111
 
@@ -117,12 +117,12 @@ class ModelBackendTest(BaseModelBackendTest, TestCase):
117 117
     UserModel = User
118 118
 
119 119
     def create_users(self):
120  
-        User.objects.create_user(
  120
+        self.user = User.objects.create_user(
121 121
             username='test',
122 122
             email='test@example.com',
123 123
             password='test',
124 124
         )
125  
-        User.objects.create_superuser(
  125
+        self.superuser = User.objects.create_superuser(
126 126
             username='test2',
127 127
             email='test2@example.com',
128 128
             password='test',
@@ -150,13 +150,13 @@ class ExtensionUserModelBackendTest(BaseModelBackendTest, TestCase):
150 150
     UserModel = ExtensionUser
151 151
 
152 152
     def create_users(self):
153  
-        ExtensionUser.objects.create_user(
  153
+        self.user = ExtensionUser.objects.create_user(
154 154
             username='test',
155 155
             email='test@example.com',
156 156
             password='test',
157 157
             date_of_birth=date(2006, 4, 25)
158 158
         )
159  
-        ExtensionUser.objects.create_superuser(
  159
+        self.superuser = ExtensionUser.objects.create_superuser(
160 160
             username='test2',
161 161
             email='test2@example.com',
162 162
             password='test',
@@ -164,6 +164,31 @@ def create_users(self):
164 164
         )
165 165
 
166 166
 
  167
+@override_settings(AUTH_USER_MODEL='auth.CustomPermissionsUser')
  168
+class CustomPermissionsUserModelBackendTest(BaseModelBackendTest, TestCase):
  169
+    """
  170
+    Tests for the ModelBackend using the CustomPermissionsUser model.
  171
+
  172
+    As with the ExtensionUser test, this isn't a perfect test, because both
  173
+    the User and CustomPermissionsUser are synchronized to the database,
  174
+    which wouldn't ordinary happen in production.
  175
+    """
  176
+
  177
+    UserModel = CustomPermissionsUser
  178
+
  179
+    def create_users(self):
  180
+        self.user = CustomPermissionsUser.objects.create_user(
  181
+            email='test@example.com',
  182
+            password='test',
  183
+            date_of_birth=date(2006, 4, 25)
  184
+        )
  185
+        self.superuser = CustomPermissionsUser.objects.create_superuser(
  186
+            email='test2@example.com',
  187
+            password='test',
  188
+            date_of_birth=date(1976, 11, 8)
  189
+        )
  190
+
  191
+
167 192
 class TestObj(object):
168 193
     pass
169 194
 
43  django/contrib/auth/tests/custom_user.py
... ...
@@ -1,5 +1,11 @@
1 1
 from django.db import models
2  
-from django.contrib.auth.models import BaseUserManager, AbstractBaseUser, AbstractUser, UserManager
  2
+from django.contrib.auth.models import (
  3
+    BaseUserManager,
  4
+    AbstractBaseUser,
  5
+    AbstractUser,
  6
+    UserManager,
  7
+    PermissionsMixin
  8
+)
3 9
 
4 10
 
5 11
 # The custom User uses email as the unique identifier, and requires
@@ -90,6 +96,40 @@ class Meta:
90 96
         app_label = 'auth'
91 97
 
92 98
 
  99
+# The CustomPermissionsUser users email as the identifier, but uses the normal
  100
+# Django permissions model. This allows us to check that the PermissionsMixin
  101
+# includes everything that is needed to interact with the ModelBackend.
  102
+
  103
+class CustomPermissionsUserManager(CustomUserManager):
  104
+    def create_superuser(self, email, password, date_of_birth):
  105
+        u = self.create_user(email, password=password, date_of_birth=date_of_birth)
  106
+        u.is_superuser = True
  107
+        u.save(using=self._db)
  108
+        return u
  109
+
  110
+
  111
+class CustomPermissionsUser(AbstractBaseUser, PermissionsMixin):
  112
+    email = models.EmailField(verbose_name='email address', max_length=255, unique=True)
  113
+    date_of_birth = models.DateField()
  114
+
  115
+    objects = CustomPermissionsUserManager()
  116
+
  117
+    USERNAME_FIELD = 'email'
  118
+    REQUIRED_FIELDS = ['date_of_birth']
  119
+
  120
+    class Meta:
  121
+        app_label = 'auth'
  122
+
  123
+    def get_full_name(self):
  124
+        return self.email
  125
+
  126
+    def get_short_name(self):
  127
+        return self.email
  128
+
  129
+    def __unicode__(self):
  130
+        return self.email
  131
+
  132
+
93 133
 class IsActiveTestUser1(AbstractBaseUser):
94 134
     """
95 135
     This test user class and derivatives test the default is_active behavior
@@ -104,4 +144,3 @@ class Meta:
104 144
         app_label = 'auth'
105 145
 
106 146
     # the is_active attr is provided by AbstractBaseUser
107  
-
70  docs/topics/auth.txt
@@ -2136,6 +2136,76 @@ override any of the definitions that refer to fields on
2136 2136
 :class:`~django.contrib.auth.models.AbstractUser` that aren't on your
2137 2137
 custom User class.
2138 2138
 
  2139
+Custom users and permissions
  2140
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  2141
+
  2142
+To make it easy to include Django's permission framework into your own User
  2143
+class, Django provides :class:`~django.contrib.auth.model.PermissionsMixin`.
  2144
+This is an abstract model you can include in the class heirarchy for your User
  2145
+model, giving you all the methods and database fields necessary to support
  2146
+Django's permission model.
  2147
+
  2148
+:class:`~django.contrib.auth.model.PermissionsMixin` provides the following
  2149
+methods and attributes:
  2150
+
  2151
+.. class:: models.PermissionsMixin
  2152
+
  2153
+    .. attribute:: models.PermissionsMixin.is_superuser
  2154
+
  2155
+        Boolean. Designates that this user has all permissions without
  2156
+        explicitly assigning them.
  2157
+
  2158
+    .. method:: models.PermissionsMixin.get_group_permissions(obj=None)
  2159
+
  2160
+        Returns a set of permission strings that the user has, through his/her
  2161
+        groups.
  2162
+
  2163
+        If ``obj`` is passed in, only returns the group permissions for
  2164
+        this specific object.
  2165
+
  2166
+    .. method:: models.PermissionsMixin.get_all_permissions(obj=None)
  2167
+
  2168
+        Returns a set of permission strings that the user has, both through
  2169
+        group and user permissions.
  2170
+
  2171
+        If ``obj`` is passed in, only returns the permissions for this
  2172
+        specific object.
  2173
+
  2174
+    .. method:: models.PermissionsMixin.has_perm(perm, obj=None)
  2175
+
  2176
+        Returns ``True`` if the user has the specified permission, where perm is
  2177
+        in the format ``"<app label>.<permission codename>"`` (see
  2178
+        `permissions`_). If the user is inactive, this method will
  2179
+        always return ``False``.
  2180
+
  2181
+        If ``obj`` is passed in, this method won't check for a permission for
  2182
+        the model, but for this specific object.
  2183
+
  2184
+    .. method:: models.PermissionsMixin.has_perms(perm_list, obj=None)
  2185
+
  2186
+        Returns ``True`` if the user has each of the specified permissions,
  2187
+        where each perm is in the format
  2188
+        ``"<app label>.<permission codename>"``. If the user is inactive,
  2189
+        this method will always return ``False``.
  2190
+
  2191
+        If ``obj`` is passed in, this method won't check for permissions for
  2192
+        the model, but for the specific object.
  2193
+
  2194
+    .. method:: models.PermissionsMixin.has_module_perms(package_name)
  2195
+
  2196
+        Returns ``True`` if the user has any permissions in the given package
  2197
+        (the Django app label). If the user is inactive, this method will
  2198
+        always return ``False``.
  2199
+
  2200
+.. admonition:: ModelBackend
  2201
+
  2202
+    If you don't include the
  2203
+    :class:`~django.contrib.auth.model.PermissionsMixin`, you must ensure you
  2204
+    don't invoke the permissions methods on ``ModelBackend``. ``ModelBackend``
  2205
+    assumes that certain fields are available on your user model. If your User
  2206
+    model doesn't provide  those fields, you will receive database errors when
  2207
+    you check permissions.
  2208
+
2139 2209
 Custom users and Proxy models
2140 2210
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2141 2211
 

0 notes on commit 311bd00

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