The Users, Groups and Removed VMs pages were bare tables: rows of
coloured icon buttons without words, five columns of small checks for
a user's role and permissions, dates broken over three lines and a
lone "+" for a new entry. The owner chose a directory layout from three
rendered options; it follows the host pages' cards:
- Each list is a card with its count and search in the header and one
list-group item per entry, which stacks on a phone without scrolling.
- A user shows their role as a badge (Superuser, Template editor) or
"User", Blocked as a red badge, their groups, a granted clone
permission and live API tokens, and the last login on one line;
role and status are searchable. Edit is the visible action, the menu
holds View profile, Block or Unblock and Delete; Block and Delete
are disabled on one's own account, which the server refuses anyway.
- A group shows its member and permission counts; its name opens it.
The page's title said "Users".
- A removed VM shows its UUID, the host it left, its owners and when
the record was made and expires.
- Logs keep their table in a card, the server search in its header,
the count of matching rows, Done and Failed as badges, and a message
column wide enough to read on a phone.
The user form was one long column of horizontal fields. The owner
rejected three boxed layouts and chose one led by what the user may
do: the Active switch, Template editor and Superuser as two cards,
groups as chips, the user's own permissions as switches each saying
what it allows, then the name and password and the quota folded into
details whose summaries tell their values (-1 as "any"); they open on
a new user and on an error inside them. A refused group or permission
is said beside its field. The group form follows it: the name, what
its members may do, its members as chips. Cancel goes back to the
list instead of the browser history.
NEW_USER_DEFAULT_INSTANCES names VMs, and a name may repeat across hosts.
A name with no VM, or with one on two hosts, made creating any user a 500.
The create form now looks the names up and says which one is not exactly
one VM; the user and its default VMs are saved together.
"Lost OTP?" mails the QR code only when exactly one active user has the
address. Any user could enter another user's address in their profile and
silently switch the recovery off for both. The profile and the admin user
forms now refuse an address another user has, ignoring case; an empty
address stays allowed for everyone.
- The administrator's password form took "password-based
authentication: Disabled" with "must change it at the next login":
the change form asks for the old password, which the user then never
had, so they could not get past it. That combination is refused, and
an administrator cannot disable their own password.
- A user who must change the password could still use the API with
their tokens. A token is now refused until they change it, as the
pages are.
The administrator's password form has "The user must change it at their
next login". With it, the user is sent to the change form until they
choose their own, as the generated admin password already does.
- An administrator could block, delete or demote (unset Active or
Superuser on) their own account, and with it the last one, leaving the
panel without anyone to undo it. These are now refused.
- The user form showed "No password set." for users who have one: its
password field never read the user's password. Its "Set password"
link went to ../password/, a page this panel does not have. The form
now says whether a password is set and links the page that sets it.
- "Staff status" said "can log into this admin site"; here it lets a
user change and delete the template VMs given to them, and is now
called Template editor.
- A quota of 0 counted as no limit, like -1, so a user's quota could not
be taken away. Now -1 has no limit and 0 allows none; the help says
so, and the labels give the units: memory in MiB, disk in GiB.
- A user's page for a user that does not exist answered with a server
error instead of not found.
- The user list said "None" for a user who never logged in; it says
"never", and dates fit their column.
- The password form's labels no longer break mid-word.
The admin page that creates a user saved the form first, which wrote
the user with the raw password in the password column, and hashed it
with a second save afterwards. The raw password was in the database,
and in its journal and any copy taken meanwhile, and stayed there if
anything failed between the two writes.
- UserCreateForm hashes the password before the first save; groups
and permissions are saved as before.
- The form applies AUTH_PASSWORD_VALIDATORS, after the user fields are
set (so the similarity check sees the new username), as the password
change forms already did. The settings template configures no
validators, so nothing changes unless an installation sets them.
The tests record the SQL parameters of the request: the previous code
wrote the raw password in the INSERT; it also accepted passwords that
configured validators reject.
SHOW_PROFILE_EDIT_PASSWORD was replaced in 2020 by the change_password
permission, settable per user or group. A post_migrate hook applied the
old setting to all users so existing installs kept their behavior, and
asked to remove the setting afterwards; but the settings template still
set it to True. Every migrate, so every upgrade, therefore granted the
permission to all users again and undid an admin's per-user or
per-group choice (with False it would have revoked it from everyone).
- Migration accounts.0008 applies the setting one last time, if a
settings.py still has it; the post_migrate hook is gone.
- New users get the permission when they are created (any path,
including LDAP); the admin user form ticks it and saves the admin's
choice afterwards.
- The template no longer sets SHOW_PROFILE_EDIT_PASSWORD; a check
(accounts.W001) asks to remove it from existing settings, and the
README upgrade notes say so.
The test that removes the permission and runs migrate fails on the
previous code. An upgrade from master with users was simulated: the
setting is applied once, and a permission removed afterwards stays
removed through the next migrate.
The forced change of the generated admin password was a session flag
set at login when the password still matched data/admin_password. A
new session after the file was gone bypassed it.
- UserAttributes.must_change_password (migration 0007) holds it; the
admin created with a generated password gets it, and a password
change clears it. ForcePasswordChangeMiddleware reads it.
- A system check (accounts.E001) stops the server unless the middleware
follows AuthenticationMiddleware; the README has the settings change.
- If the generated password cannot be stored privately, provisioning
stops without printing it and without creating the admin (one
transaction for the user and its attributes).
- The admin is provisioned on any real migrate run while no user
exists, so running migrate again after a failure creates it; flush
(plan=None) and rollbacks of accounts are skipped.
- The post_migrate receivers check the migration state for the models
they use, so a targeted migrate on a fresh database (e.g. migrate
appsettings) no longer fails with 'no such table'.