1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00

fix(instances): a new VM and each clone get a VNC password of their own

Since the VM create became a job (6f0c7300) every new VM was defined with
no VNC password, and a clone copied its source's password (or its lack
of one) exactly: every clone of a template shared it. Computes listen
for VNC on all their addresses (dev/libvirt-bootstrap.sh), so anyone who
could reach a compute's VNC ports opened any of these consoles directly,
without the panel; the README's firewall rule was the only guard.

The create job asks for create.py's random 8-character password again,
and a clone's VNC graphics get a new random one. The console page sends
the password to a user with passwordless_console, which the user create
form now ticks by default next to change_password; a user without it is
asked for the password, as the permission intends. Existing VMs keep
their settings.

Live create, clone and VM page tests pass on a TCP compute.
This commit is contained in:
catborise 2026-11-06 20:00:06 +03:00
parent c5d0672c68
commit 1f9347e148
7 changed files with 47 additions and 3 deletions

View file

@ -769,6 +769,7 @@ What an update from an older version changes for operators and users:
- Failed logins lock a user (django-axes). The limit and lock time are on the Settings page; unlock with `python3 manage.py axes_reset_username <username>`.
- Changing one's own password is the "Can change password" permission (per user or group; new users have it); `SHOW_PROFILE_EDIT_PASSWORD` is gone.
- The admin's first-login password change survives losing `data/admin_password` (migration `accounts.0007`); run `migrate` as a user who can read that file.
- A new VM and each clone get a random VNC password of their own (clones no longer share their source's). The console page sends it for a user with **Open a console without the VM's console password**, which a new user has unless unticked; others are asked for it. Existing VMs keep theirs.
- An `admin` account that still has the password `admin` (the first user of versions up to v2.0.0) must change it at its next login, and its API tokens are refused until then (migration `accounts.0013`).
- A `settings.py` kept from an older version must match the template (apps, middleware, the SQLite `OPTIONS`, the REST framework, knox, axes and password settings): rebuild it from the template and carry your own values over, as step 2 of the update does.
- The VNC console connects to `/novncd/` on the panel's own address through nginx, and the serial console to `/socket.io/`: they work on HTTPS and behind another proxy without settings. `WS_PUBLIC_PORT`, `WS_PUBLIC_HOST`, `WS_PUBLIC_PATH`, `WS_CERT` and `SOCKETIO_PUBLIC_HOST`/`_PORT`/`_PATH` are gone; remove them from `docker-compose.override.yml`, `/etc/default/webvirtcloud` or a kept `settings.py`, set `WS_HOST = "127.0.0.1"` in a kept `settings.py`, and close port 6080 in the firewall.

View file

@ -6,6 +6,7 @@ from django.contrib.auth import password_validation
from django.contrib.auth.forms import AdminPasswordChangeForm
from django.contrib.auth.models import Group, User
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from accounts.forms import UniqueEmailMixin
@ -124,9 +125,11 @@ class UserCreateForm(UserForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# New users may change their own password unless unticked here
# New users may change their own password and open their VMs' consoles
# (whose VNC password the console page sends) unless unticked here
self.fields["user_permissions"].initial = Permission.objects.filter(
content_type__app_label="accounts", codename="change_password"
Q(content_type__app_label="accounts", codename="change_password")
| Q(content_type__app_label="instances", codename="passwordless_console")
)
def clean(self):

View file

@ -1,6 +1,8 @@
"""Creating a user in the admin pages: the password reaches the database only
hashed, and the configured password validators apply as on the change forms."""
import re
from django.contrib.auth.models import Group, Permission, User
from django.db import connection
from django.test import TestCase, override_settings
@ -36,6 +38,12 @@ class UserCreateTests(TestCase):
self.assertEqual(written, [])
self.assertTrue(User.objects.get(username="newbie").check_password(RAW))
def test_a_new_user_may_change_the_password_and_open_consoles_unless_unticked(self):
# new VMs have a VNC password, which the console page sends only with passwordless_console
page = self.client.get(reverse("admin:user_create")).content.decode()
ticked = set(re.findall(r'<input[^>]*name="user_permissions"[^>]*value="(\d+)"[^>]*checked', page))
self.assertEqual(ticked, {str(self.change_password.pk), str(Permission.objects.get(codename="passwordless_console").pk)})
def test_groups_and_permissions_are_saved(self):
group = Group.objects.create(name="uc-group")
self.create(groups=[group.pk], user_permissions=[self.change_password.pk])

View file

@ -884,7 +884,7 @@ class CreateInstance(Kind):
nwfilter=data["nwfilter"],
net_model="virtio" if data["net_model"] == "default" else data["net_model"],
video=data["video"],
console_pass="",
console_pass="random", # without one, anyone reaching the VNC port is in
mac=data["mac"],
qemu_ga=data["qemu_ga"],
add_cdrom=data["add_cdrom"],

View file

@ -48,6 +48,8 @@ class CreateJobTestCase(TransactionTestCase):
op.refresh_from_db()
CreateInstance().run(op, Context(op, SimpleNamespace(identity="wvc-test:1:a")))
self.assertEqual(self.conn.create_instance.call_args.kwargs["uuid"], op.intent["uuid"])
# a console password of its own: without one, anyone reaching the VNC port is in
self.assertEqual(self.conn.create_instance.call_args.kwargs["console_pass"], "random")
self.assertTrue(Instance.objects.filter(uuid=op.intent["uuid"], name="api-vm").exists())
def test_the_api_takes_secure_boot_and_a_tpm(self):

View file

@ -2398,6 +2398,10 @@ class wvmInstance(wvmConnect):
raise ValueError(f"No MAC address for network interface {num}")
net.find("mac").set("address", self.fix_mac(mac))
# the clone's console gets a password of its own: one copied from the source would open both
for graphics in tree.findall("devices/graphics[@type='vnc']"):
graphics.set("passwd", util.randomPasswd(8)) # the length VNC allows
nvram = tree.find("os/nvram")
if nvram is not None and nvram.get("type", "file") != "file":
raise util.OperationError(gettext_noop("Cannot clone a VM whose NVRAM is not a file"))

View file

@ -0,0 +1,26 @@
"""A clone's VNC console gets a password of its own: one copied from its
source (a template's, say) would open the source's and every other clone's."""
import unittest
from vrtManager.instance import wvmInstance
def clone_graphics(passwd_attribute):
p = wvmInstance.__new__(wvmInstance) # no connection: only these calls
p.get_instances = lambda: []
p._XMLDesc = lambda flags: f"""<domain><name>tpl</name><uuid>u</uuid><devices>
<graphics type='vnc' port='-1' autoport='yes' {passwd_attribute} listen='0.0.0.0'/></devices></domain>"""
tree, _ = p._clone_definition({"name": "copy"}, "abababab-0000-1111-2222-333333333333")
return tree.find("devices/graphics")
class CloneConsoleTestCase(unittest.TestCase):
def test_each_clone_gets_its_own_vnc_password(self):
passwords = {clone_graphics("passwd='shared12'").get("passwd") for _ in range(5)}
self.assertNotIn("shared12", passwords)
self.assertEqual(len(passwords), 5)
self.assertTrue(all(len(p) == 8 for p in passwords)) # the length VNC allows
def test_a_source_without_a_password_gives_a_clone_with_one(self):
self.assertEqual(len(clone_graphics("").get("passwd")), 8)