diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index de4de7cdfd..7d62810923 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -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. diff --git a/django/contrib/auth/tests/auth_backends.py b/django/contrib/auth/tests/auth_backends.py index 61acdd8c73..71f18d32cf 100644 --- a/django/contrib/auth/tests/auth_backends.py +++ b/django/contrib/auth/tests/auth_backends.py @@ -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 diff --git a/django/contrib/auth/tests/custom_user.py b/django/contrib/auth/tests/custom_user.py index 710ac8bdd7..7e042e4895 100644 --- a/django/contrib/auth/tests/custom_user.py +++ b/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 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 - diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index bd5cfdaac2..3e2b6bbdbf 100644 --- a/docs/topics/auth.txt +++ b/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 ``"."`` (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 + ``"."``. 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~