Skip to content

Commit c3afdde

Browse files
committed
api/admin: add API for changing revisions_to_keep dynamically
This one pool/volume property makes sense to change dynamically. There may be more such properties, but lets be on the safe side and take whitelist approach - allow only selected (just one for now), instead of blacklisting any harmful ones. QubesOS/qubes-issues#3256
1 parent 81f455e commit c3afdde

File tree

3 files changed

+106
-0
lines changed

3 files changed

+106
-0
lines changed

Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ ADMIN_API_METHODS_SIMPLE = \
2323
admin.pool.List \
2424
admin.pool.ListDrivers \
2525
admin.pool.Remove \
26+
admin.pool.Set.revisions_to_keep \
2627
admin.pool.volume.Info \
2728
admin.pool.volume.List \
2829
admin.pool.volume.ListSnapshots \
2930
admin.pool.volume.Resize \
3031
admin.pool.volume.Revert \
32+
admin.pool.volume.Set.revisions_to_keep \
3133
admin.pool.volume.Snapshot \
3234
admin.property.Get \
3335
admin.property.GetDefault \
@@ -96,6 +98,7 @@ ADMIN_API_METHODS_SIMPLE = \
9698
admin.vm.volume.ListSnapshots \
9799
admin.vm.volume.Resize \
98100
admin.vm.volume.Revert \
101+
admin.vm.volume.Set.revisions_to_keep \
99102
admin.vm.Stats \
100103
$(null)
101104

qubes/api/admin.py

+40
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,25 @@ def vm_volume_import(self):
476476

477477
return '{} {}'.format(size, path)
478478

479+
@qubes.api.method('admin.vm.volume.Set.revisions_to_keep',
480+
scope='local', write=True)
481+
@asyncio.coroutine
482+
def vm_volume_set_revisions_to_keep(self, untrusted_payload):
483+
assert self.arg in self.dest.volumes.keys()
484+
try:
485+
untrusted_value = int(untrusted_payload.decode('ascii'))
486+
except (UnicodeDecodeError, ValueError):
487+
raise qubes.api.ProtocolError('Invalid value')
488+
del untrusted_payload
489+
assert untrusted_value >= 0
490+
newvalue = untrusted_value
491+
del untrusted_value
492+
493+
self.fire_event_for_permission(newvalue=newvalue)
494+
495+
self.dest.volumes[self.arg].revisions_to_keep = newvalue
496+
self.app.save()
497+
479498
@qubes.api.method('admin.vm.tag.List', no_payload=True,
480499
scope='local', read=True)
481500
@asyncio.coroutine
@@ -621,6 +640,27 @@ def pool_remove(self):
621640
self.app.remove_pool(self.arg)
622641
self.app.save()
623642

643+
@qubes.api.method('admin.pool.Set.revisions_to_keep',
644+
scope='global', write=True)
645+
@asyncio.coroutine
646+
def pool_set_revisions_to_keep(self, untrusted_payload):
647+
assert self.dest.name == 'dom0'
648+
assert self.arg in self.app.pools.keys()
649+
pool = self.app.pools[self.arg]
650+
try:
651+
untrusted_value = int(untrusted_payload.decode('ascii'))
652+
except (UnicodeDecodeError, ValueError):
653+
raise qubes.api.ProtocolError('Invalid value')
654+
del untrusted_payload
655+
assert untrusted_value >= 0
656+
newvalue = untrusted_value
657+
del untrusted_value
658+
659+
self.fire_event_for_permission(newvalue=newvalue)
660+
661+
pool.revisions_to_keep = newvalue
662+
self.app.save()
663+
624664
@qubes.api.method('admin.label.List', no_payload=True,
625665
scope='global', read=True)
626666
@asyncio.coroutine

qubes/tests/api_admin.py

+63
Original file line numberDiff line numberDiff line change
@@ -2275,6 +2275,69 @@ def test_655_vm_device_set_persistent_invalid_value(self):
22752275
self.assertNotIn(dev, self.vm.devices['testclass'].persistent())
22762276
self.assertFalse(self.app.save.called)
22772277

2278+
def test_660_pool_set_revisions_to_keep(self):
2279+
self.app.pools['test-pool'] = unittest.mock.Mock()
2280+
value = self.call_mgmt_func(b'admin.pool.Set.revisions_to_keep',
2281+
b'dom0', b'test-pool', b'2')
2282+
self.assertIsNone(value)
2283+
self.assertEqual(self.app.pools['test-pool'].mock_calls, [])
2284+
self.assertEqual(self.app.pools['test-pool'].revisions_to_keep, 2)
2285+
self.app.save.assert_called_once_with()
2286+
2287+
def test_661_pool_set_revisions_to_keep_negative(self):
2288+
self.app.pools['test-pool'] = unittest.mock.Mock()
2289+
with self.assertRaises(AssertionError):
2290+
self.call_mgmt_func(b'admin.pool.Set.revisions_to_keep',
2291+
b'dom0', b'test-pool', b'-2')
2292+
self.assertEqual(self.app.pools['test-pool'].mock_calls, [])
2293+
self.assertFalse(self.app.save.called)
2294+
2295+
def test_662_pool_set_revisions_to_keep_not_a_number(self):
2296+
self.app.pools['test-pool'] = unittest.mock.Mock()
2297+
with self.assertRaises(AssertionError):
2298+
self.call_mgmt_func(b'admin.pool.Set.revisions_to_keep',
2299+
b'dom0', b'test-pool', b'abc')
2300+
self.assertEqual(self.app.pools['test-pool'].mock_calls, [])
2301+
self.assertFalse(self.app.save.called)
2302+
2303+
def test_670_vm_volume_set_revisions_to_keep(self):
2304+
self.vm.volumes = unittest.mock.MagicMock()
2305+
volumes_conf = {
2306+
'keys.return_value': ['root', 'private', 'volatile', 'kernel'],
2307+
}
2308+
self.vm.volumes.configure_mock(**volumes_conf)
2309+
self.vm.storage = unittest.mock.Mock()
2310+
value = self.call_mgmt_func(b'admin.vm.volume.Set.revisions_to_keep',
2311+
b'test-vm1', b'private', b'2')
2312+
self.assertIsNone(value)
2313+
self.assertEqual(self.vm.volumes.mock_calls,
2314+
[unittest.mock.call.keys(),
2315+
('__getitem__', ('private',), {})])
2316+
self.assertEqual(self.vm.volumes['private'].revisions_to_keep, 2)
2317+
self.app.save.assert_called_once_with()
2318+
2319+
def test_671_vm_volume_set_revisions_to_keep_negative(self):
2320+
self.vm.volumes = unittest.mock.MagicMock()
2321+
volumes_conf = {
2322+
'keys.return_value': ['root', 'private', 'volatile', 'kernel'],
2323+
}
2324+
self.vm.volumes.configure_mock(**volumes_conf)
2325+
self.vm.storage = unittest.mock.Mock()
2326+
with self.assertRaises(AssertionError):
2327+
self.call_mgmt_func(b'admin.vm.volume.Set.revisions_to_keep',
2328+
b'test-vm1', b'private', b'-2')
2329+
2330+
def test_672_vm_volume_set_revisions_to_keep_not_a_number(self):
2331+
self.vm.volumes = unittest.mock.MagicMock()
2332+
volumes_conf = {
2333+
'keys.return_value': ['root', 'private', 'volatile', 'kernel'],
2334+
}
2335+
self.vm.volumes.configure_mock(**volumes_conf)
2336+
self.vm.storage = unittest.mock.Mock()
2337+
with self.assertRaises(AssertionError):
2338+
self.call_mgmt_func(b'admin.vm.volume.Set.revisions_to_keep',
2339+
b'test-vm1', b'private', b'abc')
2340+
22782341
def test_990_vm_unexpected_payload(self):
22792342
methods_with_no_payload = [
22802343
b'admin.vm.List',

0 commit comments

Comments
 (0)