#32897 closed Bug (invalid)
create_user() does not raise ValidationError
Reported by: | Adam McKay | Owned by: | nobody |
---|---|---|---|
Component: | contrib.auth | Version: | 3.2 |
Severity: | Normal | Keywords: | |
Cc: | Triage Stage: | Unreviewed | |
Has patch: | no | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
The django.contrib.auth.models.UserManager.create_user()
function does not perform validation checking of the user object prior to saving to the database.
The documentation (https://docs.djangoproject.com/en/3.2/ref/contrib/auth/#django.contrib.auth.models.UserManager.create_user) implies this is the canonical way to create a user object and indeed it performs functions to normalize the email address and username as well as setting an unusable password, however the object is not validated using full_clean()
which results in an object that has validation errors attempting to be committed to the database raising an IntegrityError
rather than a ValidationError
.
This impacts the ability to add errors to any forms which use create_user()
as the IntegrityError
does not return a user-friendly message, nor is it applied to a field element.
Reproduction steps in a Django shell:
>>> from django.contrib.auth.models import User >>> User.objects.create_user('example') <User: example> >>> User.objects.create_user('example') Traceback (most recent call last): File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute return self.cursor.execute(sql, params) File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/db/backends/sqlite3/base.py", line 423, in execute return Database.Cursor.execute(self, query, params) sqlite3.IntegrityError: UNIQUE constraint failed: auth_user.username The above exception was the direct cause of the following exception: Traceback (most recent call last): File "<console>", line 1, in <module> File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/contrib/auth/models.py", line 152, in create_user return self._create_user(username, email, password, **extra_fields) File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/contrib/auth/models.py", line 146, in _create_user user.save(using=self._db) File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/contrib/auth/base_user.py", line 67, in save super().save(*args, **kwargs) File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/db/models/base.py", line 726, in save self.save_base(using=using, force_insert=force_insert, File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/db/models/base.py", line 763, in save_base updated = self._save_table( File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/db/models/base.py", line 868, in _save_table results = self._do_insert(cls._base_manager, using, fields, returning_fields, raw) File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/db/models/base.py", line 906, in _do_insert return manager._insert( File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/db/models/manager.py", line 85, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/db/models/query.py", line 1270, in _insert return query.get_compiler(using=using).execute_sql(returning_fields) File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/db/models/sql/compiler.py", line 1416, in execute_sql cursor.execute(sql, params) File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/db/backends/utils.py", line 98, in execute return super().execute(sql, params) File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/db/backends/utils.py", line 66, in execute return self._execute_with_wrappers(sql, params, many=False, executor=self._execute) File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/db/backends/utils.py", line 75, in _execute_with_wrappers return executor(sql, params, many, context) File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute return self.cursor.execute(sql, params) File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/db/utils.py", line 90, in __exit__ raise dj_exc_value.with_traceback(traceback) from exc_value File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/db/backends/utils.py", line 84, in _execute return self.cursor.execute(sql, params) File "/.pyenv/versions/38-django-core/lib/python3.8/site-packages/django/db/backends/sqlite3/base.py", line 423, in execute return Database.Cursor.execute(self, query, params) django.db.utils.IntegrityError: UNIQUE constraint failed: auth_user.username
Change History (3)
follow-up: 2 comment:1 by , 3 years ago
Resolution: | → invalid |
---|---|
Status: | new → closed |
comment:2 by , 3 years ago
Replying to Tim Graham:
The lack of validation is consistent with
Model.save()
, etc. I see nothing in the documentation that suggests this would behave otherwise.
Hi Tim,
I agree the behaviour is consistent with Model.save()
, however I think the usage of create_user
goes beyond a typical model method as it will ensure a username
is specified (raises an exception if not), normalises username
and email
and can also generate a password
if one is not specified.
Perhaps I should have logged this as a feature enhancement to call full_clean()
prior to user.save()
rather than a bug? My intention being to get a more accurate error from the method.
comment:3 by , 3 years ago
I doubt there would be consensus to change the behavior, particularly because it could be backward incompatible, but you could try making your case on the DevelopersMailingList. The requirements of your use case aren't entirely clear, but an alternative could be to use django.contrib.auth.forms.UserCreationForm
.
The lack of validation is consistent with
Model.save()
, etc. I see nothing in the documentation that suggests this would behave otherwise.