Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #19412 -- Added PermissionsMixin to the auth.User heirarchy.

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.
  • Loading branch information...
commit 47e1df896b17aaaa97b73ef64010a7df4ea3d8d6 1 parent bd414ae
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, PermissionDenied
10 10
 from django.contrib.auth import authenticate
@@ -34,7 +34,7 @@ def tearDown(self):
34 34
         ContentType.objects.clear_cache()
35 35
 
36 36
     def test_has_perm(self):
37  
-        user = self.UserModel.objects.get(username='test')
  37
+        user = self.UserModel.objects.get(pk=self.user.pk)
38 38
         self.assertEqual(user.has_perm('auth.test'), False)
39 39
         user.is_staff = True
40 40
         user.save()
@@ -53,14 +53,14 @@ def test_has_perm(self):
53 53
         self.assertEqual(user.has_perm('auth.test'), False)
54 54
 
55 55
     def test_custom_perms(self):
56  
-        user = self.UserModel.objects.get(username='test')
  56
+        user = self.UserModel.objects.get(pk=self.user.pk)
57 57
         content_type = ContentType.objects.get_for_model(Group)
58 58
         perm = Permission.objects.create(name='test', content_type=content_type, codename='test')
59 59
         user.user_permissions.add(perm)
60 60
         user.save()
61 61
 
62 62
         # reloading user to purge the _perm_cache
63  
-        user = self.UserModel.objects.get(username='test')
  63
+        user = self.UserModel.objects.get(pk=self.user.pk)
64 64
         self.assertEqual(user.get_all_permissions() == set(['auth.test']), True)
65 65
         self.assertEqual(user.get_group_permissions(), set([]))
66 66
         self.assertEqual(user.has_module_perms('Group'), False)
@@ -71,7 +71,7 @@ def test_custom_perms(self):
71 71
         perm = Permission.objects.create(name='test3', content_type=content_type, codename='test3')
72 72
         user.user_permissions.add(perm)
73 73
         user.save()
74  
-        user = self.UserModel.objects.get(username='test')
  74
+        user = self.UserModel.objects.get(pk=self.user.pk)
75 75
         self.assertEqual(user.get_all_permissions(), set(['auth.test2', 'auth.test', 'auth.test3']))
76 76
         self.assertEqual(user.has_perm('test'), False)
77 77
         self.assertEqual(user.has_perm('auth.test'), True)
@@ -81,7 +81,7 @@ def test_custom_perms(self):
81 81
         group.permissions.add(perm)
82 82
         group.save()
83 83
         user.groups.add(group)
84  
-        user = self.UserModel.objects.get(username='test')
  84
+        user = self.UserModel.objects.get(pk=self.user.pk)
85 85
         exp = set(['auth.test2', 'auth.test', 'auth.test3', 'auth.test_group'])
86 86
         self.assertEqual(user.get_all_permissions(), exp)
87 87
         self.assertEqual(user.get_group_permissions(), set(['auth.test_group']))
@@ -93,7 +93,7 @@ def test_custom_perms(self):
93 93
 
94 94
     def test_has_no_object_perm(self):
95 95
         """Regressiontest for #12462"""
96  
-        user = self.UserModel.objects.get(username='test')
  96
+        user = self.UserModel.objects.get(pk=self.user.pk)
97 97
         content_type = ContentType.objects.get_for_model(Group)
98 98
         perm = Permission.objects.create(name='test', content_type=content_type, codename='test')
99 99
         user.user_permissions.add(perm)
@@ -106,7 +106,7 @@ def test_has_no_object_perm(self):
106 106
 
107 107
     def test_get_all_superuser_permissions(self):
108 108
         "A superuser has all permissions. Refs #14795"
109  
-        user = self.UserModel.objects.get(username='test2')
  109
+        user = self.UserModel.objects.get(pk=self.superuser.pk)
110 110
         self.assertEqual(len(user.get_all_permissions()), len(Permission.objects.all()))
111 111
 
112 112
 
@@ -118,12 +118,12 @@ class ModelBackendTest(BaseModelBackendTest, TestCase):
118 118
     UserModel = User
119 119
 
120 120
     def create_users(self):
121  
-        User.objects.create_user(
  121
+        self.user = User.objects.create_user(
122 122
             username='test',
123 123
             email='test@example.com',
124 124
             password='test',
125 125
         )
126  
-        User.objects.create_superuser(
  126
+        self.superuser = User.objects.create_superuser(
127 127
             username='test2',
128 128
             email='test2@example.com',
129 129
             password='test',
@@ -151,13 +151,13 @@ class ExtensionUserModelBackendTest(BaseModelBackendTest, TestCase):
151 151
     UserModel = ExtensionUser
152 152
 
153 153
     def create_users(self):
154  
-        ExtensionUser.objects.create_user(
  154
+        self.user = ExtensionUser.objects.create_user(
155 155
             username='test',
156 156
             email='test@example.com',
157 157
             password='test',
158 158
             date_of_birth=date(2006, 4, 25)
159 159
         )
160  
-        ExtensionUser.objects.create_superuser(
  160
+        self.superuser = ExtensionUser.objects.create_superuser(
161 161
             username='test2',
162 162
             email='test2@example.com',
163 163
             password='test',
@@ -165,6 +165,31 @@ def create_users(self):
165 165
         )
166 166
 
167 167
 
  168
+@override_settings(AUTH_USER_MODEL='auth.CustomPermissionsUser')
  169
+class CustomPermissionsUserModelBackendTest(BaseModelBackendTest, TestCase):
  170
+    """
  171
+    Tests for the ModelBackend using the CustomPermissionsUser model.
  172
+
  173
+    As with the ExtensionUser test, this isn't a perfect test, because both
  174
+    the User and CustomPermissionsUser are synchronized to the database,
  175
+    which wouldn't ordinary happen in production.
  176
+    """
  177
+
  178
+    UserModel = CustomPermissionsUser
  179
+
  180
+    def create_users(self):
  181
+        self.user = CustomPermissionsUser.objects.create_user(
  182
+            email='test@example.com',
  183
+            password='test',
  184
+            date_of_birth=date(2006, 4, 25)
  185
+        )
  186
+        self.superuser = CustomPermissionsUser.objects.create_superuser(
  187
+            email='test2@example.com',
  188
+            password='test',
  189
+            date_of_birth=date(1976, 11, 8)
  190
+        )
  191
+
  192
+
168 193
 class TestObj(object):
169 194
     pass
170 195
 
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 47e1df8

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