Skip to content

Commit

Permalink
q-dev: Set.required -> Set.assignment
Browse files Browse the repository at this point in the history
  • Loading branch information
piotrbartman committed Aug 28, 2024
1 parent bb02ee2 commit 65f14db
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 66 deletions.
10 changes: 5 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -66,31 +66,31 @@ ADMIN_API_METHODS_SIMPLE = \
admin.vm.device.pci.Attached \
admin.vm.device.pci.Available \
admin.vm.device.pci.Detach \
admin.vm.device.pci.Set.required \
admin.vm.device.pci.Set.assignment \
admin.vm.device.pci.Unassign \
admin.vm.device.block.Assign \
admin.vm.device.block.Assigned \
admin.vm.device.block.Attach \
admin.vm.device.block.Attached \
admin.vm.device.block.Available \
admin.vm.device.block.Detach \
admin.vm.device.block.Set.required \
admin.vm.device.block.Set.assignment \
admin.vm.device.block.Unassign \
admin.vm.device.usb.Assign \
admin.vm.device.usb.Assigned \
admin.vm.device.usb.Attach \
admin.vm.device.usb.Attached \
admin.vm.device.usb.Available \
admin.vm.device.usb.Detach \
admin.vm.device.usb.Set.required \
admin.vm.device.usb.Set.assignment \
admin.vm.device.usb.Unassign \
admin.vm.device.mic.Assign \
admin.vm.device.mic.Assigned \
admin.vm.device.mic.Attach \
admin.vm.device.mic.Attached \
admin.vm.device.mic.Available \
admin.vm.device.mic.Detach \
admin.vm.device.mic.Set.required \
admin.vm.device.mic.Set.assignment \
admin.vm.device.mic.Unassign \
admin.vm.feature.CheckWithNetvm \
admin.vm.feature.CheckWithTemplate \
Expand Down Expand Up @@ -227,7 +227,7 @@ endif
admin.vm.device.testclass.Unassign \
admin.vm.device.testclass.Attached \
admin.vm.device.testclass.Assigned \
admin.vm.device.testclass.Set.required \
admin.vm.device.testclass.Set.assignment \
admin.vm.device.testclass.Available
install -d $(DESTDIR)/etc/qubes/policy.d/include
install -m 0644 qubes-rpc-policy/admin-local-ro \
Expand Down
4 changes: 2 additions & 2 deletions qubes-rpc-policy/90-admin-default.policy.header
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@
!include-service admin.vm.device.mic.Attached * include/admin-local-ro
!include-service admin.vm.device.mic.Available * include/admin-local-ro
!include-service admin.vm.device.mic.Detach * include/admin-local-rwx
!include-service admin.vm.device.mic.Set.required * include/admin-local-rwx
!include-service admin.vm.device.mic.Set.assignment * include/admin-local-rwx
!include-service admin.vm.device.mic.Unassign * include/admin-local-rwx
!include-service admin.vm.device.usb.Assign * include/admin-local-rwx
!include-service admin.vm.device.usb.Assigned * include/admin-local-ro
!include-service admin.vm.device.usb.Attach * include/admin-local-rwx
!include-service admin.vm.device.usb.Attached * include/admin-local-ro
!include-service admin.vm.device.usb.Available * include/admin-local-ro
!include-service admin.vm.device.usb.Detach * include/admin-local-rwx
!include-service admin.vm.device.usb.Set.required * include/admin-local-rwx
!include-service admin.vm.device.usb.Set.assignment * include/admin-local-rwx
!include-service admin.vm.device.usb.Unassign * include/admin-local-rwx

21 changes: 12 additions & 9 deletions qubes/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
import qubes.vm.adminvm
import qubes.vm.qubesvm
from qubes.device_protocol import (
VirtualDevice, UnknownDevice, DeviceAssignment)
VirtualDevice, UnknownDevice, DeviceAssignment, AssignmentMode)


class QubesMgmtEventsDispatcher:
Expand Down Expand Up @@ -1391,7 +1391,7 @@ async def vm_device_detach(self, endpoint):

# Assign/Unassign action can modify only a persistent state of running VM.
# For this reason, write=True
@qubes.api.method('admin.vm.device.{endpoint}.Set.required',
@qubes.api.method('admin.vm.device.{endpoint}.Set.assignment',
endpoints=(ep.name
for ep in importlib.metadata.entry_points(group='qubes.devices')),
scope='local', write=True)
Expand All @@ -1405,17 +1405,20 @@ async def vm_device_set_required(self, endpoint, untrusted_payload):
"""
devclass = endpoint

self.enforce(untrusted_payload in (b'True', b'False'))
# now is safe to eval, since the value of untrusted_payload is trusted
# pylint: disable=eval-used
assignment = eval(untrusted_payload)
del untrusted_payload
allowed_values = {
b'required': AssignmentMode.REQUIRED,
b'ask-to-attach': AssignmentMode.ASK,
b'auto-attach': AssignmentMode.AUTO}
try:
mode = allowed_values[untrusted_payload]
except KeyError:
raise qubes.exc.PermissionDenied()

dev = VirtualDevice.from_qarg(self.arg, devclass, self.app.domains)

self.fire_event_for_permission(device=dev, mode=assignment)
self.fire_event_for_permission(device=dev, mode=mode)

await self.dest.devices[devclass].update_required(dev, assignment)
await self.dest.devices[devclass].update_assignment(dev, mode)
self.app.save()

@qubes.api.method('admin.vm.firewall.Get', no_payload=True,
Expand Down
16 changes: 8 additions & 8 deletions qubes/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
import qubes.exc
import qubes.utils
from qubes.device_protocol import (Port, DeviceInfo, UnknownDevice,
DeviceAssignment, VirtualDevice)
DeviceAssignment, VirtualDevice,
AssignmentMode)


DEVICE_DENY_LIST = "/etc/qubes/device-deny.list"
Expand Down Expand Up @@ -257,14 +258,14 @@ def load_assignment(self, device_assignment: DeviceAssignment):
assert device_assignment.attach_automatically
self._set.add(device_assignment)

async def update_required(self, device: VirtualDevice, required: bool):
async def update_assignment(
self, device: VirtualDevice, mode: AssignmentMode
):
"""
Update `required` flag of an already attached device.
:param VirtualDevice device: device for which change required flag
:param bool required: new assignment:
`False` -> device will be auto-attached to qube
`True` -> device is required to start qube
:param AssignmentMode mode: new assignment mode
"""
if self._vm.is_halted():
raise qubes.exc.QubesVMNotStartedError(
Expand All @@ -281,11 +282,10 @@ async def update_required(self, device: VirtualDevice, required: bool):

# be careful to use already present assignment, not the provided one
# - to not change options as a side effect
if assignment.required == required:
if assignment.mode == mode:
return

new_assignment = assignment.clone(
mode='required' if required else 'auto-attach')
new_assignment = assignment.clone(mode=mode)
self._set.discard(assignment)
self._set.add(new_assignment)
await self._vm.fire_event_async(
Expand Down
72 changes: 45 additions & 27 deletions qubes/tests/api_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2944,7 +2944,7 @@ def test_642_vm_create_disposable_not_allowed(self, storage_mock):
b'test-vm1')
self.assertFalse(self.app.save.called)

def test_650_vm_device_set_required_true(self):
def test_650_vm_device_set_mode_required(self):
assignment = DeviceAssignment(VirtualDevice(Port(
self.vm, '1234', 'testclass'), device_id='bee'),
mode='auto-attach', options={'opt1': 'value'})
Expand All @@ -2958,24 +2958,25 @@ def test_650_vm_device_set_required_true(self):
with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
'is_halted', lambda _: False):
value = self.call_mgmt_func(
b'admin.vm.device.testclass.Set.required',
b'test-vm1', b'test-vm1+1234:bee', b'True')
b'admin.vm.device.testclass.Set.assignment',
b'test-vm1', b'test-vm1+1234:bee', b'required')

self.assertIsNone(value)
dev = DeviceInfo(Port(
self.vm, '1234', 'testclass'), device_id='bee')
required = self.vm.devices['testclass'].get_assigned_devices(
required_only=True)
required = list(self.vm.devices['testclass'].get_assigned_devices(
required_only=True))
self.assertIn(dev, required)
self.assertEqual(required[0].mode.value, "required")
self.assertEventFired(
self.emitter,
'admin-permission:admin.vm.device.testclass.Set.required')
'admin-permission:admin.vm.device.testclass.Set.assignment')
mock_action.assert_called_once_with(
self.vm, f'device-assignment-changed:testclass',
device=assignment.virtual_device)
self.app.save.assert_called_once_with()

def test_651_vm_device_set_required_false(self):
def test_651_vm_device_set_mode_ask(self):
assignment = DeviceAssignment(VirtualDevice(Port(
self.vm, '1234', 'testclass'), device_id='bee'),
mode='required', options={'opt1': 'value'})
Expand All @@ -2989,83 +2990,100 @@ def test_651_vm_device_set_required_false(self):
with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
'is_halted', lambda _: False):
value = self.call_mgmt_func(
b'admin.vm.device.testclass.Set.required',
b'test-vm1', b'test-vm1+1234:bee', b'False')
b'admin.vm.device.testclass.Set.assignment',
b'test-vm1', b'test-vm1+1234:bee', b'ask-to-attach')

self.assertIsNone(value)
dev = DeviceInfo(Port(self.vm, '1234', 'testclass'),
device_id='bee')
required = self.vm.devices['testclass'].get_assigned_devices(
required_only=True)
self.assertNotIn(dev, required)
assignments = list(self.vm.devices['testclass'].get_assigned_devices())
self.assertEqual(assignments[0].mode.value, "ask-to-attach")
self.assertEventFired(
self.emitter,
'admin-permission:admin.vm.device.testclass.Set.required')
'admin-permission:admin.vm.device.testclass.Set.assignment')
mock_action.assert_called_once_with(
self.vm, f'device-assignment-changed:testclass',
device=assignment.virtual_device)
self.app.save.assert_called_once_with()

def test_652_vm_device_set_required_true_unchanged(self):
def test_652_vm_device_set_mode_auto(self):
assignment = DeviceAssignment(VirtualDevice(Port(
self.vm, '1234', 'testclass'), device_id='bee'),
mode='required', options={'opt1': 'value'})
self.loop.run_until_complete(
self.vm.devices['testclass'].assign(assignment))
mock_action = unittest.mock.Mock()
mock_action.return_value = None
del mock_action._is_coroutine
self.vm.add_handler(f'device-assignment-changed:testclass', mock_action)

with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
'is_halted', lambda _: False):
value = self.call_mgmt_func(
b'admin.vm.device.testclass.Set.required',
b'test-vm1', b'test-vm1+1234:bee', b'True')
b'admin.vm.device.testclass.Set.assignment',
b'test-vm1', b'test-vm1+1234:bee', b'auto-attach')

self.assertIsNone(value)
dev = DeviceInfo(Port(self.vm, '1234', 'testclass'),
device_id='bee')
required = self.vm.devices['testclass'].get_assigned_devices(
required_only=True)
self.assertIn(dev, required)
self.assertNotIn(dev, required)
assignments = list(self.vm.devices['testclass'].get_assigned_devices())
self.assertEqual(assignments[0].mode.value, "auto-attach")
self.assertEventFired(
self.emitter,
'admin-permission:admin.vm.device.testclass.Set.assignment')
mock_action.assert_called_once_with(
self.vm, f'device-assignment-changed:testclass',
device=assignment.virtual_device)
self.app.save.assert_called_once_with()

def test_653_vm_device_set_required_false_unchanged(self):
def test_653_vm_device_set_mode_unchanged(self):
assignment = DeviceAssignment(VirtualDevice(Port(
self.vm, '1234', 'testclass')),
mode='auto-attach', options={'opt1': 'value'})
self.vm, '1234', 'testclass'), device_id='bee'),
mode='required', options={'opt1': 'value'})
self.loop.run_until_complete(
self.vm.devices['testclass'].assign(assignment))
with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
'is_halted', lambda _: False):
value = self.call_mgmt_func(
b'admin.vm.device.testclass.Set.required',
b'test-vm1', b'test-vm1+1234', b'False')
b'admin.vm.device.testclass.Set.assignment',
b'test-vm1', b'test-vm1+1234:bee', b'required')
self.assertIsNone(value)
dev = qubes.device_protocol.DeviceInfo(Port(self.vm, '1234', 'testclass'))
dev = DeviceInfo(Port(self.vm, '1234', 'testclass'),
device_id='bee')
required = self.vm.devices['testclass'].get_assigned_devices(
required_only=True)
self.assertNotIn(dev, required)
self.assertIn(dev, required)
self.app.save.assert_called_once_with()

def test_654_vm_device_set_persistent_not_assigned(self):
def test_654_vm_device_set_mode_not_assigned(self):
self.vm.add_handler('device-list:testclass',
self.device_list_testclass)
with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
'is_halted', lambda _: False):
with self.assertRaises(qubes.exc.QubesValueError):
self.call_mgmt_func(
b'admin.vm.device.testclass.Set.required',
b'test-vm1', b'test-vm1+1234', b'True')
b'admin.vm.device.testclass.Set.assignment',
b'test-vm1', b'test-vm1+1234', b'required')
dev = qubes.device_protocol.DeviceInfo(Port(self.vm, '1234', 'testclass'))
self.assertNotIn(
dev, self.vm.devices['testclass'].get_assigned_devices())
self.assertFalse(self.app.save.called)

def test_655_vm_device_set_persistent_invalid_value(self):
def test_655_vm_device_set_mode_invalid_value(self):
self.vm.add_handler('device-list:testclass',
self.device_list_testclass)
with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM,
'is_halted', lambda _: False):
with self.assertRaises(qubes.exc.PermissionDenied):
self.call_mgmt_func(
b'admin.vm.device.testclass.Set.required',
b'test-vm1', b'test-vm1+1234', b'maybe')
b'admin.vm.device.testclass.Set.assignment',
b'test-vm1', b'test-vm1+1234', b'True')
dev = qubes.device_protocol.DeviceInfo(Port(self.vm, '1234', 'testclass'))
self.assertNotIn(dev, self.vm.devices['testclass'].get_assigned_devices())
self.assertFalse(self.app.save.called)
Expand Down
Loading

0 comments on commit 65f14db

Please sign in to comment.