#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 , 4 years ago
| Resolution: | → invalid |
|---|---|
| Status: | new → closed |
comment:2 by , 4 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 , 4 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.