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:
parent
c5d0672c68
commit
1f9347e148
7 changed files with 47 additions and 3 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
26
vrtManager/tests/test_clone_console.py
Normal file
26
vrtManager/tests/test_clone_console.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue