Django admin → Users: adding a person saves an empty username, and the second one fails with a 500 #427

Closed
opened 2026-10-01 10:46:37 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the admin registration and running its add form against the test database.

Related to #1.

Checked again on 2026-10-01 against main at 0a5cad587: still present. The signup rule the fix points to is now at accounts/forms.py:908-913; accounts/admin.py and the cited lines of accounts/models.py have not moved.

UserAdmin replaces Django's fieldsets and add_fieldsets with lists that leave out username, although User.USERNAME_FIELD is username and the field is unique and obligatory. The admin builds its add form from add_fieldsets, so the username is never asked for, the model validation skips the excluded field, and User.save stores ''. The first person added this way has no username; the second cannot be added at all.

  • Where: accounts/admin.py:14-29, UserAdmin.fieldsets, UserAdmin.add_fieldsets, list_display and search_fields; accounts/models.py:130-133, User.save.
  • What happens: with the admin mounted (POSTULO_ADMIN_URL set), admin.site._registry[User].get_form(request) has the fields email, password1, password2 and usable_password. Saving it for a@example.org gives a user whose username is ''; saving it again for b@example.org raises IntegrityError: UNIQUE constraint failed: accounts_user.username, which the admin answers with a 500. The change form has no username either (email, password, first_name, last_name, the permissions and the dates), so the empty username cannot be corrected there, and the list neither shows nor searches it.
  • Why it matters: the empty username breaks the rule in User's docstring that a username and a full name are both obligatory, User.__str__ returns '' wherever the account is named, and the path an operator is most likely to try in the admin ends in a server error. UserManager.create_user already derives a username from the address when none is given (accounts/models.py:61-66), but the admin form bypasses the manager.
  • Fix: put username in both add_fieldsets and fieldsets (and first_name, last_name in add_fieldsets, since a full name is obligatory too), add it to list_display and search_fields, and give the admin's add and change forms a clean_username that applies the same rules as signup, get_adapter().clean_username(...) as accounts/forms.py:911-916 does, so the blacklist holds there too. Alternatively, when the field is left blank, derive it with slug_from_email and unique_username as the manager does.

Done when

  • A test in tests/security/test_admin_exposure.py (which already mounts the admin with its with_an_admin fixture) adds two people through the admin's add form and finds both saved with non-empty, distinct usernames, and a blacklisted username refused.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the admin registration and running its add form against the test database. Related to #1. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The signup rule the fix points to is now at `accounts/forms.py:908-913`; `accounts/admin.py` and the cited lines of `accounts/models.py` have not moved. `UserAdmin` replaces Django's `fieldsets` and `add_fieldsets` with lists that leave out `username`, although `User.USERNAME_FIELD` is `username` and the field is unique and obligatory. The admin builds its add form from `add_fieldsets`, so the username is never asked for, the model validation skips the excluded field, and `User.save` stores `''`. The first person added this way has no username; the second cannot be added at all. - **Where:** `accounts/admin.py:14-29`, `UserAdmin.fieldsets`, `UserAdmin.add_fieldsets`, `list_display` and `search_fields`; `accounts/models.py:130-133`, `User.save`. - **What happens:** with the admin mounted (`POSTULO_ADMIN_URL` set), `admin.site._registry[User].get_form(request)` has the fields `email`, `password1`, `password2` and `usable_password`. Saving it for `a@example.org` gives a user whose `username` is `''`; saving it again for `b@example.org` raises `IntegrityError: UNIQUE constraint failed: accounts_user.username`, which the admin answers with a 500. The change form has no `username` either (`email`, `password`, `first_name`, `last_name`, the permissions and the dates), so the empty username cannot be corrected there, and the list neither shows nor searches it. - **Why it matters:** the empty username breaks the rule in `User`'s docstring that a username and a full name are both obligatory, `User.__str__` returns `''` wherever the account is named, and the path an operator is most likely to try in the admin ends in a server error. `UserManager.create_user` already derives a username from the address when none is given (`accounts/models.py:61-66`), but the admin form bypasses the manager. - **Fix:** put `username` in both `add_fieldsets` and `fieldsets` (and `first_name`, `last_name` in `add_fieldsets`, since a full name is obligatory too), add it to `list_display` and `search_fields`, and give the admin's add and change forms a `clean_username` that applies the same rules as signup, `get_adapter().clean_username(...)` as `accounts/forms.py:911-916` does, so the blacklist holds there too. Alternatively, when the field is left blank, derive it with `slug_from_email` and `unique_username` as the manager does. ## Done when - A test in `tests/security/test_admin_exposure.py` (which already mounts the admin with its `with_an_admin` fixture) adds two people through the admin's add form and finds both saved with non-empty, distinct usernames, and a blacklisted username refused.
tiagoagueda added this to the 0.6.0 milestone 2026-10-01 10:46:37 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#427
No description provided.