mgmt.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  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. func.not_in_api = True
  36. return func
  37. def no_payload(func):
  38. @functools.wraps(func)
  39. def wrapper(self, untrusted_payload):
  40. if untrusted_payload != b'':
  41. raise ProtocolError('unexpected payload')
  42. return func(self)
  43. return wrapper
  44. class QubesMgmt(object):
  45. def __init__(self, app, src, method, dest, arg):
  46. self.app = app
  47. self.src = self.app.domains[src.decode('ascii')]
  48. self.dest = self.app.domains[dest.decode('ascii')]
  49. self.arg = arg.decode('ascii')
  50. self.method = method.decode('ascii')
  51. untrusted_func_name = self.method
  52. if untrusted_func_name.startswith('mgmt.'):
  53. untrusted_func_name = untrusted_func_name[5:]
  54. untrusted_func_name = untrusted_func_name.lower().replace('.', '_')
  55. if untrusted_func_name.startswith('_') \
  56. or not '_' in untrusted_func_name:
  57. raise ProtocolError(
  58. 'possibly malicious function name: {!r}'.format(
  59. untrusted_func_name))
  60. try:
  61. untrusted_func = getattr(self, untrusted_func_name)
  62. except AttributeError:
  63. raise ProtocolError(
  64. 'no such attribute: {!r}'.format(
  65. untrusted_func_name))
  66. if not asyncio.iscoroutinefunction(untrusted_func):
  67. raise ProtocolError(
  68. 'no such method: {!r}'.format(
  69. untrusted_func_name))
  70. if getattr(untrusted_func, 'not_in_api', False):
  71. raise ProtocolError(
  72. 'attempt to call private method: {!r}'.format(
  73. untrusted_func_name))
  74. self.execute = untrusted_func
  75. del untrusted_func_name
  76. del untrusted_func
  77. #
  78. # PRIVATE METHODS, not to be called via RPC
  79. #
  80. @not_in_api
  81. def fire_event_for_permission(self, **kwargs):
  82. return self.src.fire_event_pre('mgmt-permission:{}'.format(self.method),
  83. dest=self.dest, arg=self.arg, **kwargs)
  84. @not_in_api
  85. def fire_event_for_filter(self, iterable, **kwargs):
  86. for selector in self.fire_event_for_permission(**kwargs):
  87. iterable = filter(selector, iterable)
  88. return iterable
  89. #
  90. # ACTUAL RPC CALLS
  91. #
  92. @asyncio.coroutine
  93. @no_payload
  94. def vm_list(self):
  95. assert not self.arg
  96. if self.dest.name == 'dom0':
  97. domains = self.fire_event_for_filter(self.app.domains)
  98. else:
  99. domains = self.fire_event_for_filter([self.dest])
  100. return ''.join('{} class={} state={}\n'.format(
  101. vm.name,
  102. vm.__class__.__name__,
  103. vm.get_power_state())
  104. for vm in sorted(domains))
  105. @asyncio.coroutine
  106. @no_payload
  107. def vm_property_list(self):
  108. assert not self.arg
  109. properties = self.fire_event_for_filter(self.dest.property_list())
  110. return ''.join('{}\n'.format(prop.__name__) for prop in properties)
  111. @asyncio.coroutine
  112. @no_payload
  113. def vm_property_get(self):
  114. assert self.arg in self.dest.property_list()
  115. self.fire_event_for_permission()
  116. property_def = self.dest.property_get_def(self.arg)
  117. # explicit list to be sure that it matches protocol spec
  118. if isinstance(property_def, qubes.vm.VMProperty):
  119. property_type = 'vm'
  120. elif property_def.type is int:
  121. property_type = 'int'
  122. elif property_def.type is bool:
  123. property_type = 'bool'
  124. elif self.arg == 'label':
  125. property_type = 'label'
  126. else:
  127. property_type = 'str'
  128. try:
  129. value = getattr(self.dest, self.arg)
  130. except AttributeError:
  131. return 'default=True type={} '.format(property_type)
  132. else:
  133. return 'default={} type={} {}'.format(
  134. str(self.dest.property_is_default(self.arg)),
  135. property_type,
  136. str(value) if value is not None else '')
  137. @asyncio.coroutine
  138. def vm_property_set(self, untrusted_payload):
  139. assert self.arg in self.dest.property_list()
  140. property_def = self.dest.property_get_def(self.arg)
  141. newvalue = property_def.sanitize(untrusted_newvalue=untrusted_payload)
  142. self.fire_event_for_permission(newvalue=newvalue)
  143. setattr(self.dest, self.arg, newvalue)
  144. self.app.save()
  145. @asyncio.coroutine
  146. @no_payload
  147. def vm_property_help(self):
  148. assert self.arg in self.dest.property_list()
  149. self.fire_event_for_permission()
  150. try:
  151. doc = self.dest.property_get_def(self.arg).__doc__
  152. except AttributeError:
  153. return ''
  154. return qubes.utils.format_doc(doc)
  155. @asyncio.coroutine
  156. @no_payload
  157. def vm_property_reset(self):
  158. assert self.arg in self.dest.property_list()
  159. self.fire_event_for_permission()
  160. delattr(self.dest, self.arg)
  161. self.app.save()
  162. @asyncio.coroutine
  163. @no_payload
  164. def vm_volume_list(self):
  165. assert not self.arg
  166. volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
  167. return ''.join('{}\n'.format(name) for name in volume_names)
  168. @asyncio.coroutine
  169. @no_payload
  170. def vm_volume_info(self):
  171. assert self.arg in self.dest.volumes.keys()
  172. self.fire_event_for_permission()
  173. volume = self.dest.volumes[self.arg]
  174. # properties defined in API
  175. volume_properties = [
  176. 'pool', 'vid', 'size', 'usage', 'rw', 'internal', 'source',
  177. 'save_on_stop', 'snap_on_start']
  178. return ''.join('{}={}\n'.format(key, getattr(volume, key)) for key in
  179. volume_properties)
  180. @asyncio.coroutine
  181. @no_payload
  182. def vm_volume_listsnapshots(self):
  183. assert self.arg in self.dest.volumes.keys()
  184. volume = self.dest.volumes[self.arg]
  185. revisions = [revision for revision in volume.revisions]
  186. revisions = self.fire_event_for_filter(revisions)
  187. return ''.join('{}\n'.format(revision) for revision in revisions)
  188. @asyncio.coroutine
  189. def vm_volume_revert(self, untrusted_payload):
  190. assert self.arg in self.dest.volumes.keys()
  191. untrusted_revision = untrusted_payload.decode('ascii').strip()
  192. del untrusted_payload
  193. volume = self.dest.volumes[self.arg]
  194. snapshots = volume.revisions
  195. assert untrusted_revision in snapshots
  196. revision = untrusted_revision
  197. self.fire_event_for_permission(revision=revision)
  198. self.dest.storage.get_pool(volume).revert(revision)
  199. self.app.save()
  200. @asyncio.coroutine
  201. def vm_volume_resize(self, untrusted_payload):
  202. assert self.arg in self.dest.volumes.keys()
  203. untrusted_size = untrusted_payload.decode('ascii').strip()
  204. del untrusted_payload
  205. assert untrusted_size.isdigit() # only digits, forbid '-' too
  206. assert len(untrusted_size) <= 20 # limit to about 2^64
  207. size = int(untrusted_size)
  208. self.fire_event_for_permission(size=size)
  209. self.dest.storage.resize(self.arg, size)
  210. self.app.save()
  211. @asyncio.coroutine
  212. @no_payload
  213. def pool_list(self):
  214. assert not self.arg
  215. assert self.dest.name == 'dom0'
  216. pools = self.fire_event_for_filter(self.app.pools)
  217. return ''.join('{}\n'.format(pool) for pool in pools)
  218. @asyncio.coroutine
  219. @no_payload
  220. def pool_listdrivers(self):
  221. assert self.dest.name == 'dom0'
  222. assert not self.arg
  223. drivers = self.fire_event_for_filter(qubes.storage.pool_drivers())
  224. return ''.join('{} {}\n'.format(
  225. driver,
  226. ' '.join(qubes.storage.driver_parameters(driver)))
  227. for driver in drivers)
  228. @asyncio.coroutine
  229. @no_payload
  230. def pool_info(self):
  231. assert self.dest.name == 'dom0'
  232. assert self.arg in self.app.pools.keys()
  233. pool = self.app.pools[self.arg]
  234. self.fire_event_for_permission(pool=pool)
  235. return ''.join('{}={}\n'.format(prop, val)
  236. for prop, val in sorted(pool.config.items()))
  237. @asyncio.coroutine
  238. def pool_add(self, untrusted_payload):
  239. assert self.dest.name == 'dom0'
  240. drivers = qubes.storage.pool_drivers()
  241. assert self.arg in drivers
  242. untrusted_pool_config = untrusted_payload.decode('ascii').splitlines()
  243. del untrusted_payload
  244. assert all(('=' in line) for line in untrusted_pool_config)
  245. # pairs of (option, value)
  246. untrusted_pool_config = [line.split('=', 1)
  247. for line in untrusted_pool_config]
  248. # reject duplicated options
  249. assert len(set(x[0] for x in untrusted_pool_config)) == \
  250. len([x[0] for x in untrusted_pool_config])
  251. # and convert to dict
  252. untrusted_pool_config = dict(untrusted_pool_config)
  253. assert 'name' in untrusted_pool_config
  254. untrusted_pool_name = untrusted_pool_config.pop('name')
  255. allowed_chars = string.ascii_letters + string.digits + '-_.'
  256. assert all(c in allowed_chars for c in untrusted_pool_name)
  257. pool_name = untrusted_pool_name
  258. assert pool_name not in self.app.pools
  259. driver_parameters = qubes.storage.driver_parameters(self.arg)
  260. assert all(key in driver_parameters for key in untrusted_pool_config)
  261. pool_config = untrusted_pool_config
  262. self.fire_event_for_permission(name=pool_name,
  263. pool_config=pool_config)
  264. self.app.add_pool(name=pool_name, driver=self.arg, **pool_config)
  265. self.app.save()
  266. @asyncio.coroutine
  267. @no_payload
  268. def pool_remove(self):
  269. assert self.dest.name == 'dom0'
  270. assert self.arg in self.app.pools.keys()
  271. self.fire_event_for_permission()
  272. self.app.remove_pool(self.arg)
  273. self.app.save()
  274. @asyncio.coroutine
  275. @no_payload
  276. def label_list(self):
  277. assert self.dest.name == 'dom0'
  278. assert not self.arg
  279. labels = self.fire_event_for_filter(self.app.labels.values())
  280. return ''.join('{}\n'.format(label.name) for label in labels)
  281. @asyncio.coroutine
  282. @no_payload
  283. def label_get(self):
  284. assert self.dest.name == 'dom0'
  285. try:
  286. label = self.app.get_label(self.arg)
  287. except KeyError:
  288. raise qubes.exc.QubesValueError
  289. self.fire_event_for_permission(label=label)
  290. return label.color
  291. @asyncio.coroutine
  292. def label_create(self, untrusted_payload):
  293. assert self.dest.name == 'dom0'
  294. # don't confuse label name with label index
  295. assert not self.arg.isdigit()
  296. allowed_chars = string.ascii_letters + string.digits + '-_.'
  297. assert all(c in allowed_chars for c in self.arg)
  298. try:
  299. self.app.get_label(self.arg)
  300. except KeyError:
  301. # ok, no such label yet
  302. pass
  303. else:
  304. raise qubes.exc.QubesValueError('label already exists')
  305. untrusted_payload = untrusted_payload.decode('ascii').strip()
  306. assert len(untrusted_payload) == 8
  307. assert untrusted_payload.startswith('0x')
  308. # besides prefix, only hex digits are allowed
  309. assert all(x in string.hexdigits for x in untrusted_payload[2:])
  310. # SEE: #2732
  311. color = untrusted_payload
  312. self.fire_event_for_permission(color=color)
  313. # allocate new index, but make sure it's outside of default labels set
  314. new_index = max(
  315. qubes.config.max_default_label, *self.app.labels.keys()) + 1
  316. label = qubes.Label(new_index, color, self.arg)
  317. self.app.labels[new_index] = label
  318. self.app.save()
  319. @asyncio.coroutine
  320. @no_payload
  321. def label_remove(self):
  322. assert self.dest.name == 'dom0'
  323. try:
  324. label = self.app.get_label(self.arg)
  325. except KeyError:
  326. raise qubes.exc.QubesValueError
  327. # don't allow removing default labels
  328. assert label.index > qubes.config.max_default_label
  329. # FIXME: this should be in app.add_label()
  330. for vm in self.app.domains:
  331. if vm.label == label:
  332. raise qubes.exc.QubesException('label still in use')
  333. self.fire_event_for_permission(label=label)
  334. del self.app.labels[label.index]
  335. self.app.save()