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

Merge branch 'fix/wave0-authorization-matrix' into wave0-integration

# Conflicts:
#	datasource/views.py
This commit is contained in:
catborise 2026-10-02 22:14:40 +03:00
commit 51db12acfe
12 changed files with 390 additions and 75 deletions

View file

@ -0,0 +1,74 @@
"""
ROADMAP Wave 0 item 5 (S-09): one rule decides who may open a VM's console
(noVNC page, novncd, the virt-viewer .vv file, the VDI URL): a superuser or
an owner of the VM. The global view_instances permission is read-only.
"""
from unittest.mock import MagicMock, patch
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from django.test import TestCase
from django.urls import reverse
from accounts.models import UserInstance
from computes.models import Compute
from instances.models import Instance
from instances.utils import can_open_console
class CanOpenConsoleTestCase(TestCase):
def setUp(self):
User = get_user_model()
self.superuser = User.objects.create_superuser(
username="cons_super", email="cons_super@example.com", password="x"
)
self.owner = User.objects.create_user(username="cons_owner", password="x")
self.inactive_owner = User.objects.create_user(
username="cons_inactive", password="x", is_active=False
)
self.viewer = User.objects.create_user(username="cons_viewer", password="x")
self.viewer.user_permissions.add(Permission.objects.get(codename="view_instances"))
self.stranger = User.objects.create_user(username="cons_stranger", password="x")
self.compute = Compute.objects.create(
name="cons-compute", hostname="127.0.0.1:1", login="root", password="", type=1
)
self.instance = Instance.objects.create(
compute=self.compute,
name="cons-vm",
uuid="77777777-8888-9999-aaaa-bbbbbbbbbbbb",
)
UserInstance.objects.create(instance=self.instance, user=self.owner)
UserInstance.objects.create(instance=self.instance, user=self.inactive_owner)
def test_superuser_and_owner_may_open_console(self):
self.assertTrue(can_open_console(self.superuser, self.instance))
self.assertTrue(can_open_console(self.owner, self.instance))
def test_view_instances_stranger_and_inactive_owner_may_not(self):
self.assertFalse(can_open_console(self.viewer, self.instance))
self.assertFalse(can_open_console(self.stranger, self.instance))
self.assertFalse(can_open_console(self.inactive_owner, self.instance))
def test_owner_can_download_vv_file(self):
self.client.force_login(self.owner)
with patch("instances.views.wvmInstances") as mock_conn_cls:
conn = mock_conn_cls.return_value
conn.graphics_type.return_value = "vnc"
conn.graphics_listen.return_value = "127.0.0.1"
conn.graphics_port.return_value = "5900"
conn.domain_name.return_value = "cons-vm"
conn.graphics_passwd.return_value = "secret"
res = self.client.get(reverse("instances:getvvfile", args=[self.instance.id]))
self.assertEqual(res.status_code, 200)
self.assertIn(b"[virt-viewer]", res.content)
def test_owner_can_get_vdi_url(self):
self.client.force_login(self.owner)
with patch("datasource.views.wvmInstance", MagicMock()), patch(
"datasource.views.get_hostname_by_ip", return_value="host"
):
res = self.client.get(
reverse("vdi_url", args=[self.compute.id, self.instance.name])
)
self.assertEqual(res.status_code, 200)

View file

@ -805,13 +805,6 @@ class InstancesTestCase(TestCase):
)
self.assertEqual(response.status_code, 200)
def test_check_instance(self):
response = self.client.get(
reverse("instances:check_instance", args=["test-vm"])
)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(response.content, {"vname": "test-vm", "exists": True})
def test_start_template(self):
# starting templates must fail
self.assertEqual(self.instance.status, 5)

View file

@ -82,6 +82,5 @@ urlpatterns = [
),
path("guess_clone_name/", views.guess_clone_name, name="guess_clone_name"),
path("random_mac_address/", views.random_mac_address, name="random_mac_address"),
path("check_instance/<vname>/", views.check_instance, name="check_instance"),
path("<int:pk>/sshkeys/", views.sshkeys, name="sshkeys"),
]

View file

@ -14,6 +14,20 @@ from vrtManager.instance import wvmInstance, wvmInstances
from .models import Instance
def can_open_console(user, instance):
"""
The single rule for console access (noVNC page, novncd, the virt-viewer
.vv file and the VDI URL): an active superuser or an active owner of the
VM. The global view_instances permission is read-only and does not grant
an interactive console (ROADMAP S-09).
"""
if not user.is_active:
return False
if user.is_superuser:
return True
return UserInstance.objects.filter(instance=instance, user=user).exists()
def get_clone_free_names(size=10):
prefix = app_settings.CLONE_INSTANCE_DEFAULT_PREFIX
free_names = []

View file

@ -255,6 +255,7 @@ def osinfo(request, pk):
return JsonResponse(results)
@superuser_only
def guess_mac_address(request, vname):
data = {"vname": vname}
mac = utils.get_dhcp_mac_address(vname)
@ -264,12 +265,14 @@ def guess_mac_address(request, vname):
return JsonResponse(data)
@superuser_only
def random_mac_address(request):
data = dict()
data["mac"] = utils.get_random_mac_address()
return JsonResponse(data)
@superuser_only
def guess_clone_name(request):
dhcp_file = "/srv/webvirtcloud/dhcpd.conf"
prefix = app_settings.CLONE_INSTANCE_DEFAULT_PREFIX
@ -288,14 +291,6 @@ def guess_clone_name(request):
return JsonResponse({})
def check_instance(request, vname):
instance = Instance.objects.filter(name=vname)
data = {"vname": vname, "exists": False}
if instance:
data["exists"] = True
return JsonResponse(data)
def sshkeys(request, pk):
"""
:param request:
@ -1829,6 +1824,9 @@ def change_options(request, pk):
def getvvfile(request, pk):
instance = get_instance(request.user, pk)
# The .vv file contains the VNC password: same rule as the console.
if not utils.can_open_console(request.user, instance):
raise PermissionDenied
conn = wvmInstances(
instance.compute.hostname,
instance.compute.login,