Opened 3 years ago

Closed 3 years ago

Last modified 3 years ago

#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)

comment:1 by Tim Graham, 3 years ago

Resolution: invalid
Status: newclosed

The lack of validation is consistent with Model.save(), etc. I see nothing in the documentation that suggests this would behave otherwise.

in reply to:  1 comment:2 by Adam McKay, 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 Tim Graham, 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.

Note: See TracTickets for help on using tickets.
Back to Top