Browse Source

qubespolicy: initial version for core3

This is rewritten version of core-admin-linux/qrexec/qrexec-policy.

It's placed outside of `qubes` module on purpose - to avoid imporing it,
which require a lot of time.

QubesOS/qubes-issues#865
QubesOS/qubes-issues#910
Marek Marczykowski-Górecki 7 years ago
parent
commit
e705a04cc5
5 changed files with 740 additions and 1 deletions
  1. 1 1
      .travis.yml
  2. 1 0
      doc/index.rst
  3. 87 0
      doc/qubes-policy.rst
  4. 646 0
      qubespolicy/__init__.py
  5. 5 0
      rpm_spec/core-dom0.spec

+ 1 - 1
.travis.yml

@@ -7,7 +7,7 @@ install:
   - pip install --quiet -r ci/requirements.txt
   - git clone https://github.com/"${TRAVIS_REPO_SLUG%%/*}"/qubes-builder ~/qubes-builder
 script:
-  - PYTHONPATH=test-packages pylint --rcfile=ci/pylintrc qubes
+  - PYTHONPATH=test-packages pylint --rcfile=ci/pylintrc qubes qubespolicy
   - ./run-tests --no-syslog
   - ~/qubes-builder/scripts/travis-build
 env:

+ 1 - 0
doc/index.rst

@@ -20,6 +20,7 @@ manpages and API documentation. For primary user documentation, see
    qubes-ext
    qubes-log
    qubes-mgmt
+   qubes-policy
    qubes-tools/index
    qubes-tests
    qubes-dochelpers

+ 87 - 0
doc/qubes-policy.rst

@@ -0,0 +1,87 @@
+:py:mod:`qubes.policy` -- Qubes RPC policy
+==========================================
+
+Every Qubes domain can trigger various RPC services, but if such call would be
+allowed depends on Qubes RPC policy (qrexec policy in short).
+
+Qrexec policy format
+--------------------
+
+Policy consists of a file, which is parsed line-by-line. First matching line
+is used as an action.
+
+Each line consist of three values separated by white characters (space(s), tab(s)):
+1. Source specification, which is one of:
+
+  - domain name
+  - `$anyvm` - any domain
+  - `$tag:some-tag` - VM having tag `some-tag`
+  - `$type:vm-type` - VM of `vm-type` type, available types:
+      AppVM, TemplateVM, StandaloneVM, DispVM
+
+2. Target specification, one of:
+
+  - domain name
+  - `$anyvm` - any domain, excluding dom0
+  - `$tag:some-tag` - domain having tag `some-tag`
+  - `$type:vm-type` - domain of `vm-type` type, available types:
+      AppVM, TemplateVM, StandaloneVM, DispVM
+  - `$default` - used when caller did not specified any VM
+  - `$dispvm:vm-name` - _new_ Disposable VM created from AppVM `vm-name`
+  - `$dispvm` - _new_ Disposable VM created from AppVM pointed by caller
+    property `default_dispvm`, which defaults to global property `default_dispvm`
+
+3. Action and optional action parameters, one of:
+
+  - `allow` - allow the call, without further questions; optional parameters:
+    - `target=` - override caller provided call target -
+      possible values are: domain name, `$dispvm` or `$dispvm:vm-name`
+    - `user=` - call the service using this user, instead of the user
+      pointed by target VM's `default_user` property
+  - `deny` - deny the call, without further questions; no optional
+    parameters are supported
+  - `ask` - ask the user for confirmation; optional parameters:
+    - `target=` - override user provided call target
+    - `user=` - call the service using this user, instead of the user
+      pointed by target VM's `default_user` property
+    - `default_target=` - suggest this target when prompting the user for
+      confirmation
+
+Alternatively, a line may consist of a single keyword `$include:` followed by a
+path. This will load a given file as its content would be in place of
+`$include` line. Relative paths are resolved relative to
+`/etc/qubes-rpc/policy` directory.
+
+Evaluating `ask` action
+-----------------------
+
+When qrexec policy specify `ask` action, the user is asked whether the call
+should be allowed or denied. In addition to that, user also need to choose
+target domain. User have to choose from a set of targets specified by the
+policy. Such set is calculated using the algorithm below:
+
+1. If `ask` action have `target=` option specified, only that target is
+considered. A prompt window will allow to choose only this value and it will
+also be pre-filled value.
+
+2. If no `target=` option is specified, all rules are evaluated to see what
+target domains (for a given source domain) would result in `ask` or `allow`
+action. If any of them have `target=` option set, that value is used instead of
+the one specified in "target" column (for this particular line). Then the user
+is presented with a confirmation dialog and an option to choose from those
+domains. 
+
+3. If `default_target=` option is set, it is used as
+suggested value, otherwise no suggestion is made (regardless of calling domain
+specified any target or not).
+
+
+
+Module contents
+---------------
+
+.. automodule:: qubespolicy
+   :members:
+   :show-inheritance:
+
+.. vim: ts=3 sw=3 et

+ 646 - 0
qubespolicy/__init__.py

@@ -0,0 +1,646 @@
+# coding=utf-8
+# The Qubes OS Project, https://www.qubes-os.org/
+#
+# Copyright (C) 2013-2015  Joanna Rutkowska <joanna@invisiblethingslab.com>
+# Copyright (C) 2013-2017  Marek Marczykowski-Górecki
+#                                   <marmarek@invisiblethingslab.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+''' Qrexec policy parser and evaluator '''
+import json
+import os
+import os.path
+import socket
+import subprocess
+import enum
+import itertools
+
+# don't import 'qubes.config' please, it takes 0.3s
+QREXEC_CLIENT = '/usr/lib/qubes/qrexec-client'
+QUBES_RPC_MULTIPLEXER_PATH = '/usr/lib/qubes/qubes-rpc-multiplexer'
+POLICY_DIR = '/etc/qubes-rpc/policy'
+QUBESD_INTERNAL_SOCK = '/var/run/qubesd.internal.sock'
+
+
+class AccessDenied(Exception):
+    ''' Raised when qrexec policy denied access '''
+    pass
+
+
+class PolicySyntaxError(AccessDenied):
+    ''' Syntax error in qrexec policy, abort parsing '''
+    def __init__(self, filename, lineno, msg):
+        super(PolicySyntaxError, self).__init__(
+            '{}:{}: {}'.format(filename, lineno, msg))
+
+
+class Action(enum.Enum):
+    ''' Action as defined by policy '''
+    allow = 1
+    deny = 2
+    ask = 3
+
+
+def verify_target_value(system_info, value):
+    ''' Check if given value names valid target
+
+    This function check if given value is not only syntactically correct,
+    but also if names valid service call target (existing domain,
+    or valid $dispvm like keyword)
+
+    :param system_info: information about the system
+    :param value: value to be checked
+    '''
+    if value == '$dispvm':
+        return True
+    elif value.startswith('$dispvm:'):
+        dispvm_base = value.split(':', 1)[1]
+        if dispvm_base not in system_info['domains']:
+            return False
+        dispvm_base_info = system_info['domains'][dispvm_base]
+        return bool(dispvm_base_info['dispvm_allowed'])
+    else:
+        return value in system_info['domains']
+
+
+def verify_special_value(value, for_target=True):
+    '''
+    Verify if given special VM-specifier ('$...') is valid
+
+    :param value: value to verify
+    :param for_target: should classify target-only values as valid (
+    '$default', '$dispvm')
+    :return: True or False
+    '''
+    # pylint: disable=too-many-return-statements
+
+    if value.startswith('$tag:') and len(value) > len('$tag:'):
+        return True
+    elif value.startswith('$type:') and len(value) > len('$type:'):
+        return True
+    elif value == '$anyvm':
+        return True
+    elif value.startswith('$dispvm:') and for_target:
+        return True
+    elif value == '$dispvm' and for_target:
+        return True
+    elif value == '$default' and for_target:
+        return True
+    return False
+
+
+class PolicyRule(object):
+    ''' A single line of policy file '''
+    def __init__(self, line, filename=None, lineno=None):
+        '''
+        Load a single line of qrexec policy and check its syntax.
+        Do not verify existence of named objects.
+
+        :raise PolicySyntaxError: when syntax error is found
+
+        :param line: a single line of actual qrexec policy (not a comment,
+        empty line or $include)
+        :param filename: name of the file from which this line is loaded
+        :param lineno: line number from which this line is loaded
+        '''
+
+        self.lineno = lineno
+        self.filename = filename
+
+        try:
+            self.source, self.target, self.full_action = line.split()
+        except ValueError:
+            raise PolicySyntaxError(filename, lineno, 'wrong number of fields')
+
+        (action, *params) = self.full_action.split(',')
+        try:
+            self.action = Action[action]
+        except KeyError:
+            raise PolicySyntaxError(filename, lineno,
+                'invalid action: {}'.format(action))
+
+        #: alternative target, used instead of the one specified by the caller
+        self.override_target = None
+
+        #: alternative user, used instead of vm.default_user
+        self.override_user = None
+
+        #: default target when asking the user for confirmation
+        self.default_target = None
+
+        for param in params:
+            try:
+                param_name, value = param.split('=')
+            except ValueError:
+                raise PolicySyntaxError(filename, lineno,
+                    'invalid action parameter syntax: {}'.format(param))
+            if param_name == 'target':
+                if self.action == Action.deny:
+                    raise PolicySyntaxError(filename, lineno,
+                        'target= option not allowed for deny action')
+                self.override_target = value
+            elif param_name == 'user':
+                if self.action == Action.deny:
+                    raise PolicySyntaxError(filename, lineno,
+                        'user= option not allowed for deny action')
+                self.override_user = value
+            elif param_name == 'default_target':
+                if self.action != Action.ask:
+                    raise PolicySyntaxError(filename, lineno,
+                        'default_target= option allowed only for ask action')
+                self.default_target = value
+            else:
+                raise PolicySyntaxError(filename, lineno,
+                    'invalid option {} for {} action'.format(param, action))
+
+        # verify special values
+        if self.source.startswith('$'):
+            if not verify_special_value(self.source, False):
+                raise PolicySyntaxError(filename, lineno,
+                    'invalid source specification: {}'.format(self.source))
+
+        if self.target.startswith('$'):
+            if not verify_special_value(self.target, True):
+                raise PolicySyntaxError(filename, lineno,
+                    'invalid target specification: {}'.format(self.target))
+
+        if self.target == '$default' \
+                and self.action == Action.allow \
+                and self.override_target is None:
+            raise PolicySyntaxError(filename, lineno,
+                'allow action for $default rule must specify target= option')
+
+        if self.override_target is not None:
+            if self.override_target.startswith('$') and not \
+                    self.override_target.startswith('$dispvm'):
+                raise PolicySyntaxError(filename, lineno,
+                    'target= option needs to name specific target')
+
+    @staticmethod
+    def is_match_single(system_info, policy_value, value):
+        '''
+        Evaluate if a single value (VM name or '$default') matches policy
+        specification
+
+        :param system_info: information about the system
+        :param policy_value: value from qrexec policy (either self.source or
+        self.target)
+        :param value: value to be compared (source or target)
+        :return: True or False
+        '''
+        # pylint: disable=too-many-return-statements
+
+        # not specified target matches only with $default and $anyvm policy
+        # entry
+        if value == '$default' or value == '':
+            return policy_value in ('$default', '$anyvm')
+
+        # if specific target used, check if it's valid
+        # this function (is_match_single) is also used for checking call source
+        # values, but this isn't a problem, because it will always be a
+        # domain name (not $dispvm or such) - this is guaranteed by a nature
+        # of qrexec call
+        if not verify_target_value(system_info, value):
+            return False
+
+        # allow any _valid_, non-dom0 target
+        if policy_value == '$anyvm':
+            return value != 'dom0'
+
+        # exact match, including $dispvm*
+        if value == policy_value:
+            return True
+
+        # if $dispvm* not matched above, reject it; missing ':' is
+        # intentional - handle both '$dispvm' and '$dispvm:xxx'
+        if value.startswith('$dispvm'):
+            return False
+
+        # at this point, value name a specific target
+        domain_info = system_info['domains'][value]
+
+        if policy_value.startswith('$tag:'):
+            tag = policy_value.split(':', 1)[1]
+            return tag in domain_info['tags']
+
+        if policy_value.startswith('$type:'):
+            type_ = policy_value.split(':', 1)[1]
+            return type_ == domain_info['type']
+
+        return False
+
+    def is_match(self, system_info, source, target):
+        '''
+        Check if given (source, target) matches this policy line.
+
+        :param system_info: information about the system - available VMs,
+        their types, labels, tags etc. as returned by
+        :py:func:`app_to_system_info`
+        :param source: name of the source VM
+        :param target: name of the target VM, or None if not specified
+        :return: True or False
+        '''
+
+        if not self.is_match_single(system_info, self.source, source):
+            return False
+        if not self.is_match_single(system_info, self.target, target):
+            return False
+        return True
+
+    def expand_target(self, system_info):
+        '''
+        Return domains matching target of this policy line
+
+        :param system_info: information about the system
+        :return: matching domains
+        '''
+
+        if self.target.startswith('$tag:'):
+            tag = self.target.split(':', 1)[1]
+            for name, domain in system_info['domains'].items():
+                if tag in domain['tags']:
+                    yield name
+        elif self.target.startswith('$type:'):
+            type_ = self.target.split(':', 1)[1]
+            for name, domain in system_info['domains'].items():
+                if type_ == domain['type']:
+                    yield name
+        elif self.target == '$anyvm':
+            for name, domain in system_info['domains'].items():
+                if name != 'dom0':
+                    yield name
+                if domain['dispvm_allowed']:
+                    yield '$dispvm:' + name
+            yield '$dispvm'
+        elif self.target.startswith('$dispvm:'):
+            dispvm_base = self.target.split(':', 1)[1]
+            try:
+                if system_info['domains'][dispvm_base]['dispvm_allowed']:
+                    yield self.target
+            except KeyError:
+                # TODO log a warning?
+                pass
+        elif self.target == '$dispvm':
+            yield self.target
+        else:
+            if self.target in system_info['domains']:
+                yield self.target
+
+    def expand_override_target(self, system_info, source):
+        '''
+        Replace '$dispvm' with specific '$dispvm:...' value, based on qrexec
+        call source.
+
+        :param system_info: System information
+        :param source: Source domain name
+        :return: :py:attr:`override_target` with '$dispvm' substituted
+        '''
+        if self.override_target == '$dispvm':
+            if system_info['domains'][source]['default_dispvm'] is None:
+                return None
+            return '$dispvm:' + system_info['domains'][source]['default_dispvm']
+        else:
+            return self.override_target
+
+
+class PolicyAction(object):
+    ''' Object representing positive policy evaluation result -
+    either ask or allow action '''
+    def __init__(self, service, source, target, rule, original_target,
+            targets_for_ask=None):
+        #: service name
+        self.service = service
+        #: calling domain
+        self.source = source
+        #: target domain the service should be connected to, None if
+        # not chosen yet
+        if targets_for_ask is None or target in targets_for_ask:
+            self.target = target
+        else:
+            # TODO: log a warning?
+            self.target = None
+        #: original target specified by the caller
+        self.original_target = original_target
+        #: targets for the user to choose from
+        self.targets_for_ask = targets_for_ask
+        #: policy rule from which this action is derived
+        self.rule = rule
+        if rule.action == Action.deny:
+            # this should be really rejected by Policy.eval()
+            raise AccessDenied(
+                'denied by policy {}:{}'.format(rule.filename, rule.lineno))
+        elif rule.action == Action.ask:
+            assert targets_for_ask is not None
+        elif rule.action == Action.allow:
+            assert targets_for_ask is None
+            assert target is not None
+        self.action = rule.action
+
+    def handle_user_response(self, response, target=None):
+        '''
+        Handle user response for the 'ask' action
+
+        :param response: whether the call was allowed or denied (bool)
+        :param target: target chosen by the user (if reponse==True)
+        :return: None
+        '''
+        assert self.action == Action.ask
+        assert self.target is None
+        if response:
+            assert target in self.targets_for_ask
+            self.target = target
+            self.action = Action.allow
+        else:
+            self.action = Action.deny  # pylint: disable=redefined-variable-type
+            raise AccessDenied(
+                'denied by the user {}:{}'.format(self.rule.filename,
+                    self.rule.lineno))
+
+    def execute(self, caller_ident):
+        ''' Execute allowed service call
+
+        :param caller_ident: Service caller ident (`process_ident,source_name,
+        source_id`)
+        '''
+        assert self.action == Action.allow
+        assert self.target is not None
+
+        if self.target == 'dom0':
+            cmd = '{multiplexer} {service} {source} {original_target}'.format(
+                multiplexer=QUBES_RPC_MULTIPLEXER_PATH,
+                service=self.service,
+                source=self.source,
+                original_target=self.original_target)
+        else:
+            cmd = '{user}:QUBESRPC {service} {source}'.format(
+                user=(self.rule.override_user or 'DEFAULT'),
+                service=self.service,
+                source=self.source)
+        if self.target.startswith('$dispvm:'):
+            target = self.spawn_dispvm()
+            dispvm = True
+        else:
+            target = self.target
+            dispvm = False
+            self.ensure_target_running()
+        qrexec_opts = ['-d', target, '-c', caller_ident]
+        if dispvm:
+            qrexec_opts.append('-W')
+        try:
+            subprocess.call([QREXEC_CLIENT] + qrexec_opts + [cmd])
+        finally:
+            if dispvm:
+                self.cleanup_dispvm(target)
+
+
+    def spawn_dispvm(self):
+        '''
+        Create and start Disposable VM based on AppVM specified in
+        :py:attr:`target`
+        :return: name of new Disposable VM
+        '''
+        base_appvm = self.target.split(':', 1)[1]
+        dispvm_name = qubesd_call(base_appvm, 'mgmtinternal.vm.Create.DispVM')
+        dispvm_name = dispvm_name.decode('ascii')
+        qubesd_call(dispvm_name, 'mgmtinternal.vm.Start')
+        return dispvm_name
+
+    def ensure_target_running(self):
+        '''
+        Start domain if not running already
+
+        :return: None
+        '''
+        try:
+            qubesd_call(self.target, 'mgmtinternal.vm.Start')
+        except QubesMgmtException as e:
+            if e.exc_type == 'QubesVMNotHaltedError':
+                pass
+            else:
+                raise
+
+    @staticmethod
+    def cleanup_dispvm(dispvm):
+        '''
+        Kill and remove Disposable VM
+
+        :param dispvm: name of Disposable VM
+        :return: None
+        '''
+        qubesd_call(dispvm, 'mgmtinternal.vm.CleanupDispVM')
+
+
+class Policy(object):
+    ''' Full policy for a given service
+
+    Usage:
+    >>> system_info = get_system_info()
+    >>> policy = Policy('some-service')
+    >>> action = policy.evaluate(system_info, 'source-name', 'target-name')
+    >>> if action.action == Action.ask:
+            (... ask the user, see action.targets_for_ask ...)
+    >>>     action.handle_user_response(response, target_chosen_by_user)
+    >>> action.execute('process-ident')
+
+    '''
+
+    def __init__(self, service):
+        policy_file = os.path.join(POLICY_DIR, service)
+        if not os.path.exists(policy_file):
+            # fallback to policy without specific argument set (if any)
+            policy_file = os.path.join(POLICY_DIR, service.split('+')[0])
+
+        #: service name
+        self.service = service
+
+        #: list of PolicyLine objects
+        self.policy_rules = []
+        try:
+            self.load_policy_file(policy_file)
+        except OSError as e:
+            raise AccessDenied(
+                'failed to load {} file: {!s}'.format(e.filename, e))
+
+    def load_policy_file(self, path):
+        ''' Load policy file and append rules to :py:attr:`policy_rules`
+
+        :param path: file to load
+        '''
+        with open(path) as policy_file:
+            for lineno, line in zip(itertools.count(start=1),
+                    policy_file.readlines()):
+                line = line.strip()
+                if not line:
+                    # skip empty lines
+                    continue
+                if line[0] == '#':
+                    # skip comments
+                    continue
+                if line.startswith('$include:'):
+                    include_path = line.split(':', 1)[1]
+                    # os.path.join will leave include_path unchanged if it's
+                    # already absolute
+                    include_path = os.path.join(POLICY_DIR, include_path)
+                    self.load_policy_file(include_path)
+                else:
+                    self.policy_rules.append(PolicyRule(line, path, lineno))
+
+    def find_matching_rule(self, system_info, source, target):
+        ''' Find the first rule matching given arguments '''
+
+        for rule in self.policy_rules:
+            if rule.is_match(system_info, source, target):
+                return rule
+        raise AccessDenied('no matching rule found')
+
+
+    def collect_targets_for_ask(self, system_info, source):
+        ''' Collect targets the user can choose from in 'ask' action
+
+        Word 'targets' is used intentionally instead of 'domains', because it
+        can also contains $dispvm like keywords.
+        '''
+        targets = set()
+
+        # iterate over rules in reversed order to easier handle 'deny'
+        # actions - simply remove matching domains from allowed set
+        for rule in reversed(self.policy_rules):
+            if rule.is_match_single(system_info, rule.source, source):
+                if rule.action == Action.deny:
+                    targets -= set(rule.expand_target(system_info))
+                else:
+                    if rule.override_target is not None:
+                        override_target = rule.expand_override_target(
+                            system_info, source)
+                        if verify_target_value(system_info, override_target):
+                            targets.add(rule.override_target)
+                    else:
+                        targets.update(rule.expand_target(system_info))
+
+        # expand default DispVM
+        if '$dispvm' in targets:
+            targets.remove('$dispvm')
+            if system_info['domains'][source]['default_dispvm'] is not None:
+                dispvm = '$dispvm:' + \
+                    system_info['domains'][source]['default_dispvm']
+                if verify_target_value(system_info, dispvm):
+                    targets.add(dispvm)
+
+        return targets
+
+    def evaluate(self, system_info, source, target):
+        ''' Evaluate policy
+
+        :raise AccessDenied: when action should be denied unconditionally
+
+        :return tuple(rule, considered_targets) - where considered targets is a
+        list of possible targets for 'ask' action (rule.action == Action.ask)
+        '''
+        rule = self.find_matching_rule(system_info, source, target)
+        if rule.action == Action.deny:
+            raise AccessDenied(
+                'denied by policy {}:{}'.format(rule.filename, rule.lineno))
+
+        if rule.override_target is not None:
+            override_target = rule.expand_override_target(system_info, source)
+            if not verify_target_value(system_info, override_target):
+                raise AccessDenied('invalid target= value in {}:{}'.format(
+                    rule.filename, rule.lineno))
+            actual_target = override_target
+        else:
+            actual_target = target
+
+        if rule.action == Action.ask:
+            if rule.override_target is not None:
+                targets = [actual_target]
+            else:
+                targets = list(
+                    self.collect_targets_for_ask(system_info, source))
+            if len(targets) == 0:
+                raise AccessDenied(
+                    'policy define \'ask\' action at {}:{} but no target is '
+                    'available to choose from'.format(
+                        rule.filename, rule.lineno))
+            return PolicyAction(self.service, source, rule.default_target,
+                rule, target, targets)
+        elif rule.action == Action.allow:
+            if actual_target == '$default':
+                raise AccessDenied(
+                    'policy define \'allow\' action at {}:{} but no target is '
+                    'specified by caller or policy'.format(
+                        rule.filename, rule.lineno))
+            return PolicyAction(self.service, source,
+                actual_target, rule, target)
+        else:
+            # should be unreachable
+            raise AccessDenied(
+                'invalid action?! {}:{}'.format(rule.filename, rule.lineno))
+
+
+class QubesMgmtException(Exception):
+    ''' Exception returned by qubesd '''
+    def __init__(self, exc_type):
+        super(QubesMgmtException, self).__init__()
+        self.exc_type = exc_type
+
+
+def qubesd_call(dest, method, arg=None, payload=None):
+    try:
+        client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        client_socket.connect(QUBESD_INTERNAL_SOCK)
+    except IOError:
+        # TODO:
+        raise
+
+    # src, method, dest, arg
+    for call_arg in ('dom0', method, dest, arg):
+        if call_arg is not None:
+            client_socket.sendall(call_arg.encode('ascii'))
+        client_socket.sendall(b'\0')
+    if payload is not None:
+        client_socket.sendall(payload)
+
+    client_socket.shutdown(socket.SHUT_WR)
+
+    return_data = client_socket.makefile('rb').read()
+    if return_data.startswith(b'0\x00'):
+        return return_data[2:]
+    elif return_data.startswith(b'2\x00'):
+        (_, exc_type, _traceback, _format_string, _args) = \
+            return_data.split(b'\x00', 4)
+        raise QubesMgmtException(exc_type.decode('ascii'))
+    else:
+        raise AssertionError(
+            'invalid qubesd response: {!r}'.format(return_data))
+
+
+def get_system_info():
+    ''' Get system information
+
+    This retrieve information necessary to process qrexec policy. Returned
+    data is nested dict structure with this structure:
+
+    - domains:
+      - <domain name>:
+        - tags: list of tags
+        - type: domain type
+        - dispvm_allowed: should DispVM based on this VM be allowed
+        - default_dispvm: name of default AppVM for DispVMs started from here
+
+    '''
+
+    system_info = qubesd_call('dom0', 'mgmtinternal.GetSystemInfo')
+    return json.loads(system_info)

+ 5 - 0
rpm_spec/core-dom0.spec

@@ -378,6 +378,11 @@ fi
 %{python3_sitelib}/qubes/qmemman/algo.py
 %{python3_sitelib}/qubes/qmemman/client.py
 
+%dir %{python3_sitelib}/qubespolicy
+%dir %{python3_sitelib}/qubespolicy/__pycache__
+%{python3_sitelib}/qubespolicy/__pycache__/*
+%{python3_sitelib}/qubespolicy/__init__.py
+
 /usr/lib/qubes/unbind-pci-device.sh
 /usr/lib/qubes/cleanup-dispvms
 /usr/lib/qubes/qfile-daemon-dvm*