diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b048586b..3b9bae96 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -46,3 +46,4 @@ checks:black: variables: DIR: . SKIP_PYLINT: 1 + BLACK_ARGS: -l88 -v --diff --color --check diff --git a/.pylintrc b/.pylintrc index 3229f3fc..e7b798a4 100644 --- a/.pylintrc +++ b/.pylintrc @@ -91,7 +91,7 @@ notes=FIXME,FIX,XXX,TODO [FORMAT] # Maximum number of characters on a single line. -max-line-length=80 +max-line-length=88 # Maximum number of lines in a module max-module-lines=3000 diff --git a/icons/scalable/devices-dark.svg b/icons/scalable/devices-dark.svg new file mode 100644 index 00000000..5d6182dc --- /dev/null +++ b/icons/scalable/devices-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/scalable/devices-light.svg b/icons/scalable/devices-light.svg new file mode 100644 index 00000000..584e6bbc --- /dev/null +++ b/icons/scalable/devices-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/scalable/qubes-icon-add.svg b/icons/scalable/qubes-icon-add.svg new file mode 100755 index 00000000..169467ef --- /dev/null +++ b/icons/scalable/qubes-icon-add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/scalable/qubes-icon-edit.svg b/icons/scalable/qubes-icon-edit.svg new file mode 100755 index 00000000..95b1bcbd --- /dev/null +++ b/icons/scalable/qubes-icon-edit.svg @@ -0,0 +1,5 @@ + + + + diff --git a/icons/scalable/qubes-icon-remove.svg b/icons/scalable/qubes-icon-remove.svg new file mode 100755 index 00000000..51e5570e --- /dev/null +++ b/icons/scalable/qubes-icon-remove.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/scalable/qubes-unplug.svg b/icons/scalable/qubes-unplug.svg new file mode 100644 index 00000000..5114bb98 --- /dev/null +++ b/icons/scalable/qubes-unplug.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/qubes_config/global_config.glade b/qubes_config/global_config.glade index 10069344..4173e56d 100644 --- a/qubes_config/global_config.glade +++ b/qubes_config/global_config.glade @@ -3,463 +3,704 @@ - + False - Qubes OS Global Config - center - - - True + dialog + + False vertical + 2 + + + False + end + + + _Save and Close + True + True + True + none + True + + + + False + True + 0 + + + + + _Cancel + True + True + True + none + True + + + + False + True + 1 + + + + + False + False + 0 + + - - main_notebook + True True + True True - left + in + True + True - - basics + True - True - never - in + False - + True False - natural + vertical - - + True False - 5 - 30 - - - True - False - Default and service qubes - 0 - - - - 0 - 2 - 2 - - + start + Qube + + + + False + True + 0 + + + + + True + False + 10 True False - Select the qubes that are used by default and that provide basic services. + center + True + Select qube: 0 - 0 - 3 - 2 + False + True + 0 - + True False - vertical - - - True - False - Clock qube: - 0 - - - - False - True - 0 - - - - - True - False - This qube sets the time in the system. - 0 - + center + 5 + 5 + True + True + + + True - - False - True - 1 - - - - 0 - 4 - - - - - True - False - Window Management - 0 - 0 - 9 - 2 + False + True + 1 + + + False + False + 1 + + + + + True + False + center + True + Select a qube to be able to select device types to block. + True + True + 0 + + + + False + True + 2 + + + + + True + False + vertical True False - Fullscreen mode: - 0 + start + Deny device attachment - 0 - 12 + False + True + 0 - + True False center - 5 - True + True + You will not be able to manually attach any of the selected device types to this qube. This does <b>NOT</b> apply to automatic attachments. + True + True + 0 - 1 - 12 - - - - - True - False - center - 5 - True - - - 1 - 13 - - - - - True - False - center - 5 - True - - - 1 - 14 + False + True + 1 - + True - False - center - 5 - 5 - True - True - - - True - 24 + True + True + in + 300 + + + True + False + + + True + False + none + + + - - 1 - 4 - - - - - True - False - center - 5 - 5 - True - True - - - True - 24 - - - - - - 1 - 6 + True + True + 2 + + + False + True + 3 + + + + + + + + + + True + True + 1 + + + + + + + False + True + dialog + + + False + vertical + 2 + + + False + end + + + _Save and Close + True + True + True + none + True + + + + False + True + 0 + + + + + _Cancel + True + True + True + none + True + + + + False + True + 1 + + + + + False + False + 0 + + + + + True + True + in + True + True + + + True + False + + + True + False + vertical + + + True + False + start + Device + + + + False + True + 0 + + + + + True + False + 10 - + True False center - 5 - 5 - True - True - - - True - 24 - - + True + Device: + 0 - 1 - 7 + False + True + 0 - + True False - center - 5 - 5 - True - True - - - True - 24 - - - 1 - 5 + False + True + 2 + + + False + False + 1 + + + + + True + False + center + 20 + 20 - + True False - Memory Balancing - 0 - + start + center + 48 + qubes-unplug - 0 - 16 - 2 + False + True + 0 - + True - False - These control the default memory settings for all qubes except those with connected PCI devices. + True + start + This device is not currently connected. The assignment cannot be modified. + True True - 0 - - 0 - 17 - 2 + False + True + 1 + + + + False + False + 2 + + + + + True + False + center + True + Apply rule to devices matching: + 0 + + + + False + True + 3 + + + + + True + False - + + Backend qube: True - False - Linux Kernel - 0 + False + True + False + Backend qube cannot be changed dynamically. Select a device from desired backend qube. + True + True - 0 - 21 - 2 + False + True + 0 - + + + + + False + True + 4 + + + + + True + False + + + Device class: True - False - True + False + True + False + Device class cannot be set independently. Select a device from desired class. + True + True + - 1 - 23 + False + True + 0 - + True False + center True + 0 + - 0 - 24 - 2 + False + True + 1 + + + False + True + 5 + + + + + True + False - + + Device identity: True - False - This is the default kernel for Linux qubes. You can select a different kernel for an individual qube in that qube's settings. - True - 0 + True + False + start + start + True - 0 - 22 - 2 + False + True + 0 - + True False - vertical - - - True - False - Default net qube: - 0 - - - - False - True - 0 - - - - - True - False - This qube provides network access to all qubes using the default networking setting. - True - 0 - - - - False - True - 1 - - + start + 2 + True + 0 + - 0 - 5 + False + True + 2 + + + False + True + 6 + + + + + True + False - + + Port: + True + True + False + True + + + + False + True + 0 + + + + True False - vertical - - - True - False - Default template: - 0 - - - - False - True - 0 - - - - - True - False - Default template for new qubes. This setting has no effect on existing qubes. - True - 0 - - - - False - True - 1 - - + center + True + 0 + - 0 - 6 + False + True + 1 + + + False + True + 7 + + + + + True + False + <b>Port</b> refers to the port this device is currently plugged in. If you select matching the port, the device <b>must</b> be plugged into this port to be automatically attached. If you select port and not device identity, all devices plugged into this port will be automatically attached. + True + True + 0 + + + + False + True + 8 + + + + + True + False + start + Action + + + + False + True + 9 + + + + + True + False + center + True + Select what happens when the device becomes available. + 0 + + + + False + True + 10 + + + + + True + True + False + True + True True @@ -469,8 +710,9 @@ True False - Default disposable template: - True + center + True + Automatically attach the device to the selected qube 0 + + + False + True + 12 + + + + + Read-only (only for block devices) + True + True + False + 10 + True + + + False + True + 13 + + + + + True + False + start + Qubes + + + + False + True + 14 + + + + + True + False + vertical - + True False - Customize the appearance and behavior of windows and tray icons. - True - 0 + 40 - 0 - 10 - 2 + False + True + 0 - + True False - center - True - 20 + 5 + 10 - + True False - end center - 48 - qubes-info + 5 + 5 + True + False @@ -545,13 +846,51 @@ - + True True + True start - Allowing qubes to use fullscreen mode can have unexpected security consequences. <a href="https://www.qubes-os.org/doc/how-to-enter-fullscreen-mode/">Learn more.</a> - True - True + none + + + True + False + 10 + + + True + False + qubes-icon-add + + + False + True + 0 + + + + + True + False + 5 + Add qube + + + + False + True + 1 + + + + + False @@ -559,26 +898,132 @@ 1 - - 0 - 11 - 2 + False + True + 1 - - - True - False - vertical - + + + False + True + 15 + + + + + False + A device cannot be attached to its own backend qube. + True + True + 0 + + + + False + True + 16 + + + + + + + + + + False + True + 1 + + + + + + + False + Qubes OS Global Config + center + + + True + False + vertical + + + main_notebook + True + True + True + left + + + basics + True + True + never + in + + + True + False + natural + + + + True + False + 5 + 30 + + + True + False + Default and service qubes + 0 + + + + 0 + 2 + 2 + + + + + True + False + Select the qubes that are used by default and that provide basic services. + 0 + + + + 0 + 3 + 2 + + + + + True + False + vertical + True False - UTF-8 window titles: + Clock qube: 0 0 - 0 + 10 + 2 - - anchor + True - True - True + False + Fullscreen mode: + 0 + 0 - 8 + 13 - - anchor + True - True - True + False + center + 5 + True + - 0 - 15 + 1 + 13 - - anchor + True - True - True + False + center + 5 + True - 0 - 20 + 1 + 14 - + True False - General Settings - 0 - + center + 5 + True - 0 - 1 - 2 + 1 + 15 - + True False - Tray icon style: - 0 + center + 5 + 5 + True + True + + + True + 24 + + - 0 - 14 + 1 + 4 - + True False - Default Linux Kernal: - 0 + center + 5 + 5 + True + True + + + True + 24 + + - 0 - 23 + 1 + 6 - + True False - Minimum qube memory: - 0 + center + 5 + 5 + True + True + + + True + 24 + + - 0 - 18 + 1 + 7 - + True False - Additional dom0 memory: - 0 + center + 5 + 5 + True + True + + + True + 24 + + - 0 - 19 + 1 + 5 - + True False - - - True - True - - - False - True - 0 - - - - - True - False - start - 5 - 5 - True - MiB - - - - False - True - 1 - - + Memory Balancing + 0 + - 1 - 18 + 0 + 17 + 2 - + True False - - - True - True - - - False - True - 0 - - - - - True - False - start - 5 - 5 - True - MiB - - - - False - True - 1 - - + These control the default memory settings for all qubes except those with connected PCI devices. + True + 0 + - 1 - 19 + 0 + 18 + 2 - - - - - - - - - - + + True + False + Linux Kernel + 0 + + + + 0 + 22 + 2 + - - - - - - - - - - True - False - - - True - False - settings-dark - - - False - True - 0 - - - - - True - False - General - - - - False - True - 1 - - - - - False - - - - - usb - True - True - in - - - True - False - natural - - - True - False - vertical - - anchor + True - True - True + False + True - False - True - 0 + 1 + 24 True False - USB Input Devices - 0 - + True + - False - True - 1 + 0 + 25 + 2 True False - USB input devices, especially keyboards, are a significant security risk. If you're not using a USB keyboard, USB mouse, USB touchscreen, or USB tablet, you can disable them here. (Note: Laptop built-in keyboards are usually not USB keyboards.) - True + This is the default kernel for Linux qubes. You can select a different kernel for an individual qube in that qube's settings. True 0 - False - True - 2 + 0 + 23 + 2 - + + True False vertical - + True False - Unexpected policy file contents: - True + Default net qube: + 0 + False @@ -954,25 +1337,43 @@ 0 - + + + True + False + This qube provides network access to all qubes using the default networking setting. + True + 0 + + + + False + True + 1 + + - False - True - 3 + 0 + 5 - + + True False vertical - + + True False - Custom policy changes found. The changes below may be overwritten by existing custom policy changes. View the files listed below to verify the existing policy. - True + Default template: + 0 + False @@ -981,12 +1382,14 @@ - + + True False - none - False + Default template for new qubes. This setting has no effect on existing qubes. + True + 0 @@ -995,242 +1398,297 @@ 1 - - False - True - 4 + 0 + 6 - - + True False - 5 - 5 + vertical - + True False - vertical - - - True - False - USB <b>qube</b> - True - 0 - - - - False - True - 0 - - - - - True - False - A USB qube is a qube to which one or more USB controllers are connected. - True - 0 - - - - False - True - 1 - - + Default disposable template: + True + 0 + - 0 - 0 + False + True + 0 True False - start - <b>Keyboard</b>: - True + Disposable qubes will be based on this template by default. + True + 0 - 0 - 1 + False + True + 1 + + + 0 + 7 + + + + + True + False + Customize the appearance and behavior of windows and tray icons. + True + 0 + + + + 0 + 11 + 2 + + + + + True + False + center + True + 20 - + True False + end + center + 48 + qubes-info + + + False + True + 0 + + + + + True + True start - <b>Mouse</b>: + Allowing qubes to use fullscreen mode can have unexpected security consequences. <a href="https://www.qubes-os.org/doc/how-to-enter-fullscreen-mode/">Learn more.</a> True - + True - 0 - 2 + False + True + 1 + + + + 0 + 12 + 2 + + + + + True + False + vertical True False - start - <b>Touchscreen/Tablet</b>: - True + UTF-8 window titles: + 0 - 0 - 3 + False + True + 0 - - - - - - - - - - - - - - - - - - - - - - + + True + False + "Allow" allows app qubes to display a wider range of characters in window titles, but increases the attack surface. + True + 0 + + + + False + True + 1 + - False - True - 5 + 0 + 14 - + anchor True True True - False - True - 6 + 0 + 0 - + + anchor True - False - U2F devices - 0 - + True + True - False - True - 7 + 0 + 9 - + + anchor True - False - Qubes U2F Proxy allows you to use Universal 2nd Factor (U2F) two-factor authentication (2FA) devices without exposing the whole device and all of its contents to every qube. <a href="https://www.qubes-os.org/doc/u2f-proxy/">Learn more.</a> - -<b>Note:</b> In order to use the U2F proxy in a qube, its template must have the <tt>qubes-u2f</tt> package installed. By default, a qube can use only keys that it has registered itself. - True - True - 0 - + True + True - False - True - 8 + 0 + 16 - - u2f_usb_qube_box - True + + anchor + True + True + True + + + 0 + 21 + + + + + True + False + General Settings + 0 + + + + 0 + 1 + 2 + + + + + True + False + Tray icon style: + 0 + + + + 0 + 15 + + + + + True + False + Default Linux Kernal: + 0 + + + + 0 + 24 + + + + + True + False + Minimum qube memory: + 0 + + + + 0 + 19 + + + + + True + False + Additional dom0 memory: + 0 + + + + 0 + 20 + + + + + True False - 50 - + True - False - vertical - - - True - False - start - <b>USB qube</b> - True - - - - False - True - 0 - - - - - True - False - Qube to which the USB controller is connected. - True - 0 - - - - False - True - 1 - - + True False @@ -1239,49 +1697,38 @@ - - u2f_usb_combo + True False start - center 5 - 50 - 5 - False - True - - - True - 24 - - + 5 + True + MiB False True - 2 + 1 - False - True - 9 + 1 + 19 - + + True False - vertical - - False - Some policy rules could not be parsed. They are correct but are too complicated for this tool to handle. They will be discarded on save. The following rules were affected: - True + + True + True False @@ -1290,12 +1737,16 @@ - + + True False - none - False + start + 5 + 5 + True + MiB @@ -1304,28 +1755,27 @@ 1 - - False - True - 10 + 1 + 20 - + + True False - center vertical - + + True False - center - The Qubes U2F Proxy service is not installed in the USB qube. If you wish to use this service, install the <tt>qubes-u2f</tt> package in the template on which the USB qube is based. - True + Preload disposables: True + 0 + False @@ -1333,25 +1783,43 @@ 0 - + + + True + False + Number of disposable qubes to preload from the default disposable template. + True + 0 + + + + False + True + 1 + + - False - True - 11 + 0 + 8 - + + True False - vertical - - False - Custom policy changes found. The changes below may be overwritten by existing custom policy changes. View the files listed below to verify the existing policy. - True + + True + True + center + 5 + 5 + True + 2 + 2 False @@ -1360,12 +1828,17 @@ - + + True False - none - False + start + 5 + 5 + 5 + 5 + True @@ -1374,60 +1847,222 @@ 1 - - False - True - 12 + 1 + 8 - + + + + + + + + + + + + + + + + + + + + + True + False + + + True + False + settings-dark + + + False + True + 0 + + + + + True + False + General + + + + False + True + 1 + + + + + False + + + + + usb + True + True + in + + + True + False + natural + + + True + False + vertical + + + anchor True True - False - 10 - 20 - True + True + + + False + True + 0 + + + + + True + False + USB Input Devices + 0 + + + + False + True + 1 + + + + + True + False + USB input devices, especially keyboards, are a significant security risk. If you're not using a USB keyboard, USB mouse, USB touchscreen, or USB tablet, you can disable them here. (Note: Laptop built-in keyboards are usually not USB keyboards.) + True + True + 0 + + + + False + True + 2 + + + + + False + vertical - + True False - start - <b>Enable the Qubes U2F Proxy</b> service - True + Unexpected policy file contents: + True + + False + True + 0 + + False True - 14 + 3 - - True + False - 30 vertical - + + False + Custom policy changes found. The changes below may be overwritten by existing custom policy changes. View the files listed below to verify the existing policy. + True + + + False + True + 0 + + + + + False + none + False + + + + False + True + 1 + + + + + + False + True + 4 + + + + + + True + False + 5 + 5 + + True False - 35 vertical True False - start - 30 - <b>List of qubes that can use the U2F Proxy:</b> + USB <b>qube</b> True + 0 + False @@ -1436,12 +2071,14 @@ - + True False - 40 + A USB qube is a qube to which one or more USB controllers are connected. + True + 0 @@ -1450,158 +2087,184 @@ 1 - - - False - 40 - 10 - - - True - False - Add qube: - - - - - - False - True - 0 - - - - - True - False - If you don't see the desired qube here, install the <tt>qubes-u2f</tt> package in the template on which it is based (or, if it's a standalone, in the qube itself). - 0.5 - 0.6000000238418579 - 20 - qubes-question - - - False - True - 3 - 1 - - - - - True - False - True - - - True - - - - - False - True - 2 - - - - - CANCEL - True - True - True - none - - - - False - True - 3 - - - - - ADD - True - True - True - none - - - - False - True - 4 - - + + + 0 + 0 + + + + + True + False + start + <b>Keyboard</b>: + True + + + + 0 + 1 + + + + + True + False + start + <b>Mouse</b>: + True + + + + 0 + 2 + + + + + True + False + start + <b>Touchscreen/Tablet</b>: + True + + + + 0 + 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + True + 5 + + + + + anchor + True + True + True + + + False + True + 6 + + + + + True + False + U2F devices + 0 + + + + False + True + 7 + + + + + True + False + Qubes U2F Proxy allows you to use Universal 2nd Factor (U2F) two-factor authentication (2FA) devices without exposing the whole device and all of its contents to every qube. <a href="https://www.qubes-os.org/doc/u2f-proxy/">Learn more.</a> + +<b>Note:</b> In order to use the U2F proxy in a qube, its template must have the <tt>qubes-u2f</tt> package installed. By default, a qube can use only keys that it has registered itself. + True + True + 0 + + + + False + True + 8 + + + + + u2f_usb_qube_box + True + False + 50 + + + True + False + vertical + + + True + False + start + <b>USB qube</b> + True False True - 2 + 0 - + True - True - True - start - none - - - True - False - 10 - - - True - False - qubes-add - - - False - True - 0 - - - - - True - False - Add Qube - - - - False - True - 1 - - - - + False + Qube to which the USB controller is connected. + True + 0 False True - 3 + 1 @@ -1612,94 +2275,194 @@ - + + u2f_usb_combo True False + start center - 10 - 60 - True + 5 + 50 + 5 + False + True + + + True + 24 + + + False True - 1 + 2 - - - True - True - False - True - - - True - False - start - Enable <b>registering new keys</b> with the U2F Proxy service - True - True - - + + + False + True + 9 + + + + + False + vertical + + + False + Some policy rules could not be parsed. They are correct but are too complicated for this tool to handle. They will be discarded on save. The following rules were affected: + True False True - 2 + 0 - + + False + none + False + + + + False + True + 1 + + + + + + False + True + 10 + + + + + False + center + vertical + + + False + center + The Qubes U2F Proxy service is not installed in the USB qube. If you wish to use this service, install the <tt>qubes-u2f</tt> package in the template on which the USB qube is based. + True + True + + + False + True + 0 + + + + + + False + True + 11 + + + + + False + vertical + + + False + Custom policy changes found. The changes below may be overwritten by existing custom policy changes. View the files listed below to verify the existing policy. + True + + + False + True + 0 + + + + + False + none + False + + + + False + True + 1 + + + + + + False + True + 12 + + + + + True + True + False + 10 + 20 + True + + True False - 30 + start + <b>Enable the Qubes U2F Proxy</b> service + True + + + + + False + True + 14 + + + + + True + False + 30 + vertical + + + True + False + 35 vertical - + True - True - False - True - True - - - True - False - - - True - False - <b>All qubes</b> - True - - - False - True - 0 - - - - - True - False - selected above can register new keys - True - - - - False - True - 1 - - - - - + False + start + <b>List of qubes that can use the U2F Proxy:</b> + True False @@ -1708,49 +2471,13 @@ - + True - True - False - True - usb_u2f_register_all_radio - - - True - False - - - True - False - <b>Only selected qubes</b> - True - - - False - True - 0 - - - - - True - False - can register new keys - True - - - - False - True - 1 - - - - + False + 10 + 40 @@ -1760,130 +2487,16 @@ - + True False - 30 - vertical - - - True - False - 40 - - - - False - True - 0 - - - - - False - 40 - 10 - - - True - False - Add qube: - - - - - - False - True - 0 - - - - - True - False - If you don't see the desired qube here, install the <tt>qubes-u2f</tt> package in the template on which it is based (or, if it's a standalone, in the qube itself). - 0.5 - 0.6000000238418579 - 20 - qubes-question - - - False - True - 3 - 1 - - - - - True - False - True - - - True - - - - - False - True - 2 - - - - - CANCEL - True - True - True - none - - - - False - True - 3 - - - - - ADD - True - True - True - none - - - - False - True - 4 - - - - - - False - True - 1 - - + 10 + 40 + 5 + 5 + 10 - + True True True @@ -1898,7 +2511,7 @@ True False - qubes-add + qubes-icon-add False @@ -1910,7 +2523,7 @@ True False - Add Qube + Add qube @@ -1934,6 +2547,23 @@ 2 + + + True + False + True + + + False + + + + + False + True + 2 + + False @@ -1941,11 +2571,30 @@ 2 + + + True + False + 10 + If you don't see the desired qube here, install the <tt>qubes-u2f</tt> package in the template on which it is based (or, if it's a standalone, in the qube itself). + True + True + 0 + + + + False + True + 3 + + False True - 3 + 0 @@ -1960,123 +2609,157 @@ False True - 4 + 1 - + True True False True - + True False - - - True - False - Allow some qubes to access <b>ALL</b> keys stored on your U2F device - True - True - - - False - True - 0 - - + start + Enable <b>registering new keys</b> with the U2F Proxy service + True + True False True - 5 + 2 - + True False 30 vertical - + True - False - center - 20 - - - True - False - start - center - 48 - qubes-info - - - False - True - 0 - - + True + False + True + True - + True False - start - Caution: -<b>All</b> keys, including those registered by other qubes and those registered previously, will be available to the qubes selected below. - True - True + + + True + False + <b>All qubes</b> + True + + + False + True + 0 + + + + + True + False + selected above can register new keys + True + + + + False + True + 1 + + - - False - True - 1 - False - False + True 0 - + True - False - 40 - - - - False - True - 1 - - - - - False - 30 - 10 + True + False + True + usb_u2f_register_all_radio - + + True + False + + + True + False + <b>Only selected qubes</b> + True + + + False + True + 0 + + + + + True + False + can register new keys + True + + + + False + True + 1 + + + + + + + + False + True + 1 + + + + + True + False + 30 + vertical + + True False - Add qube: - - - + 10 + 40 + False @@ -2085,32 +2768,102 @@ - + True False - If you don't see the desired qube here, install the <tt>qubes-u2f</tt> package in the template on which it is based (or, if it's a standalone, in the qube itself). - 0.5 - 0.6000000238418579 - 20 - qubes-question + 10 + 40 + 5 + 5 + 10 + + + True + False + True + + + True + + + + + False + True + 2 + + + + + True + True + True + start + none + + + True + False + 10 + + + True + False + qubes-icon-add + + + False + True + 0 + + + + + True + False + Add qube + + + + False + True + 1 + + + + + + + + False + True + 3 + + False True - 3 1 - + True False - True - - - True - - + 10 + If you don't see the desired qube here, install the <tt>qubes-u2f</tt> package in the template on which it is based (or, if it's a standalone, in the qube itself). + True + True + 0 + False @@ -2118,98 +2871,232 @@ 2 + + + False + True + 2 + + + + + False + True + 3 + + + + + True + False + center + 10 + 60 + True + + + False + True + 4 + + + + + True + True + False + True + + + True + False + + + True + False + Allow some qubes to access <b>ALL</b> keys stored on your U2F device + True + True + + + False + True + 0 + + + + + + + False + True + 5 + + + + + True + False + 30 + vertical + + + True + False + center + 20 - - CANCEL + True - True - True - none - + False + start + center + 48 + qubes-info False True - 3 + 0 - - ADD + True - True - True - none - + False + start + Caution: +<b>All</b> keys, including those registered by other qubes and those registered previously, will be available to the qubes selected below. + True + True False True - 4 + 1 + + + False + False + 0 + + + + + True + False + 10 + 40 + False True - 2 + 1 - + True - True - True - start - none + False + 10 + 30 + 5 + 5 + 10 - + True False - 10 - - - True - False - qubes-add - - - False - True - 0 - + True + + + True + + + + False + True + 2 + + + + + True + True + True + start + none - + True False - Add Qube - + 10 + + + True + False + qubes-icon-add + + + False + True + 0 + + + + + True + False + Add qube + + + + False + True + 1 + + - - False - True - 1 - + + + False + True + 3 + + + + False + True + 2 + + + + + True + False + 10 + If you don't see the desired qube here, install the <tt>qubes-u2f</tt> package in the template on which it is based (or, if it's a standalone, in the qube itself). + True + 0 @@ -2282,8 +3169,8 @@ - - updates + + attachments True True in @@ -2292,13 +3179,12 @@ True False - + True False vertical - 5 - + anchor True True @@ -2314,10 +3200,9 @@ True False - Dom0 updates + Device Attachment Policy 0 @@ -2331,7 +3216,8 @@ True False - Choose how updates are applied to dom0 to maintain system security and stability. + You can block the ability to manually attach some (or all) device types or devices to selected qubes. This does <b>not</b> apply to automatic attachment set below. + True True 0 + + + + + + False + True + 3 + + True False - True + 10 - + True - False - vertical - - - True - False - start - Dom0 update proxy: - - - - False - True - 0 - - + True + True + start + none - + True False - 20 - Dom0 uses a service qube as an update proxy to download updates securely. - True - 0 - + 10 + + + True + False + qubes-icon-add + + + False + True + 0 + + + + + True + False + 5 + Add new rule + + + + False + True + 1 + + - - False - True - 1 - @@ -2399,19 +3316,51 @@ - + True - False - center - True - True - - + False + True + True + start + none + + + True False + 10 + + + True + False + qubes-icon-add + + + False + True + 0 + + + + + True + False + 5 + Edit selected rule + + + + False + True + 1 + + @@ -2420,219 +3369,69 @@ 1 - - - False - True - 3 - - - - - False - vertical - + True - False - Error accessing repository data. Cannot change update settings. - True - - - False - True - 0 - - - - - - False - True - 4 - - - - - True - True - False - True - True - - - True - False - vertical - - - True - False - start - Enable <b>stable</b> updates only <b>(recommended)</b> - True - - - - False - True - 0 - - - - - True - False - Stable updates are well-tested and safe but may take time to be released. - True - 0 - - - - False - True - 2 - - - - - - - - False - True - 5 - - - - - True - True - False - True - True - updates_dom0_stable_radio - - - True - False - vertical - - - True - False - start - Enable <b>security-related testing</b> updates only - True - True - - - - False - True - 0 - - - - - True - False - This option allows for timely security fixes while minimizing bugs from non-security updates. Suitable for advanced users willing to trade some stability for security. - True - 0 - - - - False - True - 1 - - - - - - - - False - True - 6 - - - - - True - True - False - True - True - updates_dom0_stable_radio - - - True - False - vertical - - - True - False - start - Enable <b>all testing</b> updates - True - - - - False - True - 0 - - + False + True + True + start + none - + True False - This option provides immediate access to all changes, but updates may contain bugs that reduce system stability. Suitable for users who wish to help with testing new features. It is not recommended for stable or production systems. - True - 0 - + 10 + + + True + False + qubes-icon-remove + + + False + True + 0 + + + + + True + False + 5 + Remove selected rule + + + + False + True + 1 + + - - False - True - 1 - + + + False + True + 2 + - False True - 7 + 4 - + anchor True True @@ -2641,31 +3440,31 @@ False True - 8 + 5 True False - Check for qube updates + Device Assignment 0 False True - 9 + 6 True False - All qubes that have network access can check for updates. If such a qube is based on a template, you will be notified when updates are available for that template. + Devices can be assigned to selected qubes. The device will be attached as soon as the qube is running and the device is available (e.g. plugged in). + True True 0 + False True - 11 + 8 True False - center - 20 + 10 - + True - False + True + True start - center - 48 - qubes-info - - - False - True + none + + + True + False + 10 + + + True + False + qubes-icon-add + + + False + True + 0 + + + + + True + False + 5 + Add new rule + + + + False + True + 1 + + + + + + + + False + True 0 - + True - False + False + True + True start - The setting below will be applied only to <b>existing</b> qubes. New qubes have checking for updates <b>enabled</b> by default. - True - True + none + + + True + False + 10 + + + True + False + qubes-icon-add + + + False + True + 0 + + + + + True + False + 5 + Edit selected rule + + + + False + True + 1 + + + + + False @@ -2739,151 +3619,191 @@ 1 - + + + True + False + True + True + start + none + + + True + False + 10 + + + True + False + qubes-icon-remove + + + False + True + 0 + + + + + True + False + 5 + Remove selected rule + + + + False + True + 1 + + + + + + + + False + True + 2 + + False - False - 12 + True + 9 - + + anchor True True - False - True - True - - - True - False - <b>Enable</b> checking for updates for all existing qubes - True - - - - + True False True - 13 + 10 - + True - True - False - True - True - updates_enable_radio - - - True - False - <b>Disable</b> checking for updates for all existing qubes - True - - - + False + Required Devices + 0 False True - 14 + 11 - + True False + A qube can require a device to start. This option is supported only for block and PCI devices, not for USB devices. + True + True + 0 + False True - 15 + 12 - - - - + True - True - False - 30 - True - - + False + False + + True False - - - True - False - Except for the following qubes, for which checking for updates will be - - - False - True - 0 - - - - - True - False - <b>disabled</b> - True - - - False - True - 1 - - + No required assignments defined False True - 17 + 13 - + True False - 30 - vertical + 10 - + True - False - 20 - + + + False + True + 1 + + + + + @@ -2893,85 +3813,51 @@ - - False - 25 - 10 - - - True - False - Add exception: - - - - - - False - True - 0 - - + + True + False + True + True + start + none - + True False - True - - - True + 10 + + + True + False + qubes-icon-add + + False + True + 0 + + + + + True + False + 5 + Edit selected rule + + + + False + True + 1 + - - - - False - True - 1 - - - - - CANCEL - True - True - True - none - - - - False - True - 2 - - - - - ADD - True - True - True - none - - - False - True - 3 - @@ -2981,12 +3867,12 @@ - + True + False True True start - 35 none @@ -2997,7 +3883,7 @@ True False - qubes-add + qubes-icon-remove False @@ -3010,7 +3896,7 @@ True False 5 - Add Exception + Remove selected rule @@ -3038,11 +3924,77 @@ False True - 18 + 14 + + + + + + + + 2 + False + + + + + True + False + + + True + False + usb-dark + + + False + True + 0 + + + + + True + False + Device Assignments + + + + False + True + 1 + + + + + 2 + False + + + + + updates + True + True + in + + + True + False + + + True + False + vertical + 5 - + anchor True True @@ -3051,14 +4003,14 @@ False True - 19 + 0 True False - Update proxy + Dom0 updates 0 - - - False - True - 1 - - - - - - False - True - 22 - - - - - False - vertical - - - False - Some policy rules could not be parsed. They are correct but are too complicated for this tool to handle. These rules will be discarded on save. The following rules were affected: - True - - - False - True - 0 - - - - - False - none - False - - - - False - True - 1 - - - - - - False - True - 23 - - - - - - True - False - 20 - 20 - - - True - False - True - - - True - 24 - - - - - - - 1 - 0 - - - - - True - False - center - True - - - True - 24 - - - - - - - 1 - 1 - - - - - True - False - start vertical True False start - <b>Whonix</b> update proxy - True + Dom0 update proxy: @@ -3251,8 +4070,8 @@ True False - All Whonix qubes (those with the <tt>whonix-updatevm</tt> tag) will use this qube as an update proxy. This qube must be a Whonix gateway (e.g., <tt>sys-whonix</tt>). - True + 20 + Dom0 uses a service qube as an update proxy to download updates securely. True 0 - - - 0 - 0 - - - - - False - True - 24 - - - - - True - False - 20 - 10 - vertical - - - True - False - start - <b>With the following exceptions:</b> - True @@ -3319,11 +4095,20 @@ - + True False center True + True + + + False + + + False @@ -3335,22 +4120,19 @@ False True - 25 + 3 - - True + False - 10 - True + vertical - + True False - start - <b>QUBE</b> - True + Error accessing repository data. Cannot change update settings. + True False @@ -3358,69 +4140,38 @@ 0 - - - True - False - start - <b>UPDATE PROXY</b> - True - - - False - True - 1 - - - - - False - True - 26 - - - - - True - False - - - True - False - No exceptions - - - False True - 27 + 4 - + True True - True - start - none + False + True + True True False - 10 + vertical - + True False - qubes-add + start + Enable <b>stable</b> updates only <b>(recommended)</b> + True + False @@ -3432,32 +4183,152 @@ True False - Add Exception + Stable updates are well-tested and safe but may take time to be released. + True + 0 False True - 1 + 2 False True - 29 + 5 - + + True + True + False + True + True + updates_dom0_stable_radio + + + True + False + vertical + + + True + False + start + Enable <b>security-related testing</b> updates only + True + True + + + + False + True + 0 + + + + + True + False + This option allows for timely security fixes while minimizing bugs from non-security updates. Suitable for advanced users willing to trade some stability for security. + True + 0 + + + + False + True + 1 + + + + + + + + False + True + 6 + + + + + True + True + False + True + True + updates_dom0_stable_radio + + + True + False + vertical + + + True + False + start + Enable <b>all testing</b> updates + True + + + + False + True + 0 + + + + + True + False + This option provides immediate access to all changes, but updates may contain bugs that reduce system stability. Suitable for users who wish to help with testing new features. It is not recommended for stable or production systems. + True + 0 + + + + False + True + 1 + + + + + + + + False + True + 7 + + + + anchor True True @@ -3466,14 +4337,14 @@ False True - 30 + 8 True False - Template repositories + Check for qube updates 0 - - 0 - 0 - + + + False + True + 11 + + + + + True + False + center + 20 - + True - True - False - True - - - True - False - vertical - - - True - False - start - Official testing templates - - - - False - True - 0 - - - - - True - False - Templates that are still in testing may contain bugs. Recommended for testers only. - True - 0 - - - - False - True - 2 - - - - + False + start + center + 48 + qubes-info - 1 - 0 + False + True + 0 - + True - True - False - True - - - True - False - vertical - - - True - False - start - Community templates - - - - False - True - 0 - - - - - True - False - This repository contains templates maintained by the Qubes community. - True - 0 - - - - False - True - 2 - - - - - - - 0 - 1 - - - - - True - False - True - False - True - - - True - False - vertical - - - True - False - start - Community testing templates - - - - False - True - 0 - - - - - True - False - Templates that are still in testing may contain bugs. Recommended for testers only. - True - 0 - - - - False - True - 2 - - - - + False + start + The setting below will be applied only to <b>existing</b> qubes. New qubes have checking for updates <b>enabled</b> by default. + True + True - 1 - 1 + False + True + 1 - - - False - True - 33 - - - - - - - - - - 2 - - - - - True - False - - - True - False - usb-dark - - - False - True - 0 - - - - - True - False - Updates - - - - False - True - 1 - - - - - 2 - False - - - - - splitgpg - True - True - in - - - True - False - - - 5 - True - False - vertical - - - True - False - Split GPG - 0 False - True - 0 + False + 12 - + True - False - This feature protects your PGP keys by "splitting" GNU Privacy Guard (GnuPG or GPG) into two halves: one or more backend "key" qubes that securely store your PGP keys and one or more frontend "access" qubes with the ability to use those keys according to rules you specify. <a href="https://www.qubes-os.org/doc/split-gpg/">Learn more.</a> - True - True - 0 + True + False + True + True + + + True + False + <b>Enable</b> checking for updates for all existing qubes + True + + + False True - 1 + 13 - - False - vertical - - - False - Custom policy changes found. The changes below may be overwritten by existing custom policy changes. View the files listed below to verify the existing policy. - True - - - False - True - 0 - - + + True + True + False + True + True + updates_enable_radio - + + True False - none - False + <b>Disable</b> checking for updates for all existing qubes + True - - False - True - 1 - False True - 3 + 14 - + + True False - vertical + + + False + True + 15 + + + + + True + True + False + 30 + True - + + True False - Some policy rules could not be parsed. They are correct but are too complicated for this tool to handle. These rules will be discarded on save. The following rules were affected: - True - - - False - True - 0 - - - - - False - none - False - - - - False - True - 1 - - - - - - False - True - 4 - - - - - True - True - False - True - True - - - True - False - vertical True False - start - <b>Disabled.</b> - True - + Except for the following qubes, for which checking for updates will be False @@ -3905,73 +4537,11 @@ - - True - False - start - Each instance of GPG in a qube is confined to that qube. - True - - - - False - True - 1 - - - - - - - - False - True - 5 - - - - - True - True - False - True - splitgpg_disable_radio - - - True - False - vertical - - + True False - start - <b>Enabled.</b> + <b>disabled</b> True - - - - False - True - 0 - - - - - True - False - start - Split GPG is enabled according to the rules below. - True - False @@ -3979,6 +4549,9 @@ 1 + - - + 20 False True - 1 + 0 - + + True False - 50 + 20 10 - - True - False - Add new key qube: - - - - - - False - True - 0 - - - - + True False True True - 24 - - - False - True - 2 - - - - - ADD + True True True + start + 35 none + + + True + False + 10 + + + True + False + qubes-icon-add + + + False + True + 0 + + + + + True + False + 5 + Add exception + + + + False + True + 1 + + + + False True - 3 + 2 - False True - 2 + 1 + + + False + True + 18 + + + + + anchor + True + True + True + + + False + True + 19 + + + + + True + False + Update proxy + 0 + + + + False + True + 20 + + + + + True + False + Templates don't have direct network access. Instead, a service qube called an "update proxy" downloads updates for them. + True + 0 + + + + False + True + 21 + + + + + False + vertical - - True - True - True - start - 20 - 30 - none - - - True - False - 10 - - - True - False - qubes-add - - - False - True - 0 - - - - - True - False - Add New Key Qube - - - - False - True - 1 - - - - + + False + Custom policy changes found. The changes below may be overwritten by existing custom policy changes. View the files listed below to verify the existing policy. + True + + + False + True + 0 + + + + + False + none + False False True - 3 + 1 + + + + False + True + 22 + + + + + False + vertical - - True + False - 10 - vertical - - - True - False - start - <b>With the following exceptions:</b> - True - - - False - True - 0 - - - - - True - False - center - True - - - False - True - 1 - - + Some policy rules could not be parsed. They are correct but are too complicated for this tool to handle. These rules will be discarded on save. The following rules were affected: + True False True - 5 + 0 - - True + False - - - True - False - No exceptions - - - + none + False False True - 6 + 1 - - - True - True - True - start - 20 - 30 - none - - - True - False - 10 - - - True - False - qubes-add - - - False - True - 0 - - - - - True - False - Add Exception - - - - False - True - 1 - - - - - - - - False - True - 7 - - - - - + False True - 7 + 23 - + + True - True - True + False + 20 + 20 - + True False - - - 30 - 25 - True - False - 5 - 5 - 18 - qubes-expander-hidden + True + + + True + 24 + + + + + + + 1 + 0 + + + + + True + False + center + True + + + True + 24 + - - False - False - 0 - + + + + 1 + 1 + + + + + True + False + start + vertical True False - <b>View or edit</b> raw policy file for: + start + <b>Whonix</b> update proxy True + False True - 1 + 0 True False - qubes.Gpg + All Whonix qubes (those with the <tt>whonix-updatevm</tt> tag) will use this qube as an update proxy. This qube must be a Whonix gateway (e.g., <tt>sys-whonix</tt>). + True + True + 0 @@ -4431,302 +4904,215 @@ + + 0 + 1 + + + + + True + False + start + <b>Default</b> update proxy + True + + + + 0 + 0 + - False True - 8 + 24 - - + + True False + 20 + 10 + vertical - + True - True - 30 - 30 - 10 - 10 - True + False + start + <b>With the following exceptions:</b> + True + - 0 - 0 - 3 + False + True + 0 - - SAVE + True - True - True - end - none - + False + center + True - 2 - 1 + False + True + 1 + + + False + True + 25 + + + + + True + False + 10 + True - - CANCEL + True - True - True - end - none - + False + start + <b>QUBE</b> + True - 1 - 1 + False + True + 0 True False - True + start + <b>UPDATE PROXY</b> + True - 0 - 1 + False + True + 1 - False True - 9 - - - - - - - - - - 3 - - - - - True - False - - - True - False - usb-dark - - - False - True - 0 - - - - - True - False - Split GPG - - - - False - True - 1 - - - - - 3 - False - - - - - clipboard - True - True - in - - - True - False - - - True - False - vertical - 5 - - - anchor - True - True - True - - - False - True - 0 + 26 - + True False - Clipboard Shortcuts - 0 + + + True + False + No exceptions + + + False True - 1 + 27 - + + anchor True - False - Qubes OS features a secure "inter-qube" or "global" clipboard that allows you to copy and paste between qubes while preventing any qube other than your selected target from stealing content from the clipboard. Without such a system, any content copied to the global clipboard, such as a password, would instantly be exposed to every other running qube, including qubes you don't trust. By giving you precise control over exactly which qube receives inter-qube clipboard content, then immediately wiping the inter-qube clipboard afterward, Qubes OS protects the confidentiality of the text being copied. - -Inter-qube copy and paste actions are performed via special keyboard shortcuts, as specified below. These keyboard shortcuts are always intercepted by dom0 (so that rogue qubes can't perform global copy/paste actions on their own). - -<b>Note:</b> Changes below require a qube restart to take effect. - True - True - 0 - + True + True False True - 2 + 29 - + True - False - 20 - - - True - False - <b>Copy</b> shortcut: - True - True - - - - False - True - 0 - - - - - True - False - 5 - - - False - True - 1 - - - - - True - False - 40 - <b>Paste</b> shortcut: - True - True - - - - False - True - 2 - - + True + True + start + none - + True False - 5 + 10 + + + True + False + qubes-icon-add + + + False + True + 0 + + + + + True + False + Add Exception + + + + False + True + 1 + + - - False - True - 3 - + False True - 3 - - - - - anchor - True - True - True - - - False - True - 4 + 29 True False - Clipboard Policy + Template repositories 0 - False - True - 1 - - - - - - False - True - 7 - - - - - False - vertical - - - False - Some policy rules could not be parsed. They are correct but are too complicated for this tool to handle. These rules will be discarded on save. The following rules were affected: - True - - - False - True - 0 + 0 + 0 - - False - none - False - + + True + True + False + True + + + True + False + vertical + + + True + False + start + Official testing templates + + + + False + True + 0 + + + + + True + False + Templates that are still in testing may contain bugs. Recommended for testers only. + True + 0 + + + + False + True + 2 + + + + - False - True - 1 + 1 + 0 - - - - False - True - 8 - - - - - True - True - False - True - True - + True - False - vertical + True + False + True - + True False - start - <b>Default policy.</b> - True - + vertical + + + True + False + start + Community templates + + + + False + True + 0 + + + + + True + False + This repository contains templates maintained by the Qubes community. + True + 0 + + + + False + True + 2 + + - - False - True - 0 - + + + 0 + 1 + + + + + True + False + True + False + True - + True False - start - Allow any qube to copy/paste into any other qube, except dom0. - True - + vertical + + + True + False + start + Community testing templates + + + + False + True + 0 + + + + + True + False + Templates that are still in testing may contain bugs. Recommended for testers only. + True + 0 + + + + False + True + 2 + + - - False - True - 1 - + + 1 + 1 + - False True - 9 + 32 - - - True - True - False - True - True - clipboard_disable_radio - - - True - False - vertical - + + + + + + + + 3 + + + + + True + False + + + True + False + usb-dark + + + False + True + 0 + + + + + True + False + Updates + + + + False + True + 1 + + + + + 3 + False + + + + + splitgpg + True + True + in + + + True + False + + + 5 + True + False + vertical + + + True + False + Split GPG + 0 + + + + False + True + 0 + + + + + True + False + This feature protects your PGP keys by "splitting" GNU Privacy Guard (GnuPG or GPG) into two halves: one or more backend "key" qubes that securely store your PGP keys and one or more frontend "access" qubes with the ability to use those keys according to rules you specify. <a href="https://www.qubes-os.org/doc/split-gpg/">Learn more.</a> + True + True + 0 + + + + False + True + 1 + + + + + False + vertical + + + False + Custom policy changes found. The changes below may be overwritten by existing custom policy changes. View the files listed below to verify the existing policy. + True + + + False + True + 0 + + + + + False + none + False + + + + False + True + 1 + + + + + + False + True + 3 + + + + + False + vertical + + + False + Some policy rules could not be parsed. They are correct but are too complicated for this tool to handle. These rules will be discarded on save. The following rules were affected: + True + + + False + True + 0 + + + + + False + none + False + + + + False + True + 1 + + + + + + False + True + 4 + + + + + True + True + False + True + True + + + True + False + vertical + True False start - <b>Custom policy.</b> + <b>Disabled.</b> True False @@ -4986,8 +5606,11 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, True False start - <b>PERMISSION</b> - True + Split GPG is enabled according to the rules below. + True + False @@ -4995,35 +5618,124 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, 1 + + + + + + False + True + 6 + + + + + True + False + 50 + 15 + vertical + + + True + False + True True False start - <b>DESTI NATION QUBE</b> + <b>ACCESS QUBE</b> True False True - 2 + 0 - - - False - True + + + True + False + start + <b>PERMISSION</b> + True + + + False + True + 1 + + + + + True + False + + + True + False + 5 + 5 + qubes-key + + + False + True + 0 + + + + + True + False + start + <b>KEY QUBE</b> + True + + + False + True + 1 + + + + + False + True + 2 + + + + + False + True 0 - + True False 10 none + + + True + False + No key qubes + + + @@ -5032,6 +5744,149 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, 1 + + + False + 50 + 10 + + + True + False + Add new key qube: + + + + + + False + True + 0 + + + + + True + False + True + + + True + 24 + + + + + + False + True + 1 + + + + + CANCEL + True + True + True + none + + + + False + True + 2 + + + + + ADD + True + True + True + none + + + + False + True + 3 + + + + + + False + True + 2 + + + + + True + True + True + start + 20 + 30 + none + + + True + False + 10 + + + True + False + qubes-icon-add + + + False + True + 0 + + + + + True + False + Add New Key Qube + + + + False + True + 1 + + + + + + + + False + True + 3 + + True @@ -5069,15 +5924,13 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 2 + 5 - + True False - 10 - none True @@ -5085,21 +5938,23 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, No exceptions False True - 3 + 6 - + True True True @@ -5116,7 +5971,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, True False - qubes-add + qubes-icon-add False @@ -5149,18 +6004,21 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 4 + 7 + + + False True - 11 + 7 - + True True True @@ -5169,7 +6027,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, True False - + 30 25 True @@ -5186,7 +6044,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - + True False <b>View or edit</b> raw policy file for: @@ -5202,7 +6060,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, True False - qubes.ClipboardPaste + qubes.Gpg @@ -5222,15 +6080,15 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 12 + 8 - + False - + True True 30 @@ -5246,12 +6104,13 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - + SAVE True True True end + none @@ -5352,8 +6212,8 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - - file + + clipboard True True in @@ -5362,13 +6222,13 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, True False - + True False vertical 5 - + anchor True True @@ -5384,11 +6244,11 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, True False - 30 - Moving and Copying Files + Clipboard Shortcuts 0 @@ -5401,7 +6261,11 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, True False - Control access for moving and copying files between qubes using <tt>qvm-copy</tt>, <tt>qvm-move</tt>, and file manager actions such as <b>Copy/Move to other qube</b>. + Qubes OS features a secure "inter-qube" or "global" clipboard that allows you to copy and paste between qubes while preventing any qube other than your selected target from stealing content from the clipboard. Without such a system, any content copied to the global clipboard, such as a password, would instantly be exposed to every other running qube, including qubes you don't trust. By giving you precise control over exactly which qube receives inter-qube clipboard content, then immediately wiping the inter-qube clipboard afterward, Qubes OS protects the confidentiality of the text being copied. + +Inter-qube copy and paste actions are performed via special keyboard shortcuts, as specified below. These keyboard shortcuts are always intercepted by dom0 (so that rogue qubes can't perform global copy/paste actions on their own). + +<b>Note:</b> Changes below require a qube restart to take effect. True True 0 @@ -5416,11 +6280,129 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - + + True + False + 20 + + + True + False + <b>Copy</b> shortcut: + True + True + + + + False + True + 0 + + + + + True + False + 5 + + + False + True + 1 + + + + + True + False + 40 + <b>Paste</b> shortcut: + True + True + + + + False + True + 2 + + + + + True + False + 5 + + + False + True + 3 + + + + + False + True + 3 + + + + + anchor + True + True + True + + + False + True + 4 + + + + + True + False + Clipboard Policy + 0 + + + + False + True + 5 + + + + + True + False + Prevent accidental errors when using the inter-qube (global) clipboard. + True + 0 + + + + False + True + 6 + + + + False vertical - + False Custom policy changes found. The changes below may be overwritten by existing custom policy changes. View the files listed below to verify the existing policy. True @@ -5432,7 +6414,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - + False none False @@ -5453,15 +6435,15 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 3 + 7 - + False vertical - + False Some policy rules could not be parsed. They are correct but are too complicated for this tool to handle. These rules will be discarded on save. The following rules were affected: True @@ -5473,7 +6455,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - + False none False @@ -5494,11 +6476,11 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 4 + 8 - + True True False @@ -5514,7 +6496,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, True False start - <b>Default policy</b> + <b>Default policy.</b> True @@ -5610,11 +6593,11 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 6 + 10 - + True False 50 @@ -5675,7 +6658,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - + True False 10 @@ -5731,7 +6714,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - + True False 10 @@ -5743,11 +6726,13 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, No exceptions @@ -5757,7 +6742,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - + True True True @@ -5774,7 +6759,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, True False - qubes-add + qubes-icon-add False @@ -5814,11 +6799,11 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 7 + 11 - + True True True @@ -5827,7 +6812,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, True False - + 30 25 True @@ -5844,7 +6829,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - + True False <b>View or edit</b> raw policy file for: @@ -5860,7 +6845,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, True False - qubes.Filecopy + qubes.ClipboardPaste @@ -5880,15 +6865,15 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 8 + 12 - + False - + True True 30 @@ -5904,13 +6889,12 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - + SAVE True True True end - none - - - False - True - 12 + 14 - - - True - False - This policy governs Qubes OS behavior when using <tt>qvm-open-in-dvm</tt> with file arguments on the command line and <b>Edit/View in disposable</b> in file managers. - True - True + + + + + + + + 5 + + + + + True + False + + + True + False + usb-dark + + + False + True + 0 + + + + + True + False + Clipboard + + + + False + True + 1 + + + + + 5 + False + + + + + file + True + True + in + + + True + False + + + True + False + vertical + 5 + + + anchor + True + True + True + + + False + True + 0 + + + + + True + False + 30 + Moving and Copying Files 0 False True - 13 + 1 - + True False - center - 20 - 20 - - - True - False - start - center - 48 - qubes-info - - - False - True - 0 - - - - - True - True - start - To learn more about the "Open in VM" policy and its uses, please see the <a href="https://www.qubes-os.org/doc/">online documentation</a>. - True - True - - - False - True - 1 - - + Control access for moving and copying files between qubes using <tt>qvm-copy</tt>, <tt>qvm-move</tt>, and file manager actions such as <b>Copy/Move to other qube</b>. + True + True + 0 False - False - 14 + True + 2 - + False vertical - + False Custom policy changes found. The changes below may be overwritten by existing custom policy changes. View the files listed below to verify the existing policy. True @@ -6086,7 +7075,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - + False none False @@ -6107,15 +7096,15 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 15 + 3 - + False vertical - + False Some policy rules could not be parsed. They are correct but are too complicated for this tool to handle. These rules will be discarded on save. The following rules were affected: True @@ -6127,7 +7116,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - + False none False @@ -6148,156 +7137,90 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 16 + 4 - + True - False - 10 + True + False + True + True - + True False - Current Default Disposable Template: - True - + vertical + + + True + False + start + <b>Default policy</b> + True + + + + False + True + 0 + + + + + True + False + start + You will always be asked for permission when copying and moving files between qubes, except for dom0, which is handled differently. <a href="https://www.qubes-os.org/doc/how-to-copy-from-dom0/">Learn about copying to and from dom0.</a> + True + True + + + + False + True + 1 + + - - False - True - 0 - - - - - - - False - True - 17 - - - - - True - False - start - <b>Exceptions:</b> - True False True - 18 + 5 - + True - False - 30 - 10 - True - - - True - False - start - <b>QUBE</b> - True - - - False - True - 0 - - + True + False + True + filecopy_disable_radio - + True False - - - False - True - 1 - - - - - True - False - start - <b>DISPOSABLE QUBE TEMPLATE</b> - True - True - - - False - True - 2 - - - - - False - True - 19 - - - - - True - False - 30 - - - True - False - No exceptions - - - - - - - False - True - 20 - - - - - True - True - True - start - 20 - 30 - none - - - True - False - 10 + vertical - + True False - qubes-add + start + <b>Custom policy</b> + True + False @@ -6309,9 +7232,10 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, True False - Add Exception + start + As specified below: @@ -6323,47 +7247,47 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 21 + 6 - + True - True - True + False + 50 + 15 + vertical True False + True - - 30 - 25 + True False - 5 - 5 - 18 - qubes-expander-hidden + start + <b>ORIGIN QUBE</b> + True False - False + True 0 - + True False - <b>View or edit</b> raw policy file for: + start + <b>PERMISSION</b> True @@ -6376,10 +7300,9 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, True False - qubes.OpenInVM - + start + <b>DESTINATION QUBE</b> + True False @@ -6388,260 +7311,427 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - - - - - False - True - 23 - - - - - - False - - - True - True - 30 - 30 - 10 - 10 - True - - 0 - 0 - 3 + False + True + 0 - - SAVE + True - True - True - end - none + False + 10 + none - 2 - 1 + False + True + 1 - - CANCEL + True - True - True - end - none - + False + 10 + vertical + + + True + False + start + <b>With the following exceptions:</b> + True + + + False + True + 0 + + + + + True + False + center + True + + + False + True + 1 + + - 1 - 1 + False + True + 2 - + True False - True + 10 + none + + + True + False + No exceptions + + + + - 0 - 1 + False + True + 3 + + + + + True + True + True + start + 20 + 30 + none + + + True + False + 10 + + + True + False + qubes-icon-add + + + False + True + 0 + + + + + True + False + Add Exception + + + + False + True + 1 + + + + + + + + False + True + 4 - - - - False - True - 24 - - - - - - - - - - 5 - - - - - True - False - - - True - False - usb-dark - - - False - True - 0 - - - - - True - False - File Access - - - - False - True - 1 - - - - - 5 - False - - - - - url - True - True - in - - - True - False - - - True - False - vertical - 5 - - - True - False - Open URL in Disposable - 0 - False True - 0 + 7 - + True - False - This policy governs Qubes OS behavior when using <tt>qvm-open-in-dvm</tt> with URL arguments on the command line. - True - True - 0 + True + True + + + True + False + + + 30 + 25 + True + False + 5 + 5 + 18 + qubes-expander-hidden + + + False + False + 0 + + + + + True + False + <b>View or edit</b> raw policy file for: + True + + + False + True + 1 + + + + + True + False + qubes.Filecopy + + + + False + True + 2 + + + + False True - 1 + 8 - - True + + False - center - 20 - 20 - + True - False - start - center - 48 - qubes-info + True + 30 + 30 + 10 + 10 + True - False - True - 0 + 0 + 0 + 3 - + + SAVE True - False - To learn more about the "Open URL" policy and its uses, please see the <a href="https://www.qubes-os.org/doc/how-to-use-disposables/">online documentation</a>. - True - True + True + True + end + none + - False - True - 1 + 2 + 1 - - - - False - False - 2 - - - - - False - vertical - - False - Custom policy changes found. The changes below may be overwritten by existing custom policy changes. View the files listed below to verify the existing policy. - True - - - False + + CANCEL + True + True + True + end + none + + + + 1 + 1 + + + + + True + False + True + + + 0 + 1 + + + + + + False + True + 9 + + + + + True + False + 50 + + + False + True + 10 + + + + + anchor + True + True + True + + + False + True + 11 + + + + + True + False + Open in Disposable Qube + 0 + + + + False + True + 12 + + + + + True + False + This policy governs Qubes OS behavior when using <tt>qvm-open-in-dvm</tt> with file arguments on the command line and <b>Edit/View in disposable</b> in file managers. + True + True + 0 + + + + False + True + 13 + + + + + True + False + center + 20 + 20 + + + True + False + start + center + 48 + qubes-info + + + False True 0 - + + True + True + start + To learn more about the "Open in VM" policy and its uses, please see the <a href="https://www.qubes-os.org/doc/">online documentation</a>. + True + True + + + False + True + 1 + + + + + + False + False + 14 + + + + + False + vertical + + + False + Custom policy changes found. The changes below may be overwritten by existing custom policy changes. View the files listed below to verify the existing policy. + True + + + False + True + 0 + + + + False none False @@ -6662,15 +7752,15 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 3 + 15 - + False vertical - + False Some policy rules could not be parsed. They are correct but are too complicated for this tool to handle. These rules will be discarded on save. The following rules were affected: True @@ -6682,7 +7772,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - + False none False @@ -6703,11 +7793,11 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 4 + 16 - + True False 10 @@ -6717,6 +7807,9 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False Current Default Disposable Template: True + False @@ -6731,49 +7824,24 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 5 + 17 - + True False - 10 - 20 - 10 - vertical - - - True - False - start - <b>Exceptions:</b> - True - - - False - True - 0 - - - - - True - False - center - True - - - False - True - 1 - - + start + <b>Exceptions:</b> + True + False True - 6 + 18 @@ -6827,11 +7895,11 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 7 + 19 - + True False 30 @@ -6848,16 +7916,17 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 8 + 20 - + True True True @@ -6874,7 +7943,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, True False - qubes-add + qubes-icon-add False @@ -6907,11 +7976,11 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 9 + 21 - + True True True @@ -6920,7 +7989,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, True False - + 30 25 True @@ -6937,7 +8006,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - + True False <b>View or edit</b> raw policy file for: @@ -6953,7 +8022,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, True False - qubes.OpenURL + qubes.OpenInVM @@ -6973,15 +8042,15 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 11 + 23 - + False - + True True 30 @@ -6997,7 +8066,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - + SAVE True True @@ -7016,7 +8085,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - + CANCEL True True @@ -7052,7 +8121,7 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, False True - 12 + 24 @@ -7105,332 +8174,354 @@ Inter-qube copy and paste actions are performed via special keyboard shortcuts, - - thisdevice + + url True True - never in True False - - + True False - - - True - False - 250 - qubes-this-device - - - 0 - 1 - 3 - - - - - True - False - start - data - True - - - 1 - 1 - 2 - 2 - - - - - True - False - True - - - - 0 - 7 - 3 - - - - - Copy system information -to global clipboard - copy_button - True - True - True - center - end - - - 1 - 3 - - + vertical + 5 True False - start - Device Information + Open URL in Disposable + 0 - 0 - 0 - 3 - - - - - Copy HCL report -to global clipboard - copy_hcl_button - True - True - True - center - end - 30 - - - 2 - 3 + False + True + 0 True False - start - Qubes OS Security Report + This policy governs Qubes OS behavior when using <tt>qvm-open-in-dvm</tt> with URL arguments on the command line. + True + True + 0 - 0 - 4 - 3 + False + True + 1 - - + True False - start - 10 - 10 + center + 20 + 20 - + True False start - HVM: + center + 48 + qubes-info - 1 - 0 + False + True + 0 - + True False - gtk-missing-image + To learn more about the "Open URL" policy and its uses, please see the <a href="https://www.qubes-os.org/doc/how-to-use-disposables/">online documentation</a>. + True + True - 0 - 0 + False + True + 1 + + + + False + False + 2 + + + + + False + vertical - - True + False - start - I/O MMU: + Custom policy changes found. The changes below may be overwritten by existing custom policy changes. View the files listed below to verify the existing policy. + True - 1 - 1 + False + True + 0 - - True + False - gtk-missing-image + none + False + - 0 - 1 + False + True + 1 + + + + False + True + 3 + + + + + False + vertical - - True + False - start - HAP/SLAT: + Some policy rules could not be parsed. They are correct but are too complicated for this tool to handle. These rules will be discarded on save. The following rules were affected: + True - 1 - 2 + False + True + 0 - - True + False - start - TPM: + none + False + - 1 - 3 + False + True + 1 + + + + False + True + 4 + + + + + True + False + 10 - + True False - start - Remapping: + Current Default Disposable Template: + True - 1 - 4 + False + True + 0 - - True - False - gtk-missing-image - - - 0 - 2 - + - - - True + + + False + True + 5 + + + + + True + False + 10 + 20 + 10 + vertical + + + True False - gtk-missing-image + start + <b>Exceptions:</b> + True - 0 - 3 + False + True + 0 - + True False - gtk-missing-image + center + True - 0 - 4 + False + True + 1 + + + False + True + 6 + + + + + True + False + 30 + 10 + True - + True False - gtk-missing-image + start + <b>QUBE</b> + True - 0 - 5 + False + True + 0 - + True False - gtk-missing-image - 0 - 6 + False + True + 1 - + True False - center - - - True - False - center - 20 - 20 - 64 - check_yes - 6 - - - False - True - 0 - - - - - True - True - <b>This device is officially certified for your current Qubes OS release.</b> -<a href="https://www.qubes-os.org/doc/certified-hardware/">Learn more about Qubes-certified hardware.</a> - True - center - True - - - False - True - 1 - - - + start + <b>DISPOSABLE QUBE TEMPLATE</b> + True + True - 2 - 0 - 7 + False + True + 2 + + + False + True + 7 + + + + + True + False + 30 + + + True + False + No exceptions + + + + + + + False + True + 8 + + + + + True + True + True + start + 20 + 30 + none True False + 10 - + True False - start - USB keyboards: + qubes-icon-add False @@ -7439,95 +8530,1239 @@ to global clipboard - + True False - Granting a USB keyboard unconditional access can be dangerous. You can check your policy settings on the USB Devices page. - 10 - 0.5 - 0.6000000238418579 - 20 - qubes-question + Add Exception + False True - 3 1 - - 1 - 5 - + + + + False + True + 9 + + + + + True + True + True True False - + + 30 + 25 True False - start - PV qubes: + 5 + 5 + 18 + qubes-expander-hidden False - True + False 0 - + True False - Granting a USB keyboard unconditional access can be dangerous. You can check your policy settings on the USB Devices page. - 10 - 0.5 - 0.6000000238418579 - 20 - qubes-question + <b>View or edit</b> raw policy file for: + True False True - 3 1 + + + True + False + qubes.OpenURL + + + + False + True + 2 + + - - 1 - 6 - + + + False + True + 11 + + + + + + False + + + True + True + 30 + 30 + 10 + 10 + True + + + 0 + 0 + 3 + + + + + SAVE + True + True + True + end + none + + + + 2 + 1 + + + + + CANCEL + True + True + True + end + none + + + + 1 + 1 + + + + + True + False + True + + + 0 + 1 + + + + + + False + True + 12 + + + + + + + + + + 7 + + + + + True + False + + + True + False + usb-dark + + + False + True + 0 + + + + + True + False + URL Handling + + + + False + True + 1 + + + + + 7 + False + + + + + thisdevice + True + True + never + in + + + True + False + + + + True + False + + + True + False + 250 + qubes-this-device + + + 0 + 1 + 3 + + + + + True + False + start + data + True + + + 1 + 1 + 2 + 2 + + + + + True + False + True + + + + 0 + 7 + 3 + + + + + Copy system information +to global clipboard + copy_button + True + True + True + center + end + + + 1 + 3 + + + + + True + False + start + Device Information + + + + 0 + 0 + 3 + + + + + Copy HCL report +to global clipboard + copy_hcl_button + True + True + True + center + end + 30 + + + 2 + 3 + + + + + True + False + start + Qubes OS Security Report + + + + 0 + 4 + 3 + + + + + + True + False + start + 10 + 10 + + + True + False + start + HVM: + + + 1 + 0 + + + + + True + False + gtk-missing-image + + + 0 + 0 + + + + + True + False + start + I/O MMU: + + + 1 + 1 + + + + + True + False + gtk-missing-image + + + 0 + 1 + + + + + True + False + start + HAP/SLAT: + + + 1 + 2 + + + + + True + False + start + TPM: + + + 1 + 3 + + + + + True + False + start + Remapping: + + + 1 + 4 + + + + + True + False + gtk-missing-image + + + 0 + 2 + + + + + True + False + gtk-missing-image + + + 0 + 3 + + + + + True + False + gtk-missing-image + + + 0 + 4 + + + + + True + False + gtk-missing-image + + + 0 + 5 + + + + + True + False + gtk-missing-image + + + 0 + 6 + + + + + True + False + center + + + True + False + center + 20 + 20 + 64 + check_yes + 6 + + + False + True + 0 + + + + + True + True + <b>This device is officially certified for your current Qubes OS release.</b> +<a href="https://www.qubes-os.org/doc/certified-hardware/">Learn more about Qubes-certified hardware.</a> + True + center + True + + + False + True + 1 + + + + + + 2 + 0 + 7 + + + + + True + False + + + True + False + start + USB keyboards: + + + False + True + 0 + + + + + True + False + Granting a USB keyboard unconditional access can be dangerous. You can check your policy settings on the USB Devices page. + 10 + 0.5 + 0.6000000238418579 + 20 + qubes-question + + + False + True + 3 + 1 + + + + + 1 + 5 + + + + + True + False + + + True + False + start + PV qubes: + + + False + True + 0 + + + + + True + False + Granting a USB keyboard unconditional access can be dangerous. You can check your policy settings on the USB Devices page. + 10 + 0.5 + 0.6000000238418579 + 20 + qubes-question + + + False + True + 3 + 1 + + + + + 1 + 6 + + + + + + 0 + 5 + 3 + + + + + True + False + center + 20 + 20 + + + True + False + start + center + 48 + qubes-info + + + False + True + 0 + + + + + True + True + To learn more about the Qubes OS Security Report, please see the <a href="https://www.qubes-os.org/doc/">online documentation</a>. + True + True + + + False + True + 1 + + + + + + 0 + 6 + 3 + + + + + + + + + + 8 + + + + + True + False + + + True + False + usb-dark + + + False + True + 0 + + + + + True + False + This Device + + + + False + True + 1 + + + + + 8 + False + + + + + + logo_grid + True + False + start + center + 10 + 30 + 10 + + + + 0 + 0 + + + + + True + False + center + 10 + Global Config + center + + + + 1 + 0 + + + + + False + + + + + False + True + 0 + + + + + True + False + end + 20 + 20 + 20 + 20 + 10 + True + + + _Apply Changes and Close + True + True + True + True + + + + False + True + 1 + + + + + _Apply Changes + True + True + True + True + + + + False + True + 2 + + + + + _Cancel + True + True + True + True + + + + False + True + 2 + + + + + False + True + 1 + + + + + + + False + True + dialog + + + False + vertical + 2 + + + False + end + + + _Save and Close + True + True + True + none + True + + + + False + True + 0 + + + + + _Cancel + True + True + True + none + True + + + + False + True + 1 + + + + + False + False + 0 + + + + + True + True + in + True + True + + + True + False + + + True + False + vertical + + + True + False + start + Device + + + + False + True + 0 + + + + + True + False + 10 + + + True + False + center + True + Device: + 0 + + + + False + True + 0 + + + + + True + False + + + + False + True + 2 + + + + + False + False + 1 + + + + + True + False + Select available PCI or block device. + True + 0 + + + + False + True + 2 + + + + + True + False + True + center + 20 + 20 + + + True + False + start + center + 48 + qubes-unplug + + + False + True + 0 + + + + + True + False + start + This device is not currently connected. The assignment cannot be modified. + True + True + + + False + True + 1 + + + + + + False + False + 3 + + + + + True + False + start + 2 + True + Device class: +Other description: + 0 + + + + False + True + 4 + + + + + True + False + start + Options + + + + False + True + 5 + + + + + True + False + <b>No strict reset</b> and <b>permissive</b> options should only be used in case of compatibility problems. They can lead to security issues. + True + True + 0 + + + + False + True + 6 + + + + + No strict reset + True + True + False + True + + + + False + True + 7 + + + + + Permissive + True + True + False + True + + + + False + True + 8 + + + + + Read-only + True + True + False + True + + + + False + True + 9 + + + + + Require port (device will match only when plugged into the currently used port) + True + True + False + True + + + + False + True + 10 + + + + + True + False + start + Qubes + + + + False + True + 11 + + + + + True + False + vertical + + + True + False + 40 + - 0 - 5 - 3 + False + True + 0 - + True False - center - 20 - 20 + 5 + 10 - + True False - start center - 48 - qubes-info + 5 + 5 + True + False @@ -7536,12 +9771,51 @@ to global clipboard - + True True - To learn more about the Qubes OS Security Report, please see the <a href="https://www.qubes-os.org/doc/">online documentation</a>. - True - True + True + start + none + + + True + False + 10 + + + True + False + qubes-icon-add + + + False + True + 0 + + + + + True + False + 5 + Add qube + + + + False + True + 1 + + + + + False @@ -7549,186 +9823,44 @@ to global clipboard 1 - - 0 - 6 - 3 + False + True + 1 + + + False + True + 13 + + + + + False + A device cannot be attached to its own backend qube. + True + True + 0 + + False + True + 14 + - - - - - 7 - - - - - True - False - - - True - False - usb-dark - - - False - True - 0 - - - - - True - False - This Device - - - - False - True - 1 - - - - - 7 - False - - - - - - logo_grid - True - False - start - center - 10 - 30 - 10 - - - - 0 - 0 - - - - - True - False - center - 10 - Global Config - center - - 1 - 0 - - - False - - - - - False - True - 0 - - - - - True - False - end - 20 - 20 - 20 - 20 - 10 - True - - - _Apply Changes and Close - True - True - True - True - - - - False - True - 1 - - - - - _Apply Changes - True - True - True - True - - - - False - True - 2 - - - - - _Cancel - True - True - True - True - - - - False - True - 2 - diff --git a/qubes_config/global_config/basics_handler.py b/qubes_config/global_config/basics_handler.py index aca51640..836945bf 100644 --- a/qubes_config/global_config/basics_handler.py +++ b/qubes_config/global_config/basics_handler.py @@ -162,7 +162,7 @@ def update_current_value(self): new_value = self.model.get_selected() setattr(self.trait_holder, self.trait_name, new_value) - def get_model(self) -> TraitSelector: + def get_model(self) -> VMListModeler: return self.model @@ -208,6 +208,99 @@ def get_model(self) -> TraitSelector: return self.model +class PreloadDispvmHandler(AbstractTraitHolder): + """Handler for preloaded disposables. Requires SpinButton widgets: + 'basics_preload_dispvm'""" + + def __init__( + self, + qapp: qubesadmin.Qubes, + widget: Gtk.SpinButton, + defdispvm_model: VMListModeler, + is_dvm_template: Optional[Callable] = None, + ): + self.qapp = qapp + self.widget = widget + self.defdispvm_model = defdispvm_model + self.defdispvm_model.connect_change_callback(self.on_defdispvm_changed) + self.is_dvm_template = is_dvm_template + self.preload_dispvm_adjustment = Gtk.Adjustment() + self.preload_dispvm_adjustment.configure(0, 0, 50, 1, 10, 0) + self.widget.configure(self.preload_dispvm_adjustment, 0.1, 0) + self.widget.set_value(self.get_current_value()) + self.widget.set_sensitive(bool(self.get_defdispvm_value())) + + def get_defdispvm_value(self): + return self.defdispvm_model.get_selected() + + def on_defdispvm_changed(self): + value = self.get_defdispvm_value() + if value: + self.widget.set_sensitive(True) + self.widget.set_value(self.get_current_value()) + else: + self.widget.set_value(0) + self.widget.set_sensitive(False) + + @staticmethod + def get_readable_description() -> str: # pylint: disable=arguments-differ + """Get human-readable description of the widget""" + # the pylint: disable above is because pylint does not understand + # static methods + return _("Number of preloaded disposables from default dispvm") + + def save(self): + """Save changes: update system value and mark it as new initial value""" + if not self.is_changed(): + return + if not self.get_defdispvm_value(): + return + apply_feature_change( + self.qapp.domains["dom0"], + "preload-dispvm-max", + int(self.widget.get_value()), + ) + + def reset(self): + """Reset selection to the initial value.""" + if not self.widget.is_sensitive(): + return + self.widget.set_value(self.get_current_value()) + + def is_changed(self) -> bool: + """Has the user selected something different from the initial value?""" + if not self.widget.is_sensitive(): + return False + if int(self.widget.get_value()) != self.get_current_value(): + return True + return False + + def get_unsaved(self): + """Get human-readable description of unsaved changes, or + empty string if none were found.""" + if self.is_changed(): + return self.get_readable_description() + return "" + + def get_current_value(self): + """This should never be called.""" + if not self.get_defdispvm_value(): + return 0 + if not self.is_dvm_template: + return 0 + return int( + get_feature(self.qapp.domains["dom0"], "preload-dispvm-max") or 0 + ) + + def update_current_value(self): + """This should never be called.""" + raise NotImplementedError + + def get_model(self) -> TraitSelector: + """This should never be called.""" + raise NotImplementedError + + class QMemManHelper: """Helper class to handle the ugliness of managing qmemman config.""" @@ -238,9 +331,7 @@ def save_values(self, values_dict: Dict[str, int]): """Wants a dict of 'vm-min-mem': value in MiB and 'dom0-mem-boost': value in MiB""" # qmemman settings - text_dict = { - key: str(int(value)) + "MiB" for key, value in values_dict.items() - } + text_dict = {key: str(int(value)) + "MiB" for key, value in values_dict.items()} assert ( len(text_dict) == 2 @@ -409,9 +500,7 @@ def __init__(self, qapp: qubesadmin.Qubes, widget: Gtk.ComboBoxText): ) def _get_kernel_options(self) -> Dict[str, str]: - kernels = [ - kernel.vid for kernel in self.qapp.pools["linux-kernel"].volumes - ] + kernels = [kernel.vid for kernel in self.qapp.pools["linux-kernel"].volumes] kernels = sorted(kernels, key=KernelVersion) kernels_dict = {kernel: kernel for kernel in kernels} kernels_dict["(none)"] = None @@ -456,6 +545,9 @@ def __init__(self, gtk_builder: Gtk.Builder, qapp: qubesadmin.Qubes): self.defdispvm_combo: Gtk.ComboBox = gtk_builder.get_object( "basics_defdispvm_combo" ) + self.preload_dispvm_spin: Gtk.SpinButton = gtk_builder.get_object( + "basics_preload_dispvm" + ) self.fullscreen_combo: Gtk.ComboBoxText = gtk_builder.get_object( "basics_fullscreen_combo" ) @@ -513,6 +605,17 @@ def __init__(self, gtk_builder: Gtk.Builder, qapp: qubesadmin.Qubes): additional_options=NONE_CATEGORY, ) ) + defdispvm_model: VMListModeler = self.handlers[ + -1 + ].get_model() # type: ignore + self.handlers.append( + PreloadDispvmHandler( + qapp=self.qapp, + widget=self.preload_dispvm_spin, + defdispvm_model=defdispvm_model, + is_dvm_template=self._default_dispvm_filter, + ) + ) self.handlers.append( FeatureHandler( trait_holder=self.vm, @@ -559,9 +662,7 @@ def __init__(self, gtk_builder: Gtk.Builder, qapp: qubesadmin.Qubes): is_bool=False, ) ) - self.handlers.append( - KernelHolder(qapp=self.qapp, widget=self.kernel_combo) - ) + self.handlers.append(KernelHolder(qapp=self.qapp, widget=self.kernel_combo)) self.handlers.append(MemoryHandler(gtk_builder)) diff --git a/qubes_config/global_config/conflict_handler.py b/qubes_config/global_config/conflict_handler.py index aecbc9b0..437e2df4 100644 --- a/qubes_config/global_config/conflict_handler.py +++ b/qubes_config/global_config/conflict_handler.py @@ -80,9 +80,7 @@ def __init__( self.own_file_name = own_file_name self.policy_manager = policy_manager - self.problem_box: Gtk.Box = gtk_builder.get_object( - f"{prefix}_problem_box" - ) + self.problem_box: Gtk.Box = gtk_builder.get_object(f"{prefix}_problem_box") self.problem_list: Gtk.ListBox = gtk_builder.get_object( f"{prefix}_problem_files_list" ) diff --git a/qubes_config/global_config/device_attachments.py b/qubes_config/global_config/device_attachments.py new file mode 100644 index 00000000..70451fea --- /dev/null +++ b/qubes_config/global_config/device_attachments.py @@ -0,0 +1,1076 @@ +# -*- encoding: utf8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2025 Marta Marczykowska-Górecka +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with this program; if not, see . +""" +Device attachment functionality. +""" +from typing import List, Optional, Tuple, Iterator, Callable + +from qubesadmin.device_protocol import ( + DeviceAssignment, + AssignmentMode, + DeviceInfo, + Port, + DeviceInterface, +) + +from ..widgets.gtk_widgets import TokenName +from ..widgets.gtk_utils import show_error +from .device_widgets import ( + DevPolicyRow, + DevPolicyDialogHandler, + DevicePolicyHandler, +) +from .page_handler import PageHandler +from .vm_flowbox import VMFlowboxHandler +from .device_blocks import DeviceBlockHandler + +import gi + +import qubesadmin +import qubesadmin.vm +import qubesadmin.exc + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Pango + +import gettext + +t = gettext.translation("desktop-linux-manager", fallback=True) +_ = t.gettext + +DEVICE_CLASSES = { + "block": _("Block device"), + "mic": _("Microphone"), + "pci": _("PCI device"), + "usb": _("USB device"), +} + + +class AutoDeviceDialog(DevPolicyDialogHandler): + """ + The handler for an Edit Device Rule dialog window. + """ + + DEV_CLASSES = { + "mic": _("Microphone"), + "usb": _("USB Devices"), + "block": _("Block Devices"), + } + + def __init__( + self, + builder: Gtk.Builder, + qapp, + parent_window: Gtk.Window, + device_manager: "DeviceManager", + ): + """ + Dialog for editing AutoAttachment options. + """ + super().__init__(builder, qapp, "edit_device", parent_window) + self.device_manager = device_manager + + self.dev_combo: Gtk.ComboBox = builder.get_object("edit_device_device_combo") + + self.backend_check: Gtk.CheckButton = builder.get_object( + "edit_device_backend_check" + ) + self.devclass_check: Gtk.CheckButton = builder.get_object( + "edit_device_devclass_check" + ) + self.devident_check: Gtk.CheckButton = builder.get_object( + "edit_device_devident_check" + ) + self.port_check: Gtk.CheckButton = builder.get_object("edit_device_port_check") + + self.devident_label: Gtk.Label = builder.get_object( + "edit_device_devident_label" + ) + + self.backend_box: Gtk.Box = builder.get_object("edit_device_backend_box") + self.backend_child: Optional[Gtk.Widget] = None # formatted qube name + + self.auto_radio: Gtk.RadioButton = builder.get_object("edit_device_auto_radio") + self.ask_radio: Gtk.RadioButton = builder.get_object("edit_device_ask_radio") + + self.unknown_box: Gtk.Box = builder.get_object("edit_device_unknown_box") + self.err_label: Gtk.Label = builder.get_object("edit_device_err_label") + self.read_only_check: Gtk.CheckButton = builder.get_object( + "edit_device_read_only" + ) + + self.qube_handler = VMFlowboxHandler( + builder, + self.qapp, + "edit_device", + [], + filter_function=lambda vm: vm.klass != "AdminVM", + ) + + self.backend_vm: Optional[qubesadmin.vm.QubesVM] = None + self.dev_modeler = self.fill_combo_with_devices( + self.DEV_CLASSES, self.dev_combo, self.device_manager, DeviceWrapper + ) + + self.devident_check.connect("toggled", self.validate) + self.port_check.connect("toggled", self.validate) + self.qube_handler.connect_change_callback(self.validate) + self.dev_combo.connect("changed", self._combo_changed) + + def fill_checkboxes( + self, + backend_domain: Optional[qubesadmin.vm.QubesVM], + devclass: str, + dev_identity: str, + port: str, + ident_checked: bool, + port_checked: bool, + read_only: Optional[bool] = False, + ): + """Fill all checkboxes with appropriate values""" + if self.backend_child: + self.backend_box.remove(self.backend_child) + self.backend_child = None + if backend_domain: + self.backend_child = TokenName(str(backend_domain), self.qapp) + self.backend_box.pack_start(self.backend_child, False, False, 5) + self.backend_box.show_all() + self.backend_vm = backend_domain + + self.devclass_check.set_label(_("Device class: ") + devclass) + + # dev identity + self.devident_label.set_text(dev_identity) + self.devident_check.set_sensitive(True) + self.devident_check.set_active(ident_checked) + + # port + self.port_check.set_label("Port: " + port) + self.port_check.set_sensitive(True) + self.port_check.set_active(port_checked) + + if devclass == "block": + self.read_only_check.set_sensitive(True) + self.read_only_check.set_active(read_only) + else: + self.read_only_check.set_sensitive(False) + self.read_only_check.set_active(False) + + def fill_checkboxes_from_device(self, device: "DeviceWrapper"): + """Fill checkboxes based on a DeviceInfo""" + self.fill_checkboxes( + backend_domain=device.backend_domain, + devclass=device.devclass, + dev_identity=device.identity_description, + port=str(device.port), + ident_checked=True, + port_checked=True, + ) + + def fill_checkboxes_from_assignment_wrapper( + self, assignment_wrapper: "AssignmentWrapper" + ): + """Fill checkboxes based on a set of assignments""" + if not assignment_wrapper.device.device: + self.fill_checkboxes_from_none() + return + self.fill_checkboxes( + backend_domain=assignment_wrapper.device.backend_domain, + devclass=assignment_wrapper.device.devclass, + dev_identity=assignment_wrapper.device_identity_description(), + port=str(assignment_wrapper.device.device.port), + ident_checked=assignment_wrapper.device_identity_required, + port_checked=assignment_wrapper.port_required, + read_only=assignment_wrapper.read_only, + ) + + def fill_checkboxes_from_none(self): + """Fill checkboxes for an empty device""" + if self.backend_child: + self.backend_box.remove(self.backend_child) + self.backend_child = None + self.backend_vm = None + + self.devclass_check.set_label(_("Device class:")) + + # dev identity + self.devident_label.set_text("") + self.devident_check.set_active(False) + self.devident_check.set_sensitive(False) + + # port + self.port_check.set_label("Port: ") + self.port_check.set_active(True) + self.port_check.set_sensitive(False) + + self.read_only_check.set_sensitive(False) + self.read_only_check.set_active(False) + + def check_validity(self): + """Check if dialog can be saved/ok-ed""" + self.err_label.set_visible(False) + if not self.qube_handler.selected_vms: + return False + if not self.port_check.get_active() and not self.devident_check.get_active(): + return False + for vm in self.qube_handler.selected_vms: + if vm == self.backend_vm: + self.err_label.set_visible(True) + return False + return True + + def _combo_changed(self, *_args): + device = self.dev_modeler.get_selected() + if device: + self.fill_checkboxes_from_device(device) + else: + self.fill_checkboxes_from_none() + + def _save_changes(self, *_args): + if not self.current_row: + self._cancel() + return + assignment_wrapper = self.current_row.assignment_wrapper + + # we need to always save currently selected device, to avoid the hell of + # finding it again + if self.dev_modeler.get_selected(): + if assignment_wrapper.device != self.dev_modeler.get_selected(): + assignment_wrapper.changed = True + assignment_wrapper.device = self.dev_modeler.get_selected() + + if ( + assignment_wrapper.device_identity_required + != self.devident_check.get_active() + ): + assignment_wrapper.changed = True + assignment_wrapper.device_identity_required = ( + self.devident_check.get_active() + ) + + if assignment_wrapper.port_required != self.port_check.get_active(): + assignment_wrapper.changed = True + assignment_wrapper.port_required = self.port_check.get_active() + + if self.port_check.get_active(): + if assignment_wrapper.port != self.dev_modeler.get_selected().port: + assignment_wrapper.changed = True + assignment_wrapper.port = self.dev_modeler.get_selected().port + if ( + self.auto_radio.get_active() + and assignment_wrapper.mode != AssignmentMode.AUTO + ): + assignment_wrapper.changed = True + assignment_wrapper.mode = AssignmentMode.AUTO + elif ( + self.ask_radio.get_active() + and assignment_wrapper.mode != AssignmentMode.ASK + ): + assignment_wrapper.changed = True + assignment_wrapper.mode = AssignmentMode.ASK + + if assignment_wrapper.read_only != self.read_only_check.get_active(): + assignment_wrapper.changed = True + assignment_wrapper.read_only = self.read_only_check.get_active() + + if sorted(assignment_wrapper.frontends) != sorted( + self.qube_handler.selected_vms + ): + assignment_wrapper.changed = True + assignment_wrapper.frontends = self.qube_handler.selected_vms.copy() + + super()._save_changes() + + def run_for_new(self, new_row_function: Callable) -> DevPolicyRow: + """Open dialog for a new assignment""" + self.dialog.set_title(_("Create New Device Assignment")) + self.fill_checkboxes_from_none() + self.dev_combo.set_active_id(None) + self.qube_handler.reset() + self.unknown_box.set_visible(False) + + self.auto_radio.set_active(True) + self.validate() + + new_row = super().run_for_new(new_row_function) + return new_row + + def run_for_existing(self, row: DevPolicyRow): + """Open dialog for an existing assignment""" + self.dialog.set_title(_("Edit Device Assignment")) + + # load data + self.dev_modeler.select_value(row.assignment_wrapper.device) + self.fill_checkboxes_from_assignment_wrapper(row.assignment_wrapper) + + if row.assignment_wrapper.mode == AssignmentMode.AUTO: + self.auto_radio.set_active(True) + else: + self.ask_radio.set_active(True) + + # fill qubes + self.qube_handler.reset() + for qube in row.assignment_wrapper.frontends: + self.qube_handler.add_selected_vm(qube) + + if not row.assignment_wrapper.device.device.is_device_id_set: + if row.assignment_wrapper.device_identity_required: + self.unknown_box.set_visible(True) + else: + # it looks weird to show "device unavailable" box for unknown + # device when device is not shown + self.unknown_box.set_visible(False) + self.dev_combo.set_sensitive(False) + self.port_check.set_sensitive(False) + self.devident_check.set_sensitive(False) + self.auto_radio.set_sensitive(False) + self.ask_radio.set_sensitive(False) + self.qube_handler.set_sensitive(False) + else: + self.unknown_box.set_visible(False) + self.dev_combo.set_sensitive(True) + self.port_check.set_sensitive(True) + self.devident_check.set_sensitive(True) + self.auto_radio.set_sensitive(True) + self.ask_radio.set_sensitive(True) + self.qube_handler.set_sensitive(True) + + super().run_for_existing(row) + + +class RequiredDeviceDialog(DevPolicyDialogHandler): + DEV_CLASSES = {"pci": _("PCI Devices"), "block": _("Block Devices")} + + def __init__( + self, + builder: Gtk.Builder, + qapp, + parent_window: Gtk.Window, + device_manager: "DeviceManager", + ): + """ + The handler for a Required Device Rule dialog window. + """ + super().__init__(builder, qapp, "required_device", parent_window) + self.device_manager = device_manager + + self.dev_combo: Gtk.ComboBox = builder.get_object( + "required_device_device_combo" + ) + self.devident_label: Gtk.Label = builder.get_object("required_device_devident") + + self.no_strict_check: Gtk.CheckButton = builder.get_object( + "required_device_nostrict_check" + ) + self.permissive_check: Gtk.CheckButton = builder.get_object( + "required_device_permissive_check" + ) + self.readonly_check: Gtk.CheckButton = builder.get_object( + "required_device_readonly_check" + ) + self.port_check: Gtk.CheckButton = builder.get_object( + "required_device_port_check" + ) + + self.unknown_box: Gtk.Box = builder.get_object("required_device_unknown_box") + self.err_label: Gtk.Label = builder.get_object("required_device_err_label") + + self.qube_handler = VMFlowboxHandler( + builder, + self.qapp, + "required_device", + [], + filter_function=lambda vm: vm.klass != "AdminVM", + ) + + self.dev_modeler = self.fill_combo_with_devices( + self.DEV_CLASSES, self.dev_combo, self.device_manager, DeviceWrapper + ) + + self.qube_handler.connect_change_callback(self.validate) + + self.dev_combo.connect("changed", self._combo_changed) + + def check_validity(self): + """Check if dialog can be saved/ok-ed""" + self.err_label.set_visible(False) + if not self.qube_handler.selected_vms: + return False + if not self.dev_modeler.get_selected(): + return False + backend_vm = self.dev_modeler.get_selected().backend_domain + for vm in self.qube_handler.selected_vms: + if vm == backend_vm: + self.err_label.set_visible(True) + return False + return True + + @staticmethod + def _init_check( + check_button: Gtk.CheckButton, + sensitive: bool, + selected: bool, + devclass_name: str, + ): + """Helper function to set state of checkbuttons""" + check_button.set_sensitive(sensitive) + check_button.set_active(selected) + if sensitive: + check_button.set_tooltip_text("") + else: + if not selected: + check_button.set_tooltip_text( + _("Not available for {0} devices").format(devclass_name) + ) + else: + check_button.set_tooltip_text( + _("Always enabled for {0} devices").format(devclass_name) + ) + + def _combo_changed(self, *_args): + device = self.dev_modeler.get_selected() + if device: + self.devident_label.set_text( + device.identity_description + "\nPort: " + device.port_id + ) + if device.devclass == "pci": + self._init_check(self.readonly_check, False, False, "PCI") + self._init_check(self.permissive_check, True, False, "PCI") + self._init_check(self.no_strict_check, True, False, "PCI") + self._init_check(self.port_check, False, True, "PCI") + elif device.devclass == "block": + self._init_check(self.readonly_check, True, False, "block") + self._init_check(self.permissive_check, False, False, "block") + self._init_check(self.no_strict_check, False, False, "block") + self._init_check(self.port_check, True, True, "block") + else: + self.devident_label.set_text("No device selected.") + self._init_check(self.readonly_check, False, False, "not selected") + self._init_check(self.permissive_check, False, False, "not selected") + self._init_check(self.no_strict_check, False, False, "not selected") + self._init_check(self.port_check, False, False, "not selected") + + def _save_changes(self, *_args): + if not self.current_row: + self._cancel() + return + assignment_wrapper = self.current_row.assignment_wrapper + + if assignment_wrapper.no_strict_reset != self.no_strict_check.get_active(): + assignment_wrapper.changed = True + assignment_wrapper.no_strict_reset = self.no_strict_check.get_active() + + if assignment_wrapper.permissive != self.permissive_check.get_active(): + assignment_wrapper.changed = True + assignment_wrapper.permissive = self.permissive_check.get_active() + + if assignment_wrapper.read_only != self.readonly_check.get_active(): + assignment_wrapper.changed = True + assignment_wrapper.read_only = self.readonly_check.get_active() + + if assignment_wrapper.device != self.dev_modeler.get_selected(): + assignment_wrapper.changed = True + assignment_wrapper.device = self.dev_modeler.get_selected() + + if assignment_wrapper.port_required != self.port_check.get_active(): + assignment_wrapper.changed = True + assignment_wrapper.port_required = self.port_check.get_active() + + if sorted(assignment_wrapper.frontends) != sorted( + self.qube_handler.selected_vms + ): + assignment_wrapper.changed = True + assignment_wrapper.frontends = self.qube_handler.selected_vms.copy() + + super()._save_changes() + + def run_for_new(self, new_row_function: Callable) -> DevPolicyRow: + self.dialog.set_title(_("Create New Device Assignment")) + self.devident_label.set_text("No device selected.") + self._init_check(self.readonly_check, False, False, "not selected") + self._init_check(self.permissive_check, False, False, "not selected") + self._init_check(self.no_strict_check, False, False, "not selected") + self._init_check(self.port_check, False, False, "not selected") + + self.dev_combo.set_active_id(None) + self.unknown_box.set_visible(False) + self.qube_handler.reset() + self.validate() + + new_row = super().run_for_new(new_row_function) + return new_row + + def run_for_existing(self, row: DevPolicyRow): + self.dialog.set_title(_("Edit Device Assignment")) + # load data + self.dev_modeler.select_value(row.assignment_wrapper.device) + + self.no_strict_check.set_active(row.assignment_wrapper.no_strict_reset) + self.permissive_check.set_active(row.assignment_wrapper.permissive) + self.readonly_check.set_active(row.assignment_wrapper.read_only) + self.port_check.set_active(row.assignment_wrapper.port_required) + # fill qubes + self.qube_handler.reset() + for qube in row.assignment_wrapper.frontends: + self.qube_handler.add_selected_vm(qube) + + if row.assignment_wrapper.device.device.is_device_id_set: + self.unknown_box.set_visible(False) + else: + self.unknown_box.set_visible(True) + + super().run_for_existing(row) + + +class DeviceManager: + """A helper class to keep the system state in, to avoid re-querying + qubesd regularly""" + + def __init__(self, qapp: qubesadmin.Qubes): + self.qapp = qapp + + self.all_devices: List[DeviceInfo] = [] + self.assignments: List[Tuple[DeviceAssignment, qubesadmin.vm.QubesVM]] = [] + + def load_data(self): + """Load system state""" + self.all_devices.clear() + self.assignments.clear() + + for vm in self.qapp.domains: + for devclass in DEVICE_CLASSES: + for dev in vm.devices[devclass].get_exposed_devices(): + self.all_devices.append(dev) + for ass in vm.devices[devclass].get_assigned_devices(): + self.assignments.append((ass, vm)) + + def get_available_devices(self, dev_classes: List[str]) -> Iterator[DeviceInfo]: + """Get all available devices of listed classes""" + for dev in self.all_devices: + if dev.devclass in dev_classes: + yield dev + + def get_assignments( + self, dev_classes: List[str] + ) -> Iterator[Tuple[DeviceAssignment, qubesadmin.vm.QubesVM]]: + """Get all available assignments of listed classes""" + for assignment, vm in self.assignments: + if assignment.devclass in dev_classes: + yield assignment, vm + + def get_denied( + self, + ) -> Iterator[tuple[qubesadmin.vm.QubesVM, list[DeviceInterface]]]: + """Get all device interface blocks""" + for vm in self.qapp.domains: + if getattr(vm, "devices_denied", None): + yield vm, DeviceInterface.from_str_bulk(vm.devices_denied) + + +class DeviceWrapper: + """A convenience wrapper for any object that might represent what is + being connected to VMs. Can be an existing device, a virtual device or a + port.""" + + def __init__(self, devclass: str, backend_domain: Optional[qubesadmin.vm.QubesVM]): + # a bare-bones device object, ready to be filled with data + self.device: Optional[DeviceInfo] = None + self.devclass: str = devclass + self.device_id: str = "" + + # shortest possible identifier + self.port_id: str = "" + # identifier to be used in dropdowns etc. + self.long_name: str = "" + + # domain + self.backend_domain = backend_domain + + # Description of the port + self.port: Optional[Port] = None + + @property + def identity_description(self) -> str: + if not self.device: + return _("Unknown device") + result = _("Device name: ") + self.device.description + "\n" + result += ( + _("Type: ") + + ", ".join(interface.category.name for interface in self.device.interfaces) + + "\n" + ) + result += _("Vendor: ") + self.device.vendor + "\n" + result += _("Serial number: ") + self.device.serial + return result + + @classmethod + def new_from_device_info(cls, device_info: DeviceInfo): + dw = cls(device_info.devclass, device_info.backend_domain) + dw.device = device_info + dw.device_id = device_info.device_id + + dw.port_id = str(device_info.port) + dw.long_name = device_info.description + + dw.port = device_info.port + return dw + + def __eq__(self, other): + return self.device == getattr(other, "device", None) + + +class AssignmentWrapper: + """ + A set of assignments with the same device, port_id, assignment mode and + options. + """ + + def __init__(self, device: DeviceWrapper): + self.changed = False + self.assignments: List[DeviceAssignment] = [] + + # the variables below represent the state as displayed to a user; + # it might be different than current system state if the user made + # some changes and did not yet apply them + self.device: DeviceWrapper = device + + self.frontends: List[qubesadmin.vm.QubesVM] = [] + self.mode: AssignmentMode = AssignmentMode.REQUIRED + + self.port_required: bool = True + self.device_identity_required: bool = True + self.no_strict_reset = False + self.permissive = False + self.read_only = False + self.valid = True # does this set of assignments have any weirdness + # we can't handle? + + self.port: Optional[Port] = None + + @classmethod + def new_from_existing(cls, assignments: List[DeviceAssignment]): + """Create new AssignmentWrapper from a list of DeviceAssignments; + this assumes the DeviceAssignments are appropriately grouped""" + device = assignments[0].device + wrapped_device = DeviceWrapper.new_from_device_info(device_info=device) + + aw = cls(wrapped_device) + aw.assignments = assignments + + for ass in aw.assignments: + aw.frontends.append(ass.frontend_domain) + + aw.mode = aw.assignments[0].mode + aw.device_identity_required = aw.assignments[0].device_id != "*" + aw.port_required = aw.assignments[0].port_id != "*" + aw.port = aw.assignments[0].port + if not aw.device_identity_required and not aw.port_required: + aw.valid = False + + # make absolutely sure there is no tomfoolery in options (should be handled by + # load_current_state, this is a just-in-case + for assignment in aw.assignments: + assert assignment.options == aw.assignments[0].options + + aw.no_strict_reset = ( + aw.assignments[0].options.get("no-strict-reset", "") == "True" + ) + aw.permissive = aw.assignments[0].options.get("permissive", "") == "True" + aw.read_only = aw.assignments[0].options.get("read-only", "") == "True" + + for opt in aw.assignments[0].options: + if opt not in ("permissive", "no-strict-reset", "read-only"): + aw.valid = False + return aw + + def device_identity_description(self): + """Nicely readable device identity description""" + if self.device_identity_required: + return self.device.identity_description + return "" + + def __str__(self): + return ( + self.device_description() + + " " + + self.action_description() + + " " + + ", ".join([vm.name for vm in self.frontends]) + ) + + def action_description(self) -> str: + """Description of associated action""" + if self.mode == AssignmentMode.AUTO: + return _("will attach automatically to") + if self.mode == AssignmentMode.REQUIRED: + return _("is required by ") + return _("will ask to be attached to") + + def device_description(self): + """ + Description of the assignment row, for use in the List of rows + """ + if not self.device_identity_required: + # we are at port + return _("Any {} device attached to {} ").format( + self.device.devclass, str(self.port) + ) + # device ID required + if self.port_required: + return "({}) {} ".format( + str(self.device.port_id), self.device.long_name + ) + + return "({}) {} ".format( + self.device.backend_domain, self.device.long_name + ) + + def remove(self): + """ + Remove the current assignment from the system. + .""" + for assignment in self.assignments: + assignment.frontend_domain.devices[assignment.devclass].unassign(assignment) + + def save(self): + """ + Save all assignment changes via removing old assignments and + creating them again. + """ + if not self.changed: + return + if self.assignments: + options = self.assignments[0].options.copy() + # options we handle + if "no-strict-reset" in options: + del options["no-strict-reset"] + if "permissive" in options: + del options["permissive"] + else: + options = {} + + self.remove() + self.assignments.clear() + + device = self.device.device + + if not device: + raise ValueError("Device not found") + + if not self.device_identity_required: + device = device.clone(device_id="*") + if not self.port_required: + device = device.clone( + port=Port(device.backend_domain, "*", device.devclass) + ) + if self.mode == AssignmentMode.AUTO: + mode = "auto-attach" + elif self.mode == AssignmentMode.ASK: + mode = "ask-to-attach" + elif self.mode == AssignmentMode.REQUIRED: + mode = "required" + else: + raise ValueError("Assignment mode unknown") + + if self.no_strict_reset: + options["no-strict-reset"] = "True" + if self.permissive: + options["permissive"] = "True" + if self.read_only: + options["read-only"] = "True" + + for vm in self.frontends: + new_assignment = DeviceAssignment(device=device, mode=mode, options=options) + vm.devices[device.devclass].assign(new_assignment) + self.assignments.append(new_assignment) + + self.changed = False + + +class AttachmentDescriptionRow(DevPolicyRow): + """A ListBoxRow describing an existing assignment rule""" + + def __init__(self, assignment_wrapper: AssignmentWrapper, qapp: qubesadmin.Qubes): + super().__init__() + self.assignment_wrapper = assignment_wrapper + self.valid = assignment_wrapper.valid + self.qapp = qapp + + self.main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + self.main_box.set_homogeneous(True) + self.main_box.set_spacing(5) + + self.add(self.main_box) + + self.dev_box = Gtk.Box() + self.dev_label = Gtk.Label() + self.dev_label.set_max_width_chars(100) + self.dev_label.set_ellipsize(Pango.EllipsizeMode.END) + self.dev_box.add(self.dev_label) + + self.dev_label.set_halign(Gtk.Align.START) + self.dev_label.set_valign(Gtk.Align.START) + + self.action_label = Gtk.Label() + self.action_label.set_halign(Gtk.Align.CENTER) + self.action_label.set_valign(Gtk.Align.START) + + self.main_box.pack_start(self.dev_box, True, True, 0) + self.main_box.pack_start(self.action_label, True, True, 0) + + self.vm_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.vm_box.set_halign(Gtk.Align.START) + self.main_box.pack_start(self.vm_box, True, True, 0) + + self.update() + + def validate(self, row_collection) -> bool: + for row in row_collection: + if row == self: + continue + if ( + row.assignment_wrapper.device_description() + == self.assignment_wrapper.device_description() + ): + for vm in self.assignment_wrapper.frontends: + if vm in row.assignment_wrapper.frontends: + return False + return True + + def update(self): + """Update row display.""" + self.dev_label.set_markup(self.assignment_wrapper.device_description()) + self.action_label.set_markup(self.assignment_wrapper.action_description()) + + for child in self.vm_box.get_children(): + self.vm_box.remove(child) + + self.valid = True + self.set_tooltip_text("") + for vm in self.assignment_wrapper.frontends: + self.vm_box.add(TokenName(vm.name, self.qapp)) + if vm.klass == "AdminVM": + self.valid = False + self.set_tooltip_text(_("This rule cannot be edited with GUI tools.")) + + self.show_all() + + def save(self): + """ + Save row changes, if any. + """ + if not self.changed: + return + self.assignment_wrapper.save() + super().save() + + def remove_self(self): + """ + Remove all assignments associated with this row. + """ + self.assignment_wrapper.remove() + + def __str__(self): + """ + Used in descriptions of unsaved changes. + """ + return str(self.assignment_wrapper) + + +class AttachmentHandler(DevicePolicyHandler): + def __init__( + self, + builder: Gtk.Builder, + qapp: qubesadmin.Qubes, + prefix: str, + device_policy_manager: DeviceManager, + classes: list[str], + assignment_filter: Callable, + edit_dialog_class, + ): + """ + Handler for device attachments + :param builder: Gtk.Builder + :param qapp: Qubes object + :param prefix: prefix for objects from builder + :param device_policy_manager: DeviceManager + :param classes: list of device classes to be included + :param assignment_filter: function to filter assignments that make sense for + this handler; should take an Assignment and return True/False + :param edit_dialog_class: class of the editing dialog + """ + self.classes = classes + self.assignment_filter = assignment_filter + super().__init__( + prefix=prefix, + builder=builder, + qapp=qapp, + device_policy_manager=device_policy_manager, + edit_dialog_class=edit_dialog_class, + ) + + def get_edit_dialog(self, builder) -> DevPolicyDialogHandler: + return self.edit_dialog_class( + builder=builder, + qapp=self.qapp, + parent_window=self.main_window, + device_manager=self.device_policy_manager, + ) + + def create_new_row(self) -> AttachmentDescriptionRow: + blank_device = DeviceWrapper("new", None) + assignment_wrapper = AssignmentWrapper(blank_device) + new_row = AttachmentDescriptionRow(assignment_wrapper, self.qapp) + return new_row + + @staticmethod + def assignment_id(device_assignment: DeviceAssignment): + """Unique identifier of a device_assignment, consists of a device id, + port id and options.""" + return ( + device_assignment.device_id, + device_assignment.port_id, + str(device_assignment.options), + ) + + def load_current_state(self): + """Load system state""" + assignments: dict[tuple[str, str, str], list[DeviceAssignment]] = {} + for assignment, _ in self.device_policy_manager.get_assignments(self.classes): + if not self.assignment_filter(assignment): + continue + assignment_id = self.assignment_id(assignment) + assignments.setdefault(assignment_id, []).append(assignment) + + for assignment_id, assignment_list in assignments.items(): + aw = AssignmentWrapper.new_from_existing(assignment_list) + row = AttachmentDescriptionRow(aw, self.qapp) + self.rule_list.add(row) + + self.rule_list.show_all() + + +class DevAttachmentHandler(PageHandler): + """Handler for all the disparate Dev attachment functions.""" + + def __init__( + self, + qapp: qubesadmin.Qubes, + gtk_builder: Gtk.Builder, + ): + self.qapp = qapp + self.device_manager = DeviceManager(self.qapp) + self.device_manager.load_data() + + self.main_window = gtk_builder.get_object("main_window") + + self.dev_block_handler = DeviceBlockHandler( + qapp, gtk_builder, self.device_manager + ) + self.auto_attach_handler = AttachmentHandler( + builder=gtk_builder, + qapp=qapp, + prefix="devices_auto", + device_policy_manager=self.device_manager, + classes=["block", "mic", "usb"], + assignment_filter=self._filter_auto, + edit_dialog_class=AutoDeviceDialog, + ) + self.required_devices_handler = AttachmentHandler( + builder=gtk_builder, + qapp=qapp, + prefix="devices_required", + device_policy_manager=self.device_manager, + classes=["block", "pci"], + assignment_filter=self._filter_required, + edit_dialog_class=RequiredDeviceDialog, + ) + + self.lists = [ + self.dev_block_handler.rule_list, + self.auto_attach_handler.rule_list, + self.required_devices_handler.rule_list, + ] + + for widget in self.lists: + widget.connect("row-selected", self._select_one) + widget.connect("rules-changed", self.validate_all_rows) + + @staticmethod + def _filter_required(assignment: DeviceAssignment) -> bool: + return assignment.mode == AssignmentMode.REQUIRED + + @staticmethod + def _filter_auto(assignment: DeviceAssignment) -> bool: + return assignment.mode in [AssignmentMode.AUTO, AssignmentMode.ASK] + + def _select_one(self, selected_widget: Gtk.ListBox, selected_row, *_args): + if not selected_row: + return + for widget in self.lists: + if widget != selected_widget: + widget.select_row(None) + + def get_unsaved(self) -> str: + """Get human-readable description of unsaved changes, or + empty string if none were found.""" + unsaved = [ + self.dev_block_handler.get_unsaved(), + self.auto_attach_handler.get_unsaved(), + self.required_devices_handler.get_unsaved(), + ] + return "\n".join([x for x in unsaved if x]) + + def reset(self): + """Reset state to initial or last saved state, whichever is newer.""" + self.dev_block_handler.reset() + self.auto_attach_handler.reset() + self.required_devices_handler.reset() + + def validate_all_rows(self, *_args): + errors = [] + all_rows = ( + self.auto_attach_handler.rule_list.get_children() + + self.required_devices_handler.rule_list.get_children() + ) + for row in all_rows: + if not row.validate(all_rows): + errors.append(row) + row.get_style_context().add_class("error_row") + else: + row.get_style_context().remove_class("error_row") + + if errors: + show_error( + self.main_window, + _("Duplicate rules"), + _( + "Duplicate device rules were found. Those rules cannot be " + "all present in a system; only the last one will be saved. " + ), + ) + + def save(self): + """Save current rules, whatever they are - custom or default.""" + # pretty ugly solution to duplicate rule, but eh... + self.dev_block_handler.save() + self.auto_attach_handler.save() + self.required_devices_handler.save() + self.device_manager.load_data() diff --git a/qubes_config/global_config/device_blocks.py b/qubes_config/global_config/device_blocks.py new file mode 100644 index 00000000..a292e1d0 --- /dev/null +++ b/qubes_config/global_config/device_blocks.py @@ -0,0 +1,564 @@ +# -*- encoding: utf8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2025 Marta Marczykowska-Górecka +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with this program; if not, see . + +from typing import List, Optional, Iterator, Callable + +from qubesadmin.device_protocol import DeviceCategory, DeviceInterface + +from ..widgets.gtk_widgets import TokenName, VMListModeler +from .device_widgets import ( + DevPolicyDialogHandler, + DevPolicyRow, + DevicePolicyHandler, +) +import gi + +import qubesadmin.exc + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +import gettext + +t = gettext.translation("desktop-linux-manager", fallback=True) +_ = t.gettext + + +class DevicePolicyDialog(DevPolicyDialogHandler): + """Handler for the Edit Blocks window.""" + + def __init__(self, builder: Gtk.Builder, qapp, parent_window: Gtk.Window): + super().__init__(builder, qapp, "device_policy", parent_window) + + self.new_label: Gtk.Label = builder.get_object("device_policy_new_label") + + self.block_box: Gtk.Box = builder.get_object("device_policy_block_box") + self.qube_combo: Gtk.ComboBox = builder.get_object("device_policy_vm_combo") + self.listbox: Gtk.ListBox = builder.get_object("device_policy_listbox") + + # setup qube modeler + self.qube_model = VMListModeler( + combobox=self.qube_combo, + qapp=self.qapp, + filter_function=lambda vm: vm.klass != "AdminVM", + current_value=None, + ) + self.qube_model.connect_change_callback(self._qube_selected) + + # setup treeview + self.device_category_model = DeviceCategoryModeler() + for row in self.device_category_model.get_rows(): + self.listbox.add(row) + row.check_box.connect("toggled", self.validate) + + self.other_row: Optional["DeviceCategoryRow"] = None + + self.listbox.connect("row-activated", self._row_activated) + + def _save_changes(self, *_args): + if not self.current_row: + self._cancel() + return + # save changed VM + self.current_row.vm_wrapper.vm = self.qube_model.get_selected() + + # save changed categories + new_categories = [] + for row in self.listbox.get_children(): + if isinstance(row, OtherCategoryRow): + continue + if row.check_box.get_active(): + if not row.parent or not row.parent.check_box.get_active(): + new_categories.append(row.category_wrapper) + + self.current_row.vm_wrapper.categories = new_categories + + super()._save_changes() + + def run_for_new(self, new_row_function: Callable) -> DevPolicyRow: + """Run for a new rule""" + self.dialog.set_title(_("New Device Attachment Block")) + self.clear() + return super().run_for_new(new_row_function) + + def run_for_existing(self, row: "DevPolicyRow"): + """Run for an existing rule""" + self.dialog.set_title("Edit Device Attachment Block") + super().run_for_existing(row) + self.clear() + self.qube_model.select_value(row.vm_wrapper.vm) + self.select_rows(row.vm_wrapper.categories) + + if row.vm_wrapper.other_interfaces: + self.other_row = OtherCategoryRow(row.vm_wrapper.other_interfaces) + self.listbox.add(self.other_row) + + def clear(self): + """Clear all selections and dropdowns""" + self.qube_model.clear_selection() + + if self.other_row: + self.listbox.remove(self.other_row) + self.other_row = None + + for row in self.listbox.get_children(): + row.check_box.set_active(False) + + def select_rows(self, categories: list["DeviceCategoryWrapper"]): + """Select rows matching provided categories""" + last_active_parent = None + for row in self.listbox.get_children(): + if last_active_parent == row.parent: + continue + if row.category_wrapper in categories: + row.check_box.set_active(True) + last_active_parent = row + + def _row_activated(self, _box, row, *_args): + row.row_activated() + + def check_validity(self) -> bool: + """Check if current state is save-able (in this case, a VM is + selected and at least one rule is selected).""" + if self.qube_model.get_selected(): + for row in self.listbox.get_children(): + if row.check_box.get_active(): + return True + return False + + def _qube_selected(self, *_args): + if self.qube_model.get_selected(): + self.new_label.set_visible(False) + self.block_box.set_sensitive(True) + else: + self.new_label.set_visible(True) + self.block_box.set_sensitive(False) + + self.validate() + + +class DeviceCategoryWrapper: + """This class represents a DeviceCategory with a nice description + wrapped around it.""" + + def __init__( + self, name: str, device_category: DeviceCategory, description: str = "" + ): + """ + :param name: readable name of the category + :param device_category: relevant DeviceCategory + :param description: optional description (will be shown in smaller font + beneath the main description) + """ + self.name = name + self.children: List[DeviceCategoryWrapper] = [] + self.device_category = device_category + self.description = description + self.interfaces = [DeviceInterface(s) for s in self.device_category.value] + + def readable_description(self): + """Nicely formatted category description, perfect to be placed in a + Gtk.Label's markup.""" + if self.description: + return self.name + "\n{}".format(self.description) + return self.name + + def matches( + self, interfaces: list[DeviceInterface] + ) -> (Optional)[list[DeviceInterface]]: + """ + If in the given list of string representations of interfaces there is a + complete match with my own category, return the matching list of + interfaces. + """ + if set(interfaces).issuperset(set(self.interfaces)): + return self.interfaces + return None + + def __eq__(self, other): + return self.device_category == getattr(other, "device_category", None) + + +class DeviceCategoryRow(Gtk.ListBoxRow): + """Gtk.ListBoxRow representing a block category""" + + def __init__( + self, + category_wrapper: DeviceCategoryWrapper, + parent: Optional["DeviceCategoryRow"], + ): + """ + :param category_wrapper: relevant DeviceCategoryWrapper + :param parent: parent row; if present, this row will be indented + relative to the parent and will 1. be selected when the parent is + selected 2. will deselect the parent if deselected + """ + super().__init__() + self.category_wrapper = category_wrapper + self.depth: int = parent.depth + 1 if parent else 0 + self.parent = parent + + self.main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + self.main_box.set_spacing(0) + self.main_box.set_margin_start(30 * self.depth) + + self.add(self.main_box) + + self.check_box: Gtk.CheckButton = Gtk.CheckButton() + self.main_box.add(self.check_box) + self.description_label = Gtk.Label() + self.description_label.set_markup(self.category_wrapper.readable_description()) + self.main_box.add(self.description_label) + self.show_all() + + self.children: List[DeviceCategoryRow] = [] + self.check_box.connect("toggled", self._row_checked) + + def row_activated(self): + self.check_box.set_active(not self.check_box.get_active()) + + def _row_checked(self, *_args): + if not self.check_box.get_active() and self.parent: + self.parent.check_box.set_active(False) + for child in self.children: + child.check_box.set_active(self.check_box.get_active()) + + +class OtherCategoryRow(Gtk.ListBoxRow): + """Special row for the Other interfaces (listed as text and unclickable""" + + def __init__(self, interfaces: list[DeviceInterface]): + super().__init__() + self.other_interfaces: list[DeviceInterface] = interfaces + + self.main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + self.main_box.set_spacing(10) + + self.add(self.main_box) + + self.check_box = Gtk.CheckButton() + self.check_box.set_active(True) + self.main_box.add(self.check_box) + self.description_label = Gtk.Label() + self.description_label.set_markup(_("Other: ")) + self.main_box.add(self.description_label) + + self.text_box = Gtk.Entry() + self.main_box.add(self.text_box) + self.text_box.set_text(", ".join([repr(i) for i in self.other_interfaces])) + self.set_tooltip_text(_("Block set in command line.")) + self.set_sensitive(False) + self.show_all() + + +class DeviceCategoryModeler: + """Helper class to manage our DeviceCategoryWrapper model""" + + def __init__(self): + self.root_device = DeviceCategoryWrapper("All devices", DeviceCategory.Other) + self.root_device.children.append( + DeviceCategoryWrapper( + _("Network devices"), + DeviceCategory.Network, + _("modems, WiFi and Ethernet adapters"), + ) + ) + + hid_devices = DeviceCategoryWrapper( + _("Human interface devices"), + DeviceCategory.Input, + _("all input devices, such as mice, keyboards, tablets etc."), + ) + self.root_device.children.append(hid_devices) + hid_devices.children.append( + DeviceCategoryWrapper(_("Keyboards"), DeviceCategory.Keyboard) + ) + hid_devices.children.append( + DeviceCategoryWrapper(_("Mice"), DeviceCategory.Mouse) + ) + + self.root_device.children.append( + DeviceCategoryWrapper(_("Printers"), DeviceCategory.Printer) + ) + + self.root_device.children.append( + DeviceCategoryWrapper( + _("Image input devices"), + DeviceCategory.Image_Input, + _("Scanners and cameras"), + ) + ) + + self.root_device.children.append( + DeviceCategoryWrapper( + _("Multimedia output devices"), + DeviceCategory.Multimedia, + _("Displays and audio output devices"), + ) + ) + + audio_devices = DeviceCategoryWrapper(_("Audio devices"), DeviceCategory.Audio) + self.root_device.children.append(audio_devices) + audio_devices.children.append( + DeviceCategoryWrapper(_("Microphones"), DeviceCategory.Microphone) + ) + + storage_devices = DeviceCategoryWrapper( + _("Storage devices"), DeviceCategory.Storage + ) + self.root_device.children.append(storage_devices) + storage_devices.children.append( + DeviceCategoryWrapper(_("Block devices"), DeviceCategory.Block_Storage) + ) + storage_devices.children.append( + DeviceCategoryWrapper( + _("USB storage devices"), + DeviceCategory.USB_Storage, + _( + "USB storage devices may offer more capabilities " + "than block storage devices.\nMost storage devices " + "can be attached as either block device or USB " + "device" + ), + ) + ) + + self.root_device.children.append( + DeviceCategoryWrapper(_("Bluetooth"), DeviceCategory.Bluetooth) + ) + + self.root_device.children.append( + DeviceCategoryWrapper( + _("Smart card readers"), DeviceCategory.Smart_Card_Readers + ) + ) + + def get_rows( + self, parent_row: DeviceCategoryRow | None = None + ) -> Iterator[DeviceCategoryRow]: + """Get relevant ListBoxRows""" + if not parent_row: + parent_row = DeviceCategoryRow(self.root_device, None) + yield parent_row + + for child in parent_row.category_wrapper.children: + row = DeviceCategoryRow(child, parent_row) + parent_row.children.append(row) + yield row + yield from self.get_rows(row) + + def parse_interfaces( + self, + interfaces: list[DeviceInterface], + categories: list[DeviceCategoryWrapper] | None = None, + node: DeviceCategoryWrapper | None = None, + ) -> tuple[list[DeviceCategoryWrapper], list[DeviceInterface]]: + """Turn a list of DeviceInterfaces into matching DeviceCategories and + a list of remaining interfaces""" + if categories is None: + categories = [] + if node is None: + node = self.root_device + + matched_interfaces = node.matches(interfaces) + + if matched_interfaces: + for m in matched_interfaces: + interfaces.remove(m) + categories.append(node) + return categories, interfaces + for child in node.children: + categories, interfaces = self.parse_interfaces( + interfaces, categories, child + ) + return categories, interfaces + + +class BlockPolicyWrapper: + """This class is a wrapper for a VM with its associated denied device + interfaces.""" + + def __init__( + self, + vm, + interfaces: List[DeviceInterface], + dev_cat_modeler: DeviceCategoryModeler, + ): + self.original_vm = vm + self.vm = vm + self.interfaces = interfaces + self.dev_cat_modeler = dev_cat_modeler + self.categories: list[DeviceCategoryWrapper] = [] + self.other_interfaces: list[DeviceInterface] = [] + self.update() + + def get_description(self) -> str: + """Get a nicely formatted (markup) description""" + int_descr = "" + if self.categories: + int_descr += ", ".join([cat.name for cat in self.categories]) + if self.other_interfaces: + if int_descr: + int_descr += ", " + int_descr += ", ".join([repr(i) for i in self.other_interfaces]) + return _("{} cannot be attached to ").format(int_descr) + + def update(self): + """Update matching categories.""" + self.categories.clear() + self.other_interfaces.clear() + self.categories, self.other_interfaces = self.dev_cat_modeler.parse_interfaces( + self.interfaces.copy() + ) + + def save(self): + """Save changes""" + new_interfaces = [] + for c in self.categories: + new_interfaces.extend(c.interfaces) + new_interfaces.extend(self.other_interfaces) + + # remove old, make new + if self.original_vm: + # removing old only makes sense if they existed in the first place + if self.original_vm != self.vm: + self.remove() + else: + # remove only the missing ones + for interface in self.interfaces: + if interface not in new_interfaces: + self.original_vm.devices.allow(interface) + + # make new blocks + if self.original_vm != self.vm: + # all of them + for interface in new_interfaces: + self.vm.devices.deny(interface) + else: + for interface in new_interfaces: + if ( + interface not in self.interfaces + and interface not in self.other_interfaces + ): + self.vm.devices.deny(interface) + + self.interfaces = new_interfaces + + def remove(self): + """Remove all existing blocks""" + for interface in self.interfaces: + self.original_vm.devices.allow(interface) + + +class BlockPolicyRow(DevPolicyRow): + """ListBoxRow representing a vm with its associated blocks""" + + def __init__(self, vm_wrapper: BlockPolicyWrapper, qapp: qubesadmin.Qubes): + super().__init__() + self.vm_wrapper = vm_wrapper + self.qapp = qapp + + self.box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + self.add(self.box) + + self.label = Gtk.Label() + self.box.add(self.label) + self.vm_token: TokenName | None = None + + self.update() + + def description(self): + """Nicely readable description, for use in unsaved changes (for + example)""" + return self.vm_wrapper.get_description() + self.vm_wrapper.vm.name + + def update(self): + """Update displayed state.""" + if self.vm_token: + self.box.remove(self.vm_token) + self.vm_token = None + + if self.vm_wrapper.vm: + self.vm_token = TokenName(self.vm_wrapper.vm, self.qapp) + self.box.add(self.vm_token) + self.box.show_all() + else: + self.vm_token = None + + self.label.set_markup(self.vm_wrapper.get_description()) + + if self.vm_wrapper.vm: + # only show all if it's in any way a sensible row + self.show_all() + + def save(self): + """Save changes in this row.""" + if not self.changed: + return + self.vm_wrapper.save() + + super().save() + + def remove_self(self): + """Remove all blocks from the system""" + self.vm_wrapper.remove() + + def __str__(self): + return self.vm_wrapper.get_description() + self.vm_wrapper.vm.name + + +class DeviceBlockHandler(DevicePolicyHandler): + """Handler class for all interface blocks""" + + def __init__( + self, + qapp: qubesadmin.Qubes, + builder: Gtk.Builder, + device_policy_manager, + ): + self.device_category_modeler = DeviceCategoryModeler() + super().__init__( + prefix="devices_policy", + builder=builder, + qapp=qapp, + device_policy_manager=device_policy_manager, + edit_dialog_class=DevPolicyDialogHandler, + ) + + def load_current_state(self): + """Load state from system""" + rules = [] + for vm, interfaces in self.device_policy_manager.get_denied(): + rules.append( + BlockPolicyWrapper(vm, interfaces, self.device_category_modeler) + ) + + for rule in rules: + self.rule_list.add(BlockPolicyRow(rule, self.qapp)) + + def create_new_row(self) -> BlockPolicyRow: + blank_rule = BlockPolicyWrapper(None, [], self.device_category_modeler) + new_row = BlockPolicyRow(blank_rule, self.qapp) + return new_row + + def get_edit_dialog(self, builder) -> DevPolicyDialogHandler: + return DevicePolicyDialog( + builder=builder, qapp=self.qapp, parent_window=self.main_window + ) diff --git a/qubes_config/global_config/device_widgets.py b/qubes_config/global_config/device_widgets.py new file mode 100644 index 00000000..45faa403 --- /dev/null +++ b/qubes_config/global_config/device_widgets.py @@ -0,0 +1,339 @@ +# -*- encoding: utf8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2025 Marta Marczykowska-Górecka +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with this program; if not, see . +""" +Widgets relevant to Device Attachments page. +""" +import abc +from typing import List, Any, Callable + +import gi + +from ..widgets.gtk_utils import ask_question, resize_window_to_reasonable + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +import gettext + +t = gettext.translation("desktop-linux-manager", fallback=True) +_ = t.gettext + + +class HeaderComboModeler: + """A model handler for a ComboBox with a list with headers (insensitive + and unselectable)""" + + def __init__( + self, + combobox: Gtk.ComboBox, + values: dict[str, tuple[str, Any]], + ): + """ + A helper for modeling ComboBox contents that consist of a list with + headers (headers are not sensitive). + :param combobox: A Gtk.ComboBox or ComboBoxText to be filled + :param values: a dictionary that maps value_id (str) to a tuple of + nicely presented value (str) and the actual underlying value or None, + if it's a header + """ + self._combo: Gtk.ComboBox = combobox + self._combo.clear() + self._values: dict[str, tuple[str, Any]] = values + + self._model = Gtk.ListStore(str, str, bool) # id, text and sensitivity + self._fill_model() + + renderer = Gtk.CellRendererText() + self._combo.set_model(self._model) + self._combo.set_id_column(0) + self._combo.pack_start(renderer, True) + self._combo.add_attribute(renderer, "text", 1) + self._combo.add_attribute(renderer, "sensitive", 2) + + def get_selected(self): + """Get currently selected value.""" + active_id = self._combo.get_active_id() + result_tuple = self._values.get(active_id, None) + if result_tuple: + return result_tuple[1] + return None + + def _fill_model(self): + self._model.clear() + for device_id, value in self._values.items(): + description, device = value + self._model.append([device_id, description, bool(device)]) + + def select_value(self, selected_value: Any): + """Select provided value, if available.""" + self._fill_model() + + for key, value in self._values.items(): + if value[1] == selected_value: + self._combo.set_active_id(key) + + +class DevPolicyRow(Gtk.ListBoxRow): + """Base class for ListBoxRows representing various policy rules on the + Device Assignments page. + This is not an abstract class because Gtk.ListBoxRow already inherits + from an abstract class.""" + + def __init__(self): + super().__init__() + self.changed = False + self.valid = True + + @abc.abstractmethod + def update(self): + """Update displayed state""" + + @abc.abstractmethod + def save(self): + """Save changes to the system""" + self.changed = False + + @abc.abstractmethod + def remove_self(self): + """ + This should remove the current assignment or policy from the system. + """ + + +class DevPolicyDialogHandler: + """ + A generic class for various Edit Device Policy row dialogs. + + Every edit dialog needs: + - {prefix}_dialog - a Gtk.Dialog + - {prefix}_ok_button - a Gtk.Button + - {prefix}_cancel_button - a Gtk.Button + """ + + def __init__( + self, builder: Gtk.Builder, qapp, prefix: str, parent_window: Gtk.Window + ): + self.qapp = qapp + + self.dialog: Gtk.Dialog = builder.get_object(f"{prefix}_dialog") + self.dialog.set_transient_for(parent_window) + + # ok and cancel buttons + self.ok_button: Gtk.Button = builder.get_object(f"{prefix}_ok_button") + self.cancel_button: Gtk.Button = builder.get_object(f"{prefix}_cancel_button") + + self.current_row: Gtk.ListBoxRow | None = None + self.remove_on_cancel = False + + self.dialog.connect("delete-event", self._cancel) + self.ok_button.connect("clicked", self._save_changes) + self.cancel_button.connect("clicked", self._cancel) + + self.dialog.connect("size-allocate", self._resize_window) + + def _resize_window(self, *_args): + resize_window_to_reasonable(self.dialog) + + def _cancel(self, *_args): + if self.remove_on_cancel and self.current_row: + self.current_row.get_parent().remove(self.current_row) + self.current_row = None + self.remove_on_cancel = False + self.dialog.hide() + # return True to stop other handlers from destroying the dialog + return True + + def _save_changes(self, *_args): + if not self.current_row: + self._cancel() + return + self.current_row.update() + self.current_row.changed = True + parent = self.current_row.get_parent() + + self.current_row = None + self.remove_on_cancel = False + + self.dialog.hide() + parent.emit("rules-changed", None) + + def run_for_new(self, new_row_function: Callable) -> DevPolicyRow: + """Run for a new row""" + new_row = new_row_function() + self.current_row = new_row + self.remove_on_cancel = True + + self.dialog.show() + self._resize_window() + + return new_row + + def run_for_existing(self, row: DevPolicyRow): + """Run for an existing row""" + self.current_row = row + self.validate() + self.remove_on_cancel = False + self.dialog.show() + self._resize_window() + + def validate(self, *_args): + """Connect this function to any events that should trigger + re-checking for dialog validity.""" + self.ok_button.set_sensitive(self.check_validity()) + + def check_validity(self) -> bool: + """Return True if the current dialog state should allow saving, + False otherwise.""" + return True + + def fill_combo_with_devices( + self, dev_classes: dict, combo: Gtk.ComboBox, device_manager, dev_row_class + ) -> HeaderComboModeler: + """Fill provided combobox with current devices""" + dev_list = {} + for class_id, class_name in dev_classes.items(): + devices = list(device_manager.get_available_devices([class_id])) + if devices: + dev_list[class_name] = (class_name, None) + for dev in devices: + dw = dev_row_class.new_from_device_info(dev) + dev_list[dw.device_id] = (dw.long_name, dw) + + combo.connect("changed", self.validate) + + return HeaderComboModeler(combo, dev_list) + + +class DevicePolicyHandler: + """Generic handler for DeviceAssignments""" + + def __init__( + self, + prefix: str, + builder: Gtk.Builder, + qapp, + device_policy_manager, + edit_dialog_class, + ): + self.qapp = qapp + self.device_policy_manager = device_policy_manager + self.edit_dialog_class = edit_dialog_class + + self.main_window: Gtk.Window = builder.get_object("main_window") + + self.rule_list: Gtk.ListBox = builder.get_object(f"{prefix}_list") + + self.add_button: Gtk.Button = builder.get_object(f"{prefix}_add_rule_button") + self.edit_button: Gtk.Button = builder.get_object(f"{prefix}_edit_rule_button") + self.remove_button: Gtk.Button = builder.get_object(f"{prefix}_del_rule_button") + + self.add_button.connect("clicked", self.add_new_rule) + self.edit_button.connect("clicked", self.edit_rule) + self.remove_button.connect("clicked", self.remove_rule) + self.rule_list.connect("row-selected", self._row_selected) + self.rule_list.connect("row-activated", self.edit_rule) + + self.edit_button.set_sensitive(False) + self.remove_button.set_sensitive(False) + + self.removed_rows: List[DevPolicyRow] = [] + + self.edit_dialog = self.get_edit_dialog(builder) + + self.load_current_state() + + def _row_selected(self, *_args): + row = self.rule_list.get_selected_row() + if row: + self.edit_button.set_sensitive(row.valid) + self.remove_button.set_sensitive(True) + else: + self.edit_button.set_sensitive(False) + self.remove_button.set_sensitive(False) + + def add_new_rule(self, *_args): + """Add a new row to the list""" + new_row = self.edit_dialog.run_for_new(self.create_new_row) + self.rule_list.add(new_row) + + def edit_rule(self, *_args): + """Edit currently selected rule""" + current_row = self.rule_list.get_selected_row() + self.edit_dialog.run_for_existing(current_row) + + def remove_rule(self, *_args): + current_row = self.rule_list.get_selected_row() + response = ask_question( + self.main_window, + "Delete rule", + _("Are you sure you want to delete rule:\n") + str(current_row), + ) + if response == Gtk.ResponseType.NO: + return + + self.removed_rows.append(current_row) + self.rule_list.remove(current_row) + self.rule_list.emit("rules-changed", None) + + def save(self) -> None: + """Save all changes""" + # new and existing + for row in self.rule_list.get_children(): + row.save() + + # removed + for row in self.removed_rows: + row.remove_self() + + self.removed_rows.clear() + self.device_policy_manager.load_data() + + def reset(self) -> None: + """Reset all changes.""" + for row in self.rule_list.get_children(): + self.rule_list.remove(row) + self.removed_rows.clear() + self.load_current_state() + + def get_unsaved(self) -> str: + """ + Get all unsaved changes. + """ + results = [] + for row in self.rule_list.get_children(): + if row.changed: + results.append(_("Attachment changed: ") + str(row)) + + for row in self.removed_rows: + results.append(_("Removed assignment: ") + str(row)) + + return "\n".join(results) + + @abc.abstractmethod + def create_new_row(self) -> DevPolicyRow: + """This function should return a blank DevPolicyRow.""" + + def load_current_state(self): + """This function should load current system state.""" + + @abc.abstractmethod + def get_edit_dialog(self, builder: Gtk.Builder) -> DevPolicyDialogHandler: + """this function should get a DevPolicyDialogHandler for an + appropriate edit dialog.""" diff --git a/qubes_config/global_config/global_config.py b/qubes_config/global_config/global_config.py index b85a469c..2ea395e8 100644 --- a/qubes_config/global_config/global_config.py +++ b/qubes_config/global_config/global_config.py @@ -35,6 +35,7 @@ show_dialog_with_icon, load_theme, is_theme_light, + resize_window_to_reasonable, ) from ..widgets.gtk_widgets import ProgressBarDialog, ViewportHandler from ..widgets.utils import open_url_in_disposable @@ -53,11 +54,12 @@ from .basics_handler import BasicSettingsHandler, FeatureHandler from .policy_exceptions_handler import DispvmExceptionHandler from .thisdevice_handler import ThisDeviceHandler +from .device_attachments import DevAttachmentHandler import gi gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, GObject, Gio, Gdk +from gi.repository import Gtk, GLib, GObject, Gio logger = logging.getLogger("qubes-global-config") @@ -83,6 +85,9 @@ "clipboard_policy", "filecopy_policy", "open_in_vm", + "attachment_policy", + "auto_attachment", + "required_devices", ] @@ -295,29 +300,7 @@ def do_activate(self, *args, **kwargs): self.perform_setup() assert self.main_window self.main_window.show() - # resize to screen size - if ( - self.main_window.get_allocated_width() - > self.main_window.get_screen().get_width() - ): - width = int(self.main_window.get_screen().get_width() * 0.9) - else: - # try to have at least 1100 pixels - width = min( - int(self.main_window.get_screen().get_width() * 0.9), 1100 - ) - if ( - self.main_window.get_allocated_height() - > self.main_window.get_screen().get_height() * 0.9 - ): - height = int(self.main_window.get_screen().get_height() * 0.9) - else: - height = self.main_window.get_allocated_height() - self.main_window.resize(width, height) - self.main_window.set_position(Gtk.WindowPosition.CENTER_ALWAYS) - self.main_window.set_gravity(Gdk.Gravity.CENTER) - self.main_window.move(0, 0) - self.main_window.set_position(Gtk.WindowPosition.CENTER) + resize_window_to_reasonable(self.main_window) # open at specified location if self.open_at: @@ -389,16 +372,12 @@ def perform_setup(self): The function that performs actual widget realization and setup. """ self.builder = Gtk.Builder() - glade_ref = ( - importlib.resources.files("qubes_config") / "global_config.glade" - ) + glade_ref = importlib.resources.files("qubes_config") / "global_config.glade" with importlib.resources.as_file(glade_ref) as path: self.builder.add_from_file(str(path)) self.main_window: Gtk.Window = self.builder.get_object("main_window") - self.main_notebook: Gtk.Notebook = self.builder.get_object( - "main_notebook" - ) + self.main_notebook: Gtk.Notebook = self.builder.get_object("main_notebook") load_theme( widget=self.main_window, @@ -413,9 +392,7 @@ def perform_setup(self): self.progress_bar_dialog.update_progress(0) self.apply_button: Gtk.Button = self.builder.get_object("apply_button") - self.cancel_button: Gtk.Button = self.builder.get_object( - "cancel_button" - ) + self.cancel_button: Gtk.Button = self.builder.get_object("cancel_button") self.ok_button: Gtk.Button = self.builder.get_object("ok_button") self.apply_button.connect("clicked", self._apply) @@ -442,6 +419,9 @@ def perform_setup(self): ) self.progress_bar_dialog.update_progress(page_progress) + self.handlers["attachments"] = DevAttachmentHandler(self.qapp, self.builder) + self.progress_bar_dialog.update_progress(page_progress) + self.handlers["splitgpg"] = VMSubsetPolicyHandler( qapp=self.qapp, gtk_builder=self.builder, @@ -552,6 +532,7 @@ def load_icons(self): icon_dict = { "settings_tab_icon": "settings-", "usb_tab_icon": "usb-", + "devices_tab_icon": "devices-", "updates_tab_icon": "qui-updates-", "splitgpg_tab_icon": "key-", "clipboard_tab_icon": "qui-clipboard-", @@ -626,14 +607,10 @@ def _page_switched(self, *_args): def _ask_unsaved(self, description: str) -> Gtk.ResponseType: box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) label_1 = Gtk.Label() - label_1.set_markup( - _("The following unsaved changes were found:") - ) + label_1.set_markup(_("The following unsaved changes were found:")) label_1.set_xalign(0) label_2 = Gtk.Label() - label_2.set_text( - "\n".join([f"- {row}" for row in description.split("\n")]) - ) + label_2.set_text("\n".join([f"- {row}" for row in description.split("\n")])) label_2.set_margin_start(20) label_2.set_xalign(0) label_3 = Gtk.Label() diff --git a/qubes_config/global_config/policy_exceptions_handler.py b/qubes_config/global_config/policy_exceptions_handler.py index 9724b217..46e8b2fc 100644 --- a/qubes_config/global_config/policy_exceptions_handler.py +++ b/qubes_config/global_config/policy_exceptions_handler.py @@ -78,9 +78,7 @@ def __init__( self.policy_manager = policy_manager self.initial_rules: List[Rule] = [] - self.rule_list: Gtk.ListBox = gtk_builder.get_object( - f"{prefix}_exception_list" - ) + self.rule_list: Gtk.ListBox = gtk_builder.get_object(f"{prefix}_exception_list") self.add_button: Gtk.Button = gtk_builder.get_object( f"{prefix}_add_rule_button" @@ -89,15 +87,13 @@ def __init__( self.error_handler = ErrorHandler(gtk_builder, prefix) if enable_raw: - self.raw_handler: Optional[RawPolicyTextHandler] = ( - RawPolicyTextHandler( - gtk_builder=gtk_builder, - prefix=prefix, - policy_manager=self.policy_manager, - error_handler=self.error_handler, - callback_on_open_raw=self.close_all_edits, - callback_on_save_raw=self.populate_rule_lists, - ) + self.raw_handler: Optional[RawPolicyTextHandler] = RawPolicyTextHandler( + gtk_builder=gtk_builder, + prefix=prefix, + policy_manager=self.policy_manager, + error_handler=self.error_handler, + callback_on_open_raw=self.close_all_edits, + callback_on_save_raw=self.populate_rule_lists, ) else: self.raw_handler = None @@ -169,9 +165,7 @@ def current_rows(self) -> List[RuleListBoxRow]: @property def current_rules(self) -> List[Rule]: - rules = [ - row.rule.raw_rule for row in self.current_rows if not row.is_new_row - ] + rules = [row.rule.raw_rule for row in self.current_rows if not row.is_new_row] return rules def close_all_edits(self, *_args): @@ -194,9 +188,7 @@ def verify_rule_against_rows( for other_row in other_rows: if other_row == row: continue - if other_row.rule.is_rule_conflicting( - new_source, new_target, new_action - ): + if other_row.rule.is_rule_conflicting(new_source, new_target, new_action): return str(other_row) return None @@ -262,9 +254,7 @@ def __init__( self.current_state_widget: Optional[Gtk.Box] = None self.initial_rules, self.current_token = ( - self.policy_manager.get_rules_from_filename( - self.policy_file_name, "" - ) + self.policy_manager.get_rules_from_filename(self.policy_file_name, "") ) self.initialize() @@ -317,10 +307,8 @@ def save(self): self.current_token, ) - _rules, self.current_token = ( - self.policy_manager.get_rules_from_filename( - self.policy_file_name, "" - ) + _rules, self.current_token = self.policy_manager.get_rules_from_filename( + self.policy_file_name, "" ) self.initial_rules = deepcopy(self.list_handler.current_rules) diff --git a/qubes_config/global_config/policy_handler.py b/qubes_config/global_config/policy_handler.py index e40ba1cc..57ecce76 100644 --- a/qubes_config/global_config/policy_handler.py +++ b/qubes_config/global_config/policy_handler.py @@ -102,9 +102,7 @@ def __init__( f"{prefix}_custom_box" ) - self.main_list_box: Gtk.ListBox = gtk_builder.get_object( - f"{prefix}_main_list" - ) + self.main_list_box: Gtk.ListBox = gtk_builder.get_object(f"{prefix}_main_list") self.exception_list_box: Gtk.ListBox = gtk_builder.get_object( f"{prefix}_exception_list" ) @@ -127,9 +125,7 @@ def __init__( self.exception_list_box.connect("row-activated", self._rule_clicked) self.main_list_box.connect("row-activated", self._rule_clicked) - self.exception_list_box.connect( - "rules-changed", self._populate_raw_rules - ) + self.exception_list_box.connect("rules-changed", self._populate_raw_rules) self.main_list_box.connect("rules-changed", self._populate_raw_rules) self.enable_radio.connect("toggled", self._custom_toggled) @@ -194,9 +190,7 @@ def current_rules(self) -> List[Rule]: """ if self.disable_radio.get_active(): return self.policy_manager.text_to_rules(self.default_policy) - return [ - row.rule.raw_rule for row in self.current_rows if not row.is_new_row - ] + return [row.rule.raw_rule for row in self.current_rows if not row.is_new_row] @property def current_rows(self) -> List[RuleListBoxRow]: @@ -204,8 +198,7 @@ def current_rows(self) -> List[RuleListBoxRow]: Get the current list of all RuleListBoxRows """ return ( - self.exception_list_box.get_children() - + self.main_list_box.get_children() + self.exception_list_box.get_children() + self.main_list_box.get_children() ) def initialize_data(self): @@ -312,9 +305,7 @@ def cmp_token(token_1, token_2): return 1 return -1 - def rule_sorting_function( - self, row_1: RuleListBoxRow, row_2: RuleListBoxRow - ): + def rule_sorting_function(self, row_1: RuleListBoxRow, row_2: RuleListBoxRow): """Sorting function for exceptions.""" source_cmp = self.cmp_token(row_1.rule.source, row_2.rule.source) if source_cmp != 0: @@ -326,9 +317,7 @@ def check_custom_rules(self, rules: List[Rule]): Check if the provided set of rules is the same as the default set, set radio buttons accordingly. """ - if self.policy_manager.compare_rules_to_text( - rules, self.default_policy - ): + if self.policy_manager.compare_rules_to_text(rules, self.default_policy): self.disable_radio.set_active(True) else: self.enable_radio.set_active(True) @@ -362,9 +351,7 @@ def verify_rule_against_rows( for other_row in other_rows: if other_row == row: continue - if other_row.rule.is_rule_conflicting( - new_source, new_target, new_action - ): + if other_row.rule.is_rule_conflicting(new_source, new_target, new_action): return str(other_row) return None @@ -427,9 +414,7 @@ def reset(self): def save(self): """Save current rules, whatever they are - custom or default.""" rules = self.current_rules - self.policy_manager.save_rules( - self.policy_file_name, rules, self.current_token - ) + self.policy_manager.save_rules(self.policy_file_name, rules, self.current_token) _r, self.current_token = self.policy_manager.get_rules_from_filename( self.policy_file_name, self.default_policy ) @@ -450,9 +435,7 @@ def get_unsaved(self) -> str: def on_switch(self, *_args): if self.error_handler.get_errors(): - rule_text = "\n".join( - str(rule) for rule in self.error_handler.get_errors() - ) + rule_text = "\n".join(str(rule) for rule in self.error_handler.get_errors()) show_error( parent=self.main_list_box.get_toplevel(), title=_("Unknown rule found in police file"), @@ -498,13 +481,9 @@ def __init__( self.raw_expander_icon: Gtk.Image = gtk_builder.get_object( f"{prefix}_raw_expander" ) - self.raw_text: Gtk.TextView = gtk_builder.get_object( - f"{prefix}_raw_text" - ) + self.raw_text: Gtk.TextView = gtk_builder.get_object(f"{prefix}_raw_text") self.raw_save: Gtk.Button = gtk_builder.get_object(f"{prefix}_raw_save") - self.raw_cancel: Gtk.Button = gtk_builder.get_object( - f"{prefix}_raw_cancel" - ) + self.raw_cancel: Gtk.Button = gtk_builder.get_object(f"{prefix}_raw_cancel") self.text_buffer: Gtk.TextBuffer = self.raw_text.get_buffer() self.raw_save.connect("clicked", self._save_raw) @@ -700,16 +679,13 @@ def _has_partial_duplicate(rule: Rule, rules: List[Rule]) -> bool: and other_rule.target == "@default" and ( getattr(other_rule.action, "target", None) == rule.target - or getattr(other_rule.action, "default_target", None) - == rule.target + or getattr(other_rule.action, "default_target", None) == rule.target ) ): return True return False - def populate_rule_lists( - self, rules: List[Rule], drop_obsolete: bool = False - ): + def populate_rule_lists(self, rules: List[Rule], drop_obsolete: bool = False): """ Populate the rule lists. :param rules: List of Rule objects @@ -718,8 +694,7 @@ def populate_rule_lists( :return: """ for child in ( - self.main_list_box.get_children() - + self.exception_list_box.get_children() + self.main_list_box.get_children() + self.exception_list_box.get_children() ): child.get_parent().remove(child) # rules with source = '@anyvm' go to main list and their @@ -864,9 +839,7 @@ def current_rules(self) -> List[Rule]: # do not save duplicates continue rules.append(another_rule) - rules.extend( - [row.rule.raw_rule for row in self.main_list_box.get_children()] - ) + rules.extend([row.rule.raw_rule for row in self.main_list_box.get_children()]) return rules @@ -881,9 +854,7 @@ def __init__(self, gtk_builder, prefix: str): - prefix_error_list, a ListBox that contains erroneous rules """ self.error_box: Gtk.Box = gtk_builder.get_object(f"{prefix}_error_box") - self.error_list: Gtk.ListBox = gtk_builder.get_object( - f"{prefix}_error_list" - ) + self.error_list: Gtk.ListBox = gtk_builder.get_object(f"{prefix}_error_list") self._errors: List[Rule] = [] diff --git a/qubes_config/global_config/policy_manager.py b/qubes_config/global_config/policy_manager.py index 10392ba0..f25adf76 100644 --- a/qubes_config/global_config/policy_manager.py +++ b/qubes_config/global_config/policy_manager.py @@ -54,9 +54,7 @@ def get_all_policy_files(self, service: str) -> List[str]: except subprocess.CalledProcessError: return [] - def get_conflicting_policy_files( - self, service: str, own_file: str - ) -> List[str]: + def get_conflicting_policy_files(self, service: str, own_file: str) -> List[str]: """ Get a list of policy files (as str) that apply to the selected service and are before it in load order. @@ -112,9 +110,7 @@ def new_rule( lineno=0, ) - def save_rules( - self, file_name: str, rules_list: List[Rule], token: Optional[str] - ): + def save_rules(self, file_name: str, rules_list: List[Rule], token: Optional[str]): """Save provided list of rules to a file. Must provide a token corresponding to last file access, to avoid unexpected overwriting.""" diff --git a/qubes_config/global_config/policy_rules.py b/qubes_config/global_config/policy_rules.py index 07baaa11..df4d5e67 100644 --- a/qubes_config/global_config/policy_rules.py +++ b/qubes_config/global_config/policy_rules.py @@ -243,14 +243,10 @@ def __init__(self, rule: Rule): raise ValueError(_("Target must be @adminvm")) if isinstance(rule.action, Ask): if rule.action.default_target != "@adminvm": - raise ValueError( - _("If action is ask, default_target must be @adminvm") - ) + raise ValueError(_("If action is ask, default_target must be @adminvm")) if isinstance(rule.action, Allow): if rule.action.target: - raise ValueError( - _("If action is allow, no parameters are allowed") - ) + raise ValueError(_("If action is allow, no parameters are allowed")) @property def target(self): @@ -397,9 +393,7 @@ def is_rule_conflicting( Return True if rule with other_source and other_target would conflict with self. """ - if super().is_rule_conflicting( - other_source, other_target, other_action - ): + if super().is_rule_conflicting(other_source, other_target, other_action): return True if self.action == "allow" and other_source == self.source: return True @@ -574,9 +568,5 @@ def __init__( def get_verb_for_action_and_target(self, action: str, target: str) -> str: if target.startswith("@") and target != "@dispvm": - return self.multi_target_descr.get(action, "").rjust( - self.max_length, " " - ) - return self.single_target_descr.get(action, "").rjust( - self.max_length, " " - ) + return self.multi_target_descr.get(action, "").rjust(self.max_length, " ") + return self.single_target_descr.get(action, "").rjust(self.max_length, " ") diff --git a/qubes_config/global_config/rule_list_widgets.py b/qubes_config/global_config/rule_list_widgets.py index e6d06bf0..6d62669a 100644 --- a/qubes_config/global_config/rule_list_widgets.py +++ b/qubes_config/global_config/rule_list_widgets.py @@ -87,9 +87,7 @@ def __init__( initial_value: str, additional_text: Optional[str] = None, additional_widget: Optional[Gtk.Widget] = None, - filter_function: Optional[ - Callable[[qubesadmin.vm.QubesVM], bool] - ] = None, + filter_function: Optional[Callable[[qubesadmin.vm.QubesVM], bool]] = None, change_callback: Optional[Callable] = None, ): """ @@ -229,9 +227,7 @@ def __init__( self.name_widget.get_style_context().add_class(action_style_class) if self.verb_description: self.additional_text_widget = Gtk.Label() - self.additional_text_widget.get_style_context().add_class( - "didascalia" - ) + self.additional_text_widget.get_style_context().add_class("didascalia") else: self.additional_text_widget = None @@ -314,9 +310,7 @@ def __init__( enable_delete: bool = True, enable_vm_edit: bool = True, initial_verb: str = _("will"), - custom_deletion_warning: str = _( - "Are you sure you want to delete this rule?" - ), + custom_deletion_warning: str = _("Are you sure you want to delete this rule?"), is_new_row: bool = False, enable_adminvm: bool = False, ): @@ -371,9 +365,7 @@ def __init__( self.main_widget_box.pack_start(self.target_widget, False, True, 0) self.outer_box.pack_start(self.main_widget_box, False, False, 0) - self.additional_widget_box = Gtk.Box( - orientation=Gtk.Orientation.HORIZONTAL - ) + self.additional_widget_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) save_button = ImageTextButton( icon_name="qubes-ok", label=_("ACCEPT"), @@ -417,9 +409,7 @@ def get_target_widget(self) -> VMWidget: def get_action_widget(self) -> ActionWidget: """Widget to be used for Action""" - return ActionWidget( - self.rule.ACTION_CHOICES, self.verb_description, self.rule - ) + return ActionWidget(self.rule.ACTION_CHOICES, self.verb_description, self.rule) def _get_delete_button(self) -> Gtk.Button: """Get a delete button appropriate for the class.""" diff --git a/qubes_config/global_config/thisdevice_handler.py b/qubes_config/global_config/thisdevice_handler.py index c6e4f2ef..137bd2bf 100644 --- a/qubes_config/global_config/thisdevice_handler.py +++ b/qubes_config/global_config/thisdevice_handler.py @@ -53,12 +53,8 @@ def __init__( self.qapp = qapp self.policy_manager = policy_manager - self.model_label: Gtk.Label = gtk_builder.get_object( - "thisdevice_model_label" - ) - self.data_label: Gtk.Label = gtk_builder.get_object( - "thisdevice_data_label" - ) + self.model_label: Gtk.Label = gtk_builder.get_object("thisdevice_model_label") + self.data_label: Gtk.Label = gtk_builder.get_object("thisdevice_data_label") self.certified_box_yes: Gtk.Box = gtk_builder.get_object( "thisdevice_certified_box_yes" @@ -100,19 +96,13 @@ def __init__( self.compat_usbk_label: Gtk.Label = gtk_builder.get_object( "thisdevice_usbk_label" ) - self.compat_pv_image: Gtk.Image = gtk_builder.get_object( - "thisdevice_pv_image" - ) - self.compat_pv_label: Gtk.Label = gtk_builder.get_object( - "thisdevice_pv_label" - ) + self.compat_pv_image: Gtk.Image = gtk_builder.get_object("thisdevice_pv_image") + self.compat_pv_label: Gtk.Label = gtk_builder.get_object("thisdevice_pv_label") self.compat_pv_tooltip: Gtk.Image = gtk_builder.get_object( "thisdevice_pv_tooltip" ) - self.copy_button: Gtk.Button = gtk_builder.get_object( - "thisdevice_copy_button" - ) + self.copy_button: Gtk.Button = gtk_builder.get_object("thisdevice_copy_button") self.copy_hcl_button: Gtk.Button = gtk_builder.get_object( "thisdevice_copy_hcl_button" ) @@ -125,9 +115,7 @@ def __init__( ["qubes-hcl-report", "-y"] ).decode() except subprocess.CalledProcessError as ex: - label_text += _("Failed to load system data: {ex}\n").format( - ex=str(ex) - ) + label_text += _("Failed to load system data: {ex}\n").format(ex=str(ex)) self.hcl_check = "" try: @@ -172,14 +160,10 @@ def __init__( self.compat_hvm_label.set_markup(f"HVM: {self._get_data('hvm')}") self.set_state(self.compat_iommu_image, self._get_data("iommu")) - self.compat_iommu_label.set_markup( - f"I/O MMU: {self._get_data('iommu')}" - ) + self.compat_iommu_label.set_markup(f"I/O MMU: {self._get_data('iommu')}") self.set_state(self.compat_hap_image, self._get_data("slat")) - self.compat_hap_label.set_markup( - f"HAP/SLAT: {self._get_data('slat')}" - ) + self.compat_hap_label.set_markup(f"HAP/SLAT: {self._get_data('slat')}") self.set_state( self.compat_tpm_image, @@ -195,9 +179,7 @@ def __init__( self.compat_tpm_label.set_markup(_("TPM version: 1.2")) else: self.set_state(self.compat_tpm_image, "no") - self.compat_tpm_label.set_markup( - _("TPM version: device not found") - ) + self.compat_tpm_label.set_markup(_("TPM version: device not found")) self.set_state(self.compat_remapping_image, self._get_data("remap")) self.compat_remapping_label.set_markup( @@ -207,9 +189,7 @@ def __init__( self.set_policy_state() pv_vms = [ - vm - for vm in self.qapp.domains - if getattr(vm, "virt_mode", None) == "pv" + vm for vm in self.qapp.domains if getattr(vm, "virt_mode", None) == "pv" ] self.set_state(self.compat_pv_image, "no" if pv_vms else "yes") @@ -255,10 +235,7 @@ def _copy_to_clipboard(self, widget: Gtk.Button): show_error( self.copy_button.get_toplevel(), _("Failed to copy to Global Clipboard"), - _( - "An error occurred while trying to access" - " Global Clipboard" - ), + _("An error occurred while trying to access Global Clipboard"), ) @staticmethod diff --git a/qubes_config/global_config/updates_handler.py b/qubes_config/global_config/updates_handler.py index 446bde8b..30f96e34 100644 --- a/qubes_config/global_config/updates_handler.py +++ b/qubes_config/global_config/updates_handler.py @@ -70,22 +70,18 @@ def __init__(self, gtk_builder: Gtk.Builder): self.template_official: Gtk.CheckButton = gtk_builder.get_object( "updates_template_official" ) - self.template_official_testing: Gtk.CheckButton = ( - gtk_builder.get_object("updates_template_official_testing") + self.template_official_testing: Gtk.CheckButton = gtk_builder.get_object( + "updates_template_official_testing" ) self.template_community: Gtk.CheckButton = gtk_builder.get_object( "updates_template_community" ) - self.template_community_testing: Gtk.CheckButton = ( - gtk_builder.get_object("updates_template_community_testing") + self.template_community_testing: Gtk.CheckButton = gtk_builder.get_object( + "updates_template_community_testing" ) - self.problems_repo_box: Gtk.Box = gtk_builder.get_object( - "updates_problem_repo" - ) - self.problems_label: Gtk.Label = gtk_builder.get_object( - "updates_problem_label" - ) + self.problems_repo_box: Gtk.Box = gtk_builder.get_object("updates_problem_repo") + self.problems_label: Gtk.Label = gtk_builder.get_object("updates_problem_label") # the code below relies on dicts in Python 3.6+ keeping the # order of items @@ -140,9 +136,7 @@ def _load_data(self): self.repos = {} self.problems_repo_box.set_visible(True) self.problems_label.set_text( - self.problems_label.get_text() - + _(" Encountered error: ") - + str(ex) + self.problems_label.get_text() + _(" Encountered error: ") + str(ex) ) def _load_state(self): @@ -170,9 +164,7 @@ def _set_repository(self, repository, state): action = "Enable" if state else "Disable" result = self._run_qrexec_repo(f"qubes.repos.{action}", repository) if result != "ok\n": - raise RuntimeError( - "qrexec call stdout did not contain 'ok' as expected" - ) + raise RuntimeError("qrexec call stdout did not contain 'ok' as expected") def get_unsaved(self) -> str: """Get human-readable description of unsaved changes, or @@ -273,10 +265,7 @@ def __init__(self, gtk_builder: Gtk.Builder, qapp: qubesadmin.Qubes): for vm in self.qapp.domains: if vm.klass == "AdminVM": continue - if ( - get_boolean_feature(vm, self.FEATURE_NAME, True) - != self.initial_default - ): + if get_boolean_feature(vm, self.FEATURE_NAME, True) != self.initial_default: self.initial_exceptions.append(vm) if self.initial_default: @@ -299,9 +288,7 @@ def __init__(self, gtk_builder: Gtk.Builder, qapp: qubesadmin.Qubes): self.flowbox_handler.set_visible(self.exceptions_check.get_active()) - self.exceptions_check.connect( - "toggled", self._enable_exceptions_clicked - ) + self.exceptions_check.connect("toggled", self._enable_exceptions_clicked) def _set_label(self): if self.enable_radio.get_active(): @@ -491,10 +478,8 @@ def _needs_updatevm_filter(vm): def load_rules(self): """Load rules into widgets.""" - self.rules, self.current_token = ( - self.policy_manager.get_rules_from_filename( - self.policy_file_name, "" - ) + self.rules, self.current_token = self.policy_manager.get_rules_from_filename( + self.policy_file_name, "" ) def_updatevm = self.default_updatevm def_whonix_updatevm = None @@ -582,9 +567,7 @@ def is_changed(self) -> bool: return True if self.whonix_updatevm_model.is_changed(): return True - if [ - rule.raw_rule for rule in self.current_exception_rules - ] != self.rules[:-2]: + if [rule.raw_rule for rule in self.current_exception_rules] != self.rules[:-2]: return True return False @@ -621,8 +604,7 @@ def save(self): service=self.service_name, source="@type:TemplateVM", target="@default", - action="allow " - f"target={self.updatevm_model.get_selected()}", + action="allow " f"target={self.updatevm_model.get_selected()}", ) ) new_update_proxies.add(self.updatevm_model.get_selected()) diff --git a/qubes_config/global_config/usb_devices.py b/qubes_config/global_config/usb_devices.py index db3d834f..096613e8 100644 --- a/qubes_config/global_config/usb_devices.py +++ b/qubes_config/global_config/usb_devices.py @@ -54,9 +54,7 @@ class InputActionWidget(Gtk.Box): """A simple widget for a combobox for policy actions.""" - def __init__( - self, rule: RuleTargetedAdminVM, action_choices: Dict[str, str] - ): + def __init__(self, rule: RuleTargetedAdminVM, action_choices: Dict[str, str]): """ :param rule: wrapped policy rule :param action_choices: Dictionary of "nice rule name": "actual action" @@ -159,14 +157,10 @@ def load_rules(self): qubes.InputTablet * {vm.name} @adminvm deny""" for col_num, vm in enumerate(self.usb_qubes): - self.policy_grid.attach( - TokenName(vm.name, self.qapp), 1 + col_num, 0, 1, 1 - ) + self.policy_grid.attach(TokenName(vm.name, self.qapp), 1 + col_num, 0, 1, 1) - self.rules, self.current_token = ( - self.policy_manager.get_rules_from_filename( - self.policy_file_name, self.default_policy - ) + self.rules, self.current_token = self.policy_manager.get_rules_from_filename( + self.policy_file_name, self.default_policy ) for rule in self.rules: @@ -209,9 +203,7 @@ def load_rules(self): self.policy_grid.show_all() def _warn(self, error_descr: str): - self.warn_label.set_text( - self.warn_label.get_text() + "\n" + error_descr - ) + self.warn_label.set_text(self.warn_label.get_text() + "\n" + error_descr) self.warn_box.set_visible(True) def save(self): @@ -224,9 +216,7 @@ def save(self): widget.rule.action = widget.model.get_selected() rules.append(widget.rule.raw_rule) - self.policy_manager.save_rules( - self.policy_file_name, rules, self.current_token - ) + self.policy_manager.save_rules(self.policy_file_name, rules, self.current_token) _r, self.current_token = self.policy_manager.get_rules_from_filename( self.policy_file_name, self.default_policy ) @@ -289,16 +279,12 @@ def __init__( self.enable_check: Gtk.CheckButton = gtk_builder.get_object( "usb_u2f_enable_check" ) # general enable - self.box: Gtk.Box = gtk_builder.get_object( - "usb_u2f_enable_box" - ) # general box + self.box: Gtk.Box = gtk_builder.get_object("usb_u2f_enable_box") # general box self.register_check: Gtk.CheckButton = gtk_builder.get_object( "usb_u2f_register_check" ) - self.register_box: Gtk.Box = gtk_builder.get_object( - "usb_u2f_register_box" - ) + self.register_box: Gtk.Box = gtk_builder.get_object("usb_u2f_register_box") self.register_all_radio: Gtk.RadioButton = gtk_builder.get_object( "usb_u2f_register_all_radio" ) @@ -310,9 +296,7 @@ def __init__( "usb_u2f_blanket_check" ) - self.usb_qube_combo: Gtk.ComboBox = gtk_builder.get_object( - "u2f_usb_combo" - ) + self.usb_qube_combo: Gtk.ComboBox = gtk_builder.get_object("u2f_usb_combo") self.initially_enabled_vms: List[qubesadmin.vm.QubesVM] = [] self.available_vms: List[qubesadmin.vm.QubesVM] = [] @@ -366,9 +350,7 @@ def __init__( self.initial_enable_state: bool = self.enable_check.get_active() self.initial_register_state: bool = self.register_check.get_active() - self.initial_register_all_state: bool = ( - self.register_all_radio.get_active() - ) + self.initial_register_all_state: bool = self.register_all_radio.get_active() self.initial_blanket_check_state: bool = self.blanket_check.get_active() self.conflict_file_handler = ConflictFileHandler( @@ -384,9 +366,7 @@ def __init__( ) if self.usb_qube_model: - self.usb_qube_model.connect_change_callback( - self.load_rules_for_usb_qube - ) + self.usb_qube_model.connect_change_callback(self.load_rules_for_usb_qube) @staticmethod def _enable_clicked( @@ -411,10 +391,8 @@ def _initialize_data(self): self.problem_fatal_box.set_visible(False) # guess at the current sys-usb - self.rules, self.current_token = ( - self.policy_manager.get_rules_from_filename( - self.policy_filename, self.default_policy - ) + self.rules, self.current_token = self.policy_manager.get_rules_from_filename( + self.policy_filename, self.default_policy ) if not self.usb_qubes: @@ -426,9 +404,7 @@ def _initialize_data(self): usb_qube_candidates = set() for qube in self.usb_qubes: - if qube.features.check_with_template( - self.SUPPORTED_SERVICE_FEATURE - ): + if qube.features.check_with_template(self.SUPPORTED_SERVICE_FEATURE): usb_qube_candidates.add(qube) self.usb_qubes = usb_qube_candidates @@ -673,9 +649,7 @@ def save(self): ) ) - self.policy_manager.save_rules( - self.policy_filename, rules, self.current_token - ) + self.policy_manager.save_rules(self.policy_filename, rules, self.current_token) self._initialize_data() def reset(self): @@ -710,23 +684,16 @@ def get_unsaved(self) -> str: if self.initial_register_state != self.register_check.get_active(): unsaved.append(_("U2F key registration settings changed")) - elif ( - self.initial_register_all_state - != self.register_all_radio.get_active() - ): + elif self.initial_register_all_state != self.register_all_radio.get_active(): unsaved.append(_("U2F key registration settings changed")) - elif ( - self.register_some_handler.selected_vms != self.initial_register_vms - ): + elif self.register_some_handler.selected_vms != self.initial_register_vms: unsaved.append(_("U2F key registration settings changed")) if ( self.initial_blanket_check_state != self.blanket_check.get_active() or self.blanket_handler.selected_vms != self.initial_blanket_vms ): - unsaved.append( - _("List of qubes with unrestricted U2F key access changed") - ) + unsaved.append(_("List of qubes with unrestricted U2F key access changed")) return "\n".join(unsaved) diff --git a/qubes_config/global_config/vm_flowbox.py b/qubes_config/global_config/vm_flowbox.py index 3e4cb946..03e88fd2 100644 --- a/qubes_config/global_config/vm_flowbox.py +++ b/qubes_config/global_config/vm_flowbox.py @@ -79,7 +79,9 @@ def __init__(self, vm: qubesadmin.vm.QubesVM): def _remove_self(self, *_args): response = ask_question( - self, _("Delete"), _("Are you sure you want to remove this qube?") + self, + _("Delete"), + _("Are you sure you want to remove this qube from the list?"), ) if response == Gtk.ResponseType.NO: return @@ -96,10 +98,7 @@ class VMFlowboxHandler: Handler for the flowbox itself. Requires the following widgets: - {prefix}_flowbox - the flowbox widget - {prefix}_box - Box containing the entire thing - - {prefix}_add_box = Box containing the "add new exception" combo - {prefix}_qube_combo - combobox to select a qube to add - - {prefix}_add_cancel - cancel adding new qube button - - {prefix}_add_confirm - confirm adding a new qube button - {prefix}_add_button - add new qube button """ @@ -110,9 +109,7 @@ def __init__( prefix: str, initial_vms: List[qubesadmin.vm.QubesVM], filter_function: Optional[Callable] = None, - verification_callback: Optional[ - Callable[[qubesadmin.vm.QubesVM], bool] - ] = None, + verification_callback: Optional[Callable[[qubesadmin.vm.QubesVM], bool]] = None, ): """ :param gtk_builder: Gtk.Builder @@ -126,24 +123,14 @@ def __init__( """ self.qapp = qapp self.verification_callback = verification_callback + self._change_callback: Callable | None = None self.flowbox: Gtk.FlowBox = gtk_builder.get_object(f"{prefix}_flowbox") self.box: Gtk.Box = gtk_builder.get_object(f"{prefix}_box") - self.add_box: Gtk.Box = gtk_builder.get_object(f"{prefix}_add_box") - self.qube_combo: Gtk.ComboBox = gtk_builder.get_object( - f"{prefix}_qube_combo" - ) + self.qube_combo: Gtk.ComboBox = gtk_builder.get_object(f"{prefix}_qube_combo") - self.add_cancel: Gtk.Button = gtk_builder.get_object( - f"{prefix}_add_cancel" - ) - self.add_confirm: Gtk.Button = gtk_builder.get_object( - f"{prefix}_add_confirm" - ) - self.add_button: Gtk.Button = gtk_builder.get_object( - f"{prefix}_add_button" - ) + self.add_button: Gtk.Button = gtk_builder.get_object(f"{prefix}_add_button") self.add_qube_model = VMListModeler( combobox=self.qube_combo, @@ -151,6 +138,8 @@ def __init__( filter_function=filter_function, ) + self.add_qube_model.connect_change_callback(self._check_for_add_validity) + self.flowbox.set_sort_func(self._sort_flowbox) self.placeholder = PlaceholderText() self.flowbox.add(self.placeholder) @@ -160,13 +149,16 @@ def __init__( self.flowbox.add(VMFlowBoxButton(vm)) self.flowbox.show_all() self.placeholder.set_visible(not bool(self._initial_vms)) - self.add_box.set_visible(False) - self.add_button.connect("clicked", self._add_button_clicked) - self.add_cancel.connect("clicked", self._add_cancel_clicked) - self.add_confirm.connect("clicked", self._add_confirm_clicked) + self.add_button.connect("clicked", self._add_confirm_clicked) self.flowbox.connect("child-removed", self._vm_removed) + def _check_for_add_validity(self, *_args): + if self.add_qube_model.get_selected() is None: + self.add_button.set_sensitive(False) + else: + self.add_button.set_sensitive(True) + @staticmethod def _sort_flowbox(child_1, child_2): vm_1 = str(child_1) @@ -175,12 +167,6 @@ def _sort_flowbox(child_1, child_2): return 0 return 1 if vm_1 > vm_2 else -1 - def _add_button_clicked(self, _widget): - self.add_box.set_visible(True) - - def _add_cancel_clicked(self, _widget): - self.add_box.set_visible(False) - def _add_confirm_clicked(self, _widget): select_vm = self.add_qube_model.get_selected() if self.verification_callback: @@ -195,22 +181,26 @@ def _add_confirm_clicked(self, _widget): return self.flowbox.add(VMFlowBoxButton(select_vm)) self.placeholder.set_visible(False) - self.add_box.set_visible(False) + if self._change_callback: + self._change_callback() def _vm_removed(self, *_args): self.placeholder.set_visible(not bool(self.selected_vms)) + if self._change_callback: + self._change_callback() def set_visible(self, state: bool): """Set flowbox to visible/usable.""" self.box.set_visible(state) - if not state: - self.add_box.set_visible(False) def add_selected_vm(self, vm): """ Add a vm to selected vms. """ self.flowbox.add(VMFlowBoxButton(vm)) + self.placeholder.set_visible(False) + if self._change_callback: + self._change_callback() @property def selected_vms(self) -> List[qubesadmin.vm.QubesVM]: @@ -251,7 +241,17 @@ def reset(self): self.placeholder.set_visible(not bool(self.selected_vms)) def clear(self): - """Remove all selected qubes""" + """Remove all selected qubes and clear selected VM""" for child in self.flowbox.get_children(): if isinstance(child, VMFlowBoxButton): self.flowbox.remove(child) + self._vm_removed() + self.add_qube_model.clear_selection() + + def set_sensitive(self, state: bool): + self.flowbox.set_sensitive(state) + self.add_button.set_sensitive(state) + self.qube_combo.set_sensitive(state) + + def connect_change_callback(self, func: Callable): + self._change_callback = func diff --git a/qubes_config/new_qube/advanced_handler.py b/qubes_config/new_qube/advanced_handler.py index 06c4d24a..f73e0ce6 100644 --- a/qubes_config/new_qube/advanced_handler.py +++ b/qubes_config/new_qube/advanced_handler.py @@ -50,16 +50,10 @@ def __init__(self, gtk_builder: Gtk.Builder, qapp: qubesadmin.Qubes): """ self.qapp = qapp - self.events: Gtk.Button = gtk_builder.get_object( - "event_button_advanced" - ) + self.events: Gtk.Button = gtk_builder.get_object("event_button_advanced") self.box: Gtk.Box = gtk_builder.get_object("advanced_box") - self.expander_icon: Gtk.Image = gtk_builder.get_object( - "advanced_expander" - ) - self.expander_label: Gtk.Label = gtk_builder.get_object( - "advanced_label" - ) + self.expander_icon: Gtk.Image = gtk_builder.get_object("advanced_expander") + self.expander_label: Gtk.Label = gtk_builder.get_object("advanced_label") self.expander_handler = ExpanderHandler( event_button=self.events, @@ -82,12 +76,8 @@ def __init__(self, gtk_builder: Gtk.Builder, qapp: qubesadmin.Qubes): "check_launch_settings" ) - self.pool: Gtk.ComboBoxText = gtk_builder.get_object( - "storage_pool_combobox" - ) - self.initram: Gtk.SpinButton = gtk_builder.get_object( - "initram_spin_button" - ) + self.pool: Gtk.ComboBoxText = gtk_builder.get_object("storage_pool_combobox") + self.initram: Gtk.SpinButton = gtk_builder.get_object("initram_spin_button") pools: Dict[str, Any] = {} for pool in self.qapp.pools.values(): diff --git a/qubes_config/new_qube/application_selector.py b/qubes_config/new_qube/application_selector.py index 575460ed..d7f69b79 100644 --- a/qubes_config/new_qube/application_selector.py +++ b/qubes_config/new_qube/application_selector.py @@ -229,9 +229,7 @@ def __init__(self, gtk_builder: Gtk.Builder, template_selector): "label_apps_explain" ) self.apps_close: Gtk.Button = gtk_builder.get_object("apps_close") - self.apps_search: Gtk.SearchEntry = gtk_builder.get_object( - "apps_search" - ) + self.apps_search: Gtk.SearchEntry = gtk_builder.get_object("apps_search") self.apps_list_placeholder: Gtk.Label = gtk_builder.get_object( "apps_list_placeholder" ) @@ -241,9 +239,7 @@ def __init__(self, gtk_builder: Gtk.Builder, template_selector): self.label_other_templates: Gtk.Label = gtk_builder.get_object( "label_other_templates" ) - self.load_all_button: Gtk.Button = gtk_builder.get_object( - "apps_list_load_all" - ) + self.load_all_button: Gtk.Button = gtk_builder.get_object("apps_list_load_all") self.change_template_msg: Gtk.Dialog = gtk_builder.get_object( "msg_change_template" @@ -259,9 +255,7 @@ def __init__(self, gtk_builder: Gtk.Builder, template_selector): ) self.target_template_name_widget: Optional[Gtk.Widget] = None - self.change_template_cancel.connect( - "clicked", self._hide_template_change - ) + self.change_template_cancel.connect("clicked", self._hide_template_change) self.change_template_ok.connect("clicked", self._do_template_change) self.change_template_msg.connect( "key_press_event", self._keypress_change_template @@ -302,9 +296,7 @@ def _cmp(a, b): def _sort_func_app_list(self, x: ApplicationRow, y: ApplicationRow): # negation because True > False, and we want the selected rows to be # at the top - selection_comparison = self._cmp( - not x.is_selected(), not y.is_selected() - ) + selection_comparison = self._cmp(not x.is_selected(), not y.is_selected()) if selection_comparison == 0: return self._cmp(x.appdata.name, y.appdata.name) return selection_comparison @@ -325,9 +317,7 @@ def _filter_func_app_list(self, x: ApplicationRow): def _filter_func_other_list(self, x: ApplicationRow): if not self.apps_list_placeholder.get_mapped(): return False - if not self.template_selector.is_given_template_available( - x.appdata.template - ): + if not self.template_selector.is_given_template_available(x.appdata.template): return False return self._filter_func_app_list(x) @@ -384,9 +374,7 @@ def fill_app_list(self, default=False): if not template_vm: return - available_applications = self.template_selector.get_available_apps( - template_vm - ) + available_applications = self.template_selector.get_available_apps(template_vm) selected = [] if default: selected = self.template_selector.get_default_apps(template_vm) @@ -417,9 +405,7 @@ def _hide_template_change(self, *_args): def _do_template_change(self, *_args): if self.target_template_name_widget: - self.template_selector.select_template( - self.target_template_name_widget.vm - ) + self.template_selector.select_template(self.target_template_name_widget.vm) self._hide_template_change() self._hide_window() diff --git a/qubes_config/new_qube/network_selector.py b/qubes_config/new_qube/network_selector.py index 6658995d..bad20f92 100644 --- a/qubes_config/new_qube/network_selector.py +++ b/qubes_config/new_qube/network_selector.py @@ -59,23 +59,15 @@ def __init__(self, gtk_builder: Gtk.Builder, qapp: qubesadmin.Qubes): QubeName(self.qapp.default_netvm), False, False, 0 ) - self.network_tor_box: Gtk.Box = gtk_builder.get_object( - "network_tor_box" - ) - self.network_custom: Gtk.RadioButton = gtk_builder.get_object( - "network_custom" - ) + self.network_tor_box: Gtk.Box = gtk_builder.get_object("network_tor_box") + self.network_custom: Gtk.RadioButton = gtk_builder.get_object("network_custom") self.network_custom_combo: Gtk.ComboBox = gtk_builder.get_object( "network_custom_combo" ) self.network_custom.connect("toggled", self._custom_toggled) - self.network_none: Gtk.RadioButton = gtk_builder.get_object( - "network_none" - ) - self.network_tor: Gtk.RadioButton = gtk_builder.get_object( - "network_tor" - ) + self.network_none: Gtk.RadioButton = gtk_builder.get_object("network_none") + self.network_tor: Gtk.RadioButton = gtk_builder.get_object("network_tor") self.network_default: Gtk.RadioButton = gtk_builder.get_object( "network_default" ) diff --git a/qubes_config/new_qube/new_qube_app.py b/qubes_config/new_qube/new_qube_app.py index c554258d..da11064e 100644 --- a/qubes_config/new_qube/new_qube_app.py +++ b/qubes_config/new_qube/new_qube_app.py @@ -94,9 +94,7 @@ def do_activate(self, *args, **kwargs): width = int(self.main_window.get_screen().get_width() * 0.9) else: # try to have at least 1100 pixels - width = min( - int(self.main_window.get_screen().get_width() * 0.9), 800 - ) + width = min(int(self.main_window.get_screen().get_width() * 0.9), 800) if ( self.main_window.get_allocated_height() > self.main_window.get_screen().get_height() * 0.9 @@ -121,9 +119,7 @@ def perform_setup(self): self.main_window = self.builder.get_object("main_window") self.qube_name: Gtk.Entry = self.builder.get_object("qube_name") - self.qube_label_combo: Gtk.ComboBox = self.builder.get_object( - "qube_label" - ) + self.qube_label_combo: Gtk.ComboBox = self.builder.get_object("qube_label") load_theme( widget=self.main_window, @@ -139,9 +135,7 @@ def perform_setup(self): self.progress_bar_dialog.update_progress(0.1) - self.qube_type_app: Gtk.RadioButton = self.builder.get_object( - "qube_type_app" - ) + self.qube_type_app: Gtk.RadioButton = self.builder.get_object("qube_type_app") self.qube_type_template: Gtk.RadioButton = self.builder.get_object( "qube_type_template" ) @@ -154,15 +148,9 @@ def perform_setup(self): self.tooltips = { "qube_type_app": self.builder.get_object("qube_type_app_q"), - "qube_type_template": self.builder.get_object( - "qube_type_template_q" - ), - "qube_type_standalone": self.builder.get_object( - "qube_type_standalone_q" - ), - "qube_type_disposable": self.builder.get_object( - "qube_type_disposable_q" - ), + "qube_type_template": self.builder.get_object("qube_type_template_q"), + "qube_type_standalone": self.builder.get_object("qube_type_standalone_q"), + "qube_type_disposable": self.builder.get_object("qube_type_disposable_q"), } self.qube_type_app.connect("toggled", self._type_selected) @@ -200,14 +188,10 @@ def perform_setup(self): self.progress_bar_dialog.update_progress(0.1) - self.create_button: Gtk.Button = self.builder.get_object( - "create_button" - ) + self.create_button: Gtk.Button = self.builder.get_object("create_button") self.create_button.connect("clicked", self._do_create_qube) - self.cancel_button: Gtk.Button = self.builder.get_object( - "cancel_button" - ) + self.cancel_button: Gtk.Button = self.builder.get_object("cancel_button") self.cancel_button.connect("clicked", self._quit) self.viewport_handler = ViewportHandler( @@ -258,9 +242,7 @@ def _type_selected(self, button: Gtk.RadioButton): else: self.network_selector.network_default.set_active(True) - self.tooltips[button_name].set_from_pixbuf( - load_icon("qubes-question", 20, 20) - ) + self.tooltips[button_name].set_from_pixbuf(load_icon("qubes-question", 20, 20)) def _do_create_qube(self, *_args): label = self.qube_label_modeler.get_selected() diff --git a/qubes_config/new_qube/template_handler.py b/qubes_config/new_qube/template_handler.py index a77d2316..b75e16a5 100644 --- a/qubes_config/new_qube/template_handler.py +++ b/qubes_config/new_qube/template_handler.py @@ -117,9 +117,7 @@ def __init__( """ super().__init__(gtk_builder, qapp) - self.label: Gtk.Label = gtk_builder.get_object( - f"label_template_{name_suffix}" - ) + self.label: Gtk.Label = gtk_builder.get_object(f"label_template_{name_suffix}") self.explain_label: Gtk.Label = gtk_builder.get_object( f"label_template_explanation_{name_suffix}" ) @@ -182,18 +180,14 @@ def __init__( """ super().__init__(gtk_builder, qapp) - self.label: Gtk.Label = gtk_builder.get_object( - f"label_template_{name_suffix}" - ) + self.label: Gtk.Label = gtk_builder.get_object(f"label_template_{name_suffix}") self.explain_label: Gtk.Label = gtk_builder.get_object( f"label_template_explanation_{name_suffix}" ) self.radio_none: Gtk.RadioButton = gtk_builder.get_object( f"radio_{name_suffix}_none" ) - self.box_template: Gtk.Box = gtk_builder.get_object( - f"box_radio_{name_suffix}" - ) + self.box_template: Gtk.Box = gtk_builder.get_object(f"box_radio_{name_suffix}") self.radio_template: Gtk.RadioButton = gtk_builder.get_object( f"radio_template_{name_suffix}" ) @@ -281,17 +275,14 @@ def __init__(self, gtk_builder: Gtk.Builder, qapp: qubesadmin.Qubes): gtk_builder=gtk_builder, qapp=self.qapp, name_suffix="standalone", - filter_function=lambda x: x.klass - in ("TemplateVM", "StandaloneVM"), + filter_function=lambda x: x.klass in ("TemplateVM", "StandaloneVM"), default_value=None, ), "qube_type_disposable": TemplateSelectorCombo( gtk_builder=gtk_builder, qapp=self.qapp, name_suffix="dispvm", - filter_function=lambda x: getattr( - x, "template_for_dispvms", False - ), + filter_function=lambda x: getattr(x, "template_for_dispvms", False), default_value=self.qapp.default_dispvm, ), } @@ -299,9 +290,7 @@ def __init__(self, gtk_builder: Gtk.Builder, qapp: qubesadmin.Qubes): self.selected_type: Optional[str] = None self.change_vm_type("qube_type_app") - self._application_data: Dict[ - qubesadmin.vm.QubesVM, List[ApplicationData] - ] = {} + self._application_data: Dict[qubesadmin.vm.QubesVM, List[ApplicationData]] = {} self._default_applications: Dict[qubesadmin.vm.QubesVM, List[str]] = {} def change_vm_type(self, vm_type: str): @@ -318,14 +307,10 @@ def get_selected_template(self) -> Optional[qubesadmin.vm.QubesVM]: return self.template_selectors[self.selected_type].get_selected_vm() return None - def is_given_template_available( - self, template: qubesadmin.vm.QubesVM - ) -> bool: + def is_given_template_available(self, template: qubesadmin.vm.QubesVM) -> bool: """Check if given qubesVM is among available templates.""" if self.selected_type: - return self.template_selectors[self.selected_type].is_vm_available( - template - ) + return self.template_selectors[self.selected_type].is_vm_available(template) return False def load_all_available_apps(self): @@ -348,9 +333,7 @@ def get_available_apps(self, vm: Optional[qubesadmin.vm.QubesVM] = None): try: available_apps = [ ApplicationData.from_line(line, template=vm) - for line in subprocess.check_output(command) - .decode() - .splitlines() + for line in subprocess.check_output(command).decode().splitlines() ] self._application_data[vm] = available_apps except subprocess.CalledProcessError: diff --git a/qubes_config/policy_editor/policy_editor.py b/qubes_config/policy_editor/policy_editor.py index 071b1034..cd91bad4 100644 --- a/qubes_config/policy_editor/policy_editor.py +++ b/qubes_config/policy_editor/policy_editor.py @@ -45,8 +45,7 @@ HEADER_NORMAL = ( - " service_name\targument\tsource_qube" - "\ttarget_qube\taction [parameter=value] " + " service_name\targument\tsource_qube\ttarget_qube\taction [parameter=value] " ) @@ -73,9 +72,7 @@ def __init__( self.dialog_window: Gtk.Dialog = builder.get_object("open_dialog") self.file_list: Gtk.ListBox = builder.get_object("open_policy_list") self.ok_button: Gtk.Button = builder.get_object("open_button_ok") - self.cancel_button: Gtk.Button = builder.get_object( - "open_button_cancel" - ) + self.cancel_button: Gtk.Button = builder.get_object("open_button_cancel") self.file_list.connect("row-activated", self._ok) @@ -133,10 +130,7 @@ def policy_list(self): with include/""" file_list = self.policy_client.policy_list() file_list.extend( - [ - "include/" + name - for name in self.policy_client.policy_include_list() - ] + ["include/" + name for name in self.policy_client.policy_include_list()] ) return file_list @@ -186,15 +180,11 @@ def perform_setup(self): """ The function that performs actual widget realization and setup. """ - self.clipboard: Gtk.Clipboard = Gtk.Clipboard.get( - Gdk.SELECTION_CLIPBOARD - ) + self.clipboard: Gtk.Clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) self.builder = Gtk.Builder() - glade_ref = ( - importlib.resources.files("qubes_config") / "policy_editor.glade" - ) + glade_ref = importlib.resources.files("qubes_config") / "policy_editor.glade" with importlib.resources.as_file(glade_ref) as path: self.builder.add_from_file(str(path)) @@ -202,9 +192,7 @@ def perform_setup(self): self.builder, self.policy_client, self.open_policy_file ) - self.main_window: Gtk.ApplicationWindow = self.builder.get_object( - "main_window" - ) + self.main_window: Gtk.ApplicationWindow = self.builder.get_object("main_window") # Reserving 3 pixels for window border on all sides (most themes use 2) # Reserving 32x2 pixels for taskbar (default on top) and Window title. @@ -212,9 +200,7 @@ def perform_setup(self): self.main_window.set_size_request(1024 - 3 * 2, 768 - 3 * 2 - 32 * 2) width = min(1920, self.main_window.get_screen().get_width()) height = min(1280, self.main_window.get_screen().get_height()) - self.main_window.set_default_size( - width - 3 * 2, height - 3 * 2 - 32 * 2 - ) + self.main_window.set_default_size(width - 3 * 2, height - 3 * 2 - 32 * 2) # ToDo: Considering maximizing by default as it packs too much info. # self.main_window.maximize() @@ -229,9 +215,7 @@ def perform_setup(self): self.header_view.set_monospace(True) self.header_view.set_editable(False) - self.source_viewport: Gtk.Viewport = self.builder.get_object( - "source_viewport" - ) + self.source_viewport: Gtk.Viewport = self.builder.get_object("source_viewport") self.source_view = GtkSource.View() self.source_buffer: GtkSource.Buffer = self.source_view.get_buffer() @@ -239,15 +223,9 @@ def perform_setup(self): self.source_view.set_hexpand(True) self.source_viewport.add(self.source_view) - self.help_window: Gtk.ScrolledWindow = self.builder.get_object( - "help_window" - ) - self.about_window: Gtk.AboutDialog = self.builder.get_object( - "about_window" - ) - self.about_window.connect( - "response", lambda *_args: self.about_window.hide() - ) + self.help_window: Gtk.ScrolledWindow = self.builder.get_object("help_window") + self.about_window: Gtk.AboutDialog = self.builder.get_object("about_window") + self.about_window.connect("response", lambda *_args: self.about_window.hide()) self.about_window.connect("activate-link", self._open_docs) self.error_info: Gtk.Label = self.builder.get_object("error_info") @@ -311,33 +289,19 @@ def setup_menu(self): file_item = self._get_menu_item("_File") file_item.set_submenu(file_menu) file_menu.add(self._get_menu_item_with_ac("_New", "win.new", Gdk.KEY_n)) - file_menu.add( - self._get_menu_item_with_ac("_Open", "win.open", Gdk.KEY_o) - ) - file_menu.add( - self._get_menu_item_with_ac("_Save", "win.save", Gdk.KEY_s) - ) - file_menu.add( - self._get_menu_item_with_ac("_Quit", "win.quit", Gdk.KEY_q) - ) + file_menu.add(self._get_menu_item_with_ac("_Open", "win.open", Gdk.KEY_o)) + file_menu.add(self._get_menu_item_with_ac("_Save", "win.save", Gdk.KEY_s)) + file_menu.add(self._get_menu_item_with_ac("_Quit", "win.quit", Gdk.KEY_q)) self.menu_bar.add(file_item) # Edit edit_menu = Gtk.Menu() edit_item = self._get_menu_item("_Edit") edit_item.set_submenu(edit_menu) - edit_menu.add( - self._get_menu_item_with_ac("_Redo", "win.redo", Gdk.KEY_y) - ) - edit_menu.add( - self._get_menu_item_with_ac("_Undo", "win.undo", Gdk.KEY_z) - ) - edit_menu.add( - self._get_menu_item_with_ac("_Copy", "win.copy", Gdk.KEY_c) - ) - edit_menu.add( - self._get_menu_item_with_ac("_Paste", "win.paste", Gdk.KEY_v) - ) + edit_menu.add(self._get_menu_item_with_ac("_Redo", "win.redo", Gdk.KEY_y)) + edit_menu.add(self._get_menu_item_with_ac("_Undo", "win.undo", Gdk.KEY_z)) + edit_menu.add(self._get_menu_item_with_ac("_Copy", "win.copy", Gdk.KEY_c)) + edit_menu.add(self._get_menu_item_with_ac("_Paste", "win.paste", Gdk.KEY_v)) edit_menu.add(self._get_menu_item_with_ac("Re_set", "win.reset", None)) self.menu_bar.add(edit_item) @@ -346,9 +310,7 @@ def setup_menu(self): help_item = self._get_menu_item("_Help") help_item.set_submenu(help_menu) help_menu.add( - self._get_menu_item_with_ac( - "_Show/Hide Help", "win.help", Gdk.KEY_h - ) + self._get_menu_item_with_ac("_Show/Hide Help", "win.help", Gdk.KEY_h) ) help_menu.add(self._get_menu_item_with_ac("_About", "win.about", None)) self.menu_bar.add(help_item) @@ -489,9 +451,7 @@ def _save(self, *_args): self.filename, self.policy_text, self.token ) except subprocess.CalledProcessError as ex: - err_msg = ( - "An error occurred while trying to save the policy file:\n" - ) + err_msg = "An error occurred while trying to save the policy file:\n" if ex.stdout: err_msg += ex.stdout.decode() else: @@ -619,9 +579,7 @@ def _text_changed(self, *_args): if errors: self.error_info.get_style_context().remove_class("error_ok") self.error_info.get_style_context().add_class("error_bad") - self.error_info.set_markup( - "Errors found:\n" + "\n".join(errors) - ) + self.error_info.set_markup("Errors found:\n" + "\n".join(errors)) else: self.error_info.get_style_context().remove_class("error_bad") self.error_info.get_style_context().add_class("error_ok") diff --git a/qubes_config/qubes-global-config-base.css b/qubes_config/qubes-global-config-base.css index a3c598cd..0b2a75e4 100644 --- a/qubes_config/qubes-global-config-base.css +++ b/qubes_config/qubes-global-config-base.css @@ -28,9 +28,9 @@ separator { border-style: solid; padding-left: 30px; padding-right: 30px; - padding-top: 30px; + padding-top: 10px; box-shadow: none; - padding-bottom: 50px; + padding-bottom: 10px; } #main_notebook { @@ -145,11 +145,23 @@ separator { padding-bottom: 5px; } +.alternate_rows row:nth-child(even) { + background: @alt-row; +} + +.alternate_rows row:selected { + background: @qubes-blue; +} + .permission_row { background: @top-background-2; padding:5px; } +.error_row { + background: @problem-background; +} + .permission_row:hover { background: @qubes-blue; } @@ -255,3 +267,19 @@ separator { .info_grid { margin: 20px; } + +.add_rule_box { + padding: 10px 10px 10px 10px; + margin: 5px 0px 5px 0px; + background: white; + border-width: 2px; + border-style: solid; + border-radius: 4px; + border-color: @dark-gray; +} + +.error_label { + color: @red-label; + font-style: italic; + font-weight: bold; +} \ No newline at end of file diff --git a/qubes_config/tests/conftest.py b/qubes_config/tests/conftest.py index e08e1435..386abb3d 100644 --- a/qubes_config/tests/conftest.py +++ b/qubes_config/tests/conftest.py @@ -19,11 +19,12 @@ # with this program; if not, see . """Conftest helper pytest file: fixtures container here are reachable by all tests""" +# pylint: disable=protected-access +# pylint: disable=redefined-outer-name + import pytest import importlib.resources import subprocess -from typing import Mapping, Union, Tuple, List -from qubesadmin.tests import QubesTest import gi @@ -35,567 +36,184 @@ from ..global_config.policy_manager import PolicyManager from ..new_qube.new_qube_app import CreateNewQube -default_vm_properties = { - "autostart": ("bool", True, "False"), - "backup_timestamp": ("int", True, ""), - "debug": ("bool", True, "False"), - "default_user": ("str", True, "user"), - "dns": ("str", True, "10.139.1.1 10.139.1.2"), - "gateway": ("str", True, ""), - "gateway6": ("str", True, ""), - "icon": ("str", True, "appvm-green"), - "include_in_backups": ("bool", True, "True"), - "installed_by_rpm": ("bool", True, "False"), - "ip": ("str", True, "10.137.0.2"), - "ip6": ("str", True, ""), - "kernel": ("str", True, "5.15.52-1.fc32"), - "keyboard_layout": ("str", True, "us++"), - "klass": ("str", True, "AppVM"), - "label": ("label", False, "green"), - "mac": ("str", True, "00:16:3e:5e:6c:00"), - "maxmem": ("int", True, "4000"), - "memory": ("int", True, "400"), - "name": ("str", False, "testvm"), - "provides_network": ("bool", True, "False"), - "qid": ("int", False, "2"), - "qrexec_timeout": ("int", True, "60"), - "shutdown_timeout": ("int", True, "60"), - "start_time": ("str", True, ""), - "stubdom_mem": ("int", True, ""), - "stubdom_xid": ("str", True, "-1"), - "template_for_dispvms": ("bool", True, "False"), - "updateable": ("bool", True, "False"), - "uuid": ("str", False, "8fd73e95-a74b-4bf0-a87d-9978dbd1d8a4"), - "vcpus": ("int", True, "2"), - "virt_mode": ("str", True, "pvh"), - "visible_gateway": ("str", True, "10.137.0.1"), - "visible_gateway6": ("str", True, ""), - "visible_ip": ("str", True, "10.137.0.2"), - "visible_ip6": ("str", True, ""), - "visible_netmask": ("str", True, "255.255.255.255"), - "xid": ("str", True, "2"), - "audiovm": ("vm", True, "dom0"), - "default_dispvm": ("vm", False, "default-dvm"), - "guivm": ("vm", True, "dom0"), - "kernelopts": ("str", True, ""), - "management_dispvm": ("vm", True, "default-mgmt-dvm"), - "netvm": ("vm", False, "sys-firewall"), - "template": ("vm", False, "fedora-36"), - "auto_cleanup": ("bool", True, "False"), -} - -possible_tags = ["whonix-updatevm", "anon-gateway"] - - -def add_expected_vm( - qapp, - name: str, - klass: str, - properties: Mapping[str, Union[bool, str, int, Tuple[str, bool, str]]], - features, - tags, -): - """Generate expected_calls entries to get info about a VM - :param qapp: QubesTest object - :param name: name of the VM - :param klass: class of the VM (AppVM, TemplateVM etc) - :param properties: dict of properties; values can be either a value - directly, or tuple of (type, is_default, value) - :param features: dict of expected features - use 'None' as value for - feature that will be checked but is not present - :param tags: list of tags - :return: - """ - vm_list_call = ("dom0", "admin.vm.List", None, None) - vm_list = b"0\x00" - if vm_list_call in qapp.expected_calls: - vm_list = qapp.expected_calls[vm_list_call] - vm_list += f"{name} class={klass} state=Halted\n".encode() - qapp.expected_calls[vm_list_call] = vm_list - properties_getall = b"0\x00" - combined_properties = default_vm_properties.copy() - for prop, value in properties.items(): - if isinstance(value, tuple): - combined_properties[prop] = value - elif value is None: - try: - del combined_properties[prop] - except KeyError: - pass - elif prop in combined_properties: - combined_properties[prop] = ( - combined_properties[prop][0], - combined_properties[prop][1], - str(value), - ) - else: - raise KeyError(f"Unknown property '{prop}'") - - for prop, value in combined_properties.items(): - if prop == "template" and klass in ("TemplateVM", "StandaloneVM"): - qapp.expected_calls[(name, "admin.vm.property.Get", prop, None)] = ( - b"2\x00QubesNoSuchPropertyError\x00\x00No such property\x00" - ) - continue - prop_line = f"default={value[1]} type={value[0]} {value[2]}" - properties_getall += (f"{prop} " + prop_line + "\n").encode() - qapp.expected_calls[(name, "admin.vm.property.Get", prop, None)] = ( - b"0\x00" + prop_line.encode() - ) - - qapp.expected_calls[(name, "admin.vm.feature.List", None, None)] = ( - "0\x00" - + "".join( - f"{feature}\n" - for feature, value in features.items() - if value is not None - ) - ).encode() - for feature, value in features.items(): - if value is None: - qapp.expected_calls[ - (name, "admin.vm.feature.Get", feature, None) - ] = ( - b"2\x00QubesFeatureNotFoundError\x00\x00" - + str(feature).encode() - + b"\x00" - ) - else: - qapp.expected_calls[ - (name, "admin.vm.feature.Get", feature, None) - ] = (b"0\x00" + str(value).encode()) - - qapp.expected_calls[(name, "admin.vm.tag.List", None, None)] = ( - "0\x00" + "".join(f"{tag}\n" for tag in tags) - ).encode() - - for tag in possible_tags: - qapp.expected_calls[(name, "admin.vm.tag.Get", tag, None)] = b"0\x000" - - for tag in tags: - qapp.expected_calls[(name, "admin.vm.tag.Get", tag, None)] = b"0\x001" - - qapp.expected_calls[(name, "admin.vm.device.pci.List", None, None)] = ( - b"0\x00" - ) - - -def add_dom0_vm_property(qapp, prop_name, prop_value): - """Add a vm property to dom0""" - if not prop_value: - prop_value = "" - qapp.expected_calls[("dom0", "admin.property.Get", prop_name, None)] = ( - b"0\x00" + f"default=True type=vm {prop_value}".encode() - ) - - -def add_dom0_text_property(qapp, prop_name, prop_value): - """Add a str property to dom0""" - qapp.expected_calls[("dom0", "admin.property.Get", prop_name, None)] = ( - b"0\x00" + f"default=True type=str {prop_value}".encode() - ) - - -def add_dom0_feature(qapp, feature, feature_value): - """Add dom0 feature""" - if feature_value is not None: - qapp.expected_calls[("dom0", "admin.vm.feature.Get", feature, None)] = ( - b"0\x00" + f"{feature_value}".encode() - ) - else: - qapp.expected_calls[("dom0", "admin.vm.feature.Get", feature, None)] = ( - b"2\x00QubesFeatureNotFoundError\x00\x00" - + str(feature).encode() - + b"\x00" - ) - - -def add_feature_with_template_to_all( - qapp, feature_name, enable_vm_names: List[str] -): - """Add possibility of checking for a feature with templated to all qubes; - those listed in enabled_vm_names will have it set to 1, others will - have it absent.""" - for vm in qapp.domains: - if vm.name in enable_vm_names: - result = b"0\x001" - else: - result = ( - b"2\x00QubesFeatureNotFoundError\x00\x00" - + str(feature_name).encode() - + b"\x00" - ) - qapp.expected_calls[ - (vm, "admin.vm.feature.CheckWithTemplate", feature_name, None) - ] = result - - -def add_feature_to_all(qapp, feature_name, enable_vm_names: List[str]): - """Add possibility of checking for a feature to all qubes; those listed - in enabled_vm_names will have it set to 1, others will have it absent.""" - for vm in qapp.domains: - if vm.name in enable_vm_names: - result = b"0\x001" - else: - result = ( - b"2\x00QubesFeatureNotFoundError\x00\x00" - + str(feature_name).encode() - + b"\x00" - ) - qapp.expected_calls[ - (vm, "admin.vm.feature.Get", feature_name, None) - ] = result +from qubesadmin.tests.mock_app import ( + MockQubesComplete, + MockQube, + MockQubes, + MockQubesWhonix, + QubesTestWrapper, + GLOBAL_PROPERTIES, + MockDevice, +) @pytest.fixture def test_qapp(): - return test_qapp_impl() - - -def test_qapp_impl(): - """Test QubesApp""" - qapp = QubesTest() - qapp._local_name = "dom0" # pylint: disable=protected-access - - add_dom0_vm_property(qapp, "clockvm", "sys-net") - add_dom0_vm_property(qapp, "updatevm", "sys-net") - add_dom0_vm_property(qapp, "default_netvm", "sys-net") - add_dom0_vm_property(qapp, "default_template", "fedora-36") - add_dom0_vm_property(qapp, "default_dispvm", "fedora-36") - - add_dom0_text_property(qapp, "default_kernel", "1.1") - add_dom0_text_property(qapp, "default_pool", "file") - - add_dom0_feature(qapp, "gui-default-allow-fullscreen", "") - add_dom0_feature(qapp, "gui-default-allow-utf8-titles", "") - add_dom0_feature(qapp, "gui-default-trayicon-mode", "") - - # setup labels - qapp.expected_calls[("dom0", "admin.label.List", None, None)] = ( - b"0\x00red\nblue\ngreen\n" - ) - - # setup pools: - qapp.expected_calls[("dom0", "admin.pool.List", None, None)] = ( - b"0\x00linux-kernel\nlvm\nfile\n" - ) - qapp.expected_calls[ - ("dom0", "admin.pool.volume.List", "linux-kernel", None) - ] = b"0\x001.1\nmisc\n4.2\n" - - add_expected_vm( - qapp, - "dom0", - "AdminVM", - {}, - { - "service.qubes-update-check": 1, - "config.default.qubes-update-check": None, - "config-usbvm-name": None, - "gui-default-secure-copy-sequence": None, - "gui-default-secure-paste-sequence": None, - }, - [], - ) - add_expected_vm( - qapp, - "sys-net", - "AppVM", - {"provides_network": ("bool", False, "True")}, - {"service.qubes-update-check": None, "service.qubes-updates-proxy": 1}, - [], - ) - - add_expected_vm( - qapp, - "sys-firewall", - "AppVM", - {"provides_network": ("bool", False, "True")}, - {"service.qubes-update-check": None}, - [], - ) - - add_expected_vm( - qapp, "sys-usb", "AppVM", {}, {"service.qubes-update-check": None}, [] - ) - - add_expected_vm( - qapp, - "fedora-36", - "TemplateVM", - {"netvm": ("vm", False, "")}, - {"service.qubes-update-check": None}, - [], - ) - - add_expected_vm( - qapp, - "fedora-35", - "TemplateVM", - {"netvm": ("vm", False, "")}, - {"service.qubes-update-check": None}, - [], - ) - - add_expected_vm( - qapp, - "default-dvm", - "DispVM", - {"template_for_dispvms": ("bool", False, "True")}, - {"service.qubes-update-check": None}, - [], - ) + test_qapp = MockQubesComplete() + test_qapp._qubes["dom0"].features["gui-default-secure-copy-sequence"] = None + test_qapp._qubes["sys-usb"].features["supported-feature.keyboard-layout"] = "1" + test_qapp.update_vm_calls() + return test_qapp - add_expected_vm( - qapp, - "disp123", - "DispVM", - {"auto_cleanup": ("bool", False, "True")}, - {"service.qubes-update-check": None}, - [], - ) - # This DispVM will die suddently in the middle of Global Config running - add_expected_vm( - qapp, - "disp666", - "DispVM", - {}, - {"service.qubes-update-check": None}, - [], - ) +@pytest.fixture +def test_qapp_simple(): + test_qapp_simple = MockQubes() + return test_qapp_simple - add_expected_vm( - qapp, "test-vm", "AppVM", {}, {"service.qubes-update-check": None}, [] - ) - add_expected_vm( - qapp, - "test-blue", - "AppVM", - {"label": ("str", False, "blue")}, - {"service.qubes-update-check": None}, - [], - ) +@pytest.fixture +def test_qapp_whonix(): + test_qapp_whonix = MockQubesWhonix() + return test_qapp_whonix - add_expected_vm( - qapp, - "test-red", - "AppVM", - {"label": ("str", False, "red")}, - {"service.qubes-update-check": None}, - [], - ) - add_expected_vm( - qapp, - "test-standalone", - "StandaloneVM", - {"label": ("str", False, "green")}, - {"service.qubes-update-check": None}, - [], - ) +@pytest.fixture +def test_qapp_broken(): # pylint: disable=redefined-outer-name + """A qapp with no templates, no sys-net""" + # pylint does not understand fixtures + qapp = QubesTestWrapper() - add_expected_vm( - qapp, - "vault", - "AppVM", - {"netvm": ("vm", False, "")}, - {"service.qubes-update-check": None}, - [], - ) + qapp._global_properties = GLOBAL_PROPERTIES.copy() - add_feature_with_template_to_all( - qapp, - "supported-service.qubes-u2f-proxy", - ["test-vm", "fedora-35", "sys-usb"], - ) - add_feature_with_template_to_all( - qapp, "service.updates-proxy-setup", ["fedora-36", "fedora-35"] - ) - add_feature_to_all(qapp, "service.qubes-u2f-proxy", ["test-vm"]) + qapp.set_global_property("clockvm", "") + qapp.set_global_property("default_dispvm", "") + qapp.set_global_property("default_netvm", "") + qapp.set_global_property("default_template", "") + qapp.set_global_property("updatevm", "") - for vm in qapp.domains: - qapp.expected_calls[ - (vm.name, "admin.vm.device.pci.Attached", None, None) - ] = b"0\x00" + qapp.update_global_properties() + qapp.update_vm_calls() return qapp @pytest.fixture -def test_qapp_whonix(test_qapp): # pylint: disable=redefined-outer-name - # pylint does not understand fixtures - """Testing qapp with whonix vms added""" - add_expected_vm( - test_qapp, - "sys-whonix", - "AppVM", - {}, - {"service.qubes-update-check": None, "service.qubes-updates-proxy": 1}, - ["anon-gateway"], - ) - add_expected_vm( - test_qapp, - "anon-whonix", - "AppVM", - {}, - {"service.qubes-update-check": None}, - ["anon-gateway"], - ) - add_expected_vm( - test_qapp, - "whonix-gw-15", - "TemplateVM", - {"netvm": ("vm", False, "")}, - {"service.qubes-update-check": None}, - ["whonix-updatevm"], +def test_qapp_devices(): + test_qapp_devices = MockQubesComplete() + + test_qapp_devices._qubes["test-dev"] = MockQube( + name="test-dev", + qapp=test_qapp_devices, + label="purple", + devices_denied="m******", + ) + test_qapp_devices._qubes["test-dev2"] = MockQube( + name="test-dev2", + qapp=test_qapp_devices, + label="green", + devices_denied="u02****p0703**p02****ue0****p123***", + ) + + test_qapp_devices.update_vm_calls() + + # add some PCI devices + test_qapp_devices._devices.append( + MockDevice( + test_qapp_devices, + dev_class="pci", + device_id="0x8086:0x51f0::p028000", + product="Network Card", + vendor="unknown", + backend_vm="dom0", + assigned=[("sys-net", "required", None)], + port="0c.0", + ) ) - add_expected_vm( - test_qapp, - "whonix-gw-14", - "TemplateVM", - {"netvm": ("vm", False, "")}, - {"service.qubes-update-check": None}, - ["whonix-updatevm"], + test_qapp_devices._devices.append( + MockDevice( + test_qapp_devices, + dev_class="pci", + product="USB Controller", + backend_vm="dom0", + assigned=[("sys-net", "required", None)], + device_id="0x8086:0x461e::p0c0330", + port="0d.0", + vendor="ACME", + ) ) - test_qapp.domains.clear_cache() - add_feature_with_template_to_all( - test_qapp, - "service.updates-proxy-setup", - ["fedora-36", "fedora-35", "whonix-gw-15", "whonix-gw-14"], + test_qapp_devices._devices.append( + MockDevice( + test_qapp_devices, + dev_class="pci", + device_id="0x8086:0x51f0::p300000", + product="Piano Dropper", + vendor="ACME", + backend_vm="dom0", + port="0f.0", + ) ) - return test_qapp - - -@pytest.fixture -def test_qapp_simple(): # pylint: disable=redefined-outer-name - """A qapp with only one template, one sys-net and that's all""" - # pylint does not understand fixtures - qapp = QubesTest() - qapp._local_name = "dom0" # pylint: disable=protected-access - - add_dom0_vm_property(qapp, "clockvm", "sys-net") - add_dom0_vm_property(qapp, "updatevm", "sys-net") - add_dom0_vm_property(qapp, "default_netvm", "sys-net") - add_dom0_vm_property(qapp, "default_template", "fedora-36") - add_dom0_vm_property(qapp, "default_dispvm", "fedora-36") - - add_dom0_text_property(qapp, "default_kernel", "1.1") - add_dom0_text_property(qapp, "default_pool", "file") - - add_dom0_feature(qapp, "gui-default-allow-fullscreen", "") - add_dom0_feature(qapp, "gui-default-allow-utf8-titles", "") - add_dom0_feature(qapp, "gui-default-trayicon-mode", "") - - # setup labels - qapp.expected_calls[("dom0", "admin.label.List", None, None)] = ( - b"0\x00red\nblue\ngreen\n" + # and one assigned with no-strict-reset + test_qapp_devices._devices.append( + MockDevice( + test_qapp_devices, + dev_class="pci", + device_id="0x8086:0x51c8::p040300", + product="Whole Symphonic Orchestra", + vendor="Berlin Philharmonie", + backend_vm="dom0", + assigned=[("test-red", "required", ["no-strict-reset"])], + port="0h.0", + ) ) - # setup pools: - qapp.expected_calls[("dom0", "admin.pool.List", None, None)] = ( - b"0\x00linux-kernel\nlvm\nfile\n" - ) - qapp.expected_calls[ - ("dom0", "admin.pool.volume.List", "linux-kernel", None) - ] = b"0\x001.1\nmisc\n4.2\n" - - add_expected_vm( - qapp, - "dom0", - "AdminVM", - {}, - { - "service.qubes-update-check": 1, - "config.default.qubes-update-check": None, - "config-usbvm-name": None, - "gui-default-secure-copy-sequence": None, - "gui-default-secure-paste-sequence": None, - }, - [], + # add two pre-assigned devices + test_qapp_devices._devices.append( + MockDevice( + test_qapp_devices, + dev_class="usb", + product="Anvil", + vendor="ACME", + backend_vm="sys-usb", + assigned=[ + ("test-vm", "ask-to-attach", None), + ("test-red", "ask-to-attach", None), + ], + device_id="3:4:b011010", + port="2-22", + ) ) - add_expected_vm( - qapp, - "sys-net", - "AppVM", - {"provides_network": ("bool", False, "True")}, - {"service.qubes-update-check": None, "service.qubes-updates-proxy": 1}, - [], + test_qapp_devices._devices.append( + MockDevice( + test_qapp_devices, + dev_class="usb", + product="Hammer", + vendor="ACME", + backend_vm="sys-usb", + assigned=[("test-vm", "auto-attach", None)], + device_id="1:2:u011010", + port="2-23", + ) ) - add_expected_vm( - qapp, - "fedora-36", - "TemplateVM", - {"netvm": ("vm", False, "")}, - {"service.qubes-update-check": None}, - [], + # add an unassigned block device + test_qapp_devices._devices.append( + MockDevice( + test_qapp_devices, + dev_class="block", + product="Ouroboros", + vendor="ACME", + backend_vm="sys-usb", + device_id="444:888:b123422", + port="sda", + ) ) - for vm in qapp.domains: - qapp.expected_calls[ - (vm.name, "admin.vm.device.pci.Attached", None, None) - ] = b"0\x00" - - return qapp - - -@pytest.fixture -def test_qapp_broken(): # pylint: disable=redefined-outer-name - """A qapp with no templates, no sys-net""" - # pylint does not understand fixtures - qapp = QubesTest() - qapp._local_name = "dom0" # pylint: disable=protected-access - - add_dom0_vm_property(qapp, "clockvm", None) - add_dom0_vm_property(qapp, "updatevm", None) - add_dom0_vm_property(qapp, "default_netvm", None) - add_dom0_vm_property(qapp, "default_template", None) - add_dom0_vm_property(qapp, "default_dispvm", None) + test_qapp_devices.update_vm_calls() - add_dom0_text_property(qapp, "default_kernel", "1.1") - add_dom0_text_property(qapp, "default_pool", "file") - - add_dom0_feature(qapp, "gui-default-allow-fullscreen", "") - add_dom0_feature(qapp, "gui-default-allow-utf8-titles", "") - add_dom0_feature(qapp, "gui-default-trayicon-mode", "") - - # setup labels - qapp.expected_calls[("dom0", "admin.label.List", None, None)] = ( - b"0\x00red\nblue\ngreen\n" - ) + # an assignment for a currently not-connected device - # setup pools: - qapp.expected_calls[("dom0", "admin.pool.List", None, None)] = ( - b"0\x00linux-kernel\nlvm\nfile\n" - ) - qapp.expected_calls[ - ("dom0", "admin.pool.volume.List", "linux-kernel", None) - ] = b"0\x001.1\nmisc\n4.2\n" - - add_expected_vm( - qapp, - "dom0", - "AdminVM", - {}, - { - "service.qubes-update-check": 1, - "config.default.qubes-update-check": None, - "config-usbvm-name": None, - "gui-default-secure-copy-sequence": None, - "gui-default-secure-paste-sequence": None, - }, - [], - ) - - # - for vm in qapp.domains: - qapp.expected_calls[ - (vm.name, "admin.vm.device.pci.Attached", None, None) - ] = b"0\x00" + call = ("test-vm", "admin.vm.device.usb.Assigned", None, None) + assignment_string = ( + "sys-usb+2-30 device_id='0:0007:u01101' " + "port_id='2-30' devclass='usb' " + "backend_domain='sys-usb' mode='ask-to-attach' " + "frontend_domain='test-vm'\n" + ).encode() + current_response = test_qapp_devices.expected_calls[call] + test_qapp_devices.expected_calls[call] = current_response + assignment_string - return qapp + return test_qapp_devices @pytest.fixture @@ -624,9 +242,7 @@ def real_builder(): pass # test glade file contains very simple setup with correctly named widgets builder = Gtk.Builder() - glade_ref = ( - importlib.resources.files("qubes_config") / "global_config.glade" - ) + glade_ref = importlib.resources.files("qubes_config") / "global_config.glade" with importlib.resources.as_file(glade_ref) as path: builder.add_from_file(str(path)) return builder diff --git a/qubes_config/tests/test.glade b/qubes_config/tests/test.glade index 427c6de0..4a14ee87 100644 --- a/qubes_config/tests/test.glade +++ b/qubes_config/tests/test.glade @@ -48,7 +48,7 @@ - + button True True @@ -60,19 +60,6 @@ 1 - - - button - True - True - True - - - False - True - 2 - - False @@ -80,19 +67,6 @@ 1 - - - button - True - True - True - - - False - True - 2 - - False diff --git a/qubes_config/tests/test_basics_handler.py b/qubes_config/tests/test_basics_handler.py index 118cd232..f6648820 100644 --- a/qubes_config/tests/test_basics_handler.py +++ b/qubes_config/tests/test_basics_handler.py @@ -331,36 +331,93 @@ def test_kernels(test_qapp): handler.widget.set_active_id("(none)") assert handler.get_unsaved() == "Default kernel" - test_qapp.expected_calls[ - ("dom0", "admin.property.Set", "default_kernel", b"") - ] = b"0\x00" + test_qapp.expected_calls[("dom0", "admin.property.Set", "default_kernel", b"")] = ( + b"0\x00" + ) handler.save() assert handler.get_unsaved() == "" +# when dealing with features, we need to be always using helper methods +@patch("qubes_config.global_config.basics_handler.get_feature") +def test_preload_handler( + mock_get, real_builder, test_qapp +): # pylint: disable=unused-argument + mock_get.return_value = "1" + test_qapp.expected_calls[ + ("dom0", "admin.vm.feature.Get", "preload-dispvm-max", None) + ] = b"0\x00" + basics_handler = BasicSettingsHandler(real_builder, test_qapp) + + assert basics_handler.get_unsaved() == "" + + defdispvm_combo: Gtk.ComboBox = real_builder.get_object( + "basics_defdispvm_combo" + ) + preload_dispvm_spin: Gtk.SpinButton = real_builder.get_object( + "basics_preload_dispvm" + ) + + initial_preload_dispvm = preload_dispvm_spin.get_value() + initial_default_dispvm = defdispvm_combo.get_active_id() + preload_dispvm_spin.set_value(initial_preload_dispvm + 1) + defdispvm_combo.set_active_id("(none)") + assert not preload_dispvm_spin.is_sensitive() + assert preload_dispvm_spin.get_value() == 0 + # Assert that preload feature hasn't changed. + test_qapp.expected_calls[ + ("dom0", "admin.property.Set", "default_dispvm", b"") + ] = b"0\x00" + basics_handler.save() + + initial_preload_dispvm = preload_dispvm_spin.get_value() + assert not preload_dispvm_spin.is_sensitive() + defdispvm_combo.set_active_id(initial_default_dispvm) + assert preload_dispvm_spin.is_sensitive() + new_preload_value = initial_preload_dispvm + 1 + preload_dispvm_spin.set_value(new_preload_value) + test_qapp.expected_calls[ + ( + "dom0", + "admin.property.Set", + "default_dispvm", + initial_default_dispvm.encode(), + ) + ] = b"0\x00" + test_qapp.expected_calls[ + ( + "dom0", + "admin.vm.features.Set", + "preload-dispvm-max", + str(new_preload_value).encode(), + ), + ] = b"0\x00" + basics_handler.save() + + def test_basics_handler(real_builder, test_qapp): + test_qapp.expected_calls[ + ("dom0", "admin.vm.feature.Get", "preload-dispvm-max", None) + ] = b"0\x00" basics_handler = BasicSettingsHandler(real_builder, test_qapp) assert basics_handler.get_unsaved() == "" - # all handlers are tested above, so now just use one as example - # change clockvm + # All handlers are tested above, so now just use one as example. clockvm_combo: Gtk.ComboBox = real_builder.get_object( "basics_clockvm_combo" ) initial_clockvm = clockvm_combo.get_active_id() assert initial_clockvm != "test-blue" - clockvm_combo.set_active_id("test-blue") + clockvm_combo.set_active_id("test-blue") assert basics_handler.get_unsaved() == "Clock qube" basics_handler.reset() - assert clockvm_combo.get_active_id() == initial_clockvm assert basics_handler.get_unsaved() == "" clockvm_combo.set_active_id("test-blue") - test_qapp.expected_calls[ ("dom0", "admin.property.Set", "clockvm", b"test-blue") ] = b"0\x00" diff --git a/qubes_config/tests/test_device_attachments.py b/qubes_config/tests/test_device_attachments.py new file mode 100644 index 00000000..2c716ea7 --- /dev/null +++ b/qubes_config/tests/test_device_attachments.py @@ -0,0 +1,1338 @@ +# -*- encoding: utf8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2025 Marta Marczykowska-Górecka +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with this program; if not, see . +# pylint: disable=missing-module-docstring +# pylint: disable=missing-function-docstring +# pylint: disable=missing-class-docstring +# pylint: disable=protected-access +from unittest.mock import patch + +import pytest + +from qubesadmin.tests.mock_app import MockDevice +from ..global_config.device_attachments import AutoDeviceDialog, DeviceManager +from ..global_config.device_attachments import ( + AttachmentHandler, + RequiredDeviceDialog, + DevAttachmentHandler, +) + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + +### AUTO ATTACH HANDLER TESTS + +AUTO_CLASSES = ["block", "mic", "usb"] +PCI_CLASSES = ["block", "pci"] + + +# pylint fails to correctly react to fixtures +# pylint: disable=redefined-outer-name +@pytest.fixture +def auto_attach_handler(real_builder, test_qapp_devices): + dev_policy_manager = DeviceManager(test_qapp_devices) + dev_policy_manager.load_data() + + handler = AttachmentHandler( + qapp=test_qapp_devices, + builder=real_builder, + device_policy_manager=dev_policy_manager, + classes=AUTO_CLASSES, + assignment_filter=DevAttachmentHandler._filter_auto, + edit_dialog_class=AutoDeviceDialog, + prefix="devices_auto", + ) + return handler + + +@pytest.fixture +def required_handler(real_builder, test_qapp_devices): + dev_policy_manager = DeviceManager(test_qapp_devices) + dev_policy_manager.load_data() + + handler = AttachmentHandler( + qapp=test_qapp_devices, + builder=real_builder, + device_policy_manager=dev_policy_manager, + classes=PCI_CLASSES, + assignment_filter=DevAttachmentHandler._filter_required, + edit_dialog_class=RequiredDeviceDialog, + prefix="devices_required", + ) + return handler + + +def test_auto_attach_init(auto_attach_handler): + assert len(auto_attach_handler.rule_list.get_children()) == 3 + + +def test_auto_attach_edit_dialog(auto_attach_handler): + # test if the edit dialog works + qapp = auto_attach_handler.qapp + + existing_rows = auto_attach_handler.rule_list.get_children() + assert len(existing_rows) == 3 + + auto_attach_handler.add_button.clicked() + + assert auto_attach_handler.edit_dialog.dev_modeler.get_selected() is None + assert ( + "Internal Mic" not in auto_attach_handler.edit_dialog.devident_label.get_text() + ) + + auto_attach_handler.edit_dialog.dev_combo.set_active_id("dom0:mic::m000000") + assert auto_attach_handler.edit_dialog.dev_modeler.get_selected() is not None + assert "Internal Mic" in auto_attach_handler.edit_dialog.devident_label.get_text() + assert "dom0:mic" in auto_attach_handler.edit_dialog.port_check.get_label() + assert auto_attach_handler.edit_dialog.port_check.get_active() + + auto_attach_handler.edit_dialog.dev_combo.set_active_id("1:2:u011010") + assert auto_attach_handler.edit_dialog.dev_modeler.get_selected() is not None + assert "Hammer" in auto_attach_handler.edit_dialog.devident_label.get_text() + assert "sys-usb:2-23" in auto_attach_handler.edit_dialog.port_check.get_label() + + auto_attach_handler.edit_dialog.auto_radio.set_active(True) + auto_attach_handler.edit_dialog.qube_handler.add_selected_vm( + qapp.domains["test-vm"] + ) + assert auto_attach_handler.edit_dialog.port_check.get_active() + auto_attach_handler.edit_dialog.port_check.set_active(False) + + auto_attach_handler.edit_dialog.ok_button.clicked() + + new_rows = auto_attach_handler.rule_list.get_children() + assert len(new_rows) == len(existing_rows) + 1 + + diff_rows = set(new_rows) - set(existing_rows) + assert len(diff_rows) == 1 + new_row = diff_rows.pop() + assert "Hammer" in new_row.dev_label.get_text() + assert "will attach automatically" in new_row.action_label.get_text() + assert len(new_row.vm_box.get_children()) == 1 + + expected_call = ( + "test-vm", + "admin.vm.device.usb.Assign", + "sys-usb+*:1:2:u011010", + b"device_id='1:2:u011010' port_id='*' devclass='usb' " + b"backend_domain='sys-usb' mode='auto-attach'" + b" frontend_domain='test-vm'", + ) + qapp.expected_calls[expected_call] = b"0\x00" + assert expected_call not in qapp.actual_calls + + auto_attach_handler.save() + + assert expected_call in qapp.actual_calls + + +def test_auto_attach_dialog_port_two_vms(auto_attach_handler): + qapp = auto_attach_handler.qapp + auto_attach_handler.add_button.clicked() + auto_attach_handler.edit_dialog.dev_combo.set_active_id("1:2:u011010") + auto_attach_handler.edit_dialog.devident_check.set_active(False) + auto_attach_handler.edit_dialog.ask_radio.set_active(True) + auto_attach_handler.edit_dialog.qube_handler.add_selected_vm( + qapp.domains["test-vm"] + ) + auto_attach_handler.edit_dialog.qube_handler.add_selected_vm( + qapp.domains["test-red"] + ) + auto_attach_handler.edit_dialog.qube_handler.add_selected_vm( + qapp.domains["test-blue"] + ) + auto_attach_handler.edit_dialog.ok_button.clicked() + + expected_calls = [ + ( + "test-vm", + "admin.vm.device.usb.Assign", + "sys-usb+2-23:*", + b"device_id='*' port_id='2-23' devclass='usb' " + b"backend_domain='sys-usb' mode='ask-to-attach' " + b"frontend_domain='test-vm'", + ), + ( + "test-red", + "admin.vm.device.usb.Assign", + "sys-usb+2-23:*", + b"device_id='*' port_id='2-23' devclass='usb' " + b"backend_domain='sys-usb' mode='ask-to-attach' " + b"frontend_domain='test-red'", + ), + ( + "test-blue", + "admin.vm.device.usb.Assign", + "sys-usb+2-23:*", + b"device_id='*' port_id='2-23' devclass='usb' " + b"backend_domain='sys-usb' mode='ask-to-attach' " + b"frontend_domain='test-blue'", + ), + ] + for call in expected_calls: + assert call not in qapp.actual_calls + qapp.expected_calls[call] = b"0\x00" + + auto_attach_handler.save() + + for call in expected_calls: + assert call in qapp.actual_calls + + +def test_edit_rule_ident(auto_attach_handler): + qapp = auto_attach_handler.qapp + # change device and port checking, leave the rest the same + for row in auto_attach_handler.rule_list.get_children(): + if "Hammer" in row.dev_label.get_text(): + auto_attach_handler.rule_list.select_row(row) + break + else: + assert False + + auto_attach_handler.edit_button.clicked() + assert "Hammer" in auto_attach_handler.edit_dialog.devident_label.get_text() + assert auto_attach_handler.edit_dialog.devident_check.get_active() + assert auto_attach_handler.edit_dialog.port_check.get_active() + assert auto_attach_handler.edit_dialog.auto_radio.get_active() + assert auto_attach_handler.edit_dialog.qube_handler.selected_vms == [ + qapp.domains["test-vm"] + ] + + auto_attach_handler.edit_dialog.dev_combo.set_active_id("dom0:mic::m000000") + auto_attach_handler.edit_dialog.devident_check.set_active(True) + auto_attach_handler.edit_dialog.port_check.set_active(False) + + auto_attach_handler.edit_dialog.ok_button.clicked() + + expected_calls = [ + ( + "test-vm", + "admin.vm.device.mic.Assign", + "dom0+*:dom0:mic::m000000", + b"device_id='dom0:mic::m000000' port_id='*' devclass='mic' " + b"backend_domain='dom0' mode='auto-attach' " + b"frontend_domain='test-vm'", + ), + ( + "test-vm", + "admin.vm.device.usb.Unassign", + "sys-usb+2-23:1:2:u011010", + None, + ), + ] + for call in expected_calls: + assert call not in qapp.actual_calls + qapp.expected_calls[call] = b"0\x00" + + auto_attach_handler.save() + + for call in expected_calls: + assert call in qapp.actual_calls + + +def test_edit_rule_action(auto_attach_handler): + qapp = auto_attach_handler.qapp + for row in auto_attach_handler.rule_list.get_children(): + if "Hammer" in row.dev_label.get_text(): + auto_attach_handler.rule_list.select_row(row) + break + else: + assert False + + auto_attach_handler.edit_button.clicked() + + auto_attach_handler.edit_dialog.ask_radio.set_active(True) + + auto_attach_handler.edit_dialog.ok_button.clicked() + + expected_calls = [ + ( + "test-vm", + "admin.vm.device.usb.Assign", + "sys-usb+2-23:1:2:u011010", + b"device_id='1:2:u011010' port_id='2-23' devclass='usb' " + b"backend_domain='sys-usb' mode='ask-to-attach' " + b"frontend_domain='test-vm'", + ), + ( + "test-vm", + "admin.vm.device.usb.Unassign", + "sys-usb+2-23:1:2:u011010", + None, + ), + ] + for call in expected_calls: + assert call not in qapp.actual_calls + qapp.expected_calls[call] = b"0\x00" + + auto_attach_handler.save() + + for call in expected_calls: + assert call in qapp.actual_calls + + +def test_edit_rule_vms(auto_attach_handler): + qapp = auto_attach_handler.qapp + + for row in auto_attach_handler.rule_list.get_children(): + if "Hammer" in row.dev_label.get_text(): + auto_attach_handler.rule_list.select_row(row) + break + else: + assert False + + auto_attach_handler.edit_button.clicked() + + auto_attach_handler.edit_dialog.qube_handler.clear() + auto_attach_handler.edit_dialog.qube_handler.add_selected_vm( + qapp.domains["test-red"] + ) + auto_attach_handler.edit_dialog.qube_handler.add_selected_vm( + qapp.domains["test-blue"] + ) + auto_attach_handler.edit_dialog.ok_button.clicked() + + expected_calls = [ + ( + "test-red", + "admin.vm.device.usb.Assign", + "sys-usb+2-23:1:2:u011010", + b"device_id='1:2:u011010' port_id='2-23' devclass='usb' " + b"backend_domain='sys-usb' mode='auto-attach' " + b"frontend_domain='test-red'", + ), + ( + "test-blue", + "admin.vm.device.usb.Assign", + "sys-usb+2-23:1:2:u011010", + b"device_id='1:2:u011010' port_id='2-23' devclass='usb' " + b"backend_domain='sys-usb' mode='auto-attach' " + b"frontend_domain='test-blue'", + ), + ( + "test-vm", + "admin.vm.device.usb.Unassign", + "sys-usb+2-23:1:2:u011010", + None, + ), + ] + for call in expected_calls: + assert call not in qapp.actual_calls + qapp.expected_calls[call] = b"0\x00" + + auto_attach_handler.save() + + for call in expected_calls: + assert call in qapp.actual_calls + + +def test_edit_rule_unknown_opt(real_builder, test_qapp_devices): + # attachment with both port and device as * + test_qapp_devices._devices.append( + MockDevice( + test_qapp_devices, + dev_class="usb", + product="Strange", + vendor="ACME", + backend_vm="sys-usb", + assigned=[("test-vm", "auto-attach", ["misc_opt"])], + device_id="1:3:u011010", + port="2-24", + ) + ) + test_qapp_devices.update_vm_calls() + + dev_policy_manager = DeviceManager(test_qapp_devices) + dev_policy_manager.load_data() + + handler = AttachmentHandler( + qapp=test_qapp_devices, + builder=real_builder, + device_policy_manager=dev_policy_manager, + classes=AUTO_CLASSES, + assignment_filter=DevAttachmentHandler._filter_auto, + edit_dialog_class=AutoDeviceDialog, + prefix="devices_auto", + ) + assert isinstance(handler.edit_dialog, AutoDeviceDialog) + for row in handler.rule_list.get_children(): + if "Strange" in row.dev_label.get_text(): + handler.rule_list.select_row(row) + break + else: + assert False + + handler.edit_button.clicked() + + handler.edit_dialog.qube_handler.clear() + handler.edit_dialog.qube_handler.add_selected_vm( + test_qapp_devices.domains["test-red"] + ) + handler.edit_dialog.ok_button.clicked() + + expected_calls = [ + ( + "test-red", + "admin.vm.device.usb.Assign", + "sys-usb+2-24:1:3:u011010", + b"device_id='1:3:u011010' port_id='2-24' devclass='usb' " + b"backend_domain='sys-usb' mode='auto-attach' " + b"frontend_domain='test-red' _misc_opt='True'", + ), + ( + "test-vm", + "admin.vm.device.usb.Unassign", + "sys-usb+2-24:1:3:u011010", + None, + ), + ] + for call in expected_calls: + assert call not in test_qapp_devices.actual_calls + test_qapp_devices.expected_calls[call] = b"0\x00" + + handler.save() + + for call in expected_calls: + assert call in test_qapp_devices.actual_calls + + +def test_auto_not_list_required(real_builder, test_qapp_devices): + # do not show a rule for block devices that are Required in the auto-attach list + test_qapp_devices._devices.append( + MockDevice( + test_qapp_devices, + dev_class="block", + product="RequiredB", + vendor="ACME", + backend_vm="sys-usb", + assigned=[("test-vm", "required", None)], + device_id="1d6b:0104:DEADBEEF:b123456", + port="sda", + ) + ) + test_qapp_devices.update_vm_calls() + + dev_policy_manager = DeviceManager(test_qapp_devices) + dev_policy_manager.load_data() + + handler = AttachmentHandler( + qapp=test_qapp_devices, + builder=real_builder, + device_policy_manager=dev_policy_manager, + classes=AUTO_CLASSES, + assignment_filter=DevAttachmentHandler._filter_auto, + edit_dialog_class=AutoDeviceDialog, + prefix="devices_auto", + ) + assert isinstance(handler.edit_dialog, AutoDeviceDialog) + for row in handler.rule_list.get_children(): + if "RequiredB" in row.dev_label.get_text(): + assert False, "Required incorrectly listed in AutoAttach" + + +def test_remove_rule(auto_attach_handler): + qapp = auto_attach_handler.qapp + for row in auto_attach_handler.rule_list.get_children(): + if "Hammer" in row.dev_label.get_text(): + auto_attach_handler.rule_list.select_row(row) + break + else: + assert False + + with patch( + "qubes_config.global_config.device_widgets.ask_question", + return_value=Gtk.ResponseType.YES, + ): + auto_attach_handler.remove_button.clicked() + + expected_call = ( + "test-vm", + "admin.vm.device.usb.Unassign", + "sys-usb+2-23:1:2:u011010", + None, + ) + qapp.expected_calls[expected_call] = b"0\x00" + assert expected_call not in qapp.actual_calls + + auto_attach_handler.save() + + assert expected_call in qapp.actual_calls + + +def test_auto_attach_buttons(auto_attach_handler): + assert auto_attach_handler.add_button.get_sensitive() + assert not auto_attach_handler.edit_button.get_sensitive() + assert not auto_attach_handler.remove_button.get_sensitive() + + for row in auto_attach_handler.rule_list.get_children(): + auto_attach_handler.rule_list.select_row(row) + break + + assert auto_attach_handler.add_button.get_sensitive() + assert auto_attach_handler.edit_button.get_sensitive() + assert auto_attach_handler.remove_button.get_sensitive() + + auto_attach_handler.rule_list.select_row(None) + assert auto_attach_handler.add_button.get_sensitive() + assert not auto_attach_handler.edit_button.get_sensitive() + assert not auto_attach_handler.remove_button.get_sensitive() + + +def test_auto_attach_noop(auto_attach_handler): + for row in auto_attach_handler.rule_list.get_children(): + auto_attach_handler.rule_list.select_row(row) + break + + auto_attach_handler.edit_button.clicked() + auto_attach_handler.edit_dialog.ok_button.clicked() + + auto_attach_handler.save() + + +def test_auto_attach_device_unavailable(auto_attach_handler): + qapp = auto_attach_handler.qapp + + for row in auto_attach_handler.rule_list.get_children(): + # TODO: fix when prbartman fixes saving device identity + if "?***" in row.dev_label.get_text(): + auto_attach_handler.rule_list.select_row(row) + break + else: + assert False + + # this only tests removing the rule + + with patch( + "qubes_config.global_config.device_widgets.ask_question", + return_value=Gtk.ResponseType.YES, + ): + auto_attach_handler.remove_button.clicked() + + expected_call = ( + "test-vm", + "admin.vm.device.usb.Unassign", + "sys-usb+2-30:0:0007:u01101", + None, + ) + qapp.expected_calls[expected_call] = b"0\x00" + assert expected_call not in qapp.actual_calls + + auto_attach_handler.save() + + assert expected_call in qapp.actual_calls + + +def test_auto_attach_validity(auto_attach_handler): + for row in auto_attach_handler.rule_list.get_children(): + if "Hammer" in row.dev_label.get_text(): + auto_attach_handler.rule_list.select_row(row) + break + else: + assert False + + auto_attach_handler.edit_button.clicked() + + assert auto_attach_handler.edit_dialog.ok_button.get_sensitive() + + auto_attach_handler.edit_dialog.port_check.set_active(False) + auto_attach_handler.edit_dialog.devident_check.set_active(False) + + assert not auto_attach_handler.edit_dialog.ok_button.get_sensitive() + + auto_attach_handler.edit_dialog.port_check.set_active(True) + + assert auto_attach_handler.edit_dialog.ok_button.get_sensitive() + + auto_attach_handler.edit_dialog.qube_handler.clear() + + assert not auto_attach_handler.edit_dialog.ok_button.get_sensitive() + + +def test_auto_attach_get_unsaved(auto_attach_handler): + assert auto_attach_handler.get_unsaved() == "" + for row in auto_attach_handler.rule_list.get_children(): + if "Hammer" in row.dev_label.get_text(): + auto_attach_handler.rule_list.select_row(row) + break + else: + assert False + + with patch( + "qubes_config.global_config.device_widgets.ask_question", + return_value=Gtk.ResponseType.YES, + ): + auto_attach_handler.remove_button.clicked() + + for row in auto_attach_handler.rule_list.get_children(): + if "Anvil" in row.dev_label.get_text(): + auto_attach_handler.rule_list.select_row(row) + break + else: + assert False + + assert auto_attach_handler.edit_button.get_sensitive() + auto_attach_handler.edit_button.clicked() + auto_attach_handler.edit_dialog.port_check.set_active(False) + auto_attach_handler.edit_dialog.ok_button.clicked() + + assert "Hammer" in auto_attach_handler.get_unsaved() + assert "Anvil" in auto_attach_handler.get_unsaved() + + +def test_auto_attach_reset(auto_attach_handler): + qapp = auto_attach_handler.qapp + + assert auto_attach_handler.get_unsaved() == "" + for row in auto_attach_handler.rule_list.get_children(): + if "Hammer" in row.dev_label.get_text(): + auto_attach_handler.rule_list.select_row(row) + break + else: + assert False + + with patch( + "qubes_config.global_config.device_widgets.ask_question", + return_value=Gtk.ResponseType.YES, + ): + auto_attach_handler.remove_button.clicked() + + for row in auto_attach_handler.rule_list.get_children(): + if "Anvil" in row.dev_label.get_text(): + auto_attach_handler.rule_list.select_row(row) + break + else: + assert False + + auto_attach_handler.edit_button.clicked() + auto_attach_handler.edit_dialog.port_check.set_active(False) + auto_attach_handler.edit_dialog.ok_button.clicked() + + auto_attach_handler.add_button.clicked() + auto_attach_handler.edit_dialog.dev_combo.set_active_id("1:2:u011010") + auto_attach_handler.edit_dialog.devident_check.set_active(False) + auto_attach_handler.edit_dialog.port_check.set_active(True) + auto_attach_handler.edit_dialog.ask_radio.set_active(True) + auto_attach_handler.edit_dialog.qube_handler.add_selected_vm( + qapp.domains["test-vm"] + ) + auto_attach_handler.edit_dialog.ok_button.clicked() + + assert auto_attach_handler.get_unsaved() != "" + + auto_attach_handler.reset() + assert auto_attach_handler.get_unsaved() == "" + # nothing should be called here + auto_attach_handler.save() + + +def test_auto_attach_add_cancel(auto_attach_handler): + # no ghosts after cancelling adding a rule + assert len(auto_attach_handler.rule_list.get_children()) == 3 + + auto_attach_handler.add_button.clicked() + auto_attach_handler.edit_dialog.cancel_button.clicked() + + assert len(auto_attach_handler.rule_list.get_children()) == 3 + + +def test_auto_attach_block_read_only(auto_attach_handler): + qapp = auto_attach_handler.qapp + auto_attach_handler.add_button.clicked() + auto_attach_handler.edit_dialog.dev_combo.set_active_id("444:888:b123422") + auto_attach_handler.edit_dialog.devident_check.set_active(True) + auto_attach_handler.edit_dialog.port_check.set_active(True) + auto_attach_handler.edit_dialog.ask_radio.set_active(True) + auto_attach_handler.edit_dialog.qube_handler.add_selected_vm( + qapp.domains["test-vm"] + ) + auto_attach_handler.edit_dialog.qube_handler.add_selected_vm( + qapp.domains["test-red"] + ) + auto_attach_handler.edit_dialog.read_only_check.set_active(True) + auto_attach_handler.edit_dialog.ok_button.clicked() + + expected_calls = [ + ( + "test-vm", + "admin.vm.device.block.Assign", + "sys-usb+sda:444:888:b123422", + b"device_id='444:888:b123422' port_id='sda' devclass='block' " + b"backend_domain='sys-usb' mode='ask-to-attach' " + b"frontend_domain='test-vm' _read-only='True'", + ), + ( + "test-red", + "admin.vm.device.block.Assign", + "sys-usb+sda:444:888:b123422", + b"device_id='444:888:b123422' port_id='sda' devclass='block' " + b"backend_domain='sys-usb' mode='ask-to-attach' " + b"frontend_domain='test-red' _read-only='True'", + ), + ] + for call in expected_calls: + assert call not in qapp.actual_calls + qapp.expected_calls[call] = b"0\x00" + + auto_attach_handler.save() + + for call in expected_calls: + assert call in qapp.actual_calls + + +def test_req_init(required_handler): + assert len(required_handler.rule_list.get_children()) == 3 + + +def test_req_edit_dialog(required_handler): + # test if the edit dialog works + qapp = required_handler.qapp + + existing_rows = required_handler.rule_list.get_children() + required_handler.add_button.clicked() + + assert required_handler.edit_dialog.dev_modeler.get_selected() is None + assert "Piano" not in required_handler.edit_dialog.devident_label.get_text() + + required_handler.edit_dialog.dev_combo.set_active_id("0x8086:0x51f0::p028000") + assert required_handler.edit_dialog.dev_modeler.get_selected() is not None + assert "Network" in required_handler.edit_dialog.devident_label.get_text() + + required_handler.edit_dialog.dev_combo.set_active_id("0x8086:0x51f0::p300000") + assert required_handler.edit_dialog.dev_modeler.get_selected() is not None + assert "Piano" in required_handler.edit_dialog.devident_label.get_text() + + required_handler.edit_dialog.qube_handler.add_selected_vm(qapp.domains["test-vm"]) + + required_handler.edit_dialog.ok_button.clicked() + + new_rows = required_handler.rule_list.get_children() + assert len(new_rows) == len(existing_rows) + 1 + + diff_rows = set(new_rows) - set(existing_rows) + assert len(diff_rows) == 1 + new_row = diff_rows.pop() + assert "Piano" in new_row.dev_label.get_text() + assert "required" in new_row.action_label.get_text() + assert len(new_row.vm_box.get_children()) == 1 + + expected_call = ( + "test-vm", + "admin.vm.device.pci.Assign", + "dom0+0f.0:0x8086:0x51f0::p300000", + b"device_id='0x8086:0x51f0::p300000' port_id='0f.0' " + b"devclass='pci' backend_domain='dom0' mode='required'" + b" frontend_domain='test-vm'", + ) + qapp.expected_calls[expected_call] = b"0\x00" + assert expected_call not in qapp.actual_calls + + required_handler.save() + + assert expected_call in qapp.actual_calls + + +def test_req_dialog_change_vm(required_handler): + qapp = required_handler.qapp + + for row in required_handler.rule_list.get_children(): + if "Network Card" in row.dev_label.get_text(): + required_handler.rule_list.select_row(row) + break + else: + assert False + + required_handler.edit_button.clicked() + + required_handler.edit_dialog.qube_handler.clear() + required_handler.edit_dialog.qube_handler.add_selected_vm(qapp.domains["test-red"]) + required_handler.edit_dialog.ok_button.clicked() + + expected_calls = [ + ( + "test-red", + "admin.vm.device.pci.Assign", + "dom0+0c.0:0x8086:0x51f0::p028000", + b"device_id='0x8086:0x51f0::p028000' port_id='0c.0' " + b"devclass='pci' backend_domain='dom0' mode='required'" + b" frontend_domain='test-red'", + ), + ( + "sys-net", + "admin.vm.device.pci.Unassign", + "dom0+0c.0:0x8086:0x51f0::p028000", + None, + ), + ] + for call in expected_calls: + assert call not in qapp.actual_calls + qapp.expected_calls[call] = b"0\x00" + + required_handler.save() + + for call in expected_calls: + assert call in qapp.actual_calls + + +def test_req_add_no_strict(required_handler): + # test if the edit dialog works + qapp = required_handler.qapp + + required_handler.add_button.clicked() + + required_handler.edit_dialog.dev_combo.set_active_id("0x8086:0x51f0::p300000") + + assert not required_handler.edit_dialog.ok_button.get_sensitive() + required_handler.edit_dialog.qube_handler.add_selected_vm(qapp.domains["test-vm"]) + + assert not required_handler.edit_dialog.no_strict_check.get_active() + required_handler.edit_dialog.no_strict_check.set_active(True) + + required_handler.edit_dialog.ok_button.clicked() + + expected_call = ( + "test-vm", + "admin.vm.device.pci.Assign", + "dom0+0f.0:0x8086:0x51f0::p300000", + b"device_id='0x8086:0x51f0::p300000' port_id='0f.0' " + b"devclass='pci' backend_domain='dom0' mode='required'" + b" frontend_domain='test-vm' _no-strict-reset='True'", + ) + qapp.expected_calls[expected_call] = b"0\x00" + assert expected_call not in qapp.actual_calls + + required_handler.save() + + assert expected_call in qapp.actual_calls + + +def test_req_add_both_opts(required_handler): + # test if the edit dialog works + qapp = required_handler.qapp + + required_handler.add_button.clicked() + + required_handler.edit_dialog.dev_combo.set_active_id("0x8086:0x51f0::p300000") + + assert not required_handler.edit_dialog.ok_button.get_sensitive() + required_handler.edit_dialog.qube_handler.add_selected_vm(qapp.domains["test-vm"]) + assert required_handler.edit_dialog.ok_button.get_sensitive() + + assert not required_handler.edit_dialog.permissive_check.get_active() + assert not required_handler.edit_dialog.no_strict_check.get_active() + required_handler.edit_dialog.permissive_check.set_active(True) + required_handler.edit_dialog.no_strict_check.set_active(True) + + required_handler.edit_dialog.ok_button.clicked() + + expected_call = ( + "test-vm", + "admin.vm.device.pci.Assign", + "dom0+0f.0:0x8086:0x51f0::p300000", + b"device_id='0x8086:0x51f0::p300000' port_id='0f.0' " + b"devclass='pci' backend_domain='dom0' mode='required'" + b" frontend_domain='test-vm' _no-strict-reset='True' " + b"_permissive='True'", + ) + qapp.expected_calls[expected_call] = b"0\x00" + assert expected_call not in qapp.actual_calls + + required_handler.save() + + assert expected_call in qapp.actual_calls + + +def test_req_dialog_remove_opts(required_handler): + # open a bunch of edit screens in order to make sure checkboxes get + # correctly checked and unchecked + qapp = required_handler.qapp + + for row in required_handler.rule_list.get_children(): + if "Orchestra" in row.dev_label.get_text(): + required_handler.rule_list.select_row(row) + break + else: + assert False + + required_handler.edit_button.clicked() + + assert required_handler.edit_dialog.no_strict_check.get_active() + assert not required_handler.edit_dialog.permissive_check.get_active() + + # close, select another + required_handler.edit_dialog.cancel_button.clicked() + + for row in required_handler.rule_list.get_children(): + if "Network Card" in row.dev_label.get_text(): + required_handler.rule_list.select_row(row) + break + else: + assert False + + required_handler.edit_button.clicked() + + assert not required_handler.edit_dialog.no_strict_check.get_active() + assert not required_handler.edit_dialog.permissive_check.get_active() + + required_handler.edit_dialog.cancel_button.clicked() + + for row in required_handler.rule_list.get_children(): + if "Orchestra" in row.dev_label.get_text(): + required_handler.rule_list.select_row(row) + break + else: + assert False + + required_handler.edit_button.clicked() + + assert required_handler.edit_dialog.no_strict_check.get_active() + assert not required_handler.edit_dialog.permissive_check.get_active() + required_handler.edit_dialog.no_strict_check.set_active(False) + required_handler.edit_dialog.permissive_check.set_active(True) + + required_handler.edit_dialog.ok_button.clicked() + + expected_calls = [ + ( + "test-red", + "admin.vm.device.pci.Assign", + "dom0+0h.0:0x8086:0x51c8::p040300", + b"device_id='0x8086:0x51c8::p040300' port_id='0h.0' " + b"devclass='pci' backend_domain='dom0' mode='required'" + b" frontend_domain='test-red' _permissive='True'", + ), + ( + "test-red", + "admin.vm.device.pci.Unassign", + "dom0+0h.0:0x8086:0x51c8::p040300", + None, + ), + ] + for call in expected_calls: + assert call not in qapp.actual_calls + qapp.expected_calls[call] = b"0\x00" + + required_handler.save() + + for call in expected_calls: + assert call in qapp.actual_calls + + +def test_req_buttons(required_handler): + assert required_handler.add_button.get_sensitive() + assert not required_handler.edit_button.get_sensitive() + assert not required_handler.remove_button.get_sensitive() + + for row in required_handler.rule_list.get_children(): + required_handler.rule_list.select_row(row) + break + + assert required_handler.add_button.get_sensitive() + assert required_handler.edit_button.get_sensitive() + assert required_handler.remove_button.get_sensitive() + + required_handler.rule_list.select_row(None) + assert required_handler.add_button.get_sensitive() + assert not required_handler.edit_button.get_sensitive() + assert not required_handler.remove_button.get_sensitive() + + +def test_req_noop(required_handler): + for row in required_handler.rule_list.get_children(): + required_handler.rule_list.select_row(row) + break + + assert required_handler.edit_button.get_sensitive() + required_handler.edit_button.clicked() + required_handler.edit_dialog.ok_button.clicked() + + required_handler.save() + + +def test_req_validity(required_handler): + for row in required_handler.rule_list.get_children(): + if "Network Card" in row.dev_label.get_text(): + required_handler.rule_list.select_row(row) + break + else: + assert False + + required_handler.edit_button.clicked() + + assert required_handler.edit_dialog.ok_button.get_sensitive() + + required_handler.edit_dialog.qube_handler.clear() + + assert not required_handler.edit_dialog.ok_button.get_sensitive() + + required_handler.edit_dialog.qube_handler.add_selected_vm( + required_handler.qapp.domains["test-red"] + ) + + assert required_handler.edit_dialog.ok_button.get_sensitive() + + +def test_req_get_unsaved(required_handler): + assert required_handler.get_unsaved() == "" + for row in required_handler.rule_list.get_children(): + if "Network Card" in row.dev_label.get_text(): + required_handler.rule_list.select_row(row) + break + else: + assert False + + with patch( + "qubes_config.global_config.device_widgets.ask_question", + return_value=Gtk.ResponseType.YES, + ): + required_handler.remove_button.clicked() + + for row in required_handler.rule_list.get_children(): + if "Orchestra" in row.dev_label.get_text(): + required_handler.rule_list.select_row(row) + break + else: + assert False + + assert required_handler.edit_button.get_sensitive() + required_handler.edit_button.clicked() + required_handler.edit_dialog.no_strict_check.set_active(False) + required_handler.edit_dialog.ok_button.clicked() + + required_handler.add_button.clicked() + required_handler.edit_dialog.dev_combo.set_active_id("0x8086:0x461e::p0c0330") + required_handler.edit_dialog.qube_handler.add_selected_vm( + required_handler.qapp.domains["test-vm"] + ) + required_handler.edit_dialog.ok_button.clicked() + + assert "Network Card" in required_handler.get_unsaved() + assert "Orchestra" in required_handler.get_unsaved() + assert "USB Controller" in required_handler.get_unsaved() + + +def test_req_reset(required_handler): + qapp = required_handler.qapp + + assert required_handler.get_unsaved() == "" + for row in required_handler.rule_list.get_children(): + if "Network Card" in row.dev_label.get_text(): + required_handler.rule_list.select_row(row) + break + else: + assert False + + with patch( + "qubes_config.global_config.device_widgets.ask_question", + return_value=Gtk.ResponseType.YES, + ): + required_handler.remove_button.clicked() + + for row in required_handler.rule_list.get_children(): + if "Orchestra" in row.dev_label.get_text(): + required_handler.rule_list.select_row(row) + break + else: + assert False + + required_handler.edit_button.clicked() + required_handler.edit_dialog.no_strict_check.set_active(False) + required_handler.edit_dialog.ok_button.clicked() + + required_handler.add_button.clicked() + required_handler.edit_dialog.dev_combo.set_active_id("0x8086:0x461e::p0c0330") + required_handler.edit_dialog.qube_handler.add_selected_vm(qapp.domains["test-vm"]) + required_handler.edit_dialog.ok_button.clicked() + + assert required_handler.get_unsaved() != "" + + required_handler.reset() + assert required_handler.get_unsaved() == "" + # nothing should be called here + required_handler.save() + + +def test_req_add_cancel(required_handler): + # no ghosts after cancelling adding a rule + assert len(required_handler.rule_list.get_children()) == 3 + + required_handler.add_button.clicked() + required_handler.edit_dialog.cancel_button.clicked() + + assert len(required_handler.rule_list.get_children()) == 3 + + +def test_req_multiple_save(required_handler): + """Check if we do not do superfluous saves when saving multiple times.""" + qapp = required_handler.qapp + required_handler.add_button.clicked() + + required_handler.edit_dialog.dev_combo.set_active_id("0x8086:0x51f0::p300000") + required_handler.edit_dialog.qube_handler.add_selected_vm(qapp.domains["test-vm"]) + required_handler.edit_dialog.ok_button.clicked() + + for row in required_handler.rule_list.get_children(): + if "Network Card" in row.dev_label.get_text(): + required_handler.rule_list.select_row(row) + break + else: + assert False + + with patch( + "qubes_config.global_config.device_widgets.ask_question", + return_value=Gtk.ResponseType.YES, + ): + required_handler.remove_button.clicked() + + for row in required_handler.rule_list.get_children(): + if "Orchestra" in row.dev_label.get_text(): + required_handler.rule_list.select_row(row) + break + else: + assert False + + assert required_handler.edit_button.get_sensitive() + required_handler.edit_button.clicked() + required_handler.edit_dialog.no_strict_check.set_active(False) + required_handler.edit_dialog.permissive_check.set_active(True) + required_handler.edit_dialog.ok_button.clicked() + + expected_calls = [ + ( + "test-vm", + "admin.vm.device.pci.Assign", + "dom0+0f.0:0x8086:0x51f0::p300000", + b"device_id='0x8086:0x51f0::p300000' port_id='0f.0' " + b"devclass='pci' backend_domain='dom0' mode='required'" + b" frontend_domain='test-vm'", + ), # new assignment + ( + "test-red", + "admin.vm.device.pci.Unassign", + "dom0+0h.0:0x8086:0x51c8::p040300", + None, + ), # remove edited assignment + ( + "test-red", + "admin.vm.device.pci.Assign", + "dom0+0h.0:0x8086:0x51c8::p040300", + b"device_id='0x8086:0x51c8::p040300' port_id='0h.0' " + b"devclass='pci' backend_domain='dom0' mode='required'" + b" frontend_domain='test-red' _permissive='True'", + ), # add edited + ( + "sys-net", + "admin.vm.device.pci.Unassign", + "dom0+0c.0:0x8086:0x51f0::p028000", + None, + ), # removed assignment + ] + for call in expected_calls: + assert call not in qapp.actual_calls + qapp.expected_calls[call] = b"0\x00" + + required_handler.save() + + for call in expected_calls: + assert call in qapp.actual_calls + + # see if next saves do not do any superfluous calls + for call in expected_calls: + del qapp.expected_calls[call] + + required_handler.save() + required_handler.save() + + for call in expected_calls: + assert qapp.actual_calls.count(call) == 1 + + +def test_req_options_after_save(required_handler): + qapp = required_handler.qapp + # test for a bug where applying options leads to incorrect options being + # shown in edit dialog + for row in required_handler.rule_list.get_children(): + if "USB Controller" in row.dev_label.get_text(): + required_handler.rule_list.select_row(row) + break + else: + assert False + + required_handler.edit_button.clicked() + assert not required_handler.edit_dialog.permissive_check.get_active() + + required_handler.edit_dialog.permissive_check.set_active(True) + required_handler.edit_dialog.ok_button.clicked() + + expected_calls = [ + ( + "sys-net", + "admin.vm.device.pci.Assign", + "dom0+0d.0:0x8086:0x461e::p0c0330", + b"device_id='0x8086:0x461e::p0c0330' port_id='0d.0' " + b"devclass='pci' backend_domain='dom0' mode='required'" + b" frontend_domain='sys-net' _permissive='True'", + ), # new assignment + ( + "sys-net", + "admin.vm.device.pci.Unassign", + "dom0+0d.0:0x8086:0x461e::p0c0330", + None, + ), # remove edited assignment + ] + for call in expected_calls: + assert call not in qapp.actual_calls + qapp.expected_calls[call] = b"0\x00" + + # because the main program does a reset after save to ensure correctness, + # we need to check that the data loads correctly + qapp.expected_calls[("sys-net", "admin.vm.device.pci.Assigned", None, None)] = ( + "0\x00dom0+0d.0:0x8086:0x461e::p0c0330 " + "device_id='0x8086:0x461e::p0c0330' port_id='0d.0' devclass='pci' " + "backend_domain='dom0' mode='required' frontend_domain='sys-net' " + "_permissive='True'\n".encode() + ) + + required_handler.save() + + for call in expected_calls: + assert call in qapp.actual_calls + + required_handler.reset() + for row in required_handler.rule_list.get_children(): + if "USB Controller" in row.dev_label.get_text(): + required_handler.rule_list.select_row(row) + break + else: + assert False + + required_handler.edit_button.clicked() + assert required_handler.edit_dialog.permissive_check.get_active() + + +def test_req_readonly(required_handler): + # test if the edit dialog works + qapp = required_handler.qapp + + required_handler.add_button.clicked() + + required_handler.edit_dialog.dev_combo.set_active_id("0x8086:0x51f0::p300000") + + assert not required_handler.edit_dialog.ok_button.get_sensitive() + required_handler.edit_dialog.qube_handler.add_selected_vm(qapp.domains["test-vm"]) + + assert not required_handler.edit_dialog.no_strict_check.get_active() + required_handler.edit_dialog.no_strict_check.set_active(True) + + required_handler.edit_dialog.ok_button.clicked() + + expected_call = ( + "test-vm", + "admin.vm.device.pci.Assign", + "dom0+0f.0:0x8086:0x51f0::p300000", + b"device_id='0x8086:0x51f0::p300000' port_id='0f.0' " + b"devclass='pci' backend_domain='dom0' mode='required'" + b" frontend_domain='test-vm' _no-strict-reset='True'", + ) + qapp.expected_calls[expected_call] = b"0\x00" + assert expected_call not in qapp.actual_calls + + required_handler.save() + + assert expected_call in qapp.actual_calls + + +def test_req_grouping(real_builder, test_qapp_devices): + # a device that has three assignments with different opts should appear + # thrice + test_qapp_devices._devices.append( + MockDevice( + test_qapp_devices, + dev_class="pci", + product="OptionsDevice", + backend_vm="dom0", + assigned=[ + ("sys-net", "required", ["permissive"]), + ("sys-usb", "required", ["no-strict-reset"]), + ("test-vm", "required", None), + ], + device_id="0x8086:0x8857::p0c0330", + port="0d.0", + vendor="ACME", + ) + ) + + test_qapp_devices.update_vm_calls() + + dev_policy_manager = DeviceManager(test_qapp_devices) + dev_policy_manager.load_data() + + handler = AttachmentHandler( + qapp=test_qapp_devices, + builder=real_builder, + device_policy_manager=dev_policy_manager, + classes=PCI_CLASSES, + assignment_filter=DevAttachmentHandler._filter_required, + edit_dialog_class=RequiredDeviceDialog, + prefix="devices_required", + ) + + rows = [] + for row in handler.rule_list.get_children(): + if "OptionsDevice" in row.dev_label.get_text(): + rows.append(row) + + assert len(rows) == 3 + + +def test_req_port_not_required(required_handler): + qapp = required_handler.qapp + + required_handler.add_button.clicked() + + assert required_handler.edit_dialog.dev_modeler.get_selected() is None + assert "Ouroboros" not in required_handler.edit_dialog.devident_label.get_text() + + required_handler.edit_dialog.dev_combo.set_active_id("444:888:b123422") + + required_handler.edit_dialog.qube_handler.add_selected_vm(qapp.domains["test-vm"]) + + assert required_handler.edit_dialog.port_check.get_active() + required_handler.edit_dialog.port_check.set_active(False) + required_handler.edit_dialog.ok_button.clicked() + + expected_calls = [ + ( + "test-vm", + "admin.vm.device.block.Assign", + "sys-usb+*:444:888:b123422", + b"device_id='444:888:b123422' port_id='*' " + b"devclass='block' backend_domain='sys-usb' mode='required'" + b" frontend_domain='test-vm'", + ), + ] + for call in expected_calls: + assert call not in qapp.actual_calls + qapp.expected_calls[call] = b"0\x00" + + required_handler.save() + + for call in expected_calls: + assert call in qapp.actual_calls diff --git a/qubes_config/tests/test_device_blocks.py b/qubes_config/tests/test_device_blocks.py new file mode 100644 index 00000000..81a0e254 --- /dev/null +++ b/qubes_config/tests/test_device_blocks.py @@ -0,0 +1,457 @@ +# -*- encoding: utf8 -*- +# +# The Qubes OS Project, http://www.qubes-os.org +# +# Copyright (C) 2025 Marta Marczykowska-Górecka +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with this program; if not, see . +# pylint: disable=missing-module-docstring +# pylint: disable=missing-function-docstring +# pylint: disable=missing-class-docstring +# pylint: disable=protected-access +from unittest.mock import patch + +import pytest + +from ..global_config.device_attachments import DeviceManager +from ..global_config.device_blocks import DeviceBlockHandler, OtherCategoryRow + +import gi + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk + + +### DEVICE POLICY BLOCK HANDLER TESTS + + +# pylint fails to correctly react to fixtures +# pylint: disable=redefined-outer-name +@pytest.fixture +def block_handler(real_builder, test_qapp_devices): + dev_policy_manager = DeviceManager(test_qapp_devices) + dev_policy_manager.load_data() + + handler = DeviceBlockHandler(test_qapp_devices, real_builder, dev_policy_manager) + return handler + + +def test_policy_block_init(block_handler): + assert len(block_handler.rule_list.get_children()) == 2 + + +def test_policy_block_remove(block_handler): + qapp = block_handler.qapp + assert not block_handler.remove_button.is_sensitive() + + for row in block_handler.rule_list.get_children(): + if row.vm_wrapper.vm.name == "test-dev2": + block_handler.rule_list.select_row(row) + break + else: + assert False, "Failed to find test-dev2 row" + + assert block_handler.remove_button.is_sensitive() + + with patch( + "qubes_config.global_config.device_widgets.ask_question", + return_value=Gtk.ResponseType.YES, + ): + block_handler.remove_button.clicked() + + expected_calls = [ + ("test-dev2", "admin.vm.device.denied.Remove", None, b"p02****"), + ("test-dev2", "admin.vm.device.denied.Remove", None, b"u02****"), + ("test-dev2", "admin.vm.device.denied.Remove", None, b"p0703**"), + ("test-dev2", "admin.vm.device.denied.Remove", None, b"ue0****"), + ("test-dev2", "admin.vm.device.denied.Remove", None, b"p123***"), + ] + for call in expected_calls: + assert call not in qapp.actual_calls + qapp.expected_calls[call] = b"0\x00" + + block_handler.save() + + for call in expected_calls: + assert call in qapp.actual_calls + + +def test_policy_block_add(block_handler): + qapp = block_handler.qapp + assert block_handler.add_button.is_sensitive() + + block_handler.add_button.clicked() + + block_handler.edit_dialog.qube_model.select_value("test-blue") + + for row in block_handler.edit_dialog.listbox.get_children(): + if row.category_wrapper.name == "Human interface devices": + row.activate() + break + else: + assert False, "Category not found" + + for row in block_handler.edit_dialog.listbox.get_children(): + assert row.check_box.get_active() == ( + row.category_wrapper.name + in ("Human interface devices", "Mice", "Keyboards") + ) + + assert block_handler.edit_dialog.ok_button.get_sensitive() + block_handler.edit_dialog.ok_button.clicked() + assert len(block_handler.rule_list.get_children()) == 3 + + # only top level categories should block + expected_calls = [ + ("test-blue", "admin.vm.device.denied.Add", None, b"u03****"), + ("test-blue", "admin.vm.device.denied.Add", None, b"p09****"), + ] + for call in expected_calls: + assert call not in qapp.actual_calls + qapp.expected_calls[call] = b"0\x00" + + block_handler.save() + + for call in expected_calls: + assert call in qapp.actual_calls + + +def test_policy_block_all(block_handler): + qapp = block_handler.qapp + assert block_handler.add_button.is_sensitive() + + block_handler.add_button.clicked() + + block_handler.edit_dialog.qube_model.select_value("test-blue") + + for row in block_handler.edit_dialog.listbox.get_children(): + if row.category_wrapper.name == "All devices": + row.activate() + break + else: + assert False, "Category not found" + + for row in block_handler.edit_dialog.listbox.get_children(): + assert row.check_box.get_active() + + assert block_handler.edit_dialog.ok_button.get_sensitive() + block_handler.edit_dialog.ok_button.clicked() + assert len(block_handler.rule_list.get_children()) == 3 + + # only top level categories should block + expected_calls = [ + ("test-blue", "admin.vm.device.denied.Add", None, b"*******"), + ] + for call in expected_calls: + assert call not in qapp.actual_calls + qapp.expected_calls[call] = b"0\x00" + + block_handler.save() + + for call in expected_calls: + assert call in qapp.actual_calls + + +def test_policy_block_edit_vm(block_handler): + qapp = block_handler.qapp + assert not block_handler.edit_button.is_sensitive() + + for row in block_handler.rule_list.get_children(): + if row.vm_wrapper.vm.name == "test-dev2": + block_handler.rule_list.select_row(row) + break + else: + assert False, "Failed to find test-dev2 row" + + assert block_handler.edit_button.is_sensitive() + + block_handler.edit_button.clicked() + + assert block_handler.edit_dialog.qube_model.get_selected().name == "test-dev2" + + for row in block_handler.edit_dialog.listbox.get_children(): + if isinstance(row, OtherCategoryRow): + assert row.text_box.get_text() == "p123***" + break + else: + assert False, "Misc row not found" + + for row in block_handler.edit_dialog.listbox.get_children(): + assert isinstance(row, OtherCategoryRow) or ( + row.check_box.get_active() + == (row.category_wrapper.name == "Network devices") + ) + + block_handler.edit_dialog.qube_model.select_value("test-blue") + block_handler.edit_dialog.ok_button.clicked() + + expected_calls = [ + ("test-dev2", "admin.vm.device.denied.Remove", None, b"p02****"), + ("test-dev2", "admin.vm.device.denied.Remove", None, b"u02****"), + ("test-dev2", "admin.vm.device.denied.Remove", None, b"p0703**"), + ("test-dev2", "admin.vm.device.denied.Remove", None, b"ue0****"), + ("test-dev2", "admin.vm.device.denied.Remove", None, b"p123***"), + ("test-blue", "admin.vm.device.denied.Add", None, b"p02****"), + ("test-blue", "admin.vm.device.denied.Add", None, b"p0703**"), + ("test-blue", "admin.vm.device.denied.Add", None, b"ue0****"), + ("test-blue", "admin.vm.device.denied.Add", None, b"u02****"), + ("test-blue", "admin.vm.device.denied.Add", None, b"p123***"), + ] + for call in expected_calls: + assert call not in qapp.actual_calls + qapp.expected_calls[call] = b"0\x00" + + block_handler.save() + + for call in expected_calls: + assert call in qapp.actual_calls + + +def test_policy_block_edit_rules(block_handler): + qapp = block_handler.qapp + assert not block_handler.edit_button.is_sensitive() + + for row in block_handler.rule_list.get_children(): + if row.vm_wrapper.vm.name == "test-dev2": + block_handler.rule_list.select_row(row) + break + else: + assert False, "Failed to find test-dev2 row" + + assert block_handler.edit_button.is_sensitive() + + block_handler.edit_button.clicked() + + for row in block_handler.edit_dialog.listbox.get_children(): + if row.category_wrapper.name == "Human interface devices": + row.activate() + break + else: + assert False, "Category not found" + + block_handler.edit_dialog.ok_button.clicked() + + expected_calls = [ + ("test-dev2", "admin.vm.device.denied.Add", None, b"u03****"), + ("test-dev2", "admin.vm.device.denied.Add", None, b"p09****"), + ] + for call in expected_calls: + assert call not in qapp.actual_calls + qapp.expected_calls[call] = b"0\x00" + + block_handler.save() + + for call in expected_calls: + assert call in qapp.actual_calls + + +def test_policy_block_add_cancel(block_handler): + assert block_handler.add_button.is_sensitive() + + assert len(block_handler.rule_list.get_children()) == 2 + + block_handler.add_button.clicked() + block_handler.edit_dialog.cancel_button.clicked() + + assert len(block_handler.rule_list.get_children()) == 2 + + +def test_policy_block_unsaved(block_handler): + # add + block_handler.add_button.clicked() + block_handler.edit_dialog.qube_model.select_value("test-red") + + for row in block_handler.edit_dialog.listbox.get_children(): + if row.category_wrapper.name == "Printers": + row.activate() + break + else: + assert False, "Category not found" + block_handler.edit_dialog.ok_button.clicked() + + # remove + for row in block_handler.rule_list.get_children(): + if row.vm_wrapper.vm.name == "test-dev2": + block_handler.rule_list.select_row(row) + break + else: + assert False, "Failed to find test-dev2 row" + + with patch( + "qubes_config.global_config.device_widgets.ask_question", + return_value=Gtk.ResponseType.YES, + ): + block_handler.remove_button.clicked() + + # edit + for row in block_handler.rule_list.get_children(): + if row.vm_wrapper.vm.name == "test-dev": + block_handler.rule_list.select_row(row) + break + else: + assert False, "Failed to find test-dev row" + block_handler.edit_button.clicked() + block_handler.edit_dialog.qube_model.select_value("test-blue") + block_handler.edit_dialog.ok_button.clicked() + + unsaved = block_handler.get_unsaved().split("\n") + assert len(unsaved) == 3 + assert len([l for l in unsaved if "Removed" in l and "test-dev2" in l]) == 1 + assert len([l for l in unsaved if "changed" in l and "test-blue" in l]) == 1 + assert ( + len( + [ + l + for l in unsaved + if "changed" in l and "test-red" in l and "Printers" in l + ] + ) + == 1 + ) + + +def test_policy_block_reset(block_handler): + # add + block_handler.add_button.clicked() + block_handler.edit_dialog.qube_model.select_value("test-red") + + for row in block_handler.edit_dialog.listbox.get_children(): + if row.category_wrapper.name == "Printers": + row.activate() + break + else: + assert False, "Category not found" + block_handler.edit_dialog.ok_button.clicked() + + # remove + for row in block_handler.rule_list.get_children(): + if row.vm_wrapper.vm.name == "test-dev2": + block_handler.rule_list.select_row(row) + break + else: + assert False, "Failed to find test-dev2 row" + + with patch( + "qubes_config.global_config.device_widgets.ask_question", + return_value=Gtk.ResponseType.YES, + ): + block_handler.remove_button.clicked() + + # edit + for row in block_handler.rule_list.get_children(): + if row.vm_wrapper.vm.name == "test-dev": + block_handler.rule_list.select_row(row) + break + else: + assert False, "Failed to find test-dev row" + block_handler.edit_button.clicked() + block_handler.edit_dialog.qube_model.select_value("test-blue") + block_handler.edit_dialog.ok_button.clicked() + + block_handler.reset() + assert block_handler.get_unsaved() == "" + + # nothing should be called + block_handler.save() + + +def test_policy_block_add_after_edit(block_handler): + for row in block_handler.rule_list.get_children(): + if row.vm_wrapper.vm.name == "test-dev2": + block_handler.rule_list.select_row(row) + break + else: + assert False, "Failed to find test-dev2 row" + + assert block_handler.edit_button.is_sensitive() + block_handler.edit_button.clicked() + block_handler.edit_dialog.cancel_button.clicked() + + block_handler.add_button.clicked() + + assert block_handler.edit_dialog.qube_model.get_selected() is None + for row in block_handler.edit_dialog.listbox.get_children(): + assert not row.check_box.get_active() + + +def test_multiple_save(block_handler): + qapp = block_handler.qapp + + # add + block_handler.add_button.clicked() + block_handler.edit_dialog.qube_model.select_value("test-red") + + for row in block_handler.edit_dialog.listbox.get_children(): + if row.category_wrapper.name == "Printers": + row.activate() + break + else: + assert False, "Category not found" + block_handler.edit_dialog.ok_button.clicked() + # remove + for row in block_handler.rule_list.get_children(): + if row.vm_wrapper.vm.name == "test-dev2": + block_handler.rule_list.select_row(row) + break + else: + assert False, "Failed to find test-dev2 row" + + with patch( + "qubes_config.global_config.device_widgets.ask_question", + return_value=Gtk.ResponseType.YES, + ): + block_handler.remove_button.clicked() + + # edit + for row in block_handler.rule_list.get_children(): + if row.vm_wrapper.vm.name == "test-dev": + block_handler.rule_list.select_row(row) + break + else: + assert False, "Failed to find test-dev row" + block_handler.edit_button.clicked() + block_handler.edit_dialog.qube_model.select_value("test-blue") + block_handler.edit_dialog.ok_button.clicked() + + expected_calls = [ + ("test-red", "admin.vm.device.denied.Add", None, b"u07****"), + ("test-dev2", "admin.vm.device.denied.Remove", None, b"p123***"), + ("test-dev2", "admin.vm.device.denied.Remove", None, b"p02****"), + ("test-dev2", "admin.vm.device.denied.Remove", None, b"p0703**"), + ("test-dev2", "admin.vm.device.denied.Remove", None, b"ue0****"), + ("test-dev2", "admin.vm.device.denied.Remove", None, b"u02****"), + ("test-blue", "admin.vm.device.denied.Add", None, b"m******"), + ("test-dev", "admin.vm.device.denied.Remove", None, b"m******"), + ] + for call in expected_calls: + assert call not in qapp.actual_calls + qapp.expected_calls[call] = b"0\x00" + + block_handler.save() + + for call in expected_calls: + assert call in qapp.actual_calls + + # save a couple more times + for call in expected_calls: + del qapp.expected_calls[call] + assert qapp.actual_calls.count(call) == 1 + + block_handler.save() + block_handler.save() + block_handler.save() + + for call in expected_calls: + assert qapp.actual_calls.count(call) == 1 diff --git a/qubes_config/tests/test_global_config.py b/qubes_config/tests/test_global_config.py index 67190f79..ec712025 100644 --- a/qubes_config/tests/test_global_config.py +++ b/qubes_config/tests/test_global_config.py @@ -25,6 +25,7 @@ import time from unittest.mock import patch +from qubesadmin.tests.mock_app import MockQube from ..global_config.global_config import ( GlobalConfig, ClipboardHandler, @@ -54,6 +55,21 @@ @patch("qubesadmin.app.VMCollection", TestVMCollection) def test_vmcollection_global_config(test_qapp): + test_qapp._qubes["disp123"] = MockQube( + name="disp123", + qapp=test_qapp, + klass="DispVM", + auto_cleanup=True, + ) + # this dispvm will die suddenly in the middle of Global Config running + test_qapp._qubes["disp666"] = MockQube( + name="disp666", + qapp=test_qapp, + klass="DispVM", + ) + + test_qapp.update_vm_calls() + collection = VMCollection(test_qapp) test_qapp.expected_calls[ ("disp666", "admin.vm.property.Get", "auto_cleanup", None) @@ -75,6 +91,9 @@ def test_qubes_global_config(): def test_global_config_init( mock_error, mock_subprocess, test_qapp, test_policy_manager, test_builder ): + test_qapp.expected_calls[ + ("dom0", "admin.vm.feature.Get", "preload-dispvm-max", None) + ] = b"0\x00" mock_subprocess.return_value = b"" app = GlobalConfig(test_qapp, test_policy_manager) # do not call do_activate - it will make Gtk confused and, in case @@ -84,9 +103,7 @@ def test_global_config_init( # switch across pages, nothing should happen while ( - app.main_notebook.get_nth_page( - app.main_notebook.get_current_page() - ).get_name() + app.main_notebook.get_nth_page(app.main_notebook.get_current_page()).get_name() != "thisdevice" ): app.main_notebook.next_page() @@ -95,9 +112,7 @@ def test_global_config_init( app.main_notebook.set_current_page(0) while ( - app.main_notebook.get_nth_page( - app.main_notebook.get_current_page() - ).get_name() + app.main_notebook.get_nth_page(app.main_notebook.get_current_page()).get_name() != "clipboard" ): app.main_notebook.next_page() @@ -140,6 +155,9 @@ def test_global_config_init( def test_global_config_page_change( mock_error, mock_subprocess, test_qapp, test_policy_manager, test_builder ): + test_qapp.expected_calls[ + ("dom0", "admin.vm.feature.Get", "preload-dispvm-max", None) + ] = b"0\x00" mock_subprocess.return_value = b"" app = GlobalConfig(test_qapp, test_policy_manager) # do not call do_activate - it will make Gtk confused and, in case @@ -148,9 +166,7 @@ def test_global_config_page_change( assert test_builder while ( - app.main_notebook.get_nth_page( - app.main_notebook.get_current_page() - ).get_name() + app.main_notebook.get_nth_page(app.main_notebook.get_current_page()).get_name() != "file" ): app.main_notebook.next_page() @@ -238,6 +254,9 @@ def test_global_config_page_change( def test_global_config_failure( mock_error, mock_subprocess, test_qapp, test_policy_manager, test_builder ): + test_qapp.expected_calls[ + ("dom0", "admin.vm.feature.Get", "preload-dispvm-max", None) + ] = b"0\x00" mock_subprocess.return_value = b"" app = GlobalConfig(test_qapp, test_policy_manager) # do not call do_activate - it will make Gtk confused and, in case @@ -287,9 +306,7 @@ def test_global_config_broken_system( # switch across pages, nothing should happen while ( - app.main_notebook.get_nth_page( - app.main_notebook.get_current_page() - ).get_name() + app.main_notebook.get_nth_page(app.main_notebook.get_current_page()).get_name() != "thisdevice" ): app.main_notebook.next_page() @@ -302,6 +319,9 @@ def test_global_config_broken_system( def test_global_config_open_at( mock_error, mock_subprocess, test_qapp, test_policy_manager, real_builder ): + test_qapp.expected_calls[ + ("dom0", "admin.vm.feature.Get", "preload-dispvm-max", None) + ] = b"0\x00" mock_subprocess.return_value = b"" app = GlobalConfig(test_qapp, test_policy_manager) # do not call do_activate - it will make Gtk confused and, in case diff --git a/qubes_config/tests/test_new_qube/test_application_selector.py b/qubes_config/tests/test_new_qube/test_application_selector.py index 0b0d0313..1599ec3a 100644 --- a/qubes_config/tests/test_new_qube/test_application_selector.py +++ b/qubes_config/tests/test_new_qube/test_application_selector.py @@ -54,10 +54,7 @@ def mock_output(command): mock_subprocess.side_effect = mock_output template_handler = TemplateHandler(new_qube_builder, test_qapp) - assert ( - template_handler.get_selected_template() - == test_qapp.domains["fedora-36"] - ) + assert template_handler.get_selected_template() == test_qapp.domains["fedora-36"] app_selector = ApplicationBoxHandler(new_qube_builder, template_handler) # default template is selected at start, so: @@ -103,10 +100,7 @@ def mock_output(command): mock_subprocess.side_effect = mock_output template_handler = TemplateHandler(new_qube_builder, test_qapp) - assert ( - template_handler.get_selected_template() - == test_qapp.domains["fedora-36"] - ) + assert template_handler.get_selected_template() == test_qapp.domains["fedora-36"] app_selector = ApplicationBoxHandler(new_qube_builder, template_handler) # click the plus button @@ -156,9 +150,7 @@ def mock_output(command): @patch("subprocess.check_output") -def test_app_handler_change_template( - mock_subprocess, test_qapp, new_qube_builder -): +def test_app_handler_change_template(mock_subprocess, test_qapp, new_qube_builder): def mock_output(command): vm_name = command[-1] if command[1] == "--get-available": @@ -183,10 +175,7 @@ def mock_output(command): mock_subprocess.side_effect = mock_output template_handler = TemplateHandler(new_qube_builder, test_qapp) - assert ( - template_handler.get_selected_template() - == test_qapp.domains["fedora-36"] - ) + assert template_handler.get_selected_template() == test_qapp.domains["fedora-36"] app_selector = ApplicationBoxHandler(new_qube_builder, template_handler) assert app_selector.get_selected_apps() == ["firefox.desktop"] @@ -272,10 +261,7 @@ def mock_output(command): mock_subprocess.side_effect = mock_output template_handler = TemplateHandler(new_qube_builder, test_qapp) - assert ( - template_handler.get_selected_template() - == test_qapp.domains["fedora-36"] - ) + assert template_handler.get_selected_template() == test_qapp.domains["fedora-36"] app_selector = ApplicationBoxHandler(new_qube_builder, template_handler) for child in app_selector.flowbox.get_children(): @@ -309,10 +295,7 @@ def mock_output(command): else: assert False # didn't find udon - assert ( - template_handler.get_selected_template() - == test_qapp.domains["fedora-35"] - ) + assert template_handler.get_selected_template() == test_qapp.domains["fedora-35"] @patch("subprocess.check_output") @@ -341,10 +324,7 @@ def mock_output(command): mock_subprocess.side_effect = mock_output template_handler = TemplateHandler(new_qube_builder, test_qapp) - assert ( - template_handler.get_selected_template() - == test_qapp.domains["fedora-36"] - ) + assert template_handler.get_selected_template() == test_qapp.domains["fedora-36"] app_selector = ApplicationBoxHandler(new_qube_builder, template_handler) assert app_selector.get_selected_apps() == ["firefox.desktop"] diff --git a/qubes_config/tests/test_new_qube/test_network_selector.py b/qubes_config/tests/test_new_qube/test_network_selector.py index 891c1900..6042a6db 100644 --- a/qubes_config/tests/test_new_qube/test_network_selector.py +++ b/qubes_config/tests/test_new_qube/test_network_selector.py @@ -59,10 +59,5 @@ def test_network_handler_whonix(test_qapp_whonix, new_qube_builder): handler.network_tor.set_active(True) - assert ( - handler.get_selected_netvm() == test_qapp_whonix.domains["sys-whonix"] - ) - assert ( - handler.network_current_widget.vm - == test_qapp_whonix.domains["sys-whonix"] - ) + assert handler.get_selected_netvm() == test_qapp_whonix.domains["sys-whonix"] + assert handler.network_current_widget.vm == test_qapp_whonix.domains["sys-whonix"] diff --git a/qubes_config/tests/test_new_qube/test_new_qube.py b/qubes_config/tests/test_new_qube/test_new_qube.py index c38bf536..e4179bbe 100644 --- a/qubes_config/tests/test_new_qube/test_new_qube.py +++ b/qubes_config/tests/test_new_qube/test_new_qube.py @@ -50,9 +50,7 @@ def mock_output(command, *_args, **_kwargs): @patch("subprocess.check_output", side_effect=mock_output) @patch("qubes_config.new_qube.new_qube_app.show_error") -def test_simple_new_qube( - mock_error, mock_subprocess, test_qapp, new_qube_builder -): +def test_simple_new_qube(mock_error, mock_subprocess, test_qapp, new_qube_builder): # the builder fixture must be called to register needed signals and # only do it once assert new_qube_builder @@ -77,9 +75,9 @@ def test_simple_new_qube( ] = b"0\x00" # netvm is default - test_qapp.expected_calls[ - ("test", "admin.vm.property.Reset", "netvm", None) - ] = b"0\x00" + test_qapp.expected_calls[("test", "admin.vm.property.Reset", "netvm", None)] = ( + b"0\x00" + ) # not provide network test_qapp.expected_calls[ @@ -107,8 +105,7 @@ def test_simple_new_qube( in mock_popen.mock_calls ) assert ( - call().__enter__().communicate(b"firefox.desktop") - in mock_popen.mock_calls + call().__enter__().communicate(b"firefox.desktop") in mock_popen.mock_calls ) mock_error.assert_not_called() @@ -118,9 +115,7 @@ def test_simple_new_qube( @patch("subprocess.check_output", side_effect=mock_output) @patch("qubes_config.new_qube.new_qube_app.show_error") -def test_complex_new_qube( - mock_error, mock_subprocess, test_qapp, new_qube_builder -): +def test_complex_new_qube(mock_error, mock_subprocess, test_qapp, new_qube_builder): # the builder fixture must be called to register needed signals and # only do it once assert new_qube_builder @@ -147,9 +142,7 @@ def test_complex_new_qube( else: assert False # button not found for row in new_qube_app.app_box_handler.apps_list.get_children(): - if hasattr(row, "appdata") and ( - row.appdata.name in ["Green App", "Eggs App"] - ): + if hasattr(row, "appdata") and (row.appdata.name in ["Green App", "Eggs App"]): row.activate() new_qube_app.app_box_handler.apps_close.clicked() @@ -163,9 +156,7 @@ def test_complex_new_qube( ] = b"0\x00" # netvm is None - test_qapp.expected_calls[ - ("test", "admin.vm.property.Set", "netvm", b"") - ] = b"0\x00" + test_qapp.expected_calls[("test", "admin.vm.property.Set", "netvm", b"")] = b"0\x00" # not provide network test_qapp.expected_calls[ @@ -204,9 +195,7 @@ def test_complex_new_qube( @patch("subprocess.check_output", side_effect=mock_output) @patch("qubes_config.new_qube.new_qube_app.show_error") -def test_new_template_cloned( - mock_error, mock_subprocess, test_qapp, new_qube_builder -): +def test_new_template_cloned(mock_error, mock_subprocess, test_qapp, new_qube_builder): # the builder fixture must be called to register needed signals and # only do it once assert new_qube_builder @@ -233,14 +222,14 @@ def test_new_template_cloned( ("dom0", "admin.vm.Create.TemplateVM", None, b"name=test label=green") ] = b"0\x00" # but as user requested red label, we replace it - test_qapp.expected_calls[ - ("test", "admin.vm.property.Set", "label", b"red") - ] = b"0\x00" + test_qapp.expected_calls[("test", "admin.vm.property.Set", "label", b"red")] = ( + b"0\x00" + ) # netvm is default - test_qapp.expected_calls[ - ("test", "admin.vm.property.Reset", "netvm", None) - ] = b"0\x00" + test_qapp.expected_calls[("test", "admin.vm.property.Reset", "netvm", None)] = ( + b"0\x00" + ) # not provide network test_qapp.expected_calls[ @@ -302,17 +291,17 @@ def test_new_standalone( ] = b"0\x00" # it's a standalone so it should be a hvm with no kernel - test_qapp.expected_calls[ - ("test", "admin.vm.property.Set", "virt_mode", b"hvm") - ] = b"0\x00" - test_qapp.expected_calls[ - ("test", "admin.vm.property.Set", "kernel", b"") - ] = b"0\x00" + test_qapp.expected_calls[("test", "admin.vm.property.Set", "virt_mode", b"hvm")] = ( + b"0\x00" + ) + test_qapp.expected_calls[("test", "admin.vm.property.Set", "kernel", b"")] = ( + b"0\x00" + ) # netvm is default - test_qapp.expected_calls[ - ("test", "admin.vm.property.Reset", "netvm", None) - ] = b"0\x00" + test_qapp.expected_calls[("test", "admin.vm.property.Reset", "netvm", None)] = ( + b"0\x00" + ) # not provide network test_qapp.expected_calls[ @@ -332,8 +321,7 @@ def test_new_standalone( assert mock_dialog.mock_calls # called to tell us about the success assert ( - call(["qubes-vm-boot-from-device", "test"]) - in mock_check_call.mock_calls + call(["qubes-vm-boot-from-device", "test"]) in mock_check_call.mock_calls ) # called install system to qube # but no apps were added @@ -352,9 +340,7 @@ def test_new_standalone( @patch("subprocess.check_output", side_effect=mock_output) @patch("qubes_config.new_qube.new_qube_app.show_error") -def test_new_disposable( - mock_error, mock_subprocess, test_qapp, new_qube_builder -): +def test_new_disposable(mock_error, mock_subprocess, test_qapp, new_qube_builder): # the builder fixture must be called to register needed signals and # only do it once assert new_qube_builder @@ -389,17 +375,17 @@ def test_new_disposable( ] = b"0\x00" # it's a standalone so it should be a hvm with no kernel - test_qapp.expected_calls[ - ("test", "admin.vm.property.Set", "virt_mode", b"hvm") - ] = b"0\x00" - test_qapp.expected_calls[ - ("test", "admin.vm.property.Set", "kernel", b"") - ] = b"0\x00" + test_qapp.expected_calls[("test", "admin.vm.property.Set", "virt_mode", b"hvm")] = ( + b"0\x00" + ) + test_qapp.expected_calls[("test", "admin.vm.property.Set", "kernel", b"")] = ( + b"0\x00" + ) # netvm is default - test_qapp.expected_calls[ - ("test", "admin.vm.property.Reset", "netvm", None) - ] = b"0\x00" + test_qapp.expected_calls[("test", "admin.vm.property.Reset", "netvm", None)] = ( + b"0\x00" + ) # not provide network test_qapp.expected_calls[ @@ -467,18 +453,18 @@ def test_advanced_new_qube( ] = b"0\x00" # netvm is default - test_qapp.expected_calls[ - ("test", "admin.vm.property.Reset", "netvm", None) - ] = b"0\x00" + test_qapp.expected_calls[("test", "admin.vm.property.Reset", "netvm", None)] = ( + b"0\x00" + ) # not provide network test_qapp.expected_calls[ ("test", "admin.vm.property.Set", "provides_network", b"False") ] = b"0\x00" # memory - test_qapp.expected_calls[ - ("test", "admin.vm.property.Set", "memory", b"400") - ] = b"0\x00" + test_qapp.expected_calls[("test", "admin.vm.property.Set", "memory", b"400")] = ( + b"0\x00" + ) # also add a call we do after adding the vm: test_qapp.expected_calls[("dom0", "admin.vm.List", None, None)] = ( @@ -501,8 +487,7 @@ def test_advanced_new_qube( in mock_popen.mock_calls ) assert ( - call().__enter__().communicate(b"firefox.desktop") - in mock_popen.mock_calls + call().__enter__().communicate(b"firefox.desktop") in mock_popen.mock_calls ) mock_error.assert_not_called() diff --git a/qubes_config/tests/test_new_qube/test_template_handlers.py b/qubes_config/tests/test_new_qube/test_template_handlers.py index c0504e04..3a529120 100644 --- a/qubes_config/tests/test_new_qube/test_template_handlers.py +++ b/qubes_config/tests/test_new_qube/test_template_handlers.py @@ -68,9 +68,7 @@ def test_template_handler_none(mock_subprocess, test_qapp, new_qube_builder): # templates are available assert handler.is_given_template_available(test_qapp.domains["fedora-36"]) assert handler.is_given_template_available(test_qapp.domains["fedora-35"]) - assert not handler.is_given_template_available( - test_qapp.domains["test-standalone"] - ) + assert not handler.is_given_template_available(test_qapp.domains["test-standalone"]) assert not handler.is_given_template_available(test_qapp.domains["dom0"]) assert not handler.is_given_template_available(test_qapp.domains["test-vm"]) @@ -99,9 +97,7 @@ def test_template_handler_none(mock_subprocess, test_qapp, new_qube_builder): @patch("subprocess.check_output") -def test_template_handler_select_vm( - mock_subprocess, test_qapp, new_qube_builder -): +def test_template_handler_select_vm(mock_subprocess, test_qapp, new_qube_builder): mock_subprocess.return_value = b"" handler = TemplateHandler(new_qube_builder, test_qapp) diff --git a/qubes_config/tests/test_policy_editor.py b/qubes_config/tests/test_policy_editor.py index 0546f8d4..be372dc8 100644 --- a/qubes_config/tests/test_policy_editor.py +++ b/qubes_config/tests/test_policy_editor.py @@ -46,9 +46,7 @@ def test_open_file(test_policy_client): def test_open_file_not_found(test_policy_client): - with patch( - "qubes_config.policy_editor.policy_editor.ask_question" - ) as mock_ask: + with patch("qubes_config.policy_editor.policy_editor.ask_question") as mock_ask: mock_ask.return_value = Gtk.ResponseType.CANCEL policy_editor = PolicyEditor("new-file", test_policy_client) with patch.object(policy_editor, "_quit") as mock_quit: @@ -65,9 +63,7 @@ def test_open_file_not_found(test_policy_client): ) assert mock_quit.call_count == 1 - with patch( - "qubes_config.policy_editor.policy_editor.ask_question" - ) as mock_ask: + with patch("qubes_config.policy_editor.policy_editor.ask_question") as mock_ask: mock_ask.return_value = Gtk.ResponseType.NO policy_editor = PolicyEditor("new-file", test_policy_client) with patch.object(policy_editor, "_quit") as mock_quit: @@ -77,9 +73,7 @@ def test_open_file_not_found(test_policy_client): assert not policy_editor.source_view.get_sensitive() assert mock_quit.call_count == 0 - with patch( - "qubes_config.policy_editor.policy_editor.ask_question" - ) as mock_ask: + with patch("qubes_config.policy_editor.policy_editor.ask_question") as mock_ask: mock_ask.return_value = Gtk.ResponseType.YES policy_editor = PolicyEditor("new-file", test_policy_client) with patch.object(policy_editor, "_quit") as mock_quit: @@ -103,42 +97,30 @@ def test_detect_changes(test_policy_client): policy_editor.perform_setup() assert not policy_editor.builder.get_object("button_save").get_sensitive() - assert not policy_editor.builder.get_object( - "button_save_exit" - ).get_sensitive() + assert not policy_editor.builder.get_object("button_save_exit").get_sensitive() assert policy_editor.error_info.get_style_context().has_class("error_ok") - assert not policy_editor.error_info.get_style_context().has_class( - "error_bad" - ) + assert not policy_editor.error_info.get_style_context().has_class("error_bad") policy_editor.source_buffer.set_text("Test * @anyvm @anyvm allow") assert policy_editor.builder.get_object("button_save").get_sensitive() assert policy_editor.builder.get_object("button_save_exit").get_sensitive() assert policy_editor.error_info.get_style_context().has_class("error_ok") - assert not policy_editor.error_info.get_style_context().has_class( - "error_bad" - ) + assert not policy_editor.error_info.get_style_context().has_class("error_bad") policy_editor.source_buffer.set_text("Test * @anyvm @anyvm andruty") assert not policy_editor.builder.get_object("button_save").get_sensitive() - assert not policy_editor.builder.get_object( - "button_save_exit" - ).get_sensitive() + assert not policy_editor.builder.get_object("button_save_exit").get_sensitive() assert policy_editor.error_info.get_style_context().has_class("error_bad") - assert not policy_editor.error_info.get_style_context().has_class( - "error_ok" - ) + assert not policy_editor.error_info.get_style_context().has_class("error_ok") policy_editor.source_buffer.set_text("Test +any work @anyvm allow") assert policy_editor.builder.get_object("button_save").get_sensitive() assert policy_editor.builder.get_object("button_save_exit").get_sensitive() assert policy_editor.error_info.get_style_context().has_class("error_ok") - assert not policy_editor.error_info.get_style_context().has_class( - "error_bad" - ) + assert not policy_editor.error_info.get_style_context().has_class("error_bad") def test_open_another_file(test_policy_client): @@ -175,9 +157,7 @@ def test_open_another_file(test_policy_client): Test * test-red test-blue deny""" ) assert not policy_editor.builder.get_object("button_save").get_sensitive() - assert not policy_editor.builder.get_object( - "button_save_exit" - ).get_sensitive() + assert not policy_editor.builder.get_object("button_save_exit").get_sensitive() def test_save_changes(test_policy_client): @@ -199,20 +179,14 @@ def test_save_changes(test_policy_client): assert test_policy_client.files["a-test"] == "Test * @anyvm @anyvm allow" assert not policy_editor.builder.get_object("button_save").get_sensitive() - assert not policy_editor.builder.get_object( - "button_save_exit" - ).get_sensitive() + assert not policy_editor.builder.get_object("button_save_exit").get_sensitive() assert policy_editor.error_info.get_style_context().has_class("error_ok") - assert not policy_editor.error_info.get_style_context().has_class( - "error_bad" - ) + assert not policy_editor.error_info.get_style_context().has_class("error_bad") def test_save_from_new(test_policy_client): policy_editor = PolicyEditor("c-test", test_policy_client) - with patch( - "qubes_config.policy_editor.policy_editor.ask_question" - ) as mock_ask: + with patch("qubes_config.policy_editor.policy_editor.ask_question") as mock_ask: mock_ask.return_value = Gtk.ResponseType.YES policy_editor.perform_setup() assert "c-test" not in test_policy_client.files @@ -242,9 +216,7 @@ def test_deselect_file_on_hide(test_policy_client): policy_editor.action_items["open"].activate() assert policy_editor.file_select_handler.dialog_window.get_visible() - assert ( - policy_editor.file_select_handler.file_list.get_selected_row() is None - ) + assert policy_editor.file_select_handler.file_list.get_selected_row() is None policy_editor.file_select_handler.cancel_button.clicked() assert ( diff --git a/qubes_config/tests/test_policy_exceptions_handler.py b/qubes_config/tests/test_policy_exceptions_handler.py index 192ecc8b..ce2bef4f 100644 --- a/qubes_config/tests/test_policy_exceptions_handler.py +++ b/qubes_config/tests/test_policy_exceptions_handler.py @@ -59,9 +59,7 @@ def test_policy_exc_handler_load_state( current_policy = """TestService * test-vm @dispvm allow target=@dispvm TestService * test-red @dispvm deny TestService * test-blue @dispvm ask default_target=@dispvm:default-dvm""" - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = DispvmExceptionHandler( qapp=test_qapp, @@ -74,12 +72,9 @@ def test_policy_exc_handler_load_state( assert handler.list_handler.current_rules rules = [ - str(rule).replace("\t", " ") - for rule in handler.list_handler.current_rules - ] - expected_rules = [ - rule.replace("\t", " ") for rule in current_policy.split("\n") + str(rule).replace("\t", " ") for rule in handler.list_handler.current_rules ] + expected_rules = [rule.replace("\t", " ") for rule in current_policy.split("\n")] assert sorted(rules) == sorted(expected_rules) @@ -89,9 +84,7 @@ def test_policy_exc_add_rule( current_policy = """TestService * test-vm @dispvm allow target=@dispvm TestService * test-red @dispvm deny""" current_policy_rules = test_policy_manager.text_to_rules(current_policy) - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = DispvmExceptionHandler( qapp=test_qapp, @@ -102,9 +95,7 @@ def test_policy_exc_add_rule( policy_file_name="c-test", ) - assert compare_rule_lists( - handler.list_handler.current_rules, current_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, current_policy_rules) add_rule( handler.list_handler, @@ -118,9 +109,7 @@ def test_policy_exc_add_rule( TestService * test-blue @dispvm ask default_target=@dispvm:default-dvm""" expected_policy_rules = test_policy_manager.text_to_rules(expected_policy) - assert compare_rule_lists( - handler.list_handler.current_rules, expected_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, expected_policy_rules) handler.save() assert compare_rule_lists( @@ -135,9 +124,7 @@ def test_policy_exc_add_rule_error( current_policy = """TestService * test-vm @dispvm allow target=@dispvm TestService * test-red @dispvm deny""" current_policy_rules = test_policy_manager.text_to_rules(current_policy) - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = DispvmExceptionHandler( qapp=test_qapp, @@ -148,9 +135,7 @@ def test_policy_exc_add_rule_error( policy_file_name="c-test", ) - assert compare_rule_lists( - handler.list_handler.current_rules, current_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, current_policy_rules) # error should have occurred add_rule( @@ -162,9 +147,7 @@ def test_policy_exc_add_rule_error( ) # no superfluous rules were added - assert compare_rule_lists( - handler.list_handler.current_rules, current_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, current_policy_rules) # but the row is being edited edited_row = None @@ -185,9 +168,7 @@ def test_policy_exc_add_rule_error( expected_policy_rules = test_policy_manager.text_to_rules(expected_policy) - assert compare_rule_lists( - handler.list_handler.current_rules, expected_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, expected_policy_rules) def test_policy_exc_add_rule_twice( @@ -196,9 +177,7 @@ def test_policy_exc_add_rule_twice( current_policy = """TestService * test-vm @dispvm allow target=@dispvm TestService * test-red @dispvm deny""" current_policy_rules = test_policy_manager.text_to_rules(current_policy) - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = DispvmExceptionHandler( qapp=test_qapp, @@ -209,18 +188,14 @@ def test_policy_exc_add_rule_twice( policy_file_name="c-test", ) - assert compare_rule_lists( - handler.list_handler.current_rules, current_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, current_policy_rules) # click add_rule twice handler.list_handler.add_button.clicked() handler.list_handler.add_button.clicked() # no superfluous rules were yet added - assert compare_rule_lists( - handler.list_handler.current_rules, current_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, current_policy_rules) # but there is a singular row is being edited edited_row = None @@ -242,9 +217,7 @@ def test_policy_exc_add_rule_twice( assert False # wrong, we just closed an edited row # no superfluous rules were added - assert compare_rule_lists( - handler.list_handler.current_rules, current_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, current_policy_rules) def test_policy_exc_edit_rule( @@ -253,9 +226,7 @@ def test_policy_exc_edit_rule( current_policy = """TestService * test-vm @dispvm allow target=@dispvm TestService * test-red @dispvm deny""" current_policy_rules = test_policy_manager.text_to_rules(current_policy) - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = DispvmExceptionHandler( qapp=test_qapp, @@ -266,9 +237,7 @@ def test_policy_exc_edit_rule( policy_file_name="c-test", ) - assert compare_rule_lists( - handler.list_handler.current_rules, current_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, current_policy_rules) for row in handler.list_handler.current_rows: if row.rule.source == "test-red": @@ -288,9 +257,7 @@ def test_policy_exc_edit_rule( expected_policy = """TestService * test-vm @dispvm allow target=@dispvm TestService * test-red @dispvm allow target=@dispvm:default-dvm""" expected_policy_rules = test_policy_manager.text_to_rules(expected_policy) - assert compare_rule_lists( - handler.list_handler.current_rules, expected_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, expected_policy_rules) def test_policy_exc_edit_double_click( @@ -299,9 +266,7 @@ def test_policy_exc_edit_double_click( current_policy = """TestService * test-vm @dispvm allow target=@dispvm TestService * test-red @dispvm ask default_target=@dispvm:default-dvm""" current_policy_rules = test_policy_manager.text_to_rules(current_policy) - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = DispvmExceptionHandler( qapp=test_qapp, @@ -312,9 +277,7 @@ def test_policy_exc_edit_double_click( policy_file_name="c-test", ) - assert compare_rule_lists( - handler.list_handler.current_rules, current_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, current_policy_rules) for row in handler.list_handler.current_rows: if row.rule.source == "test-red": @@ -336,9 +299,7 @@ def test_policy_exc_edit_double_click( TestService * test-red @dispvm ask default_target=@dispvm""" expected_policy_rules = test_policy_manager.text_to_rules(expected_policy) - assert compare_rule_lists( - handler.list_handler.current_rules, expected_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, expected_policy_rules) def test_policy_exc_edit_cancel( @@ -347,9 +308,7 @@ def test_policy_exc_edit_cancel( current_policy = """TestService * test-vm @dispvm allow target=@dispvm TestService * test-red @dispvm ask default_target=@dispvm:default-dvm""" current_policy_rules = test_policy_manager.text_to_rules(current_policy) - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = DispvmExceptionHandler( qapp=test_qapp, @@ -360,9 +319,7 @@ def test_policy_exc_edit_cancel( policy_file_name="c-test", ) - assert compare_rule_lists( - handler.list_handler.current_rules, current_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, current_policy_rules) for row in handler.list_handler.current_rows: if row.rule.source == "test-red": @@ -375,9 +332,7 @@ def test_policy_exc_edit_cancel( else: assert False # expected rule to edit not found! - assert compare_rule_lists( - handler.list_handler.current_rules, current_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, current_policy_rules) # click another row, dismiss message with patch(show_dialog_with_icon_path) as mock_ask: @@ -388,9 +343,7 @@ def test_policy_exc_edit_cancel( break assert mock_ask.mock_calls - assert compare_rule_lists( - handler.list_handler.current_rules, current_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, current_policy_rules) # now do the same, but do not dismiss the message for row in handler.list_handler.current_rows: @@ -418,9 +371,7 @@ def test_policy_exc_edit_cancel( TestService * test-red @dispvm ask default_target=@dispvm""" expected_policy_rules = test_policy_manager.text_to_rules(expected_policy) - assert compare_rule_lists( - handler.list_handler.current_rules, expected_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, expected_policy_rules) def test_policy_exc_close_all_fail( @@ -429,9 +380,7 @@ def test_policy_exc_close_all_fail( current_policy = """TestService * test-vm @dispvm allow target=@dispvm TestService * test-red @dispvm ask default_target=@dispvm:default-dvm""" current_policy_rules = test_policy_manager.text_to_rules(current_policy) - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = DispvmExceptionHandler( qapp=test_qapp, @@ -442,9 +391,7 @@ def test_policy_exc_close_all_fail( policy_file_name="c-test", ) - assert compare_rule_lists( - handler.list_handler.current_rules, current_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, current_policy_rules) for row in handler.list_handler.current_rows: if row.rule.source == "test-red": @@ -469,9 +416,7 @@ def test_policy_exc_close_all_fail( assert mock_ask.mock_calls assert mock_error.mock_calls - assert compare_rule_lists( - handler.list_handler.current_rules, current_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, current_policy_rules) def test_policy_handler_reset( @@ -480,9 +425,7 @@ def test_policy_handler_reset( current_policy = """TestService * test-vm @dispvm allow target=@dispvm TestService * test-red @dispvm ask default_target=@dispvm:default-dvm""" current_policy_rules = test_policy_manager.text_to_rules(current_policy) - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = DispvmExceptionHandler( qapp=test_qapp, @@ -493,9 +436,7 @@ def test_policy_handler_reset( policy_file_name="c-test", ) - assert compare_rule_lists( - handler.list_handler.current_rules, current_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, current_policy_rules) add_rule(handler.list_handler, source="test-blue", action="deny") @@ -504,14 +445,10 @@ def test_policy_handler_reset( TestService * test-blue @dispvm deny""" expected_policy_rules = test_policy_manager.text_to_rules(expected_policy) - assert compare_rule_lists( - handler.list_handler.current_rules, expected_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, expected_policy_rules) handler.reset() - assert compare_rule_lists( - handler.list_handler.current_rules, current_policy_rules - ) + assert compare_rule_lists(handler.list_handler.current_rules, current_policy_rules) handler.save() assert compare_rule_lists( @@ -526,9 +463,7 @@ def test_policy_exc_get_unsupported( current_policy = """TestService * test-vm @anyvm allow target=@dispvm TestService * test-red @dispvm ask default_target=@dispvm:default-dvm""" # current_policy_rules = test_policy_manager.text_to_rules(current_policy) - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = DispvmExceptionHandler( qapp=test_qapp, diff --git a/qubes_config/tests/test_policy_handler.py b/qubes_config/tests/test_policy_handler.py index 0b0c8864..2414d677 100644 --- a/qubes_config/tests/test_policy_handler.py +++ b/qubes_config/tests/test_policy_handler.py @@ -135,9 +135,7 @@ def test_policy_handler_non_default_policy( TestService * @anyvm @anyvm deny""" current_policy_rules = test_policy_manager.text_to_rules(current_policy) - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = PolicyHandler( qapp=test_qapp, @@ -176,9 +174,7 @@ def test_policy_handler_enforce_deny_all( current_policy_with_deny ) - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = PolicyHandler( qapp=test_qapp, @@ -194,9 +190,7 @@ def test_policy_handler_enforce_deny_all( # this should have completely empty policy, enabled default policy assert handler.enable_radio.get_active() - assert compare_rule_lists( - handler.current_rules, current_policy_rules_with_deny - ) + assert compare_rule_lists(handler.current_rules, current_policy_rules_with_deny) def test_policy_handler_add_rule( @@ -606,9 +600,7 @@ def test_policy_handler_view_raw( TestService * @anyvm @anyvm deny""" current_policy_rules = test_policy_manager.text_to_rules(current_policy) - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = PolicyHandler( qapp=test_qapp, @@ -647,9 +639,7 @@ def test_policy_handler_edit_raw( current_policy = """TestService * test-vm test-red allow TestService * @anyvm @anyvm deny""" - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = PolicyHandler( qapp=test_qapp, @@ -683,9 +673,7 @@ def test_policy_handler_edit_raw_error( TestService * @anyvm @anyvm deny""" current_policy_rules = test_policy_manager.text_to_rules(current_policy) - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = PolicyHandler( qapp=test_qapp, @@ -704,9 +692,7 @@ def test_policy_handler_edit_raw_error( TestService * @anyvm @anyvm deny""" handler.raw_handler.text_buffer.set_text(expected_policy) - with patch( - "qubes_config.global_config.policy_handler.show_error" - ) as mock_error: + with patch("qubes_config.global_config.policy_handler.show_error") as mock_error: assert not mock_error.mock_calls handler.raw_handler.raw_save.clicked() assert mock_error.mock_calls @@ -731,9 +717,7 @@ def test_policy_handler_edit_raw_close( current_policy = """TestService * test-vm test-red allow TestService * @anyvm @anyvm deny""" - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = PolicyHandler( qapp=test_qapp, @@ -869,9 +853,7 @@ def test_policy_handler_get_unsaved_unsupported( current_policy = """TestService * test-vm test-blue allow target=test-red TestService * @anyvm @anyvm deny""" - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = PolicyHandler( qapp=test_qapp, @@ -900,9 +882,7 @@ def test_policy_handler_get_unsaved_unsupported( ####### Subset handler -def test_subset_handler( - test_builder, test_qapp, test_policy_manager: PolicyManager -): +def test_subset_handler(test_builder, test_qapp, test_policy_manager: PolicyManager): default_policy = """ TestService * @anyvm test-blue allow""" @@ -927,9 +907,7 @@ def test_subset_handler( handler.add_select_button.clicked() assert handler.add_select_box.get_visible() handler.select_qube_model.select_value("vault") - with patch( - "qubes_config.global_config.policy_handler.ask_question" - ) as mock_ask: + with patch("qubes_config.global_config.policy_handler.ask_question") as mock_ask: handler.add_select_confirm.clicked() # vault is not networked assert not mock_ask.mock_calls @@ -944,9 +922,7 @@ def test_subset_handler( handler.add_select_button.clicked() assert handler.add_select_box.get_visible() handler.select_qube_model.select_value("test-red") - with patch( - "qubes_config.global_config.policy_handler.ask_question" - ) as mock_ask: + with patch("qubes_config.global_config.policy_handler.ask_question") as mock_ask: handler.add_select_confirm.clicked() # test-red is networked assert mock_ask.mock_calls @@ -964,9 +940,7 @@ def test_subset_handler_get_unsaved_unsupported( ): current_policy = """TestService * @anyvm test-blue allow target=test=red""" - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = PolicyHandler( qapp=test_qapp, @@ -1217,9 +1191,7 @@ def test_subset_handler_unsupported( TestService * @anyvm vault allow target=test-blue """ - test_policy_manager.policy_client.policy_replace( - "c-test", current_policy, "any" - ) + test_policy_manager.policy_client.policy_replace("c-test", current_policy, "any") handler = VMSubsetPolicyHandler( qapp=test_qapp, diff --git a/qubes_config/tests/test_policy_manager.py b/qubes_config/tests/test_policy_manager.py index a4e7682f..52875ea3 100644 --- a/qubes_config/tests/test_policy_manager.py +++ b/qubes_config/tests/test_policy_manager.py @@ -34,8 +34,7 @@ def return_files(service_name): manager = PolicyManager() with patch( - "qubes_config.global_config.policy_manager." - "PolicyClient.policy_get_files" + "qubes_config.global_config.policy_manager.PolicyClient.policy_get_files" ) as mock_get: mock_get.side_effect = return_files @@ -48,9 +47,7 @@ def return_files(service_name): "a-test", "b-test", ] - assert manager.get_conflicting_policy_files("test", "b-test") == [ - "a-test" - ] + assert manager.get_conflicting_policy_files("test", "b-test") == ["a-test"] assert not manager.get_conflicting_policy_files("test", "a-test") assert not manager.get_conflicting_policy_files("other", "test") @@ -127,25 +124,15 @@ def test_compare_rules_to_text(): Test * work @anyvm allow""" rules_1 = [ - Rule.from_line( - None, "Test * @anyvm @anyvm deny", filepath=None, lineno=0 - ) + Rule.from_line(None, "Test * @anyvm @anyvm deny", filepath=None, lineno=0) ] rules_2 = [ - Rule.from_line( - None, "Test * @anyvm @anyvm deny", filepath=None, lineno=0 - ), - Rule.from_line( - None, "Test +Test2 work @anyvm allow", filepath=None, lineno=0 - ), + Rule.from_line(None, "Test * @anyvm @anyvm deny", filepath=None, lineno=0), + Rule.from_line(None, "Test +Test2 work @anyvm allow", filepath=None, lineno=0), ] rules_3 = [ - Rule.from_line( - None, "Test * @anyvm @anyvm deny", filepath=None, lineno=0 - ), - Rule.from_line( - None, "Test * work @anyvm allow", filepath=None, lineno=0 - ), + Rule.from_line(None, "Test * @anyvm @anyvm deny", filepath=None, lineno=0), + Rule.from_line(None, "Test * work @anyvm allow", filepath=None, lineno=0), ] assert manager.compare_rules_to_text(rules_1, rule_text_1) @@ -160,9 +147,7 @@ def test_compare_rules_to_text(): def test_new_rule(): manager = PolicyManager() - rule_1 = Rule.from_line( - None, "Test * @anyvm @anyvm deny", filepath=None, lineno=0 - ) + rule_1 = Rule.from_line(None, "Test * @anyvm @anyvm deny", filepath=None, lineno=0) rule_2 = Rule.from_line( None, "Test +Test @anyvm vault allow target=dom0", @@ -170,9 +155,7 @@ def test_new_rule(): lineno=0, ) - assert str(manager.new_rule("Test", "@anyvm", "@anyvm", "deny")) == str( - rule_1 - ) + assert str(manager.new_rule("Test", "@anyvm", "@anyvm", "deny")) == str(rule_1) assert str( manager.new_rule( service="Test", @@ -188,9 +171,7 @@ def test_save_policy(): manager = PolicyManager() rule_text = "Test\t*\t@anyvm\t@anyvm\tdeny" - rule = Rule.from_line( - None, "Test * @anyvm @anyvm deny", filepath=None, lineno=0 - ) + rule = Rule.from_line(None, "Test * @anyvm @anyvm deny", filepath=None, lineno=0) def replace_file(file_name: str, new_text: str, _token): if file_name == "test": @@ -202,8 +183,7 @@ def replace_file(file_name: str, new_text: str, _token): assert False with patch( - "qubes_config.global_config.policy_manager." - "PolicyClient.policy_replace" + "qubes_config.global_config.policy_manager.PolicyClient.policy_replace" ) as mock_replace: mock_replace.side_effect = replace_file manager.save_rules("test", [rule], "any") diff --git a/qubes_config/tests/test_policy_rules.py b/qubes_config/tests/test_policy_rules.py index dff99078..8093072c 100644 --- a/qubes_config/tests/test_policy_rules.py +++ b/qubes_config/tests/test_policy_rules.py @@ -187,9 +187,7 @@ def test_targeted_tokens(): allow_rule = make_rule("vm1", "@default", "allow target=vm2") wrapped_rule = RuleTargeted(allow_rule) wrapped_rule.target = "@anyvm" - assert str(wrapped_rule.raw_rule) == str( - make_rule("vm1", "@anyvm", "allow") - ) + assert str(wrapped_rule.raw_rule) == str(make_rule("vm1", "@anyvm", "allow")) ask_rule = make_rule("vm1", "@default", "ask default_target=vm2") wrapped_rule = RuleTargeted(ask_rule) @@ -257,15 +255,11 @@ def test_targeted_adminvm_change_tokens(): wrapped_ask.source = "vm2" wrapped_deny.source = "vm2" - assert str(wrapped_allow.raw_rule) == str( - make_rule("vm2", "@adminvm", "allow") - ) + assert str(wrapped_allow.raw_rule) == str(make_rule("vm2", "@adminvm", "allow")) assert str(wrapped_ask.raw_rule) == str( make_rule("vm2", "@adminvm", "ask default_target=@adminvm") ) - assert str(wrapped_deny.raw_rule) == str( - make_rule("vm2", "@adminvm", "deny") - ) + assert str(wrapped_deny.raw_rule) == str(make_rule("vm2", "@adminvm", "deny")) def test_targeted_fundamental(): @@ -281,12 +275,8 @@ def test_targeted_fundamental(): def test_targeted_validity(): - assert RuleTargeted.get_rule_errors( - source="vm1", target="@anyvm", action="ask" - ) - assert RuleTargeted.get_rule_errors( - source="vm1", target="@anyvm", action="allow" - ) + assert RuleTargeted.get_rule_errors(source="vm1", target="@anyvm", action="ask") + assert RuleTargeted.get_rule_errors(source="vm1", target="@anyvm", action="allow") assert not RuleTargeted.get_rule_errors( source="vm1", target="@anyvm", action="deny" ) @@ -301,15 +291,9 @@ def test_targeted_validity(): source="vm1", target="@dispvm", action="deny" ) - assert not RuleTargeted.get_rule_errors( - source="vm1", target="vm2", action="ask" - ) - assert not RuleTargeted.get_rule_errors( - source="vm1", target="vm2", action="allow" - ) - assert not RuleTargeted.get_rule_errors( - source="vm1", target="vm2", action="deny" - ) + assert not RuleTargeted.get_rule_errors(source="vm1", target="vm2", action="ask") + assert not RuleTargeted.get_rule_errors(source="vm1", target="vm2", action="allow") + assert not RuleTargeted.get_rule_errors(source="vm1", target="vm2", action="deny") def test_targeted_conflict(): diff --git a/qubes_config/tests/test_rule_list_widgets.py b/qubes_config/tests/test_rule_list_widgets.py index 5fbe06d8..5392d3a9 100644 --- a/qubes_config/tests/test_rule_list_widgets.py +++ b/qubes_config/tests/test_rule_list_widgets.py @@ -50,15 +50,11 @@ def make_rule(source, target, action): ) -VERB_DESCR = SimpleVerbDescription( - {"ask": "ASK", "allow": "ALLOW", "deny": "DENY"} -) +VERB_DESCR = SimpleVerbDescription({"ask": "ASK", "allow": "ALLOW", "deny": "DENY"}) def test_vm_widget(test_qapp): - simple_widget = VMWidget( - qapp=test_qapp, categories=None, initial_value="test-vm" - ) + simple_widget = VMWidget(qapp=test_qapp, categories=None, initial_value="test-vm") # test if parts of the widgets behave as expected # at the start, name should be visible and combo hidden @@ -71,9 +67,7 @@ def test_vm_widget(test_qapp): def test_vm_widget_changes(test_qapp): - simple_widget = VMWidget( - qapp=test_qapp, categories=None, initial_value="test-vm" - ) + simple_widget = VMWidget(qapp=test_qapp, categories=None, initial_value="test-vm") assert not simple_widget.is_changed() assert str(simple_widget.get_selected()) == "test-vm" @@ -121,10 +115,7 @@ def test_action_widget_choices(): assert not action_widget.is_changed() assert str(action_widget.get_selected()) == "allow" - assert ( - action_widget.name_widget.get_text() - == RuleSimple.ACTION_CHOICES["allow"] - ) + assert action_widget.name_widget.get_text() == RuleSimple.ACTION_CHOICES["allow"] action_widget.set_editable(True) action_widget.combobox.set_active_id(RuleSimple.ACTION_CHOICES["ask"]) @@ -133,14 +124,8 @@ def test_action_widget_choices(): # get back to ineditable, change should be discarded action_widget.set_editable(False) assert str(action_widget.get_selected()) == "allow" - assert ( - action_widget.name_widget.get_text() - == RuleSimple.ACTION_CHOICES["allow"] - ) - assert ( - action_widget.combobox.get_active_id() - == RuleSimple.ACTION_CHOICES["allow"] - ) + assert action_widget.name_widget.get_text() == RuleSimple.ACTION_CHOICES["allow"] + assert action_widget.combobox.get_active_id() == RuleSimple.ACTION_CHOICES["allow"] assert not action_widget.is_changed() # let's change stuff for real @@ -153,13 +138,8 @@ def test_action_widget_choices(): assert not action_widget.is_changed() assert str(action_widget.get_selected()) == "ask" - assert ( - action_widget.name_widget.get_text() == RuleSimple.ACTION_CHOICES["ask"] - ) - assert ( - action_widget.combobox.get_active_id() - == RuleSimple.ACTION_CHOICES["ask"] - ) + assert action_widget.name_widget.get_text() == RuleSimple.ACTION_CHOICES["ask"] + assert action_widget.combobox.get_active_id() == RuleSimple.ACTION_CHOICES["ask"] def test_action_widget_verbdescr(): @@ -187,9 +167,7 @@ def test_rule_row(test_qapp): mock_handler.verify_new_rule.return_value = None rule = make_rule("test-blue", "test-red", "ask") - rule_row = RuleListBoxRow( - parent_handler=mock_handler, rule=rule, qapp=test_qapp - ) + rule_row = RuleListBoxRow(parent_handler=mock_handler, rule=rule, qapp=test_qapp) # it should start in a non-editable mode assert not rule_row.action_widget.combobox.get_visible() @@ -241,8 +219,7 @@ def test_rule_delete_new(test_qapp): rule_row.set_edit_mode(True) with patch( - "qubes_config.global_config.rule_list_widgets." - "RuleListBoxRow._do_delete_self" + "qubes_config.global_config.rule_list_widgets.RuleListBoxRow._do_delete_self" ) as mock_delete: # check that rule will try to delete itself if exiting edit mode # with no changes @@ -256,8 +233,7 @@ def test_rule_delete_new(test_qapp): rule_row.set_edit_mode(True) rule_row.source_widget.model.select_value("test-vm") with patch( - "qubes_config.global_config.rule_list_widgets." - "RuleListBoxRow._do_delete_self" + "qubes_config.global_config.rule_list_widgets.RuleListBoxRow._do_delete_self" ) as mock_delete, patch.object(rule_row, "get_parent"): # check that rule will NOT try to delete itself if saving with changes assert rule_row.validate_and_save() diff --git a/qubes_config/tests/test_update_handler.py b/qubes_config/tests/test_update_handler.py index 1b04d998..5ae164d9 100644 --- a/qubes_config/tests/test_update_handler.py +++ b/qubes_config/tests/test_update_handler.py @@ -117,9 +117,7 @@ def test_repo_handler_error(mock_output, real_builder): assert handler.problems_repo_box.get_visible() -def mock_qrexec( - _vm, service, arg, repo_list="", enable_repos=None, disable_repos=None -): +def mock_qrexec(_vm, service, arg, repo_list="", enable_repos=None, disable_repos=None): if "qubes.repos.List" in service: return repo_list @@ -225,9 +223,7 @@ def test_repo_handler_save_fail(real_builder): handler.dom0_stable_radio.set_active(True) - with patch( - "qubes_config.global_config.updates_handler.qrexec_call" - ) as mock_output: + with patch("qubes_config.global_config.updates_handler.qrexec_call") as mock_output: mock_output.side_effect = RuntimeError() with pytest.raises(qubesadmin.exc.QubesException): handler.save() @@ -352,9 +348,7 @@ def test_updates_checker_init_disabled(real_builder, test_qapp): handler = UpdateCheckerHandler(real_builder, test_qapp) assert handler.disable_radio.get_active() assert handler.exceptions_check.get_active() - assert [ - str(vm) for vm in handler.flowbox_handler.selected_vms - ] == expected_qubes + assert [str(vm) for vm in handler.flowbox_handler.selected_vms] == expected_qubes def test_updates_checker_exceptions(real_builder, test_qapp): @@ -388,9 +382,7 @@ def test_updates_checker_exceptions(real_builder, test_qapp): handler = UpdateCheckerHandler(real_builder, test_qapp) assert handler.enable_radio.get_active() assert handler.exceptions_check.get_active() - assert [ - str(vm) for vm in handler.flowbox_handler.selected_vms - ] == disabled_vms + assert [str(vm) for vm in handler.flowbox_handler.selected_vms] == disabled_vms # switch to disable handler.disable_radio.set_active(True) @@ -490,9 +482,7 @@ def test_updates_checker_save_dom0(mock_feature, real_builder, test_qapp): @patch("qubes_config.global_config.updates_handler.apply_feature_change") -def test_updates_checker_save_dom0_initial_none( - mock_feature, real_builder, test_qapp -): +def test_updates_checker_save_dom0_initial_none(mock_feature, real_builder, test_qapp): test_qapp.expected_calls[ ( "dom0", @@ -501,8 +491,7 @@ def test_updates_checker_save_dom0_initial_none( None, ) ] = ( - b"2\x00QubesFeatureNotFoundError\x00\x00service." - b"qubes-update-check\x00" + b"2\x00QubesFeatureNotFoundError\x00\x00service." b"qubes-update-check\x00" ) handler = UpdateCheckerHandler(real_builder, test_qapp) @@ -515,9 +504,7 @@ def test_updates_checker_save_dom0_initial_none( @patch("qubes_config.global_config.updates_handler.apply_feature_change") -def test_updates_checker_save_add_exception( - mock_feature, real_builder, test_qapp -): +def test_updates_checker_save_add_exception(mock_feature, real_builder, test_qapp): test_qapp.expected_calls[ ( "dom0", @@ -581,9 +568,7 @@ def test_updates_checker_save_del_exception( @patch("qubes_config.global_config.updates_handler.apply_feature_change") -def test_updates_checker_save_change_default( - mock_feature, real_builder, test_qapp -): +def test_updates_checker_save_change_default(mock_feature, real_builder, test_qapp): test_qapp.expected_calls[ ( "dom0", @@ -678,9 +663,7 @@ def test_updates_check_reset(mock_question, real_builder, test_qapp): ] -def test_update_proxy_init_no_whonix( - real_builder, test_qapp, test_policy_manager -): +def test_update_proxy_init_no_whonix(real_builder, test_qapp, test_policy_manager): handler = UpdateProxy( real_builder, test_qapp, test_policy_manager, "proxy-test", "proxy" ) @@ -692,9 +675,7 @@ def test_update_proxy_init_no_whonix( assert not handler.whonix_updatevm_box.get_visible() -def test_update_proxy_init_whonix( - real_builder, test_qapp_whonix, test_policy_manager -): +def test_update_proxy_init_whonix(real_builder, test_qapp_whonix, test_policy_manager): handler = UpdateProxy( real_builder, test_qapp_whonix, @@ -916,9 +897,7 @@ def test_update_proxy_save_updatevm( file, rules, arg = mock_save.mock_calls[0].args assert arg is None assert file == "proxy-file" - assert [str(rule) for rule in expected_rules] == [ - str(rule) for rule in rules - ] + assert [str(rule) for rule in expected_rules] == [str(rule) for rule in rules] assert len(mock_feature.mock_calls) == 4 assert ( @@ -1017,9 +996,7 @@ def test_update_proxy_save_justwhonix( file, rules, arg = mock_save.mock_calls[0].args assert arg is None assert file == "proxy-file" - assert [str(rule) for rule in expected_rules] == [ - str(rule) for rule in rules - ] + assert [str(rule) for rule in expected_rules] == [str(rule) for rule in rules] assert len(mock_feature.mock_calls) == 3 assert ( @@ -1083,9 +1060,7 @@ def test_update_proxy_save_add_rule( file, rules, arg = mock_save.mock_calls[0].args assert arg is None assert file == "proxy-file" - assert [str(rule) for rule in expected_rules] == [ - str(rule) for rule in rules - ] + assert [str(rule) for rule in expected_rules] == [str(rule) for rule in rules] assert len(mock_feature.mock_calls) == 3 assert ( @@ -1114,9 +1089,7 @@ def test_update_proxy_save_add_rule( ) -def test_update_proxy_reset( - real_builder, test_qapp_whonix, test_policy_manager -): +def test_update_proxy_reset(real_builder, test_qapp_whonix, test_policy_manager): policy = """ Proxy * fedora-36 @default allow target=sys-net Proxy * @type:TemplateVM @default allow target=sys-firewall @@ -1144,9 +1117,7 @@ def test_update_proxy_reset( # change things - assert handler.updatevm_model.is_vm_available( - test_qapp_whonix.domains["sys-net"] - ) + assert handler.updatevm_model.is_vm_available(test_qapp_whonix.domains["sys-net"]) handler.updatevm_model.select_value("sys-net") # add exception @@ -1179,17 +1150,12 @@ def test_update_proxy_reset( @patch("qubes_config.global_config.updates_handler.qrexec_call") -def test_complete_handler( - mock_output, real_builder, test_qapp, test_policy_manager -): +def test_complete_handler(mock_output, real_builder, test_qapp, test_policy_manager): mock_output.return_value = ALL_ENABLED handler = UpdatesHandler(test_qapp, test_policy_manager, real_builder) # check if dom0 updatevm worked - assert ( - handler.dom0_updatevm_model.get_selected() - == test_qapp.domains["sys-net"] - ) + assert handler.dom0_updatevm_model.get_selected() == test_qapp.domains["sys-net"] # change selection assert handler.dom0_updatevm_model.is_vm_available( @@ -1209,10 +1175,7 @@ def test_complete_handler( handler.reset() assert handler.get_unsaved() == "" - assert ( - handler.dom0_updatevm_model.get_selected() - == test_qapp.domains["sys-net"] - ) + assert handler.dom0_updatevm_model.get_selected() == test_qapp.domains["sys-net"] assert handler.update_checker.dom0_update_check.get_active() @@ -1224,10 +1187,7 @@ def test_complete_handle_dom0updatevm( handler = UpdatesHandler(test_qapp, test_policy_manager, real_builder) # check if dom0 updatevm worked - assert ( - handler.dom0_updatevm_model.get_selected() - == test_qapp.domains["sys-net"] - ) + assert handler.dom0_updatevm_model.get_selected() == test_qapp.domains["sys-net"] # change selection assert handler.dom0_updatevm_model.is_vm_available( diff --git a/qubes_config/tests/test_usb_devices.py b/qubes_config/tests/test_usb_devices.py index f5c231a9..dc9134cb 100644 --- a/qubes_config/tests/test_usb_devices.py +++ b/qubes_config/tests/test_usb_devices.py @@ -38,9 +38,7 @@ from gi.repository import Gtk -def test_input_devices_simple_policy( - test_qapp, test_policy_manager, real_builder -): +def test_input_devices_simple_policy(test_qapp, test_policy_manager, real_builder): sys_usb = test_qapp.domains["sys-usb"] test_policy_manager.policy_client.files[ "50-config-input" @@ -59,8 +57,7 @@ def test_input_devices_simple_policy( assert widget.get_parent() assert ( - handler.widgets[("qubes.InputMouse", "sys-usb")].model.get_selected() - == "ask" + handler.widgets[("qubes.InputMouse", "sys-usb")].model.get_selected() == "ask" ) assert ( handler.widgets[("qubes.InputKeyboard", "sys-usb")].model.get_selected() @@ -87,14 +84,10 @@ def test_input_devices_simple_policy( ) assert len(mock_save.mock_calls) == 1 _, rules, _ = mock_save.mock_calls[0].args - assert [str(rule) for rule in expected_rules] == [ - str(rule) for rule in rules - ] + assert [str(rule) for rule in expected_rules] == [str(rule) for rule in rules] -def test_input_devices_complex_policy( - test_qapp, test_policy_manager, real_builder -): +def test_input_devices_complex_policy(test_qapp, test_policy_manager, real_builder): sys_usb = test_qapp.domains["sys-usb"] sys_net = test_qapp.domains["sys-net"] test_policy_manager.policy_client.files[ @@ -117,8 +110,7 @@ def test_input_devices_complex_policy( assert widget.get_parent() assert ( - handler.widgets[("qubes.InputMouse", "sys-usb")].model.get_selected() - == "ask" + handler.widgets[("qubes.InputMouse", "sys-usb")].model.get_selected() == "ask" ) assert ( handler.widgets[("qubes.InputKeyboard", "sys-usb")].model.get_selected() @@ -129,8 +121,7 @@ def test_input_devices_complex_policy( == "allow" ) assert ( - handler.widgets[("qubes.InputMouse", "sys-net")].model.get_selected() - == "deny" + handler.widgets[("qubes.InputMouse", "sys-net")].model.get_selected() == "deny" ) assert ( handler.widgets[("qubes.InputKeyboard", "sys-net")].model.get_selected() @@ -171,9 +162,7 @@ def test_input_devices_complex_policy( ) -def test_input_devices_no_policy_one_usb( - test_qapp, test_policy_manager, real_builder -): +def test_input_devices_no_policy_one_usb(test_qapp, test_policy_manager, real_builder): sys_usb = test_qapp.domains["sys-usb"] handler = InputDeviceHandler( test_qapp, test_policy_manager, real_builder, {sys_usb} @@ -214,9 +203,7 @@ def test_input_devices_no_policy_one_usb( ) assert len(mock_save.mock_calls) == 1 _, rules, _ = mock_save.mock_calls[0].args - assert [str(rule) for rule in expected_rules] == [ - str(rule) for rule in rules - ] + assert [str(rule) for rule in expected_rules] == [str(rule) for rule in rules] def test_input_devices_faulty_policy_lines( @@ -296,9 +283,7 @@ def test_input_devices_faulty_policy_lines_2( ) assert len(mock_save.mock_calls) == 1 _, rules, _ = mock_save.mock_calls[0].args - assert [str(rule) for rule in expected_rules] == [ - str(rule) for rule in rules - ] + assert [str(rule) for rule in expected_rules] == [str(rule) for rule in rules] def test_input_devices_no_usbvm(test_qapp, test_policy_manager, real_builder): @@ -311,9 +296,7 @@ def test_input_devices_no_usbvm(test_qapp, test_policy_manager, real_builder): """ test_policy_manager.policy_client.file_tokens["50-config-input"] = "55" - handler = InputDeviceHandler( - test_qapp, test_policy_manager, real_builder, set() - ) + handler = InputDeviceHandler(test_qapp, test_policy_manager, real_builder, set()) for widget in handler.widgets.values(): assert not widget.get_parent() @@ -328,9 +311,7 @@ def test_input_devices_no_usbvm(test_qapp, test_policy_manager, real_builder): assert len(mock_save.mock_calls) == 0 -def test_input_devices_faulty_policy_err( - test_qapp, test_policy_manager, real_builder -): +def test_input_devices_faulty_policy_err(test_qapp, test_policy_manager, real_builder): test_policy_manager.policy_client.files[ "50-config-input" ] = """ @@ -355,9 +336,7 @@ def test_input_devices_faulty_policy_err( def test_u2f_handler_init(test_qapp, test_policy_manager, real_builder): sys_usb = test_qapp.domains["sys-usb"] - handler = U2FPolicyHandler( - test_qapp, test_policy_manager, real_builder, {sys_usb} - ) + handler = U2FPolicyHandler(test_qapp, test_policy_manager, real_builder, {sys_usb}) assert handler.get_unsaved() == "" @@ -372,12 +351,8 @@ def test_u2f_handler_init(test_qapp, test_policy_manager, real_builder): 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( - testred - ) - assert not handler.enable_some_handler.add_qube_model.is_vm_available( - sysusb - ) + 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) assert not handler.register_check.get_active() assert not handler.register_some_handler.selected_vms @@ -401,17 +376,13 @@ def test_u2f_handler_init_disable(test_qapp, test_policy_manager, real_builder): + b"\x00" ) - handler = U2FPolicyHandler( - test_qapp, test_policy_manager, real_builder, {sys_usb} - ) + handler = U2FPolicyHandler(test_qapp, test_policy_manager, real_builder, {sys_usb}) assert not handler.enable_check.get_active() assert not handler.problem_fatal_box.get_visible() -def test_u2f_handler_init_no_u2f_in_sysub( - test_qapp, test_policy_manager, real_builder -): +def test_u2f_handler_init_no_u2f_in_sysub(test_qapp, test_policy_manager, real_builder): sys_usb = test_qapp.domains["sys-usb"] test_qapp.expected_calls[ ( @@ -426,9 +397,7 @@ def test_u2f_handler_init_no_u2f_in_sysub( + b"\x00" ) - handler = U2FPolicyHandler( - test_qapp, test_policy_manager, real_builder, {sys_usb} - ) + handler = U2FPolicyHandler(test_qapp, test_policy_manager, real_builder, {sys_usb}) assert not handler.enable_check.get_sensitive() assert not handler.enable_check.get_active() @@ -436,9 +405,7 @@ def test_u2f_handler_init_no_u2f_in_sysub( def test_u2f_handler_no_usb_vm(test_qapp, test_policy_manager, real_builder): - handler = U2FPolicyHandler( - test_qapp, test_policy_manager, real_builder, set() - ) + handler = U2FPolicyHandler(test_qapp, test_policy_manager, real_builder, set()) assert not handler.enable_check.get_sensitive() assert not handler.enable_check.get_active() @@ -468,9 +435,7 @@ def test_u2f_handler_init_policy(test_qapp, test_policy_manager, real_builder): """ test_policy_manager.policy_client.file_tokens["50-config-u2f"] = "55" - handler = U2FPolicyHandler( - test_qapp, test_policy_manager, real_builder, {sys_usb} - ) + handler = U2FPolicyHandler(test_qapp, test_policy_manager, real_builder, {sys_usb}) assert handler.enable_check.get_active() assert handler.enable_some_handler.selected_vms == [fedora35, testvm] @@ -483,9 +448,7 @@ def test_u2f_handler_init_policy(test_qapp, test_policy_manager, real_builder): assert handler.blanket_handler.selected_vms == [testvm] -def test_u2f_handler_init_no_policy( - test_qapp, test_policy_manager, real_builder -): +def test_u2f_handler_init_no_policy(test_qapp, test_policy_manager, real_builder): sys_usb = test_qapp.domains["sys-usb"] # disable service test_qapp.expected_calls[ @@ -501,9 +464,7 @@ def test_u2f_handler_init_no_policy( + b"\x00" ) - handler = U2FPolicyHandler( - test_qapp, test_policy_manager, real_builder, {sys_usb} - ) + handler = U2FPolicyHandler(test_qapp, test_policy_manager, real_builder, {sys_usb}) assert handler.enable_check.get_sensitive() assert not handler.enable_check.get_active() @@ -515,9 +476,7 @@ def test_u2f_handler_init_no_policy( assert not handler.blanket_check.get_active() -def test_u2f_handler_init_policy_2( - test_qapp, test_policy_manager, real_builder -): +def test_u2f_handler_init_policy_2(test_qapp, test_policy_manager, real_builder): sys_usb = test_qapp.domains["sys-usb"] fedora35 = test_qapp.domains["fedora-35"] testvm = test_qapp.domains["test-vm"] @@ -538,9 +497,7 @@ def test_u2f_handler_init_policy_2( """ test_policy_manager.policy_client.file_tokens["50-config-u2f"] = "55" - handler = U2FPolicyHandler( - test_qapp, test_policy_manager, real_builder, {sys_usb} - ) + handler = U2FPolicyHandler(test_qapp, test_policy_manager, real_builder, {sys_usb}) assert handler.enable_check.get_active() assert handler.enable_some_handler.selected_vms == [fedora35, testvm] @@ -552,9 +509,7 @@ def test_u2f_handler_init_policy_2( assert not handler.problem_fatal_box.get_visible() -def test_u2f_handler_init_policy_mismatch( - test_qapp, test_policy_manager, real_builder -): +def test_u2f_handler_init_policy_mismatch(test_qapp, test_policy_manager, real_builder): sys_usb = test_qapp.domains["sys-usb"] fedora35 = test_qapp.domains["fedora-35"] testvm = test_qapp.domains["test-vm"] @@ -587,9 +542,7 @@ def test_u2f_handler_init_policy_mismatch( """ test_policy_manager.policy_client.file_tokens["50-config-u2f"] = "55" - handler = U2FPolicyHandler( - test_qapp, test_policy_manager, real_builder, {sys_usb} - ) + handler = U2FPolicyHandler(test_qapp, test_policy_manager, real_builder, {sys_usb}) assert handler.usb_qube_model.get_selected() == sys_usb assert handler.enable_check.get_active() @@ -647,9 +600,7 @@ def test_u2f_handler_2_usbvms(test_qapp, test_policy_manager, real_builder): assert not handler.error_handler.error_box.get_visible() -def test_u2f_handler_2_usbvms_switch( - test_qapp, test_policy_manager, real_builder -): +def test_u2f_handler_2_usbvms_switch(test_qapp, test_policy_manager, real_builder): sys_usb = test_qapp.domains["sys-usb"] test_standalone = test_qapp.domains["test-standalone"] testvm = test_qapp.domains["test-vm"] @@ -716,9 +667,7 @@ def test_u2f_handler_2_usbvms_switch( assert not handler.error_handler.error_box.get_visible() -def test_u2f_handler_2_usbvms_broken( - test_qapp, test_policy_manager, real_builder -): +def test_u2f_handler_2_usbvms_broken(test_qapp, test_policy_manager, real_builder): sys_usb = test_qapp.domains["sys-usb"] test_standalone = test_qapp.domains["test-standalone"] testvm = test_qapp.domains["test-vm"] @@ -761,9 +710,7 @@ def test_u2f_handler_2_usbvms_broken( def test_u2f_unsaved_reset(test_qapp, test_policy_manager, real_builder): sys_usb = test_qapp.domains["sys-usb"] - handler = U2FPolicyHandler( - test_qapp, test_policy_manager, real_builder, {sys_usb} - ) + handler = U2FPolicyHandler(test_qapp, test_policy_manager, real_builder, {sys_usb}) testvm = test_qapp.domains["test-vm"] fedora35 = test_qapp.domains["fedora-35"] @@ -812,9 +759,7 @@ def test_u2f_unsaved_reset(test_qapp, test_policy_manager, real_builder): def test_u2f_save_disable(test_qapp, test_policy_manager, real_builder): sys_usb = test_qapp.domains["sys-usb"] - handler = U2FPolicyHandler( - test_qapp, test_policy_manager, real_builder, {sys_usb} - ) + handler = U2FPolicyHandler(test_qapp, test_policy_manager, real_builder, {sys_usb}) handler.enable_check.set_active(False) @@ -837,16 +782,12 @@ def test_u2f_save_disable(test_qapp, test_policy_manager, real_builder): ) assert len(mock_save.mock_calls) == 1 _, rules, _ = mock_save.mock_calls[0].args - assert [str(rule) for rule in expected_rules] == [ - str(rule) for rule in rules - ] + assert [str(rule) for rule in expected_rules] == [str(rule) for rule in rules] def test_u2f_save_service(test_qapp, test_policy_manager, real_builder): sys_usb = test_qapp.domains["sys-usb"] - handler = U2FPolicyHandler( - test_qapp, test_policy_manager, real_builder, {sys_usb} - ) + handler = U2FPolicyHandler(test_qapp, test_policy_manager, real_builder, {sys_usb}) fedora35 = test_qapp.domains["fedora-35"] assert handler.enable_check.get_active() @@ -870,9 +811,7 @@ def test_u2f_save_service(test_qapp, test_policy_manager, real_builder): ) assert len(mock_save.mock_calls) == 1 _, rules, _ = mock_save.mock_calls[0].args - assert [str(rule) for rule in expected_rules] == [ - str(rule) for rule in rules - ] + assert [str(rule) for rule in expected_rules] == [str(rule) for rule in rules] def test_u2f_handler_save_complex(test_qapp, test_policy_manager, real_builder): @@ -892,9 +831,7 @@ def test_u2f_handler_save_complex(test_qapp, test_policy_manager, real_builder): + b"\x00" ) - handler = U2FPolicyHandler( - test_qapp, test_policy_manager, real_builder, {sys_usb} - ) + handler = U2FPolicyHandler(test_qapp, test_policy_manager, real_builder, {sys_usb}) assert not handler.enable_check.get_active() @@ -932,14 +869,10 @@ def test_u2f_handler_save_complex(test_qapp, test_policy_manager, real_builder): ) assert len(mock_save.mock_calls) == 1 _, rules, _ = mock_save.mock_calls[0].args - assert [str(rule) for rule in expected_rules] == [ - str(rule) for rule in rules - ] + assert [str(rule) for rule in expected_rules] == [str(rule) for rule in rules] -def test_u2f_handler_save_complex_2( - test_qapp, test_policy_manager, real_builder -): +def test_u2f_handler_save_complex_2(test_qapp, test_policy_manager, real_builder): sys_usb = test_qapp.domains["sys-usb"] testvm = test_qapp.domains["test-vm"] fedora35 = test_qapp.domains["fedora-35"] @@ -956,9 +889,7 @@ def test_u2f_handler_save_complex_2( + b"\x00" ) - handler = U2FPolicyHandler( - test_qapp, test_policy_manager, real_builder, {sys_usb} - ) + handler = U2FPolicyHandler(test_qapp, test_policy_manager, real_builder, {sys_usb}) assert not handler.enable_check.get_active() @@ -997,20 +928,14 @@ def test_u2f_handler_save_complex_2( ) assert len(mock_save.mock_calls) == 1 _, rules, _ = mock_save.mock_calls[0].args - assert [str(rule) for rule in expected_rules] == [ - str(rule) for rule in rules - ] + assert [str(rule) for rule in expected_rules] == [str(rule) for rule in rules] -def test_u2f_handler_add_without_service( - 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"] testvm = test_qapp.domains["test-vm"] - handler = U2FPolicyHandler( - test_qapp, test_policy_manager, real_builder, {sys_usb} - ) + handler = U2FPolicyHandler(test_qapp, test_policy_manager, real_builder, {sys_usb}) assert handler.get_unsaved() == "" @@ -1023,24 +948,19 @@ def test_u2f_handler_add_without_service( assert not handler.register_some_handler.selected_vms assert handler.enable_some_handler.selected_vms == [testvm] - handler.register_some_handler.add_button.clicked() handler.register_some_handler.add_qube_model.select_value("fedora-35") # refuse - with patch( - "qubes_config.global_config.usb_devices.ask_question" - ) as mock_question: + with patch("qubes_config.global_config.usb_devices.ask_question") as mock_question: mock_question.return_value = Gtk.ResponseType.NO - handler.register_some_handler.add_confirm.clicked() + handler.register_some_handler.add_button.clicked() assert mock_question.mock_calls assert not handler.register_some_handler.selected_vms assert handler.enable_some_handler.selected_vms == [testvm] # accept - with patch( - "qubes_config.global_config.usb_devices.ask_question" - ) as mock_question: + with patch("qubes_config.global_config.usb_devices.ask_question") as mock_question: mock_question.return_value = Gtk.ResponseType.YES - handler.register_some_handler.add_confirm.clicked() + handler.register_some_handler.add_button.clicked() assert mock_question.mock_calls assert handler.register_some_handler.selected_vms == [fedora35] @@ -1055,9 +975,7 @@ def test_devices_handler_unsaved(test_qapp, test_policy_manager, real_builder): b"backend_domain='dom0' mode='required' " b"_no-strict-reset='yes'\n" ) - test_qapp.expected_calls[ - ("dom0", "admin.vm.device.pci.Available", None, None) - ] = ( + test_qapp.expected_calls[("dom0", "admin.vm.device.pci.Available", None, None)] = ( b"0\x0000_0d.0 device_id='0000:0000::p0c0300' port_id='00_0d.0' " b"devclass='pci' backend_domain='dom0' interfaces='p0c0300' " b"_function='0' _bus='00' _libvirt_name='pci_0000_00_0d_0' " @@ -1069,9 +987,7 @@ def test_devices_handler_unsaved(test_qapp, test_policy_manager, real_builder): assert handler.get_unsaved() == "" # some changes - kb_widget = handler.input_handler.widgets[ - ("qubes.InputKeyboard", "sys-usb") - ] + kb_widget = handler.input_handler.widgets[("qubes.InputKeyboard", "sys-usb")] assert kb_widget.model.get_selected() == "deny" kb_widget.model.select_value("ask") @@ -1082,9 +998,7 @@ def test_devices_handler_unsaved(test_qapp, test_policy_manager, real_builder): assert "U2F disabled" in handler.get_unsaved() -def test_devices_handler_detect_usbvms( - test_qapp, test_policy_manager, real_builder -): +def test_devices_handler_detect_usbvms(test_qapp, test_policy_manager, real_builder): test_qapp.expected_calls[ ("sys-usb", "admin.vm.device.pci.Attached", None, None) ] = ( @@ -1099,9 +1013,7 @@ def test_devices_handler_detect_usbvms( b"backend_domain='dom0' mode='required' " b"_no-strict-reset='yes'\n" ) - test_qapp.expected_calls[ - ("dom0", "admin.vm.device.pci.Available", None, None) - ] = ( + test_qapp.expected_calls[("dom0", "admin.vm.device.pci.Available", None, None)] = ( b"0\x0000_0f.0 device_id='0000:0000::p0c0300' port_id='00_0f.0' " b"devclass='pci' backend_domain='dom0' interfaces='p0c0300' " b"_function='0' _bus='00' _libvirt_name='pci_0000_00_0f_0' " @@ -1120,9 +1032,7 @@ def test_devices_handler_detect_usbvms( assert handler.input_handler.usb_qubes == {sys_usb, test_standalone} -def test_devices_handler_save_reset( - test_qapp, test_policy_manager, real_builder -): +def test_devices_handler_save_reset(test_qapp, test_policy_manager, real_builder): handler = DevicesHandler(test_qapp, test_policy_manager, real_builder) # check all handlers have their save/reset called @@ -1144,9 +1054,7 @@ def test_devices_handler_save_reset( def test_devices_handler_no_sys_usb( test_qapp_simple, test_policy_manager, real_builder ): - handler = DevicesHandler( - test_qapp_simple, test_policy_manager, real_builder - ) + handler = DevicesHandler(test_qapp_simple, test_policy_manager, real_builder) assert not handler.input_handler.usb_qubes assert handler.u2f_handler.problem_fatal_box.get_visible() diff --git a/qubes_config/tests/test_utils.py b/qubes_config/tests/test_utils.py index 037f687a..da270844 100644 --- a/qubes_config/tests/test_utils.py +++ b/qubes_config/tests/test_utils.py @@ -72,9 +72,9 @@ def test_get_feature(test_qapp): apply_feature_change(vm, feature_name, "text") assert call in test_qapp.actual_calls - test_qapp.expected_calls[ - ("test-vm", "admin.vm.feature.List", None, None) - ] = b"0\x00test_feature" + test_qapp.expected_calls[("test-vm", "admin.vm.feature.List", None, None)] = ( + b"0\x00test_feature" + ) test_qapp.expected_calls[ ("test-vm", "admin.vm.feature.Remove", feature_name, None) ] = b"0\x001" @@ -134,9 +134,9 @@ def get_selected(self): ] = b"0\0" apply_feature_change_from_widget(MockWidget(True, "text"), vm, feature_name) - test_qapp.expected_calls[ - ("test-vm", "admin.vm.feature.List", None, None) - ] = b"0\x00other-feature" + test_qapp.expected_calls[("test-vm", "admin.vm.feature.List", None, None)] = ( + b"0\x00other-feature" + ) test_qapp.expected_calls[ ("test-vm", "admin.vm.feature.Remove", feature_name, None) ] = b"0\x001" diff --git a/qubes_config/tests/test_vm_flowbox.py b/qubes_config/tests/test_vm_flowbox.py index fca58883..aa07e8f3 100644 --- a/qubes_config/tests/test_vm_flowbox.py +++ b/qubes_config/tests/test_vm_flowbox.py @@ -53,11 +53,8 @@ def test_simple_flowbox_init_empty(test_qapp, test_builder): assert not flowbox_handler.is_changed() assert len(flowbox_handler.flowbox.get_children()) == 1 # only placeholder - assert isinstance( - flowbox_handler.flowbox.get_children()[0], PlaceholderText - ) + assert isinstance(flowbox_handler.flowbox.get_children()[0], PlaceholderText) assert flowbox_handler.flowbox.get_children()[0].get_visible() - assert not flowbox_handler.add_box.get_visible() def test_simple_flowbox_init_not_empty(test_qapp, test_builder): @@ -137,24 +134,10 @@ def test_flowbox_add_vm(test_qapp, test_builder): test_builder, test_qapp, "flowtest", initial_vms=initial_vms ) - # try to add a VM and abort - assert not flowbox_handler.add_box.get_visible() - flowbox_handler.add_button.clicked() - assert flowbox_handler.add_box.get_visible() - flowbox_handler.add_qube_model.select_value("test-blue") - flowbox_handler.add_cancel.clicked() - - assert not flowbox_handler.add_box.get_visible() - assert sorted(flowbox_handler.selected_vms) == sorted(initial_vms) - assert sorted(get_visible_vms(flowbox_handler)) == sorted(initial_vms) - # now try to add and do not abort - flowbox_handler.add_button.clicked() - assert flowbox_handler.add_box.get_visible() flowbox_handler.add_qube_model.select_value("test-blue") - flowbox_handler.add_confirm.clicked() + flowbox_handler.add_button.clicked() - assert not flowbox_handler.add_box.get_visible() expected_vms = sorted( [test_qapp.domains["test-vm"], test_qapp.domains["test-blue"]] ) @@ -162,17 +145,12 @@ def test_flowbox_add_vm(test_qapp, test_builder): assert sorted(get_visible_vms(flowbox_handler)) == expected_vms # now try to add something that's already selected - flowbox_handler.add_button.clicked() - assert flowbox_handler.add_box.get_visible() flowbox_handler.add_qube_model.select_value("test-blue") - with patch( - "qubes_config.global_config.vm_flowbox.show_error" - ) as mock_error: + with patch("qubes_config.global_config.vm_flowbox.show_error") as mock_error: assert not mock_error.mock_calls - flowbox_handler.add_confirm.clicked() + flowbox_handler.add_button.clicked() assert mock_error.mock_calls - # the box should not have hidden, maybe user wants to change selection - assert flowbox_handler.add_box.get_visible() + expected_vms = sorted( [test_qapp.domains["test-vm"], test_qapp.domains["test-blue"]] ) @@ -281,21 +259,17 @@ def test_flowbox_verify(test_qapp, test_builder): ) # attempt to add and see an erorr - flowbox_handler.add_button.clicked() - assert flowbox_handler.add_box.get_visible() flowbox_handler.add_qube_model.select_value("test-blue") # vm will not be added, but the verification callback is responsible # for messaging (it can propose additional actions) - flowbox_handler.add_confirm.clicked() + flowbox_handler.add_button.clicked() assert flowbox_handler.selected_vms == [test_vm] assert get_visible_vms(flowbox_handler) == [test_vm] assert not flowbox_handler.is_changed() # but adding correct stuff still works - flowbox_handler.add_button.clicked() - assert flowbox_handler.add_box.get_visible() flowbox_handler.add_qube_model.select_value("test-red") - flowbox_handler.add_confirm.clicked() + flowbox_handler.add_button.clicked() assert flowbox_handler.selected_vms == [red_vm, test_vm] assert get_visible_vms(flowbox_handler) == [red_vm, test_vm] assert flowbox_handler.is_changed() diff --git a/qubes_config/widgets/gtk_utils.py b/qubes_config/widgets/gtk_utils.py index 93175218..9dad5d07 100644 --- a/qubes_config/widgets/gtk_utils.py +++ b/qubes_config/widgets/gtk_utils.py @@ -275,3 +275,22 @@ def copy_to_global_clipboard(text: str): source.write("dom0") with open(XEVENT, "w", encoding="ascii") as timestamp: timestamp.write(str(Gtk.get_current_event_time())) + + +def resize_window_to_reasonable(window: Gtk.Window, min_width: int = 1100): + """Make sure the provided window is visible and does not exceed available screen + space.""" + if window.get_allocated_width() > window.get_screen().get_width(): + width = int(window.get_screen().get_width() * 0.9) + else: + # try to have at least min_width pixels + width = min(int(window.get_screen().get_width() * 0.9), min_width) + if window.get_allocated_height() > window.get_screen().get_height() * 0.9: + height = int(window.get_screen().get_height() * 0.9) + else: + height = window.get_allocated_height() + window.resize(width, height) + window.set_position(Gtk.WindowPosition.CENTER_ALWAYS) + window.set_gravity(Gdk.Gravity.CENTER) + window.move(0, 0) + window.set_position(Gtk.WindowPosition.CENTER) diff --git a/qubes_config/widgets/gtk_widgets.py b/qubes_config/widgets/gtk_widgets.py index 869f257d..9f7d8719 100644 --- a/qubes_config/widgets/gtk_widgets.py +++ b/qubes_config/widgets/gtk_widgets.py @@ -228,9 +228,7 @@ def __init__( self, combobox: Gtk.ComboBox, qapp: qubesadmin.Qubes, - filter_function: Optional[ - Callable[[qubesadmin.vm.QubesVM], bool] - ] = None, + filter_function: Optional[Callable[[qubesadmin.vm.QubesVM], bool]] = None, event_callback: Optional[Callable[[], None]] = None, default_value: Optional[Union[qubesadmin.vm.QubesVM, str]] = None, current_value: Optional[Union[qubesadmin.vm.QubesVM, str]] = None, @@ -304,6 +302,13 @@ def update_initial(self): if self.style_changes: self.entry_box.get_style_context().remove_class("combo-changed") + def clear_selection(self): + """ + Clear currently selected item. + """ + self.entry_box.set_text("") + self.combo.set_active_id(None) + def reset(self): """Reset changes.""" self.combo.set_active_id(self._initial_id) @@ -397,9 +402,7 @@ def _apply_model(self): assert isinstance(self.combo, Gtk.ComboBox) list_store = Gtk.ListStore(int, str, GdkPixbuf.Pixbuf, str, str, str) - for entry_no, display_name in zip( - itertools.count(), sorted(self._entries) - ): + for entry_no, display_name in zip(itertools.count(), sorted(self._entries)): entry = self._entries[display_name] list_store.append( [ @@ -466,10 +469,7 @@ def get_selected(self) -> Optional[qubesadmin.vm.QubesVM]: # special treatment for None: if self._entries[selected]["api_name"] == "None": return None - return ( - self._entries[selected]["vm"] - or self._entries[selected]["api_name"] - ) + return self._entries[selected]["vm"] or self._entries[selected]["api_name"] return None def select_value(self, vm_name): diff --git a/qubes_config/widgets/utils.py b/qubes_config/widgets/utils.py index be9d74b4..f413390e 100644 --- a/qubes_config/widgets/utils.py +++ b/qubes_config/widgets/utils.py @@ -76,10 +76,9 @@ def apply_feature_change( except qubesadmin.exc.QubesDaemonAccessError: # pylint: disable=raise-missing-from raise qubesadmin.exc.QubesException( - _( - "Failed to set {feature_name} due to insufficient " - "permissions" - ).format(feature_name=feature_name) + _("Failed to set {feature_name} due to insufficient permissions").format( + feature_name=feature_name + ) ) @@ -108,9 +107,7 @@ def __delitem__(self, key): super().__delitem__(key) -def compare_rule_lists( - rule_list_1: List[Rule], rule_list_2: List[Rule] -) -> bool: +def compare_rule_lists(rule_list_1: List[Rule], rule_list_2: List[Rule]) -> bool: """Check if two provided rule lists are the same. Return True if yes.""" if len(rule_list_1) != len(rule_list_2): return False diff --git a/qui/clipboard.py b/qui/clipboard.py index d04dff62..028607f6 100644 --- a/qui/clipboard.py +++ b/qui/clipboard.py @@ -73,13 +73,9 @@ PASTE_FEATURE = "gui-default-secure-paste-sequence" # Defining all messages in one place for easy modification -ERROR_MALFORMED_DATA = _( - "Malformed clipboard data received from qube: {vmname}" -) +ERROR_MALFORMED_DATA = _("Malformed clipboard data received from qube: {vmname}") ERROR_ON_COPY = _("Failed to fetch clipboard data from qube: {vmname}") -ERROR_ON_PASTE = _( - "Failed to paste global clipboard contents to qube: {vmname}" -) +ERROR_ON_PASTE = _("Failed to paste global clipboard contents to qube: {vmname}") ERROR_OVERSIZED_DATA = _( "Global clipboard size exceeded.\n" "qube: {vmname} attempted to send {size} bytes to global clipboard." @@ -276,9 +272,7 @@ def clipboard_formatted_size(size: int = None) -> str: formatted_bytes = str(file_size) + _(" bytes") if file_size > 0: - magnitude = min( - int(math.log(file_size) / math.log(2) * 0.1), len(units) - 1 - ) + magnitude = min(int(math.log(file_size) / math.log(2) * 0.1), len(units) - 1) if magnitude > 0: # pylint: disable=consider-using-f-string return "%s (%.1f %s)" % ( @@ -330,9 +324,7 @@ def __init__(self, wm, qapp, dispatcher, **properties): self.setup_watcher() for feature in [COPY_FEATURE, PASTE_FEATURE]: - self.dispatcher.add_handler( - f"domain-feature-set:{feature}", self.setup_ui - ) + self.dispatcher.add_handler(f"domain-feature-set:{feature}", self.setup_ui) self.dispatcher.add_handler( f"domain-feature-delete:{feature}", self.setup_ui ) @@ -355,21 +347,17 @@ def show_menu(self, _unused, event): Gtk.get_current_event_time(), ) # activate_time - def update_clipboard_contents( - self, vm=None, size=0, message=None, icon=None - ): + def update_clipboard_contents(self, vm=None, size=0, message=None, icon=None): if not vm or not size: - self.clipboard_label.set_markup( - _("Global clipboard is empty") - ) + self.clipboard_label.set_markup(_("Global clipboard is empty")) self.icon.set_from_icon_name("qui-clipboard") # todo the icon should be empty and full depending on state else: self.clipboard_label.set_markup( - _( - "Global clipboard contents: {0} from {1}" - ).format(size, vm) + _("Global clipboard contents: {0} from {1}").format( + size, vm + ) ) self.icon.set_from_icon_name("qui-clipboard") @@ -404,10 +392,9 @@ def setup_ui(self, *_args, **_kwargs): help_label = Gtk.Label(xalign=0) help_label.set_markup( - _( - "Use {copy} to copy and " - "{paste} to paste." - ).format(copy=self.copy_shortcut, paste=self.paste_shortcut) + _("Use {copy} to copy and {paste} to paste.").format( + copy=self.copy_shortcut, paste=self.paste_shortcut + ) ) help_item = Gtk.MenuItem() help_item.set_margin_left(10) @@ -426,9 +413,7 @@ def copy_dom0_clipboard(self, *_args, **_kwargs): text = clipboard.wait_for_text() if not text: - self.send_notify( - _("Dom0 clipboard is empty!"), icon="dialog-information" - ) + self.send_notify(_("Dom0 clipboard is empty!"), icon="dialog-information") return try: diff --git a/qui/decorators.py b/qui/decorators.py index 06cb4163..27222ed9 100644 --- a/qui/decorators.py +++ b/qui/decorators.py @@ -110,34 +110,24 @@ def update_tooltip(self, netvm_changed=False, storage_changed=False): if not self.template_name: self.template_name = getattr(self.vm, "template", None) self.template_name = ( - _("None") - if not self.template_name - else str(self.template_name) + _("None") if not self.template_name else str(self.template_name) ) if not self.netvm_name or netvm_changed: - self.netvm_name = getattr( - self.vm, "netvm", _("permission denied") - ) + self.netvm_name = getattr(self.vm, "netvm", _("permission denied")) self.netvm_name = ( - _("None") - if not self.netvm_name - else str(self.netvm_name) + _("None") if not self.netvm_name else str(self.netvm_name) ) if not self.cur_storage or storage_changed: try: - self.cur_storage = ( - self.vm.get_disk_utilization() / 1024**3 - ) + self.cur_storage = self.vm.get_disk_utilization() / 1024**3 except (exc.QubesDaemonNoResponseError, KeyError): self.cur_storage = 0 if not self.max_storage or storage_changed: try: - self.max_storage = ( - self.vm.volumes["private"].size / 1024**3 - ) + self.max_storage = self.vm.volumes["private"].size / 1024**3 except (exc.QubesDaemonNoResponseError, KeyError): self.max_storage = 0 @@ -160,9 +150,7 @@ def update_tooltip(self, netvm_changed=False, storage_changed=False): ) if self.outdated: - tooltip += _( - "\n\nRestart qube to apply changes in template." - ) + tooltip += _("\n\nRestart qube to apply changes in template.") if self.updates_available: tooltip += _("\n\nUpdates available.") diff --git a/qui/devices/actionable_widgets.py b/qui/devices/actionable_widgets.py index 812337b8..853a5ae6 100644 --- a/qui/devices/actionable_widgets.py +++ b/qui/devices/actionable_widgets.py @@ -68,9 +68,7 @@ def load_icon(icon_name: str, backup_name: str, size: int = 24): + icon_name + ".svg" ) - return GdkPixbuf.Pixbuf.new_from_file_at_size( - icon_path, size, size - ) + return GdkPixbuf.Pixbuf.new_from_file_at_size(icon_path, size, size) except (GLib.Error, TypeError): # we are giving up and just using a blank icon pixbuf: GdkPixbuf.Pixbuf = GdkPixbuf.Pixbuf.new( @@ -166,9 +164,7 @@ def __init__(self, device: backend.Device, variant: str = "dark"): backend_vm = device.backend_domain frontend_vms = list(device.attachments) # backend is always there - backend_vm_icon = VMWithIcon( - backend_vm, name_extension=device.id_string - ) + backend_vm_icon = VMWithIcon(backend_vm, name_extension=device.id_string) backend_vm_icon.get_style_context().add_class("main_device_vm") self.pack_start(backend_vm_icon, False, False, 4) @@ -246,12 +242,8 @@ def widget_action(self, *_args): class DetachWidget(ActionableWidget, SimpleActionWidget): """Detach device from a VM""" - def __init__( - self, vm: backend.VM, device: backend.Device, variant: str = "dark" - ): - super().__init__( - "detach", "Detach from " + vm.name + "", variant - ) + def __init__(self, vm: backend.VM, device: backend.Device, variant: str = "dark"): + super().__init__("detach", "Detach from " + vm.name + "", variant) self.vm = vm self.device = device @@ -262,9 +254,7 @@ def widget_action(self, *_args): class DetachAndShutdownWidget(ActionableWidget, SimpleActionWidget): """Detach device from a disposable VM and shut it down.""" - def __init__( - self, vm: backend.VM, device: backend.Device, variant: str = "dark" - ): + def __init__(self, vm: backend.VM, device: backend.Device, variant: str = "dark"): super().__init__( "detach", "Detach and shut down " + vm.name + "", variant ) @@ -279,9 +269,7 @@ def widget_action(self, *_args): class DetachAndAttachWidget(ActionableWidget, VMWithIcon): """Detach device from current attachment(s) and attach to another""" - def __init__( - self, vm: backend.VM, device: backend.Device, variant: str = "dark" - ): + def __init__(self, vm: backend.VM, device: backend.Device, variant: str = "dark"): super().__init__(vm, variant=variant) self.vm = vm self.device = device @@ -295,17 +283,13 @@ def widget_action(self, *_args): class AttachDisposableWidget(ActionableWidget, VMWithIcon): """Attach to a new disposable qube""" - def __init__( - self, vm: backend.VM, device: backend.Device, variant: str = "dark" - ): + def __init__(self, vm: backend.VM, device: backend.Device, variant: str = "dark"): super().__init__(vm, variant=variant) self.vm = vm self.device = device def widget_action(self, *_args): - new_dispvm = qubesadmin.vm.DispVM.from_appvm( - self.vm.vm_object.app, self.vm - ) + new_dispvm = qubesadmin.vm.DispVM.from_appvm(self.vm.vm_object.app, self.vm) new_dispvm.start() self.device.attach_to_vm(backend.VM(new_dispvm)) @@ -314,18 +298,14 @@ def widget_action(self, *_args): class DetachAndAttachDisposableWidget(ActionableWidget, VMWithIcon): """Detach from all current attachments and attach to new disposable""" - def __init__( - self, vm: backend.VM, device: backend.Device, variant: str = "dark" - ): + def __init__(self, vm: backend.VM, device: backend.Device, variant: str = "dark"): super().__init__(vm, variant=variant) self.vm = vm self.device = device def widget_action(self, *_args): self.device.detach_from_vm(self.vm) - new_dispvm = qubesadmin.vm.DispVM.from_appvm( - self.vm.vm_object.app, self.vm - ) + new_dispvm = qubesadmin.vm.DispVM.from_appvm(self.vm.vm_object.app, self.vm) new_dispvm.start() self.device.attach_to_vm(backend.VM(new_dispvm)) @@ -466,9 +446,7 @@ def __init__(self, device: backend.Device, variant: str = "dark"): self.vm_diagram = VMAttachmentDiagram(device, self.variant) self.attach(self.vm_diagram, 1, 1, 3, 1) - def get_child_widgets( - self, vms, disp_vm_templates - ) -> Iterable[ActionableWidget]: + def get_child_widgets(self, vms, disp_vm_templates) -> Iterable[ActionableWidget]: """ Get type-appropriate list of child widgets. :return: iterable of ActionableWidgets, ready to be packed in somewhere @@ -496,9 +474,7 @@ def get_child_widgets( yield InfoHeader("Detach and attach to new disposable qube:") for vm in disp_vm_templates: - yield DetachAndAttachDisposableWidget( - vm, self.device, self.variant - ) + yield DetachAndAttachDisposableWidget(vm, self.device, self.variant) else: yield InfoHeader("Attach to qube:") diff --git a/qui/devices/backend.py b/qui/devices/backend.py index 00f45d43..27ad9eaf 100644 --- a/qui/devices/backend.py +++ b/qui/devices/backend.py @@ -98,9 +98,7 @@ def should_be_cleaned_up(self): class Device: - def __init__( - self, dev: qubesadmin.devices.DeviceInfo, gtk_app: Gtk.Application - ): + def __init__(self, dev: qubesadmin.devices.DeviceInfo, gtk_app: Gtk.Application): self.gtk_app: Gtk.Application = gtk_app self._dev: qubesadmin.devices.DeviceInfo = dev self.__hash = hash(dev) @@ -256,9 +254,9 @@ def attach_to_vm(self, vm: VM): except Exception as ex: # pylint: disable=broad-except self.gtk_app.emit_notification( _("Error"), - _( - "Attaching device {0} to {1} failed. Error: {2} - {3}" - ).format(self.description, vm, type(ex).__name__, ex), + _("Attaching device {0} to {1} failed. Error: {2} - {3}").format( + self.description, vm, type(ex).__name__, ex + ), Gio.NotificationPriority.HIGH, error=True, notification_id=self.notification_id, diff --git a/qui/devices/device_widget.py b/qui/devices/device_widget.py index d325c3a6..f3e65b6e 100644 --- a/qui/devices/device_widget.py +++ b/qui/devices/device_widget.py @@ -152,9 +152,7 @@ def device_list_update(self, vm, _event, **_kwargs): try: for devclass in DEV_TYPES: for device in vm.devices[devclass]: - changed_devices[str(device.port)] = backend.Device( - device, self - ) + changed_devices[str(device.port)] = backend.Device(device, self) except qubesadmin.exc.QubesException: changed_devices = {} # VM was removed @@ -204,9 +202,7 @@ def initialize_dev_data(self): for devclass in DEV_TYPES: try: for device in domain.devices[devclass]: - self.devices[str(device.port)] = backend.Device( - device, self - ) + self.devices[str(device.port)] = backend.Device(device, self) except qubesadmin.exc.QubesException: # we have no permission to access VM's devices continue @@ -215,17 +211,13 @@ def initialize_dev_data(self): for domain in self.qapp.domains: for devclass in DEV_TYPES: try: - for device in domain.devices[ - devclass - ].get_attached_devices(): + for device in domain.devices[devclass].get_attached_devices(): dev = str(device.port) if dev in self.devices: # occassionally ghost UnknownDevices appear when a # device was removed but not detached from a VM # FUTURE: is this still true after api changes? - self.devices[dev].attachments.add( - backend.VM(domain) - ) + self.devices[dev].attachments.add(backend.VM(domain)) except qubesadmin.exc.QubesException: # we have no permission to access VM's devices continue @@ -321,9 +313,7 @@ def load_css(widget) -> str: theme = "light" if is_theme_light(widget) else "dark" screen = Gdk.Screen.get_default() provider = Gtk.CssProvider() - css_file_ref = ( - importlib.resources.files("qui") / f"qubes-devices-{theme}.css" - ) + css_file_ref = importlib.resources.files("qui") / f"qubes-devices-{theme}.css" with importlib.resources.as_file(css_file_ref) as css_file: provider.load_from_path(str(css_file)) @@ -344,9 +334,7 @@ def show_menu(self, _unused, _event): menu_items = [] sorted_vms = sorted(self.vms) sorted_dispvms = sorted(self.dispvm_templates) - sorted_devices = sorted( - self.devices.values(), key=lambda x: x.sorting_key - ) + sorted_devices = sorted(self.devices.values(), key=lambda x: x.sorting_key) for i, dev in enumerate(sorted_devices): if i == 0 or dev.device_group != sorted_devices[i - 1].device_group: diff --git a/qui/styles/qubes-colors-dark.css b/qui/styles/qubes-colors-dark.css index 266977b3..384e477e 100644 --- a/qui/styles/qubes-colors-dark.css +++ b/qui/styles/qubes-colors-dark.css @@ -16,6 +16,7 @@ @define-color separator-color #262626; @define-color soft-text-color #e7e7e7; @define-color misc-text-color #f2f2f2; +@define-color alt-row #111827; @define-color problem-background #991b1b; @define-color problem-frame #dc2626; diff --git a/qui/styles/qubes-colors-light.css b/qui/styles/qubes-colors-light.css index d6a870f2..e0685516 100644 --- a/qui/styles/qubes-colors-light.css +++ b/qui/styles/qubes-colors-light.css @@ -16,6 +16,7 @@ @define-color separator-color #cdcdcd; @define-color soft-text-color #858585; @define-color misc-text-color #979797; +@define-color alt-row #e5e7eb; @define-color problem-background #fecaca; @define-color problem-frame #b91c1c; diff --git a/qui/styles/qubes-widgets-base.css b/qui/styles/qubes-widgets-base.css index e45a2861..c9558008 100644 --- a/qui/styles/qubes-widgets-base.css +++ b/qui/styles/qubes-widgets-base.css @@ -121,6 +121,11 @@ radiobutton radio { color: white; } +.button_save:disabled { + background: @medium-gray; + color: black; +} + .button_cancel { background: @button-color; color: @dark-text-color; diff --git a/qui/tests/tests_domains.py b/qui/tests/tests_domains.py index 414adad8..726810a6 100644 --- a/qui/tests/tests_domains.py +++ b/qui/tests/tests_domains.py @@ -48,9 +48,7 @@ def test_01_correct_vm_state(self): for menu_item in self.widget.tray_menu: domain = self.qapp.domains[menu_item.vm["name"]] domains_in_widget.append(domain) - self.assertTrue( - domain.is_running(), "halted domain listed incorrectly" - ) + self.assertTrue(domain.is_running(), "halted domain listed incorrectly") for domain in self.qapp.domains: if domain.klass != "AdminVM": self.assertEqual( diff --git a/qui/tools/qubes_device_agent.py b/qui/tools/qubes_device_agent.py index bcd0bf41..a6f6ac33 100644 --- a/qui/tools/qubes_device_agent.py +++ b/qui/tools/qubes_device_agent.py @@ -99,9 +99,7 @@ def apply_icon(self, entry, qube_name): f"The following source qube does not exist: {qube_name}" ) else: - raise TypeError( - "Only expecting Gtk.Entry objects to want our icon." - ) + raise TypeError("Only expecting Gtk.Entry objects to want our icon.") class AttachmentConfirmationWindow(RPCConfirmationWindow): @@ -144,27 +142,17 @@ def __init__( self._gtk_builder = Gtk.Builder() with importlib.resources.as_file(self._source_file_ref) as path: self._gtk_builder.add_from_file(str(path)) - self._rpc_window = self._gtk_builder.get_object( - self._source_id["window"] - ) - self._rpc_ok_button = self._gtk_builder.get_object( - self._source_id["ok"] - ) + self._rpc_window = self._gtk_builder.get_object(self._source_id["window"]) + self._rpc_ok_button = self._gtk_builder.get_object(self._source_id["ok"]) self._rpc_cancel_button = self._gtk_builder.get_object( self._source_id["cancel"] ) self._device_label = self._gtk_builder.get_object( self._source_id["device_label"] ) - self._source_entry = self._gtk_builder.get_object( - self._source_id["source"] - ) - self._rpc_combo_box = self._gtk_builder.get_object( - self._source_id["target"] - ) - self._error_bar = self._gtk_builder.get_object( - self._source_id["error_bar"] - ) + self._source_entry = self._gtk_builder.get_object(self._source_id["source"]) + self._rpc_combo_box = self._gtk_builder.get_object(self._source_id["target"]) + self._error_bar = self._gtk_builder.get_object(self._source_id["error_bar"]) self._error_message = self._gtk_builder.get_object( self._source_id["error_message"] ) diff --git a/qui/tray/disk_space.py b/qui/tray/disk_space.py index 3d8f87f8..4e8464aa 100644 --- a/qui/tray/disk_space.py +++ b/qui/tray/disk_space.py @@ -125,9 +125,7 @@ def __init__(self, vm): self.set_label(_("Do not show notifications about this qube")) try: - self.set_active( - self.vm.features.get("disk-space-not-notify", False) - ) + self.set_active(self.vm.features.get("disk-space-not-notify", False)) except exc.QubesDaemonCommunicationError: self.set_active(False) self.set_sensitive(False) @@ -344,9 +342,7 @@ def emit_notification(gtk_app, title, text, vm=None): notification.set_icon(Gio.ThemedIcon.new("dialog-warning")) if vm: - notification.add_button( - _("Open qube settings"), f"app.prefs::{vm.name}" - ) + notification.add_button(_("Open qube settings"), f"app.prefs::{vm.name}") gtk_app.send_notification(None, notification) @@ -393,8 +389,7 @@ def refresh_icon(self): emit_notification( self, _("Disk usage warning!"), - _("You are running out of disk space.") - + "".join(pool_warning), + _("You are running out of disk space.") + "".join(pool_warning), ) self.pool_warned = True else: @@ -414,9 +409,7 @@ def refresh_icon(self): emit_notification( self, _("Qube usage warning"), - _("Qube {} is running out of storage space.").format( - vm.name - ), + _("Qube {} is running out of storage space.").format(vm.name), vm=vm, ) self.vms_warned.add(vm) diff --git a/qui/tray/domains.py b/qui/tray/domains.py index 97b19c11..e9d2b696 100644 --- a/qui/tray/domains.py +++ b/qui/tray/domains.py @@ -78,18 +78,14 @@ def get_icon(self, icon_name): ) self.icons[icon_name] = icon except (TypeError, GLib.Error): - icon = GdkPixbuf.Pixbuf.new( - GdkPixbuf.Colorspace.RGB, True, 8, 16, 16 - ) + icon = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True, 8, 16, 16) icon.fill(0) self.icons[icon_name] = icon return icon def show_error(title, text): - dialog = Gtk.MessageDialog( - None, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK - ) + dialog = Gtk.MessageDialog(None, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK) dialog.set_title(title) dialog.set_markup(text) dialog.connect("response", lambda *x: dialog.destroy()) @@ -254,9 +250,7 @@ class KillItem(VMActionMenuItem): """Kill domain menu Item. When activated kills the domain.""" def __init__(self, vm, icon_cache): - super().__init__( - vm, label=_("Kill"), icon_cache=icon_cache, icon_name="kill" - ) + super().__init__(vm, label=_("Kill"), icon_cache=icon_cache, icon_name="kill") async def perform_action(self, *_args, **_kwargs): try: @@ -333,9 +327,7 @@ async def perform_action(self): if self.as_root: service_args["user"] = "root" try: - self.vm.run_service( - "qubes.StartApp+qubes-run-terminal", **service_args - ) + self.vm.run_service("qubes.StartApp+qubes-run-terminal", **service_args) except exc.QubesException as ex: show_error( _("Error starting terminal"), @@ -423,9 +415,7 @@ def __init__(self, vm, app, icon_cache): self.app = app self.add(OpenFileManagerItem(self.vm, icon_cache)) - self.add( - RunTerminalItem(self.vm, icon_cache, as_root=app.terminal_as_root) - ) + self.add(RunTerminalItem(self.vm, icon_cache, as_root=app.terminal_as_root)) # Debug console for developers, troubleshooting, headless qubes self.debug_console = RunDebugConsoleItem(self.vm, icon_cache) @@ -550,9 +540,7 @@ def __init__(self): hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) # Icon box with fixed width - iconbox = Gtk.Image.new_from_icon_name( - "qubes-logo-icon", Gtk.IconSize.MENU - ) + iconbox = Gtk.Image.new_from_icon_name("qubes-logo-icon", Gtk.IconSize.MENU) hbox.pack_start(iconbox, False, True, 6) # Name box @@ -761,34 +749,22 @@ def register_events(self): self.dispatcher.add_handler("connection-established", self.refresh_all) self.dispatcher.add_handler("domain-pre-start", self.update_domain_item) self.dispatcher.add_handler("domain-start", self.update_domain_item) - self.dispatcher.add_handler( - "domain-start-failed", self.update_domain_item - ) + self.dispatcher.add_handler("domain-start-failed", self.update_domain_item) self.dispatcher.add_handler("domain-paused", self.update_domain_item) self.dispatcher.add_handler("domain-unpaused", self.update_domain_item) self.dispatcher.add_handler("domain-shutdown", self.update_domain_item) - self.dispatcher.add_handler( - "domain-pre-shutdown", self.update_domain_item - ) - self.dispatcher.add_handler( - "domain-shutdown-failed", self.update_domain_item - ) + self.dispatcher.add_handler("domain-pre-shutdown", self.update_domain_item) + self.dispatcher.add_handler("domain-shutdown-failed", self.update_domain_item) self.dispatcher.add_handler("domain-add", self.add_domain_item) self.dispatcher.add_handler("domain-delete", self.remove_domain_item) self.dispatcher.add_handler("domain-pre-start", self.emit_notification) self.dispatcher.add_handler("domain-start", self.emit_notification) - self.dispatcher.add_handler( - "domain-start-failed", self.emit_notification - ) - self.dispatcher.add_handler( - "domain-pre-shutdown", self.emit_notification - ) + self.dispatcher.add_handler("domain-start-failed", self.emit_notification) + self.dispatcher.add_handler("domain-pre-shutdown", self.emit_notification) self.dispatcher.add_handler("domain-shutdown", self.emit_notification) - self.dispatcher.add_handler( - "domain-shutdown-failed", self.emit_notification - ) + self.dispatcher.add_handler("domain-shutdown-failed", self.emit_notification) self.dispatcher.add_handler("domain-start", self.check_pause_notify) self.dispatcher.add_handler("domain-paused", self.check_pause_notify) @@ -807,12 +783,8 @@ def register_events(self): self.dispatcher.add_handler("property-set:debug", self.debug_change) self.dispatcher.add_handler("property-set:guivm", self.debug_change) self.dispatcher.add_handler("domain-feature-set:gui", self.debug_change) - self.dispatcher.add_handler( - "domain-feature-delete:gui", self.debug_change - ) - self.dispatcher.add_handler( - "domain-feature-set:expert-mode", self.debug_change - ) + self.dispatcher.add_handler("domain-feature-delete:gui", self.debug_change) + self.dispatcher.add_handler("domain-feature-set:expert-mode", self.debug_change) self.dispatcher.add_handler( "domain-feature-delete:expert-mode", self.debug_change ) @@ -828,9 +800,9 @@ def register_events(self): def debug_change(self, vm, *_args, **_kwargs): if vm == self.qapp.local_name: - self.expert_mode = self.qapp.domains[ - self.qapp.local_name - ].features.get("expert-mode", False) + self.expert_mode = self.qapp.domains[self.qapp.local_name].features.get( + "expert-mode", False + ) vms = self.menu_items else: vms = {vm} @@ -844,16 +816,12 @@ def show_menu(self, _unused, event): self.tray_menu.popup_at_pointer(event) # None means current event def emit_notification(self, vm, event, **kwargs): - notification = Gio.Notification.new( - _("Qube Status: {}").format(vm.name) - ) + notification = Gio.Notification.new(_("Qube Status: {}").format(vm.name)) notification.set_priority(Gio.NotificationPriority.NORMAL) if event == "domain-start-failed": notification.set_body( - _("Qube {} has failed to start: {}").format( - vm.name, kwargs["reason"] - ) + _("Qube {} has failed to start: {}").format(vm.name, kwargs["reason"]) ) notification.set_priority(Gio.NotificationPriority.HIGH) notification.set_icon(Gio.ThemedIcon.new("dialog-warning")) @@ -869,9 +837,7 @@ def emit_notification(self, vm, event, **kwargs): notification.set_body(_("Qube {} has shut down.").format(vm.name)) elif event == "domain-shutdown-failed": notification.set_body( - _("Qube {} failed to shut down: {}").format( - vm.name, kwargs["reason"] - ) + _("Qube {} failed to shut down: {}").format(vm.name, kwargs["reason"]) ) notification.set_priority(Gio.NotificationPriority.HIGH) notification.set_icon(Gio.ThemedIcon.new("dialog-warning")) @@ -881,9 +847,7 @@ def emit_notification(self, vm, event, **kwargs): def emit_paused_notification(self): if not self.pause_notification_out: - notification = Gio.Notification.new( - _("Your qubes have been paused!") - ) + notification = Gio.Notification.new(_("Your qubes have been paused!")) notification.set_body( _( "All your qubes are currently paused. If this was an " @@ -1023,8 +987,7 @@ def handle_domain_shutdown(self, vm): template1 = getattr(menu_item.vm, "template", None) template2 = getattr(template1, "template", None) if vm in (template1, template2) and any( - vol.is_outdated() - for vol in menu_item.vm.volumes.values() + vol.is_outdated() for vol in menu_item.vm.volumes.values() ): menu_item.name.update_outdated(True) except exc.QubesVMNotFoundError: @@ -1075,23 +1038,17 @@ def update_domain_item(self, vm, event, **kwargs): def update_stats(self, vm, _event, **kwargs): if vm not in self.menu_items: return - self.menu_items[vm].update_stats( - kwargs["memory_kb"], kwargs["cpu_usage"] - ) + self.menu_items[vm].update_stats(kwargs["memory_kb"], kwargs["cpu_usage"]) def initialize_menu(self): self.tray_menu.add(DomainMenuItem(None, self, self.icon_cache)) # Add AdminVMS - for vm in sorted( - [vm for vm in self.qapp.domains if vm.klass == "AdminVM"] - ): + for vm in sorted([vm for vm in self.qapp.domains if vm.klass == "AdminVM"]): self.add_domain_item(None, None, vm) # and the rest of them - for vm in sorted( - [vm for vm in self.qapp.domains if vm.klass != "AdminVM"] - ): + for vm in sorted([vm for vm in self.qapp.domains if vm.klass != "AdminVM"]): self.add_domain_item(None, None, vm) for item in self.menu_items.values(): @@ -1128,26 +1085,14 @@ def run(self): # pylint: disable=arguments-differ self.initialize_menu() def _disconnect_signals(self, _event): - self.dispatcher.remove_handler( - "connection-established", self.refresh_all - ) - self.dispatcher.remove_handler( - "domain-pre-start", self.update_domain_item - ) + self.dispatcher.remove_handler("connection-established", self.refresh_all) + self.dispatcher.remove_handler("domain-pre-start", self.update_domain_item) self.dispatcher.remove_handler("domain-start", self.update_domain_item) - self.dispatcher.remove_handler( - "domain-start-failed", self.update_domain_item - ) + self.dispatcher.remove_handler("domain-start-failed", self.update_domain_item) self.dispatcher.remove_handler("domain-paused", self.update_domain_item) - self.dispatcher.remove_handler( - "domain-unpaused", self.update_domain_item - ) - self.dispatcher.remove_handler( - "domain-shutdown", self.update_domain_item - ) - self.dispatcher.remove_handler( - "domain-pre-shutdown", self.update_domain_item - ) + self.dispatcher.remove_handler("domain-unpaused", self.update_domain_item) + self.dispatcher.remove_handler("domain-shutdown", self.update_domain_item) + self.dispatcher.remove_handler("domain-pre-shutdown", self.update_domain_item) self.dispatcher.remove_handler( "domain-shutdown-failed", self.update_domain_item ) @@ -1155,31 +1100,17 @@ def _disconnect_signals(self, _event): self.dispatcher.remove_handler("domain-add", self.add_domain_item) self.dispatcher.remove_handler("domain-delete", self.remove_domain_item) - self.dispatcher.remove_handler( - "domain-pre-start", self.emit_notification - ) + self.dispatcher.remove_handler("domain-pre-start", self.emit_notification) self.dispatcher.remove_handler("domain-start", self.emit_notification) - self.dispatcher.remove_handler( - "domain-start-failed", self.emit_notification - ) - self.dispatcher.remove_handler( - "domain-pre-shutdown", self.emit_notification - ) - self.dispatcher.remove_handler( - "domain-shutdown", self.emit_notification - ) - self.dispatcher.remove_handler( - "domain-shutdown-failed", self.emit_notification - ) + self.dispatcher.remove_handler("domain-start-failed", self.emit_notification) + self.dispatcher.remove_handler("domain-pre-shutdown", self.emit_notification) + self.dispatcher.remove_handler("domain-shutdown", self.emit_notification) + self.dispatcher.remove_handler("domain-shutdown-failed", self.emit_notification) self.dispatcher.remove_handler("domain-start", self.check_pause_notify) self.dispatcher.remove_handler("domain-paused", self.check_pause_notify) - self.dispatcher.remove_handler( - "domain-unpaused", self.check_pause_notify - ) - self.dispatcher.remove_handler( - "domain-shutdown", self.check_pause_notify - ) + self.dispatcher.remove_handler("domain-unpaused", self.check_pause_notify) + self.dispatcher.remove_handler("domain-shutdown", self.check_pause_notify) self.dispatcher.remove_handler( "domain-feature-set:updates-available", self.feature_change @@ -1187,21 +1118,13 @@ def _disconnect_signals(self, _event): self.dispatcher.remove_handler( "domain-feature-delete:updates-available", self.feature_change ) - self.dispatcher.remove_handler( - "property-set:netvm", self.property_change - ) - self.dispatcher.remove_handler( - "property-set:label", self.property_change - ) + self.dispatcher.remove_handler("property-set:netvm", self.property_change) + self.dispatcher.remove_handler("property-set:label", self.property_change) self.dispatcher.remove_handler("property-set:debug", self.debug_change) self.dispatcher.remove_handler("property-set:guivm", self.debug_change) - self.dispatcher.remove_handler( - "domain-feature-set:gui", self.debug_change - ) - self.dispatcher.remove_handler( - "domain-feature-delete:gui", self.debug_change - ) + self.dispatcher.remove_handler("domain-feature-set:gui", self.debug_change) + self.dispatcher.remove_handler("domain-feature-delete:gui", self.debug_change) self.dispatcher.remove_handler( "domain-feature-set:expert-mode", self.debug_change ) @@ -1259,9 +1182,7 @@ def main(): stats_dispatcher = qubesadmin.events.EventsDispatcher( qapp, api_method="admin.vm.Stats" ) - app = DomainTray( - "org.qubes.qui.tray.Domains", qapp, dispatcher, stats_dispatcher - ) + app = DomainTray("org.qubes.qui.tray.Domains", qapp, dispatcher, stats_dispatcher) app.run() loop = asyncio.get_event_loop() @@ -1270,9 +1191,7 @@ def main(): asyncio.ensure_future(stats_dispatcher.listen_for_events()), ] - return qui.utils.run_asyncio_and_show_errors( - loop, tasks, "Qubes Domains Widget" - ) + return qui.utils.run_asyncio_and_show_errors(loop, tasks, "Qubes Domains Widget") if __name__ == "__main__": diff --git a/qui/tray/gtk3_xwayland_menu_dismisser.py b/qui/tray/gtk3_xwayland_menu_dismisser.py index c1b553a0..0f08fd2c 100644 --- a/qui/tray/gtk3_xwayland_menu_dismisser.py +++ b/qui/tray/gtk3_xwayland_menu_dismisser.py @@ -12,9 +12,7 @@ # Modifying the environment while multiple threads # are running leads to use-after-free in glibc, so # ensure that only one thread is running. -assert ( - len(os.listdir("/proc/self/task")) == 1 -), "multiple threads already running" +assert len(os.listdir("/proc/self/task")) == 1, "multiple threads already running" # Only the X11 backend is supported os.environ["GDK_BACKEND"] = "x11" @@ -144,9 +142,7 @@ def _hide(self, widget: Gtk.Widget, /) -> None: self._window.hide() # pylint: disable=line-too-long - def on_button_press( - self, window: Gtk.Window, _event: Gdk.EventButton, / - ) -> None: + def on_button_press(self, window: Gtk.Window, _event: Gdk.EventButton, /) -> None: # Hide the window and the widget. window.hide() self._widget.hide() diff --git a/qui/tray/updates.py b/qui/tray/updates.py index 3a578ee0..a8b5c4ba 100644 --- a/qui/tray/updates.py +++ b/qui/tray/updates.py @@ -108,8 +108,7 @@ def setup_menu(self): self.tray_menu.append( RunItem( _( - "Updates for {} qubes are available!\n" - "Launch updater" + "Updates for {} qubes are available!\nLaunch updater" ).format(len(self.vms_needing_update)), self.launch_updater, ) @@ -127,9 +126,7 @@ def setup_menu(self): + ", ".join([str(vm) for vm in self.obsolete_vms]) + _("\nInstall new templates with Template Manager") ) - self.tray_menu.append( - RunItem(obsolete_text, self.launch_template_manager) - ) + self.tray_menu.append(RunItem(obsolete_text, self.launch_template_manager)) self.tray_menu.show_all() @@ -177,9 +174,7 @@ def connect_events(self): ) self.dispatcher.add_handler("domain-add", self.domain_added) self.dispatcher.add_handler("domain-delete", self.domain_removed) - self.dispatcher.add_handler( - "domain-feature-set:os-eol", self.feature_change - ) + self.dispatcher.add_handler("domain-feature-set:os-eol", self.feature_change) def domain_added(self, _submitter, _event, vm, *_args, **_kwargs): try: diff --git a/qui/updater/intro_page.py b/qui/updater/intro_page.py index 21b780de..cb63a80a 100644 --- a/qui/updater/intro_page.py +++ b/qui/updater/intro_page.py @@ -67,9 +67,7 @@ def __init__(self, builder, log, next_button): self.vm_list.connect("query-tooltip", self.on_query_tooltip) self.list_store: Optional[ListWrapper] = None - checkbox_column: Gtk.TreeViewColumn = self.builder.get_object( - "checkbox_column" - ) + checkbox_column: Gtk.TreeViewColumn = self.builder.get_object("checkbox_column") checkbox_column.connect("clicked", self.on_header_toggled) header_button = checkbox_column.get_button() @@ -86,9 +84,7 @@ def __init__(self, builder, log, next_button): self.vm_list.connect("row-activated", self.on_checkbox_toggled) - self.info_how_it_works: Gtk.Label = self.builder.get_object( - "info_how_it_works" - ) + self.info_how_it_works: Gtk.Label = self.builder.get_object("info_how_it_works") self.info_how_it_works.set_label( self.info_how_it_works.get_label().format( MAYBE=f'' @@ -103,9 +99,7 @@ def __init__(self, builder, log, next_button): def populate_vm_list(self, qapp, settings): """Adds to list any updatable vms with update info.""" self.log.debug("Populate update list") - self.list_store = ListWrapper( - UpdateRowWrapper, self.vm_list.get_model() - ) + self.list_store = ListWrapper(UpdateRowWrapper, self.vm_list.get_model()) for vm in qapp.domains: if vm.klass == "AdminVM": @@ -168,9 +162,9 @@ def refresh_update_list(self, update_if_stale): if row.vm.name == "dom0": continue row.updates_available = bool(row.vm.name in to_update) - row.selected = bool( - row.vm.name in to_update - ) and not row.vm.features.get("prohibit-start", False) + row.selected = bool(row.vm.name in to_update) and not row.vm.features.get( + "prohibit-start", False + ) def get_vms_to_update(self) -> ListWrapper: """Returns list of vms selected to be updated""" @@ -215,9 +209,7 @@ def on_header_toggled(self, _emitter): If the user has selected any vms that do not match the defined states, the cycle will start from (1). """ - on_head_checkbox_toggled( - self.list_store, self.head_checkbox, self.select_rows - ) + on_head_checkbox_toggled(self.list_store, self.head_checkbox, self.select_rows) def select_rows(self): for row in self.list_store: @@ -277,9 +269,7 @@ def _get_stale_qubes(self, cmd): return { vm_name.strip() - for vm_name in output_lines[0] - .split(":", maxsplit=1)[1] - .split(",") + for vm_name in output_lines[0].split(":", maxsplit=1)[1].split(",") } except subprocess.CalledProcessError as err: if err.returncode != 100: @@ -296,10 +286,7 @@ def _handle_cli_dom0(dom0, to_update, cliargs): ) ) if ( - default_select - and cliargs.non_interactive - or cliargs.all - or cliargs.dom0 + default_select and cliargs.non_interactive or cliargs.all or cliargs.dom0 ) and ( cliargs.force_update or bool(dom0.features.get("updates-available", False)) @@ -317,16 +304,12 @@ def on_query_tooltip(self, widget, x, y, keyboard_tip, tooltip): """Show appropriate qube tooltip. Currently only for prohibit-start.""" if not widget.get_tooltip_context(x, y, keyboard_tip): return False - _, x, y, model, path, iterator = widget.get_tooltip_context( - x, y, keyboard_tip - ) + _, x, y, model, path, iterator = widget.get_tooltip_context(x, y, keyboard_tip) if path: status = model[iterator][4] if status == type(status).PROHIBITED: tooltip.set_text( - "Start prohibition rationale:\n{}".format( - str(model[iterator][9]) - ) + "Start prohibition rationale:\n{}".format(str(model[iterator][9])) ) widget.set_tooltip_cell(tooltip, path, None, None) return True @@ -420,9 +403,7 @@ def updates_available(self): @updates_available.setter def updates_available(self, value): prohibited = bool(self.vm.features.get("prohibit-start", False)) - updates_available = bool( - self.vm.features.get("updates-available", False) - ) + updates_available = bool(self.vm.features.get("updates-available", False)) supported = check_support(self.vm) if value and not updates_available: diff --git a/qui/updater/progress_page.py b/qui/updater/progress_page.py index 93089f39..c251dd43 100644 --- a/qui/updater/progress_page.py +++ b/qui/updater/progress_page.py @@ -62,19 +62,15 @@ def __init__( progress_store = self.progressbar.get_model() progress_store.append([0]) self.total_progress = progress_store[-1] - self.progressbar_renderer: Gtk.CellRendererProgress = ( - self.builder.get_object("progressbar_renderer") + self.progressbar_renderer: Gtk.CellRendererProgress = self.builder.get_object( + "progressbar_renderer" ) self.progressbar_renderer.set_fixed_size(-1, 26) - self.progress_list: Gtk.TreeView = self.builder.get_object( - "progress_list" - ) + self.progress_list: Gtk.TreeView = self.builder.get_object("progress_list") self.selection: Gtk.TreeSelection = self.progress_list.get_selection() self.progress_list.connect("row-activated", self.row_selected) - progress_column: Gtk.TreeViewColumn = self.builder.get_object( - "progress_column" - ) + progress_column: Gtk.TreeViewColumn = self.builder.get_object("progress_column") renderer = CellRendererProgressWithResult() renderer.props.ypad = 10 progress_column.pack_start(renderer, True) @@ -111,18 +107,12 @@ def interrupt_update(self): """ self.log.debug("Interrupting updates") self.exit_triggered = True - GLib.idle_add( - self.header_label.set_text, l("Interrupting the update...") - ) + GLib.idle_add(self.header_label.set_text, l("Interrupting the update...")) def perform_update(self, settings): """Updates dom0 and then other vms.""" - admins = [ - row for row in self.vms_to_update if row.vm.klass == "AdminVM" - ] - templs = [ - row for row in self.vms_to_update if row.vm.klass != "AdminVM" - ] + admins = [row for row in self.vms_to_update if row.vm.klass == "AdminVM"] + templs = [row for row in self.vms_to_update if row.vm.klass != "AdminVM"] GLib.idle_add(self.set_total_progress, 0) if admins: @@ -195,9 +185,7 @@ def qubes_dom0_update(*args): if returncode != 0: GLib.idle_add(admin.set_status, UpdateStatus.Error) else: - GLib.idle_add( - admin.set_status, UpdateStatus.NoUpdatesFound - ) + GLib.idle_add(admin.set_status, UpdateStatus.NoUpdatesFound) self.update_details.update_buffer() return @@ -311,9 +299,7 @@ def update_templates(self, to_update, settings): self.log.debug("Start templateVM updating") for row in to_update: - GLib.idle_add( - row.append_text_view, l("Updating {}\n").format(row.name) - ) + GLib.idle_add(row.append_text_view, l("Updating {}\n").format(row.name)) GLib.idle_add(row.set_status, UpdateStatus.InProgress) self.update_details.update_buffer() @@ -332,9 +318,7 @@ def update_templates(self, to_update, settings): GLib.idle_add(row.set_status, UpdateStatus.Error) self.update_details.update_buffer() - def do_update_templates( - self, rows: Dict[str, RowWrapper], settings: Settings - ): + def do_update_templates(self, rows: Dict[str, RowWrapper], settings: Settings): """Runs `qubes-vm-update` command.""" targets = ",".join((name for name in rows.keys())) @@ -357,12 +341,8 @@ def do_update_templates( stdout=subprocess.PIPE, ) - read_err_thread = threading.Thread( - target=self.read_stderrs, args=(proc, rows) - ) - read_out_thread = threading.Thread( - target=self.read_stdouts, args=(proc, rows) - ) + read_err_thread = threading.Thread(target=self.read_stderrs, args=(proc, rows)) + read_out_thread = threading.Thread(target=self.read_stdouts, args=(proc, rows)) read_err_thread.start() read_out_thread.start() @@ -479,9 +459,7 @@ def row_selected(self, _emitter, path, _col): Set updated details (name of vm and textview).""" self.selection.unselect_all() self.selection.select_path(path) - self.update_details.set_active_row( - self.vms_to_update[path.get_indices()[0]] - ) + self.update_details.set_active_row(self.vms_to_update[path.get_indices()[0]]) def get_update_summary(self): """Returns update summary. @@ -492,11 +470,7 @@ def get_update_summary(self): 3. vms that update was canceled before starting. """ updated = len( - [ - row - for row in self.vms_to_update - if row.status == UpdateStatus.Success - ] + [row for row in self.vms_to_update if row.status == UpdateStatus.Success] ) no_updates = len( [ @@ -506,18 +480,10 @@ def get_update_summary(self): ] ) failed = len( - [ - row - for row in self.vms_to_update - if row.status == UpdateStatus.Error - ] + [row for row in self.vms_to_update if row.status == UpdateStatus.Error] ) cancelled = len( - [ - row - for row in self.vms_to_update - if row.status == UpdateStatus.Cancelled - ] + [row for row in self.vms_to_update if row.status == UpdateStatus.Cancelled] ) return updated, no_updates, failed, cancelled @@ -561,8 +527,8 @@ def __init__(self, builder): self.progress_textview: Gtk.TextView = self.builder.get_object( "progress_textview" ) - self.progress_scrolled_window: Gtk.ScrolledWindow = ( - self.builder.get_object("progress_scrolled_window") + self.progress_scrolled_window: Gtk.ScrolledWindow = self.builder.get_object( + "progress_scrolled_window" ) def copy_content(self, _emitter): @@ -601,9 +567,7 @@ def update_buffer(self): def _autoscroll(self): adjustment = self.progress_scrolled_window.get_vadjustment() - adjustment.set_value( - adjustment.get_upper() - adjustment.get_page_size() - ) + adjustment.set_value(adjustment.get_upper() - adjustment.get_page_size()) class CellRendererProgressWithResult(Gtk.CellRendererProgress): diff --git a/qui/updater/summary_page.py b/qui/updater/summary_page.py index f4215956..268755a6 100644 --- a/qui/updater/summary_page.py +++ b/qui/updater/summary_page.py @@ -62,9 +62,7 @@ class SummaryPage: Show the summary of vm updates and appms that should be restarted. """ - def __init__( - self, builder, log, next_button, cancel_button, back_by_row_selection - ): + def __init__(self, builder, log, next_button, cancel_button, back_by_row_selection): self.builder = builder self.log = log self.next_button = next_button @@ -76,9 +74,7 @@ def __init__( self.updated_tmpls: Optional[list] = None - self.restart_list: Gtk.TreeView = self.builder.get_object( - "restart_list" - ) + self.restart_list: Gtk.TreeView = self.builder.get_object("restart_list") self.list_store: Optional[ListWrapper] = None self.stack: Gtk.Stack = self.builder.get_object("main_stack") @@ -86,20 +82,16 @@ def __init__( self.label_summary: Gtk.Label = self.builder.get_object("label_summary") self.restart_list.connect("row-activated", self.on_checkbox_toggled) - self.app_vm_list: Gtk.ListStore = self.builder.get_object( - "restart_list_store" - ) + self.app_vm_list: Gtk.ListStore = self.builder.get_object("restart_list_store") restart_checkbox_column: Gtk.TreeViewColumn = self.builder.get_object( "restart_checkbox_column" ) restart_checkbox_column.connect("clicked", self.on_header_toggled) restart_header_button: Gtk.Button = restart_checkbox_column.get_button() restart_header_button.connect("realize", pass_through_event_window) - self.restart_header: Gtk.Label = self.builder.get_object( - "restart_header" - ) - self.restart_scrolled_window: Gtk.ScrolledWindow = ( - self.builder.get_object("restart_scrolled_window") + self.restart_header: Gtk.Label = self.builder.get_object("restart_header") + self.restart_scrolled_window: Gtk.ScrolledWindow = self.builder.get_object( + "restart_scrolled_window" ) self.head_checkbox_button: Gtk.CheckButton = self.builder.get_object( @@ -111,9 +103,7 @@ def __init__( self.head_checkbox_button, self.next_button ) - self.summary_list: Gtk.TreeView = self.builder.get_object( - "summary_list" - ) + self.summary_list: Gtk.TreeView = self.builder.get_object("summary_list") self.summary_list.connect("row-activated", back_by_row_selection) @disable_checkboxes @@ -151,9 +141,7 @@ def on_header_toggled(self, _emitter): If the user has selected any vms that do not match the defined states, the cycle will start from (1). """ - on_head_checkbox_toggled( - self.list_store, self.head_checkbox, self.select_rows - ) + on_head_checkbox_toggled(self.list_store, self.head_checkbox, self.select_rows) @property def is_populated(self) -> bool: @@ -211,21 +199,16 @@ def populate_restart_list(self, restart, vm_updated, settings): self.updated_tmpls = [ row for row in vm_updated - if bool(row.status) - and QubeClass[row.vm.klass] == QubeClass.TemplateVM + if bool(row.status) and QubeClass[row.vm.klass] == QubeClass.TemplateVM ] possibly_changed_vms = set() for template in self.updated_tmpls: possibly_changed_vms.update(template.vm.derived_vms) - self.list_store = ListWrapper( - RestartRowWrapper, self.restart_list.get_model() - ) + self.list_store = ListWrapper(RestartRowWrapper, self.restart_list.get_model()) for vm in possibly_changed_vms: - if vm.is_running() and ( - vm.klass != "DispVM" or not vm.auto_cleanup - ): + if vm.is_running() and (vm.klass != "DispVM" or not vm.auto_cleanup): self.list_store.append_vm(vm) if settings.restart_service_vms: @@ -333,9 +316,7 @@ def shutdown_domains(self, to_shutdown): self.log.info("Shutdown %s", vm.name) except qubesadmin.exc.QubesVMError as err: self.err += vm.name + " cannot shutdown: " + str(err) + "\n" - self.log.error( - "Cannot shutdown %s because %s", vm.name, str(err) - ) + self.log.error("Cannot shutdown %s because %s", vm.name, str(err)) self.status = RestartStatus.ERROR_TMPL_DOWN loop = asyncio.get_event_loop() @@ -464,9 +445,7 @@ def is_error(self: "RestartStatus") -> bool: class RestartHeaderCheckbox(HeaderCheckbox): def __init__(self, checkbox_column_button, next_button): - super().__init__( - checkbox_column_button, [None, None, AppVMType.EXCLUDED] - ) + super().__init__(checkbox_column_button, [None, None, AppVMType.EXCLUDED]) self.next_button = next_button def allow_service_vms(self, value=True): diff --git a/qui/updater/tests/conftest.py b/qui/updater/tests/conftest.py index 79951d3a..ca253487 100644 --- a/qui/updater/tests/conftest.py +++ b/qui/updater/tests/conftest.py @@ -22,18 +22,9 @@ import pytest import importlib.resources -from qubes_config.tests.conftest import ( - add_dom0_vm_property, - add_dom0_text_property, - add_dom0_feature, - add_expected_vm, - add_feature_with_template_to_all, - add_feature_to_all, -) -from qubesadmin.tests import QubesTest - import gi +from qubesadmin.tests.mock_app import MockQube, MockQubesComplete from qui.updater.intro_page import UpdateRowWrapper from qui.updater.summary_page import RestartRowWrapper from qui.updater.utils import ListWrapper @@ -45,172 +36,32 @@ @pytest.fixture def test_qapp(): - return test_qapp_impl() - - -def test_qapp_impl(): - """Test QubesApp""" - qapp = QubesTest() - qapp._local_name = "dom0" # pylint: disable=protected-access - - add_dom0_vm_property(qapp, "clockvm", "sys-net") - add_dom0_vm_property(qapp, "updatevm", "sys-net") - add_dom0_vm_property(qapp, "default_netvm", "sys-net") - add_dom0_vm_property(qapp, "default_template", "fedora-36") - add_dom0_vm_property(qapp, "default_dispvm", "fedora-36") - - add_dom0_text_property(qapp, "default_kernel", "1.1") - add_dom0_text_property(qapp, "default_pool", "file") - - add_dom0_feature(qapp, "gui-default-allow-fullscreen", "") - add_dom0_feature(qapp, "gui-default-allow-utf8-titles", "") - add_dom0_feature(qapp, "gui-default-trayicon-mode", "") - add_dom0_feature(qapp, "qubes-vm-update-update-if-stale", None) - add_dom0_feature(qapp, "skip-update", None) - add_dom0_feature(qapp, "qubes-vm-update-hide-skipped", None) - add_dom0_feature(qapp, "qubes-vm-update-hide-updated", None) - - # setup labels - qapp.expected_calls[("dom0", "admin.label.List", None, None)] = ( - b"0\x00red\nblue\ngreen\n" - ) - - # setup pools: - qapp.expected_calls[("dom0", "admin.pool.List", None, None)] = ( - b"0\x00linux-kernel\nlvm\nfile\n" - ) - qapp.expected_calls[ - ("dom0", "admin.pool.volume.List", "linux-kernel", None) - ] = b"0\x001.1\nmisc\n4.2\n" - - add_expected_vm( - qapp, - "dom0", - "AdminVM", - {}, - { - "service.qubes-update-check": 1, - "config.default.qubes-update-check": None, - "config-usbvm-name": None, - "gui-default-secure-copy-sequence": None, - "gui-default-secure-paste-sequence": None, - }, - [], - ) - add_expected_vm( - qapp, - "sys-net", - "AppVM", - {"provides_network": ("bool", False, "True")}, - {"service.qubes-update-check": None, "service.qubes-updates-proxy": 1}, - [], - ) - - add_expected_vm( - qapp, - "sys-firewall", - "AppVM", - {"provides_network": ("bool", False, "True")}, - {"service.qubes-update-check": None}, - [], - ) - - add_expected_vm( - qapp, "sys-usb", "AppVM", {}, {"service.qubes-update-check": None}, [] - ) - - add_expected_vm( - qapp, - "fedora-36", - "TemplateVM", - {"netvm": ("vm", False, ""), "updateable": ("bool", True, "True")}, - {"service.qubes-update-check": None}, - [], - ) - - add_expected_vm( - qapp, - "fedora-35", - "TemplateVM", - {"netvm": ("vm", False, ""), "updateable": ("bool", True, "True")}, - {"service.qubes-update-check": None}, - [], - ) - - add_expected_vm( - qapp, - "default-dvm", - "DispVM", - { - "template_for_dispvms": ("bool", False, "True"), - "auto_cleanup": ("bool", False, "False"), + qapp = MockQubesComplete() + qapp._qubes["fedora-35"].updateable = True + qapp._qubes["fedora-36"] = MockQube( + name="fedora-36", + qapp=qapp, + klass="TemplateVM", + netvm="", + updateable=True, + installed_by_rpm=True, + features={ + "supported-service.qubes-u2f-proxy": "1", + "service.qubes-update-check": "1", + "service.updates-proxy-setup": "1", }, - {"service.qubes-update-check": None}, - [], - ) - - add_expected_vm( - qapp, "test-vm", "AppVM", {}, {"service.qubes-update-check": None}, [] - ) - - add_expected_vm( - qapp, - "test-blue", - "AppVM", - {"label": ("str", False, "blue")}, - {"service.qubes-update-check": None}, - [], ) - add_expected_vm( - qapp, - "test-red", - "AppVM", - {"label": ("str", False, "red")}, - {"service.qubes-update-check": None}, - [], - ) + qapp._qubes["fedora-35"].features["updates-available"] = "" - add_expected_vm( - qapp, - "test-standalone", - "StandaloneVM", - { - "label": ("str", False, "green"), - "updateable": ("bool", True, "True"), - }, - {"service.qubes-update-check": None}, - [], - ) + qapp._qubes["test-standalone"].updateable = True - add_expected_vm( - qapp, - "vault", - "AppVM", - {"netvm": ("vm", False, "")}, - {"service.qubes-update-check": None}, - [], - ) + qapp.update_vm_calls() + return qapp - add_feature_with_template_to_all( - qapp, - "supported-service.qubes-u2f-proxy", - ["test-vm", "fedora-35", "sys-usb"], - ) - add_feature_to_all(qapp, "service.qubes-u2f-proxy", ["test-vm"]) - add_feature_to_all(qapp, "restart-after-update", []) - add_feature_to_all(qapp, "updates-available", []) - add_feature_to_all(qapp, "last-update", []) - add_feature_to_all(qapp, "last-updates-check", []) - add_feature_to_all(qapp, "template-name", []) - add_feature_to_all( - qapp, "servicevm", ["sys-usb", "sys-firewall", "sys-net"] - ) - add_feature_to_all(qapp, "os-eol", []) - add_feature_to_all(qapp, "skip-update", []) - add_feature_to_all(qapp, "prohibit-start", []) - return qapp +def test_qapp_impl(): + return MockQubesComplete() @pytest.fixture diff --git a/qui/updater/tests/test_intro_page.py b/qui/updater/tests/test_intro_page.py index fa4d97d9..d4ea8d36 100644 --- a/qui/updater/tests/test_intro_page.py +++ b/qui/updater/tests/test_intro_page.py @@ -64,11 +64,13 @@ def test_populate_vm_list( assert len(sut.get_vms_to_update()) == 2 +# i-th expectations value is an expected number of selected VMs after clicking on the +# colum header i times @pytest.mark.parametrize( "updates_available, expectations", ( - pytest.param((2, 6), (0, 2, 6, 12, 0)), - pytest.param((6, 0), (0, 6, 12, 0)), + pytest.param((2, 6), (0, 2, 6, 13, 0)), + pytest.param((6, 0), (0, 6, 13, 0)), ), ) def test_on_header_toggled( @@ -88,7 +90,7 @@ def test_on_header_toggled( for vm in test_qapp.domains: sut.list_store.append_vm(vm) - assert len(sut.list_store) == 12 + assert len(sut.list_store) == 13 for i, row in enumerate(sut.list_store): if i < updates_available[0]: @@ -107,9 +109,9 @@ def test_on_header_toggled( assert selected_num == expected assert ( sut.checkbox_column_button.get_inconsistent() - and expected not in (0, 12) + and expected not in (0, 13) or sut.checkbox_column_button.get_active() - and expected == 12 + and expected == 13 or not sut.checkbox_column_button.get_active() and expected == 0 ) @@ -128,7 +130,7 @@ def test_on_checkbox_toggled( for vm in test_qapp.domains: sut.list_store.append_vm(vm) - assert len(sut.list_store) == 12 + assert len(sut.list_store) == 13 sut.head_checkbox.state = HeaderCheckbox.NONE sut.head_checkbox.set_buttons() @@ -186,7 +188,7 @@ def test_prohibit_start( for vm in test_qapp.domains: sut.list_store.append_vm(vm) - assert len(sut.list_store) == 12 + assert len(sut.list_store) == 13 sut.head_checkbox.state = HeaderCheckbox.NONE sut.head_checkbox.set_buttons() @@ -358,9 +360,7 @@ def test_prohibit_start_rationale_tooltip( ), # `qubes-update-gui --dom0` # Target dom0 - pytest.param( - ("--dom0", "--force-update"), b"", b"", {"dom0"}, None, id="dom0" - ), + pytest.param(("--dom0", "--force-update"), b"", b"", {"dom0"}, None, id="dom0"), # `qubes-update-gui --dom0 --skip dom0` # Comma separated list of VMs to be skipped, # works with all other options. @@ -398,9 +398,7 @@ def test_prohibit_start_rationale_tooltip( ("--skip", "fedora-36,garbage-name", "--templates"), id="templates with skip", ), - pytest.param( - ("--force-update",), b"", b"", set(), None, id="force-update" - ), + pytest.param(("--force-update",), b"", b"", set(), None, id="force-update"), ), ) def test_select_rows_ignoring_conditions( @@ -424,13 +422,12 @@ def test_select_rows_ignoring_conditions( for vm in test_qapp.domains: sut.list_store.append_vm(vm) - assert len(sut.list_store) == 12 + assert len(sut.list_store) == 13 result = b"" if tmpls_and_stndas: result += ( - b"Following templates and standalones will be updated: " - + tmpls_and_stndas + b"Following templates and standalones will be updated: " + tmpls_and_stndas ) if derived_qubes: if result: diff --git a/qui/updater/tests/test_summary_page.py b/qui/updater/tests/test_summary_page.py index fdc2d121..977081f7 100644 --- a/qui/updater/tests/test_summary_page.py +++ b/qui/updater/tests/test_summary_page.py @@ -96,7 +96,7 @@ def test_on_header_toggled( sut.head_checkbox._allowed[0] = AppVMType.SERVICEVM service_num = 3 sut.head_checkbox._allowed[1] = AppVMType.NON_SERVICEVM - non_excluded_num = 6 + non_excluded_num = 7 sut.head_checkbox.state = HeaderCheckbox.NONE @@ -176,9 +176,9 @@ def test_on_checkbox_toggled( # expected data based on test_qapp setup -UP_VMS = 7 +UP_VMS = 9 UP_SERVICE_VMS = 3 -UP_APP_VMS = 4 +UP_APP_VMS = 6 @pytest.mark.parametrize( @@ -381,6 +381,7 @@ def test_perform_restart( "test-blue", "test-red", "test-vm", + "test-old", "vault", ) expected_shutdown_calls = [ @@ -394,9 +395,7 @@ def test_perform_restart( "sys-net", "sys-usb", ) - expected_start_calls = [ - (tmpl, "admin.vm.Start", None, None) for tmpl in to_start - ] + expected_start_calls = [(tmpl, "admin.vm.Start", None, None) for tmpl in to_start] for call_ in expected_start_calls: test_qapp.expected_calls[call_] = b"0\x00" diff --git a/qui/updater/tests/test_updater.py b/qui/updater/tests/test_updater.py index 53c34b04..44397026 100644 --- a/qui/updater/tests/test_updater.py +++ b/qui/updater/tests/test_updater.py @@ -60,16 +60,12 @@ def test_setup_non_interactive_nothing_to_do( def test_setup_update_if_available( select, populate_vm_list, _mock_logging, __mock_logging, test_qapp ): - sut = QubesUpdater( - test_qapp, parse_args(("--update-if-available",), test_qapp) - ) + sut = QubesUpdater(test_qapp, parse_args(("--update-if-available",), test_qapp)) sut.perform_setup() calls = [call(sut.qapp, sut.settings)] populate_vm_list.assert_has_calls(calls) select.assert_called_once() - assert ( - sut.intro_page.head_checkbox.state == sut.intro_page.head_checkbox.SAFE - ) + assert sut.intro_page.head_checkbox.state == sut.intro_page.head_checkbox.SAFE @patch("logging.FileHandler") @@ -84,9 +80,7 @@ def test_setup_force_update( calls = [call(sut.qapp, sut.settings)] populate_vm_list.assert_has_calls(calls) select.assert_called_once() - assert ( - sut.intro_page.head_checkbox.state == sut.intro_page.head_checkbox.ALL - ) + assert sut.intro_page.head_checkbox.state == sut.intro_page.head_checkbox.ALL @patch("logging.FileHandler") @@ -168,9 +162,7 @@ def set_vms(_vms_to_update, _settings): assert not sut.intro_page.active assert sut.progress_page.is_visible - sut.progress_page.init_update.assert_called_once_with( - vms_to_update, sut.settings - ) + sut.progress_page.init_update.assert_called_once_with(vms_to_update, sut.settings) # set sut.summary_page.is_populated = False sut.summary_page.list_store = None @@ -234,9 +226,7 @@ def set_vms(_vms_to_update, _settings): assert not sut.intro_page.active assert sut.progress_page.is_visible - sut.progress_page.init_update.assert_called_once_with( - vms_to_update, sut.settings - ) + sut.progress_page.init_update.assert_called_once_with(vms_to_update, sut.settings) # set sut.summary_page.is_populated = False sut.summary_page.list_store = None diff --git a/qui/updater/tests/test_updater_settings.py b/qui/updater/tests/test_updater_settings.py index 18c3f6b4..206610f9 100644 --- a/qui/updater/tests/test_updater_settings.py +++ b/qui/updater/tests/test_updater_settings.py @@ -356,9 +356,9 @@ def test_limit_concurrency(test_qapp): # Set True sut.show() sut.limit_concurrency_checkbox.set_active(True) - test_qapp.expected_calls[ - (*dom0_set_max_concurrency, sut.DEFAULT_CONCURRENCY) - ] = b"0\x00" + test_qapp.expected_calls[(*dom0_set_max_concurrency, sut.DEFAULT_CONCURRENCY)] = ( + b"0\x00" + ) sut.save_and_close(None) test_qapp.expected_calls[dom0_get_max_concurrency] = ( b"0\x00" + str(sut.DEFAULT_CONCURRENCY).encode() @@ -378,9 +378,7 @@ def test_limit_concurrency(test_qapp): # Set concurrency to max value again sut.show() sut.limit_concurrency_checkbox.set_active(True) - del test_qapp.expected_calls[ - (*dom0_set_max_concurrency, sut.DEFAULT_CONCURRENCY) - ] + del test_qapp.expected_calls[(*dom0_set_max_concurrency, sut.DEFAULT_CONCURRENCY)] sut.save_and_close(None) # Set False diff --git a/qui/updater/tests/test_utils.py b/qui/updater/tests/test_utils.py index 321ebe74..c0609704 100644 --- a/qui/updater/tests/test_utils.py +++ b/qui/updater/tests/test_utils.py @@ -23,21 +23,15 @@ def test_check_support(): qapp = MockQubes() - vm_supported = MockQube( - "test-qube-1", qapp, features={"os-eol": "2060-01-01"} - ) - vm_not_supported = MockQube( - "test-qube-2", qapp, features={"os-eol": "1990-01-01"} - ) + vm_supported = MockQube("test-qube-1", qapp, features={"os-eol": "2060-01-01"}) + vm_not_supported = MockQube("test-qube-2", qapp, features={"os-eol": "1990-01-01"}) fedora_min = MockQube( "test-qube-3", qapp, features={"template-name": "fedora-36-minimal"} ) fedora_xfce = MockQube( "test-qube-4", qapp, features={"template-name": "fedora-35-xfce"} ) - wrong_name = MockQube( - "test-qube-5", qapp, features={"template-name": "faedora-66"} - ) + wrong_name = MockQube("test-qube-5", qapp, features={"template-name": "faedora-66"}) debian_minimal = MockQube( "test-qube-6", qapp, features={"template-name": "debian-9-minimal"} ) diff --git a/qui/updater/updater.py b/qui/updater/updater.py index 9e0c516c..d97e3720 100644 --- a/qui/updater/updater.py +++ b/qui/updater/updater.py @@ -57,9 +57,7 @@ def __init__(self, qapp, cliargs): self.cliargs = cliargs self.retcode = 0 - log_handler = logging.FileHandler( - QubesUpdater.LOGPATH, encoding="utf-8" - ) + log_handler = logging.FileHandler(QubesUpdater.LOGPATH, encoding="utf-8") log_formatter = logging.Formatter(QubesUpdater.LOG_FORMAT) log_handler.setFormatter(log_formatter) @@ -94,9 +92,7 @@ def perform_setup(self, *_args, **_kwargs): self.main_window: Gtk.Window = self.builder.get_object("main_window") self.next_button: Gtk.Button = self.builder.get_object("button_next") self.next_button.connect("clicked", self.next_clicked) - self.cancel_button: Gtk.Button = self.builder.get_object( - "button_cancel" - ) + self.cancel_button: Gtk.Button = self.builder.get_object("button_cancel") self.cancel_button.connect("clicked", self.cancel_clicked) self.EffectiveCssProvider = load_theme( @@ -130,9 +126,7 @@ def perform_setup(self, *_args, **_kwargs): self.progress_page.back_by_row_selection, ) - self.button_settings: Gtk.Button = self.builder.get_object( - "button_settings" - ) + self.button_settings: Gtk.Button = self.builder.get_object("button_settings") self.button_settings.connect("clicked", self.open_settings_window) settings_pixbuf = load_icon_at_gtk_size( "qubes-customize", Gtk.IconSize.LARGE_TOOLBAR @@ -184,12 +178,8 @@ def cell_data_func(_column, cell, model, it, data): cell.set_property("markup", str(obj)) for col, name in headers: - renderer: Gtk.CellRenderer = self.builder.get_object( - name + "_renderer" - ) - column: Gtk.TreeViewColumn = self.builder.get_object( - name + "_column" - ) + renderer: Gtk.CellRenderer = self.builder.get_object(name + "_renderer") + column: Gtk.TreeViewColumn = self.builder.get_object(name + "_column") column.set_cell_data_func(renderer, cell_data_func, col) renderer.props.ypad = 10 if not name.endswith("name") and name != "summary_status": @@ -213,14 +203,10 @@ def cell_data_func(_column, cell, model, it, data): else: # default update_if_stale -> do nothing if self.cliargs.update_if_available: - self.intro_page.head_checkbox.state = ( - self.intro_page.head_checkbox.SAFE - ) + self.intro_page.head_checkbox.state = self.intro_page.head_checkbox.SAFE self.intro_page.select_rows() elif self.cliargs.force_update: - self.intro_page.head_checkbox.state = ( - self.intro_page.head_checkbox.ALL - ) + self.intro_page.head_checkbox.state = self.intro_page.head_checkbox.ALL self.intro_page.select_rows() self.log.info("Show intro page.") self.main_window.show_all() @@ -271,9 +257,7 @@ def next_clicked(self, _emitter, skip_intro=False): self.summary_page.show(updated, no_updates, failed + cancelled) else: # at this point retcode is in (0, 100) - self._restart_phase( - show_only_error=self.cliargs.non_interactive - ) + self._restart_phase(show_only_error=self.cliargs.non_interactive) # at thi point retcode is in (0, 100) # or an error message have been already shown if self.cliargs.non_interactive and self.retcode in (0, 100): @@ -308,8 +292,7 @@ def _show_success_dialog(self): ) msg = "Nothing to do." if ( # at least all vms with available updates was updated - (self.cliargs.all and not self.cliargs.skip) - or not non_default_select + (self.cliargs.all and not self.cliargs.skip) or not non_default_select ) and self.retcode in (0, 100): msg = "Qubes OS is up to date." elif self.retcode == 0: @@ -407,8 +390,7 @@ def parse_args(args, app): parser.add_argument( "--signal-no-updates", action="store_true", - help="Return exit code 100 instread of 0 " - "if there is no updates available.", + help="Return exit code 100 instread of 0 if there is no updates available.", ) restart = parser.add_mutually_exclusive_group() @@ -435,8 +417,7 @@ def parse_args(args, app): update_state.add_argument( "--force-update", action="store_true", - help="Attempt to update all targeted VMs " - "even if no updates are available", + help="Attempt to update all targeted VMs even if no updates are available", ) update_state.add_argument( "--update-if-stale", @@ -492,8 +473,7 @@ def parse_args(args, app): parser.add_argument( "--dom0", action="store_true", - help="Target dom0. " - "If present, skip manual selection of qubes to update.", + help="Target dom0. If present, skip manual selection of qubes to update.", ) parser.add_argument( diff --git a/qui/updater/updater_settings.py b/qui/updater/updater_settings.py index 92838519..448ee6c1 100644 --- a/qui/updater/updater_settings.py +++ b/qui/updater/updater_settings.py @@ -83,9 +83,7 @@ def __init__( with importlib.resources.as_file(glade_ref) as path: self.builder.add_from_file(str(path)) - self.settings_window: Gtk.Window = self.builder.get_object( - "main_window" - ) + self.settings_window: Gtk.Window = self.builder.get_object("main_window") self.settings_window.set_transient_for(main_window) self.settings_window.connect("delete-event", self.close_without_saving) @@ -93,17 +91,13 @@ def __init__( self.cancel_button: Gtk.Button = self.builder.get_object( "button_settings_cancel" ) - self.cancel_button.connect( - "clicked", lambda _: self.settings_window.close() - ) + self.cancel_button.connect("clicked", lambda _: self.settings_window.close()) - self.save_button: Gtk.Button = self.builder.get_object( - "button_settings_save" - ) + self.save_button: Gtk.Button = self.builder.get_object("button_settings_save") self.save_button.connect("clicked", self.save_and_close) - self.days_without_update_button: Gtk.SpinButton = ( - self.builder.get_object("days_without_update") + self.days_without_update_button: Gtk.SpinButton = self.builder.get_object( + "days_without_update" ) adj = Gtk.Adjustment( Settings.DEFAULT_UPDATE_IF_STALE, @@ -115,8 +109,8 @@ def __init__( ) self.days_without_update_button.configure(adj, 1, 0) - self.restart_servicevms_checkbox: Gtk.CheckButton = ( - self.builder.get_object("restart_servicevms") + self.restart_servicevms_checkbox: Gtk.CheckButton = self.builder.get_object( + "restart_servicevms" ) self.restart_servicevms_checkbox.connect( "toggled", self._show_restart_exceptions @@ -125,9 +119,7 @@ def __init__( self.restart_other_checkbox: Gtk.CheckButton = self.builder.get_object( "restart_other" ) - self.restart_other_checkbox.connect( - "toggled", self._show_restart_exceptions - ) + self.restart_other_checkbox.connect("toggled", self._show_restart_exceptions) self.hide_skipped_checkbox: Gtk.CheckButton = self.builder.get_object( "hide_skipped" @@ -140,9 +132,7 @@ def __init__( self.available_vms = [ vm for vm in self.qapp.domains - if vm.klass == "DispVM" - and not vm.auto_cleanup - or vm.klass == "AppVM" + if vm.klass == "DispVM" and not vm.auto_cleanup or vm.klass == "AppVM" ] self.excluded_vms = [ vm @@ -160,8 +150,8 @@ def __init__( "restart_exceptions_page" ) - self.limit_concurrency_checkbox: Gtk.CheckButton = ( - self.builder.get_object("limit_concurrency") + self.limit_concurrency_checkbox: Gtk.CheckButton = self.builder.get_object( + "limit_concurrency" ) self.limit_concurrency_checkbox.connect( "toggled", self._limit_concurrency_toggled @@ -268,16 +258,10 @@ def load_settings(self): self._init_restart_servicevms = self.restart_service_vms self._init_restart_other_vms = self.restart_other_vms - self.restart_servicevms_checkbox.set_sensitive( - not self.overrides.apply_to_sys - ) - self.restart_servicevms_checkbox.set_active( - self._init_restart_servicevms - ) + self.restart_servicevms_checkbox.set_sensitive(not self.overrides.apply_to_sys) + self.restart_servicevms_checkbox.set_active(self._init_restart_servicevms) self.restart_other_checkbox.set_active(self._init_restart_other_vms) - self.restart_other_checkbox.set_sensitive( - not self.overrides.apply_to_other - ) + self.restart_other_checkbox.set_sensitive(not self.overrides.apply_to_other) self._init_max_concurrency = self.max_concurrency self._init_limit_concurrency = self._init_max_concurrency is not None diff --git a/qui/updater/utils.py b/qui/updater/utils.py index da5bdd52..23efbcdc 100644 --- a/qui/updater/utils.py +++ b/qui/updater/utils.py @@ -117,9 +117,7 @@ def on_head_checkbox_toggled(list_store, head_checkbox, select_rows): head_checkbox.state = HeaderCheckbox.NONE selected_num = 0 else: - selected_num = selected_num_old = sum( - row.selected for row in list_store - ) + selected_num = selected_num_old = sum(row.selected for row in list_store) while selected_num == selected_num_old: head_checkbox.next_state() select_rows() diff --git a/qui/updater_settings.glade b/qui/updater_settings.glade index 8f0c3e10..f957d4fd 100644 --- a/qui/updater_settings.glade +++ b/qui/updater_settings.glade @@ -264,19 +264,59 @@ + True False 28 10 - + True - False - True - - - True + True + True + start + none + + + True + False + 28 + 28 + + + True + False + +ADD + + + + False + True + 0 + + + + + True + False + 5 + qube + + + + False + True + 1 + + + False @@ -285,45 +325,25 @@ - - Cancel + True - True - False - none - + False + True + + + True + + False True - 2 + 1 - - ADD - True - True - True - none - - - - False - True - 3 - + - False @@ -344,61 +364,6 @@ 2 - - - True - True - True - start - none - - - True - False - 28 - - - True - False - +ADD - - - - False - True - 0 - - - - - True - False - 5 - qube - - - - False - True - 1 - - - - - - - - False - True - 3 - - False @@ -497,13 +462,13 @@ - Filtering Options True False start start 18 True + Filtering Options True