Browse Source

Update windows-related feature requests

Handle 'os' feature - if it's Windows, then set rpc-clipboard feature.
Handle 'gui-emulated' feature - request for specifically stubdomain GUI.
With 'gui' feature it is only possible to enable gui-agent based on, or
disable GUI completely.
Handle 'default-user' - verify it for weird characters and set
'default_user' property (if wasn't already set).

QubesOS/qubes-issues#3585
Marek Marczykowski-Górecki 5 years ago
parent
commit
af7d54d388
7 changed files with 129 additions and 5 deletions
  1. 2 1
      qubes/api/misc.py
  2. 2 2
      qubes/ext/core_features.py
  3. 64 0
      qubes/ext/windows.py
  4. 8 2
      qubes/tests/api_misc.py
  5. 51 0
      qubes/tests/ext.py
  6. 1 0
      rpm_spec/core-dom0.spec.in
  7. 1 0
      setup.py

+ 2 - 1
qubes/api/misc.py

@@ -76,7 +76,8 @@ class QubesMiscAPI(qubes.api.AbstractQubesAPI):
 
         untrusted_features = {}
         safe_set = string.ascii_letters + string.digits
-        expected_features = ('qrexec', 'gui', 'default-user')
+        expected_features = ('qrexec', 'gui', 'gui-emulated', 'default-user',
+            'os')
         for feature in expected_features:
             untrusted_value = self.src.untrusted_qdb.read(
                 '/qubes-tools/' + feature)

+ 2 - 2
qubes/ext/core_features.py

@@ -32,7 +32,7 @@ class CoreFeatures(qubes.ext.Extension):
             return
 
         requested_features = {}
-        for feature in ('qrexec', 'gui', 'qubes-firewall'):
+        for feature in ('qrexec', 'gui', 'gui-emulated', 'qubes-firewall'):
             untrusted_value = untrusted_features.get(feature, None)
             if untrusted_value in ('1', '0'):
                 requested_features[feature] = bool(int(untrusted_value))
@@ -44,7 +44,7 @@ class CoreFeatures(qubes.ext.Extension):
         # gui agent presence (0 or 1)
 
         qrexec_before = vm.features.get('qrexec', False)
-        for feature in ('qrexec', 'gui'):
+        for feature in ('qrexec', 'gui', 'gui-emulated'):
             # do not allow (Template)VM to override setting if already set
             # some other way
             if feature in requested_features and feature not in vm.features:

+ 64 - 0
qubes/ext/windows.py

@@ -0,0 +1,64 @@
+# -*- encoding: utf-8 -*-
+#
+# The Qubes OS Project, http://www.qubes-os.org
+#
+# Copyright (C) 2017 Marek Marczykowski-Górecki
+#                               <marmarek@invisiblethingslab.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, see <http://www.gnu.org/licenses/>.
+
+import qubes.ext
+
+class WindowsFeatures(qubes.ext.Extension):
+    # pylint: disable=too-few-public-methods
+    @qubes.ext.handler('features-request')
+    def qubes_features_request(self, vm, event, untrusted_features):
+        '''Handle features provided requested by Qubes Windows Tools'''
+        # pylint: disable=no-self-use,unused-argument
+        if getattr(vm, 'template', None):
+            vm.log.warning(
+                'Ignoring qubes.NotifyTools for template-based VM')
+            return
+
+        guest_os = None
+        if 'os' in untrusted_features:
+            if untrusted_features['os'] in ['Windows']:
+                guest_os = untrusted_features['os']
+
+        qrexec = None
+        if 'qrexec' in untrusted_features:
+            if untrusted_features['qrexec'] == '1':
+                # qrexec feature is set by CoreFeatures extension
+                qrexec = True
+
+        del untrusted_features
+
+        if guest_os:
+            vm.features['os'] = guest_os
+        if guest_os == 'Windows' and qrexec:
+            vm.features['rpc-clipboard'] = True
+
+    @qubes.ext.handler('domain-add', system=True)
+    def on_domain_add(self, app, _event, vm, **kwargs):
+        # pylint: disable=no-self-use,unused-argument
+        if getattr(vm, 'template', None) is None:
+            # handle only template-based vms
+            return
+
+        template = vm.template
+        if template.features.check_with_template('os', None) != 'Windows':
+            # ignore non-windows templates
+            return
+
+        # TODO: consider copying template's root volume here

+ 8 - 2
qubes/tests/api_misc.py

@@ -131,11 +131,14 @@ class TC_00_API_Misc(qubes.tests.QubesTestCase):
         self.assertEqual(self.src.mock_calls, [
             mock.call.untrusted_qdb.read('/qubes-tools/qrexec'),
             mock.call.untrusted_qdb.read('/qubes-tools/gui'),
+            mock.call.untrusted_qdb.read('/qubes-tools/gui-emulated'),
             mock.call.untrusted_qdb.read('/qubes-tools/default-user'),
+            mock.call.untrusted_qdb.read('/qubes-tools/os'),
             mock.call.fire_event_async('features-request', untrusted_features={
                 'gui': '1',
                 'default-user': 'user',
-                'qrexec': '1'}),
+                'qrexec': '1',
+                'os': 'Linux'}),
             ('fire_event_async().__iter__', (), {}),
         ])
         self.assertEqual(self.app.mock_calls, [mock.call.save()])
@@ -153,11 +156,14 @@ class TC_00_API_Misc(qubes.tests.QubesTestCase):
         self.assertEqual(self.src.mock_calls, [
             mock.call.untrusted_qdb.read('/qubes-tools/qrexec'),
             mock.call.untrusted_qdb.read('/qubes-tools/gui'),
+            mock.call.untrusted_qdb.read('/qubes-tools/gui-emulated'),
             mock.call.untrusted_qdb.read('/qubes-tools/default-user'),
+            mock.call.untrusted_qdb.read('/qubes-tools/os'),
             mock.call.fire_event_async('features-request', untrusted_features={
                 'gui': '1',
                 'default-user': 'user',
-                'qrexec': '1'}),
+                'qrexec': '1',
+                'os': 'Linux'}),
             ('fire_event_async().__iter__', (), {}),
         ])
         self.assertEqual(self.app.mock_calls, [mock.call.save()])

+ 51 - 0
qubes/tests/ext.py

@@ -21,6 +21,7 @@
 from unittest import mock
 
 import qubes.ext.core_features
+import qubes.ext.windows
 import qubes.tests
 
 
@@ -163,3 +164,53 @@ class TC_00_CoreFeatures(qubes.tests.QubesTestCase):
             ('features.__contains__', ('qrexec',), {}),
             ('features.__contains__', ('gui',), {}),
         ])
+
+class TC_10_WindowsFeatures(qubes.tests.QubesTestCase):
+    def setUp(self):
+        super().setUp()
+        self.ext = qubes.ext.windows.WindowsFeatures()
+        self.vm = mock.MagicMock()
+        self.features = {}
+        self.vm.configure_mock(**{
+            'features.get.side_effect': self.features.get,
+            'features.__contains__.side_effect': self.features.__contains__,
+            'features.__setitem__.side_effect': self.features.__setitem__,
+            })
+
+    def test_000_notify_tools_full(self):
+        del self.vm.template
+        self.ext.qubes_features_request(self.vm, 'features-request',
+            untrusted_features={
+                'gui': '1',
+                'version': '1',
+                'default-user': 'user',
+                'qrexec': '1',
+                'os': 'Windows'})
+        self.assertEqual(self.vm.mock_calls, [
+            ('features.__setitem__', ('os', 'Windows'), {}),
+            ('features.__setitem__', ('rpc-clipboard', True), {}),
+        ])
+
+    def test_001_notify_tools_no_qrexec(self):
+        del self.vm.template
+        self.ext.qubes_features_request(self.vm, 'features-request',
+            untrusted_features={
+                'gui': '1',
+                'version': '1',
+                'default-user': 'user',
+                'qrexec': '0',
+                'os': 'Windows'})
+        self.assertEqual(self.vm.mock_calls, [
+            ('features.__setitem__', ('os', 'Windows'), {}),
+        ])
+
+    def test_002_notify_tools_other_os(self):
+        del self.vm.template
+        self.ext.qubes_features_request(self.vm, 'features-request',
+            untrusted_features={
+                'gui': '1',
+                'version': '1',
+                'default-user': 'user',
+                'qrexec': '1',
+                'os': 'Linux'})
+        self.assertEqual(self.vm.mock_calls, [])

+ 1 - 0
rpm_spec/core-dom0.spec.in

@@ -284,6 +284,7 @@ fi
 %{python3_sitelib}/qubes/ext/qubesmanager.py
 %{python3_sitelib}/qubes/ext/r3compatibility.py
 %{python3_sitelib}/qubes/ext/services.py
+%{python3_sitelib}/qubes/ext/windows.py
 
 %dir %{python3_sitelib}/qubes/tests
 %dir %{python3_sitelib}/qubes/tests/__pycache__

+ 1 - 0
setup.py

@@ -74,6 +74,7 @@ if __name__ == '__main__':
                 'qubes.ext.pci = qubes.ext.pci:PCIDeviceExtension',
                 'qubes.ext.block = qubes.ext.block:BlockDeviceExtension',
                 'qubes.ext.services = qubes.ext.services:ServicesExtension',
+                'qubes.ext.windows = qubes.ext.windows:WindowsFeatures',
             ],
             'qubes.devices': [
                 'pci = qubes.ext.pci:PCIDevice',