From 3632d2f36c144a079b34cd29e71e0a3cd27eff59 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Thu, 12 Feb 2026 16:59:07 +0100 Subject: [PATCH 1/4] Hide improper qubes from selection Improve filters of several comboboxes to show only valid options: - Templates nor disposable templates shouldn't be used: - as service qubes - have devices assigned to it - GPG key qube - Minimal template may lack some packages and services, improve filter: - "updatevm" requires "supported-rpc.qube.TemplateDownload" - "split-gpg" requires "supported-rpc.qube.Gpg" - every model using VMSubsetPolicyHandler will check "service_name" --- qubes_config/global_config/basics_handler.py | 6 +++- .../global_config/device_attachments.py | 6 ++-- qubes_config/global_config/global_config.py | 5 ++++ qubes_config/global_config/policy_handler.py | 20 +++++++++++-- qubes_config/global_config/updates_handler.py | 4 +++ qubes_config/tests/test_policy_handler.py | 30 +++++++++++++++++++ qubes_config/widgets/utils.py | 14 ++++++--- 7 files changed, 76 insertions(+), 9 deletions(-) diff --git a/qubes_config/global_config/basics_handler.py b/qubes_config/global_config/basics_handler.py index a8f906ea..d072979d 100644 --- a/qubes_config/global_config/basics_handler.py +++ b/qubes_config/global_config/basics_handler.py @@ -742,7 +742,11 @@ def __init__(self, gtk_builder: Gtk.Builder, qapp: qubesadmin.Qubes): @staticmethod def _clock_vm_filter(vm) -> bool: - return vm.klass != "TemplateVM" + return ( + vm.klass != "TemplateVM" + and not getattr(vm, "template_for_dispvms", False) + and (vm.klass == "AdminVM" or vm.is_networked()) + ) @staticmethod def _default_template_filter(vm) -> bool: diff --git a/qubes_config/global_config/device_attachments.py b/qubes_config/global_config/device_attachments.py index 0299821f..c688aaf9 100644 --- a/qubes_config/global_config/device_attachments.py +++ b/qubes_config/global_config/device_attachments.py @@ -122,7 +122,8 @@ def __init__( self.qapp, "edit_device", [], - filter_function=lambda vm: vm.klass != "AdminVM", + filter_function=lambda vm: vm.klass not in ["AdminVM", "TemplateVM"] + and not getattr(vm, "template_for_dispvms", False), ) self.backend_vm: Optional[qubesadmin.vm.QubesVM] = None @@ -398,7 +399,8 @@ def __init__( self.qapp, "required_device", [], - filter_function=lambda vm: vm.klass != "AdminVM", + filter_function=lambda vm: vm.klass not in ["AdminVM", "TemplateVM"] + and not getattr(vm, "template_for_dispvms", False), ) self.dev_modeler = self.fill_combo_with_devices( diff --git a/qubes_config/global_config/global_config.py b/qubes_config/global_config/global_config.py index 8bb9292e..ad1a8d10 100644 --- a/qubes_config/global_config/global_config.py +++ b/qubes_config/global_config/global_config.py @@ -434,6 +434,11 @@ def perform_setup(self): service_name="qubes.Gpg", policy_file_name="50-config-splitgpg", default_policy="", + filter_function=lambda vm: vm.klass not in ["AdminVM", "TemplateVM"] + and not getattr(vm, "template_for_dispvms", False) + and not vm.features.get("service.guivm") + and not vm.features.get("service.audiovm") + and not getattr(vm, "provides_network", False), main_rule_class=RuleSimpleNoAllow, main_verb_description=SimpleVerbDescription( { diff --git a/qubes_config/global_config/policy_handler.py b/qubes_config/global_config/policy_handler.py index 57ecce76..35de0d15 100644 --- a/qubes_config/global_config/policy_handler.py +++ b/qubes_config/global_config/policy_handler.py @@ -44,7 +44,7 @@ import qubesadmin import qubesadmin.vm -from ..widgets.utils import compare_rule_lists +from ..widgets.utils import compare_rule_lists, get_boolean_feature gi.require_version("Gtk", "3.0") from gi.repository import Gtk @@ -562,6 +562,7 @@ def __init__( main_rule_class: Type[AbstractRuleWrapper], exception_verb_description: AbstractVerbDescription, exception_rule_class: Type[AbstractRuleWrapper], + filter_function: Callable[[qubesadmin.vm.QubesVM], bool] | None = None, ): """ :param qapp: Qubes object @@ -619,7 +620,22 @@ def __init__( # populate combo self.select_qube_model = VMListModeler( - combobox=self.select_qube_combo, qapp=self.qapp + combobox=self.select_qube_combo, + qapp=self.qapp, + filter_function=( + ( + lambda vm: filter_function(vm) + and get_boolean_feature( + vm, "supported-rpc." + service_name, recurse_template=True + ) + ) + if filter_function + else ( + lambda vm: get_boolean_feature( + vm, "supported-rpc." + service_name, recurse_template=True + ) + ) + ), ) # connect events diff --git a/qubes_config/global_config/updates_handler.py b/qubes_config/global_config/updates_handler.py index a7dfd00d..47e77b01 100644 --- a/qubes_config/global_config/updates_handler.py +++ b/qubes_config/global_config/updates_handler.py @@ -686,6 +686,10 @@ def __init__( lambda vm: vm.klass != "TemplateVM" and vm.klass != "AdminVM" and vm.is_networked() + and not getattr(vm, "template_for_dispvms", False) + and get_boolean_feature( + vm, "supported-rpc.qubes.TemplateDownload", recurse_template=True + ) ), current_value=self.qapp.updatevm, additional_options=NONE_CATEGORY, diff --git a/qubes_config/tests/test_policy_handler.py b/qubes_config/tests/test_policy_handler.py index 2414d677..2c28a9be 100644 --- a/qubes_config/tests/test_policy_handler.py +++ b/qubes_config/tests/test_policy_handler.py @@ -886,6 +886,11 @@ def test_subset_handler(test_builder, test_qapp, test_policy_manager: PolicyMana default_policy = """ TestService * @anyvm test-blue allow""" + for qube in test_qapp.domains: + # pylint: disable=protected-access + test_qapp._qubes[qube].features["supported-rpc.TestService"] = "1" + test_qapp.update_vm_calls() + handler = VMSubsetPolicyHandler( qapp=test_qapp, gtk_builder=test_builder, @@ -973,6 +978,11 @@ def test_subset_handler_limited_choice( TestService * @anyvm test-blue allow TestService * @anyvm test-red allow""" + for qube in test_qapp.domains: + # pylint: disable=protected-access + test_qapp._qubes[qube].features["supported-rpc.TestService"] = "1" + test_qapp.update_vm_calls() + handler = VMSubsetPolicyHandler( qapp=test_qapp, gtk_builder=test_builder, @@ -1066,6 +1076,11 @@ def test_subset_handler_remove_choice( TestService * @anyvm test-blue allow TestService * @anyvm vault ask""" + for qube in test_qapp.domains: + # pylint: disable=protected-access + test_qapp._qubes[qube].features["supported-rpc.TestService"] = "1" + test_qapp.update_vm_calls() + handler = VMSubsetPolicyHandler( qapp=test_qapp, gtk_builder=test_builder, @@ -1122,6 +1137,11 @@ def test_subset_handler_duplicates( default_policy = """ TestService * @anyvm vault ask""" + for qube in test_qapp.domains: + # pylint: disable=protected-access + test_qapp._qubes[qube].features["supported-rpc.TestService"] = "1" + test_qapp.update_vm_calls() + handler = VMSubsetPolicyHandler( qapp=test_qapp, gtk_builder=test_builder, @@ -1161,6 +1181,11 @@ def test_subset_handler_duplicates_load( TestService * @anyvm vault ask """ + for qube in test_qapp.domains: + # pylint: disable=protected-access + test_qapp._qubes[qube].features["supported-rpc.TestService"] = "1" + test_qapp.update_vm_calls() + handler = VMSubsetPolicyHandler( qapp=test_qapp, gtk_builder=test_builder, @@ -1191,6 +1216,11 @@ def test_subset_handler_unsupported( TestService * @anyvm vault allow target=test-blue """ + for qube in test_qapp.domains: + # pylint: disable=protected-access + test_qapp._qubes[qube].features["supported-rpc.TestService"] = "1" + test_qapp.update_vm_calls() + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = VMSubsetPolicyHandler( diff --git a/qubes_config/widgets/utils.py b/qubes_config/widgets/utils.py index f413390e..e1976bf8 100644 --- a/qubes_config/widgets/utils.py +++ b/qubes_config/widgets/utils.py @@ -33,19 +33,25 @@ _ = t.gettext -def get_feature(vm, feature_name, default_value=None): +def get_feature(vm, feature_name, default_value=None, recurse_template=False): """Get feature, with a working default_value.""" + if recurse_template: + feat = vm.features.check_with_template + else: + feat = vm.features.get try: - return vm.features.get(feature_name, default_value) + return feat(feature_name, default_value) except qubesadmin.exc.QubesDaemonAccessError: return default_value -def get_boolean_feature(vm, feature_name, default=False): +def get_boolean_feature(vm, feature_name, default=False, recurse_template=False): """helper function to get a feature converted to a Bool if it does exist. Necessary because of the true/false in features being coded as 1/empty string.""" - result = get_feature(vm, feature_name, None) + result = get_feature( + vm, feature_name, default_value=None, recurse_template=recurse_template + ) if result is not None: result = bool(result) else: From 404e02c8351e63d96ffe1c2f906996145b82abe3 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Thu, 26 Mar 2026 04:32:41 +0100 Subject: [PATCH 2/4] UNFINISHED - Experiment with warning inadvisable selection Missing labels in glade. --- qubes_config/global_config.glade | 94 ++++++++++++------- qubes_config/global_config/basics_handler.py | 20 +++- .../global_config/device_attachments.py | 17 +++- qubes_config/global_config/global_config.py | 6 +- qubes_config/global_config/policy_handler.py | 5 + qubes_config/global_config/updates_handler.py | 12 ++- qubes_config/global_config/vm_flowbox.py | 5 + qubes_config/qubes-global-config-base.css | 12 ++- qubes_config/widgets/gtk_widgets.py | 33 +++++++ qubes_config/widgets/utils.py | 18 ++++ 10 files changed, 175 insertions(+), 47 deletions(-) diff --git a/qubes_config/global_config.glade b/qubes_config/global_config.glade index 10c5d18f..19bba8e2 100644 --- a/qubes_config/global_config.glade +++ b/qubes_config/global_config.glade @@ -976,7 +976,7 @@ False natural - + True False @@ -1069,7 +1069,7 @@ 0 - 11 + 12 2 @@ -1085,7 +1085,7 @@ 0 - 14 + 15 @@ -1101,7 +1101,7 @@ 1 - 14 + 15 @@ -1114,7 +1114,7 @@ 1 - 15 + 16 @@ -1127,7 +1127,7 @@ 1 - 16 + 17 @@ -1175,7 +1175,7 @@ 1 - 6 + 7 @@ -1199,7 +1199,7 @@ 1 - 7 + 8 @@ -1223,7 +1223,7 @@ 1 - 5 + 6 @@ -1238,7 +1238,7 @@ 0 - 18 + 19 2 @@ -1255,7 +1255,7 @@ 0 - 19 + 20 2 @@ -1271,7 +1271,7 @@ 0 - 23 + 24 2 @@ -1283,7 +1283,7 @@ 1 - 25 + 26 @@ -1295,7 +1295,7 @@ 0 - 26 + 27 2 @@ -1312,7 +1312,7 @@ 0 - 24 + 25 2 @@ -1357,7 +1357,7 @@ 0 - 5 + 6 @@ -1401,7 +1401,7 @@ 0 - 6 + 7 @@ -1446,7 +1446,7 @@ 0 - 7 + 8 @@ -1462,7 +1462,7 @@ 0 - 12 + 13 2 @@ -1509,7 +1509,7 @@ 0 - 13 + 14 2 @@ -1554,7 +1554,7 @@ 0 - 15 + 16 @@ -1578,7 +1578,7 @@ 0 - 10 + 11 @@ -1590,7 +1590,7 @@ 0 - 17 + 18 @@ -1602,7 +1602,7 @@ 0 - 22 + 23 @@ -1633,7 +1633,7 @@ 0 - 16 + 17 @@ -1648,7 +1648,7 @@ 0 - 25 + 26 @@ -1663,7 +1663,7 @@ 0 - 20 + 21 @@ -1678,7 +1678,7 @@ 0 - 21 + 22 @@ -1718,7 +1718,7 @@ 1 - 20 + 21 @@ -1758,7 +1758,7 @@ 1 - 21 + 22 @@ -1815,7 +1815,7 @@ 0 - 8 + 9 @@ -1882,7 +1882,7 @@ 1 - 8 + 9 @@ -1925,7 +1925,7 @@ 1 - 9 + 10 @@ -1953,7 +1953,33 @@ 0 - 9 + 10 + + + + + False + vertical + + + True + False + True + + + False + True + 0 + + + + + + 0 + 5 + 2 diff --git a/qubes_config/global_config/basics_handler.py b/qubes_config/global_config/basics_handler.py index d072979d..4c8b828f 100644 --- a/qubes_config/global_config/basics_handler.py +++ b/qubes_config/global_config/basics_handler.py @@ -43,6 +43,7 @@ get_feature, get_boolean_feature, apply_feature_change, + inadvisable_selection, ) import gi @@ -130,9 +131,11 @@ def __init__( self, qapp: qubesadmin.Qubes, trait_holder: Any, - trait_name: str, widget: Gtk.ComboBox, + trait_name: str, + gtk_builder: Gtk.Builder = None, vm_filter: Optional[Callable] = None, + vm_inadvisable: Optional[Callable] = None, readable_name: Optional[str] = None, additional_options: Dict[Any | str | None, str] | None = None, show_internal: bool = False, @@ -146,7 +149,10 @@ def __init__( self.model = VMListModeler( combobox=self.widget, qapp=qapp, + gtk_builder=gtk_builder, + trait_name=trait_name, filter_function=vm_filter, + vm_inadvisable=vm_inadvisable, current_value=self.get_current_value(), style_changes=True, additional_options=additional_options, @@ -644,7 +650,9 @@ def __init__(self, gtk_builder: Gtk.Builder, qapp: qubesadmin.Qubes): trait_holder=self.qapp, trait_name="clockvm", widget=self.clockvm_combo, + gtk_builder=gtk_builder, vm_filter=self._clock_vm_filter, + vm_inadvisable=self._clock_vm_inadvisable, readable_name=_("Clock qube"), additional_options=NONE_CATEGORY, ) @@ -742,10 +750,14 @@ def __init__(self, gtk_builder: Gtk.Builder, qapp: qubesadmin.Qubes): @staticmethod def _clock_vm_filter(vm) -> bool: + return vm.klass != "TemplateVM" and (vm.klass == "AdminVM" or vm.is_networked()) + + @staticmethod + def _clock_vm_inadvisable(vm) -> str: return ( - vm.klass != "TemplateVM" - and not getattr(vm, "template_for_dispvms", False) - and (vm.klass == "AdminVM" or vm.is_networked()) + inadvisable_selection( + readable_name="clock qube", vm=vm, warn_dispvm_template=True + ), ) @staticmethod diff --git a/qubes_config/global_config/device_attachments.py b/qubes_config/global_config/device_attachments.py index c688aaf9..4974beb1 100644 --- a/qubes_config/global_config/device_attachments.py +++ b/qubes_config/global_config/device_attachments.py @@ -31,6 +31,7 @@ ProtocolError, ) +from ..widgets.utils import inadvisable_selection from ..widgets.gtk_widgets import TokenName from ..widgets.gtk_utils import show_error from .device_widgets import ( @@ -122,8 +123,12 @@ def __init__( self.qapp, "edit_device", [], - filter_function=lambda vm: vm.klass not in ["AdminVM", "TemplateVM"] - and not getattr(vm, "template_for_dispvms", False), + filter_function=lambda vm: vm.klass not in ["AdminVM", "TemplateVM"], + vm_inadvisable=lambda vm: inadvisable_selection( + readable_name="qube for having devices", + vm=vm, + warn_dispvm_template=True, + ), ) self.backend_vm: Optional[qubesadmin.vm.QubesVM] = None @@ -399,8 +404,12 @@ def __init__( self.qapp, "required_device", [], - filter_function=lambda vm: vm.klass not in ["AdminVM", "TemplateVM"] - and not getattr(vm, "template_for_dispvms", False), + filter_function=lambda vm: vm.klass not in ["AdminVM", "TemplateVM"], + vm_inadvisable=lambda vm: inadvisable_selection( + readable_name="qube for required device", + vm=vm, + warn_dispvm_template=True, + ), ) self.dev_modeler = self.fill_combo_with_devices( diff --git a/qubes_config/global_config/global_config.py b/qubes_config/global_config/global_config.py index ad1a8d10..4df65dec 100644 --- a/qubes_config/global_config/global_config.py +++ b/qubes_config/global_config/global_config.py @@ -40,7 +40,7 @@ show_dialog, ) from ..widgets.gtk_widgets import ProgressBarDialog, ViewportHandler -from ..widgets.utils import open_url_in_disposable +from ..widgets.utils import open_url_in_disposable, inadvisable_selection from .page_handler import PageHandler from .policy_handler import PolicyHandler, VMSubsetPolicyHandler from .policy_rules import ( @@ -435,10 +435,12 @@ def perform_setup(self): policy_file_name="50-config-splitgpg", default_policy="", filter_function=lambda vm: vm.klass not in ["AdminVM", "TemplateVM"] - and not getattr(vm, "template_for_dispvms", False) and not vm.features.get("service.guivm") and not vm.features.get("service.audiovm") and not getattr(vm, "provides_network", False), + vm_inadvisable=lambda vm: inadvisable_selection( + readable_name="key qube", vm=vm, warn_dispvm_template=True + ), main_rule_class=RuleSimpleNoAllow, main_verb_description=SimpleVerbDescription( { diff --git a/qubes_config/global_config/policy_handler.py b/qubes_config/global_config/policy_handler.py index 35de0d15..0fb63b97 100644 --- a/qubes_config/global_config/policy_handler.py +++ b/qubes_config/global_config/policy_handler.py @@ -563,6 +563,7 @@ def __init__( exception_verb_description: AbstractVerbDescription, exception_rule_class: Type[AbstractRuleWrapper], filter_function: Callable[[qubesadmin.vm.QubesVM], bool] | None = None, + vm_inadvisable: Callable[[qubesadmin.vm.QubesVM], bool] | None = None, ): """ :param qapp: Qubes object @@ -582,6 +583,7 @@ def __init__( the exception rules :param exception_rule_class: class to be used for exception Rules, must inherit from AbstractRuleWrapper + :param vm_inadvisable: warn if selection is not advisable """ self.select_qubes: Set[str] = set() self.main_verb_description = main_verb_description @@ -621,6 +623,8 @@ def __init__( # populate combo self.select_qube_model = VMListModeler( combobox=self.select_qube_combo, + trait_name=prefix, + gtk_builder=gtk_builder, qapp=self.qapp, filter_function=( ( @@ -636,6 +640,7 @@ def __init__( ) ) ), + vm_inadvisable=vm_inadvisable, ) # connect events diff --git a/qubes_config/global_config/updates_handler.py b/qubes_config/global_config/updates_handler.py index 47e77b01..27765ff6 100644 --- a/qubes_config/global_config/updates_handler.py +++ b/qubes_config/global_config/updates_handler.py @@ -28,7 +28,11 @@ from qrexec.client import call as qrexec_call from ..widgets.gtk_widgets import VMListModeler, NONE_CATEGORY -from ..widgets.utils import get_boolean_feature, apply_feature_change +from ..widgets.utils import ( + get_boolean_feature, + apply_feature_change, + inadvisable_selection, +) from .page_handler import PageHandler from .policy_rules import RuleTargeted, SimpleVerbDescription from .policy_handler import PolicyHandler @@ -681,16 +685,20 @@ def __init__( self.dom0_updatevm_model = VMListModeler( combobox=self.dom0_updatevm_combo, + trait_name="dom0_updatevm", + gtk_builder=gtk_builder, qapp=self.qapp, filter_function=( lambda vm: vm.klass != "TemplateVM" and vm.klass != "AdminVM" and vm.is_networked() - and not getattr(vm, "template_for_dispvms", False) and get_boolean_feature( vm, "supported-rpc.qubes.TemplateDownload", recurse_template=True ) ), + vm_inadvisable=lambda vm: inadvisable_selection( + readable_name="dom0 updates proxy", vm=vm, warn_dispvm_template=True + ), current_value=self.qapp.updatevm, additional_options=NONE_CATEGORY, style_changes=True, diff --git a/qubes_config/global_config/vm_flowbox.py b/qubes_config/global_config/vm_flowbox.py index 03e88fd2..f20660bd 100644 --- a/qubes_config/global_config/vm_flowbox.py +++ b/qubes_config/global_config/vm_flowbox.py @@ -109,6 +109,7 @@ def __init__( prefix: str, initial_vms: List[qubesadmin.vm.QubesVM], filter_function: Optional[Callable] = None, + vm_inadvisable: Optional[Callable] = None, verification_callback: Optional[Callable[[qubesadmin.vm.QubesVM], bool]] = None, ): """ @@ -117,6 +118,7 @@ def __init__( :param prefix: widget name prefix (see above) :param initial_vms: list of initially selected vms :param filter_function: function to filter vms available in the dropdown + :param vm_inadvisable: function to warn if selection is inadvisable :param verification_callback: if provided, will be called before adding a vm; return True if verification was successful and false if it has failed @@ -134,8 +136,11 @@ def __init__( self.add_qube_model = VMListModeler( combobox=self.qube_combo, + trait_name=prefix, + gtk_builder=gtk_builder, qapp=self.qapp, filter_function=filter_function, + vm_inadvisable=vm_inadvisable, ) self.add_qube_model.connect_change_callback(self._check_for_add_validity) diff --git a/qubes_config/qubes-global-config-base.css b/qubes_config/qubes-global-config-base.css index 0b2a75e4..639954c2 100644 --- a/qubes_config/qubes-global-config-base.css +++ b/qubes_config/qubes-global-config-base.css @@ -187,6 +187,16 @@ separator { background: @problem-background; } +.problem_box_to_previous { + margin: 0px 0px 10px 0px; + padding: 10px; + border-width: 2px; + border-style: solid; + border-radius: 4px; + border-color: @problem-frame; + background: @problem-background; +} + .problem_list { margin-top: 10px; background: @problem-background; @@ -282,4 +292,4 @@ separator { color: @red-label; font-style: italic; font-weight: bold; -} \ No newline at end of file +} diff --git a/qubes_config/widgets/gtk_widgets.py b/qubes_config/widgets/gtk_widgets.py index 68689487..ab365371 100644 --- a/qubes_config/widgets/gtk_widgets.py +++ b/qubes_config/widgets/gtk_widgets.py @@ -230,7 +230,10 @@ def __init__( self, combobox: Gtk.ComboBox, qapp: qubesadmin.Qubes, + trait_name: str = "", + gtk_builder: Gtk.Builder = None, filter_function: Callable[[qubesadmin.vm.QubesVM], bool] | None = None, + vm_inadvisable: Callable[[qubesadmin.vm.QubesVM], bool] | None = None, event_callback: Callable[[], None] | None = None, default_value: qubesadmin.vm.QubesVM | str | None = None, current_value: qubesadmin.vm.QubesVM | str | None = None, @@ -241,9 +244,13 @@ def __init__( """ :param combobox: target ComboBox object :param qapp: Qubes object, necessary to retrieve VM info + :param trait_name: trait that is being considered + :param gtk_builder: builder passed from upstream :param filter_function: function used to filter VMs, must take as input QubesVM object and return bool; caution: remember not all properties are always available for all VMs, in particular dom0 can cause problems + :param vm_inadvisable: function used to return human-readable information if + selected qube is not recommended. :param event_callback: function to be called whenever combobox value changes :param default_value: default VM (will get a (default) decoration @@ -262,6 +269,7 @@ def __init__( self.qapp = qapp self.combo = combobox self.entry_box = self.combo.get_child() + self.vm_inadvisable = vm_inadvisable self.change_function = event_callback self.style_changes = style_changes self.show_internal = show_internal @@ -279,6 +287,15 @@ def __init__( self._initial_id = None + if gtk_builder: + self.warn_box = gtk_builder.get_object(trait_name + "_warn_box") + self.warn_label: Gtk.Label = gtk_builder.get_object( + trait_name + "_warn_label" + ) + self.warn_box.set_visible(False) + else: + self.warn_box = None + if current_value: self.select_value(current_value) elif default_value: @@ -397,17 +414,28 @@ def _get_valid_qube_name(self): def _combo_change(self, _widget): name = self._get_valid_qube_name() + inadvisable = "" if name: entry = self._entries[name] self.entry_box.set_icon_from_pixbuf( Gtk.EntryIconPosition.PRIMARY, entry["icon"] ) + vm = entry["vm"] + if vm and self.vm_inadvisable and (inadvisable := self.vm_inadvisable(vm)): + self._warn(inadvisable) else: self.entry_box.set_icon_from_pixbuf( Gtk.EntryIconPosition.PRIMARY, load_icon("gtk-find", 18, 18) ) + if ( + self.warn_box is not None + and not inadvisable + and self.warn_box.get_visible() + ): + self.warn_box.set_visible(False) + if self.change_function: self.change_function() @@ -500,6 +528,11 @@ def select_value(self, vm_name): if entry["api_name"] == vm_name: self.combo.set_active_id(display_name) + def _warn(self, error_descr: str): + self.warn_label.set_text(error_descr) + if self.warn_box is not None: + self.warn_box.set_visible(True) + def is_vm_available(self, vm: qubesadmin.vm.QubesVM) -> bool: """Check if given VM is available in the list.""" for entry in self._entries.values(): diff --git a/qubes_config/widgets/utils.py b/qubes_config/widgets/utils.py index e1976bf8..1a0b8123 100644 --- a/qubes_config/widgets/utils.py +++ b/qubes_config/widgets/utils.py @@ -147,3 +147,21 @@ def open_url_in_disposable(url: str, qapp: qubesadmin.Qubes): group=None, target=_open_url_in_dvm, args=[url, default_dvm] ) open_thread.start() + + +def inadvisable_selection( + readable_name: str, vm: qubesadmin.vm.QubesVM, warn_dispvm_template: bool = False +) -> str: + inadvisable = [] + if warn_dispvm_template: + inadvisable.append(dispvm_template_inadvisable(vm)) + pretty_inadvisable = " ".join(i for i in inadvisable if i) + if not pretty_inadvisable: + return "" + return f"Inadvisable {readable_name.lower()}: {pretty_inadvisable}" + + +def dispvm_template_inadvisable(vm: qubesadmin.vm.QubesVM) -> str: + if getattr(vm, "template_for_dispvms", False): + return "It's a disposable template, normally not intended for such cases." + return "" From 916c7a9dc29645fdfe1adfb412115c004598adf5 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Fri, 13 Feb 2026 19:14:12 +0100 Subject: [PATCH 3/4] Allow fewer qubes to use the U2F proxy Most qubes are not intended to connect to the U2F proxy. Trim the selection down by hiding improper qubes that should never be clients, such as templates, disposable templates, netvm, AudioVM, GUIVM. --- qubes_config/global_config/usb_devices.py | 16 +++++++----- qubes_config/tests/test_usb_devices.py | 32 ++++++++++++++++------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/qubes_config/global_config/usb_devices.py b/qubes_config/global_config/usb_devices.py index 096613e8..14820e47 100644 --- a/qubes_config/global_config/usb_devices.py +++ b/qubes_config/global_config/usb_devices.py @@ -27,7 +27,7 @@ from qubesadmin.device_protocol import DeviceCategory from ..widgets.gtk_widgets import TokenName, TextModeler, VMListModeler -from ..widgets.utils import get_feature, apply_feature_change +from ..widgets.utils import get_feature, get_boolean_feature, apply_feature_change from ..widgets.gtk_utils import ask_question, show_error from .page_handler import PageHandler from .policy_rules import RuleTargetedAdminVM, Rule @@ -465,13 +465,17 @@ def load_rules_for_usb_qube(self): self.error_handler.clear_all_errors() for vm in self.qapp.domains: - if vm.features.check_with_template(self.SUPPORTED_SERVICE_FEATURE): - if vm == usb_qube: - continue + if vm == usb_qube: + continue + if vm.features.check_with_template(self.SUPPORTED_SERVICE_FEATURE) and not ( + vm.klass == "TemplateVM" + or getattr(vm, "template_for_dispvms", False) + or getattr(vm, "provides_network", False) + or get_boolean_feature(vm, "service.audiovm") + or get_boolean_feature(vm, "service.guivm") + ): self.available_vms.append(vm) if get_feature(vm, self.SERVICE_FEATURE): - if vm == usb_qube: - continue self.initially_enabled_vms.append(vm) if not self.available_vms: diff --git a/qubes_config/tests/test_usb_devices.py b/qubes_config/tests/test_usb_devices.py index 40087c02..9d8f53c0 100644 --- a/qubes_config/tests/test_usb_devices.py +++ b/qubes_config/tests/test_usb_devices.py @@ -350,7 +350,7 @@ def test_u2f_handler_init(test_qapp, test_policy_manager, real_builder): assert handler.enable_check.get_active() assert handler.enable_some_handler.selected_vms == [testvm] assert handler.enable_some_handler.add_qube_model.is_vm_available(testvm) - assert handler.enable_some_handler.add_qube_model.is_vm_available(fedora35) + assert not handler.enable_some_handler.add_qube_model.is_vm_available(fedora35) assert not handler.enable_some_handler.add_qube_model.is_vm_available(testred) assert not handler.enable_some_handler.add_qube_model.is_vm_available(sysusb) @@ -856,9 +856,9 @@ def test_u2f_handler_save_complex(test_qapp, test_policy_manager, real_builder): ) assert ( call(test_qapp.domains["fedora-35"], handler.SERVICE_FEATURE, True) - in mock_apply.mock_calls + not in mock_apply.mock_calls ) - assert len(mock_apply.mock_calls) == 3 + assert len(mock_apply.mock_calls) == 2 expected_rules = handler.policy_manager.text_to_rules( """ @@ -915,9 +915,9 @@ def test_u2f_handler_save_complex_2(test_qapp, test_policy_manager, real_builder ) assert ( call(test_qapp.domains["fedora-35"], handler.SERVICE_FEATURE, True) - in mock_apply.mock_calls + not in mock_apply.mock_calls ) - assert len(mock_apply.mock_calls) == 3 + assert len(mock_apply.mock_calls) == 2 expected_rules = handler.policy_manager.text_to_rules( """ @@ -933,14 +933,14 @@ def test_u2f_handler_save_complex_2(test_qapp, test_policy_manager, real_builder def test_u2f_handler_add_without_service(test_qapp, test_policy_manager, real_builder): sys_usb = test_qapp.domains["sys-usb"] - fedora35 = test_qapp.domains["fedora-35"] + testblue = test_qapp.domains["test-blue"] testvm = test_qapp.domains["test-vm"] handler = U2FPolicyHandler(test_qapp, test_policy_manager, real_builder, {sys_usb}) assert handler.get_unsaved() == "" # settings from conftest: only vms that have this available are 'test-vm' - # and 'fedora-35', only test-vm can use the service, policy is default + # and 'test-blue', only test-vm can use the service, policy is default handler.register_check.set_active(True) handler.register_some_radio.set_active(True) @@ -948,7 +948,7 @@ def test_u2f_handler_add_without_service(test_qapp, test_policy_manager, real_bu assert not handler.register_some_handler.selected_vms assert handler.enable_some_handler.selected_vms == [testvm] - handler.register_some_handler.add_qube_model.select_value("fedora-35") + handler.register_some_handler.add_qube_model.select_value("test-blue") # refuse with patch("qubes_config.global_config.usb_devices.ask_question") as mock_question: mock_question.return_value = Gtk.ResponseType.NO @@ -962,9 +962,9 @@ def test_u2f_handler_add_without_service(test_qapp, test_policy_manager, real_bu mock_question.return_value = Gtk.ResponseType.YES handler.register_some_handler.add_button.clicked() assert mock_question.mock_calls - assert handler.register_some_handler.selected_vms == [fedora35] + assert handler.register_some_handler.selected_vms == [testblue] - assert handler.enable_some_handler.selected_vms == [fedora35, testvm] + assert handler.enable_some_handler.selected_vms == [testblue, testvm] def test_devices_handler_unsaved(test_qapp, test_policy_manager, real_builder): @@ -981,6 +981,12 @@ def test_devices_handler_unsaved(test_qapp, test_policy_manager, real_builder): b"_function='0' _bus='00' _libvirt_name='pci_0000_00_0d_0' " b"_device='0d'\n" ) + test_qapp.expected_calls[ + ("test-vm", "admin.vm.feature.Get", "service.audiovm", None) + ] = b"0\x00" + test_qapp.expected_calls[ + ("test-vm", "admin.vm.feature.Get", "service.guivm", None) + ] = b"0\x00" handler = DevicesHandler(test_qapp, test_policy_manager, real_builder) @@ -1023,6 +1029,12 @@ def test_devices_handler_detect_usbvms(test_qapp, test_policy_manager, real_buil b"_function='0' _bus='00' _libvirt_name='pci_0000_00_0d_0' " b"_device='0d'\n" ) + test_qapp.expected_calls[ + ("test-vm", "admin.vm.feature.Get", "service.audiovm", None) + ] = b"0\x00" + test_qapp.expected_calls[ + ("test-vm", "admin.vm.feature.Get", "service.guivm", None) + ] = b"0\x00" handler = DevicesHandler(test_qapp, test_policy_manager, real_builder) From 49cd624d2db1a7e69481463150d5b3ea182d9256 Mon Sep 17 00:00:00 2001 From: Ben Grande Date: Thu, 26 Mar 2026 06:05:14 +0100 Subject: [PATCH 4/4] UNFINISHED - Experiment with warning u2f Missing labels in glade. --- qubes_config/global_config/usb_devices.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/qubes_config/global_config/usb_devices.py b/qubes_config/global_config/usb_devices.py index 14820e47..f8910b5f 100644 --- a/qubes_config/global_config/usb_devices.py +++ b/qubes_config/global_config/usb_devices.py @@ -27,7 +27,12 @@ from qubesadmin.device_protocol import DeviceCategory from ..widgets.gtk_widgets import TokenName, TextModeler, VMListModeler -from ..widgets.utils import get_feature, get_boolean_feature, apply_feature_change +from ..widgets.utils import ( + get_feature, + get_boolean_feature, + apply_feature_change, + dispvm_template_inadvisable, +) from ..widgets.gtk_utils import ask_question, show_error from .page_handler import PageHandler from .policy_rules import RuleTargetedAdminVM, Rule @@ -317,6 +322,7 @@ def __init__( "usb_u2f_enable_some", self.initially_enabled_vms, lambda vm: vm in self.available_vms, + vm_inadvisable=lambda vm: dispvm_template_inadvisable(vm), ) self.register_some_handler = VMFlowboxHandler( @@ -326,6 +332,7 @@ def __init__( self.initial_register_vms, lambda vm: vm in self.available_vms, verification_callback=self._verify_additional_vm, + vm_inadvisable=lambda vm: dispvm_template_inadvisable(vm), ) self.blanket_handler = VMFlowboxHandler( @@ -335,6 +342,7 @@ def __init__( self.initial_blanket_vms, lambda vm: vm in self.available_vms, verification_callback=self._verify_additional_vm, + vm_inadvisable=lambda vm: dispvm_template_inadvisable(vm), ) self.widget_to_box = { @@ -469,7 +477,6 @@ def load_rules_for_usb_qube(self): continue if vm.features.check_with_template(self.SUPPORTED_SERVICE_FEATURE) and not ( vm.klass == "TemplateVM" - or getattr(vm, "template_for_dispvms", False) or getattr(vm, "provides_network", False) or get_boolean_feature(vm, "service.audiovm") or get_boolean_feature(vm, "service.guivm")