1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
Commit graph

174 commits

Author SHA1 Message Date
catborise
84dfbd5884 style(accounts): the password pages have the account forms' layout
The page where a user changes their own password and the one where an
administrator sets a user's password shared an old card: a horizontal
form, a blue Cancel and a green Change. The administrator's had Django's
"Password-based authentication: Enabled/Disabled" with a paragraph about
other backends, and its password fields stayed while Disabled was chosen.

Both pages now have the user and group forms' layout, each its own
template. The administrator chooses how the user signs in, With a
password or Without a password (such as LDAP), and the password fields
and "must change it" show only with a password; Cancel goes back to the
user. A user's own page says that their other sessions end, or, when
they must change it, that they choose one before going on, without a
Cancel. A refused password is said at its field only, without the
general "Wrong Data Provided". The section styles of these forms move
into webvirtcloud.css.
2026-11-04 23:11:52 +03:00
catborise
6523fd0955 style(admin): the user form asks who, what role, what quota, then what more
The create and edit pages of a user had one form for both, ordered for
editing: permissions first, the name and password folded below them and
the quota folded and closed at the end; a new user's name came last.

Both pages now go Account (name, password, may sign in), Role, Quota,
then Groups and permissions, each section with what it is for beside its
fields. Template editor and Superuser become one Role choice with User:
choosing Superuser hides the quota and permissions, which a superuser
does not need (no quota check applies, every permission is theirs); it
keeps the is_staff it had. The quota is always open: each limit takes a
number from 0, or No limit, which is kept as -1 as before; memory is
entered in GiB and kept in MiB, shown exactly so saving a user again
changes nothing. The button says Create user or Save changes.
2026-11-04 22:33:07 +03:00
catborise
393aecb8f1 refactor(templates): the shared page scripts load from static files
A step towards a Content-Security-Policy without inline scripts. The
scripts every page or many pages share now load from static files: the
error focus of base.html, the please-wait dialog, the sign-in forms'
password toggle and busy button, the jobs pages' reload while a job
runs, and the start of the job panel and of the VM list's state poll,
whose values come as json_script data. Cancel links go back through a
data-history-back attribute instead of a javascript: URL. The IE8
html5shiv/respond.js block, X-UA-Compatible and shrink-to-fit are gone:
no supported browser reads them.
2026-11-02 08:47:58 +03:00
catborise
488a77b3c7 feat(templates): the shared form and delete pages in a centred column
The pages many views share were a horizontal form whose Cancel went
back in a primary colour beside a green Save, and a delete question in
a yellow alert squeezed into half the width, which wrapped word by word
on a phone.

Both now sit in a centred column with their heading: the form's fields
with their labels above in a card, Cancel outlined and Save the primary
action; the delete page asks "Delete <name>?" in a card with a red
edge, says what deleting does, and offers Cancel and Delete.

The form for a user's rights on a VM names them as the VM's Users tab
does: Change settings, Delete the VM, and Change console settings,
which only counts together with Change settings.
2026-10-31 18:10:05 +03:00
catborise
0044359202 feat(accounts): the administrator's view of a user as an identity strip and cards
A superuser's page of a user was a heading, two unlabelled icon buttons
and three tabs: the VMs a table of check columns, the SSH keys a table
that pushed the page sideways on a phone.

Now a card at the top shows the user's name, role, Blocked when they
are, username, email, groups, last login and joining date, with Edit
user and Email OTP QR code as labelled buttons and links to the sections
below with their counts. The VMs card shows each VM with its host and
what the user may do beyond power and console as badges in the VM Users
tab's words (Change settings, Delete the VM, Change console settings),
with Change access and Remove from user in a menu, and Add VM in its
header. The SSH keys and the API tokens are cards; the key rows are now
one include with the user's own Profile, and both lists say when they
are empty. The full key shows over its fingerprint.

The tab script had no page left and goes.
2026-10-31 18:10:05 +03:00
catborise
3c7346c820 feat(accounts): the profile with details beside the user's keys and tokens
The Profile page was three tabs, each a box inside a box with horizontal
labels: the SSH keys a centred bare row with a grey trash icon, a tall
empty textarea under them, and the user known only by the heading.

Now the user's name, username, role, email and last login head a card
that holds the profile form and the password link; beside it the SSH
keys and the API tokens are cards with their count, a line on what
they are for, one row each with Delete in a menu, and the form to add
one behind a button. Adding or deleting a key returns to #sshkeys.

The token rows are a list-group include that the administrator's view
of a user shares.
2026-10-31 18:10:05 +03:00
catborise
3c6de221ca feat(admin): the admin lists as a people directory, the user form by what it allows
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.
2026-10-31 18:10:05 +03:00
catborise
cd2dd517b7 fix(instances): two owners added at once keep a VM to one owner
With one owner per VM, the VM page and a user's page counted the VM's
owners and then added one in separate steps, so two owners added at the
same moment both found the VM without one, and it got two. The check
and the insert now run in one transaction that holds the ownership lock
(PostgreSQL's advisory lock; SQLite's IMMEDIATE transactions already
serialize them): the second waits for the first and is refused. The
audit row is written after the transaction, so a failure keeps its row.
2026-10-31 00:21:06 +03:00
catborise
3d632f1592 feat(instances): a VM's owners with their rights, and XML edits kept until saved
Settings > Users was a bold title, an unlabelled "+" and a table of
names with a trash icon. The owner chose this layout from three
rendered options: one list of owners, each with what it may do
(change settings, delete the VM, change console settings, or power and
console only), Edit rights and Remove, an empty state, and a labelled
"Add owner".

- Remove and Edit rights come back to the VM's Users tab: the return
  address carried #users unencoded, so it was lost, and Edit rights
  went to the user's account page. Both accounts views now go to a
  ?next page of this site.
- The add-owner list offers only users who are not owners yet and not
  administrators, who can use every VM anyway; a superuser owner also
  took the only place when a VM may have one owner.
- The unused del_owner view and URL are removed: the list uses the
  accounts page, which asks before removing an owner.

XML:

- An XML edit not saved yet is kept: the page no longer reloads itself
  when the VM's state changes, and the browser asks before leaving it.
  A state change lost the edit.
- The button that opens the review dialog reads "Review changes"; it
  did not save, as "Change" suggested. Closing the dialog gives focus
  back to the editor.
2026-10-30 17:02:38 +03:00
catborise
e59870a660 refactor(accounts): the sign-in pages on Bootstrap components instead of signin.css
static/css/signin.css (308 lines) gave the sign-in and two-factor
sign-in pages their own components: a card, input groups, a submit
button, a footer and a status dot, with gradients, a blurred card,
entry and pulse animations, and rules that undid base.html's layout
(its container, its padding) from a "signin-page" class the pages'
scripts put on the body.

The pages are Bootstrap now: a card in the grid's centre column, the
logo a rounded-3 bg-primary block, the fields Bootstrap input-groups
with the password toggle an outline button, the submit a w-100
btn-primary, the footer a border-top line with a rounded-circle dot.
They follow the selected Bootswatch theme like every other page.
signin.css, its body class and its decoration go; so does the footer's
"v2.0.0", a version the panel never had.

The sign-out page (logout.html) was never shown: LOGOUT_REDIRECT_URL
sends a signed-out user to the sign-in page. It goes, with the view's
template name.
2026-10-27 19:39:19 +03:00
catborise
faa6e4bb14 refactor(templates): Bootstrap utilities instead of the upstream rules that redid them
webvirtcloud.css still carried the upstream panel's rules for what
Bootstrap 5.3 has as utilities and components:
- the page's bottom space (body padding) is pb-5 on base.html's body,
  and the console page no longer has to undo it;
- the VM page's action tabs (.action-button, .action-block with a
  fixed 65px width) are text-center with a d-block icon; its tab panes
  (#power ... #undefine, one rule each) mt-3;
- the bordered sub-tab panes (.tab-pane-bordered, a fixed #eee border
  that dark themes could not change) border border-top-0 p-3;
- the search box and header buttons (.search, .btn-header) m-1 and
  mt-1; the SSH key select (.keyselect) a form-select;
- the boot order list (.multipleselect, fixed size, #c0c0c0 border) a
  list-group of form-check-input items; its unused .multipleselect-on
  goes;
- table cells centred by a global rule are align-middle on each table.
Two rules against Bootstrap's behaviour go: menus opened on hover on
wide screens (Bootstrap opens them on a click, and keyboard and touch
users had no hover), and "a { outline: 0 }", which hid the keyboard
focus ring.

The paragraph and page-header margins and the 900px container stay
for a separate change: they set every page's layout.
2026-10-27 19:05:48 +03:00
catborise
93d3cc7238 fix(accounts): SSH keys with spaced comments, one token per create, OTP errors
- An SSH public key whose comment holds spaces (ssh-keygen -C "John Doe
  laptop") is accepted; only the type and the key are checked.
- The created API token is shown by a GET after a redirect, so reloading
  the page no longer creates a second token the user never sees.
- The OTP login page shows field errors instead of an empty error box.
- A user without a local password (an LDAP user) is not offered a password
  change that could never succeed.
2026-10-26 16:14:37 +03:00
catborise
b619cf3cd4 fix(accounts): a user's page shows permissions as marks, says when there are no VMs, deletes keys
On a superuser's page of a user the VM permissions read True/False; they
are now check marks as on the Users list. A user without VMs gets "No VMs."
instead of an empty table. A superuser can delete one of the user's SSH
keys, as with API tokens: the same view takes the user, the row names
whose key it was.

Closes #218
2026-10-26 11:48:40 +03:00
catborise
d80ec72fe0 fix(accounts): a user's page reads only that user's VMs
The administrator's view of a user loaded every VM in the panel and never
used them; the user's own VMs are now read in one query.
2026-10-26 11:19:42 +03:00
catborise
2e5ef34473 fix(accounts): an email address belongs to one user, so "Lost OTP?" reaches it
"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.
2026-10-26 11:19:42 +03:00
catborise
0b5eb990ed fix(admin): a forced password change cannot trap a user or be skipped with a token
- 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.
2026-10-26 10:12:53 +03:00
catborise
24896aaecd feat(admin): a password an administrator sets can be one the user must change
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.
2026-10-25 22:38:40 +03:00
catborise
0e8130f1b0 fix(admin): the Users pages keep an administrator in and say what their fields mean
- 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.
2026-10-25 22:38:40 +03:00
catborise
8063dcec66 feat(admin): the user list shows how many API tokens each user has
Which users can reach the API with a token was seen only by opening each
user. The user list now has an API tokens column: the number of the
user's tokens that still work (expired ones are left out; they are
deleted only at the user's next token), linked to the user's token tab.
One query counts them for all users.
2026-10-25 20:44:59 +03:00
catborise
85b5c769e9 feat(accounts): a superuser sees and deletes a user's API tokens
The user page a superuser opens from the user list has an API Tokens
tab: the user's tokens with their name, first characters, creation,
expiry and last use, and a delete button for one that may have leaked.
The token itself is never shown; only its digest is kept.

The delete reuses the owner's delete view with the user's id; only a
superuser may name another user. The confirmation and the log row say
whose token it was ("API token backup of alice"); the owner's own
confirmation now reads "Delete "API token backup"?" too. The profile
and the user page share one token table, and one script that opens a
tab named in the address (#apitokens).
2026-10-25 14:13:47 +03:00
catborise
6b51c30dd6 feat(accounts): API tokens show their last use, and a user has at most ten
The API Tokens tab gets a "Last used" column, so an unused token can be
told from one a script still needs. The token's last use is written when
it authenticates, at most once a minute (accounts.auth.TokenAuthentication,
knox's class with that one addition; the API schema describes it as
knox's).

A user has at most ten tokens; a new one is refused until one is
deleted. Expired tokens do not count, and are deleted when a new token is
made.
2026-10-25 10:16:52 +03:00
catborise
d9088b410e fix(accounts): knox creates no table of its own
knox's first migration creates its AuthToken table even when
KNOX_TOKEN_MODEL replaces it with accounts.ApiToken. The unused
knox_authtoken table references auth_user, and Django, which does not see
it, does not empty it with the others: on PostgreSQL every test that
empties the database failed ("cannot truncate a table referenced in a
foreign key constraint"). SQLite does not check this, so only CI's
PostgreSQL leg showed it. knox stays installed for its token code, with
its migrations turned off (MIGRATION_MODULES).
2026-10-25 10:00:28 +03:00
catborise
1c60696ea1 feat(accounts): API tokens for scripts and tools
The REST API took only the login session, so a script could not use it
without a browser. A user now makes API tokens on the profile page (API
Tokens tab): a name, and an expiry of 30, 90 or 365 days or none. The
token is shown once, on a page that is not cached, and then kept only as
a SHA-512 digest; the list shows its name, first characters, creation
and expiry, and a token is deleted there. Tools send it as
"Authorization: Token wvc_...", and act as its user, with that user's
permissions. Making and deleting a token is logged, without the token.

A password alone (HTTP Basic) still does not reach the API, as it would
skip the OTP second factor: a token is made in a session that passed the
login and its OTP. The token class comes first, so a refused request gets
401 with its challenge instead of 403.

django-rest-knox 5.1.0 provides the tokens (accounts.ApiToken adds the
name); settings.py takes "knox", KNOX_TOKEN_MODEL and REST_KNOX from the
template.
2026-10-24 23:14:27 +03:00
catborise
c1d36053d6 feat(logs): every VM operation writes an audit row the VM's Logs tab finds by UUID
Operations on a VM now write their rows the way the admin audit does: one
row per operation, done or failed with a safe reason, with the user, IP and
host. Each row names the VM ("VM web-1: delete snapshot snap1") and carries
its UUID in the new Logs.instance_uuid column.

- audit_vm and failed_vm wrap audit() and failed() for a VM. A failure the
  view turns into a message is caught outside the block, so its failed row
  is written; busy claims, permission and quota refusals and invalid input
  write nothing. No password, key or XML goes into a row.
- All VM page operations move from addlogmsg to audit_vm, and addlogmsg
  goes. QoS changes and defining a VM from XML are logged now; a gstfsd
  error reply (root password, SSH key) is a failed row, not a done one.
- Partial results are rows of their own: the VM delete, then each disk
  delete; the disk detach, then the volume delete; the migration, then a
  failed autostart restore, NVRAM removal or record on the target.
- Migration rows use the compute name as the host column, as every other
  row does.
- VM job rows (create, clone, snapshots) and their resolutions carry the
  VM's UUID; snapshot job intents now record it. The owner rows of the VM
  page and the accounts pages, and the ownership system rows, carry it too.
- The Logs tab loads vm_logs/<pk>/ and shows the rows with the VM's UUID,
  so a rename keeps the history, two VMs with one name are told apart and
  a VM on two computes is one VM. It gains a Result column. Rows from
  before this change have no UUID and stay on the Logs page only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 14:14:53 +03:00
catborise
3210ef2eee fix(accounts): a username longer than the field is refused by the login form
Django's login form gives the username its max_length after the field
is built, so a longer username was not refused and reached
authentication. django-axes then stored it in its 255-character column,
which PostgreSQL refuses: a login with a 300-character username was a
500 (SQLite does not enforce the length). Both login forms, with and
without OTP, now check the length, so such a username is a form error
and never reaches authentication, the lockout or the Logs page.

This also turns the PostgreSQL leg of CI green again: the S-16 test
from #167 hit exactly this.
2026-10-23 16:06:10 +03:00
catborise
1eaa3524d5 fix(accounts): a VM's only owner can be edited when one owner is allowed
With "Multiple Owner for VM" off, the ownership form counted the
ownership being edited as another owner, so changing the only owner's
rights (change, delete, console) always failed with "Instance owned by
another user". The check now leaves out the ownership being edited; a
second owner is still refused.
2026-10-23 13:30:25 +03:00
catborise
e04adec87b feat(logs): changes to users, groups, owners, flavors and settings are logged
The Logs page now records the panel's own records too: users (add,
change with the changed fields, delete, block, unblock, an admin's
password set, a user's own password change including the first-login
one, own profile edits), groups with their members and permissions,
VM owners from the accounts pages and from the VM page, OTP QR code
mails, flavors from web and API, app settings and the theme,
removed-VM records and SSH keys. No row carries a password or a key.

- A user is created in one transaction: when a VM named in
  NEW_USER_DEFAULT_INSTANCES is missing, no half-made user stays.
- The OTP mail checks the email address before it creates a device;
  without one it writes a failed row and makes no device.
- The VM page's owner rows read "VM web-1 owner bob: add" and
  "...: remove" instead of "Add owner: bob" and "Delete owner: 17"; a
  second owner refused by the single-owner setting is a failed row.
  Owner rows keep the VM's host and name, so the VM's log tab shows
  them.
- A setting or theme posted with its current value is no change and
  writes no row; the old free-text setting rows are gone.

Host-side objects (networks, storage, interfaces, secrets, filters)
follow in the next layer.
2026-10-23 11:10:24 +03:00
catborise
8c0fbff8f7 feat(logs): admin changes and logins are recorded on the Logs page
The Logs page shows who added, changed or deleted a compute, from web
or API, and every login, failed login, lockout and logout, with the
client address and a Result column (Done, Failed, Unknown for older
rows). A failed change is recorded with its reason and the error still
reaches the user; a refused one is recorded too.

- logs.audit.write saves each row in its own savepoint and never
  raises, so a log write cannot break or undo the action it records.
  The audit() context manager writes one row when the change ends.
- Logs gains success and ip; user, host and instance are wider and
  date is indexed. The page orders by date then id.
- A compute edit names the changed fields; an edit that changes
  nothing writes no row, and an empty password field is no change.
- A compute's VMs leaving the panel get a system row each: owners
  kept on the Removed VMs page, moved to another compute, restored,
  left without owners, or the removed record expired.
- Job rows carry the result. A job's log entry that cannot be
  computed is logged and skipped instead of rolling back the finish
  (test_service.py changes this on purpose).
- Logins are recorded by signal receivers; CustomLoginView is removed
  and the plain LoginView is used. A wrong OTP code is a failed login,
  counted by django-axes toward the lockout.

Panel records and host-side objects follow in the next layers.
2026-10-23 10:30:48 +03:00
catborise
fbe93d9aee fix(templates): each delete page says what deleting its object does
The shared delete page asked only whether to delete the object. Each
view now gives its consequence: a deleted compute's VMs keep running
but the panel forgets them and their owners, a deleted user's VMs
stay, a group's members keep their accounts, a VM's owner loses only
the access, a removed VM's record no longer brings its owners back,
an SSH key stays on the VMs that have it, a flavor only goes as a
size template.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 11:15:00 +03:00
catborise
961b43a42e fix(accounts): a signed-in user is sent on from the login page
The login page showed its sign-in card to a user who was already signed
in, with the navbar squeezed beside it. Both login views, with and
without OTP, now send that user to the page in next, or to the default
page when next is missing or points elsewhere. Every permission check
answers 403 instead of redirecting to the login, so this cannot loop.
The page test renders the login page signed out.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 20:33:44 +03:00
catborise
676bcfeb09 fix(templates): forms and detail lists fit a phone screen
Column offsets without a breakpoint applied below it, where the column
is already full width, so the change password, compute, user, VM
create and VM settings forms ran past the right edge on a phone. Each
offset now starts at its column's breakpoint. The interface and network
detail lists were rows directly inside rows and sit in a column now;
the compute form loses a Bootstrap 3 thumbnail padding that broke its
gutters, and the add-rule dialog's hint is no longer cut off. The page
test checks both grid rules on every page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 13:19:14 +03:00
catborise
e324e79fcb fix(templates): a wide table scrolls in its own box on a narrow screen
Twenty tables had no .table-responsive wrapper, so on a phone the user
list, compute list, VM lists, network and network filter pages scrolled
sideways as a whole. Each table now scrolls inside its box, as the
other ten already did. The page test walks the network page too, and
checks that every table on every page has the wrapper.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 12:57:26 +03:00
catborise
c55f52f025 test: each app keeps its tests in a tests package
131 test modules sat next to the code they test; instances alone had
51 of them beside views.py and utils.py. They move into a
tests/ package per app, and the JavaScript checks into
webvirtcloud/tests/js. Each non-empty tests.py becomes tests/test_views.py;
the empty generated stubs are removed.

Imports of the app's own modules become absolute, paths built from
__file__ go one level up, the live helper is instances.tests.livetest,
and coverage omits */tests/*. The suite discovers the same 960 tests as
before.
2026-10-12 15:17:30 +03:00
catborise
72681ab54e build(deps): the OTP e-mail draws its QR code with segno
django-qr-code was used for one thing: the QR code in the OTP e-mail,
as an inline SVG and a PNG. It brought pydantic, pydantic-core,
annotated-types and typing-inspection (about 10 MB) and, from 5.0 on,
Pillow. segno, which it used underneath, now draws both images with the
same options (error correction M, 4-module border, 1.8 mm SVG modules,
12 px PNG modules); the SVG, the PNG and the alt text are byte for byte
what django-qr-code 4.2 produced. account.html loaded the tag library
without using it.
2026-10-12 07:24:56 +03:00
catborise
40287dbd2a fix(accounts): OTP login works per user and "Lost OTP?" cannot impose 2FA
With OTP_ENABLED the login used django-otp's OTPAuthenticationForm. Under the
pinned django-otp 1.7.3 that form could not log anyone in through the shipped
page: the page posts only a 6-digit otp_token, but the form now needs the
device named explicitly, so an enrolled user got "Please select a device.",
and a user who never enrolled got "Please enter your OTP token." with no
device to produce one. Verified in an isolated test database.

SingleDeviceOTPForm makes OTP per user: a user with a confirmed TOTP device
must enter a valid code; a user without one logs in with the password alone.
The form chooses the device itself (the user's oldest confirmed TOTP device,
locked with select_for_update inside clean_otp's transaction so replay and
throttling counters cannot race) and ignores any posted otp_device, which
django-otp does not check for confirmed status or type. The login page drops
the HTML "required" on the token field (the field is optional for an
unenrolled user) and says so.

The "Lost OTP?" page (email_otp) created a confirmed device when none existed.
With per-user OTP that let anyone who knows an address turn a password-only
account into an OTP-required one and, if the mail never arrived, lock it out.
It now resends the QR only to a user who already enrolled and never creates a
device (get_existing_totp_device); admin enrollment (admin_email_otp) still
creates one. Email QR distribution and confirmed-on-create enrollment are
unchanged by user decision.

Tests: per-user login (no device -> password only; device -> valid token
passes, wrong/empty token fails), a posted or another user's device is not
used, a token cannot be replayed, and the real login page over HTTP with
OTP_ENABLED; email_otp sends nothing and creates no device for an unenrolled
user. The new tests fail on the previous code.
2026-10-07 08:48:55 +03:00
catborise
c2fea2aa39 feat(accounts): failed logins lock a user for the address they come from
Nothing limited failed logins: a password could be guessed at the speed
the server answers. django-axes (8.3.1, Django 5.2, Python 3.10-3.13)
now counts them.

- A user is locked for the address the failed logins come from
  (username and IP together), so an attacker cannot lock someone out
  from elsewhere and users behind one NAT address do not share a lock.
  A successful login resets the count. axes sits in front of every
  authentication: the login page with or without OTP, LDAP and the API.
- The number of failed logins (default 5) and the lock time in minutes
  (default 15) are on the Settings page (LOGIN_FAILURE_LIMIT,
  LOGIN_LOCKOUT_MINUTES, added by migration appsettings.0014) and apply
  at once; an invalid value falls back to the default. Unlock with
  "manage.py axes_reset_username <username>".
- Behind the bundled nginx every request comes from 127.0.0.1; the
  client address is read from X-Real-IP, which nginx sets, but only for
  requests from a loopback address or from LOGIN_TRUSTED_PROXIES, so a
  forged header does not escape a lock.

settings.py.template gets the app, middleware, backend and AXES_*
settings; README tells existing installations what to copy. Tests that
call client.login now pass a request, which the axes backend needs.
The new tests fail with axes disabled.
2026-10-06 11:05:03 +03:00
catborise
da63e570e5 fix(accounts): the "Lost OTP?" page cannot be used to flood or break
/accounts/email_otp/ mails a user their OTP QR code and needs no login.
Measured with the default settings (OTP disabled):

- It was reachable although OTP was off, and created OTP devices.
- Every request sent a mail: 20 requests for one address, 20 mails, so
  anyone who knew an address could fill that mailbox.
- An address shared by two users, or a mail server that could not be
  reached, ended in an error page (500).
- Inactive users were mailed too.

The page now exists only while OTP_ENABLED is set. It mails a single
active user whose address matches (case-insensitively), at most once
every 5 minutes per user (Django's cache: with several workers the
bound is per worker), and logs a failed delivery. The answer is the
same in every case, so it does not reveal whether an address belongs
to a user.

The QR codes come from django-qr-code (segno); both the mail and the
account page render them without the qrcode package removed earlier.
The new tests fail on the previous code.
2026-10-05 20:06:02 +03:00
catborise
99dabbdcec fix(accounts): migrate no longer overrides the change-password permission
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.
2026-10-04 20:26:53 +03:00
catborise
f94b3cee7a chore: drop references to the local roadmap from code and tests
Comments, docstrings and live test names cited item IDs of a roadmap
that is not part of the repository. They now say what is meant, or
nothing where the sentence stands without them. Test assertions and
behavior are unchanged.
2026-10-04 11:09:25 +03:00
catborise
e94d69a524 feat(accounts): Django's LoginRequiredMiddleware replaces the package
django-login-required-middleware is dropped; Django 5.1+ ships the same
middleware. The login page is already marked login_not_required by
Django; email_otp gets the decorator, which replaces
LOGIN_REQUIRED_IGNORE_VIEW_NAMES. The datasource still requires login.

Existing settings.py files still name the old middleware. A system
check (accounts.E002) stops migrate and the server until MIDDLEWARE
uses Django's middleware, instead of an ImportError at startup; the
README has the steps.

A new matrix test walks every URL anonymously: everything outside a
short allowlist (login pages, the OpenAPI docs) sends the visitor to
the login page, and API views deny with 401/403. It passed with the old
middleware first and passes unchanged now.
2026-10-04 01:08:11 +03:00
catborise
920f318601 build: Django 5.2.17 LTS
Django 4.2 reached end of life on 2026-04-07.

- Django==5.2.17; django-login-required-middleware 0.9.0 keeps working
  (the authorization matrix passes).
- USE_L10N removed from the settings template.
- Log out is a POST form in the navbar; Django 5 removed GET logout.
- pip-audit in CI no longer ignores the Django 4.2.30 advisories.
- README: Django 5.2, backup step and upgrade notes for existing
  settings and GET logout callers.

python -Wd manage.py test shows no Django deprecation warnings. The live
libvirt suite passes on 5.2 and on 4.2 against a RHEL qemu-kvm host.
2026-10-04 01:08:11 +03:00
catborise
b04f656d2b ci: replace super-linter with ruff
Super-linter ran from the unpinned :latest image and, because of
branches-ignore: [master], never on pull requests to master. Ruff now
checks syntax errors and pyflakes rules (E9, F) on every push and PR.

Fixes the existing findings: unused imports and variables, an unused
duplicate import in instances/tests.py, and the star import in
accounts/views.py. settings-dev.py keeps its star import of the base
settings.
2026-10-04 01:08:11 +03:00
catborise
6dabd69445 fix(accounts): first-login password change is stored, not in the session
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'.
2026-10-04 01:08:11 +03:00
catborise
78852eb4c8 feat(accounts): force a password change at the first login with the generated password
While data/admin_password exists and a user's password still matches it,
the login sets a session flag (checked once at login, not per request).
ForcePasswordChangeMiddleware then sends that user to the password-change
page for every view except change password, login and logout. Setting a
new password clears the flag and deletes data/admin_password. No schema
change; if ADMIN_PASSWORD was set, there is no file and nothing is forced.
2026-10-02 23:55:31 +03:00
catborise
40822e7184 fix: write the admin password file without following symlinks
In Docker, migrate runs as root while data/ belongs to www-data, so a
planted data/admin_password symlink would make root overwrite its target.
Remove any existing path first and create the file with
O_CREAT|O_EXCL|O_NOFOLLOW (mode set via fchmod). install.sh's /tmp
fallback log is now created with mktemp instead of a fixed name.
2026-10-02 23:17:32 +03:00
catborise
7b53493800 fix: stop printing secrets during installation
- The generated admin password is written to data/admin_password (mode
  0600) instead of being printed, so it no longer ends up in the install
  log or `docker logs`. If the file cannot be written it is printed as
  before, so the admin is never locked out.
- webvirtcloud.sh no longer echoes the generated SECRET_KEY.
- install.sh creates the install log with mode 0600.
2026-10-02 22:47:56 +03:00
catborise
cf1cde376b fix: require POST for user block/unblock and OTP email, flag console cookie
- user_block, user_unblock and admin_email_otp changed state on GET, so an
  <img> tag could lock a user out or trigger OTP emails through an
  admin's session. They now require POST; the buttons are CSRF forms.
- The console token cookie is set with httponly, SameSite=Lax and, on
  HTTPS, secure. Only novncd reads it; the page gets the token directly.
2026-10-02 22:44:00 +03:00
catborise
b3e6fb2b7a feat(security): enforce require_POST on power actions, prevent open redirects, and isolate instance permissions 2026-09-27 13:20:45 +03:00
catborise
1227674d8f feat(security): implement P0 critical security fixes for API, console, and access control
- Computes API: mark password as write_only in ComputeSerializer, restrict ComputeViewSet and architectures to IsAdminUser.
- Instances API: enforce user ownership tenant isolation in InstanceViewSet.list, restrict MigrateViewSet and CreateInstanceViewSet to IsAdminUser, require IsAdminUser for mutating FlavorViewSet.
- Storage, Network, and Interface APIs: restrict StorageViewSet, VolumeViewSet, NetworkViewSet, InterfaceViewSet to IsAdminUser.
- AppSettings & NWFilters: protect appsettings and nwfilter views with @superuser_only (403 Forbidden for non-superusers).
- noVNC Console: sign tokens with TimestampSigner (salt='console.novnc', max_age=600), verify signature and user authorization in novncd with backward-compatible fallback for debug/test mode.
- Accounts: avoid hardcoded default admin password; generate cryptographically secure random password unless ADMIN_PASSWORD is set (preserve 'admin' in test runner).
- gstfsd daemon: bind to 127.0.0.1 by default instead of 0.0.0.0.
- Tests: added comprehensive test coverage for API security, token expiration/rejection, and view permissions (132 tests passing).
2026-09-27 10:50:09 +03:00
catborise
ac91248a29 feat(ui): make authentication pages 100% compatible with Bootswatch themes
- Replace hardcoded colors with Bootstrap & Bootswatch CSS custom properties
- Adapt background, card, borders, and inputs dynamically to active light and dark themes
- Inherit primary color, border-radius, and font-family from active theme
- Use native Bootswatch alert and button styles for consistent visual hierarchy
2026-09-26 23:31:24 +03:00