Opened 6 months ago
Last modified 43 hours ago
#36225 assigned Cleanup/optimization
USERNAME_FIELD must be unique in order to use createsuperuser command
Reported by: | Jonas Dittrich | Owned by: | Abderrahmane MELEK |
---|---|---|---|
Component: | contrib.auth | Version: | 5.1 |
Severity: | Normal | Keywords: | |
Cc: | Triage Stage: | Accepted | |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | yes |
Easy pickings: | no | UI/UX: | no |
Description
We have a custom user model defined, that does not have a natural key and in fact all fields (except pk) may be duplicated. That is because we can have "special" users that do not have an email.
Our model looks like this (minimal user model without a natural key implementation):
from django.contrib.auth.models import PermissionsMixin from django.db import models class UserProfile(PermissionsMixin, models.Model): # null=True because certain external users don't have an address email = models.EmailField(max_length=255, unique=True, blank=True, null=True, verbose_name="email address") password = models.CharField("password", max_length=128) @property def is_anonymous(self): return False @property def is_authenticated(self): return True USERNAME_FIELD = "email" REQUIRED_FIELDS = ["password"]
Now, using ./manage.py createsuperuser
is no longer possible. After entering an email address:
Email address: user@example.com Traceback (most recent call last): File "./manage.py", line 22, in <module> main() File "./manage.py", line 18, in main execute_from_command_line(sys.argv) File ".../lib/python3.10/site-packages/django/core/management/__init__.py", line 442, in execute_from_command_line utility.execute() File ".../lib/python3.10/site-packages/django/core/management/__init__.py", line 436, in execute self.fetch_command(subcommand).run_from_argv(self.argv) File ".../lib/python3.10/site-packages/django/core/management/base.py", line 413, in run_from_argv self.execute(*args, **cmd_options) File ".../lib/python3.10/site-packages/django/contrib/auth/management/commands/createsuperuser.py", line 90, in execute return super().execute(*args, **options) File ".../lib/python3.10/site-packages/django/core/management/base.py", line 459, in execute output = self.handle(*args, **options) File ".../lib/python3.10/site-packages/django/contrib/auth/management/commands/createsuperuser.py", line 132, in handle error_msg = self._validate_username( File ".../lib/python3.10/site-packages/django/contrib/auth/management/commands/createsuperuser.py", line 305, in _validate_username self.UserModel._default_manager.db_manager(database).get_by_natural_key( AttributeError: 'Manager' object has no attribute 'get_by_natural_key'
Command._validate_username
does check if the username field is unique by looking at the unique
flag passed to the field. However, we also set null=True
, so our username is not unique in the sense that a get_by_natural_key
can be implemented at all.
The create superuser command should either check for a nullable or blank username field in addition to the unique
flag, or directly check if the user model has the attribute get_by_natural_key
defined.
Change History (9)
comment:1 by , 6 months ago
Resolution: | → duplicate |
---|---|
Status: | new → closed |
comment:2 by , 6 months ago
Resolution: | duplicate |
---|---|
Status: | closed → new |
Hello Sarah. Sadly, even after defining a user manager, the problem persists. For brevity, I removed the user manager from the initial problem description.
Here is the updated, still failing example with a user model manager. Note that I can't use BaseUserManager
, because defining a natural key is impossible (also see my discussion at https://forum.djangoproject.com/t/how-to-serialize-user-profiles-without-natural-keys/34447/4).
from django.contrib.auth.models import PermissionsMixin, Group from django.contrib.auth.password_validation import validate_password from django.db import models class UserProfileManager(models.Manager): def create_user(self, *, email, password=None): user = self.model(email=email) validate_password(password, user=user) user.set_password(password) user.save() return user def create_superuser(self, *, email, password=None): user = self.create_user(password=password, email=email) user.is_superuser = True user.save() user.groups.add(Group.objects.get(name="Manager")) return user class UserProfile(PermissionsMixin, models.Model): # null=True because certain external users don't have an address email = models.EmailField( max_length=255, unique=True, blank=True, verbose_name="email address" ) password = models.CharField("password", max_length=128) @property def is_anonymous(self): return False @property def is_authenticated(self): return True USERNAME_FIELD = "email" REQUIRED_FIELDS = ["password"] objects = UserProfileManager()
comment:3 by , 6 months ago
Component: | Uncategorized → contrib.auth |
---|---|
Summary: | User profile models with nullable emails cannot use createsuperuser command → USERNAME_FIELD must be unique in order to use createsuperuser command |
Triage Stage: | Unreviewed → Accepted |
Type: | Uncategorized → Cleanup/optimization |
Based off the forum thread, the confusion here I see is from the USERNAME_FIELD docs saying:
The field must be unique (e.g. have unique=True set in its definition), unless you use a custom authentication backend that can support non-unique usernames.
I think we could remove the "unless you use a custom authentication backend that can support non-unique usernames." because it implies we support this use case.
Accepting for Documentation tweaks
comment:4 by , 6 months ago
Owner: | set to |
---|---|
Status: | new → assigned |
comment:5 by , 6 months ago
Spencer, it's great that you're interested on working on this ticket but it was accepted on the basis that the documentation should be adjusted (see comment:3) not that the create_superuser
command should be adjusted.
comment:6 by , 6 months ago
Thanks for the clarification, Simon. I initially interpreted the ticket as a code adjustment rather than just a documentation update. Since it's a documentation tweak, would you like me to update the relevant part of the USERNAME_FIELD docs and submit a PR for that instead?
On a side note, at least I got everything up and running to be able to contribute code in the future!
comment:7 by , 2 days ago
Has patch: | set |
---|---|
Owner: | changed from | to
comment:8 by , 43 hours ago
The plan from comment:3 to tweak the docs gives me pause, since this use case was explicitly tested and added in #24910.
From my reading of the forum post, the two sharp edges that came up for the implementer were:
- reimplementing
AbstractBaseUser
from scratch to avoid having todel AbstractBaseUser.natural_key
to support serialization - createsuperuser's username validation not being aware of
null=True, distinct=True
username fields
We could tweak the docs instead to explain that natural_key
must be overridden on the model. The use case for nullable usernames and the attendant weirdness around having to override get_by_natural_key
on the manager doesn't seem worth documenting to me: it could just be implicit from the advice to "override".
comment:9 by , 43 hours ago
Patch needs improvement: | set |
---|
Hi Jonas, you should create a write a manager for your custom user model which defines create_superuser.
This is also documented here: https://docs.djangoproject.com/en/5.1/topics/auth/customizing/#writing-a-manager-for-a-custom-user-model
This is roughly a duplicate of #26412