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

fix(instances): an owner cannot power a VM an administrator paused

Only administrators suspend and resume, and the page tells an owner of a
paused VM "Administrator blocked your instance", but the power views let
the owner force it off or cycle it, on the page and the API, and start it
again: the block ended. The power permission now refuses a paused VM to
everyone but administrators.
This commit is contained in:
catborise 2026-10-26 12:07:45 +03:00
parent 42a1783178
commit 182746c73e
2 changed files with 59 additions and 5 deletions

View file

@ -0,0 +1,44 @@
"""A VM an administrator paused stays paused until an administrator resumes
it: its owners cannot power it off or cycle it, on the page or the API."""
from unittest.mock import patch
from accounts.models import UserInstance
from computes.models import Compute
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from instances.models import Instance
from libvirt import VIR_DOMAIN_PAUSED, VIR_DOMAIN_RUNNING
class PausedBlockTests(TestCase):
def setUp(self):
User = get_user_model()
self.owner = User.objects.create_user("pb-owner", password="pw")
self.admin = User.objects.create_superuser("pb-admin", "pb@example.com", "pw")
compute = Compute.objects.create(name="pb", hostname="192.0.2.1", login="u", password="p", type=1)
self.vm = Instance.objects.create(compute=compute, name="pb-vm", uuid="14141414-0000-0000-0000-000000000000")
UserInstance.objects.create(user=self.owner, instance=self.vm, is_change=True, is_delete=True)
patcher = patch("instances.models.wvmInstance")
self.proxy = patcher.start().return_value
self.addCleanup(patcher.stop)
def test_an_owner_cannot_power_a_paused_vm(self):
self.proxy.get_status.return_value = VIR_DOMAIN_PAUSED
self.client.force_login(self.owner)
for view in ("force_off", "powercycle", "poweroff", "poweron"):
self.assertEqual(self.client.post(reverse(f"instances:{view}", args=[self.vm.pk])).status_code, 403, view)
api = reverse("compute-instance-forceoff", kwargs={"compute_pk": self.vm.compute_id, "pk": self.vm.pk})
self.assertEqual(self.client.post(api).status_code, 403)
self.proxy.force_shutdown.assert_not_called()
self.proxy.shutdown.assert_not_called()
def test_an_administrator_can_and_an_owner_of_a_running_vm_can(self):
self.proxy.get_status.return_value = VIR_DOMAIN_PAUSED
self.client.force_login(self.admin)
self.client.post(reverse("instances:force_off", args=[self.vm.pk]))
self.proxy.get_status.return_value = VIR_DOMAIN_RUNNING
self.client.force_login(self.owner)
self.client.post(reverse("instances:force_off", args=[self.vm.pk]))
self.assertEqual(self.proxy.force_shutdown.call_count, 2)

View file

@ -33,7 +33,8 @@ from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_noop as _
from django.views.decorators.http import require_POST
from libvirt import (VIR_DOMAIN_UNDEFINE_KEEP_NVRAM,
from libvirt import (VIR_DOMAIN_PAUSED,
VIR_DOMAIN_UNDEFINE_KEEP_NVRAM,
VIR_DOMAIN_UNDEFINE_MANAGED_SAVE,
VIR_DOMAIN_UNDEFINE_SNAPSHOTS_METADATA,
VIR_DOMAIN_UNDEFINE_NVRAM,
@ -546,6 +547,15 @@ def get_instance(user, pk, perm_type="view"):
return instance
def _power_instance(user, pk):
"""The VM to power on, off or cycle: a VM an administrator paused stays
paused until an administrator resumes it."""
instance = get_instance(user, pk, perm_type="power")
if not user.is_superuser and instance.proxy.get_status() == VIR_DOMAIN_PAUSED:
raise PermissionDenied
return instance
def from_script(request):
"""A form a page's script sends and answers itself: the create page's
flavor forms (it swaps in the reply), the XML editor's dialog (JSON)."""
@ -684,7 +694,7 @@ def reject_disk_options(request, pk, invalid):
@require_POST
@serialize_instance_mutation
def poweron(request, pk):
instance = get_instance(request.user, pk, perm_type="power")
instance = _power_instance(request.user, pk)
if instance.is_template:
messages.warning(request, _("Templates cannot be started."))
else:
@ -699,7 +709,7 @@ def poweron(request, pk):
@require_POST
@serialize_instance_mutation
def powercycle(request, pk):
instance = get_instance(request.user, pk, perm_type="power")
instance = _power_instance(request.user, pk)
if instance.is_template:
messages.warning(request, _("Templates cannot be started."))
else:
@ -714,7 +724,7 @@ def powercycle(request, pk):
@require_POST
@serialize_instance_mutation
def poweroff(request, pk):
instance = get_instance(request.user, pk, perm_type="power")
instance = _power_instance(request.user, pk)
with audit_vm(request, instance, "power off"):
instance.proxy.shutdown()
@ -750,7 +760,7 @@ def resume(request, pk):
@require_POST
@serialize_instance_mutation
def force_off(request, pk):
instance = get_instance(request.user, pk, perm_type="power")
instance = _power_instance(request.user, pk)
with audit_vm(request, instance, "force off"):
instance.proxy.force_shutdown()
return get_safe_redirect(