mgmt.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. #
  2. # The Qubes OS Project, https://www.qubes-os.org/
  3. #
  4. # Copyright (C) 2017 Wojtek Porczyk <woju@invisiblethingslab.com>
  5. #
  6. # This program is free software; you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation; either version 2 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License along
  17. # with this program; if not, write to the Free Software Foundation, Inc.,
  18. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  19. #
  20. '''
  21. Qubes OS Management API
  22. '''
  23. import asyncio
  24. import string
  25. import functools
  26. import qubes.vm.qubesvm
  27. import qubes.storage
  28. class ProtocolError(AssertionError):
  29. '''Raised when something is wrong with data received'''
  30. pass
  31. class PermissionDenied(Exception):
  32. '''Raised deliberately by handlers when we decide not to cooperate'''
  33. pass
  34. def not_in_api(func):
  35. '''Decorator for methods not intended to appear in API.
  36. The decorated method cannot be called from public API using
  37. :py:class:`QubesMgmt` class. The method becomes "private", and can be
  38. called only as a helper for other methods.
  39. '''
  40. func.not_in_api = True
  41. return func
  42. def no_payload(func):
  43. @functools.wraps(func)
  44. def wrapper(self, untrusted_payload):
  45. if untrusted_payload != b'':
  46. raise ProtocolError('unexpected payload')
  47. return func(self)
  48. return wrapper
  49. class QubesMgmt(object):
  50. '''Implementation of Qubes Management API calls
  51. This class contains all the methods available in the API.
  52. '''
  53. def __init__(self, app, src, method, dest, arg):
  54. #: :py:class:`qubes.Qubes` object
  55. self.app = app
  56. #: source qube
  57. self.src = self.app.domains[src.decode('ascii')]
  58. #: destination qube
  59. self.dest = self.app.domains[dest.decode('ascii')]
  60. #: argument
  61. self.arg = arg.decode('ascii')
  62. #: name of the method
  63. self.method = method.decode('ascii')
  64. untrusted_func_name = self.method
  65. if untrusted_func_name.startswith('mgmt.'):
  66. untrusted_func_name = untrusted_func_name[5:]
  67. untrusted_func_name = untrusted_func_name.lower().replace('.', '_')
  68. if untrusted_func_name.startswith('_') \
  69. or not '_' in untrusted_func_name:
  70. raise ProtocolError(
  71. 'possibly malicious function name: {!r}'.format(
  72. untrusted_func_name))
  73. try:
  74. untrusted_func = getattr(self, untrusted_func_name)
  75. except AttributeError:
  76. raise ProtocolError(
  77. 'no such attribute: {!r}'.format(
  78. untrusted_func_name))
  79. if not asyncio.iscoroutinefunction(untrusted_func):
  80. raise ProtocolError(
  81. 'no such method: {!r}'.format(
  82. untrusted_func_name))
  83. if getattr(untrusted_func, 'not_in_api', False):
  84. raise ProtocolError(
  85. 'attempt to call private method: {!r}'.format(
  86. untrusted_func_name))
  87. self.execute = untrusted_func
  88. del untrusted_func_name
  89. del untrusted_func
  90. #
  91. # PRIVATE METHODS, not to be called via RPC
  92. #
  93. @not_in_api
  94. def fire_event_for_permission(self, **kwargs):
  95. '''Fire an event on the source qube to check for permission'''
  96. return self.src.fire_event_pre('mgmt-permission:{}'.format(self.method),
  97. dest=self.dest, arg=self.arg, **kwargs)
  98. @not_in_api
  99. def fire_event_for_filter(self, iterable, **kwargs):
  100. '''Fire an event on the source qube to filter for permission'''
  101. for selector in self.fire_event_for_permission(**kwargs):
  102. iterable = filter(selector, iterable)
  103. return iterable
  104. #
  105. # ACTUAL RPC CALLS
  106. #
  107. @asyncio.coroutine
  108. @no_payload
  109. def vm_list(self):
  110. '''List all the domains'''
  111. assert not self.arg
  112. if self.dest.name == 'dom0':
  113. domains = self.fire_event_for_filter(self.app.domains)
  114. else:
  115. domains = self.fire_event_for_filter([self.dest])
  116. return ''.join('{} class={} state={}\n'.format(
  117. vm.name,
  118. vm.__class__.__name__,
  119. vm.get_power_state())
  120. for vm in sorted(domains))
  121. @asyncio.coroutine
  122. @no_payload
  123. def vm_property_list(self):
  124. '''List all properties on a qube'''
  125. assert not self.arg
  126. properties = self.fire_event_for_filter(self.dest.property_list())
  127. return ''.join('{}\n'.format(prop.__name__) for prop in properties)
  128. @asyncio.coroutine
  129. @no_payload
  130. def vm_property_get(self):
  131. '''Get a value of one property'''
  132. assert self.arg in self.dest.property_list()
  133. self.fire_event_for_permission()
  134. property_def = self.dest.property_get_def(self.arg)
  135. # explicit list to be sure that it matches protocol spec
  136. if isinstance(property_def, qubes.vm.VMProperty):
  137. property_type = 'vm'
  138. elif property_def.type is int:
  139. property_type = 'int'
  140. elif property_def.type is bool:
  141. property_type = 'bool'
  142. elif self.arg == 'label':
  143. property_type = 'label'
  144. else:
  145. property_type = 'str'
  146. try:
  147. value = getattr(self.dest, self.arg)
  148. except AttributeError:
  149. return 'default=True type={} '.format(property_type)
  150. else:
  151. return 'default={} type={} {}'.format(
  152. str(self.dest.property_is_default(self.arg)),
  153. property_type,
  154. str(value) if value is not None else '')
  155. @asyncio.coroutine
  156. def vm_property_set(self, untrusted_payload):
  157. assert self.arg in self.dest.property_list()
  158. property_def = self.dest.property_get_def(self.arg)
  159. newvalue = property_def.sanitize(untrusted_newvalue=untrusted_payload)
  160. self.fire_event_for_permission(newvalue=newvalue)
  161. setattr(self.dest, self.arg, newvalue)
  162. self.app.save()
  163. @asyncio.coroutine
  164. @no_payload
  165. def vm_property_help(self):
  166. '''Get help for one property'''
  167. assert self.arg in self.dest.property_list()
  168. self.fire_event_for_permission()
  169. try:
  170. doc = self.dest.property_get_def(self.arg).__doc__
  171. except AttributeError:
  172. return ''
  173. return qubes.utils.format_doc(doc)
  174. @asyncio.coroutine
  175. @no_payload
  176. def vm_property_reset(self):
  177. '''Reset a property to a default value'''
  178. assert self.arg in self.dest.property_list()
  179. self.fire_event_for_permission()
  180. delattr(self.dest, self.arg)
  181. self.app.save()
  182. @asyncio.coroutine
  183. @no_payload
  184. def vm_volume_list(self):
  185. assert not self.arg
  186. volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
  187. return ''.join('{}\n'.format(name) for name in volume_names)
  188. @asyncio.coroutine
  189. @no_payload
  190. def vm_volume_info(self):
  191. assert self.arg in self.dest.volumes.keys()
  192. self.fire_event_for_permission()
  193. volume = self.dest.volumes[self.arg]
  194. # properties defined in API
  195. volume_properties = [
  196. 'pool', 'vid', 'size', 'usage', 'rw', 'internal', 'source',
  197. 'save_on_stop', 'snap_on_start']
  198. return ''.join('{}={}\n'.format(key, getattr(volume, key)) for key in
  199. volume_properties)
  200. @asyncio.coroutine
  201. @no_payload
  202. def vm_volume_listsnapshots(self):
  203. assert self.arg in self.dest.volumes.keys()
  204. volume = self.dest.volumes[self.arg]
  205. revisions = [revision for revision in volume.revisions]
  206. revisions = self.fire_event_for_filter(revisions)
  207. return ''.join('{}\n'.format(revision) for revision in revisions)
  208. @asyncio.coroutine
  209. def vm_volume_revert(self, untrusted_payload):
  210. assert self.arg in self.dest.volumes.keys()
  211. untrusted_revision = untrusted_payload.decode('ascii').strip()
  212. del untrusted_payload
  213. volume = self.dest.volumes[self.arg]
  214. snapshots = volume.revisions
  215. assert untrusted_revision in snapshots
  216. revision = untrusted_revision
  217. self.fire_event_for_permission(revision=revision)
  218. self.dest.storage.get_pool(volume).revert(revision)
  219. self.app.save()
  220. @asyncio.coroutine
  221. def vm_volume_resize(self, untrusted_payload):
  222. assert self.arg in self.dest.volumes.keys()
  223. untrusted_size = untrusted_payload.decode('ascii').strip()
  224. del untrusted_payload
  225. assert untrusted_size.isdigit() # only digits, forbid '-' too
  226. assert len(untrusted_size) <= 20 # limit to about 2^64
  227. size = int(untrusted_size)
  228. self.fire_event_for_permission(size=size)
  229. self.dest.storage.resize(self.arg, size)
  230. self.app.save()
  231. @asyncio.coroutine
  232. @no_payload
  233. def pool_list(self):
  234. assert not self.arg
  235. assert self.dest.name == 'dom0'
  236. pools = self.fire_event_for_filter(self.app.pools)
  237. return ''.join('{}\n'.format(pool) for pool in pools)
  238. @asyncio.coroutine
  239. @no_payload
  240. def pool_listdrivers(self):
  241. assert self.dest.name == 'dom0'
  242. assert not self.arg
  243. drivers = self.fire_event_for_filter(qubes.storage.pool_drivers())
  244. return ''.join('{} {}\n'.format(
  245. driver,
  246. ' '.join(qubes.storage.driver_parameters(driver)))
  247. for driver in drivers)
  248. @asyncio.coroutine
  249. @no_payload
  250. def pool_info(self):
  251. assert self.dest.name == 'dom0'
  252. assert self.arg in self.app.pools.keys()
  253. pool = self.app.pools[self.arg]
  254. self.fire_event_for_permission(pool=pool)
  255. return ''.join('{}={}\n'.format(prop, val)
  256. for prop, val in sorted(pool.config.items()))
  257. @asyncio.coroutine
  258. def pool_add(self, untrusted_payload):
  259. assert self.dest.name == 'dom0'
  260. drivers = qubes.storage.pool_drivers()
  261. assert self.arg in drivers
  262. untrusted_pool_config = untrusted_payload.decode('ascii').splitlines()
  263. del untrusted_payload
  264. assert all(('=' in line) for line in untrusted_pool_config)
  265. # pairs of (option, value)
  266. untrusted_pool_config = [line.split('=', 1)
  267. for line in untrusted_pool_config]
  268. # reject duplicated options
  269. assert len(set(x[0] for x in untrusted_pool_config)) == \
  270. len([x[0] for x in untrusted_pool_config])
  271. # and convert to dict
  272. untrusted_pool_config = dict(untrusted_pool_config)
  273. assert 'name' in untrusted_pool_config
  274. untrusted_pool_name = untrusted_pool_config.pop('name')
  275. allowed_chars = string.ascii_letters + string.digits + '-_.'
  276. assert all(c in allowed_chars for c in untrusted_pool_name)
  277. pool_name = untrusted_pool_name
  278. assert pool_name not in self.app.pools
  279. driver_parameters = qubes.storage.driver_parameters(self.arg)
  280. assert all(key in driver_parameters for key in untrusted_pool_config)
  281. pool_config = untrusted_pool_config
  282. self.fire_event_for_permission(name=pool_name,
  283. pool_config=pool_config)
  284. self.app.add_pool(name=pool_name, driver=self.arg, **pool_config)
  285. self.app.save()
  286. @asyncio.coroutine
  287. @no_payload
  288. def pool_remove(self):
  289. assert self.dest.name == 'dom0'
  290. assert self.arg in self.app.pools.keys()
  291. self.fire_event_for_permission()
  292. self.app.remove_pool(self.arg)
  293. self.app.save()
  294. @asyncio.coroutine
  295. @no_payload
  296. def label_list(self):
  297. assert self.dest.name == 'dom0'
  298. assert not self.arg
  299. labels = self.fire_event_for_filter(self.app.labels.values())
  300. return ''.join('{}\n'.format(label.name) for label in labels)
  301. @asyncio.coroutine
  302. @no_payload
  303. def label_get(self):
  304. assert self.dest.name == 'dom0'
  305. try:
  306. label = self.app.get_label(self.arg)
  307. except KeyError:
  308. raise qubes.exc.QubesValueError
  309. self.fire_event_for_permission(label=label)
  310. return label.color
  311. @asyncio.coroutine
  312. def label_create(self, untrusted_payload):
  313. assert self.dest.name == 'dom0'
  314. # don't confuse label name with label index
  315. assert not self.arg.isdigit()
  316. allowed_chars = string.ascii_letters + string.digits + '-_.'
  317. assert all(c in allowed_chars for c in self.arg)
  318. try:
  319. self.app.get_label(self.arg)
  320. except KeyError:
  321. # ok, no such label yet
  322. pass
  323. else:
  324. raise qubes.exc.QubesValueError('label already exists')
  325. untrusted_payload = untrusted_payload.decode('ascii').strip()
  326. assert len(untrusted_payload) == 8
  327. assert untrusted_payload.startswith('0x')
  328. # besides prefix, only hex digits are allowed
  329. assert all(x in string.hexdigits for x in untrusted_payload[2:])
  330. # SEE: #2732
  331. color = untrusted_payload
  332. self.fire_event_for_permission(color=color)
  333. # allocate new index, but make sure it's outside of default labels set
  334. new_index = max(
  335. qubes.config.max_default_label, *self.app.labels.keys()) + 1
  336. label = qubes.Label(new_index, color, self.arg)
  337. self.app.labels[new_index] = label
  338. self.app.save()
  339. @asyncio.coroutine
  340. @no_payload
  341. def label_remove(self):
  342. assert self.dest.name == 'dom0'
  343. try:
  344. label = self.app.get_label(self.arg)
  345. except KeyError:
  346. raise qubes.exc.QubesValueError
  347. # don't allow removing default labels
  348. assert label.index > qubes.config.max_default_label
  349. # FIXME: this should be in app.add_label()
  350. for vm in self.app.domains:
  351. if vm.label == label:
  352. raise qubes.exc.QubesException('label still in use')
  353. self.fire_event_for_permission(label=label)
  354. del self.app.labels[label.index]
  355. self.app.save()