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,6 +11,7 @@ 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
@ -30,10 +31,15 @@ Each line consist of three values separated by white characters (space(s), tab(s
- `$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.

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

@ -304,10 +304,12 @@ class QubesArgumentParser(argparse.ArgumentParser):
: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) - N (an integer) consumes N arguments (and produces a list)
- '?' consumes zero or one arguments - '?' consumes zero or one arguments
- '*' consumes zero 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) - '+' 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,7 +594,7 @@ 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():

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']:
@ -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')
@ -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]
@ -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,7 +668,7 @@ 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

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,6 +159,7 @@ 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)
@ -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',