diff --git a/Makefile b/Makefile index 3b27dc74..241ef477 100644 --- a/Makefile +++ b/Makefile @@ -55,14 +55,17 @@ ADMIN_API_METHODS_SIMPLE = \ admin.vm.device.pci.Available \ admin.vm.device.pci.Detach \ admin.vm.device.pci.List \ + admin.vm.device.pci.Set.persistent \ admin.vm.device.block.Attach \ admin.vm.device.block.Available \ admin.vm.device.block.Detach \ admin.vm.device.block.List \ + admin.vm.device.block.Set.persistent \ admin.vm.device.mic.Attach \ admin.vm.device.mic.Available \ admin.vm.device.mic.Detach \ admin.vm.device.mic.List \ + admin.vm.device.mic.Set.persistent \ admin.vm.feature.CheckWithTemplate \ admin.vm.feature.Get \ admin.vm.feature.List \ @@ -188,6 +191,7 @@ endif admin.vm.device.testclass.Attach \ admin.vm.device.testclass.Detach \ admin.vm.device.testclass.List \ + admin.vm.device.testclass.Set.persistent \ admin.vm.device.testclass.Available # sanity check for method in $(DESTDIR)/etc/qubes-rpc/policy/admin.*; do \ diff --git a/qubes/api/admin.py b/qubes/api/admin.py index c8d694a6..b355048b 100644 --- a/qubes/api/admin.py +++ b/qubes/api/admin.py @@ -1093,6 +1093,36 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI): yield from self.dest.devices[devclass].detach(assignment) self.app.save() + # Attach/Detach action can both modify persistent state (with + # persistent=True) and volatile state of running VM (with persistent=False). + # For this reason, write=True + execute=True + @qubes.api.method('admin.vm.device.{endpoint}.Set.persistent', + endpoints=(ep.name + for ep in pkg_resources.iter_entry_points('qubes.devices')), + scope='local', write=True, execute=True) + @asyncio.coroutine + def vm_device_set_persistent(self, endpoint, untrusted_payload): + devclass = endpoint + + assert untrusted_payload in (b'True', b'False') + persistent = untrusted_payload == b'True' + del untrusted_payload + + # qrexec already verified that no strange characters are in self.arg + backend_domain, ident = self.arg.split('+', 1) + # device must be already attached + matching_devices = [dev for dev + in self.dest.devices[devclass].attached() + if dev.backend_domain.name == backend_domain and dev.ident == ident] + assert len(matching_devices) == 1 + dev = matching_devices[0] + + self.fire_event_for_permission(device=dev, + persistent=persistent) + + self.dest.devices[devclass].update_persistent(dev, persistent) + self.app.save() + @qubes.api.method('admin.vm.firewall.Get', no_payload=True, scope='local', read=True) @asyncio.coroutine diff --git a/qubes/tests/api_admin.py b/qubes/tests/api_admin.py index 7ce27f4a..fbf7b7a0 100644 --- a/qubes/tests/api_admin.py +++ b/qubes/tests/api_admin.py @@ -2131,6 +2131,102 @@ class TC_00_VMs(AdminAPITestCase): b'test-vm1') self.assertFalse(self.app.save.called) + def test_650_vm_device_set_persistent_true(self): + self.vm.add_handler('device-list:testclass', + self.device_list_testclass) + self.vm.add_handler('device-list-attached:testclass', + self.device_list_attached_testclass) + with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM, + 'is_halted', lambda _: False): + value = self.call_mgmt_func( + b'admin.vm.device.testclass.Set.persistent', + b'test-vm1', b'test-vm1+1234', b'True') + self.assertIsNone(value) + dev = qubes.devices.DeviceInfo(self.vm, '1234') + self.assertIn(dev, self.vm.devices['testclass'].persistent()) + self.app.save.assert_called_once_with() + + def test_651_vm_device_set_persistent_false_unchanged(self): + self.vm.add_handler('device-list:testclass', + self.device_list_testclass) + self.vm.add_handler('device-list-attached:testclass', + self.device_list_attached_testclass) + with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM, + 'is_halted', lambda _: False): + value = self.call_mgmt_func( + b'admin.vm.device.testclass.Set.persistent', + b'test-vm1', b'test-vm1+1234', b'False') + self.assertIsNone(value) + dev = qubes.devices.DeviceInfo(self.vm, '1234') + self.assertNotIn(dev, self.vm.devices['testclass'].persistent()) + self.app.save.assert_called_once_with() + + def test_652_vm_device_set_persistent_false(self): + self.vm.add_handler('device-list:testclass', + self.device_list_testclass) + assignment = qubes.devices.DeviceAssignment(self.vm, '1234', {}, + True) + self.loop.run_until_complete( + self.vm.devices['testclass'].attach(assignment)) + self.vm.add_handler('device-list-attached:testclass', + self.device_list_attached_testclass) + dev = qubes.devices.DeviceInfo(self.vm, '1234') + self.assertIn(dev, self.vm.devices['testclass'].persistent()) + with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM, + 'is_halted', lambda _: False): + value = self.call_mgmt_func( + b'admin.vm.device.testclass.Set.persistent', + b'test-vm1', b'test-vm1+1234', b'False') + self.assertIsNone(value) + self.assertNotIn(dev, self.vm.devices['testclass'].persistent()) + self.assertIn(dev, self.vm.devices['testclass'].attached()) + self.app.save.assert_called_once_with() + + def test_653_vm_device_set_persistent_true_unchanged(self): + self.vm.add_handler('device-list:testclass', + self.device_list_testclass) + assignment = qubes.devices.DeviceAssignment(self.vm, '1234', {}, + True) + self.loop.run_until_complete( + self.vm.devices['testclass'].attach(assignment)) + self.vm.add_handler('device-list-attached:testclass', + self.device_list_attached_testclass) + with unittest.mock.patch.object(qubes.vm.qubesvm.QubesVM, + 'is_halted', lambda _: False): + value = self.call_mgmt_func( + b'admin.vm.device.testclass.Set.persistent', + b'test-vm1', b'test-vm1+1234', b'True') + self.assertIsNone(value) + dev = qubes.devices.DeviceInfo(self.vm, '1234') + self.assertIn(dev, self.vm.devices['testclass'].persistent()) + self.assertIn(dev, self.vm.devices['testclass'].attached()) + self.app.save.assert_called_once_with() + + def test_654_vm_device_set_persistent_not_attached(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(AssertionError): + self.call_mgmt_func( + b'admin.vm.device.testclass.Set.persistent', + b'test-vm1', b'test-vm1+1234', b'True') + dev = qubes.devices.DeviceInfo(self.vm, '1234') + self.assertNotIn(dev, self.vm.devices['testclass'].persistent()) + self.assertFalse(self.app.save.called) + + def test_655_vm_device_set_persistent_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(AssertionError): + self.call_mgmt_func( + b'admin.vm.device.testclass.Set.persistent', + b'test-vm1', b'test-vm1+1234', b'maybe') + dev = qubes.devices.DeviceInfo(self.vm, '1234') + self.assertNotIn(dev, self.vm.devices['testclass'].persistent()) + self.assertFalse(self.app.save.called) def test_990_vm_unexpected_payload(self): methods_with_no_payload = [