__init__.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765
  1. # coding=utf-8
  2. # The Qubes OS Project, https://www.qubes-os.org/
  3. #
  4. # Copyright (C) 2013-2015 Joanna Rutkowska <joanna@invisiblethingslab.com>
  5. # Copyright (C) 2013-2017 Marek Marczykowski-Górecki
  6. # <marmarek@invisiblethingslab.com>
  7. #
  8. # This library is free software; you can redistribute it and/or
  9. # modify it under the terms of the GNU Lesser General Public
  10. # License as published by the Free Software Foundation; either
  11. # version 2.1 of the License, or (at your option) any later version.
  12. #
  13. # This library is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  16. # Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public
  19. # License along with this library; if not, see <https://www.gnu.org/licenses/>.
  20. # pylint: disable=no-else-return,useless-object-inheritance,try-except-raise
  21. ''' Qrexec policy parser and evaluator '''
  22. import enum
  23. import itertools
  24. import json
  25. import os
  26. import os.path
  27. import socket
  28. import subprocess
  29. # don't import 'qubes.config' please, it takes 0.3s
  30. QREXEC_CLIENT = '/usr/lib/qubes/qrexec-client'
  31. POLICY_DIR = '/etc/qubes-rpc/policy'
  32. QUBESD_INTERNAL_SOCK = '/var/run/qubesd.internal.sock'
  33. QUBESD_SOCK = '/var/run/qubesd.sock'
  34. class AccessDenied(Exception):
  35. ''' Raised when qrexec policy denied access '''
  36. class PolicySyntaxError(AccessDenied):
  37. ''' Syntax error in qrexec policy, abort parsing '''
  38. def __init__(self, filename, lineno, msg):
  39. super(PolicySyntaxError, self).__init__(
  40. '{}:{}: {}'.format(filename, lineno, msg))
  41. class PolicyNotFound(AccessDenied):
  42. ''' Policy was not found for this service '''
  43. def __init__(self, service_name):
  44. super(PolicyNotFound, self).__init__(
  45. 'Policy not found for service {}'.format(service_name))
  46. class Action(enum.Enum):
  47. ''' Action as defined by policy '''
  48. allow = 1
  49. deny = 2
  50. ask = 3
  51. def is_special_value(value):
  52. '''Check if given source/target specification is special (keyword) value
  53. '''
  54. return value.startswith('@')
  55. def verify_target_value(system_info, value):
  56. ''' Check if given value names valid target
  57. This function check if given value is not only syntactically correct,
  58. but also if names valid service call target (existing domain,
  59. or valid @dispvm like keyword)
  60. :param system_info: information about the system
  61. :param value: value to be checked
  62. '''
  63. if value == '@dispvm':
  64. return True
  65. elif value == '@adminvm':
  66. return True
  67. elif value.startswith('@dispvm:'):
  68. dispvm_base = value.split(':', 1)[1]
  69. if dispvm_base not in system_info['domains']:
  70. return False
  71. dispvm_base_info = system_info['domains'][dispvm_base]
  72. return bool(dispvm_base_info['template_for_dispvms'])
  73. else:
  74. return value in system_info['domains']
  75. def verify_special_value(value, for_target=True, specific_target=False):
  76. '''
  77. Verify if given special VM-specifier ('@...') is valid
  78. :param value: value to verify
  79. :param for_target: should classify target-only values as valid (
  80. '@default', '@dispvm')
  81. :param specific_target: allow only values naming specific target
  82. (for use with target=, default= etc)
  83. :return: True or False
  84. '''
  85. # pylint: disable=too-many-return-statements
  86. # values used only for matching VMs, not naming specific one (for actual
  87. # call target)
  88. if not specific_target:
  89. if value.startswith('@tag:') and len(value) > len('@tag:'):
  90. return True
  91. if value.startswith('@type:') and len(value) > len('@type:'):
  92. return True
  93. if for_target and value.startswith('@dispvm:@tag:') and \
  94. len(value) > len('@dispvm:@tag:'):
  95. return True
  96. if value == '@anyvm':
  97. return True
  98. if for_target and value == '@default':
  99. return True
  100. # those can be used to name one specific call VM
  101. if value == '@adminvm':
  102. return True
  103. # allow only specific dispvm, not based on any @xxx keyword - don't name
  104. # @tag here specifically, to work also with any future keywords
  105. if for_target and value.startswith('@dispvm:') and \
  106. not value.startswith('@dispvm:@'):
  107. return True
  108. if for_target and value == '@dispvm':
  109. return True
  110. return False
  111. class PolicyRule(object):
  112. ''' A single line of policy file '''
  113. def __init__(self, line, filename=None, lineno=None):
  114. '''
  115. Load a single line of qrexec policy and check its syntax.
  116. Do not verify existence of named objects.
  117. :raise PolicySyntaxError: when syntax error is found
  118. :param line: a single line of actual qrexec policy (not a comment,
  119. empty line or @include)
  120. :param filename: name of the file from which this line is loaded
  121. :param lineno: line number from which this line is loaded
  122. '''
  123. self.lineno = lineno
  124. self.filename = filename
  125. try:
  126. self.source, self.target, self.full_action = line.split(maxsplit=2)
  127. except ValueError:
  128. raise PolicySyntaxError(filename, lineno, 'wrong number of fields')
  129. (action, *params) = self.full_action.replace(',', ' ').split()
  130. try:
  131. self.action = Action[action]
  132. except KeyError:
  133. raise PolicySyntaxError(filename, lineno,
  134. 'invalid action: {}'.format(action))
  135. #: alternative target, used instead of the one specified by the caller
  136. self.override_target = None
  137. #: alternative user, used instead of vm.default_user
  138. self.override_user = None
  139. #: default target when asking the user for confirmation
  140. self.default_target = None
  141. for param in params:
  142. try:
  143. param_name, value = param.split('=')
  144. except ValueError:
  145. raise PolicySyntaxError(filename, lineno,
  146. 'invalid action parameter syntax: {}'.format(param))
  147. if param_name == 'target':
  148. if self.action == Action.deny:
  149. raise PolicySyntaxError(filename, lineno,
  150. 'target= option not allowed for deny action')
  151. self.override_target = value
  152. elif param_name == 'user':
  153. if self.action == Action.deny:
  154. raise PolicySyntaxError(filename, lineno,
  155. 'user= option not allowed for deny action')
  156. self.override_user = value
  157. elif param_name == 'default_target':
  158. if self.action != Action.ask:
  159. raise PolicySyntaxError(filename, lineno,
  160. 'default_target= option allowed only for ask action')
  161. self.default_target = value
  162. else:
  163. raise PolicySyntaxError(filename, lineno,
  164. 'invalid option {} for {} action'.format(param, action))
  165. # verify special values
  166. if is_special_value(self.source):
  167. if not verify_special_value(self.source, False, False):
  168. raise PolicySyntaxError(filename, lineno,
  169. 'invalid source specification: {}'.format(self.source))
  170. if is_special_value(self.target):
  171. if not verify_special_value(self.target, True, False):
  172. raise PolicySyntaxError(filename, lineno,
  173. 'invalid target specification: {}'.format(self.target))
  174. if self.target == '@default' \
  175. and self.action == Action.allow \
  176. and self.override_target is None:
  177. raise PolicySyntaxError(filename, lineno,
  178. 'allow action for @default rule must specify target= option')
  179. if self.override_target is not None:
  180. if is_special_value(self.override_target) and \
  181. not verify_special_value(self.override_target, True, True):
  182. raise PolicySyntaxError(filename, lineno,
  183. 'target= option needs to name specific target')
  184. if self.default_target is not None:
  185. if is_special_value(self.default_target) and \
  186. not verify_special_value(self.default_target, True, True):
  187. raise PolicySyntaxError(filename, lineno,
  188. 'target= option needs to name specific target')
  189. @staticmethod
  190. def is_match_single(system_info, policy_value, value):
  191. '''
  192. Evaluate if a single value (VM name or '@default') matches policy
  193. specification
  194. :param system_info: information about the system
  195. :param policy_value: value from qrexec policy (either self.source or
  196. self.target)
  197. :param value: value to be compared (source or target)
  198. :return: True or False
  199. '''
  200. # pylint: disable=too-many-return-statements
  201. # not specified target matches only with @default and @anyvm policy
  202. # entry
  203. if value == '@default':
  204. return policy_value in ('@default', '@anyvm')
  205. # if specific target used, check if it's valid
  206. # this function (is_match_single) is also used for checking call source
  207. # values, but this isn't a problem, because it will always be a
  208. # domain name (not @dispvm or such) - this is guaranteed by a nature
  209. # of qrexec call
  210. if not verify_target_value(system_info, value):
  211. return False
  212. # handle @adminvm keyword
  213. if policy_value == 'dom0':
  214. # TODO: log a warning in Qubes 4.1
  215. policy_value = '@adminvm'
  216. if value == 'dom0':
  217. value = '@adminvm'
  218. # allow any _valid_, non-dom0 target
  219. if policy_value == '@anyvm':
  220. return value != '@adminvm'
  221. # exact match, including @dispvm* and @adminvm
  222. if value == policy_value:
  223. return True
  224. # DispVM request, using tags to match
  225. if policy_value.startswith('@dispvm:@tag:') \
  226. and value.startswith('@dispvm:'):
  227. tag = policy_value.split(':', 2)[2]
  228. dispvm_base = value.split(':', 1)[1]
  229. # already checked for existence by verify_target_value call
  230. dispvm_base_info = system_info['domains'][dispvm_base]
  231. return tag in dispvm_base_info['tags']
  232. # if @dispvm* not matched above, reject it; default DispVM (bare
  233. # @dispvm) was resolved by the caller
  234. if value.startswith('@dispvm:'):
  235. return False
  236. # require @adminvm to be matched explicitly (not through @tag or @type)
  237. # - if not matched already, reject it
  238. if value == '@adminvm':
  239. return False
  240. # at this point, value name a specific target
  241. domain_info = system_info['domains'][value]
  242. if policy_value.startswith('@tag:'):
  243. tag = policy_value.split(':', 1)[1]
  244. return tag in domain_info['tags']
  245. if policy_value.startswith('@type:'):
  246. type_ = policy_value.split(':', 1)[1]
  247. return type_ == domain_info['type']
  248. return False
  249. def is_match(self, system_info, source, target):
  250. '''
  251. Check if given (source, target) matches this policy line.
  252. :param system_info: information about the system - available VMs,
  253. their types, labels, tags etc. as returned by
  254. :py:func:`app_to_system_info`
  255. :param source: name of the source VM
  256. :param target: name of the target VM, or None if not specified
  257. :return: True or False
  258. '''
  259. if not self.is_match_single(system_info, self.source, source):
  260. return False
  261. # @dispvm in policy matches _only_ @dispvm (but not @dispvm:some-vm,
  262. # even if that would be the default one)
  263. if self.target == '@dispvm' and target == '@dispvm':
  264. return True
  265. if target == '@dispvm':
  266. # resolve default DispVM, to check all kinds of @dispvm:*
  267. default_dispvm = system_info['domains'][source]['default_dispvm']
  268. if default_dispvm is None:
  269. # if this VM have no default DispVM, match only with @anyvm
  270. return self.target == '@anyvm'
  271. target = '@dispvm:' + default_dispvm
  272. if not self.is_match_single(system_info, self.target, target):
  273. return False
  274. return True
  275. def expand_target(self, system_info):
  276. '''
  277. Return domains matching target of this policy line
  278. :param system_info: information about the system
  279. :return: matching domains
  280. '''
  281. if self.target.startswith('@tag:'):
  282. tag = self.target.split(':', 1)[1]
  283. for name, domain in system_info['domains'].items():
  284. if tag in domain['tags']:
  285. yield name
  286. elif self.target.startswith('@type:'):
  287. type_ = self.target.split(':', 1)[1]
  288. for name, domain in system_info['domains'].items():
  289. if type_ == domain['type']:
  290. yield name
  291. elif self.target == '@anyvm':
  292. for name, domain in system_info['domains'].items():
  293. if name != 'dom0':
  294. yield name
  295. if domain['template_for_dispvms']:
  296. yield '@dispvm:' + name
  297. yield '@dispvm'
  298. elif self.target.startswith('@dispvm:@tag:'):
  299. tag = self.target.split(':', 2)[2]
  300. for name, domain in system_info['domains'].items():
  301. if tag in domain['tags']:
  302. if domain['template_for_dispvms']:
  303. yield '@dispvm:' + name
  304. elif self.target.startswith('@dispvm:'):
  305. dispvm_base = self.target.split(':', 1)[1]
  306. try:
  307. if system_info['domains'][dispvm_base]['template_for_dispvms']:
  308. yield self.target
  309. except KeyError:
  310. # TODO log a warning?
  311. pass
  312. elif self.target == '@adminvm':
  313. yield self.target
  314. elif self.target == '@dispvm':
  315. yield self.target
  316. else:
  317. if self.target in system_info['domains']:
  318. yield self.target
  319. def expand_override_target(self, system_info, source):
  320. '''
  321. Replace '@dispvm' with specific '@dispvm:...' value, based on qrexec
  322. call source.
  323. :param system_info: System information
  324. :param source: Source domain name
  325. :return: :py:attr:`override_target` with '@dispvm' substituted
  326. '''
  327. if self.override_target == '@dispvm':
  328. if system_info['domains'][source]['default_dispvm'] is None:
  329. return None
  330. return '@dispvm:' + system_info['domains'][source]['default_dispvm']
  331. else:
  332. return self.override_target
  333. class PolicyAction(object):
  334. ''' Object representing positive policy evaluation result -
  335. either ask or allow action '''
  336. def __init__(self, service, source, target, rule, original_target,
  337. targets_for_ask=None):
  338. #: service name
  339. self.service = service
  340. #: calling domain
  341. self.source = source
  342. #: target domain the service should be connected to, None if
  343. # not chosen yet
  344. if targets_for_ask is None or target in targets_for_ask:
  345. self.target = target
  346. else:
  347. # TODO: log a warning?
  348. self.target = None
  349. #: original target specified by the caller
  350. self.original_target = original_target
  351. #: targets for the user to choose from
  352. self.targets_for_ask = targets_for_ask
  353. #: policy rule from which this action is derived
  354. self.rule = rule
  355. if rule.action == Action.deny:
  356. # this should be really rejected by Policy.eval()
  357. raise AccessDenied(
  358. 'denied by policy {}:{}'.format(rule.filename, rule.lineno))
  359. if rule.action == Action.ask:
  360. if targets_for_ask is None:
  361. raise AccessDenied(
  362. 'invalid policy {}:{}'.format(rule.filename, rule.lineno))
  363. elif rule.action == Action.allow:
  364. if targets_for_ask is not None or target is None:
  365. raise AccessDenied(
  366. 'invalid policy {}:{}'.format(rule.filename, rule.lineno))
  367. self.action = rule.action
  368. def handle_user_response(self, response, target=None):
  369. '''
  370. Handle user response for the 'ask' action
  371. :param response: whether the call was allowed or denied (bool)
  372. :param target: target chosen by the user (if reponse==True)
  373. :return: None
  374. '''
  375. assert self.action == Action.ask
  376. if response:
  377. assert target in self.targets_for_ask
  378. self.target = target
  379. self.action = Action.allow
  380. else:
  381. self.action = Action.deny
  382. raise AccessDenied(
  383. 'denied by the user {}:{}'.format(self.rule.filename,
  384. self.rule.lineno))
  385. def execute(self, caller_ident):
  386. ''' Execute allowed service call
  387. :param caller_ident: Service caller ident
  388. (`process_ident,source_name, source_id`)
  389. '''
  390. assert self.action == Action.allow
  391. assert self.target is not None
  392. if self.target == '@adminvm':
  393. self.target = 'dom0'
  394. if self.target == 'dom0':
  395. original_target_type = \
  396. 'keyword' if is_special_value(self.original_target) else 'name'
  397. original_target = self.original_target.lstrip('@')
  398. cmd = \
  399. 'QUBESRPC {service} {source} {original_target_type} ' \
  400. '{original_target}'.format(
  401. service=self.service,
  402. source=self.source,
  403. original_target_type=original_target_type,
  404. original_target=original_target)
  405. else:
  406. cmd = '{user}:QUBESRPC {service} {source}'.format(
  407. user=(self.rule.override_user or 'DEFAULT'),
  408. service=self.service,
  409. source=self.source)
  410. if self.target.startswith('@dispvm:'):
  411. target = self.spawn_dispvm()
  412. dispvm = True
  413. else:
  414. target = self.target
  415. dispvm = False
  416. self.ensure_target_running()
  417. qrexec_opts = ['-d', target, '-c', caller_ident]
  418. if dispvm:
  419. qrexec_opts.append('-W')
  420. # XXX remove when #951 gets fixed
  421. if self.source == target:
  422. raise AccessDenied('loopback qrexec connection not supported')
  423. try:
  424. subprocess.call([QREXEC_CLIENT] + qrexec_opts + [cmd])
  425. finally:
  426. if dispvm:
  427. self.cleanup_dispvm(target)
  428. def spawn_dispvm(self):
  429. '''
  430. Create and start Disposable VM based on AppVM specified in
  431. :py:attr:`target`
  432. :return: name of new Disposable VM
  433. '''
  434. base_appvm = self.target.split(':', 1)[1]
  435. dispvm_name = qubesd_call(base_appvm, 'admin.vm.CreateDisposable')
  436. dispvm_name = dispvm_name.decode('ascii')
  437. qubesd_call(dispvm_name, 'admin.vm.Start')
  438. return dispvm_name
  439. def ensure_target_running(self):
  440. '''
  441. Start domain if not running already
  442. :return: None
  443. '''
  444. if self.target == 'dom0':
  445. return
  446. try:
  447. qubesd_call(self.target, 'admin.vm.Start')
  448. except QubesMgmtException as e:
  449. if e.exc_type == 'QubesVMNotHaltedError':
  450. pass
  451. else:
  452. raise
  453. @staticmethod
  454. def cleanup_dispvm(dispvm):
  455. '''
  456. Kill and remove Disposable VM
  457. :param dispvm: name of Disposable VM
  458. :return: None
  459. '''
  460. qubesd_call(dispvm, 'admin.vm.Kill')
  461. class Policy(object):
  462. ''' Full policy for a given service
  463. Usage:
  464. >>> system_info = get_system_info()
  465. >>> policy = Policy('some-service')
  466. >>> action = policy.evaluate(system_info, 'source-name', 'target-name')
  467. >>> if action.action == Action.ask:
  468. >>> # ... ask the user, see action.targets_for_ask ...
  469. >>> action.handle_user_response(response, target_chosen_by_user)
  470. >>> action.execute('process-ident')
  471. '''
  472. def __init__(self, service, policy_dir=POLICY_DIR):
  473. policy_file = os.path.join(policy_dir, service)
  474. if not os.path.exists(policy_file):
  475. # fallback to policy without specific argument set (if any)
  476. policy_file = os.path.join(policy_dir, service.split('+')[0])
  477. if not os.path.exists(policy_file):
  478. raise PolicyNotFound(service)
  479. #: policy storage directory
  480. self.policy_dir = policy_dir
  481. #: service name
  482. self.service = service
  483. #: list of PolicyLine objects
  484. self.policy_rules = []
  485. try:
  486. self.load_policy_file(policy_file)
  487. except OSError as e:
  488. raise AccessDenied(
  489. 'failed to load {} file: {!s}'.format(e.filename, e))
  490. def load_policy_file(self, path):
  491. ''' Load policy file and append rules to :py:attr:`policy_rules`
  492. :param path: file to load
  493. '''
  494. with open(path) as policy_file:
  495. for lineno, line in zip(itertools.count(start=1),
  496. policy_file.readlines()):
  497. line = line.strip()
  498. # compatibility with old keywords notation
  499. line = line.replace('$', '@')
  500. if not line:
  501. # skip empty lines
  502. continue
  503. if line[0] == '#':
  504. # skip comments
  505. continue
  506. if line.startswith('@include:'):
  507. include_path = line.split(':', 1)[1]
  508. # os.path.join will leave include_path unchanged if it's
  509. # already absolute
  510. include_path = os.path.join(self.policy_dir, include_path)
  511. self.load_policy_file(include_path)
  512. else:
  513. self.policy_rules.append(PolicyRule(line, path, lineno))
  514. def find_matching_rule(self, system_info, source, target):
  515. ''' Find the first rule matching given arguments '''
  516. for rule in self.policy_rules:
  517. if rule.is_match(system_info, source, target):
  518. return rule
  519. raise AccessDenied('no matching rule found')
  520. def collect_targets_for_ask(self, system_info, source):
  521. ''' Collect targets the user can choose from in 'ask' action
  522. Word 'targets' is used intentionally instead of 'domains', because it
  523. can also contains @dispvm like keywords.
  524. '''
  525. targets = set()
  526. # iterate over rules in reversed order to easier handle 'deny'
  527. # actions - simply remove matching domains from allowed set
  528. for rule in reversed(self.policy_rules):
  529. if rule.is_match_single(system_info, rule.source, source):
  530. if rule.action == Action.deny:
  531. targets -= set(rule.expand_target(system_info))
  532. else:
  533. if rule.override_target is not None:
  534. override_target = rule.expand_override_target(
  535. system_info, source)
  536. if verify_target_value(system_info, override_target):
  537. targets.add(rule.override_target)
  538. else:
  539. targets.update(rule.expand_target(system_info))
  540. # expand default DispVM
  541. if '@dispvm' in targets:
  542. targets.remove('@dispvm')
  543. if system_info['domains'][source]['default_dispvm'] is not None:
  544. dispvm = '@dispvm:' + \
  545. system_info['domains'][source]['default_dispvm']
  546. if verify_target_value(system_info, dispvm):
  547. targets.add(dispvm)
  548. # expand other keywords
  549. if '@adminvm' in targets:
  550. targets.remove('@adminvm')
  551. targets.add('dom0')
  552. # XXX remove when #951 gets fixed
  553. if source in targets:
  554. targets.remove(source)
  555. return targets
  556. def evaluate(self, system_info, source, target):
  557. ''' Evaluate policy
  558. :raise AccessDenied: when action should be denied unconditionally
  559. :return tuple(rule, considered_targets) - where considered targets is a
  560. list of possible targets for 'ask' action (rule.action == Action.ask)
  561. '''
  562. if target == '':
  563. target = '@default'
  564. rule = self.find_matching_rule(system_info, source, target)
  565. if rule.action == Action.deny:
  566. raise AccessDenied(
  567. 'denied by policy {}:{}'.format(rule.filename, rule.lineno))
  568. if rule.override_target is not None:
  569. override_target = rule.expand_override_target(system_info, source)
  570. if not verify_target_value(system_info, override_target):
  571. raise AccessDenied('invalid target= value in {}:{}'.format(
  572. rule.filename, rule.lineno))
  573. actual_target = override_target
  574. else:
  575. actual_target = target
  576. if rule.action == Action.ask:
  577. if rule.override_target is not None:
  578. targets = [actual_target]
  579. else:
  580. targets = list(
  581. self.collect_targets_for_ask(system_info, source))
  582. if not targets:
  583. raise AccessDenied(
  584. 'policy define \'ask\' action at {}:{} but no target is '
  585. 'available to choose from'.format(
  586. rule.filename, rule.lineno))
  587. return PolicyAction(self.service, source, rule.default_target,
  588. rule, target, targets)
  589. elif rule.action == Action.allow:
  590. if actual_target == '@default':
  591. raise AccessDenied(
  592. 'policy define \'allow\' action at {}:{} but no target is '
  593. 'specified by caller or policy'.format(
  594. rule.filename, rule.lineno))
  595. if actual_target == '@dispvm':
  596. if system_info['domains'][source]['default_dispvm'] is None:
  597. raise AccessDenied(
  598. 'policy define \'allow\' action to @dispvm at {}:{} '
  599. 'but no DispVM base is set for this VM'.format(
  600. rule.filename, rule.lineno))
  601. actual_target = '@dispvm:' + \
  602. system_info['domains'][source]['default_dispvm']
  603. return PolicyAction(self.service, source,
  604. actual_target, rule, target)
  605. else:
  606. # should be unreachable
  607. raise AccessDenied(
  608. 'invalid action?! {}:{}'.format(rule.filename, rule.lineno))
  609. class QubesMgmtException(Exception):
  610. ''' Exception returned by qubesd '''
  611. def __init__(self, exc_type):
  612. super(QubesMgmtException, self).__init__()
  613. self.exc_type = exc_type
  614. def qubesd_call(dest, method, arg=None, payload=None):
  615. if method.startswith('internal.'):
  616. socket_path = QUBESD_INTERNAL_SOCK
  617. else:
  618. socket_path = QUBESD_SOCK
  619. try:
  620. client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  621. client_socket.connect(socket_path)
  622. except IOError:
  623. # TODO:
  624. raise
  625. # src, method, dest, arg
  626. for call_arg in ('dom0', method, dest, arg):
  627. if call_arg is not None:
  628. client_socket.sendall(call_arg.encode('ascii'))
  629. client_socket.sendall(b'\0')
  630. if payload is not None:
  631. client_socket.sendall(payload)
  632. client_socket.shutdown(socket.SHUT_WR)
  633. return_data = client_socket.makefile('rb').read()
  634. if return_data.startswith(b'0\x00'):
  635. return return_data[2:]
  636. elif return_data.startswith(b'2\x00'):
  637. (_, exc_type, _traceback, _format_string, _args) = \
  638. return_data.split(b'\x00', 4)
  639. raise QubesMgmtException(exc_type.decode('ascii'))
  640. else:
  641. raise AssertionError(
  642. 'invalid qubesd response: {!r}'.format(return_data))
  643. def get_system_info():
  644. ''' Get system information
  645. This retrieve information necessary to process qrexec policy. Returned
  646. data is nested dict structure with this structure:
  647. - domains:
  648. - `<domain name>`:
  649. - tags: list of tags
  650. - type: domain type
  651. - template_for_dispvms: should DispVM based on this VM be allowed
  652. - default_dispvm: name of default AppVM for DispVMs started from here
  653. '''
  654. system_info = qubesd_call('dom0', 'internal.GetSystemInfo')
  655. return json.loads(system_info.decode('utf-8'))