Skip to content

Commit

Permalink
Fixed #19412 -- Added PermissionsMixin to the auth.User heirarchy.
Browse files Browse the repository at this point in the history
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
freakboy3742 committed Dec 15, 2012
1 parent bd414ae commit 47e1df8
Show file tree
Hide file tree
Showing 4 changed files with 232 additions and 88 deletions.
158 changes: 84 additions & 74 deletions django/contrib/auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 '
Expand All @@ -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
Expand Down Expand Up @@ -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)

This comment has been minimized.

Copy link
@anuj-kumar

anuj-kumar Dec 10, 2020

Q: Why there's no unique constraint here? Does Django allow multiple users with the same email?

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.
Expand Down
49 changes: 37 additions & 12 deletions django/contrib/auth/tests/auth_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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']))
Expand All @@ -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)
Expand All @@ -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()))


Expand 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',
Expand Down Expand Up @@ -151,20 +151,45 @@ 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',
date_of_birth=date(1976, 11, 8)
)


@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

Expand Down
43 changes: 41 additions & 2 deletions django/contrib/auth/tests/custom_user.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -104,4 +144,3 @@ class Meta:
app_label = 'auth'

# the is_active attr is provided by AbstractBaseUser

Loading

0 comments on commit 47e1df8

Please sign in to comment.