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:
parent
bd414aed01
commit
47e1df896b
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
Loading…
Reference in New Issue