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'.
- Routers use path converters (use_regex_path=False): computes, instances
and flavors take <int:...> ids, so non-numeric ids get 404 and
drf-spectacular derives the parameter types itself.
- Each method declares what it really returns: detail serializers,
a shared StatusSerializer for status-only replies, no request body for
actions. Instance properties used by the details serializer get return
type hints.
- The create-instance API drops the firmware field. Its only accepted
value was "" (BIOS), which produced an empty, invalid enum, and the
UEFI branch behind it was unreachable. Clients that still send it are
unaffected; DRF ignores unknown fields.
The schema now passes spectacular --validate --fail-on-warn; a test
keeps it that way.
app_active and view_active read request.resolver_match, which is None
when no URL matched, so the navbar raised AttributeError and the 404
became a 500.
The *migrations/* ignore rule is removed. appsettings, computes and
logs pin default_auto_field to AutoField, so makemigrations --check
reports no pending BigAutoField migrations and existing installs need
no schema change.
The Vagrant provisioning no longer runs makemigrations, which created
install-specific migration files that conflict with the committed
history; it only runs migrate.
- Add the public key to authorized_keys instead of replacing the file.
- Use AF_INET6 when GSTFSD_BIND_HOST is an IPv6 address (binding ::1
crashed with the AF_INET server).
- Close idle client connections after 30 s.
Tests run the real daemon against a small fake guestfs module over a
socket; this also exercises the earlier sendall() fix.
- instances/test_xss.py: scan all templates for .html(, innerHTML = and
append('<...') and require each line to be in a reviewed allowlist, so
new sinks fail the test (the exact-string tests only caught the lines
they named).
- Authorization matrix: fill the datasource "version" parameter with
"latest" instead of "x". The datasource views then answer 200 to any
logged-in user; they are listed in the allowlist as known F-14.
A disk without driver/@type could not be edited: the empty format was
rejected, and edit_disk would have written type=''. An empty format is
now accepted and edit_disk leaves the type attribute out.
readQueryVariable('token', ...) preferred the unsigned host-uuid token
from the URL, which novncd rejects; use the signed token the view
renders, like the full page does.
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.
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.
- The serial console cannot work until socketiod is rewritten (it does
not understand the signed console token), so the README no longer
describes how to enable it; only the hardening line for existing
installs is kept. The reverse-proxy example no longer sets
SOCKETIO_PUBLIC_PORT.
- Move the users, roles and permissions reference to doc/permissions.md
and link it from the README.
Clone refused VMs that cloned fine before: a disk without a volume got a
None disk name, and a source image with characters such as a space or "@"
produced a derived name outside the allowed pattern. Disks without a
volume (which clone_instance does not copy) are no longer validated, and
derived disk names are sanitized to the allowed charset. Names typed by a
superuser are still validated strictly.
gstfsd only listens on loopback unless GSTFSD_BIND_HOST and
GSTFSD_ALLOW_REMOTE=1 are set; add commented environment lines for both
service definitions. gstfsd.conf now ends with a newline, which matters
because dev/libvirt-bootstrap.sh appends it to /etc/supervisor.conf.
The edit_volume bus change detached the disk and attached a new one built
as a file disk, so block and rbd disks lost their source and auth, and a
failed attach left the disk detached. Add wvmInstance.change_disk_bus,
which reuses the disk element and only changes <target> (dropping the old
bus address), and re-attaches the original disk if attaching fails.
edit_disk also rebuilt every disk with <source file=...>; it now keeps the
existing <source> and <auth> of block and network disks.
- Hash the root password with a random SHA-512 salt instead of the fixed
"$6$kgPoiREy".
- Talk to gstfsd through one helper with a 120 s socket timeout; an
unreachable daemon or a bad reply is an error message, not a 500.
- gstfsd refuses to listen on a non-loopback address unless
GSTFSD_ALLOW_REMOTE=1: it is an unauthenticated root service.
- gstfsd: encode replies (sendall(str) raised TypeError on Python 3, so
the success path never worked) and use octal chmod modes.
- README: what gstfsd does and how to allow it on a remote compute.
- 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.
- 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.
compute_graph (text/javascript) and the machine type, disk bus, video
model and domain capability views (text/html) return JSON; send it as
application/json. create_instance_w1.html builds the chipset <option>
elements with .val().text() instead of string concatenation.
- edit_volume passed driver_type to attach_disk, which has no such
parameter, so every bus change failed after the disk was detached; pass
format_type.
- A disk without a driver type shows "None" in the edit form; keep the
disk's current format instead of rejecting it.
- Validate with fullmatch, so a trailing newline is no longer accepted,
and use one volume-name pattern for disk views and clone.
- Comment out the nginx /socket.io/ location: a firewall on 6081 does not
protect a console that nginx proxies on 80/443.
- Default SOCKETIO_HOST to 127.0.0.1.
- README: short enable steps and the changes existing installs need.
- test_socketiod_disabled writes its settings module to a temporary
directory instead of the source tree.
For six roles (global view_instances with and without is_staff, and VM
owners with no flags, is_change, is_delete, and is_change+is_vnc as staff)
walk every URL and compare the reachable set with the expected one in both
directions, so both privilege leaks and lost access fail the test. Each
request runs in a rolled-back transaction so destructive views do not
affect the following requests. The expectations mirror the roles table in
README.md and depend on all Wave 0 fixes, so the test lives on the
integration branch.
change_options set is_template for any owner with is_change; the
"staff only" rule existed only as a disabled checkbox in the UI. A
non-staff owner could mark or unmark templates with a POST, and because a
disabled checkbox is not submitted, a non-staff owner who only edited a
template's title or description silently unmarked it, which lifted the
template-only restrictions.
Only change is_template for superusers and staff (for them an unchecked,
unsubmitted checkbox still means "not a template"); leave it alone for
everyone else.
is_staff does not reveal console settings or VNC passwords (fixed on
fix/wave0-staff-console-password), but staff owners with is_change may
also change VMs marked as templates. Superuser-only pages answer 403 to
everyone else, and the console page answers 403 both for a missing VM and
for a VM the user may not open.
The console settings form on the VM detail page renders the VNC password
(PasswordInput(render_value=True)) and was shown to every is_staff user.
A staff user with the global view_instances permission, or a staff owner
without is_vnc, could read the VNC password of every VM they could see.
Add instances.utils.can_manage_console (active superuser, or an active
owner with is_change and is_vnc), which matches what update_console
already enforced. The detail view only builds the form, and so only puts
the password into the page, when it returns True; the template and
update_console use the same rule.
Describe the access model: superuser, the global permissions
(view_instances as a read-only auditor role, clone_instances,
snapshot_instances, passwordless_console, change_password) and per-VM
ownership flags (is_change, is_delete, is_vnc), with a table of which
VM actions each combination allows.
The table describes the state after all Wave 0 branches are merged:
disk/media views are superuser-only (fix/wave0-disk-tenant-isolation) and
clone requires is_change except for templates (fix/wave0-clone-permission).
novncd accepted signed tokens without the user part, which skipped the
per-user access check, and accepted unsigned "host-uuid" tokens whenever
"test" appeared in sys.argv. The console view always signs
host:uuid:user_id, so anything else is now rejected with PermissionError,
and the user check always runs.
Add webvirtcloud/test_authorization_matrix.py, which walks every named URL
and checks that a user without any VM/compute relation only reaches an
explicit allowlist, and that the global view_instances permission does not
open consoles. It found:
- /computes/<id>/... views without any check (compute_graph,
get_compute_*, get_dom_capabilities, storages.get_volumes): the whole
computes URL namespace is now wrapped with superuser_only, so a view
added there later cannot be exposed by a missing decorator.
- guess_mac_address (leaks dhcpd.conf host/MAC pairs), random_mac_address
and guess_clone_name: superuser-only, like the buttons that use them.
- check_instance: cross-tenant VM name lookup with no caller; removed.
- getvvfile and get_vdi_url: reachable with only view_instances, and the
.vv file contains the VNC password.
Console access now follows one rule, instances.utils.can_open_console:
an active superuser or an active owner of the VM. The global
view_instances permission is read-only. It is used by the console page,
novncd, getvvfile and get_vdi_url (getvvfile and the VDI tab are kept for
virt-viewer users). The console page answers 403 instead of 500 when
access is denied, with the same answer for a missing VM.
clone only required "view" access, so a user with clone_instances and the
global view_instances permission could clone anyone's VM and read its
disks. It also copied every POST field into clone_data, which let the
client override the server-set disk_owner_uid/gid, and disk-<dev> values
went unvalidated into volume XML and os.path.join.
- Require change permission on the source VM, except for templates,
which any user who can view them may clone.
- Build clone_data from an allowlist: name, title and description for
everyone; disk names and MAC addresses only from superusers.
- For non-superusers, derive disk names (get_clone_disk_name) and MAC
addresses (DHCP or random) on the server; their form only sent the
source VM's own values, which could never work.
- Validate every MAC address (not only the first) and every disk name;
a missing MAC is an error message instead of a KeyError.
Several views returned json.dumps() output through HttpResponse, i.e. as
text/html, and guess_mac_address reflects the URL's vname into it
(reflected XSS). Templates filled the log table and volume dropdowns by
concatenating server data into HTML strings, and log messages contain
user-entered snapshot/disk/clone names (stored XSS that runs when a
superuser opens the Logs tab).
- Use JsonResponse in guess_mac_address, random_mac_address,
guess_clone_name, sshkeys, storages.get_volumes,
datasource.os_metadata_json and logs.vm_logs; sshkeys?plain=true is
served as text/plain.
- stats_tab.html builds log rows with .text(); instance.html and
create_instance_w2.html build <option> elements with .val().text().
socketiod does not validate the console token, keeps a single global PTY
and broadcasts its output to every connected client, so anyone who can
reach port 6081 can read and write a serial console that someone opened.
It is also broken for legitimate users, since console/views.py now writes
signed tokens that socketiod cannot parse.
Until it is rewritten, turn the serial console off by default:
- Add SERIAL_CONSOLE_ENABLED = False to the settings template; installs
whose settings.py lacks it are treated as disabled.
- The console view shows an error instead of the xterm page for pty
consoles while the setting is off.
- socketiod refuses to start unless SERIAL_CONSOLE_ENABLED is True.
- Supervisor: autostart=false for socketiod; drop -d (debug) from the
supervisor and systemd definitions; fix the systemd unit description;
conf/systemd/install.sh no longer suggests enabling socketiod.
- README: stop opening port 6081 and document how to enable the serial
console on a trusted network.
Disk and media mutation views (add_new_vol, add_existing_vol, edit_volume,
delete_vol, detach_vol, add_cdrom, detach_cdrom, mount_iso, unmount_iso)
were reachable by any owner with is_change, although the UI only shows the
Disk tab to superusers. An ordinary user could delete any volume on the
compute or attach arbitrary host files to their VM.
- Require superuser for all disk/media views, matching the UI.
- delete_vol resolves the volume from the VM's own disk XML by dev and
ignores the posted storage/name.
- Validate bus/cache/io/discard/detect_zeroes against the hypervisor's
lists, and format/serial/volume name against strict patterns.
- add_existing_vol only accepts volumes that exist in the selected pool.
- edit_volume only accepts a dev attached to the VM.
- vrtManager: escape all values interpolated into disk XML and pass dev
as an XPath variable in detach_disk/edit_disk.
- Rewrite purpose/features intro around libvirt as single source of truth
- Generate SECRET_KEY into data/secret_key instead of editing settings.py
- Consolidate local development setup into one flow using
--system-site-packages, settings-dev and runserver --nostatic
- Document the generated admin password instead of admin/admin
- Use the shared supervisor config on RHEL (adds missing socketiod)
and add supervisor setup on openSUSE
- Document SOCKETIO_PUBLIC_PORT and CSRF_TRUSTED_ORIGINS for proxies
- Point test setup at the dev virtualenv
runserver imports the WSGI application from a worker thread, where
signal.signal() raises ValueError and the dev server fails to start.
Gunicorn imports it from the worker's main thread, so production
keeps reaping child processes as before.