mgmt.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  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 reprlib
  25. import string
  26. import qubes.vm.qubesvm
  27. import qubes.storage
  28. class ProtocolRepr(reprlib.Repr):
  29. def repr1(self, x, level):
  30. if isinstance(x, qubes.vm.qubesvm.QubesVM):
  31. x = x.name
  32. return super().repr1(x, level)
  33. # pylint: disable=invalid-name
  34. def repr_str(self, x, level):
  35. '''Warning: this is incompatible with python 3 wrt to b'' '''
  36. return "'{}'".format(''.join(
  37. chr(c)
  38. if 0x20 < c < 0x7f and c not in (ord("'"), ord('\\'))
  39. else '\\x{:02x}'.format(c)
  40. for c in x.encode()))
  41. def repr_Label(self, x, level):
  42. return self.repr1(x.name, level)
  43. class ProtocolError(AssertionError):
  44. '''Raised when something is wrong with data received'''
  45. pass
  46. class PermissionDenied(Exception):
  47. '''Raised deliberately by handlers when we decide not to cooperate'''
  48. pass
  49. def not_in_api(func):
  50. func.not_in_api = True
  51. return func
  52. class QubesMgmt(object):
  53. def __init__(self, app, src, method, dest, arg):
  54. self.app = app
  55. self.src = self.app.domains[src.decode('ascii')]
  56. self.dest = self.app.domains[dest.decode('ascii')]
  57. self.arg = arg.decode('ascii')
  58. self.prepr = ProtocolRepr()
  59. self.method = method.decode('ascii')
  60. untrusted_func_name = self.method
  61. if untrusted_func_name.startswith('mgmt.'):
  62. untrusted_func_name = untrusted_func_name[5:]
  63. untrusted_func_name = untrusted_func_name.lower().replace('.', '_')
  64. if untrusted_func_name.startswith('_') \
  65. or not '_' in untrusted_func_name:
  66. raise ProtocolError(
  67. 'possibly malicious function name: {!r}'.format(
  68. untrusted_func_name))
  69. try:
  70. untrusted_func = getattr(self, untrusted_func_name)
  71. except AttributeError:
  72. raise ProtocolError(
  73. 'no such attribute: {!r}'.format(
  74. untrusted_func_name))
  75. if not asyncio.iscoroutinefunction(untrusted_func):
  76. raise ProtocolError(
  77. 'no such method: {!r}'.format(
  78. untrusted_func_name))
  79. if getattr(untrusted_func, 'not_in_api', False):
  80. raise ProtocolError(
  81. 'attempt to call private method: {!r}'.format(
  82. untrusted_func_name))
  83. self.execute = untrusted_func
  84. del untrusted_func_name
  85. del untrusted_func
  86. #
  87. # PRIVATE METHODS, not to be called via RPC
  88. #
  89. @not_in_api
  90. def fire_event_for_permission(self, **kwargs):
  91. return self.src.fire_event_pre('mgmt-permission:{}'.format(self.method),
  92. dest=self.dest, arg=self.arg, **kwargs)
  93. @not_in_api
  94. def fire_event_for_filter(self, iterable, **kwargs):
  95. for selector in self.fire_event_for_permission(**kwargs):
  96. iterable = filter(selector, iterable)
  97. return iterable
  98. @not_in_api
  99. def repr(self, *args, **kwargs):
  100. return self.prepr.repr(*args, **kwargs)
  101. #
  102. # ACTUAL RPC CALLS
  103. #
  104. @asyncio.coroutine
  105. def vm_list(self, untrusted_payload):
  106. assert not self.arg
  107. assert not untrusted_payload
  108. del untrusted_payload
  109. if self.dest.name == 'dom0':
  110. domains = self.fire_event_for_filter(self.app.domains)
  111. else:
  112. domains = self.fire_event_for_filter([self.dest])
  113. return ''.join('{} class={} state={}\n'.format(
  114. vm.name,
  115. vm.__class__.__name__,
  116. vm.get_power_state())
  117. for vm in sorted(domains))
  118. @asyncio.coroutine
  119. def vm_property_list(self, untrusted_payload):
  120. assert not self.arg
  121. assert not untrusted_payload
  122. del untrusted_payload
  123. properties = self.fire_event_for_filter(self.dest.property_list())
  124. return ''.join('{}\n'.format(prop.__name__) for prop in properties)
  125. @asyncio.coroutine
  126. def vm_property_get(self, untrusted_payload):
  127. assert self.arg in self.dest.property_list()
  128. assert not untrusted_payload
  129. del untrusted_payload
  130. self.fire_event_for_permission()
  131. property_def = self.dest.property_get_def(self.arg)
  132. # explicit list to be sure that it matches protocol spec
  133. if isinstance(property_def, qubes.vm.VMProperty):
  134. property_type = 'vm'
  135. elif property_def.type is int:
  136. property_type = 'int'
  137. elif property_def.type is bool:
  138. property_type = 'bool'
  139. elif self.arg == 'label':
  140. property_type = 'label'
  141. else:
  142. property_type = 'str'
  143. try:
  144. value = getattr(self.dest, self.arg)
  145. except AttributeError:
  146. return 'default=True type={} '.format(property_type)
  147. else:
  148. return 'default={} type={} {}'.format(
  149. str(self.dest.property_is_default(self.arg)),
  150. property_type,
  151. str(value) if value is not None else '')
  152. @asyncio.coroutine
  153. def vm_property_set(self, untrusted_payload):
  154. assert self.arg in self.dest.property_list()
  155. property_def = self.dest.property_get_def(self.arg)
  156. # do not treat type='str' as sufficient validation
  157. if isinstance(property_def, qubes.vm.VMProperty):
  158. try:
  159. untrusted_vmname = untrusted_payload.decode('ascii')
  160. except UnicodeDecodeError:
  161. raise qubes.exc.QubesValueError
  162. qubes.vm.qubesvm.validate_name(self.dest, property_def,
  163. untrusted_vmname)
  164. try:
  165. newvalue = self.app.domains[untrusted_vmname]
  166. except KeyError:
  167. raise qubes.exc.QubesPropertyValueError(
  168. self.dest, property_def, untrusted_vmname)
  169. elif property_def.type is not None and property_def.type is not str:
  170. if property_def.type is bool:
  171. try:
  172. untrusted_value = untrusted_payload.decode('ascii')
  173. except UnicodeDecodeError:
  174. raise qubes.exc.QubesValueError
  175. newvalue = qubes.property.bool(None, None, untrusted_value)
  176. else:
  177. try:
  178. newvalue = property_def.type(untrusted_payload)
  179. except ValueError:
  180. raise qubes.exc.QubesValueError
  181. else:
  182. # other types (including explicitly defined 'str') coerce to str
  183. # with limited characters allowed
  184. try:
  185. untrusted_value = untrusted_payload.decode('ascii')
  186. except UnicodeDecodeError:
  187. raise qubes.exc.QubesValueError
  188. allowed_set = string.printable
  189. if not all(x in allowed_set for x in untrusted_value):
  190. raise qubes.exc.QubesValueError(
  191. 'Invalid characters in property value')
  192. newvalue = untrusted_value
  193. self.fire_event_for_permission(newvalue=newvalue)
  194. setattr(self.dest, self.arg, newvalue)
  195. self.app.save()
  196. @asyncio.coroutine
  197. def vm_property_help(self, untrusted_payload):
  198. assert self.arg in self.dest.property_list()
  199. assert not untrusted_payload
  200. del untrusted_payload
  201. self.fire_event_for_permission()
  202. try:
  203. doc = self.dest.property_get_def(self.arg).__doc__
  204. except AttributeError:
  205. return ''
  206. return qubes.utils.format_doc(doc)
  207. @asyncio.coroutine
  208. def vm_property_reset(self, untrusted_payload):
  209. assert self.arg in self.dest.property_list()
  210. assert not untrusted_payload
  211. del untrusted_payload
  212. self.fire_event_for_permission()
  213. delattr(self.dest, self.arg)
  214. self.app.save()
  215. @asyncio.coroutine
  216. def vm_volume_list(self, untrusted_payload):
  217. assert not self.arg
  218. assert not untrusted_payload
  219. del untrusted_payload
  220. volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
  221. return ''.join('{}\n'.format(name) for name in volume_names)
  222. @asyncio.coroutine
  223. def vm_volume_info(self, untrusted_payload):
  224. assert self.arg in self.dest.volumes.keys()
  225. assert not untrusted_payload
  226. del untrusted_payload
  227. self.fire_event_for_permission()
  228. volume = self.dest.volumes[self.arg]
  229. # properties defined in API
  230. volume_properties = [
  231. 'pool', 'vid', 'size', 'usage', 'rw', 'internal', 'source',
  232. 'save_on_stop', 'snap_on_start']
  233. return ''.join('{}={}\n'.format(key, getattr(volume, key)) for key in
  234. volume_properties)
  235. @asyncio.coroutine
  236. def vm_volume_listsnapshots(self, untrusted_payload):
  237. assert self.arg in self.dest.volumes.keys()
  238. assert not untrusted_payload
  239. del untrusted_payload
  240. self.fire_event_for_permission()
  241. volume = self.dest.volumes[self.arg]
  242. return ''.join('{}\n'.format(revision) for revision in volume.revisions)
  243. @asyncio.coroutine
  244. def vm_volume_revert(self, untrusted_payload):
  245. assert self.arg in self.dest.volumes.keys()
  246. untrusted_revision = untrusted_payload.decode('ascii').strip()
  247. del untrusted_payload
  248. volume = self.dest.volumes[self.arg]
  249. snapshots = volume.revisions
  250. assert untrusted_revision in snapshots
  251. revision = untrusted_revision
  252. self.fire_event_for_permission(revision=revision)
  253. self.dest.storage.get_pool(volume).revert(revision)
  254. self.app.save()
  255. @asyncio.coroutine
  256. def vm_volume_resize(self, untrusted_payload):
  257. assert self.arg in self.dest.volumes.keys()
  258. untrusted_size = untrusted_payload.decode('ascii').strip()
  259. del untrusted_payload
  260. assert untrusted_size.isdigit() # only digits, forbid '-' too
  261. assert len(untrusted_size) <= 20 # limit to about 2^64
  262. size = int(untrusted_size)
  263. self.fire_event_for_permission(size=size)
  264. self.dest.storage.resize(self.arg, size)
  265. self.app.save()
  266. @asyncio.coroutine
  267. def pool_list(self, untrusted_payload):
  268. assert not self.arg
  269. assert self.dest.name == 'dom0'
  270. assert not untrusted_payload
  271. del untrusted_payload
  272. pools = self.fire_event_for_filter(self.app.pools)
  273. return ''.join('{}\n'.format(pool) for pool in pools)
  274. @asyncio.coroutine
  275. def pool_listdrivers(self, untrusted_payload):
  276. assert self.dest.name == 'dom0'
  277. assert not self.arg
  278. assert not untrusted_payload
  279. del untrusted_payload
  280. drivers = self.fire_event_for_filter(qubes.storage.pool_drivers())
  281. return ''.join('{} {}\n'.format(
  282. driver,
  283. ' '.join(qubes.storage.driver_parameters(driver)))
  284. for driver in drivers)
  285. @asyncio.coroutine
  286. def pool_info(self, untrusted_payload):
  287. assert self.dest.name == 'dom0'
  288. assert self.arg in self.app.pools.keys()
  289. assert not untrusted_payload
  290. del untrusted_payload
  291. pool = self.app.pools[self.arg]
  292. self.fire_event_for_permission(pool=pool)
  293. return ''.join('{}={}\n'.format(prop, val)
  294. for prop, val in sorted(pool.config.items()))
  295. @asyncio.coroutine
  296. def pool_add(self, untrusted_payload):
  297. assert self.dest.name == 'dom0'
  298. drivers = qubes.storage.pool_drivers()
  299. assert self.arg in drivers
  300. untrusted_pool_config = untrusted_payload.decode('ascii').splitlines()
  301. del untrusted_payload
  302. assert all(('=' in line) for line in untrusted_pool_config)
  303. # pairs of (option, value)
  304. untrusted_pool_config = [line.split('=', 1)
  305. for line in untrusted_pool_config]
  306. # reject duplicated options
  307. assert len(set(x[0] for x in untrusted_pool_config)) == \
  308. len([x[0] for x in untrusted_pool_config])
  309. # and convert to dict
  310. untrusted_pool_config = dict(untrusted_pool_config)
  311. assert 'name' in untrusted_pool_config
  312. untrusted_pool_name = untrusted_pool_config.pop('name')
  313. allowed_chars = string.ascii_letters + string.digits + '-_.'
  314. assert all(c in allowed_chars for c in untrusted_pool_name)
  315. pool_name = untrusted_pool_name
  316. assert pool_name not in self.app.pools
  317. driver_parameters = qubes.storage.driver_parameters(self.arg)
  318. assert all(key in driver_parameters for key in untrusted_pool_config)
  319. # option names validated, validation of option values is delegated to
  320. # extension (through events mechanism)
  321. self.fire_event_for_permission(name=pool_name,
  322. untrusted_pool_config=untrusted_pool_config)
  323. pool_config = untrusted_pool_config
  324. self.app.add_pool(name=pool_name, driver=self.arg, **pool_config)
  325. self.app.save()
  326. @asyncio.coroutine
  327. def pool_remove(self, untrusted_payload):
  328. assert self.dest.name == 'dom0'
  329. assert self.arg in self.app.pools.keys()
  330. assert not untrusted_payload
  331. del untrusted_payload
  332. self.fire_event_for_permission()
  333. self.app.remove_pool(self.arg)
  334. self.app.save()
  335. @asyncio.coroutine
  336. def label_list(self, untrusted_payload):
  337. assert self.dest.name == 'dom0'
  338. assert not self.arg
  339. assert not untrusted_payload
  340. del untrusted_payload
  341. labels = self.fire_event_for_filter(self.app.labels.values())
  342. return ''.join('{}\n'.format(label.name) for label in labels)
  343. @asyncio.coroutine
  344. def label_get(self, untrusted_payload):
  345. assert self.dest.name == 'dom0'
  346. assert not untrusted_payload
  347. del untrusted_payload
  348. try:
  349. label = self.app.get_label(self.arg)
  350. except KeyError:
  351. raise qubes.exc.QubesValueError
  352. self.fire_event_for_permission(label=label)
  353. return label.color
  354. @asyncio.coroutine
  355. def label_create(self, untrusted_payload):
  356. assert self.dest.name == 'dom0'
  357. # don't confuse label name with label index
  358. assert not self.arg.isdigit()
  359. allowed_chars = string.ascii_letters + string.digits + '-_.'
  360. assert all(c in allowed_chars for c in self.arg)
  361. try:
  362. self.app.get_label(self.arg)
  363. except KeyError:
  364. # ok, no such label yet
  365. pass
  366. else:
  367. raise qubes.exc.QubesValueError('label already exists')
  368. untrusted_payload = untrusted_payload.decode('ascii').strip()
  369. assert len(untrusted_payload) == 8
  370. assert untrusted_payload.startswith('0x')
  371. # besides prefix, only hex digits are allowed
  372. assert all(x in string.hexdigits for x in untrusted_payload[2:])
  373. # TODO: try to avoid creating label too similar to existing one?
  374. color = untrusted_payload
  375. self.fire_event_for_permission(color=color)
  376. # allocate new index, but make sure it's outside of default labels set
  377. new_index = max(
  378. qubes.config.max_default_label, *self.app.labels.keys()) + 1
  379. label = qubes.Label(new_index, color, self.arg)
  380. self.app.labels[new_index] = label
  381. self.app.save()
  382. @asyncio.coroutine
  383. def label_remove(self, untrusted_payload):
  384. assert self.dest.name == 'dom0'
  385. assert not untrusted_payload
  386. del untrusted_payload
  387. try:
  388. label = self.app.get_label(self.arg)
  389. except KeyError:
  390. raise qubes.exc.QubesValueError
  391. # don't allow removing default labels
  392. assert label.index > qubes.config.max_default_label
  393. # FIXME: this should be in app.add_label()
  394. for vm in self.app.domains:
  395. if vm.label == label:
  396. raise qubes.exc.QubesException('label still in use')
  397. self.fire_event_for_permission(label=label)
  398. del self.app.labels[label.index]
  399. self.app.save()