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

This commit is contained in:
Marek Marczykowski-Górecki 2017-07-04 14:51:51 +02:00
commit 51022cada5
No known key found for this signature in database
GPG Key ID: 063938BA42CFA724
24 changed files with 657 additions and 159 deletions

View File

@ -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 \ PYTHONPATH=.:test-packages qubes-rpc-policy/generate-admin-policy \
install -m 0644 qubes-rpc-policy/admin-default \ --destdir=$(DESTDIR)/etc/qubes-rpc/policy \
$(DESTDIR)/etc/qubes-rpc/policy/$$method; \ --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)"

View File

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

View File

@ -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'):

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,5 +1,6 @@
## This file is included from all admin.* policy files _in default ## This file is included from all local read-write admin.* policy files
## configuration_. To allow only specific action, edit specific policy file. ## _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

View File

@ -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())

View File

@ -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 :return: sanitized value
:raises qubes.exc.QubesValueError :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:

View File

@ -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 = [] candidates = list(self.list_methods(self.method))
for attr in dir(self):
func = getattr(self, attr)
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: rpcnames = func.rpcnames
if mname != self.method:
continue
untrusted_candidates.append((func, endpoint))
except AttributeError: except AttributeError:
continue continue
if not untrusted_candidates: for mname, endpoint in rpcnames:
raise ProtocolError('no such method: {!r}'.format(self.method)) if select_method is None or mname == select_method:
yield (func, mname, endpoint)
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
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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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: consumed. Values include:
- N (an integer) consumes N arguments (and produces a list)
- '?' consumes zero or one arguments - N (an integer) consumes N arguments (and produces a list)
- '*' consumes zero or more arguments (and produces a list) - '?' consumes zero or one arguments
- '+' consumes one or more arguments (and produces a list) - '*' 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:

View File

@ -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 ''' Return all :py:class:`qubes.storage.BlockDevice` for current domain
for serialization in the libvirt XML template as <disk>. 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

View File

@ -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 \ if self.override_target.startswith('$') and \
self.override_target.startswith('$dispvm'): 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 their types, labels, tags etc. as returned by
:py:func:`app_to_system_info` :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, :param caller_ident: Service caller ident
source_id`) (`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): def __init__(self, service, policy_dir=POLICY_DIR):
policy_file = os.path.join(POLICY_DIR, service) 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>: - `<domain name>`:
- tags: list of tags - tags: list of tags
- type: domain type - type: domain type
- dispvm_allowed: should DispVM based on this VM be allowed - dispvm_allowed: should DispVM based on this VM be allowed
- default_dispvm: name of default AppVM for DispVMs started from here - default_dispvm: name of default AppVM for DispVMs started from here
''' '''

122
qubespolicy/graph.py Normal file
View File

@ -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())

View File

@ -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')

View File

@ -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

View File

@ -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',