Переглянути джерело

Merge remote-tracking branch 'origin/policy-adminvm'

Marek Marczykowski-Górecki 7 роки тому
батько
коміт
51022cada5

+ 19 - 10
Makefile

@@ -8,7 +8,6 @@ OS ?= Linux
 PYTHON ?= python3
 PYTHON ?= python3
 
 
 ADMIN_API_METHODS_SIMPLE = \
 ADMIN_API_METHODS_SIMPLE = \
-	admin.vm.List \
 	admin.vmclass.List \
 	admin.vmclass.List \
 	admin.Events \
 	admin.Events \
 	admin.backup.Execute \
 	admin.backup.Execute \
@@ -17,6 +16,7 @@ ADMIN_API_METHODS_SIMPLE = \
 	admin.label.Create \
 	admin.label.Create \
 	admin.label.Get \
 	admin.label.Get \
 	admin.label.List \
 	admin.label.List \
+	admin.label.Index \
 	admin.label.Remove \
 	admin.label.Remove \
 	admin.pool.Add \
 	admin.pool.Add \
 	admin.pool.Info \
 	admin.pool.Info \
@@ -83,6 +83,8 @@ ADMIN_API_METHODS_SIMPLE = \
 	admin.vm.tag.List \
 	admin.vm.tag.List \
 	admin.vm.tag.Remove \
 	admin.vm.tag.Remove \
 	admin.vm.tag.Set \
 	admin.vm.tag.Set \
+	admin.vm.volume.CloneFrom \
+	admin.vm.volume.CloneTo \
 	admin.vm.volume.Info \
 	admin.vm.volume.Info \
 	admin.vm.volume.List \
 	admin.vm.volume.List \
 	admin.vm.volume.ListSnapshots \
 	admin.vm.volume.ListSnapshots \
@@ -90,10 +92,6 @@ ADMIN_API_METHODS_SIMPLE = \
 	admin.vm.volume.Revert \
 	admin.vm.volume.Revert \
 	$(null)
 	$(null)
 
 
-ADMIN_API_METHODS := $(ADMIN_API_METHODS_SIMPLE) \
-	 admin.vm.volume.Import \
-	 $(null)
-
 ifeq ($(OS),Linux)
 ifeq ($(OS),Linux)
 DATADIR ?= /var/lib/qubes
 DATADIR ?= /var/lib/qubes
 STATEDIR ?= /var/run/qubes
 STATEDIR ?= /var/run/qubes
@@ -172,15 +170,26 @@ endif
 	install qubes-rpc/qubesd-query-fast $(DESTDIR)/usr/libexec/qubes/
 	install qubes-rpc/qubesd-query-fast $(DESTDIR)/usr/libexec/qubes/
 	for method in $(ADMIN_API_METHODS_SIMPLE); do \
 	for method in $(ADMIN_API_METHODS_SIMPLE); do \
 		ln -s ../../usr/libexec/qubes/qubesd-query-fast \
 		ln -s ../../usr/libexec/qubes/qubesd-query-fast \
-			$(DESTDIR)/etc/qubes-rpc/$$method; \
+			$(DESTDIR)/etc/qubes-rpc/$$method || exit 1; \
 	done
 	done
 	install qubes-rpc/admin.vm.volume.Import $(DESTDIR)/etc/qubes-rpc/
 	install qubes-rpc/admin.vm.volume.Import $(DESTDIR)/etc/qubes-rpc/
-	for method in $(ADMIN_API_METHODS); do \
-		install -m 0644 qubes-rpc-policy/admin-default \
-			$(DESTDIR)/etc/qubes-rpc/policy/$$method; \
+	PYTHONPATH=.:test-packages qubes-rpc-policy/generate-admin-policy \
+		--destdir=$(DESTDIR)/etc/qubes-rpc/policy \
+		--exclude admin.vm.Create.AdminVM \
+				  admin.vm.CreateInPool.AdminVM \
+		          admin.vm.device.testclass.Attach \
+				  admin.vm.device.testclass.Detach \
+				  admin.vm.device.testclass.List \
+				  admin.vm.device.testclass.Available
+	# sanity check
+	for method in $(DESTDIR)/etc/qubes-rpc/policy/admin.*; do \
+		ls $(DESTDIR)/etc/qubes-rpc/$$(basename $$method) >/dev/null || exit 1; \
 	done
 	done
 	install -d $(DESTDIR)/etc/qubes-rpc/policy/include
 	install -d $(DESTDIR)/etc/qubes-rpc/policy/include
-	install -m 0644 qubes-rpc-policy/admin-all \
+	install -m 0644 qubes-rpc-policy/admin-local-ro \
+		qubes-rpc-policy/admin-local-rwx \
+		qubes-rpc-policy/admin-global-ro \
+		qubes-rpc-policy/admin-global-rwx \
 		$(DESTDIR)/etc/qubes-rpc/policy/include/
 		$(DESTDIR)/etc/qubes-rpc/policy/include/
 
 
 	mkdir -p "$(DESTDIR)$(FILESDIR)"
 	mkdir -p "$(DESTDIR)$(FILESDIR)"

+ 2 - 2
ci/coveragerc

@@ -1,3 +1,3 @@
 [run]
 [run]
-source = qubes
-omit = qubes/tests/*
+source = qubes, qubespolicy
+omit = qubes/tests/*, qubespolicy/tests/*

+ 2 - 0
doc/conf.py

@@ -243,6 +243,8 @@ _man_pages_author = []
 man_pages = [
 man_pages = [
     ('manpages/qubesd-query', 'qubesd-query',
     ('manpages/qubesd-query', 'qubesd-query',
         u'Low-level qubesd interrogation tool', _man_pages_author, 1),
         u'Low-level qubesd interrogation tool', _man_pages_author, 1),
+    ('manpages/qrexec-policy-graph', 'qrexec-policy-graph',
+        u'Graph qrexec policy', _man_pages_author, 1),
 ]
 ]
 
 
 if os.path.exists('sandbox.rst'):
 if os.path.exists('sandbox.rst'):

+ 73 - 0
doc/manpages/qrexec-policy-graph.rst

@@ -0,0 +1,73 @@
+.. program:: qrexec-policy-graph
+
+:program:`qrexec-policy-graph` -- Graph qrexec policy
+=====================================================
+
+Synopsis
+--------
+
+:command:`qrexec-policy-graph` [-h] [--include-ask] [--source *SOURCE* [*SOURCE* ...]] [--target *TARGET* [*TARGET* ...]] [--service *SERVICE* [*SERVICE* ...]] [--output *OUTPUT*] [--policy-dir POLICY_DIR] [--system-info SYSTEM_INFO]
+
+
+Options
+-------
+
+.. option:: --help, -h
+
+   show this help message and exit
+
+.. option:: --include-ask
+
+   Include `ask` action in graph. In most cases produce unreadable graphs
+   because many services contains `$anyvm $anyvm ask` rules. It's recommended to
+   limit graph using other options.
+
+.. option:: --source
+
+   Limit graph to calls from *source*. You can specify multiple names.
+
+.. option:: --target
+
+   Limit graph to calls to *target*. You can specify multiple names.
+
+.. option:: --service
+
+   Limit graph to *service*. You can specify multiple names. This can be either
+   bare service name, or service with argument (joined with `+`). If bare
+   service name is given, output will contain also policies for specific
+   arguments.
+
+.. option:: --output
+
+   Write to *output* instead of stdout. The file will be overwritten without
+   confirmation.
+
+.. option:: --policy-dir
+
+   Look for policy in *policy-dir*. This can be useful to process policy
+   extracted from other system. This option adjust only base directory, if any
+   policy file contains `$include:path` with absolute path, it will try to load
+   the file from that location.
+   See also --system-info option.
+
+.. option:: --system-info
+
+   Load system information from file instead of querying local qubesd instance.
+   The file should be in json format, as returned by `internal.GetSystemInfo`
+   qubesd method. This can be obtained by running in dom0:
+
+        qubesd-query -e -c /var/run/qubesd.internal.sock dom0 \
+        internal.GetSystemInfo dom0 | cut -b 3-
+
+.. option:: --skip-labels
+
+   Do not include service names on the graph. Also, include only a single
+   connection between qubes if any service call is allowed there.
+
+
+Authors
+-------
+
+| Marek Marczykowski-Górecki <marmarek at invisiblethingslab dot com>
+
+.. vim: ts=3 sw=3 et tw=80

+ 9 - 2
doc/qubes-policy.rst

@@ -11,13 +11,14 @@ Policy consists of a file, which is parsed line-by-line. First matching line
 is used as an action.
 is used as an action.
 
 
 Each line consist of three values separated by white characters (space(s), tab(s)):
 Each line consist of three values separated by white characters (space(s), tab(s)):
+
 1. Source specification, which is one of:
 1. Source specification, which is one of:
 
 
   - domain name
   - domain name
   - `$anyvm` - any domain
   - `$anyvm` - any domain
   - `$tag:some-tag` - VM having tag `some-tag`
   - `$tag:some-tag` - VM having tag `some-tag`
   - `$type:vm-type` - VM of `vm-type` type, available types:
   - `$type:vm-type` - VM of `vm-type` type, available types:
-      AppVM, TemplateVM, StandaloneVM, DispVM
+    AppVM, TemplateVM, StandaloneVM, DispVM
 
 
 2. Target specification, one of:
 2. Target specification, one of:
 
 
@@ -25,15 +26,20 @@ Each line consist of three values separated by white characters (space(s), tab(s
   - `$anyvm` - any domain, excluding dom0
   - `$anyvm` - any domain, excluding dom0
   - `$tag:some-tag` - domain having tag `some-tag`
   - `$tag:some-tag` - domain having tag `some-tag`
   - `$type:vm-type` - domain of `vm-type` type, available types:
   - `$type:vm-type` - domain of `vm-type` type, available types:
-      AppVM, TemplateVM, StandaloneVM, DispVM
+    AppVM, TemplateVM, StandaloneVM, DispVM
   - `$default` - used when caller did not specified any VM
   - `$default` - used when caller did not specified any VM
   - `$dispvm:vm-name` - _new_ Disposable VM created from AppVM `vm-name`
   - `$dispvm:vm-name` - _new_ Disposable VM created from AppVM `vm-name`
   - `$dispvm` - _new_ Disposable VM created from AppVM pointed by caller
   - `$dispvm` - _new_ Disposable VM created from AppVM pointed by caller
     property `default_dispvm`, which defaults to global property `default_dispvm`
     property `default_dispvm`, which defaults to global property `default_dispvm`
+  - `$adminvm` - Admin VM aka dom0
+
+  Dom0 can only be matched explicitly - either as `dom0` or `$adminvm` keyword.
+  None of `$anyvm`, `$tag:some-tag`, `$type:AdminVM` will match.
 
 
 3. Action and optional action parameters, one of:
 3. Action and optional action parameters, one of:
 
 
   - `allow` - allow the call, without further questions; optional parameters:
   - `allow` - allow the call, without further questions; optional parameters:
+
     - `target=` - override caller provided call target -
     - `target=` - override caller provided call target -
       possible values are: domain name, `$dispvm` or `$dispvm:vm-name`
       possible values are: domain name, `$dispvm` or `$dispvm:vm-name`
     - `user=` - call the service using this user, instead of the user
     - `user=` - call the service using this user, instead of the user
@@ -41,6 +47,7 @@ Each line consist of three values separated by white characters (space(s), tab(s
   - `deny` - deny the call, without further questions; no optional
   - `deny` - deny the call, without further questions; no optional
     parameters are supported
     parameters are supported
   - `ask` - ask the user for confirmation; optional parameters:
   - `ask` - ask the user for confirmation; optional parameters:
+
     - `target=` - override user provided call target
     - `target=` - override user provided call target
     - `user=` - call the service using this user, instead of the user
     - `user=` - call the service using this user, instead of the user
       pointed by target VM's `default_user` property
       pointed by target VM's `default_user` property

+ 0 - 13
qubes-rpc-policy/admin-default

@@ -1,13 +0,0 @@
-## Note that policy parsing stops at the first match,
-## so adding anything below "$anyvm $anyvm action" line will have no effect
-
-## Please use a single # to start your custom comments
-
-## Add your entries here, make sure to append ",target=dom0" to all allow/ask actions
-
-## Include a single file for all admin.* methods to ease setting up Management VM.
-## To allow only specific actions, edit specific policy file, like this one. To
-## allow all of them, edit /etc/qubes-rpc/include/admin-all.
-$include:/etc/qubes-rpc/policy/include/admin-all
-
-$anyvm $anyvm deny

+ 13 - 0
qubes-rpc-policy/admin-global-ro

@@ -0,0 +1,13 @@
+## This file is included from all global read-only admin.* policy files
+## _in default configuration_. To allow only specific action,
+## edit specific policy file.
+
+## Note that policy parsing stops at the first match,
+
+## Please use a single # to start your custom comments
+
+## Include all already having write access
+$include:include/admin-global-rwx
+
+## Add your entries here, make sure to append ",target=dom0" to all allow/ask actions
+

+ 11 - 0
qubes-rpc-policy/admin-global-rwx

@@ -0,0 +1,11 @@
+## This file is included from all global read-write admin.* policy files
+## _in default configuration_. To allow only specific action,
+## edit specific policy file.
+
+## Note that policy parsing stops at the first match,
+## so adding anything below "$anyvm $anyvm action" line will have no effect
+
+## Please use a single # to start your custom comments
+
+## Add your entries here, make sure to append ",target=dom0" to all allow/ask actions
+

+ 14 - 0
qubes-rpc-policy/admin-local-ro

@@ -0,0 +1,14 @@
+## This file is included from all local read-only admin.* policy files
+## _in default configuration_. To allow only specific action,
+## edit specific policy file.
+
+## Note that policy parsing stops at the first match,
+## so adding anything below "$anyvm $anyvm action" line will have no effect
+
+## Please use a single # to start your custom comments
+
+## Include all already having write access
+$include:include/admin-local-rwx
+
+## Add your entries here, make sure to append ",target=dom0" to all allow/ask actions
+

+ 3 - 3
qubes-rpc-policy/admin-all → qubes-rpc-policy/admin-local-rwx

@@ -1,5 +1,6 @@
-## This file is included from all admin.* policy files _in default
-## configuration_. To allow only specific action, edit specific policy file.
+## This file is included from all local read-write admin.* policy files
+## _in default configuration_. To allow only specific action,
+## edit specific policy file.
 
 
 ## Note that policy parsing stops at the first match,
 ## Note that policy parsing stops at the first match,
 ## so adding anything below "$anyvm $anyvm action" line will have no effect
 ## so adding anything below "$anyvm $anyvm action" line will have no effect
@@ -8,4 +9,3 @@
 
 
 ## Add your entries here, make sure to append ",target=dom0" to all allow/ask actions
 ## Add your entries here, make sure to append ",target=dom0" to all allow/ask actions
 
 
-$anyvm $anyvm deny

+ 96 - 0
qubes-rpc-policy/generate-admin-policy

@@ -0,0 +1,96 @@
+#!/usr/bin/python3
+# coding=utf-8
+# The Qubes OS Project, https://www.qubes-os.org/
+#
+# Copyright (C) 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.
+
+import argparse
+import os
+
+import sys
+
+import qubes.api.admin
+
+parser = argparse.ArgumentParser(
+    description='Generate default Admin API policy')
+parser.add_argument('--include-base', action='store',
+    default='include',
+    help='Base path for included paths (default: %(default)s)')
+parser.add_argument('--destdir', action='store',
+    default='/etc/qubes-rpc/policy',
+    help='Directory where write output files to (default: %(default)s)')
+parser.add_argument('--verbose', action='store_true', default=False,
+    help='Be verbose')
+parser.add_argument('--exclude', action='store', nargs='*',
+    help='Exclude service')
+parser.add_argument('service', nargs='*', action='store',
+    help='Generate policy for those services (default: all)')
+
+default_policy_header = '''\
+## Note that policy parsing stops at the first match.
+## Anything not specifically allowed here (or in included file) will be denied.
+
+## Please use a single # to start your custom comments
+
+## Add your entries here, make sure to append ",target=dom0" to all allow/ask actions
+
+## Include a common file for all admin.* methods to ease setting up
+## Management VM.
+## To allow only specific actions, edit specific policy file, like this one. To
+## allow all of them, edit appropriate /etc/qubes-rpc/include/admin-*.
+
+'''
+
+def write_default_policy(args, apiname, clasifiers):
+    ''' Write single default policy for given API call '''
+    assert 'scope' in clasifiers, \
+        'Method {} lack scope classifier'.format(apiname)
+    assert any(attr in clasifiers for attr in ('read', 'write', 'execute')), \
+        'Method {} lack read/write/execute classifier'.format(apiname)
+    assert clasifiers['scope'] in ('local', 'global'), \
+        'Method {} have invalid scope: {}'.format(apiname, clasifiers['scope'])
+
+    file_to_include = 'admin-{scope}-{rwx}'.format(
+        scope=clasifiers['scope'],
+        rwx=('rwx' if clasifiers.get('write', False) or
+                      clasifiers.get('execute', False)
+        else 'ro'))
+
+    if args.verbose:
+        print('Service {}: include {}'.format(apiname, file_to_include),
+            file=sys.stderr)
+    with open(os.path.join(args.destdir, apiname), 'w') as f:
+        f.write(default_policy_header)
+        f.write('$include:{}\n'.format(
+            os.path.join(args.include_base, file_to_include)))
+
+
+def main(args=None):
+    ''' Main function of default-admin-policy tool'''
+    args = parser.parse_args(args)
+
+    for func, apiname, _ in qubes.api.admin.QubesAdminAPI.list_methods():
+        if args.service and apiname not in args.service:
+            continue
+        if args.exclude and apiname in args.exclude:
+            continue
+        write_default_policy(args, apiname, func.classifiers)
+
+
+if __name__ == '__main__':
+    sys.exit(main())

+ 2 - 2
qubes/__init__.py

@@ -329,8 +329,8 @@ class property(object):  # pylint: disable=redefined-builtin,invalid-name
         setter. Can raise QubesValueError if the value is invalid.
         setter. Can raise QubesValueError if the value is invalid.
 
 
         :param untrusted_newvalue: value to be validated
         :param untrusted_newvalue: value to be validated
-        :return sanitized value
-        :raises qubes.exc.QubesValueError
+        :return: sanitized value
+        :raises: qubes.exc.QubesValueError
         '''
         '''
         # do not treat type='str' as sufficient validation
         # do not treat type='str' as sufficient validation
         if self.type is not None and self.type is not str:
         if self.type is not None and self.type is not str:

+ 29 - 22
qubes/api/__init__.py

@@ -41,7 +41,7 @@ class PermissionDenied(Exception):
     pass
     pass
 
 
 
 
-def method(name, *, no_payload=False, endpoints=None):
+def method(name, *, no_payload=False, endpoints=None, **classifiers):
     '''Decorator factory for methods intended to appear in API.
     '''Decorator factory for methods intended to appear in API.
 
 
     The decorated method can be called from public API using a child of
     The decorated method can be called from public API using a child of
@@ -51,6 +51,8 @@ def method(name, *, no_payload=False, endpoints=None):
     :param str name: qrexec rpc method name
     :param str name: qrexec rpc method name
     :param bool no_payload: if :py:obj:`True`, will barf on non-empty payload; \
     :param bool no_payload: if :py:obj:`True`, will barf on non-empty payload; \
         also will not pass payload at all to the method
         also will not pass payload at all to the method
+    :param iterable endpoints: if specified, method serve multiple API calls
+        generated by replacing `{endpoint}` with each value in this iterable
 
 
     The expected function method should have one argument (other than usual
     The expected function method should have one argument (other than usual
     *self*), ``untrusted_payload``, which will contain the payload.
     *self*), ``untrusted_payload``, which will contain the payload.
@@ -75,11 +77,14 @@ def method(name, *, no_payload=False, endpoints=None):
 
 
         # pylint: disable=protected-access
         # pylint: disable=protected-access
         if endpoints is None:
         if endpoints is None:
-            func._rpcname = ((name, None),)
+            func.rpcnames = ((name, None),)
         else:
         else:
-            func._rpcname = tuple(
+            func.rpcnames = tuple(
                 (name.format(endpoint=endpoint), endpoint)
                 (name.format(endpoint=endpoint), endpoint)
                 for endpoint in endpoints)
                 for endpoint in endpoints)
+
+        func.classifiers = classifiers
+
         return func
         return func
 
 
     return decorator
     return decorator
@@ -133,43 +138,45 @@ class AbstractQubesAPI(object):
         #: is this operation cancellable?
         #: is this operation cancellable?
         self.cancellable = False
         self.cancellable = False
 
 
-        untrusted_candidates = []
-        for attr in dir(self):
-            func = getattr(self, attr)
+        candidates = list(self.list_methods(self.method))
+
+        if not candidates:
+            raise ProtocolError('no such method: {!r}'.format(self.method))
+
+        assert len(candidates) == 1, \
+            'multiple candidates for method {!r}'.format(self.method)
 
 
+        #: the method to execute
+        self._handler = candidates[0]
+        self._running_handler = None
+
+    @classmethod
+    def list_methods(cls, select_method=None):
+        for attr in dir(cls):
+            func = getattr(cls, attr)
             if not callable(func):
             if not callable(func):
                 continue
                 continue
 
 
             try:
             try:
                 # pylint: disable=protected-access
                 # pylint: disable=protected-access
-                for mname, endpoint in func._rpcname:
-                    if mname != self.method:
-                        continue
-                    untrusted_candidates.append((func, endpoint))
+                rpcnames = func.rpcnames
             except AttributeError:
             except AttributeError:
                 continue
                 continue
 
 
-        if not untrusted_candidates:
-            raise ProtocolError('no such method: {!r}'.format(self.method))
-
-        assert len(untrusted_candidates) == 1, \
-            'multiple candidates for method {!r}'.format(self.method)
-
-        #: the method to execute
-        self._handler = untrusted_candidates[0]
-        self._running_handler = None
-        del untrusted_candidates
+            for mname, endpoint in rpcnames:
+                if select_method is None or mname == select_method:
+                    yield (func, mname, endpoint)
 
 
     def execute(self, *, untrusted_payload):
     def execute(self, *, untrusted_payload):
         '''Execute management operation.
         '''Execute management operation.
 
 
         This method is a coroutine.
         This method is a coroutine.
         '''
         '''
-        handler, endpoint = self._handler
+        handler, _, endpoint = self._handler
         kwargs = {}
         kwargs = {}
         if endpoint is not None:
         if endpoint is not None:
             kwargs['endpoint'] = endpoint
             kwargs['endpoint'] = endpoint
-        self._running_handler = asyncio.ensure_future(handler(
+        self._running_handler = asyncio.ensure_future(handler(self,
             untrusted_payload=untrusted_payload, **kwargs))
             untrusted_payload=untrusted_payload, **kwargs))
         return self._running_handler
         return self._running_handler
 
 

+ 118 - 55
qubes/api/admin.py

@@ -76,7 +76,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
 
 
     SOCKNAME = '/var/run/qubesd.sock'
     SOCKNAME = '/var/run/qubesd.sock'
 
 
-    @qubes.api.method('admin.vmclass.List', no_payload=True)
+    @qubes.api.method('admin.vmclass.List', no_payload=True,
+        scope='global', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vmclass_list(self):
     def vmclass_list(self):
         '''List all VM classes'''
         '''List all VM classes'''
@@ -89,7 +90,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         return ''.join('{}\n'.format(ep.name)
         return ''.join('{}\n'.format(ep.name)
             for ep in entrypoints)
             for ep in entrypoints)
 
 
-    @qubes.api.method('admin.vm.List', no_payload=True)
+    @qubes.api.method('admin.vm.List', no_payload=True,
+        scope='global', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_list(self):
     def vm_list(self):
         '''List all the domains'''
         '''List all the domains'''
@@ -106,13 +108,15 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
                 vm.get_power_state())
                 vm.get_power_state())
             for vm in sorted(domains))
             for vm in sorted(domains))
 
 
-    @qubes.api.method('admin.vm.property.List', no_payload=True)
+    @qubes.api.method('admin.vm.property.List', no_payload=True,
+        scope='local', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_property_list(self):
     def vm_property_list(self):
         '''List all properties on a qube'''
         '''List all properties on a qube'''
         return self._property_list(self.dest)
         return self._property_list(self.dest)
 
 
-    @qubes.api.method('admin.property.List', no_payload=True)
+    @qubes.api.method('admin.property.List', no_payload=True,
+        scope='global', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def property_list(self):
     def property_list(self):
         '''List all global properties'''
         '''List all global properties'''
@@ -126,13 +130,15 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
 
 
         return ''.join('{}\n'.format(prop.__name__) for prop in properties)
         return ''.join('{}\n'.format(prop.__name__) for prop in properties)
 
 
-    @qubes.api.method('admin.vm.property.Get', no_payload=True)
+    @qubes.api.method('admin.vm.property.Get', no_payload=True,
+        scope='local', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_property_get(self):
     def vm_property_get(self):
         '''Get a value of one property'''
         '''Get a value of one property'''
         return self._property_get(self.dest)
         return self._property_get(self.dest)
 
 
-    @qubes.api.method('admin.property.Get', no_payload=True)
+    @qubes.api.method('admin.property.Get', no_payload=True,
+        scope='global', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def property_get(self):
     def property_get(self):
         '''Get a value of one global property'''
         '''Get a value of one global property'''
@@ -168,14 +174,16 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
                 property_type,
                 property_type,
                 str(value) if value is not None else '')
                 str(value) if value is not None else '')
 
 
-    @qubes.api.method('admin.vm.property.Set')
+    @qubes.api.method('admin.vm.property.Set',
+        scope='local', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_property_set(self, untrusted_payload):
     def vm_property_set(self, untrusted_payload):
         '''Set property value'''
         '''Set property value'''
         return self._property_set(self.dest,
         return self._property_set(self.dest,
             untrusted_payload=untrusted_payload)
             untrusted_payload=untrusted_payload)
 
 
-    @qubes.api.method('admin.property.Set')
+    @qubes.api.method('admin.property.Set',
+        scope='global', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def property_set(self, untrusted_payload):
     def property_set(self, untrusted_payload):
         '''Set property value'''
         '''Set property value'''
@@ -195,13 +203,15 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         setattr(dest, self.arg, newvalue)
         setattr(dest, self.arg, newvalue)
         self.app.save()
         self.app.save()
 
 
-    @qubes.api.method('admin.vm.property.Help', no_payload=True)
+    @qubes.api.method('admin.vm.property.Help', no_payload=True,
+        scope='local', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_property_help(self):
     def vm_property_help(self):
         '''Get help for one property'''
         '''Get help for one property'''
         return self._property_help(self.dest)
         return self._property_help(self.dest)
 
 
-    @qubes.api.method('admin.property.Help', no_payload=True)
+    @qubes.api.method('admin.property.Help', no_payload=True,
+        scope='global', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def property_help(self):
     def property_help(self):
         '''Get help for one property'''
         '''Get help for one property'''
@@ -221,13 +231,15 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
 
 
         return qubes.utils.format_doc(doc)
         return qubes.utils.format_doc(doc)
 
 
-    @qubes.api.method('admin.vm.property.Reset', no_payload=True)
+    @qubes.api.method('admin.vm.property.Reset', no_payload=True,
+        scope='local', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_property_reset(self):
     def vm_property_reset(self):
         '''Reset a property to a default value'''
         '''Reset a property to a default value'''
         return self._property_reset(self.dest)
         return self._property_reset(self.dest)
 
 
-    @qubes.api.method('admin.property.Reset', no_payload=True)
+    @qubes.api.method('admin.property.Reset', no_payload=True,
+        scope='global', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def property_reset(self):
     def property_reset(self):
         '''Reset a property to a default value'''
         '''Reset a property to a default value'''
@@ -243,7 +255,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         delattr(dest, self.arg)
         delattr(dest, self.arg)
         self.app.save()
         self.app.save()
 
 
-    @qubes.api.method('admin.vm.volume.List', no_payload=True)
+    @qubes.api.method('admin.vm.volume.List', no_payload=True,
+        scope='local', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_volume_list(self):
     def vm_volume_list(self):
         assert not self.arg
         assert not self.arg
@@ -251,7 +264,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
         volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
         return ''.join('{}\n'.format(name) for name in volume_names)
         return ''.join('{}\n'.format(name) for name in volume_names)
 
 
-    @qubes.api.method('admin.vm.volume.Info', no_payload=True)
+    @qubes.api.method('admin.vm.volume.Info', no_payload=True,
+        scope='local', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_volume_info(self):
     def vm_volume_info(self):
         assert self.arg in self.dest.volumes.keys()
         assert self.arg in self.dest.volumes.keys()
@@ -266,7 +280,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         return ''.join('{}={}\n'.format(key, getattr(volume, key)) for key in
         return ''.join('{}={}\n'.format(key, getattr(volume, key)) for key in
             volume_properties)
             volume_properties)
 
 
-    @qubes.api.method('admin.vm.volume.ListSnapshots', no_payload=True)
+    @qubes.api.method('admin.vm.volume.ListSnapshots', no_payload=True,
+        scope='local', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_volume_listsnapshots(self):
     def vm_volume_listsnapshots(self):
         assert self.arg in self.dest.volumes.keys()
         assert self.arg in self.dest.volumes.keys()
@@ -277,7 +292,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
 
 
         return ''.join('{}\n'.format(revision) for revision in revisions)
         return ''.join('{}\n'.format(revision) for revision in revisions)
 
 
-    @qubes.api.method('admin.vm.volume.Revert')
+    @qubes.api.method('admin.vm.volume.Revert',
+        scope='local', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_volume_revert(self, untrusted_payload):
     def vm_volume_revert(self, untrusted_payload):
         assert self.arg in self.dest.volumes.keys()
         assert self.arg in self.dest.volumes.keys()
@@ -294,7 +310,10 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         self.dest.storage.get_pool(volume).revert(revision)
         self.dest.storage.get_pool(volume).revert(revision)
         self.app.save()
         self.app.save()
 
 
-    @qubes.api.method('admin.vm.volume.CloneFrom', no_payload=True)
+    # write=True because this allow to clone VM - and most likely modify that
+    # one - still having the same data
+    @qubes.api.method('admin.vm.volume.CloneFrom', no_payload=True,
+        scope='local', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_volume_clone_from(self):
     def vm_volume_clone_from(self):
         assert self.arg in self.dest.volumes.keys()
         assert self.arg in self.dest.volumes.keys()
@@ -314,7 +333,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         self.app.api_admin_pending_clone[token] = volume
         self.app.api_admin_pending_clone[token] = volume
         return token
         return token
 
 
-    @qubes.api.method('admin.vm.volume.CloneTo')
+    @qubes.api.method('admin.vm.volume.CloneTo',
+        scope='local', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_volume_clone_to(self, untrusted_payload):
     def vm_volume_clone_to(self, untrusted_payload):
         assert self.arg in self.dest.volumes.keys()
         assert self.arg in self.dest.volumes.keys()
@@ -347,7 +367,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         self.dest.volumes[self.arg] = op_retval
         self.dest.volumes[self.arg] = op_retval
         self.app.save()
         self.app.save()
 
 
-    @qubes.api.method('admin.vm.volume.Resize')
+    @qubes.api.method('admin.vm.volume.Resize',
+        scope='local', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_volume_resize(self, untrusted_payload):
     def vm_volume_resize(self, untrusted_payload):
         assert self.arg in self.dest.volumes.keys()
         assert self.arg in self.dest.volumes.keys()
@@ -363,7 +384,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         self.dest.storage.resize(self.arg, size)
         self.dest.storage.resize(self.arg, size)
         self.app.save()
         self.app.save()
 
 
-    @qubes.api.method('admin.vm.volume.Import', no_payload=True)
+    @qubes.api.method('admin.vm.volume.Import', no_payload=True,
+        scope='local', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_volume_import(self):
     def vm_volume_import(self):
         '''Import volume data.
         '''Import volume data.
@@ -392,7 +414,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
 
 
         return '{} {}'.format(size, path)
         return '{} {}'.format(size, path)
 
 
-    @qubes.api.method('admin.vm.tag.List', no_payload=True)
+    @qubes.api.method('admin.vm.tag.List', no_payload=True,
+        scope='local', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_tag_list(self):
     def vm_tag_list(self):
         assert not self.arg
         assert not self.arg
@@ -403,7 +426,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
 
 
         return ''.join('{}\n'.format(tag) for tag in sorted(tags))
         return ''.join('{}\n'.format(tag) for tag in sorted(tags))
 
 
-    @qubes.api.method('admin.vm.tag.Get', no_payload=True)
+    @qubes.api.method('admin.vm.tag.Get', no_payload=True,
+        scope='local', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_tag_get(self):
     def vm_tag_get(self):
         qubes.vm.Tags.validate_tag(self.arg)
         qubes.vm.Tags.validate_tag(self.arg)
@@ -412,7 +436,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
 
 
         return '1' if self.arg in self.dest.tags else '0'
         return '1' if self.arg in self.dest.tags else '0'
 
 
-    @qubes.api.method('admin.vm.tag.Set', no_payload=True)
+    @qubes.api.method('admin.vm.tag.Set', no_payload=True,
+        scope='local', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_tag_set(self):
     def vm_tag_set(self):
         qubes.vm.Tags.validate_tag(self.arg)
         qubes.vm.Tags.validate_tag(self.arg)
@@ -422,7 +447,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         self.dest.tags.add(self.arg)
         self.dest.tags.add(self.arg)
         self.app.save()
         self.app.save()
 
 
-    @qubes.api.method('admin.vm.tag.Remove', no_payload=True)
+    @qubes.api.method('admin.vm.tag.Remove', no_payload=True,
+        scope='local', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_tag_remove(self):
     def vm_tag_remove(self):
         qubes.vm.Tags.validate_tag(self.arg)
         qubes.vm.Tags.validate_tag(self.arg)
@@ -435,7 +461,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
             raise qubes.exc.QubesTagNotFoundError(self.dest, self.arg)
             raise qubes.exc.QubesTagNotFoundError(self.dest, self.arg)
         self.app.save()
         self.app.save()
 
 
-    @qubes.api.method('admin.pool.List', no_payload=True)
+    @qubes.api.method('admin.pool.List', no_payload=True,
+        scope='global', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def pool_list(self):
     def pool_list(self):
         assert not self.arg
         assert not self.arg
@@ -445,7 +472,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
 
 
         return ''.join('{}\n'.format(pool) for pool in pools)
         return ''.join('{}\n'.format(pool) for pool in pools)
 
 
-    @qubes.api.method('admin.pool.ListDrivers', no_payload=True)
+    @qubes.api.method('admin.pool.ListDrivers', no_payload=True,
+        scope='global', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def pool_listdrivers(self):
     def pool_listdrivers(self):
         assert self.dest.name == 'dom0'
         assert self.dest.name == 'dom0'
@@ -458,7 +486,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
             ' '.join(qubes.storage.driver_parameters(driver)))
             ' '.join(qubes.storage.driver_parameters(driver)))
             for driver in drivers)
             for driver in drivers)
 
 
-    @qubes.api.method('admin.pool.Info', no_payload=True)
+    @qubes.api.method('admin.pool.Info', no_payload=True,
+        scope='global', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def pool_info(self):
     def pool_info(self):
         assert self.dest.name == 'dom0'
         assert self.dest.name == 'dom0'
@@ -471,7 +500,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         return ''.join('{}={}\n'.format(prop, val)
         return ''.join('{}={}\n'.format(prop, val)
             for prop, val in sorted(pool.config.items()))
             for prop, val in sorted(pool.config.items()))
 
 
-    @qubes.api.method('admin.pool.Add')
+    @qubes.api.method('admin.pool.Add',
+        scope='global', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def pool_add(self, untrusted_payload):
     def pool_add(self, untrusted_payload):
         assert self.dest.name == 'dom0'
         assert self.dest.name == 'dom0'
@@ -506,7 +536,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         self.app.add_pool(name=pool_name, driver=self.arg, **pool_config)
         self.app.add_pool(name=pool_name, driver=self.arg, **pool_config)
         self.app.save()
         self.app.save()
 
 
-    @qubes.api.method('admin.pool.Remove', no_payload=True)
+    @qubes.api.method('admin.pool.Remove', no_payload=True,
+        scope='global', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def pool_remove(self):
     def pool_remove(self):
         assert self.dest.name == 'dom0'
         assert self.dest.name == 'dom0'
@@ -517,7 +548,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         self.app.remove_pool(self.arg)
         self.app.remove_pool(self.arg)
         self.app.save()
         self.app.save()
 
 
-    @qubes.api.method('admin.label.List', no_payload=True)
+    @qubes.api.method('admin.label.List', no_payload=True,
+        scope='global', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def label_list(self):
     def label_list(self):
         assert self.dest.name == 'dom0'
         assert self.dest.name == 'dom0'
@@ -527,7 +559,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
 
 
         return ''.join('{}\n'.format(label.name) for label in labels)
         return ''.join('{}\n'.format(label.name) for label in labels)
 
 
-    @qubes.api.method('admin.label.Get', no_payload=True)
+    @qubes.api.method('admin.label.Get', no_payload=True,
+        scope='global', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def label_get(self):
     def label_get(self):
         assert self.dest.name == 'dom0'
         assert self.dest.name == 'dom0'
@@ -541,7 +574,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
 
 
         return label.color
         return label.color
 
 
-    @qubes.api.method('admin.label.Index', no_payload=True)
+    @qubes.api.method('admin.label.Index', no_payload=True,
+        scope='global', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def label_index(self):
     def label_index(self):
         assert self.dest.name == 'dom0'
         assert self.dest.name == 'dom0'
@@ -555,7 +589,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
 
 
         return str(label.index)
         return str(label.index)
 
 
-    @qubes.api.method('admin.label.Create')
+    @qubes.api.method('admin.label.Create',
+        scope='global', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def label_create(self, untrusted_payload):
     def label_create(self, untrusted_payload):
         assert self.dest.name == 'dom0'
         assert self.dest.name == 'dom0'
@@ -591,7 +626,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         self.app.labels[new_index] = label
         self.app.labels[new_index] = label
         self.app.save()
         self.app.save()
 
 
-    @qubes.api.method('admin.label.Remove', no_payload=True)
+    @qubes.api.method('admin.label.Remove', no_payload=True,
+        scope='global', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def label_remove(self):
     def label_remove(self):
         assert self.dest.name == 'dom0'
         assert self.dest.name == 'dom0'
@@ -613,7 +649,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         del self.app.labels[label.index]
         del self.app.labels[label.index]
         self.app.save()
         self.app.save()
 
 
-    @qubes.api.method('admin.vm.Start', no_payload=True)
+    @qubes.api.method('admin.vm.Start', no_payload=True,
+        scope='local', execute=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_start(self):
     def vm_start(self):
         assert not self.arg
         assert not self.arg
@@ -625,35 +662,40 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
             raise qubes.exc.QubesException('Start failed: ' + str(e))
             raise qubes.exc.QubesException('Start failed: ' + str(e))
 
 
 
 
-    @qubes.api.method('admin.vm.Shutdown', no_payload=True)
+    @qubes.api.method('admin.vm.Shutdown', no_payload=True,
+        scope='local', execute=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_shutdown(self):
     def vm_shutdown(self):
         assert not self.arg
         assert not self.arg
         self.fire_event_for_permission()
         self.fire_event_for_permission()
         yield from self.dest.shutdown()
         yield from self.dest.shutdown()
 
 
-    @qubes.api.method('admin.vm.Pause', no_payload=True)
+    @qubes.api.method('admin.vm.Pause', no_payload=True,
+        scope='local', execute=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_pause(self):
     def vm_pause(self):
         assert not self.arg
         assert not self.arg
         self.fire_event_for_permission()
         self.fire_event_for_permission()
         yield from self.dest.pause()
         yield from self.dest.pause()
 
 
-    @qubes.api.method('admin.vm.Unpause', no_payload=True)
+    @qubes.api.method('admin.vm.Unpause', no_payload=True,
+        scope='local', execute=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_unpause(self):
     def vm_unpause(self):
         assert not self.arg
         assert not self.arg
         self.fire_event_for_permission()
         self.fire_event_for_permission()
         yield from self.dest.unpause()
         yield from self.dest.unpause()
 
 
-    @qubes.api.method('admin.vm.Kill', no_payload=True)
+    @qubes.api.method('admin.vm.Kill', no_payload=True,
+        scope='local', execute=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_kill(self):
     def vm_kill(self):
         assert not self.arg
         assert not self.arg
         self.fire_event_for_permission()
         self.fire_event_for_permission()
         yield from self.dest.kill()
         yield from self.dest.kill()
 
 
-    @qubes.api.method('admin.Events', no_payload=True)
+    @qubes.api.method('admin.Events', no_payload=True,
+        scope='global', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def events(self):
     def events(self):
         assert not self.arg
         assert not self.arg
@@ -694,14 +736,16 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         else:
         else:
             self.dest.remove_handler('*', dispatcher.vm_handler)
             self.dest.remove_handler('*', dispatcher.vm_handler)
 
 
-    @qubes.api.method('admin.vm.feature.List', no_payload=True)
+    @qubes.api.method('admin.vm.feature.List', no_payload=True,
+        scope='local', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_feature_list(self):
     def vm_feature_list(self):
         assert not self.arg
         assert not self.arg
         features = self.fire_event_for_filter(self.dest.features.keys())
         features = self.fire_event_for_filter(self.dest.features.keys())
         return ''.join('{}\n'.format(feature) for feature in features)
         return ''.join('{}\n'.format(feature) for feature in features)
 
 
-    @qubes.api.method('admin.vm.feature.Get', no_payload=True)
+    @qubes.api.method('admin.vm.feature.Get', no_payload=True,
+        scope='local', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_feature_get(self):
     def vm_feature_get(self):
         # validation of self.arg done by qrexec-policy is enough
         # validation of self.arg done by qrexec-policy is enough
@@ -713,7 +757,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
             raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
             raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
         return value
         return value
 
 
-    @qubes.api.method('admin.vm.feature.CheckWithTemplate', no_payload=True)
+    @qubes.api.method('admin.vm.feature.CheckWithTemplate', no_payload=True,
+        scope='local', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_feature_checkwithtemplate(self):
     def vm_feature_checkwithtemplate(self):
         # validation of self.arg done by qrexec-policy is enough
         # validation of self.arg done by qrexec-policy is enough
@@ -725,7 +770,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
             raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
             raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
         return value
         return value
 
 
-    @qubes.api.method('admin.vm.feature.Remove', no_payload=True)
+    @qubes.api.method('admin.vm.feature.Remove', no_payload=True,
+        scope='local', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_feature_remove(self):
     def vm_feature_remove(self):
         # validation of self.arg done by qrexec-policy is enough
         # validation of self.arg done by qrexec-policy is enough
@@ -737,7 +783,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
             raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
             raise qubes.exc.QubesFeatureNotFoundError(self.dest, self.arg)
         self.app.save()
         self.app.save()
 
 
-    @qubes.api.method('admin.vm.feature.Set')
+    @qubes.api.method('admin.vm.feature.Set',
+        scope='local', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_feature_set(self, untrusted_payload):
     def vm_feature_set(self, untrusted_payload):
         # validation of self.arg done by qrexec-policy is enough
         # validation of self.arg done by qrexec-policy is enough
@@ -749,14 +796,16 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         self.app.save()
         self.app.save()
 
 
     @qubes.api.method('admin.vm.Create.{endpoint}', endpoints=(ep.name
     @qubes.api.method('admin.vm.Create.{endpoint}', endpoints=(ep.name
-            for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)))
+            for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)),
+        scope='global', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_create(self, endpoint, untrusted_payload=None):
     def vm_create(self, endpoint, untrusted_payload=None):
         return self._vm_create(endpoint, allow_pool=False,
         return self._vm_create(endpoint, allow_pool=False,
             untrusted_payload=untrusted_payload)
             untrusted_payload=untrusted_payload)
 
 
     @qubes.api.method('admin.vm.CreateInPool.{endpoint}', endpoints=(ep.name
     @qubes.api.method('admin.vm.CreateInPool.{endpoint}', endpoints=(ep.name
-            for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)))
+            for ep in pkg_resources.iter_entry_points(qubes.vm.VM_ENTRY_POINT)),
+        scope='global', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_create_in_pool(self, endpoint, untrusted_payload=None):
     def vm_create_in_pool(self, endpoint, untrusted_payload=None):
         return self._vm_create(endpoint, allow_pool=True,
         return self._vm_create(endpoint, allow_pool=True,
@@ -846,7 +895,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
             raise
             raise
         self.app.save()
         self.app.save()
 
 
-    @qubes.api.method('admin.vm.Remove', no_payload=True)
+    @qubes.api.method('admin.vm.Remove', no_payload=True,
+        scope='global', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_remove(self):
     def vm_remove(self):
         assert not self.arg
         assert not self.arg
@@ -867,7 +917,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
 
 
     @qubes.api.method('admin.vm.device.{endpoint}.Available', endpoints=(ep.name
     @qubes.api.method('admin.vm.device.{endpoint}.Available', endpoints=(ep.name
             for ep in pkg_resources.iter_entry_points('qubes.devices')),
             for ep in pkg_resources.iter_entry_points('qubes.devices')),
-            no_payload=True)
+            no_payload=True,
+        scope='local', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_device_available(self, endpoint):
     def vm_device_available(self, endpoint):
         devclass = endpoint
         devclass = endpoint
@@ -901,7 +952,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
 
 
     @qubes.api.method('admin.vm.device.{endpoint}.List', endpoints=(ep.name
     @qubes.api.method('admin.vm.device.{endpoint}.List', endpoints=(ep.name
             for ep in pkg_resources.iter_entry_points('qubes.devices')),
             for ep in pkg_resources.iter_entry_points('qubes.devices')),
-            no_payload=True)
+            no_payload=True,
+        scope='local', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_device_list(self, endpoint):
     def vm_device_list(self, endpoint):
         devclass = endpoint
         devclass = endpoint
@@ -932,8 +984,12 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         return ''.join('{} {}\n'.format(ident, dev_info[ident])
         return ''.join('{} {}\n'.format(ident, dev_info[ident])
             for ident in sorted(dev_info))
             for ident in sorted(dev_info))
 
 
+    # Attach/Detach action can both modify persistent state (with
+    # persistent=True) and volatile state of running VM (with persistent=False).
+    # For this reason, write=True + execute=True
     @qubes.api.method('admin.vm.device.{endpoint}.Attach', endpoints=(ep.name
     @qubes.api.method('admin.vm.device.{endpoint}.Attach', endpoints=(ep.name
-            for ep in pkg_resources.iter_entry_points('qubes.devices')))
+            for ep in pkg_resources.iter_entry_points('qubes.devices')),
+        scope='local', write=True, execute=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_device_attach(self, endpoint, untrusted_payload):
     def vm_device_attach(self, endpoint, untrusted_payload):
         devclass = endpoint
         devclass = endpoint
@@ -972,9 +1028,13 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         yield from self.dest.devices[devclass].attach(assignment)
         yield from self.dest.devices[devclass].attach(assignment)
         self.app.save()
         self.app.save()
 
 
+    # Attach/Detach action can both modify persistent state (with
+    # persistent=True) and volatile state of running VM (with persistent=False).
+    # For this reason, write=True + execute=True
     @qubes.api.method('admin.vm.device.{endpoint}.Detach', endpoints=(ep.name
     @qubes.api.method('admin.vm.device.{endpoint}.Detach', endpoints=(ep.name
             for ep in pkg_resources.iter_entry_points('qubes.devices')),
             for ep in pkg_resources.iter_entry_points('qubes.devices')),
-            no_payload=True)
+            no_payload=True,
+        scope='local', write=True, execute=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_device_detach(self, endpoint):
     def vm_device_detach(self, endpoint):
         devclass = endpoint
         devclass = endpoint
@@ -994,7 +1054,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         yield from self.dest.devices[devclass].detach(assignment)
         yield from self.dest.devices[devclass].detach(assignment)
         self.app.save()
         self.app.save()
 
 
-    @qubes.api.method('admin.vm.firewall.Get', no_payload=True)
+    @qubes.api.method('admin.vm.firewall.Get', no_payload=True,
+            scope='local', read=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_firewall_get(self):
     def vm_firewall_get(self):
         assert not self.arg
         assert not self.arg
@@ -1004,7 +1065,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         return ''.join('{}\n'.format(rule.api_rule)
         return ''.join('{}\n'.format(rule.api_rule)
             for rule in self.dest.firewall.rules)
             for rule in self.dest.firewall.rules)
 
 
-    @qubes.api.method('admin.vm.firewall.Set')
+    @qubes.api.method('admin.vm.firewall.Set',
+            scope='local', write=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_firewall_set(self, untrusted_payload):
     def vm_firewall_set(self, untrusted_payload):
         assert not self.arg
         assert not self.arg
@@ -1020,7 +1082,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
         self.dest.firewall.rules = rules
         self.dest.firewall.rules = rules
         self.dest.firewall.save()
         self.dest.firewall.save()
 
 
-    @qubes.api.method('admin.vm.firewall.Reload', no_payload=True)
+    @qubes.api.method('admin.vm.firewall.Reload', no_payload=True,
+            scope='local', execute=True)
     @asyncio.coroutine
     @asyncio.coroutine
     def vm_firewall_reload(self):
     def vm_firewall_reload(self):
         assert not self.arg
         assert not self.arg

+ 3 - 2
qubes/backup.py

@@ -119,6 +119,7 @@ class BackupHeader(object):
 
 
         :param untrusted_header_text: header content
         :param untrusted_header_text: header content
         :type untrusted_header_text: basestring
         :type untrusted_header_text: basestring
+
         .. warning::
         .. warning::
             This function may be exposed to not yet verified header,
             This function may be exposed to not yet verified header,
             so is security critical.
             so is security critical.
@@ -864,9 +865,9 @@ def handle_streams(stream_in, streams_out, processes, size_limit=None,
     :param processes: dict of subprocess.Popen objects to monitor
     :param processes: dict of subprocess.Popen objects to monitor
     :param size_limit: int maximum data amount to process
     :param size_limit: int maximum data amount to process
     :param progress_callback: callable function to report progress, will be
     :param progress_callback: callable function to report progress, will be
-    given copied data size (it should accumulate internally)
+        given copied data size (it should accumulate internally)
     :return: failed process name, failed stream name, "size_limit" or None (
     :return: failed process name, failed stream name, "size_limit" or None (
-    no error)
+        no error)
     '''
     '''
     buffer_size = 409600
     buffer_size = 409600
     bytes_copied = 0
     bytes_copied = 0

+ 2 - 2
qubes/dochelpers.py

@@ -249,7 +249,7 @@ class OptionsCheckVisitor(docutils.nodes.SparseNodeVisitor):
             While the documentation talks about a
             While the documentation talks about a
             'SparseNodeVisitor.depart_document()' function, this function does
             'SparseNodeVisitor.depart_document()' function, this function does
             not exists. (For details see implementation of
             not exists. (For details see implementation of
-            :py:method:`NodeVisitor.dispatch_departure()`) So we need to
+            :py:meth:`NodeVisitor.dispatch_departure()`) So we need to
             manually call this.
             manually call this.
         '''
         '''
         if ignored_options is None:
         if ignored_options is None:
@@ -316,7 +316,7 @@ class CommandCheckVisitor(docutils.nodes.SparseNodeVisitor):
             While the documentation talks about a
             While the documentation talks about a
             'SparseNodeVisitor.depart_document()' function, this function does
             'SparseNodeVisitor.depart_document()' function, this function does
             not exists. (For details see implementation of
             not exists. (For details see implementation of
-            :py:method:`NodeVisitor.dispatch_departure()`) So we need to
+            :py:meth:`NodeVisitor.dispatch_departure()`) So we need to
             manually call this.
             manually call this.
         '''
         '''
         if self.sub_commands:
         if self.sub_commands:

+ 1 - 1
qubes/tests/__init__.py

@@ -177,7 +177,7 @@ def expectedFailureIfTemplate(templates):
     Decorator for marking specific test as expected to fail only for some
     Decorator for marking specific test as expected to fail only for some
     templates. Template name is compared as substring, so 'whonix' will
     templates. Template name is compared as substring, so 'whonix' will
     handle both 'whonix-ws' and 'whonix-gw'.
     handle both 'whonix-ws' and 'whonix-gw'.
-     templates can be either a single string, or an iterable
+    templates can be either a single string, or an iterable
     """
     """
     def decorator(func):
     def decorator(func):
         @functools.wraps(func)
         @functools.wraps(func)

+ 7 - 5
qubes/tools/__init__.py

@@ -303,11 +303,13 @@ class QubesArgumentParser(argparse.ArgumentParser):
         :py:class:`qubes.Qubes` object, just add argument for custom xml file
         :py:class:`qubes.Qubes` object, just add argument for custom xml file
     :param bool want_force_root: add ``--force-root`` option
     :param bool want_force_root: add ``--force-root`` option
     :param mixed vmname_nargs: The number of ``VMNAME`` arguments that should be
     :param mixed vmname_nargs: The number of ``VMNAME`` arguments that should be
-            consumed. Values include:
-                - N (an integer) consumes N arguments (and produces a list)
-                - '?' consumes zero or one arguments
-                - '*' consumes zero or more arguments (and produces a list)
-                - '+' consumes one or more arguments (and produces a list)
+        consumed. Values include:
+
+            - N (an integer) consumes N arguments (and produces a list)
+            - '?' consumes zero or one arguments
+            - '*' consumes zero or more arguments (and produces a list)
+            - '+' consumes one or more arguments (and produces a list)
+
     *kwargs* are passed to :py:class:`argparser.ArgumentParser`.
     *kwargs* are passed to :py:class:`argparser.ArgumentParser`.
 
 
     Currenty supported options:
     Currenty supported options:

+ 4 - 4
qubes/vm/qubesvm.py

@@ -594,8 +594,8 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
 
 
     @property
     @property
     def block_devices(self):
     def block_devices(self):
-        ''' Return all :py:class:`qubes.storage.BlockDevice`s for current domain
-            for serialization in the libvirt XML template as <disk>.
+        ''' Return all :py:class:`qubes.storage.BlockDevice` for current domain
+        for serialization in the libvirt XML template as <disk>.
         '''
         '''
         for v in self.volumes.values():
         for v in self.volumes.values():
             block_dev = v.block_device()
             block_dev = v.block_device()
@@ -1050,7 +1050,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
 
 
         :param str service: service name
         :param str service: service name
         :param qubes.vm.qubesvm.QubesVM source: source domain as presented to
         :param qubes.vm.qubesvm.QubesVM source: source domain as presented to
-        this VM
+            this VM
         :param str user: username to run service as
         :param str user: username to run service as
         :param bool filter_esc: filter escape sequences to protect terminal \
         :param bool filter_esc: filter escape sequences to protect terminal \
             emulator
             emulator
@@ -1221,7 +1221,7 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
         This function take care to run it as appropriate user.
         This function take care to run it as appropriate user.
 
 
         :param command: command to run (array for
         :param command: command to run (array for
-        :py:meth:`subprocess.check_call`)
+            :py:meth:`subprocess.check_call`)
         :param kwargs: args for :py:meth:`subprocess.check_call`
         :param kwargs: args for :py:meth:`subprocess.check_call`
         :return: None
         :return: None
         '''  # pylint: disable=redefined-builtin
         '''  # pylint: disable=redefined-builtin

+ 56 - 22
qubespolicy/__init__.py

@@ -66,6 +66,8 @@ def verify_target_value(system_info, value):
     '''
     '''
     if value == '$dispvm':
     if value == '$dispvm':
         return True
         return True
+    elif value == '$adminvm':
+        return True
     elif value.startswith('$dispvm:'):
     elif value.startswith('$dispvm:'):
         dispvm_base = value.split(':', 1)[1]
         dispvm_base = value.split(':', 1)[1]
         if dispvm_base not in system_info['domains']:
         if dispvm_base not in system_info['domains']:
@@ -82,7 +84,7 @@ def verify_special_value(value, for_target=True):
 
 
     :param value: value to verify
     :param value: value to verify
     :param for_target: should classify target-only values as valid (
     :param for_target: should classify target-only values as valid (
-    '$default', '$dispvm')
+        '$default', '$dispvm')
     :return: True or False
     :return: True or False
     '''
     '''
     # pylint: disable=too-many-return-statements
     # pylint: disable=too-many-return-statements
@@ -93,6 +95,8 @@ def verify_special_value(value, for_target=True):
         return True
         return True
     elif value == '$anyvm':
     elif value == '$anyvm':
         return True
         return True
+    elif value == '$adminvm':
+        return True
     elif value.startswith('$dispvm:') and for_target:
     elif value.startswith('$dispvm:') and for_target:
         return True
         return True
     elif value == '$dispvm' and for_target:
     elif value == '$dispvm' and for_target:
@@ -121,11 +125,11 @@ class PolicyRule(object):
         self.filename = filename
         self.filename = filename
 
 
         try:
         try:
-            self.source, self.target, self.full_action = line.split()
+            self.source, self.target, self.full_action = line.split(maxsplit=2)
         except ValueError:
         except ValueError:
             raise PolicySyntaxError(filename, lineno, 'wrong number of fields')
             raise PolicySyntaxError(filename, lineno, 'wrong number of fields')
 
 
-        (action, *params) = self.full_action.split(',')
+        (action, *params) = self.full_action.replace(',', ' ').split()
         try:
         try:
             self.action = Action[action]
             self.action = Action[action]
         except KeyError:
         except KeyError:
@@ -184,8 +188,9 @@ class PolicyRule(object):
                 'allow action for $default rule must specify target= option')
                 'allow action for $default rule must specify target= option')
 
 
         if self.override_target is not None:
         if self.override_target is not None:
-            if self.override_target.startswith('$') and not \
-                    self.override_target.startswith('$dispvm'):
+            if self.override_target.startswith('$') and \
+                    not self.override_target.startswith('$dispvm') and \
+                    self.override_target != '$adminvm':
                 raise PolicySyntaxError(filename, lineno,
                 raise PolicySyntaxError(filename, lineno,
                     'target= option needs to name specific target')
                     'target= option needs to name specific target')
 
 
@@ -197,7 +202,7 @@ class PolicyRule(object):
 
 
         :param system_info: information about the system
         :param system_info: information about the system
         :param policy_value: value from qrexec policy (either self.source or
         :param policy_value: value from qrexec policy (either self.source or
-        self.target)
+            self.target)
         :param value: value to be compared (source or target)
         :param value: value to be compared (source or target)
         :return: True or False
         :return: True or False
         '''
         '''
@@ -216,11 +221,19 @@ class PolicyRule(object):
         if not verify_target_value(system_info, value):
         if not verify_target_value(system_info, value):
             return False
             return False
 
 
+        # handle $adminvm keyword
+        if policy_value == 'dom0':
+            # TODO: log a warning in Qubes 4.1
+            policy_value = '$adminvm'
+
+        if value == 'dom0':
+            value = '$adminvm'
+
         # allow any _valid_, non-dom0 target
         # allow any _valid_, non-dom0 target
         if policy_value == '$anyvm':
         if policy_value == '$anyvm':
-            return value != 'dom0'
+            return value != '$adminvm'
 
 
-        # exact match, including $dispvm*
+        # exact match, including $dispvm* and $adminvm
         if value == policy_value:
         if value == policy_value:
             return True
             return True
 
 
@@ -229,6 +242,11 @@ class PolicyRule(object):
         if value.startswith('$dispvm'):
         if value.startswith('$dispvm'):
             return False
             return False
 
 
+        # require $adminvm to be matched explicitly (not through $tag or $type)
+        # - if not matched already, reject it
+        if value == '$adminvm':
+            return False
+
         # at this point, value name a specific target
         # at this point, value name a specific target
         domain_info = system_info['domains'][value]
         domain_info = system_info['domains'][value]
 
 
@@ -247,8 +265,8 @@ class PolicyRule(object):
         Check if given (source, target) matches this policy line.
         Check if given (source, target) matches this policy line.
 
 
         :param system_info: information about the system - available VMs,
         :param system_info: information about the system - available VMs,
-        their types, labels, tags etc. as returned by
-        :py:func:`app_to_system_info`
+            their types, labels, tags etc. as returned by
+            :py:func:`app_to_system_info`
         :param source: name of the source VM
         :param source: name of the source VM
         :param target: name of the target VM, or None if not specified
         :param target: name of the target VM, or None if not specified
         :return: True or False
         :return: True or False
@@ -293,6 +311,8 @@ class PolicyRule(object):
             except KeyError:
             except KeyError:
                 # TODO log a warning?
                 # TODO log a warning?
                 pass
                 pass
+        elif self.target == '$adminvm':
+            yield self.target
         elif self.target == '$dispvm':
         elif self.target == '$dispvm':
             yield self.target
             yield self.target
         else:
         else:
@@ -372,12 +392,14 @@ class PolicyAction(object):
     def execute(self, caller_ident):
     def execute(self, caller_ident):
         ''' Execute allowed service call
         ''' Execute allowed service call
 
 
-        :param caller_ident: Service caller ident (`process_ident,source_name,
-        source_id`)
+        :param caller_ident: Service caller ident
+            (`process_ident,source_name, source_id`)
         '''
         '''
         assert self.action == Action.allow
         assert self.action == Action.allow
         assert self.target is not None
         assert self.target is not None
 
 
+        if self.target == '$adminvm':
+            self.target = 'dom0'
         if self.target == 'dom0':
         if self.target == 'dom0':
             cmd = '{multiplexer} {service} {source} {original_target}'.format(
             cmd = '{multiplexer} {service} {source} {original_target}'.format(
                 multiplexer=QUBES_RPC_MULTIPLEXER_PATH,
                 multiplexer=QUBES_RPC_MULTIPLEXER_PATH,
@@ -451,17 +473,20 @@ class Policy(object):
     >>> policy = Policy('some-service')
     >>> policy = Policy('some-service')
     >>> action = policy.evaluate(system_info, 'source-name', 'target-name')
     >>> action = policy.evaluate(system_info, 'source-name', 'target-name')
     >>> if action.action == Action.ask:
     >>> if action.action == Action.ask:
-            (... ask the user, see action.targets_for_ask ...)
+    >>>     # ... ask the user, see action.targets_for_ask ...
     >>>     action.handle_user_response(response, target_chosen_by_user)
     >>>     action.handle_user_response(response, target_chosen_by_user)
     >>> action.execute('process-ident')
     >>> action.execute('process-ident')
 
 
     '''
     '''
 
 
-    def __init__(self, service):
-        policy_file = os.path.join(POLICY_DIR, service)
+    def __init__(self, service, policy_dir=POLICY_DIR):
+        policy_file = os.path.join(policy_dir, service)
         if not os.path.exists(policy_file):
         if not os.path.exists(policy_file):
             # fallback to policy without specific argument set (if any)
             # fallback to policy without specific argument set (if any)
-            policy_file = os.path.join(POLICY_DIR, service.split('+')[0])
+            policy_file = os.path.join(policy_dir, service.split('+')[0])
+
+        #: policy storage directory
+        self.policy_dir = policy_dir
 
 
         #: service name
         #: service name
         self.service = service
         self.service = service
@@ -493,7 +518,7 @@ class Policy(object):
                     include_path = line.split(':', 1)[1]
                     include_path = line.split(':', 1)[1]
                     # os.path.join will leave include_path unchanged if it's
                     # os.path.join will leave include_path unchanged if it's
                     # already absolute
                     # already absolute
-                    include_path = os.path.join(POLICY_DIR, include_path)
+                    include_path = os.path.join(self.policy_dir, include_path)
                     self.load_policy_file(include_path)
                     self.load_policy_file(include_path)
                 else:
                 else:
                     self.policy_rules.append(PolicyRule(line, path, lineno))
                     self.policy_rules.append(PolicyRule(line, path, lineno))
@@ -582,6 +607,15 @@ class Policy(object):
                     'policy define \'allow\' action at {}:{} but no target is '
                     'policy define \'allow\' action at {}:{} but no target is '
                     'specified by caller or policy'.format(
                     'specified by caller or policy'.format(
                         rule.filename, rule.lineno))
                         rule.filename, rule.lineno))
+            if actual_target == '$dispvm':
+                if system_info['domains'][source]['default_dispvm'] is None:
+                    raise AccessDenied(
+                        'policy define \'allow\' action to $dispvm at {}:{} '
+                        'but no DispVM base is set for this VM'.format(
+                            rule.filename, rule.lineno))
+                actual_target = '$dispvm:' + \
+                    system_info['domains'][source]['default_dispvm']
+
             return PolicyAction(self.service, source,
             return PolicyAction(self.service, source,
                 actual_target, rule, target)
                 actual_target, rule, target)
         else:
         else:
@@ -634,11 +668,11 @@ def get_system_info():
     data is nested dict structure with this structure:
     data is nested dict structure with this structure:
 
 
     - domains:
     - 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
+       - `<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
 
 
     '''
     '''
 
 

+ 122 - 0
qubespolicy/graph.py

@@ -0,0 +1,122 @@
+# -*- encoding: utf8 -*-
+#
+# The Qubes OS Project, http://www.qubes-os.org
+#
+# Copyright (C) 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, see <http://www.gnu.org/licenses/>.
+
+import argparse
+import json
+import os
+
+import sys
+
+import qubespolicy
+
+parser = argparse.ArgumentParser(description='Graph qrexec policy')
+parser.add_argument('--include-ask', action='store_true',
+    help='Include `ask` action in graph')
+parser.add_argument('--source', action='store', nargs='+',
+    help='Limit graph to calls from *source*')
+parser.add_argument('--target', action='store', nargs='+',
+    help='Limit graph to calls to *target*')
+parser.add_argument('--service', action='store', nargs='+',
+    help='Limit graph to *service*')
+parser.add_argument('--output', action='store',
+    help='Write to *output* instead of stdout')
+parser.add_argument('--policy-dir', action='store',
+    default=qubespolicy.POLICY_DIR,
+    help='Look for policy in *policy-dir*')
+parser.add_argument('--system-info', action='store',
+    help='Load system information from file instead of querying qubesd')
+parser.add_argument('--skip-labels', action='store_true',
+    help='Do not include service names on the graph, also deduplicate '
+         'connections.')
+
+def handle_single_action(args, action):
+    '''Get single policy action and output (or not) a line to add'''
+    if args.skip_labels:
+        service = ''
+    else:
+        service = action.service
+    if action.action == qubespolicy.Action.ask:
+        if args.include_ask:
+            # handle forced target=
+            if len(action.targets_for_ask) == 1:
+                return '  "{}" -> "{}" [label="{}" color=orange];\n'.format(
+                    action.source, action.targets_for_ask[0], service)
+            return '  "{}" -> "{}" [label="{}" color=orange];\n'.format(
+                action.source, action.original_target, service)
+    elif action.action == qubespolicy.Action.allow:
+        return '  "{}" -> "{}" [label="{}" color=red];\n'.format(
+                action.source, action.target, service)
+    return ''
+
+def main(args=None):
+    args = parser.parse_args(args)
+
+    output = sys.stdout
+    if args.output:
+        output = open(args.output, 'w')
+
+    if args.system_info:
+        with open(args.system_info) as f_system_info:
+            system_info = json.load(f_system_info)
+    else:
+        system_info = qubespolicy.get_system_info()
+
+    sources = list(system_info['domains'].keys())
+    if args.source:
+        sources = args.source
+
+    targets = list(system_info['domains'].keys())
+    if args.target:
+        targets = args.target
+    else:
+        targets.append('$dispvm')
+        targets.extend('$dispvm:' + dom for dom in system_info['domains']
+            if system_info['domains'][dom]['dispvm_allowed'])
+
+    connections = set()
+
+    output.write('digraph g {\n')
+    for service in os.listdir(args.policy_dir):
+        if os.path.isdir(os.path.join(args.policy_dir, service)):
+            continue
+        if args.service and service not in args.service and \
+                not any(service.startswith(srv + '+') for srv in args.service):
+            continue
+
+        policy = qubespolicy.Policy(service, args.policy_dir)
+        for source in sources:
+            for target in targets:
+                try:
+                    action = policy.evaluate(system_info, source, target)
+                    line = handle_single_action(args, action)
+                    if line in connections:
+                        continue
+                    if line:
+                        output.write(line)
+                    connections.add(line)
+                except qubespolicy.AccessDenied:
+                    continue
+
+    output.write('}\n')
+    if args.output:
+        output.close()
+
+if __name__ == '__main__':
+    sys.exit(main())

+ 63 - 13
qubespolicy/tests/__init__.py

@@ -31,7 +31,7 @@ tmp_policy_dir = '/tmp/policy'
 system_info = {
 system_info = {
     'domains': {
     'domains': {
         'dom0': {
         'dom0': {
-            'tags': [],
+            'tags': ['dom0-tag'],
             'type': 'AdminVM',
             'type': 'AdminVM',
             'default_dispvm': 'default-dvm',
             'default_dispvm': 'default-dvm',
             'dispvm_allowed': False,
             'dispvm_allowed': False,
@@ -102,6 +102,8 @@ class TC_00_PolicyRule(qubes.tests.QubesTestCase):
             qubespolicy.verify_target_value(system_info, 'test-template'))
             qubespolicy.verify_target_value(system_info, 'test-template'))
         self.assertTrue(
         self.assertTrue(
             qubespolicy.verify_target_value(system_info, 'test-standalone'))
             qubespolicy.verify_target_value(system_info, 'test-standalone'))
+        self.assertTrue(
+            qubespolicy.verify_target_value(system_info, '$adminvm'))
         self.assertFalse(
         self.assertFalse(
             qubespolicy.verify_target_value(system_info, 'no-such-vm'))
             qubespolicy.verify_target_value(system_info, 'no-such-vm'))
         self.assertFalse(
         self.assertFalse(
@@ -127,6 +129,8 @@ class TC_00_PolicyRule(qubes.tests.QubesTestCase):
             for_target=False))
             for_target=False))
         self.assertTrue(qubespolicy.verify_special_value('$type:AppVM',
         self.assertTrue(qubespolicy.verify_special_value('$type:AppVM',
             for_target=False))
             for_target=False))
+        self.assertTrue(qubespolicy.verify_special_value('$adminvm',
+            for_target=False))
         self.assertFalse(qubespolicy.verify_special_value('$default',
         self.assertFalse(qubespolicy.verify_special_value('$default',
             for_target=False))
             for_target=False))
         self.assertFalse(qubespolicy.verify_special_value('$dispvm',
         self.assertFalse(qubespolicy.verify_special_value('$dispvm',
@@ -155,15 +159,16 @@ class TC_00_PolicyRule(qubes.tests.QubesTestCase):
         self.assertIsNone(line.default_target)
         self.assertIsNone(line.default_target)
 
 
     def test_021_line_simple(self):
     def test_021_line_simple(self):
+        # also check spaces in action field
         line = qubespolicy.PolicyRule(
         line = qubespolicy.PolicyRule(
-            '$tag:tag1 $type:AppVM ask,target=test-vm2,user=user',
+            '$tag:tag1 $type:AppVM ask, target=test-vm2, user=user',
             'filename', 12)
             'filename', 12)
         self.assertEqual(line.filename, 'filename')
         self.assertEqual(line.filename, 'filename')
         self.assertEqual(line.lineno, 12)
         self.assertEqual(line.lineno, 12)
         self.assertEqual(line.action, qubespolicy.Action.ask)
         self.assertEqual(line.action, qubespolicy.Action.ask)
         self.assertEqual(line.source, '$tag:tag1')
         self.assertEqual(line.source, '$tag:tag1')
         self.assertEqual(line.target, '$type:AppVM')
         self.assertEqual(line.target, '$type:AppVM')
-        self.assertEqual(line.full_action, 'ask,target=test-vm2,user=user')
+        self.assertEqual(line.full_action, 'ask, target=test-vm2, user=user')
         self.assertEqual(line.override_target, 'test-vm2')
         self.assertEqual(line.override_target, 'test-vm2')
         self.assertEqual(line.override_user, 'user')
         self.assertEqual(line.override_user, 'user')
         self.assertIsNone(line.default_target)
         self.assertIsNone(line.default_target)
@@ -196,6 +201,20 @@ class TC_00_PolicyRule(qubes.tests.QubesTestCase):
         self.assertIsNone(line.override_user)
         self.assertIsNone(line.override_user)
         self.assertEqual(line.default_target, 'test-vm1')
         self.assertEqual(line.default_target, 'test-vm1')
 
 
+    def test_024_line_simple(self):
+        line = qubespolicy.PolicyRule(
+            '$anyvm $adminvm ask,default_target=$adminvm',
+            'filename', 12)
+        self.assertEqual(line.filename, 'filename')
+        self.assertEqual(line.lineno, 12)
+        self.assertEqual(line.action, qubespolicy.Action.ask)
+        self.assertEqual(line.source, '$anyvm')
+        self.assertEqual(line.target, '$adminvm')
+        self.assertEqual(line.full_action, 'ask,default_target=$adminvm')
+        self.assertIsNone(line.override_target)
+        self.assertIsNone(line.override_user)
+        self.assertEqual(line.default_target, '$adminvm')
+
     def test_030_line_invalid(self):
     def test_030_line_invalid(self):
         invalid_lines = [
         invalid_lines = [
             '$dispvm $default allow',  # $dispvm can't be a source
             '$dispvm $default allow',  # $dispvm can't be a source
@@ -235,6 +254,9 @@ class TC_00_PolicyRule(qubes.tests.QubesTestCase):
         self.assertTrue(is_match_single(system_info,
         self.assertTrue(is_match_single(system_info,
             '$anyvm', '$dispvm:default-dvm'))
             '$anyvm', '$dispvm:default-dvm'))
         self.assertTrue(is_match_single(system_info, '$dispvm', '$dispvm'))
         self.assertTrue(is_match_single(system_info, '$dispvm', '$dispvm'))
+        self.assertTrue(is_match_single(system_info, '$adminvm', '$adminvm'))
+        self.assertTrue(is_match_single(system_info, '$adminvm', 'dom0'))
+        self.assertTrue(is_match_single(system_info, 'dom0', '$adminvm'))
         self.assertTrue(is_match_single(system_info, 'dom0', 'dom0'))
         self.assertTrue(is_match_single(system_info, 'dom0', 'dom0'))
         self.assertTrue(is_match_single(system_info,
         self.assertTrue(is_match_single(system_info,
             '$dispvm:default-dvm', '$dispvm:default-dvm'))
             '$dispvm:default-dvm', '$dispvm:default-dvm'))
@@ -253,6 +275,15 @@ class TC_00_PolicyRule(qubes.tests.QubesTestCase):
         self.assertFalse(is_match_single(system_info,
         self.assertFalse(is_match_single(system_info,
             '$dispvm:test-vm1', '$dispvm:test-vm1'))
             '$dispvm:test-vm1', '$dispvm:test-vm1'))
         self.assertFalse(is_match_single(system_info, '$anyvm', 'dom0'))
         self.assertFalse(is_match_single(system_info, '$anyvm', 'dom0'))
+        self.assertFalse(is_match_single(system_info, '$anyvm', '$adminvm'))
+        self.assertFalse(is_match_single(system_info,
+            '$tag:dom0-tag', '$adminvm'))
+        self.assertFalse(is_match_single(system_info,
+            '$type:AdminVM', '$adminvm'))
+        self.assertFalse(is_match_single(system_info,
+            '$tag:dom0-tag', 'dom0'))
+        self.assertFalse(is_match_single(system_info,
+            '$type:AdminVM', 'dom0'))
         self.assertFalse(is_match_single(system_info, '$tag:tag1', 'dom0'))
         self.assertFalse(is_match_single(system_info, '$tag:tag1', 'dom0'))
         self.assertFalse(is_match_single(system_info, '$anyvm', '$tag:tag1'))
         self.assertFalse(is_match_single(system_info, '$anyvm', '$tag:tag1'))
         self.assertFalse(is_match_single(system_info, '$anyvm', '$type:AppVM'))
         self.assertFalse(is_match_single(system_info, '$anyvm', '$type:AppVM'))
@@ -338,6 +369,13 @@ class TC_00_PolicyRule(qubes.tests.QubesTestCase):
             line.expand_override_target(system_info, 'test-no-dvm'),
             line.expand_override_target(system_info, 'test-no-dvm'),
             'dom0')
             'dom0')
 
 
+    def test_075_expand_override_target_dom0(self):
+        line = qubespolicy.PolicyRule(
+            '$anyvm $anyvm allow,target=$adminvm')
+        self.assertEqual(
+            line.expand_override_target(system_info, 'test-no-dvm'),
+            '$adminvm')
+
 
 
 class TC_10_PolicyAction(qubes.tests.QubesTestCase):
 class TC_10_PolicyAction(qubes.tests.QubesTestCase):
     def test_000_init(self):
     def test_000_init(self):
@@ -485,7 +523,6 @@ class TC_10_PolicyAction(qubes.tests.QubesTestCase):
             [unittest.mock.call('test-vm2', 'internal.vm.Start')])
             [unittest.mock.call('test-vm2', 'internal.vm.Start')])
         self.assertEqual(mock_subprocess.mock_calls, [])
         self.assertEqual(mock_subprocess.mock_calls, [])
 
 
-@unittest.mock.patch('qubespolicy.POLICY_DIR', tmp_policy_dir)
 class TC_20_Policy(qubes.tests.QubesTestCase):
 class TC_20_Policy(qubes.tests.QubesTestCase):
 
 
     def setUp(self):
     def setUp(self):
@@ -505,7 +542,7 @@ class TC_20_Policy(qubes.tests.QubesTestCase):
             f.write('test-vm2 test-vm3 ask\n')
             f.write('test-vm2 test-vm3 ask\n')
             f.write('   # comment  \n')
             f.write('   # comment  \n')
             f.write('$anyvm $anyvm ask\n')
             f.write('$anyvm $anyvm ask\n')
-        policy = qubespolicy.Policy('test.service')
+        policy = qubespolicy.Policy('test.service', tmp_policy_dir)
         self.assertEqual(policy.service, 'test.service')
         self.assertEqual(policy.service, 'test.service')
         self.assertEqual(len(policy.policy_rules), 3)
         self.assertEqual(len(policy.policy_rules), 3)
         self.assertEqual(policy.policy_rules[0].source, 'test-vm1')
         self.assertEqual(policy.policy_rules[0].source, 'test-vm1')
@@ -515,7 +552,7 @@ class TC_20_Policy(qubes.tests.QubesTestCase):
 
 
     def test_001_not_existent(self):
     def test_001_not_existent(self):
         with self.assertRaises(qubespolicy.AccessDenied):
         with self.assertRaises(qubespolicy.AccessDenied):
-            qubespolicy.Policy('no-such.service')
+            qubespolicy.Policy('no-such.service', tmp_policy_dir)
 
 
     def test_002_include(self):
     def test_002_include(self):
         with open(os.path.join(tmp_policy_dir, 'test.service'), 'w') as f:
         with open(os.path.join(tmp_policy_dir, 'test.service'), 'w') as f:
@@ -524,7 +561,7 @@ class TC_20_Policy(qubes.tests.QubesTestCase):
             f.write('$anyvm $anyvm deny\n')
             f.write('$anyvm $anyvm deny\n')
         with open(os.path.join(tmp_policy_dir, 'test.service2'), 'w') as f:
         with open(os.path.join(tmp_policy_dir, 'test.service2'), 'w') as f:
             f.write('test-vm3 $default allow,target=test-vm2\n')
             f.write('test-vm3 $default allow,target=test-vm2\n')
-        policy = qubespolicy.Policy('test.service')
+        policy = qubespolicy.Policy('test.service', tmp_policy_dir)
         self.assertEqual(policy.service, 'test.service')
         self.assertEqual(policy.service, 'test.service')
         self.assertEqual(len(policy.policy_rules), 3)
         self.assertEqual(len(policy.policy_rules), 3)
         self.assertEqual(policy.policy_rules[0].source, 'test-vm1')
         self.assertEqual(policy.policy_rules[0].source, 'test-vm1')
@@ -557,7 +594,7 @@ class TC_20_Policy(qubes.tests.QubesTestCase):
             f.write('test-vm2 $tag:tag2 allow\n')
             f.write('test-vm2 $tag:tag2 allow\n')
             f.write('$type:AppVM $default allow,target=test-vm3\n')
             f.write('$type:AppVM $default allow,target=test-vm3\n')
             f.write('$tag:tag1 $type:AppVM allow\n')
             f.write('$tag:tag1 $type:AppVM allow\n')
-        policy = qubespolicy.Policy('test.service')
+        policy = qubespolicy.Policy('test.service', tmp_policy_dir)
         self.assertEqual(policy.find_matching_rule(
         self.assertEqual(policy.find_matching_rule(
             system_info, 'test-vm1', 'test-vm2'), policy.policy_rules[0])
             system_info, 'test-vm1', 'test-vm2'), policy.policy_rules[0])
         self.assertEqual(policy.find_matching_rule(
         self.assertEqual(policy.find_matching_rule(
@@ -593,7 +630,7 @@ class TC_20_Policy(qubes.tests.QubesTestCase):
             f.write('$tag:tag1 $type:AppVM allow\n')
             f.write('$tag:tag1 $type:AppVM allow\n')
             f.write('test-no-dvm $dispvm allow\n')
             f.write('test-no-dvm $dispvm allow\n')
             f.write('test-standalone $dispvm allow\n')
             f.write('test-standalone $dispvm allow\n')
-        policy = qubespolicy.Policy('test.service')
+        policy = qubespolicy.Policy('test.service', tmp_policy_dir)
         self.assertCountEqual(policy.collect_targets_for_ask(system_info,
         self.assertCountEqual(policy.collect_targets_for_ask(system_info,
             'test-vm1'), ['test-vm1', 'test-vm2', 'test-vm3',
             'test-vm1'), ['test-vm1', 'test-vm2', 'test-vm3',
                 '$dispvm:test-vm3',
                 '$dispvm:test-vm3',
@@ -614,7 +651,7 @@ class TC_20_Policy(qubes.tests.QubesTestCase):
         with open(os.path.join(tmp_policy_dir, 'test.service'), 'w') as f:
         with open(os.path.join(tmp_policy_dir, 'test.service'), 'w') as f:
             f.write('test-vm1 test-vm2 allow\n')
             f.write('test-vm1 test-vm2 allow\n')
 
 
-        policy = qubespolicy.Policy('test.service')
+        policy = qubespolicy.Policy('test.service', tmp_policy_dir)
         action = policy.evaluate(system_info, 'test-vm1', 'test-vm2')
         action = policy.evaluate(system_info, 'test-vm1', 'test-vm2')
         self.assertEqual(action.rule, policy.policy_rules[0])
         self.assertEqual(action.rule, policy.policy_rules[0])
         self.assertEqual(action.action, qubespolicy.Action.allow)
         self.assertEqual(action.action, qubespolicy.Action.allow)
@@ -633,7 +670,7 @@ class TC_20_Policy(qubes.tests.QubesTestCase):
             f.write('$tag:tag2 $anyvm allow\n')
             f.write('$tag:tag2 $anyvm allow\n')
             f.write('test-vm3 $anyvm deny\n')
             f.write('test-vm3 $anyvm deny\n')
 
 
-        policy = qubespolicy.Policy('test.service')
+        policy = qubespolicy.Policy('test.service', tmp_policy_dir)
         action = policy.evaluate(system_info, 'test-vm1', '$default')
         action = policy.evaluate(system_info, 'test-vm1', '$default')
         self.assertEqual(action.rule, policy.policy_rules[1])
         self.assertEqual(action.rule, policy.policy_rules[1])
         self.assertEqual(action.action, qubespolicy.Action.allow)
         self.assertEqual(action.action, qubespolicy.Action.allow)
@@ -655,7 +692,7 @@ class TC_20_Policy(qubes.tests.QubesTestCase):
             f.write('$tag:tag2 $anyvm allow\n')
             f.write('$tag:tag2 $anyvm allow\n')
             f.write('test-vm3 $anyvm deny\n')
             f.write('test-vm3 $anyvm deny\n')
 
 
-        policy = qubespolicy.Policy('test.service')
+        policy = qubespolicy.Policy('test.service', tmp_policy_dir)
         action = policy.evaluate(system_info, 'test-standalone', 'test-vm2')
         action = policy.evaluate(system_info, 'test-standalone', 'test-vm2')
         self.assertEqual(action.rule, policy.policy_rules[2])
         self.assertEqual(action.rule, policy.policy_rules[2])
         self.assertEqual(action.action, qubespolicy.Action.ask)
         self.assertEqual(action.action, qubespolicy.Action.ask)
@@ -676,7 +713,7 @@ class TC_20_Policy(qubes.tests.QubesTestCase):
             f.write('$tag:tag2 $anyvm allow\n')
             f.write('$tag:tag2 $anyvm allow\n')
             f.write('test-vm3 $anyvm deny\n')
             f.write('test-vm3 $anyvm deny\n')
 
 
-        policy = qubespolicy.Policy('test.service')
+        policy = qubespolicy.Policy('test.service', tmp_policy_dir)
         action = policy.evaluate(system_info, 'test-standalone', 'test-vm3')
         action = policy.evaluate(system_info, 'test-standalone', 'test-vm3')
         self.assertEqual(action.rule, policy.policy_rules[3])
         self.assertEqual(action.rule, policy.policy_rules[3])
         self.assertEqual(action.action, qubespolicy.Action.ask)
         self.assertEqual(action.action, qubespolicy.Action.ask)
@@ -688,6 +725,19 @@ class TC_20_Policy(qubes.tests.QubesTestCase):
                 'default-dvm', '$dispvm:default-dvm', 'test-invalid-dvm',
                 'default-dvm', '$dispvm:default-dvm', 'test-invalid-dvm',
                 'test-no-dvm', 'test-template', 'test-standalone'])
                 'test-no-dvm', 'test-template', 'test-standalone'])
 
 
+    def test_034_eval_resolve_dispvm(self):
+        with open(os.path.join(tmp_policy_dir, 'test.service'), 'w') as f:
+            f.write('test-vm3 $dispvm allow\n')
+
+        policy = qubespolicy.Policy('test.service', tmp_policy_dir)
+        action = policy.evaluate(system_info, 'test-vm3', '$dispvm')
+        self.assertEqual(action.rule, policy.policy_rules[0])
+        self.assertEqual(action.action, qubespolicy.Action.allow)
+        self.assertEqual(action.target, '$dispvm:default-dvm')
+        self.assertEqual(action.original_target, '$dispvm')
+        self.assertEqual(action.service, 'test.service')
+        self.assertIsNone(action.targets_for_ask)
+
 
 
 class TC_30_Misc(qubes.tests.QubesTestCase):
 class TC_30_Misc(qubes.tests.QubesTestCase):
     @unittest.mock.patch('socket.socket')
     @unittest.mock.patch('socket.socket')

+ 7 - 1
rpm_spec/core-dom0.spec

@@ -213,7 +213,9 @@ fi
 /usr/bin/qubesd*
 /usr/bin/qubesd*
 /usr/bin/qrexec-policy
 /usr/bin/qrexec-policy
 /usr/bin/qrexec-policy-agent
 /usr/bin/qrexec-policy-agent
+/usr/bin/qrexec-policy-graph
 
 
+%{_mandir}/man1/qrexec-policy-graph.1*
 %{_mandir}/man1/qubes*.1*
 %{_mandir}/man1/qubes*.1*
 
 
 %dir %{python3_sitelib}/qubes-*.egg-info
 %dir %{python3_sitelib}/qubes-*.egg-info
@@ -372,6 +374,7 @@ fi
 %{python3_sitelib}/qubespolicy/gtkhelpers.py
 %{python3_sitelib}/qubespolicy/gtkhelpers.py
 %{python3_sitelib}/qubespolicy/rpcconfirmation.py
 %{python3_sitelib}/qubespolicy/rpcconfirmation.py
 %{python3_sitelib}/qubespolicy/utils.py
 %{python3_sitelib}/qubespolicy/utils.py
+%{python3_sitelib}/qubespolicy/graph.py
 
 
 %dir %{python3_sitelib}/qubespolicy/tests
 %dir %{python3_sitelib}/qubespolicy/tests
 %dir %{python3_sitelib}/qubespolicy/tests/__pycache__
 %dir %{python3_sitelib}/qubespolicy/tests/__pycache__
@@ -410,7 +413,10 @@ fi
 /etc/xen/scripts/block-origin
 /etc/xen/scripts/block-origin
 /etc/xen/scripts/vif-route-qubes
 /etc/xen/scripts/vif-route-qubes
 %attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/admin.*
 %attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/admin.*
-%attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/include/admin-all
+%attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/include/admin-local-ro
+%attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/include/admin-local-rwx
+%attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/include/admin-global-ro
+%attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/include/admin-global-rwx
 %attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/qubes.FeaturesRequest
 %attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/qubes.FeaturesRequest
 %attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/qubes.Filecopy
 %attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/qubes.Filecopy
 %attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/qubes.GetImageRGBA
 %attr(0664,root,qubes) %config(noreplace) /etc/qubes-rpc/policy/qubes.GetImageRGBA

+ 1 - 0
setup.py

@@ -34,6 +34,7 @@ if __name__ == '__main__':
             'console_scripts': list(get_console_scripts()) + [
             'console_scripts': list(get_console_scripts()) + [
                 'qrexec-policy = qubespolicy.cli:main',
                 'qrexec-policy = qubespolicy.cli:main',
                 'qrexec-policy-agent = qubespolicy.agent:main',
                 'qrexec-policy-agent = qubespolicy.agent:main',
+                'qrexec-policy-graph = qubespolicy.graph:main',
             ],
             ],
             'qubes.vm': [
             'qubes.vm': [
                 'AppVM = qubes.vm.appvm:AppVM',
                 'AppVM = qubes.vm.appvm:AppVM',