ext/services: mechanism for advertising supported services

Support 'supported-service.*' features requests coming from VMs. Set
such features directly (allow only value '1') and remove any not
reported in given call. This way uninstalling package providing given
service will automatically remove related 'supported-service...'
feature.

Fixes QubesOS/qubes-issues#4402
This commit is contained in:
Marek Marczykowski-Górecki 2018-10-18 05:44:08 +02:00
parent 58bcec2a64
commit d1f5cb5d15
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
2 changed files with 73 additions and 0 deletions

View File

@ -62,3 +62,40 @@ class ServicesExtension(qubes.ext.Extension):
return
service = feature[len('service.'):]
vm.untrusted_qdb.rm('/qubes-service/{}'.format(service))
@qubes.ext.handler('features-request')
def supported_services(self, vm, event, untrusted_features):
'''Handle advertisement of supported services'''
# pylint: disable=no-self-use,unused-argument
if getattr(vm, 'template', None):
vm.log.warning(
'Ignoring qubes.FeaturesRequest from template-based VM')
return
new_supported_services = set()
for requested_service in untrusted_features:
if not requested_service.startswith('supported-service.'):
continue
if untrusted_features[requested_service] == '1':
# only allow to advertise service as supported, lack of entry
# means service is not supported
new_supported_services.add(requested_service)
del untrusted_features
# if no service is supported, ignore the whole thing - do not clear
# all services in case of empty request (manual or such)
if not new_supported_services:
return
old_supported_services = set(
feat for feat in vm.features
if feat.startswith('supported-service.') and vm.features[feat])
for feature in new_supported_services.difference(
old_supported_services):
vm.features[feature] = True
for feature in old_supported_services.difference(
new_supported_services):
del vm.features[feature]

View File

@ -223,6 +223,7 @@ class TC_20_Services(qubes.tests.QubesTestCase):
self.vm = mock.MagicMock()
self.features = {}
self.vm.configure_mock(**{
'template': None,
'is_running.return_value': True,
'features.get.side_effect': self.features.get,
'features.items.side_effect': self.features.items,
@ -269,3 +270,38 @@ class TC_20_Services(qubes.tests.QubesTestCase):
self.assertEqual(sorted(self.vm.untrusted_qdb.mock_calls), [
('rm', ('/qubes-service/test3',), {}),
])
def test_010_supported_services(self):
self.ext.supported_services(self.vm, 'features-request',
untrusted_features={
'supported-service.test1': '1', # ok
'supported-service.test2': '0', # ignored
'supported-service.test3': 'some text', # ignored
'no-service': '1', # ignored
})
self.assertEqual(self.features, {
'supported-service.test1': True,
})
def test_011_supported_services_add(self):
self.features['supported-service.test1'] = '1'
self.ext.supported_services(self.vm, 'features-request',
untrusted_features={
'supported-service.test1': '1', # ok
'supported-service.test2': '1', # ok
})
# also check if existing one is untouched
self.assertEqual(self.features, {
'supported-service.test1': '1',
'supported-service.test2': True,
})
def test_012_supported_services_remove(self):
self.features['supported-service.test1'] = '1'
self.ext.supported_services(self.vm, 'features-request',
untrusted_features={
'supported-service.test2': '1', # ok
})
self.assertEqual(self.features, {
'supported-service.test2': True,
})