diff --git a/qui/devices/actionable_widgets.py b/qui/devices/actionable_widgets.py index 0bcc1ac1..5c7572f9 100644 --- a/qui/devices/actionable_widgets.py +++ b/qui/devices/actionable_widgets.py @@ -240,6 +240,11 @@ def __init__(self, vm: backend.VM, device: backend.Device): super().__init__(vm) self.vm = vm self.device = device + if not self.device.is_valid_for_vm(self.vm): + self.backend_label.set_markup( + self.backend_label.get_text() + " (blocked by policy)" + ) + self.actionable = False async def widget_action(self, *_args): self.device.attach_to_vm(self.vm) diff --git a/qui/devices/backend.py b/qui/devices/backend.py index 9c78c784..051e31d5 100644 --- a/qui/devices/backend.py +++ b/qui/devices/backend.py @@ -24,7 +24,7 @@ import qubesadmin.devices import qubesadmin.vm from qubesadmin.utils import size_to_human -from qubesadmin.device_protocol import DeviceAssignment, DeviceCategory +from qubesadmin.device_protocol import DeviceAssignment, DeviceCategory, DeviceInterface import gi @@ -50,9 +50,12 @@ def __init__(self, vm: qubesadmin.vm.QubesVM): self._vm = vm self.name = vm.name self.vm_class = vm.klass + self._devices_denied = [] self.is_running = True # in most cases, this is just True, unless we're at # sysusb + self.update_denied_devices() + def __str__(self): return self.name @@ -130,6 +133,18 @@ def toggle_feature_value(self, feature_name, value): new_feature = " ".join(all_devs) self._vm.features[feature_name] = new_feature + @property + def devices_denied(self) -> List[DeviceInterface]: + return self._devices_denied + + def update_denied_devices(self): + try: + self._devices_denied = DeviceInterface.from_str_bulk( + self._vm.devices_denied + ) + except AttributeError: + self._devices_denied = [] + class Device: @classmethod @@ -141,6 +156,7 @@ def __init__(self, dev: qubesadmin.devices.DeviceInfo, gtk_app: Gtk.Application) self._dev: qubesadmin.devices.DeviceInfo = dev self.__hash = hash(dev) self._port: str = str(dev.port) + self._interfaces = dev.interfaces # Monotonic connection timestamp only for new devices self.connection_timestamp: float = None @@ -353,3 +369,10 @@ def detach_from_all(self, with_aux_devices: bool = True): """ for vm in self.attachments: self.detach_from_vm(vm, with_aux_devices) + + def is_valid_for_vm(self, vm: VM): + for deny_interface in vm.devices_denied: + for interface in self._interfaces: + if deny_interface.matches(interface): + return False + return True diff --git a/qui/devices/device_widget.py b/qui/devices/device_widget.py index 2d375944..6133818b 100644 --- a/qui/devices/device_widget.py +++ b/qui/devices/device_widget.py @@ -157,7 +157,6 @@ def __init__(self, app_name, qapp, dispatcher): self.dispatcher.add_handler( "property-set:template_for_dispvms", self.vm_dispvm_template_change ) - self.dispatcher.add_handler( "property-reset:template_for_dispvms", self.vm_dispvm_template_change, @@ -165,6 +164,15 @@ def __init__(self, app_name, qapp, dispatcher): self.dispatcher.add_handler( "property-del:template_for_dispvms", self.vm_dispvm_template_change ) + self.dispatcher.add_handler( + "property-set:devices_denied", self.vm_denied_changed + ) + self.dispatcher.add_handler( + "property-reset:devices_denied", self.vm_denied_changed + ) + self.dispatcher.add_handler( + "property-del:devices_denied", self.vm_denied_changed + ) self.dispatcher.add_handler( "domain-feature-set:internal", self.update_internal_feature @@ -604,6 +612,13 @@ def vm_dispvm_template_change(self, vm, _event, **_kwargs): else: self.dispvm_templates.discard(wrapped_vm) + def vm_denied_changed(self, vm, _event, **_kwargs): + """devices_denied property changed""" + for wrapped_vm in self.vms: + if wrapped_vm.name == vm.name: + wrapped_vm.update_denied_devices() + return + @staticmethod def load_css(widget) -> str: """Load appropriate css. This should be called whenever menu is shown,