Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

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
@freakboy3742 freakboy3742 authored
View
158 django/contrib/auth/models.py
@@ -195,38 +195,6 @@ def create_superuser(self, username, email, password, **extra_fields):
return u
-# A few helper functions for common logic between User and AnonymousUser.
-def _user_get_all_permissions(user, obj):
- permissions = set()
- for backend in auth.get_backends():
- if hasattr(backend, "get_all_permissions"):
- if obj is not None:
- permissions.update(backend.get_all_permissions(user, obj))
- else:
- permissions.update(backend.get_all_permissions(user))
- return permissions
-
-
-def _user_has_perm(user, perm, obj):
- for backend in auth.get_backends():
- if hasattr(backend, "has_perm"):
- if obj is not None:
- if backend.has_perm(user, perm, obj):
- return True
- else:
- if backend.has_perm(user, perm):
- return True
- return False
-
-
-def _user_has_module_perms(user, app_label):
- for backend in auth.get_backends():
- if hasattr(backend, "has_module_perms"):
- if backend.has_module_perms(user, app_label):
- return True
- return False
-
-
@python_2_unicode_compatible
class AbstractBaseUser(models.Model):
password = models.CharField(_('password'), max_length=128)
@@ -290,32 +258,46 @@ def get_short_name(self):
raise NotImplementedError()
-class AbstractUser(AbstractBaseUser):
- """
- An abstract base class implementing a fully featured User model with
- admin-compliant permissions.
+# A few helper functions for common logic between User and AnonymousUser.
+def _user_get_all_permissions(user, obj):
+ permissions = set()
+ for backend in auth.get_backends():
+ if hasattr(backend, "get_all_permissions"):
+ if obj is not None:
+ permissions.update(backend.get_all_permissions(user, obj))
+ else:
+ permissions.update(backend.get_all_permissions(user))
+ return permissions
- Username, password and email are required. Other fields are optional.
+
+def _user_has_perm(user, perm, obj):
+ for backend in auth.get_backends():
+ if hasattr(backend, "has_perm"):
+ if obj is not None:
+ if backend.has_perm(user, perm, obj):
+ return True
+ else:
+ if backend.has_perm(user, perm):
+ return True
+ return False
+
+
+def _user_has_module_perms(user, app_label):
+ for backend in auth.get_backends():
+ if hasattr(backend, "has_module_perms"):
+ if backend.has_module_perms(user, app_label):
+ return True
+ return False
+
+
+class PermissionsMixin(models.Model):
+ """
+ A mixin class that adds the fields and methods necessary to support
+ Django's Group and Permission model using the ModelBackend.
"""
- username = models.CharField(_('username'), max_length=30, unique=True,
- help_text=_('Required. 30 characters or fewer. Letters, numbers and '
- '@/./+/-/_ characters'),
- validators=[
- validators.RegexValidator(re.compile('^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid')
- ])
- first_name = models.CharField(_('first name'), max_length=30, blank=True)
- last_name = models.CharField(_('last name'), max_length=30, blank=True)
- email = models.EmailField(_('email address'), blank=True)
- is_staff = models.BooleanField(_('staff status'), default=False,
- help_text=_('Designates whether the user can log into this admin '
- 'site.'))
- is_active = models.BooleanField(_('active'), default=True,
- help_text=_('Designates whether this user should be treated as '
- 'active. Unselect this instead of deleting accounts.'))
is_superuser = models.BooleanField(_('superuser status'), default=False,
help_text=_('Designates that this user has all permissions without '
'explicitly assigning them.'))
- date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
groups = models.ManyToManyField(Group, verbose_name=_('groups'),
blank=True, help_text=_('The groups this user belongs to. A user will '
'get all permissions granted to each of '
@@ -324,30 +306,9 @@ class AbstractUser(AbstractBaseUser):
verbose_name=_('user permissions'), blank=True,
help_text='Specific permissions for this user.')
- objects = UserManager()
-
- USERNAME_FIELD = 'username'
- REQUIRED_FIELDS = ['email']
-
class Meta:
- verbose_name = _('user')
- verbose_name_plural = _('users')
abstract = True
- def get_absolute_url(self):
- return "/users/%s/" % urlquote(self.username)
-
- def get_full_name(self):
- """
- Returns the first_name plus the last_name, with a space in between.
- """
- full_name = '%s %s' % (self.first_name, self.last_name)
- return full_name.strip()
-
- def get_short_name(self):
- "Returns the short name for the user."
- return self.first_name
-
def get_group_permissions(self, obj=None):
"""
Returns a list of permission strings that this user has through his/her
@@ -405,6 +366,55 @@ def has_module_perms(self, app_label):
return _user_has_module_perms(self, app_label)
+
+class AbstractUser(AbstractBaseUser, PermissionsMixin):
+ """
+ An abstract base class implementing a fully featured User model with
+ admin-compliant permissions.
+
+ Username, password and email are required. Other fields are optional.
+ """
+ username = models.CharField(_('username'), max_length=30, unique=True,
+ help_text=_('Required. 30 characters or fewer. Letters, numbers and '
+ '@/./+/-/_ characters'),
+ validators=[
+ validators.RegexValidator(re.compile('^[\w.@+-]+$'), _('Enter a valid username.'), 'invalid')
+ ])
+ first_name = models.CharField(_('first name'), max_length=30, blank=True)
+ last_name = models.CharField(_('last name'), max_length=30, blank=True)
+ email = models.EmailField(_('email address'), blank=True)
+ is_staff = models.BooleanField(_('staff status'), default=False,
+ help_text=_('Designates whether the user can log into this admin '
+ 'site.'))
+ is_active = models.BooleanField(_('active'), default=True,
+ help_text=_('Designates whether this user should be treated as '
+ 'active. Unselect this instead of deleting accounts.'))
+ date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
+
+ objects = UserManager()
+
+ USERNAME_FIELD = 'username'
+ REQUIRED_FIELDS = ['email']
+
+ class Meta:
+ verbose_name = _('user')
+ verbose_name_plural = _('users')
+ abstract = True
+
+ def get_absolute_url(self):
+ return "/users/%s/" % urlquote(self.username)
+
+ def get_full_name(self):
+ """
+ Returns the first_name plus the last_name, with a space in between.
+ """
+ full_name = '%s %s' % (self.first_name, self.last_name)
+ return full_name.strip()
+
+ def get_short_name(self):
+ "Returns the short name for the user."
+ return self.first_name
+
def email_user(self, subject, message, from_email=None):
"""
Sends an email to this User.
View
49 django/contrib/auth/tests/auth_backends.py
@@ -4,7 +4,7 @@
from django.conf import settings
from django.contrib.auth.models import User, Group, Permission, AnonymousUser
from django.contrib.auth.tests.utils import skipIfCustomUser
-from django.contrib.auth.tests.custom_user import ExtensionUser
+from django.contrib.auth.tests.custom_user import ExtensionUser, CustomPermissionsUser
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.contrib.auth import authenticate
@@ -34,7 +34,7 @@ def tearDown(self):
ContentType.objects.clear_cache()
def test_has_perm(self):
- user = self.UserModel.objects.get(username='test')
+ user = self.UserModel.objects.get(pk=self.user.pk)
self.assertEqual(user.has_perm('auth.test'), False)
user.is_staff = True
user.save()
@@ -53,14 +53,14 @@ def test_has_perm(self):
self.assertEqual(user.has_perm('auth.test'), False)
def test_custom_perms(self):
- user = self.UserModel.objects.get(username='test')
+ user = self.UserModel.objects.get(pk=self.user.pk)
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 = self.UserModel.objects.get(username='test')
+ user = self.UserModel.objects.get(pk=self.user.pk)
self.assertEqual(user.get_all_permissions() == set(['auth.test']), True)
self.assertEqual(user.get_group_permissions(), set([]))
self.assertEqual(user.has_module_perms('Group'), False)
@@ -71,7 +71,7 @@ def test_custom_perms(self):
perm = Permission.objects.create(name='test3', content_type=content_type, codename='test3')
user.user_permissions.add(perm)
user.save()
- user = self.UserModel.objects.get(username='test')
+ user = self.UserModel.objects.get(pk=self.user.pk)
self.assertEqual(user.get_all_permissions(), set(['auth.test2', 'auth.test', 'auth.test3']))
self.assertEqual(user.has_perm('test'), False)
self.assertEqual(user.has_perm('auth.test'), True)
@@ -81,7 +81,7 @@ def test_custom_perms(self):
group.permissions.add(perm)
group.save()
user.groups.add(group)
- user = self.UserModel.objects.get(username='test')
+ user = self.UserModel.objects.get(pk=self.user.pk)
exp = set(['auth.test2', 'auth.test', 'auth.test3', 'auth.test_group'])
self.assertEqual(user.get_all_permissions(), exp)
self.assertEqual(user.get_group_permissions(), set(['auth.test_group']))
@@ -93,7 +93,7 @@ def test_custom_perms(self):
def test_has_no_object_perm(self):
"""Regressiontest for #12462"""
- user = self.UserModel.objects.get(username='test')
+ user = self.UserModel.objects.get(pk=self.user.pk)
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)
@@ -106,7 +106,7 @@ def test_has_no_object_perm(self):
def test_get_all_superuser_permissions(self):
"A superuser has all permissions. Refs #14795"
- user = self.UserModel.objects.get(username='test2')
+ user = self.UserModel.objects.get(pk=self.superuser.pk)
self.assertEqual(len(user.get_all_permissions()), len(Permission.objects.all()))
@@ -118,12 +118,12 @@ class ModelBackendTest(BaseModelBackendTest, TestCase):
UserModel = User
def create_users(self):
- User.objects.create_user(
+ self.user = User.objects.create_user(
username='test',
email='test@example.com',
password='test',
)
- User.objects.create_superuser(
+ self.superuser = User.objects.create_superuser(
username='test2',
email='test2@example.com',
password='test',
@@ -151,13 +151,13 @@ class ExtensionUserModelBackendTest(BaseModelBackendTest, TestCase):
UserModel = ExtensionUser
def create_users(self):
- ExtensionUser.objects.create_user(
+ self.user = ExtensionUser.objects.create_user(
username='test',
email='test@example.com',
password='test',
date_of_birth=date(2006, 4, 25)
)
- ExtensionUser.objects.create_superuser(
+ self.superuser = ExtensionUser.objects.create_superuser(
username='test2',
email='test2@example.com',
password='test',
@@ -165,6 +165,31 @@ def create_users(self):
)
+@override_settings(AUTH_USER_MODEL='auth.CustomPermissionsUser')
+class CustomPermissionsUserModelBackendTest(BaseModelBackendTest, TestCase):
+ """
+ Tests for the ModelBackend using the CustomPermissionsUser model.
+
+ As with the ExtensionUser test, this isn't a perfect test, because both
+ the User and CustomPermissionsUser are synchronized to the database,
+ which wouldn't ordinary happen in production.
+ """
+
+ UserModel = CustomPermissionsUser
+
+ def create_users(self):
+ self.user = CustomPermissionsUser.objects.create_user(
+ email='test@example.com',
+ password='test',
+ date_of_birth=date(2006, 4, 25)
+ )
+ self.superuser = CustomPermissionsUser.objects.create_superuser(
+ email='test2@example.com',
+ password='test',
+ date_of_birth=date(1976, 11, 8)
+ )
+
+
class TestObj(object):
pass
View
43 django/contrib/auth/tests/custom_user.py
@@ -1,5 +1,11 @@
from django.db import models
-from django.contrib.auth.models import BaseUserManager, AbstractBaseUser, AbstractUser, UserManager
+from django.contrib.auth.models import (
+ BaseUserManager,
+ AbstractBaseUser,
+ AbstractUser,
+ UserManager,
+ PermissionsMixin
+)
# The custom User uses email as the unique identifier, and requires
@@ -90,6 +96,40 @@ class Meta:
app_label = 'auth'
+# The CustomPermissionsUser users email as the identifier, but uses the normal
+# Django permissions model. This allows us to check that the PermissionsMixin
+# includes everything that is needed to interact with the ModelBackend.
+
+class CustomPermissionsUserManager(CustomUserManager):
+ def create_superuser(self, email, password, date_of_birth):
+ u = self.create_user(email, password=password, date_of_birth=date_of_birth)
+ u.is_superuser = True
+ u.save(using=self._db)
+ return u
+
+
+class CustomPermissionsUser(AbstractBaseUser, PermissionsMixin):
+ email = models.EmailField(verbose_name='email address', max_length=255, unique=True)
+ date_of_birth = models.DateField()
+
+ objects = CustomPermissionsUserManager()
+
+ USERNAME_FIELD = 'email'
+ REQUIRED_FIELDS = ['date_of_birth']
+
+ class Meta:
+ app_label = 'auth'
+
+ def get_full_name(self):
+ return self.email
+
+ def get_short_name(self):
+ return self.email
+
+ def __unicode__(self):
+ return self.email
+
+
class IsActiveTestUser1(AbstractBaseUser):
"""
This test user class and derivatives test the default is_active behavior
@@ -104,4 +144,3 @@ class Meta:
app_label = 'auth'
# the is_active attr is provided by AbstractBaseUser
-
View
70 docs/topics/auth.txt
@@ -2136,6 +2136,76 @@ override any of the definitions that refer to fields on
:class:`~django.contrib.auth.models.AbstractUser` that aren't on your
custom User class.
+Custom users and permissions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To make it easy to include Django's permission framework into your own User
+class, Django provides :class:`~django.contrib.auth.model.PermissionsMixin`.
+This is an abstract model you can include in the class heirarchy for your User
+model, giving you all the methods and database fields necessary to support
+Django's permission model.
+
+:class:`~django.contrib.auth.model.PermissionsMixin` provides the following
+methods and attributes:
+
+.. class:: models.PermissionsMixin
+
+ .. attribute:: models.PermissionsMixin.is_superuser
+
+ Boolean. Designates that this user has all permissions without
+ explicitly assigning them.
+
+ .. method:: models.PermissionsMixin.get_group_permissions(obj=None)
+
+ Returns a set of permission strings that the user has, through his/her
+ groups.
+
+ If ``obj`` is passed in, only returns the group permissions for
+ this specific object.
+
+ .. method:: models.PermissionsMixin.get_all_permissions(obj=None)
+
+ Returns a set of permission strings that the user has, both through
+ group and user permissions.
+
+ If ``obj`` is passed in, only returns the permissions for this
+ specific object.
+
+ .. method:: models.PermissionsMixin.has_perm(perm, obj=None)
+
+ Returns ``True`` if the user has the specified permission, where perm is
+ in the format ``"<app label>.<permission codename>"`` (see
+ `permissions`_). If the user is inactive, this method will
+ always return ``False``.
+
+ If ``obj`` is passed in, this method won't check for a permission for
+ the model, but for this specific object.
+
+ .. method:: models.PermissionsMixin.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``.
+
+ If ``obj`` is passed in, this method won't check for permissions for
+ the model, but for the specific object.
+
+ .. method:: models.PermissionsMixin.has_module_perms(package_name)
+
+ Returns ``True`` if the user has any permissions in the given package
+ (the Django app label). If the user is inactive, this method will
+ always return ``False``.
+
+.. admonition:: ModelBackend
+
+ If you don't include the
+ :class:`~django.contrib.auth.model.PermissionsMixin`, you must ensure you
+ don't invoke the permissions methods on ``ModelBackend``. ``ModelBackend``
+ assumes that certain fields are available on your user model. If your User
+ model doesn't provide those fields, you will receive database errors when
+ you check permissions.
+
Custom users and Proxy models
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Please sign in to comment.
Something went wrong with that request. Please try again.