From fe914341c83b37fd6aa8fd85620cf49dd2328ab0 Mon Sep 17 00:00:00 2001 From: Dan Watson Date: Thu, 2 Apr 2015 12:52:45 -0400 Subject: [PATCH] Fixed #24564 -- Moved AbstractBaseUser and BaseUserManager so they can be used without auth in INSTALLED_APPS --- django/contrib/auth/base_user.py | 114 +++++++++++++++++++++++++++++++ django/contrib/auth/models.py | 108 +---------------------------- docs/releases/1.9.txt | 6 ++ docs/topics/auth/customizing.txt | 10 +++ 4 files changed, 131 insertions(+), 107 deletions(-) create mode 100644 django/contrib/auth/base_user.py diff --git a/django/contrib/auth/base_user.py b/django/contrib/auth/base_user.py new file mode 100644 index 0000000000..b279e152ae --- /dev/null +++ b/django/contrib/auth/base_user.py @@ -0,0 +1,114 @@ +""" +This module allows importing AbstractBaseUser even when django.contrib.auth is +not in INSTALLED_APPS. +""" +from __future__ import unicode_literals + +from django.contrib.auth.hashers import ( + check_password, is_password_usable, make_password, +) +from django.db import models +from django.utils.crypto import get_random_string, salted_hmac +from django.utils.encoding import python_2_unicode_compatible +from django.utils.translation import ugettext_lazy as _ + + +class BaseUserManager(models.Manager): + + @classmethod + def normalize_email(cls, email): + """ + Normalize the email address by lowercasing the domain part of the it. + """ + email = email or '' + try: + email_name, domain_part = email.strip().rsplit('@', 1) + except ValueError: + pass + else: + email = '@'.join([email_name, domain_part.lower()]) + return email + + def make_random_password(self, length=10, + allowed_chars='abcdefghjkmnpqrstuvwxyz' + 'ABCDEFGHJKLMNPQRSTUVWXYZ' + '23456789'): + """ + Generate a random password with the given length and given + allowed_chars. The default value of allowed_chars does not have "I" or + "O" or letters and digits that look similar -- just to avoid confusion. + """ + return get_random_string(length, allowed_chars) + + def get_by_natural_key(self, username): + return self.get(**{self.model.USERNAME_FIELD: username}) + + +@python_2_unicode_compatible +class AbstractBaseUser(models.Model): + password = models.CharField(_('password'), max_length=128) + last_login = models.DateTimeField(_('last login'), blank=True, null=True) + + is_active = True + + REQUIRED_FIELDS = [] + + class Meta: + abstract = True + + def get_username(self): + "Return the identifying username for this User" + return getattr(self, self.USERNAME_FIELD) + + def __str__(self): + return self.get_username() + + def natural_key(self): + return (self.get_username(),) + + def is_anonymous(self): + """ + Always return False. This is a way of comparing User objects to + anonymous users. + """ + return False + + def is_authenticated(self): + """ + Always return True. This is a way to tell if the user has been + authenticated in templates. + """ + return True + + def set_password(self, raw_password): + self.password = make_password(raw_password) + + def check_password(self, raw_password): + """ + Return a boolean of whether the raw_password was correct. Handles + hashing formats behind the scenes. + """ + def setter(raw_password): + self.set_password(raw_password) + self.save(update_fields=["password"]) + return check_password(raw_password, self.password, setter) + + def set_unusable_password(self): + # Set a value that will never be a valid hash + self.password = make_password(None) + + def has_usable_password(self): + return is_password_usable(self.password) + + def get_full_name(self): + raise NotImplementedError('subclasses of AbstractBaseUser must provide a get_full_name() method') + + def get_short_name(self): + raise NotImplementedError('subclasses of AbstractBaseUser must provide a get_short_name() method.') + + def get_session_auth_hash(self): + """ + Return an HMAC of the password field. + """ + key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash" + return salted_hmac(key_salt, self.password).hexdigest() diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index b631f11271..84088f6525 100644 --- a/django/contrib/auth/models.py +++ b/django/contrib/auth/models.py @@ -1,9 +1,7 @@ from __future__ import unicode_literals from django.contrib import auth -from django.contrib.auth.hashers import ( - check_password, is_password_usable, make_password, -) +from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager from django.contrib.auth.signals import user_logged_in from django.contrib.contenttypes.models import ContentType from django.core import validators @@ -12,7 +10,6 @@ from django.core.mail import send_mail from django.db import models from django.db.models.manager import EmptyManager from django.utils import six, timezone -from django.utils.crypto import get_random_string, salted_hmac from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ @@ -129,39 +126,6 @@ class Group(models.Model): return (self.name,) -class BaseUserManager(models.Manager): - - @classmethod - def normalize_email(cls, email): - """ - Normalize the address by lowercasing the domain part of the email - address. - """ - email = email or '' - try: - email_name, domain_part = email.strip().rsplit('@', 1) - except ValueError: - pass - else: - email = '@'.join([email_name, domain_part.lower()]) - return email - - def make_random_password(self, length=10, - allowed_chars='abcdefghjkmnpqrstuvwxyz' - 'ABCDEFGHJKLMNPQRSTUVWXYZ' - '23456789'): - """ - Generates a random password with the given length and given - allowed_chars. Note that the default value of allowed_chars does not - have "I" or "O" or letters and digits that look similar -- just to - avoid confusion. - """ - return get_random_string(length, allowed_chars) - - def get_by_natural_key(self, username): - return self.get(**{self.model.USERNAME_FIELD: username}) - - class UserManager(BaseUserManager): use_in_migrations = True @@ -189,76 +153,6 @@ class UserManager(BaseUserManager): **extra_fields) -@python_2_unicode_compatible -class AbstractBaseUser(models.Model): - password = models.CharField(_('password'), max_length=128) - last_login = models.DateTimeField(_('last login'), blank=True, null=True) - - is_active = True - - REQUIRED_FIELDS = [] - - class Meta: - abstract = True - - def get_username(self): - "Return the identifying username for this User" - return getattr(self, self.USERNAME_FIELD) - - def __str__(self): - return self.get_username() - - def natural_key(self): - return (self.get_username(),) - - def is_anonymous(self): - """ - Always returns False. This is a way of comparing User objects to - anonymous users. - """ - return False - - def is_authenticated(self): - """ - Always return True. This is a way to tell if the user has been - authenticated in templates. - """ - return True - - def set_password(self, raw_password): - self.password = make_password(raw_password) - - def check_password(self, raw_password): - """ - Returns a boolean of whether the raw_password was correct. Handles - hashing formats behind the scenes. - """ - def setter(raw_password): - self.set_password(raw_password) - self.save(update_fields=["password"]) - return check_password(raw_password, self.password, setter) - - def set_unusable_password(self): - # Sets a value that will never be a valid hash - self.password = make_password(None) - - def has_usable_password(self): - return is_password_usable(self.password) - - def get_full_name(self): - raise NotImplementedError('subclasses of AbstractBaseUser must provide a get_full_name() method') - - def get_short_name(self): - raise NotImplementedError('subclasses of AbstractBaseUser must provide a get_short_name() method.') - - def get_session_auth_hash(self): - """ - Returns an HMAC of the password field. - """ - key_salt = "django.contrib.auth.models.AbstractBaseUser.get_session_auth_hash" - return salted_hmac(key_salt, self.password).hexdigest() - - # A few helper functions for common logic between User and AnonymousUser. def _user_get_all_permissions(user, obj): permissions = set() diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index cceb0611b5..513177fff2 100644 --- a/docs/releases/1.9.txt +++ b/docs/releases/1.9.txt @@ -59,6 +59,12 @@ Minor features * The ``BCryptSHA256PasswordHasher`` will now update passwords if its ``rounds`` attribute is changed. +* ``AbstractBaseUser`` and ``BaseUserManager`` were moved to a new + ``django.contrib.auth.base_user`` module so that they can be imported without + including ``django.contrib.auth`` in :setting:`INSTALLED_APPS` (this raised + a deprecation warning in older versions and is no longer supported in + Django 1.9). + :mod:`django.contrib.gis` ^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/topics/auth/customizing.txt b/docs/topics/auth/customizing.txt index cc7a326c23..9ca2e72b2d 100644 --- a/docs/topics/auth/customizing.txt +++ b/docs/topics/auth/customizing.txt @@ -586,6 +586,16 @@ password resets. You must then provide some key implementation details: identifies the user in an informal way. It may also return the same value as :meth:`django.contrib.auth.models.User.get_full_name()`. + .. admonition:: Importing ``AbstractBaseUser`` + + .. versionadded:: 1.9 + + ``AbstractBaseUser`` and ``BaseUserManager`` are importable from + ``django.contrib.auth.base_user`` so that they can be imported without + including ``django.contrib.auth`` in :setting:`INSTALLED_APPS` (this + raised a deprecation warning in older versions and is no longer + supported in Django 1.9). + The following methods are available on any subclass of :class:`~django.contrib.auth.models.AbstractBaseUser`: