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.
This commit is contained in:
Russell Keith-Magee 2012-12-15 22:15:11 +08:00
parent bd414aed01
commit 47e1df896b
4 changed files with 232 additions and 88 deletions

View File

@ -195,38 +195,6 @@ class UserManager(BaseUserManager):
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 @@ class AbstractBaseUser(models.Model):
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 @@ class AbstractUser(AbstractBaseUser):
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 File

@ -4,7 +4,7 @@ from datetime import date
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 @@ class BaseModelBackendTest(object):
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 @@ class BaseModelBackendTest(object):
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 @@ class BaseModelBackendTest(object):
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 @@ class BaseModelBackendTest(object):
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 @@ class BaseModelBackendTest(object):
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 @@ class BaseModelBackendTest(object):
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 @@ class ExtensionUserModelBackendTest(BaseModelBackendTest, TestCase):
)
@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 File

@ -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 ExtensionUser(AbstractUser):
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 IsActiveTestUser1(AbstractBaseUser):
app_label = 'auth'
# the is_active attr is provided by AbstractBaseUser

View File

@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~