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

View File

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

View File

@ -243,6 +243,8 @@ _man_pages_author = []
man_pages = [
('manpages/qubesd-query', 'qubesd-query',
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'):

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.
Each line consist of three values separated by white characters (space(s), tab(s)):
1. Source specification, which is one of:
- domain name
- `$anyvm` - any domain
- `$tag:some-tag` - VM having tag `some-tag`
- `$type:vm-type` - VM of `vm-type` type, available types:
AppVM, TemplateVM, StandaloneVM, DispVM
AppVM, TemplateVM, StandaloneVM, DispVM
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
- `$tag:some-tag` - domain having tag `some-tag`
- `$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
- `$dispvm:vm-name` - _new_ Disposable VM created from AppVM `vm-name`
- `$dispvm` - _new_ Disposable VM created from AppVM pointed by caller
property `default_dispvm`, which defaults to global property `default_dispvm`
- `$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:
- `allow` - allow the call, without further questions; optional parameters:
- `target=` - override caller provided call target -
possible values are: domain name, `$dispvm` or `$dispvm:vm-name`
- `user=` - call the service using this user, instead of the user
@ -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
parameters are supported
- `ask` - ask the user for confirmation; optional parameters:
- `target=` - override user provided call target
- `user=` - call the service using this user, instead of the user
pointed by target VM's `default_user` property

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
## configuration_. To allow only specific action, edit specific policy file.
## This file is included from all local read-write admin.* policy files
## _in default configuration_. To allow only specific action,
## edit specific policy file.
## Note that policy parsing stops at the first match,
## 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
$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.
:param untrusted_newvalue: value to be validated
:return sanitized value
:raises qubes.exc.QubesValueError
:return: sanitized value
:raises: qubes.exc.QubesValueError
'''
# do not treat type='str' as sufficient validation
if self.type is not None and self.type is not str:

View File

@ -41,7 +41,7 @@ class PermissionDenied(Exception):
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.
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 bool no_payload: if :py:obj:`True`, will barf on non-empty payload; \
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
*self*), ``untrusted_payload``, which will contain the payload.
@ -75,11 +77,14 @@ def method(name, *, no_payload=False, endpoints=None):
# pylint: disable=protected-access
if endpoints is None:
func._rpcname = ((name, None),)
func.rpcnames = ((name, None),)
else:
func._rpcname = tuple(
func.rpcnames = tuple(
(name.format(endpoint=endpoint), endpoint)
for endpoint in endpoints)
func.classifiers = classifiers
return func
return decorator
@ -133,43 +138,45 @@ class AbstractQubesAPI(object):
#: is this operation cancellable?
self.cancellable = False
untrusted_candidates = []
for attr in dir(self):
func = getattr(self, attr)
candidates = list(self.list_methods(self.method))
if not candidates:
raise ProtocolError('no such method: {!r}'.format(self.method))
assert len(candidates) == 1, \
'multiple candidates for method {!r}'.format(self.method)
#: the method to execute
self._handler = candidates[0]
self._running_handler = None
@classmethod
def list_methods(cls, select_method=None):
for attr in dir(cls):
func = getattr(cls, attr)
if not callable(func):
continue
try:
# pylint: disable=protected-access
for mname, endpoint in func._rpcname:
if mname != self.method:
continue
untrusted_candidates.append((func, endpoint))
rpcnames = func.rpcnames
except AttributeError:
continue
if not untrusted_candidates:
raise ProtocolError('no such method: {!r}'.format(self.method))
assert len(untrusted_candidates) == 1, \
'multiple candidates for method {!r}'.format(self.method)
#: the method to execute
self._handler = untrusted_candidates[0]
self._running_handler = None
del untrusted_candidates
for mname, endpoint in rpcnames:
if select_method is None or mname == select_method:
yield (func, mname, endpoint)
def execute(self, *, untrusted_payload):
'''Execute management operation.
This method is a coroutine.
'''
handler, endpoint = self._handler
handler, _, endpoint = self._handler
kwargs = {}
if endpoint is not None:
kwargs['endpoint'] = endpoint
self._running_handler = asyncio.ensure_future(handler(
self._running_handler = asyncio.ensure_future(handler(self,
untrusted_payload=untrusted_payload, **kwargs))
return self._running_handler

View File

@ -76,7 +76,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
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
def vmclass_list(self):
'''List all VM classes'''
@ -89,7 +90,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
return ''.join('{}\n'.format(ep.name)
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
def vm_list(self):
'''List all the domains'''
@ -106,13 +108,15 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
vm.get_power_state())
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
def vm_property_list(self):
'''List all properties on a qube'''
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
def property_list(self):
'''List all global properties'''
@ -126,13 +130,15 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
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
def vm_property_get(self):
'''Get a value of one property'''
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
def property_get(self):
'''Get a value of one global property'''
@ -168,14 +174,16 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
property_type,
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
def vm_property_set(self, untrusted_payload):
'''Set property value'''
return self._property_set(self.dest,
untrusted_payload=untrusted_payload)
@qubes.api.method('admin.property.Set')
@qubes.api.method('admin.property.Set',
scope='global', write=True)
@asyncio.coroutine
def property_set(self, untrusted_payload):
'''Set property value'''
@ -195,13 +203,15 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
setattr(dest, self.arg, newvalue)
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
def vm_property_help(self):
'''Get help for one property'''
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
def property_help(self):
'''Get help for one property'''
@ -221,13 +231,15 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
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
def vm_property_reset(self):
'''Reset a property to a default value'''
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
def property_reset(self):
'''Reset a property to a default value'''
@ -243,7 +255,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
delattr(dest, self.arg)
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
def vm_volume_list(self):
assert not self.arg
@ -251,7 +264,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
volume_names = self.fire_event_for_filter(self.dest.volumes.keys())
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
def vm_volume_info(self):
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
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
def vm_volume_listsnapshots(self):
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)
@qubes.api.method('admin.vm.volume.Revert')
@qubes.api.method('admin.vm.volume.Revert',
scope='local', write=True)
@asyncio.coroutine
def vm_volume_revert(self, untrusted_payload):
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.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
def vm_volume_clone_from(self):
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
return token
@qubes.api.method('admin.vm.volume.CloneTo')
@qubes.api.method('admin.vm.volume.CloneTo',
scope='local', write=True)
@asyncio.coroutine
def vm_volume_clone_to(self, untrusted_payload):
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.app.save()
@qubes.api.method('admin.vm.volume.Resize')
@qubes.api.method('admin.vm.volume.Resize',
scope='local', write=True)
@asyncio.coroutine
def vm_volume_resize(self, untrusted_payload):
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.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
def vm_volume_import(self):
'''Import volume data.
@ -392,7 +414,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
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
def vm_tag_list(self):
assert not self.arg
@ -403,7 +426,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
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
def vm_tag_get(self):
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'
@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
def vm_tag_set(self):
qubes.vm.Tags.validate_tag(self.arg)
@ -422,7 +447,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
self.dest.tags.add(self.arg)
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
def vm_tag_remove(self):
qubes.vm.Tags.validate_tag(self.arg)
@ -435,7 +461,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
raise qubes.exc.QubesTagNotFoundError(self.dest, self.arg)
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
def pool_list(self):
assert not self.arg
@ -445,7 +472,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
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
def pool_listdrivers(self):
assert self.dest.name == 'dom0'
@ -458,7 +486,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
' '.join(qubes.storage.driver_parameters(driver)))
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
def pool_info(self):
assert self.dest.name == 'dom0'
@ -471,7 +500,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
return ''.join('{}={}\n'.format(prop, val)
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
def pool_add(self, untrusted_payload):
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.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
def pool_remove(self):
assert self.dest.name == 'dom0'
@ -517,7 +548,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
self.app.remove_pool(self.arg)
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
def label_list(self):
assert self.dest.name == 'dom0'
@ -527,7 +559,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
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
def label_get(self):
assert self.dest.name == 'dom0'
@ -541,7 +574,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
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
def label_index(self):
assert self.dest.name == 'dom0'
@ -555,7 +589,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
return str(label.index)
@qubes.api.method('admin.label.Create')
@qubes.api.method('admin.label.Create',
scope='global', write=True)
@asyncio.coroutine
def label_create(self, untrusted_payload):
assert self.dest.name == 'dom0'
@ -591,7 +626,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
self.app.labels[new_index] = label
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
def label_remove(self):
assert self.dest.name == 'dom0'
@ -613,7 +649,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
del self.app.labels[label.index]
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
def vm_start(self):
assert not self.arg
@ -625,35 +662,40 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
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
def vm_shutdown(self):
assert not self.arg
self.fire_event_for_permission()
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
def vm_pause(self):
assert not self.arg
self.fire_event_for_permission()
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
def vm_unpause(self):
assert not self.arg
self.fire_event_for_permission()
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
def vm_kill(self):
assert not self.arg
self.fire_event_for_permission()
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
def events(self):
assert not self.arg
@ -694,14 +736,16 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
else:
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
def vm_feature_list(self):
assert not self.arg
features = self.fire_event_for_filter(self.dest.features.keys())
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
def vm_feature_get(self):
# 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)
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
def vm_feature_checkwithtemplate(self):
# 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)
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
def vm_feature_remove(self):
# 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)
self.app.save()
@qubes.api.method('admin.vm.feature.Set')
@qubes.api.method('admin.vm.feature.Set',
scope='local', write=True)
@asyncio.coroutine
def vm_feature_set(self, untrusted_payload):
# validation of self.arg done by qrexec-policy is enough
@ -749,14 +796,16 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
self.app.save()
@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
def vm_create(self, endpoint, untrusted_payload=None):
return self._vm_create(endpoint, allow_pool=False,
untrusted_payload=untrusted_payload)
@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
def vm_create_in_pool(self, endpoint, untrusted_payload=None):
return self._vm_create(endpoint, allow_pool=True,
@ -846,7 +895,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
raise
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
def vm_remove(self):
assert not self.arg
@ -867,7 +917,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
@qubes.api.method('admin.vm.device.{endpoint}.Available', endpoints=(ep.name
for ep in pkg_resources.iter_entry_points('qubes.devices')),
no_payload=True)
no_payload=True,
scope='local', read=True)
@asyncio.coroutine
def vm_device_available(self, endpoint):
devclass = endpoint
@ -901,7 +952,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
@qubes.api.method('admin.vm.device.{endpoint}.List', endpoints=(ep.name
for ep in pkg_resources.iter_entry_points('qubes.devices')),
no_payload=True)
no_payload=True,
scope='local', read=True)
@asyncio.coroutine
def vm_device_list(self, endpoint):
devclass = endpoint
@ -932,8 +984,12 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
return ''.join('{} {}\n'.format(ident, dev_info[ident])
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
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
def vm_device_attach(self, endpoint, untrusted_payload):
devclass = endpoint
@ -972,9 +1028,13 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
yield from self.dest.devices[devclass].attach(assignment)
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
for ep in pkg_resources.iter_entry_points('qubes.devices')),
no_payload=True)
no_payload=True,
scope='local', write=True, execute=True)
@asyncio.coroutine
def vm_device_detach(self, endpoint):
devclass = endpoint
@ -994,7 +1054,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
yield from self.dest.devices[devclass].detach(assignment)
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
def vm_firewall_get(self):
assert not self.arg
@ -1004,7 +1065,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
return ''.join('{}\n'.format(rule.api_rule)
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
def vm_firewall_set(self, untrusted_payload):
assert not self.arg
@ -1020,7 +1082,8 @@ class QubesAdminAPI(qubes.api.AbstractQubesAPI):
self.dest.firewall.rules = rules
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
def vm_firewall_reload(self):
assert not self.arg

View File

@ -119,6 +119,7 @@ class BackupHeader(object):
:param untrusted_header_text: header content
:type untrusted_header_text: basestring
.. warning::
This function may be exposed to not yet verified header,
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 size_limit: int maximum data amount to process
: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 (
no error)
no error)
'''
buffer_size = 409600
bytes_copied = 0

View File

@ -249,7 +249,7 @@ class OptionsCheckVisitor(docutils.nodes.SparseNodeVisitor):
While the documentation talks about a
'SparseNodeVisitor.depart_document()' function, this function does
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.
'''
if ignored_options is None:
@ -316,7 +316,7 @@ class CommandCheckVisitor(docutils.nodes.SparseNodeVisitor):
While the documentation talks about a
'SparseNodeVisitor.depart_document()' function, this function does
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.
'''
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
templates. Template name is compared as substring, so 'whonix' will
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):
@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
:param bool want_force_root: add ``--force-root`` option
:param mixed vmname_nargs: The number of ``VMNAME`` arguments that should be
consumed. Values include:
- N (an integer) consumes N arguments (and produces a list)
- '?' consumes zero or one arguments
- '*' consumes zero or more arguments (and produces a list)
- '+' consumes one or more arguments (and produces a list)
consumed. Values include:
- N (an integer) consumes N arguments (and produces a list)
- '?' consumes zero or one arguments
- '*' consumes zero or more arguments (and produces a list)
- '+' consumes one or more arguments (and produces a list)
*kwargs* are passed to :py:class:`argparser.ArgumentParser`.
Currenty supported options:

View File

@ -594,8 +594,8 @@ class QubesVM(qubes.vm.mix.net.NetVMMixin, qubes.vm.BaseVM):
@property
def block_devices(self):
''' Return all :py:class:`qubes.storage.BlockDevice`s for current domain
for serialization in the libvirt XML template as <disk>.
''' Return all :py:class:`qubes.storage.BlockDevice` for current domain
for serialization in the libvirt XML template as <disk>.
'''
for v in self.volumes.values():
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 qubes.vm.qubesvm.QubesVM source: source domain as presented to
this VM
this VM
:param str user: username to run service as
:param bool filter_esc: filter escape sequences to protect terminal \
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.
: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`
:return: None
''' # pylint: disable=redefined-builtin

View File

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

122
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 = {
'domains': {
'dom0': {
'tags': [],
'tags': ['dom0-tag'],
'type': 'AdminVM',
'default_dispvm': 'default-dvm',
'dispvm_allowed': False,
@ -102,6 +102,8 @@ class TC_00_PolicyRule(qubes.tests.QubesTestCase):
qubespolicy.verify_target_value(system_info, 'test-template'))
self.assertTrue(
qubespolicy.verify_target_value(system_info, 'test-standalone'))
self.assertTrue(
qubespolicy.verify_target_value(system_info, '$adminvm'))
self.assertFalse(
qubespolicy.verify_target_value(system_info, 'no-such-vm'))
self.assertFalse(
@ -127,6 +129,8 @@ class TC_00_PolicyRule(qubes.tests.QubesTestCase):
for_target=False))
self.assertTrue(qubespolicy.verify_special_value('$type:AppVM',
for_target=False))
self.assertTrue(qubespolicy.verify_special_value('$adminvm',
for_target=False))
self.assertFalse(qubespolicy.verify_special_value('$default',
for_target=False))
self.assertFalse(qubespolicy.verify_special_value('$dispvm',
@ -155,15 +159,16 @@ class TC_00_PolicyRule(qubes.tests.QubesTestCase):
self.assertIsNone(line.default_target)
def test_021_line_simple(self):
# also check spaces in action field
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)
self.assertEqual(line.filename, 'filename')
self.assertEqual(line.lineno, 12)
self.assertEqual(line.action, qubespolicy.Action.ask)
self.assertEqual(line.source, '$tag:tag1')
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_user, 'user')
self.assertIsNone(line.default_target)
@ -196,6 +201,20 @@ class TC_00_PolicyRule(qubes.tests.QubesTestCase):
self.assertIsNone(line.override_user)
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):
invalid_lines = [
'$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,
'$anyvm', '$dispvm:default-dvm'))
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,
'$dispvm:default-dvm', '$dispvm:default-dvm'))
@ -253,6 +275,15 @@ class TC_00_PolicyRule(qubes.tests.QubesTestCase):
self.assertFalse(is_match_single(system_info,
'$dispvm:test-vm1', '$dispvm:test-vm1'))
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, '$anyvm', '$tag:tag1'))
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'),
'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):
def test_000_init(self):
@ -485,7 +523,6 @@ class TC_10_PolicyAction(qubes.tests.QubesTestCase):
[unittest.mock.call('test-vm2', 'internal.vm.Start')])
self.assertEqual(mock_subprocess.mock_calls, [])
@unittest.mock.patch('qubespolicy.POLICY_DIR', tmp_policy_dir)
class TC_20_Policy(qubes.tests.QubesTestCase):
def setUp(self):
@ -505,7 +542,7 @@ class TC_20_Policy(qubes.tests.QubesTestCase):
f.write('test-vm2 test-vm3 ask\n')
f.write(' # comment \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(len(policy.policy_rules), 3)
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):
with self.assertRaises(qubespolicy.AccessDenied):
qubespolicy.Policy('no-such.service')
qubespolicy.Policy('no-such.service', tmp_policy_dir)
def test_002_include(self):
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')
with open(os.path.join(tmp_policy_dir, 'test.service2'), 'w') as f:
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(len(policy.policy_rules), 3)
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('$type:AppVM $default allow,target=test-vm3\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(
system_info, 'test-vm1', 'test-vm2'), policy.policy_rules[0])
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('test-no-dvm $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,
'test-vm1'), ['test-vm1', 'test-vm2', '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:
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')
self.assertEqual(action.rule, policy.policy_rules[0])
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('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')
self.assertEqual(action.rule, policy.policy_rules[1])
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('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')
self.assertEqual(action.rule, policy.policy_rules[2])
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('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')
self.assertEqual(action.rule, policy.policy_rules[3])
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',
'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):
@unittest.mock.patch('socket.socket')

View File

@ -213,7 +213,9 @@ fi
/usr/bin/qubesd*
/usr/bin/qrexec-policy
/usr/bin/qrexec-policy-agent
/usr/bin/qrexec-policy-graph
%{_mandir}/man1/qrexec-policy-graph.1*
%{_mandir}/man1/qubes*.1*
%dir %{python3_sitelib}/qubes-*.egg-info
@ -372,6 +374,7 @@ fi
%{python3_sitelib}/qubespolicy/gtkhelpers.py
%{python3_sitelib}/qubespolicy/rpcconfirmation.py
%{python3_sitelib}/qubespolicy/utils.py
%{python3_sitelib}/qubespolicy/graph.py
%dir %{python3_sitelib}/qubespolicy/tests
%dir %{python3_sitelib}/qubespolicy/tests/__pycache__
@ -410,7 +413,10 @@ fi
/etc/xen/scripts/block-origin
/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/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.Filecopy
%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()) + [
'qrexec-policy = qubespolicy.cli:main',
'qrexec-policy-agent = qubespolicy.agent:main',
'qrexec-policy-graph = qubespolicy.graph:main',
],
'qubes.vm': [
'AppVM = qubes.vm.appvm:AppVM',