Commit 47e1df89 authored by Russell Keith-Magee's avatar Russell Keith-Magee
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.
parent bd414aed
Loading
Loading
Loading
Loading
+84 −74
Original line number Diff line number Diff line
@@ -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.
+37 −12
Original line number Diff line number Diff line
@@ -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

+41 −2
Original line number Diff line number Diff line
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
+70 −0
Original line number Diff line number Diff line
@@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~