Merge branch 'remove-qrexec'
This commit is contained in:
commit
c5aaf8cdd7
@ -12,7 +12,7 @@ install:
|
||||
- pip install --quiet -r ci/requirements.txt
|
||||
- git clone https://github.com/"${TRAVIS_REPO_SLUG%%/*}"/qubes-builder ~/qubes-builder
|
||||
script:
|
||||
- PYTHONPATH=test-packages pylint --rcfile=ci/pylintrc qubes qubespolicy
|
||||
- PYTHONPATH=test-packages pylint --rcfile=ci/pylintrc qubes
|
||||
- ./run-tests --no-syslog
|
||||
- ~/qubes-builder/scripts/travis-build
|
||||
env:
|
||||
|
1
Makefile
1
Makefile
@ -191,7 +191,6 @@ endif
|
||||
cp qubes-rpc/qubes.GetRandomizedTime $(DESTDIR)/etc/qubes-rpc/
|
||||
cp qubes-rpc/qubes.NotifyTools $(DESTDIR)/etc/qubes-rpc/
|
||||
cp qubes-rpc/qubes.NotifyUpdates $(DESTDIR)/etc/qubes-rpc/
|
||||
cp qubes-rpc/policy.RegisterArgument $(DESTDIR)/etc/qubes-rpc/
|
||||
install qubes-rpc/qubesd-query-fast $(DESTDIR)/usr/libexec/qubes/
|
||||
install -m 0755 qvm-tools/qubes-bug-report $(DESTDIR)/usr/bin/qubes-bug-report
|
||||
install -m 0755 qvm-tools/qubes-hcl-report $(DESTDIR)/usr/bin/qubes-hcl-report
|
||||
|
@ -1,3 +1,3 @@
|
||||
[run]
|
||||
source = qubes, qubespolicy
|
||||
omit = qubes/tests/*, qubespolicy/tests/*
|
||||
source = qubes
|
||||
omit = qubes/tests/*
|
||||
|
@ -7,5 +7,4 @@ jinja2
|
||||
lxml
|
||||
pylint
|
||||
sphinx
|
||||
pydbus
|
||||
PyYAML
|
||||
|
@ -246,8 +246,6 @@ _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'):
|
||||
|
@ -1,73 +0,0 @@
|
||||
.. 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
|
@ -6,12 +6,7 @@ install:
|
||||
cp vif-route-qubes $(DESTDIR)/etc/xen/scripts
|
||||
cp block-snapshot $(DESTDIR)/etc/xen/scripts
|
||||
ln -s block-snapshot $(DESTDIR)/etc/xen/scripts/block-origin
|
||||
install -d $(DESTDIR)/etc/xdg/autostart
|
||||
install -m 0644 qrexec-policy-agent.desktop $(DESTDIR)/etc/xdg/autostart/
|
||||
install -m 0644 -D tmpfiles-qubes.conf $(DESTDIR)/usr/lib/tmpfiles.d/qubes.conf
|
||||
install -d $(DESTDIR)/etc/dbus-1/system.d
|
||||
install -m 0644 dbus-org.qubesos.PolicyAgent.conf \
|
||||
$(DESTDIR)/etc/dbus-1/system.d/org.qubesos.PolicyAgent.conf
|
||||
install -d $(DESTDIR)/etc/logrotate.d
|
||||
install -m 0644 logrotate-qubes \
|
||||
$(DESTDIR)/etc/logrotate.d/qubes
|
||||
|
@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> <!-- -*- XML -*- -->
|
||||
|
||||
<!DOCTYPE busconfig PUBLIC
|
||||
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
|
||||
<busconfig>
|
||||
<!-- User need to be in qubes group to own the service -->
|
||||
<policy group="qubes">
|
||||
<allow own="org.qubesos.PolicyAgent"/>
|
||||
</policy>
|
||||
<policy context="default">
|
||||
|
||||
<allow send_destination="org.qubesos.PolicyAgent"
|
||||
send_interface="org.freedesktop.DBus.Introspectable"/>
|
||||
|
||||
<allow send_destination="org.qubesos.PolicyAgent"
|
||||
send_interface="org.qubesos.PolicyAgent"/>
|
||||
</policy>
|
||||
</busconfig>
|
@ -1,7 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Name=Qubes Qrexec Policy agent
|
||||
Comment=Agent for handling policy confirmation prompts
|
||||
Icon=qubes
|
||||
Exec=qrexec-policy-agent
|
||||
Terminal=false
|
||||
Type=Application
|
@ -1,127 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# The Qubes OS Project, https://www.qubes-os.org/
|
||||
#
|
||||
# Copyright (C) 2017 Wojtek Porczyk <woju@invisiblethingslab.com>
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
'''policy.RegisterArgument
|
||||
|
||||
This qrexec is meant for services, which require some kind of "registering"
|
||||
before use (say ``example.Register`` and ``example.Perform+ARGUMENT``). After
|
||||
registering, the backend should invoke this call with frontend as the intended
|
||||
destination, with the actual service in argument of this call and the argument
|
||||
as the payload. The policy generated will be a single line with explicit
|
||||
frontend and backend domain names, and a plain "allow", without further
|
||||
qualifiers.
|
||||
|
||||
The call allows for registering an argument only once, for one frontend domain.
|
||||
There is not possibility of deregistering or reregistering for another frontend.
|
||||
The backend can always register another argument for any frontend, including
|
||||
one that is already registered for some other argument.
|
||||
|
||||
By default this qrexec is disabled by policy. To actually use it you should
|
||||
drop a policy for an exact call you want to register which will redirect the
|
||||
call to dom0.
|
||||
|
||||
.. code-block:: none
|
||||
:caption: /etc/qubes-rpc/policy/policy.RegisterArgument+example.Perform
|
||||
|
||||
backendvm $anyvm allow,target=dom0
|
||||
|
||||
It will generate, for argument ``EXAMPLE``:
|
||||
|
||||
.. code-bloc:: none
|
||||
:caption: /etc/qubes-rpc/policy/example.Perform+EXAMPLE
|
||||
|
||||
frontendvm backendvm allow
|
||||
'''
|
||||
|
||||
import logging
|
||||
import os
|
||||
import string
|
||||
import sys
|
||||
import pathlib
|
||||
|
||||
POLICY_PATH = pathlib.Path('/etc/qubes-rpc/policy')
|
||||
POLICY_RULE = '{frontend} {backend} allow\n'
|
||||
|
||||
# linux-utils/qrexec-lib/qrexec.h
|
||||
MAX_ARGUMENT_LEN = 64
|
||||
|
||||
# core-admin-linux/qrexec/qrexec-daemon.c
|
||||
VALID_CHARS = set(map(ord, string.ascii_letters + string.digits + '-._'))
|
||||
|
||||
def die(*args, **kwargs):
|
||||
logging.error(*args, **kwargs)
|
||||
sys.exit(1)
|
||||
|
||||
def main():
|
||||
# pylint: disable=missing-docstring
|
||||
logging.basicConfig(
|
||||
level=logging.WARNING,
|
||||
filename='/var/log/qubes/policy-register.log',
|
||||
format='%(asctime)s %(message)s')
|
||||
|
||||
backend = os.environ['QREXEC_REMOTE_DOMAIN']
|
||||
frontend = os.environ['QREXEC_REQUESTED_TARGET']
|
||||
rpcname = os.environ['QREXEC_SERVICE_ARGUMENT']
|
||||
|
||||
logging.debug('%s %s → %s request, reading argument',
|
||||
rpcname, frontend, backend)
|
||||
|
||||
untrusted_argument = sys.stdin.buffer.read(MAX_ARGUMENT_LEN)
|
||||
untrusted_overflow = sys.stdin.buffer.read(1)
|
||||
sys.stdin.buffer.close()
|
||||
|
||||
if untrusted_overflow:
|
||||
die('%s: %s → %s request refused: argument too long',
|
||||
rpcname, frontend, backend)
|
||||
|
||||
if not untrusted_argument:
|
||||
die('%s: %s → %s request refused: empty argument',
|
||||
rpcname, frontend, backend)
|
||||
|
||||
if any(c not in VALID_CHARS for c in untrusted_argument):
|
||||
die('%s: %s → %s request refused: invalid argument',
|
||||
rpcname, frontend, backend)
|
||||
|
||||
# argument may also be too long, so that length of rpcname, separator and
|
||||
# argument exceed 64 bytes, but that's fine, the call just wont work
|
||||
|
||||
argument = untrusted_argument
|
||||
del untrusted_argument
|
||||
argument = argument.decode('ascii', errors='strict')
|
||||
|
||||
filename = '{}+{}'.format(rpcname, argument)
|
||||
logging.debug('%s %s → %s argument %s filename %s',
|
||||
rpcname, frontend, backend, argument, filename)
|
||||
|
||||
try:
|
||||
# the 'x' enforces that argument cannot be registered twice
|
||||
with open(str(POLICY_PATH / filename), 'x') as file:
|
||||
rule = POLICY_RULE.format(frontend=frontend, backend=backend)
|
||||
logging.warning('%s: %s → %s %s argument allowed',
|
||||
rpcname, frontend, backend, argument)
|
||||
logging.debug('%s: %s → %s %s adding rule %r',
|
||||
rpcname, frontend, backend, rule)
|
||||
file.write(rule)
|
||||
|
||||
except FileExistsError:
|
||||
die('%s: %s → %s %s argument failed: file exists')
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -31,8 +31,8 @@ import os.path
|
||||
qubes_base_dir = "/var/lib/qubes"
|
||||
system_path = {
|
||||
'qubes_guid_path': '/usr/bin/qubes-guid',
|
||||
'qrexec_daemon_path': '/usr/lib/qubes/qrexec-daemon',
|
||||
'qrexec_client_path': '/usr/lib/qubes/qrexec-client',
|
||||
'qrexec_daemon_path': '/usr/sbin/qrexec-daemon',
|
||||
'qrexec_client_path': '/usr/bin/qrexec-client',
|
||||
'qubesdb_daemon_path': '/usr/sbin/qubesdb-daemon',
|
||||
|
||||
# Relative to qubes_base_dir
|
||||
|
@ -1342,14 +1342,6 @@ def load_tests(loader, tests, pattern): # pylint: disable=unused-argument
|
||||
'qubes.tests.api_admin',
|
||||
'qubes.tests.api_misc',
|
||||
'qubes.tests.api_internal',
|
||||
'qubespolicy.tests',
|
||||
'qubespolicy.tests.cli',
|
||||
):
|
||||
tests.addTests(loader.loadTestsFromName(modname))
|
||||
|
||||
for modname in (
|
||||
'qubespolicy.tests.gtkhelpers',
|
||||
'qubespolicy.tests.rpcconfirmation',
|
||||
):
|
||||
tests.addTests(loader.loadTestsFromName(modname))
|
||||
|
||||
@ -1360,6 +1352,10 @@ def load_tests(loader, tests, pattern): # pylint: disable=unused-argument
|
||||
return tests
|
||||
|
||||
for modname in (
|
||||
'qrexec.tests',
|
||||
'qrexec.tests.cli',
|
||||
'qrexec.tests.gtkhelpers',
|
||||
'qrexec.tests.rpcconfirmation',
|
||||
# integration tests
|
||||
'qubes.tests.integ.basic',
|
||||
'qubes.tests.integ.storage',
|
||||
|
@ -1,765 +0,0 @@
|
||||
# coding=utf-8
|
||||
# The Qubes OS Project, https://www.qubes-os.org/
|
||||
#
|
||||
# Copyright (C) 2013-2015 Joanna Rutkowska <joanna@invisiblethingslab.com>
|
||||
# Copyright (C) 2013-2017 Marek Marczykowski-Górecki
|
||||
# <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# pylint: disable=no-else-return,useless-object-inheritance,try-except-raise
|
||||
|
||||
''' Qrexec policy parser and evaluator '''
|
||||
import enum
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import socket
|
||||
import subprocess
|
||||
|
||||
# don't import 'qubes.config' please, it takes 0.3s
|
||||
QREXEC_CLIENT = '/usr/lib/qubes/qrexec-client'
|
||||
POLICY_DIR = '/etc/qubes-rpc/policy'
|
||||
QUBESD_INTERNAL_SOCK = '/var/run/qubesd.internal.sock'
|
||||
QUBESD_SOCK = '/var/run/qubesd.sock'
|
||||
|
||||
|
||||
class AccessDenied(Exception):
|
||||
''' Raised when qrexec policy denied access '''
|
||||
|
||||
|
||||
class PolicySyntaxError(AccessDenied):
|
||||
''' Syntax error in qrexec policy, abort parsing '''
|
||||
def __init__(self, filename, lineno, msg):
|
||||
super(PolicySyntaxError, self).__init__(
|
||||
'{}:{}: {}'.format(filename, lineno, msg))
|
||||
|
||||
class PolicyNotFound(AccessDenied):
|
||||
''' Policy was not found for this service '''
|
||||
def __init__(self, service_name):
|
||||
super(PolicyNotFound, self).__init__(
|
||||
'Policy not found for service {}'.format(service_name))
|
||||
|
||||
|
||||
class Action(enum.Enum):
|
||||
''' Action as defined by policy '''
|
||||
allow = 1
|
||||
deny = 2
|
||||
ask = 3
|
||||
|
||||
def is_special_value(value):
|
||||
'''Check if given source/target specification is special (keyword) value
|
||||
'''
|
||||
return value.startswith('@')
|
||||
|
||||
def verify_target_value(system_info, value):
|
||||
''' Check if given value names valid target
|
||||
|
||||
This function check if given value is not only syntactically correct,
|
||||
but also if names valid service call target (existing domain,
|
||||
or valid @dispvm like keyword)
|
||||
|
||||
:param system_info: information about the system
|
||||
:param value: value to be checked
|
||||
'''
|
||||
if value == '@dispvm':
|
||||
return True
|
||||
elif value == '@adminvm':
|
||||
return True
|
||||
elif value.startswith('@dispvm:'):
|
||||
dispvm_base = value.split(':', 1)[1]
|
||||
if dispvm_base not in system_info['domains']:
|
||||
return False
|
||||
dispvm_base_info = system_info['domains'][dispvm_base]
|
||||
return bool(dispvm_base_info['template_for_dispvms'])
|
||||
else:
|
||||
return value in system_info['domains']
|
||||
|
||||
|
||||
def verify_special_value(value, for_target=True, specific_target=False):
|
||||
'''
|
||||
Verify if given special VM-specifier ('@...') is valid
|
||||
|
||||
:param value: value to verify
|
||||
:param for_target: should classify target-only values as valid (
|
||||
'@default', '@dispvm')
|
||||
:param specific_target: allow only values naming specific target
|
||||
(for use with target=, default= etc)
|
||||
:return: True or False
|
||||
'''
|
||||
# pylint: disable=too-many-return-statements
|
||||
|
||||
# values used only for matching VMs, not naming specific one (for actual
|
||||
# call target)
|
||||
if not specific_target:
|
||||
if value.startswith('@tag:') and len(value) > len('@tag:'):
|
||||
return True
|
||||
if value.startswith('@type:') and len(value) > len('@type:'):
|
||||
return True
|
||||
if for_target and value.startswith('@dispvm:@tag:') and \
|
||||
len(value) > len('@dispvm:@tag:'):
|
||||
return True
|
||||
if value == '@anyvm':
|
||||
return True
|
||||
if for_target and value == '@default':
|
||||
return True
|
||||
|
||||
# those can be used to name one specific call VM
|
||||
if value == '@adminvm':
|
||||
return True
|
||||
# allow only specific dispvm, not based on any @xxx keyword - don't name
|
||||
# @tag here specifically, to work also with any future keywords
|
||||
if for_target and value.startswith('@dispvm:') and \
|
||||
not value.startswith('@dispvm:@'):
|
||||
return True
|
||||
if for_target and value == '@dispvm':
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class PolicyRule(object):
|
||||
''' A single line of policy file '''
|
||||
def __init__(self, line, filename=None, lineno=None):
|
||||
'''
|
||||
Load a single line of qrexec policy and check its syntax.
|
||||
Do not verify existence of named objects.
|
||||
|
||||
:raise PolicySyntaxError: when syntax error is found
|
||||
|
||||
:param line: a single line of actual qrexec policy (not a comment,
|
||||
empty line or @include)
|
||||
:param filename: name of the file from which this line is loaded
|
||||
:param lineno: line number from which this line is loaded
|
||||
'''
|
||||
|
||||
self.lineno = lineno
|
||||
self.filename = filename
|
||||
|
||||
try:
|
||||
self.source, self.target, self.full_action = line.split(maxsplit=2)
|
||||
except ValueError:
|
||||
raise PolicySyntaxError(filename, lineno, 'wrong number of fields')
|
||||
|
||||
(action, *params) = self.full_action.replace(',', ' ').split()
|
||||
try:
|
||||
self.action = Action[action]
|
||||
except KeyError:
|
||||
raise PolicySyntaxError(filename, lineno,
|
||||
'invalid action: {}'.format(action))
|
||||
|
||||
#: alternative target, used instead of the one specified by the caller
|
||||
self.override_target = None
|
||||
|
||||
#: alternative user, used instead of vm.default_user
|
||||
self.override_user = None
|
||||
|
||||
#: default target when asking the user for confirmation
|
||||
self.default_target = None
|
||||
|
||||
for param in params:
|
||||
try:
|
||||
param_name, value = param.split('=')
|
||||
except ValueError:
|
||||
raise PolicySyntaxError(filename, lineno,
|
||||
'invalid action parameter syntax: {}'.format(param))
|
||||
if param_name == 'target':
|
||||
if self.action == Action.deny:
|
||||
raise PolicySyntaxError(filename, lineno,
|
||||
'target= option not allowed for deny action')
|
||||
self.override_target = value
|
||||
elif param_name == 'user':
|
||||
if self.action == Action.deny:
|
||||
raise PolicySyntaxError(filename, lineno,
|
||||
'user= option not allowed for deny action')
|
||||
self.override_user = value
|
||||
elif param_name == 'default_target':
|
||||
if self.action != Action.ask:
|
||||
raise PolicySyntaxError(filename, lineno,
|
||||
'default_target= option allowed only for ask action')
|
||||
self.default_target = value
|
||||
else:
|
||||
raise PolicySyntaxError(filename, lineno,
|
||||
'invalid option {} for {} action'.format(param, action))
|
||||
|
||||
# verify special values
|
||||
if is_special_value(self.source):
|
||||
if not verify_special_value(self.source, False, False):
|
||||
raise PolicySyntaxError(filename, lineno,
|
||||
'invalid source specification: {}'.format(self.source))
|
||||
|
||||
if is_special_value(self.target):
|
||||
if not verify_special_value(self.target, True, False):
|
||||
raise PolicySyntaxError(filename, lineno,
|
||||
'invalid target specification: {}'.format(self.target))
|
||||
|
||||
if self.target == '@default' \
|
||||
and self.action == Action.allow \
|
||||
and self.override_target is None:
|
||||
raise PolicySyntaxError(filename, lineno,
|
||||
'allow action for @default rule must specify target= option')
|
||||
|
||||
if self.override_target is not None:
|
||||
if is_special_value(self.override_target) and \
|
||||
not verify_special_value(self.override_target, True, True):
|
||||
raise PolicySyntaxError(filename, lineno,
|
||||
'target= option needs to name specific target')
|
||||
|
||||
if self.default_target is not None:
|
||||
if is_special_value(self.default_target) and \
|
||||
not verify_special_value(self.default_target, True, True):
|
||||
raise PolicySyntaxError(filename, lineno,
|
||||
'target= option needs to name specific target')
|
||||
|
||||
@staticmethod
|
||||
def is_match_single(system_info, policy_value, value):
|
||||
'''
|
||||
Evaluate if a single value (VM name or '@default') matches policy
|
||||
specification
|
||||
|
||||
:param system_info: information about the system
|
||||
:param policy_value: value from qrexec policy (either self.source or
|
||||
self.target)
|
||||
:param value: value to be compared (source or target)
|
||||
:return: True or False
|
||||
'''
|
||||
# pylint: disable=too-many-return-statements
|
||||
|
||||
# not specified target matches only with @default and @anyvm policy
|
||||
# entry
|
||||
if value == '@default':
|
||||
return policy_value in ('@default', '@anyvm')
|
||||
|
||||
# if specific target used, check if it's valid
|
||||
# this function (is_match_single) is also used for checking call source
|
||||
# values, but this isn't a problem, because it will always be a
|
||||
# domain name (not @dispvm or such) - this is guaranteed by a nature
|
||||
# of qrexec call
|
||||
if not verify_target_value(system_info, value):
|
||||
return False
|
||||
|
||||
# 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 != '@adminvm'
|
||||
|
||||
# exact match, including @dispvm* and @adminvm
|
||||
if value == policy_value:
|
||||
return True
|
||||
|
||||
# DispVM request, using tags to match
|
||||
if policy_value.startswith('@dispvm:@tag:') \
|
||||
and value.startswith('@dispvm:'):
|
||||
tag = policy_value.split(':', 2)[2]
|
||||
dispvm_base = value.split(':', 1)[1]
|
||||
# already checked for existence by verify_target_value call
|
||||
dispvm_base_info = system_info['domains'][dispvm_base]
|
||||
return tag in dispvm_base_info['tags']
|
||||
|
||||
# if @dispvm* not matched above, reject it; default DispVM (bare
|
||||
# @dispvm) was resolved by the caller
|
||||
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]
|
||||
|
||||
if policy_value.startswith('@tag:'):
|
||||
tag = policy_value.split(':', 1)[1]
|
||||
return tag in domain_info['tags']
|
||||
|
||||
if policy_value.startswith('@type:'):
|
||||
type_ = policy_value.split(':', 1)[1]
|
||||
return type_ == domain_info['type']
|
||||
|
||||
return False
|
||||
|
||||
def is_match(self, system_info, source, target):
|
||||
'''
|
||||
Check if given (source, target) matches this policy line.
|
||||
|
||||
:param system_info: information about the system - available VMs,
|
||||
their types, labels, tags etc. as returned by
|
||||
:py:func:`app_to_system_info`
|
||||
:param source: name of the source VM
|
||||
:param target: name of the target VM, or None if not specified
|
||||
:return: True or False
|
||||
'''
|
||||
|
||||
if not self.is_match_single(system_info, self.source, source):
|
||||
return False
|
||||
# @dispvm in policy matches _only_ @dispvm (but not @dispvm:some-vm,
|
||||
# even if that would be the default one)
|
||||
if self.target == '@dispvm' and target == '@dispvm':
|
||||
return True
|
||||
if target == '@dispvm':
|
||||
# resolve default DispVM, to check all kinds of @dispvm:*
|
||||
default_dispvm = system_info['domains'][source]['default_dispvm']
|
||||
if default_dispvm is None:
|
||||
# if this VM have no default DispVM, match only with @anyvm
|
||||
return self.target == '@anyvm'
|
||||
target = '@dispvm:' + default_dispvm
|
||||
if not self.is_match_single(system_info, self.target, target):
|
||||
return False
|
||||
return True
|
||||
|
||||
def expand_target(self, system_info):
|
||||
'''
|
||||
Return domains matching target of this policy line
|
||||
|
||||
:param system_info: information about the system
|
||||
:return: matching domains
|
||||
'''
|
||||
|
||||
if self.target.startswith('@tag:'):
|
||||
tag = self.target.split(':', 1)[1]
|
||||
for name, domain in system_info['domains'].items():
|
||||
if tag in domain['tags']:
|
||||
yield name
|
||||
elif self.target.startswith('@type:'):
|
||||
type_ = self.target.split(':', 1)[1]
|
||||
for name, domain in system_info['domains'].items():
|
||||
if type_ == domain['type']:
|
||||
yield name
|
||||
elif self.target == '@anyvm':
|
||||
for name, domain in system_info['domains'].items():
|
||||
if name != 'dom0':
|
||||
yield name
|
||||
if domain['template_for_dispvms']:
|
||||
yield '@dispvm:' + name
|
||||
yield '@dispvm'
|
||||
elif self.target.startswith('@dispvm:@tag:'):
|
||||
tag = self.target.split(':', 2)[2]
|
||||
for name, domain in system_info['domains'].items():
|
||||
if tag in domain['tags']:
|
||||
if domain['template_for_dispvms']:
|
||||
yield '@dispvm:' + name
|
||||
elif self.target.startswith('@dispvm:'):
|
||||
dispvm_base = self.target.split(':', 1)[1]
|
||||
try:
|
||||
if system_info['domains'][dispvm_base]['template_for_dispvms']:
|
||||
yield self.target
|
||||
except KeyError:
|
||||
# TODO log a warning?
|
||||
pass
|
||||
elif self.target == '@adminvm':
|
||||
yield self.target
|
||||
elif self.target == '@dispvm':
|
||||
yield self.target
|
||||
else:
|
||||
if self.target in system_info['domains']:
|
||||
yield self.target
|
||||
|
||||
def expand_override_target(self, system_info, source):
|
||||
'''
|
||||
Replace '@dispvm' with specific '@dispvm:...' value, based on qrexec
|
||||
call source.
|
||||
|
||||
:param system_info: System information
|
||||
:param source: Source domain name
|
||||
:return: :py:attr:`override_target` with '@dispvm' substituted
|
||||
'''
|
||||
if self.override_target == '@dispvm':
|
||||
if system_info['domains'][source]['default_dispvm'] is None:
|
||||
return None
|
||||
return '@dispvm:' + system_info['domains'][source]['default_dispvm']
|
||||
else:
|
||||
return self.override_target
|
||||
|
||||
|
||||
class PolicyAction(object):
|
||||
''' Object representing positive policy evaluation result -
|
||||
either ask or allow action '''
|
||||
def __init__(self, service, source, target, rule, original_target,
|
||||
targets_for_ask=None):
|
||||
#: service name
|
||||
self.service = service
|
||||
#: calling domain
|
||||
self.source = source
|
||||
#: target domain the service should be connected to, None if
|
||||
# not chosen yet
|
||||
if targets_for_ask is None or target in targets_for_ask:
|
||||
self.target = target
|
||||
else:
|
||||
# TODO: log a warning?
|
||||
self.target = None
|
||||
#: original target specified by the caller
|
||||
self.original_target = original_target
|
||||
#: targets for the user to choose from
|
||||
self.targets_for_ask = targets_for_ask
|
||||
#: policy rule from which this action is derived
|
||||
self.rule = rule
|
||||
if rule.action == Action.deny:
|
||||
# this should be really rejected by Policy.eval()
|
||||
raise AccessDenied(
|
||||
'denied by policy {}:{}'.format(rule.filename, rule.lineno))
|
||||
if rule.action == Action.ask:
|
||||
if targets_for_ask is None:
|
||||
raise AccessDenied(
|
||||
'invalid policy {}:{}'.format(rule.filename, rule.lineno))
|
||||
elif rule.action == Action.allow:
|
||||
if targets_for_ask is not None or target is None:
|
||||
raise AccessDenied(
|
||||
'invalid policy {}:{}'.format(rule.filename, rule.lineno))
|
||||
self.action = rule.action
|
||||
|
||||
def handle_user_response(self, response, target=None):
|
||||
'''
|
||||
Handle user response for the 'ask' action
|
||||
|
||||
:param response: whether the call was allowed or denied (bool)
|
||||
:param target: target chosen by the user (if reponse==True)
|
||||
:return: None
|
||||
'''
|
||||
assert self.action == Action.ask
|
||||
if response:
|
||||
assert target in self.targets_for_ask
|
||||
self.target = target
|
||||
self.action = Action.allow
|
||||
else:
|
||||
self.action = Action.deny
|
||||
raise AccessDenied(
|
||||
'denied by the user {}:{}'.format(self.rule.filename,
|
||||
self.rule.lineno))
|
||||
|
||||
def execute(self, caller_ident):
|
||||
''' Execute allowed service call
|
||||
|
||||
:param caller_ident: Service caller ident
|
||||
(`process_ident,source_name, source_id`)
|
||||
'''
|
||||
assert self.action == Action.allow
|
||||
assert self.target is not None
|
||||
|
||||
if self.target == '@adminvm':
|
||||
self.target = 'dom0'
|
||||
if self.target == 'dom0':
|
||||
original_target_type = \
|
||||
'keyword' if is_special_value(self.original_target) else 'name'
|
||||
original_target = self.original_target.lstrip('@')
|
||||
cmd = \
|
||||
'QUBESRPC {service} {source} {original_target_type} ' \
|
||||
'{original_target}'.format(
|
||||
service=self.service,
|
||||
source=self.source,
|
||||
original_target_type=original_target_type,
|
||||
original_target=original_target)
|
||||
else:
|
||||
cmd = '{user}:QUBESRPC {service} {source}'.format(
|
||||
user=(self.rule.override_user or 'DEFAULT'),
|
||||
service=self.service,
|
||||
source=self.source)
|
||||
if self.target.startswith('@dispvm:'):
|
||||
target = self.spawn_dispvm()
|
||||
dispvm = True
|
||||
else:
|
||||
target = self.target
|
||||
dispvm = False
|
||||
self.ensure_target_running()
|
||||
qrexec_opts = ['-d', target, '-c', caller_ident]
|
||||
if dispvm:
|
||||
qrexec_opts.append('-W')
|
||||
# XXX remove when #951 gets fixed
|
||||
if self.source == target:
|
||||
raise AccessDenied('loopback qrexec connection not supported')
|
||||
try:
|
||||
subprocess.call([QREXEC_CLIENT] + qrexec_opts + [cmd])
|
||||
finally:
|
||||
if dispvm:
|
||||
self.cleanup_dispvm(target)
|
||||
|
||||
|
||||
def spawn_dispvm(self):
|
||||
'''
|
||||
Create and start Disposable VM based on AppVM specified in
|
||||
:py:attr:`target`
|
||||
:return: name of new Disposable VM
|
||||
'''
|
||||
base_appvm = self.target.split(':', 1)[1]
|
||||
dispvm_name = qubesd_call(base_appvm, 'admin.vm.CreateDisposable')
|
||||
dispvm_name = dispvm_name.decode('ascii')
|
||||
qubesd_call(dispvm_name, 'admin.vm.Start')
|
||||
return dispvm_name
|
||||
|
||||
def ensure_target_running(self):
|
||||
'''
|
||||
Start domain if not running already
|
||||
|
||||
:return: None
|
||||
'''
|
||||
if self.target == 'dom0':
|
||||
return
|
||||
try:
|
||||
qubesd_call(self.target, 'admin.vm.Start')
|
||||
except QubesMgmtException as e:
|
||||
if e.exc_type == 'QubesVMNotHaltedError':
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def cleanup_dispvm(dispvm):
|
||||
'''
|
||||
Kill and remove Disposable VM
|
||||
|
||||
:param dispvm: name of Disposable VM
|
||||
:return: None
|
||||
'''
|
||||
qubesd_call(dispvm, 'admin.vm.Kill')
|
||||
|
||||
|
||||
class Policy(object):
|
||||
''' Full policy for a given service
|
||||
|
||||
Usage:
|
||||
>>> system_info = get_system_info()
|
||||
>>> policy = Policy('some-service')
|
||||
>>> action = policy.evaluate(system_info, 'source-name', 'target-name')
|
||||
>>> if action.action == Action.ask:
|
||||
>>> # ... ask the user, see action.targets_for_ask ...
|
||||
>>> action.handle_user_response(response, target_chosen_by_user)
|
||||
>>> action.execute('process-ident')
|
||||
|
||||
'''
|
||||
|
||||
def __init__(self, service, policy_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])
|
||||
if not os.path.exists(policy_file):
|
||||
raise PolicyNotFound(service)
|
||||
|
||||
#: policy storage directory
|
||||
self.policy_dir = policy_dir
|
||||
|
||||
#: service name
|
||||
self.service = service
|
||||
|
||||
#: list of PolicyLine objects
|
||||
self.policy_rules = []
|
||||
try:
|
||||
self.load_policy_file(policy_file)
|
||||
except OSError as e:
|
||||
raise AccessDenied(
|
||||
'failed to load {} file: {!s}'.format(e.filename, e))
|
||||
|
||||
def load_policy_file(self, path):
|
||||
''' Load policy file and append rules to :py:attr:`policy_rules`
|
||||
|
||||
:param path: file to load
|
||||
'''
|
||||
with open(path) as policy_file:
|
||||
for lineno, line in zip(itertools.count(start=1),
|
||||
policy_file.readlines()):
|
||||
line = line.strip()
|
||||
# compatibility with old keywords notation
|
||||
line = line.replace('$', '@')
|
||||
if not line:
|
||||
# skip empty lines
|
||||
continue
|
||||
if line[0] == '#':
|
||||
# skip comments
|
||||
continue
|
||||
if line.startswith('@include:'):
|
||||
include_path = line.split(':', 1)[1]
|
||||
# os.path.join will leave include_path unchanged if it's
|
||||
# already absolute
|
||||
include_path = os.path.join(self.policy_dir, include_path)
|
||||
self.load_policy_file(include_path)
|
||||
else:
|
||||
self.policy_rules.append(PolicyRule(line, path, lineno))
|
||||
|
||||
def find_matching_rule(self, system_info, source, target):
|
||||
''' Find the first rule matching given arguments '''
|
||||
|
||||
for rule in self.policy_rules:
|
||||
if rule.is_match(system_info, source, target):
|
||||
return rule
|
||||
raise AccessDenied('no matching rule found')
|
||||
|
||||
|
||||
def collect_targets_for_ask(self, system_info, source):
|
||||
''' Collect targets the user can choose from in 'ask' action
|
||||
|
||||
Word 'targets' is used intentionally instead of 'domains', because it
|
||||
can also contains @dispvm like keywords.
|
||||
'''
|
||||
targets = set()
|
||||
|
||||
# iterate over rules in reversed order to easier handle 'deny'
|
||||
# actions - simply remove matching domains from allowed set
|
||||
for rule in reversed(self.policy_rules):
|
||||
if rule.is_match_single(system_info, rule.source, source):
|
||||
if rule.action == Action.deny:
|
||||
targets -= set(rule.expand_target(system_info))
|
||||
else:
|
||||
if rule.override_target is not None:
|
||||
override_target = rule.expand_override_target(
|
||||
system_info, source)
|
||||
if verify_target_value(system_info, override_target):
|
||||
targets.add(rule.override_target)
|
||||
else:
|
||||
targets.update(rule.expand_target(system_info))
|
||||
|
||||
# expand default DispVM
|
||||
if '@dispvm' in targets:
|
||||
targets.remove('@dispvm')
|
||||
if system_info['domains'][source]['default_dispvm'] is not None:
|
||||
dispvm = '@dispvm:' + \
|
||||
system_info['domains'][source]['default_dispvm']
|
||||
if verify_target_value(system_info, dispvm):
|
||||
targets.add(dispvm)
|
||||
|
||||
# expand other keywords
|
||||
if '@adminvm' in targets:
|
||||
targets.remove('@adminvm')
|
||||
targets.add('dom0')
|
||||
|
||||
# XXX remove when #951 gets fixed
|
||||
if source in targets:
|
||||
targets.remove(source)
|
||||
|
||||
return targets
|
||||
|
||||
def evaluate(self, system_info, source, target):
|
||||
''' Evaluate policy
|
||||
|
||||
:raise AccessDenied: when action should be denied unconditionally
|
||||
|
||||
:return tuple(rule, considered_targets) - where considered targets is a
|
||||
list of possible targets for 'ask' action (rule.action == Action.ask)
|
||||
'''
|
||||
if target == '':
|
||||
target = '@default'
|
||||
rule = self.find_matching_rule(system_info, source, target)
|
||||
if rule.action == Action.deny:
|
||||
raise AccessDenied(
|
||||
'denied by policy {}:{}'.format(rule.filename, rule.lineno))
|
||||
|
||||
if rule.override_target is not None:
|
||||
override_target = rule.expand_override_target(system_info, source)
|
||||
if not verify_target_value(system_info, override_target):
|
||||
raise AccessDenied('invalid target= value in {}:{}'.format(
|
||||
rule.filename, rule.lineno))
|
||||
actual_target = override_target
|
||||
else:
|
||||
actual_target = target
|
||||
|
||||
if rule.action == Action.ask:
|
||||
if rule.override_target is not None:
|
||||
targets = [actual_target]
|
||||
else:
|
||||
targets = list(
|
||||
self.collect_targets_for_ask(system_info, source))
|
||||
if not targets:
|
||||
raise AccessDenied(
|
||||
'policy define \'ask\' action at {}:{} but no target is '
|
||||
'available to choose from'.format(
|
||||
rule.filename, rule.lineno))
|
||||
return PolicyAction(self.service, source, rule.default_target,
|
||||
rule, target, targets)
|
||||
elif rule.action == Action.allow:
|
||||
if actual_target == '@default':
|
||||
raise AccessDenied(
|
||||
'policy define \'allow\' action at {}:{} but no target is '
|
||||
'specified by caller or policy'.format(
|
||||
rule.filename, rule.lineno))
|
||||
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:
|
||||
# should be unreachable
|
||||
raise AccessDenied(
|
||||
'invalid action?! {}:{}'.format(rule.filename, rule.lineno))
|
||||
|
||||
|
||||
class QubesMgmtException(Exception):
|
||||
''' Exception returned by qubesd '''
|
||||
def __init__(self, exc_type):
|
||||
super(QubesMgmtException, self).__init__()
|
||||
self.exc_type = exc_type
|
||||
|
||||
|
||||
def qubesd_call(dest, method, arg=None, payload=None):
|
||||
if method.startswith('internal.'):
|
||||
socket_path = QUBESD_INTERNAL_SOCK
|
||||
else:
|
||||
socket_path = QUBESD_SOCK
|
||||
try:
|
||||
client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
client_socket.connect(socket_path)
|
||||
except IOError:
|
||||
# TODO:
|
||||
raise
|
||||
|
||||
# src, method, dest, arg
|
||||
for call_arg in ('dom0', method, dest, arg):
|
||||
if call_arg is not None:
|
||||
client_socket.sendall(call_arg.encode('ascii'))
|
||||
client_socket.sendall(b'\0')
|
||||
if payload is not None:
|
||||
client_socket.sendall(payload)
|
||||
|
||||
client_socket.shutdown(socket.SHUT_WR)
|
||||
|
||||
return_data = client_socket.makefile('rb').read()
|
||||
if return_data.startswith(b'0\x00'):
|
||||
return return_data[2:]
|
||||
elif return_data.startswith(b'2\x00'):
|
||||
(_, exc_type, _traceback, _format_string, _args) = \
|
||||
return_data.split(b'\x00', 4)
|
||||
raise QubesMgmtException(exc_type.decode('ascii'))
|
||||
else:
|
||||
raise AssertionError(
|
||||
'invalid qubesd response: {!r}'.format(return_data))
|
||||
|
||||
|
||||
def get_system_info():
|
||||
''' Get system information
|
||||
|
||||
This retrieve information necessary to process qrexec policy. Returned
|
||||
data is nested dict structure with this structure:
|
||||
|
||||
- domains:
|
||||
- `<domain name>`:
|
||||
- tags: list of tags
|
||||
- type: domain type
|
||||
- template_for_dispvms: should DispVM based on this VM be allowed
|
||||
- default_dispvm: name of default AppVM for DispVMs started from here
|
||||
|
||||
'''
|
||||
|
||||
system_info = qubesd_call('dom0', 'internal.GetSystemInfo')
|
||||
return json.loads(system_info.decode('utf-8'))
|
@ -1,89 +0,0 @@
|
||||
# -*- encoding: utf8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2017 Marek Marczykowski-Górecki
|
||||
# <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
''' Agent running in user session, responsible for asking the user about policy
|
||||
decisions.'''
|
||||
|
||||
import pydbus
|
||||
# pylint: disable=import-error,wrong-import-position
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import GLib
|
||||
# pylint: enable=import-error
|
||||
|
||||
import qubespolicy.rpcconfirmation
|
||||
import qubespolicy.policycreateconfirmation
|
||||
# pylint: enable=wrong-import-position
|
||||
|
||||
class PolicyAgent:
|
||||
# pylint: disable=too-few-public-methods
|
||||
dbus = """
|
||||
<node>
|
||||
<interface name='org.qubesos.PolicyAgent'>
|
||||
<method name='Ask'>
|
||||
<arg type='s' name='source' direction='in'/>
|
||||
<arg type='s' name='service_name' direction='in'/>
|
||||
<arg type='as' name='targets' direction='in'/>
|
||||
<arg type='s' name='default_target' direction='in'/>
|
||||
<arg type='a{ss}' name='icons' direction='in'/>
|
||||
<arg type='s' name='response' direction='out'/>
|
||||
</method>
|
||||
<method name='ConfirmPolicyCreate'>
|
||||
<arg type='s' name='source' direction='in'/>
|
||||
<arg type='s' name='service_name' direction='in'/>
|
||||
<arg type='b' name='response' direction='out'/>
|
||||
</method>
|
||||
</interface>
|
||||
</node>
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def Ask(source, service_name, targets, default_target,
|
||||
icons):
|
||||
# pylint: disable=invalid-name
|
||||
entries_info = {}
|
||||
for entry in icons:
|
||||
entries_info[entry] = {}
|
||||
entries_info[entry]['icon'] = icons.get(entry, None)
|
||||
|
||||
response = qubespolicy.rpcconfirmation.confirm_rpc(
|
||||
entries_info, source, service_name,
|
||||
targets, default_target or None)
|
||||
return response or ''
|
||||
|
||||
@staticmethod
|
||||
def ConfirmPolicyCreate(source, service_name):
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
response = qubespolicy.policycreateconfirmation.confirm(
|
||||
source, service_name)
|
||||
return response
|
||||
|
||||
def main():
|
||||
loop = GLib.MainLoop()
|
||||
bus = pydbus.SystemBus()
|
||||
obj = PolicyAgent()
|
||||
bus.publish('org.qubesos.PolicyAgent', obj)
|
||||
loop.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -1,144 +0,0 @@
|
||||
# -*- encoding: utf8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2017 Marek Marczykowski-Górecki
|
||||
# <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
|
||||
import argparse
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
|
||||
import sys
|
||||
|
||||
import qubespolicy
|
||||
|
||||
parser = argparse.ArgumentParser(description="Evaluate qrexec policy")
|
||||
|
||||
parser.add_argument("--assume-yes-for-ask", action="store_true",
|
||||
dest="assume_yes_for_ask", default=False,
|
||||
help="Allow run of service without confirmation if policy say 'ask'")
|
||||
parser.add_argument("--just-evaluate", action="store_true",
|
||||
dest="just_evaluate", default=False,
|
||||
help="Do not run the service, only evaluate policy; "
|
||||
"retcode=0 means 'allow'")
|
||||
parser.add_argument('domain_id', metavar='src-domain-id',
|
||||
help='Source domain ID (Xen ID or similar, not Qubes ID)')
|
||||
parser.add_argument('domain', metavar='src-domain-name',
|
||||
help='Source domain name')
|
||||
parser.add_argument('target', metavar='dst-domain-name',
|
||||
help='Target domain name')
|
||||
parser.add_argument('service_name', metavar='service-name',
|
||||
help='Service name')
|
||||
parser.add_argument('process_ident', metavar='process-ident',
|
||||
help='Qrexec process identifier - for connecting data channel')
|
||||
|
||||
|
||||
def create_default_policy(service_name):
|
||||
policy_file = os.path.join(qubespolicy.POLICY_DIR, service_name)
|
||||
with open(policy_file, "w") as policy:
|
||||
policy.write(
|
||||
"## Policy file automatically created on first service call.\n")
|
||||
policy.write(
|
||||
"## Feel free to edit.\n")
|
||||
policy.write("## Note that policy parsing stops at the first match\n")
|
||||
policy.write("\n")
|
||||
policy.write("## Please use a single # to start your custom comments\n")
|
||||
policy.write("\n")
|
||||
policy.write("@anyvm @anyvm ask\n")
|
||||
|
||||
|
||||
def main(args=None):
|
||||
args = parser.parse_args(args)
|
||||
|
||||
# Add source domain information, required by qrexec-client for establishing
|
||||
# connection
|
||||
caller_ident = args.process_ident + "," + args.domain + "," + args.domain_id
|
||||
log = logging.getLogger('qubespolicy')
|
||||
log.setLevel(logging.INFO)
|
||||
if not log.handlers:
|
||||
handler = logging.handlers.SysLogHandler(address='/dev/log')
|
||||
log.addHandler(handler)
|
||||
log_prefix = 'qrexec: {}: {} -> {}:'.format(
|
||||
args.service_name, args.domain, args.target)
|
||||
try:
|
||||
system_info = qubespolicy.get_system_info()
|
||||
except qubespolicy.QubesMgmtException as e:
|
||||
log.error('%s error getting system info: %s', log_prefix, str(e))
|
||||
return 1
|
||||
try:
|
||||
try:
|
||||
policy = qubespolicy.Policy(args.service_name)
|
||||
except qubespolicy.PolicyNotFound:
|
||||
service_name = args.service_name.split('+')[0]
|
||||
import pydbus
|
||||
bus = pydbus.SystemBus()
|
||||
proxy = bus.get('org.qubesos.PolicyAgent',
|
||||
'/org/qubesos/PolicyAgent')
|
||||
create_policy = proxy.ConfirmPolicyCreate(
|
||||
args.domain, service_name)
|
||||
if create_policy:
|
||||
create_default_policy(service_name)
|
||||
policy = qubespolicy.Policy(args.service_name)
|
||||
else:
|
||||
raise
|
||||
|
||||
action = policy.evaluate(system_info, args.domain, args.target)
|
||||
if args.assume_yes_for_ask and action.action == qubespolicy.Action.ask:
|
||||
action.action = qubespolicy.Action.allow
|
||||
if args.just_evaluate:
|
||||
return {
|
||||
qubespolicy.Action.allow: 0,
|
||||
qubespolicy.Action.deny: 1,
|
||||
qubespolicy.Action.ask: 1,
|
||||
}[action.action]
|
||||
if action.action == qubespolicy.Action.ask:
|
||||
# late import to save on time for allow/deny actions
|
||||
import pydbus
|
||||
bus = pydbus.SystemBus()
|
||||
proxy = bus.get('org.qubesos.PolicyAgent',
|
||||
'/org/qubesos/PolicyAgent')
|
||||
|
||||
icons = {name: system_info['domains'][name]['icon']
|
||||
for name in system_info['domains'].keys()}
|
||||
for dispvm_base in system_info['domains']:
|
||||
if not (system_info['domains'][dispvm_base]
|
||||
['template_for_dispvms']):
|
||||
continue
|
||||
dispvm_api_name = '@dispvm:' + dispvm_base
|
||||
icons[dispvm_api_name] = \
|
||||
system_info['domains'][dispvm_base]['icon']
|
||||
icons[dispvm_api_name] = \
|
||||
icons[dispvm_api_name].replace('app', 'disp')
|
||||
|
||||
response = proxy.Ask(args.domain, args.service_name,
|
||||
action.targets_for_ask, action.target or '', icons)
|
||||
if response:
|
||||
action.handle_user_response(True, response)
|
||||
else:
|
||||
action.handle_user_response(False)
|
||||
log.info('%s allowed to %s', log_prefix, str(action.target))
|
||||
action.execute(caller_ident)
|
||||
except qubespolicy.PolicySyntaxError as e:
|
||||
log.error('%s error loading policy: %s', log_prefix, str(e))
|
||||
return 1
|
||||
except qubespolicy.AccessDenied as e:
|
||||
log.info('%s denied: %s', log_prefix, str(e))
|
||||
return 1
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
@ -1,141 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.20.0 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkMessageDialog" id="PolicyCreateConfirmationWindow">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">Default service policy</property>
|
||||
<property name="icon_name">dialog-warning</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<property name="message_type">question</property>
|
||||
<property name="buttons">ok-cancel</property>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="layout_style">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="messageLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Policy for requested service does not exist.
|
||||
Do you want to create default one (ask for everything)?</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="padding">2</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkGrid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="row_spacing">2</property>
|
||||
<property name="column_spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="sourceLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">4</property>
|
||||
<property name="label" translatable="yes">Source:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="serviceLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">4</property>
|
||||
<property name="label" translatable="yes">Service:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="sourceEntry">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="serviceEntry">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="confirmLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Type capital YES to confirm: </property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="confirmEntry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="activates_default">True</property>
|
||||
<property name="caps_lock_warning">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
@ -1,359 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.18.3 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.12"/>
|
||||
<object class="GtkWindow" id="RPCConfirmationWindow">
|
||||
<property name="width_request">400</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">Operation execution</property>
|
||||
<property name="window_position">center</property>
|
||||
<property name="icon_name">dialog-question</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<property name="urgency_hint">True</property>
|
||||
<property name="gravity">center</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="WindowBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="ErrorBar">
|
||||
<property name="app_paintable">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="no_show_all">True</property>
|
||||
<property name="message_type">error</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox" id="ActionArea">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox" id="ContentArea">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">16</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="ErrorImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-dialog-error</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ErrorMessage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">ErrorMessage</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="MainBox">
|
||||
<property name="width_request">100</property>
|
||||
<property name="height_request">80</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">12</property>
|
||||
<property name="margin_right">12</property>
|
||||
<property name="margin_top">12</property>
|
||||
<property name="margin_bottom">12</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="ButtonBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancelButton">
|
||||
<property name="label">gtk-cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="okButton">
|
||||
<property name="label">gtk-ok</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="can_default">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="ContentBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="AlwaysShownBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_bottom">6</property>
|
||||
<property name="spacing">12</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="RPCConfirmationIcon">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-dialog-question</property>
|
||||
<property name="icon_size">6</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="rpcDescription">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Do you want to allow the following operation?
|
||||
<small>Select the target domain and confirm with 'OK'</small></property>
|
||||
<property name="use_markup">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkGrid" id="grid1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">12</property>
|
||||
<property name="margin_bottom">6</property>
|
||||
<property name="row_spacing">12</property>
|
||||
<property name="column_spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="TargetDescLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">1</property>
|
||||
<property name="label" translatable="yes">Target:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="TargetCombo">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="has_entry">True</property>
|
||||
<child internal-child="entry">
|
||||
<object class="GtkEntry" id="TargetComboEntry">
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">5</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="placeholder_text" translatable="yes">Start typing or use the arrow</property>
|
||||
<property name="input_hints">GTK_INPUT_HINT_WORD_COMPLETION | GTK_INPUT_HINT_NONE</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="sourceDescLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">1</property>
|
||||
<property name="label" translatable="yes">Source:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="sourceEntry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="text" translatable="yes">source</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="operationDescLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">1</property>
|
||||
<property name="label" translatable="yes">Operation:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="rpcLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="label" translatable="yes">qubes.<b>MyOperation</b></property>
|
||||
<property name="use_markup">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkExpander" id="AdvancedSection">
|
||||
<property name="can_focus">True</property>
|
||||
<property name="no_show_all">True</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="AdvancedOptions">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">6</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="DisplayTemplates">
|
||||
<property name="label" translatable="yes">Display templates in the target list</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="CustomLocation">
|
||||
<property name="label" translatable="yes">Choose a custom destination in the target</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="AdvancedLabel">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Advanced options</property>
|
||||
<property name="use_underline">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
@ -1,121 +0,0 @@
|
||||
# -*- encoding: utf8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2017 Marek Marczykowski-Górecki
|
||||
# <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, see <https://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
|
||||
target = action.target or action.original_target
|
||||
# handle forced target=
|
||||
if action.rule.override_target:
|
||||
target = action.rule.override_target
|
||||
if args.target and target not in args.target:
|
||||
return ''
|
||||
if action.action == qubespolicy.Action.ask:
|
||||
if args.include_ask:
|
||||
return ' "{}" -> "{}" [label="{}" color=orange];\n'.format(
|
||||
action.source, target, service)
|
||||
elif action.action == qubespolicy.Action.allow:
|
||||
return ' "{}" -> "{}" [label="{}" color=red];\n'.format(
|
||||
action.source, 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())
|
||||
targets.append('@dispvm')
|
||||
targets.extend('@dispvm:' + dom for dom in system_info['domains']
|
||||
if system_info['domains'][dom]['template_for_dispvms'])
|
||||
|
||||
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())
|
@ -1,277 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# The Qubes OS Project, https://www.qubes-os.org/
|
||||
#
|
||||
# Copyright (C) 2017 boring-stuff <boring-stuff@users.noreply.github.com>
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import itertools
|
||||
# pylint: disable=import-error,wrong-import-position
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Gdk, GdkPixbuf, GObject, GLib
|
||||
# pylint: enable=import-error
|
||||
|
||||
from qubespolicy.utils import sanitize_domain_name
|
||||
# pylint: enable=wrong-import-position
|
||||
|
||||
class VMListModeler:
|
||||
def __init__(self, domains_info=None):
|
||||
self._entries = {}
|
||||
self._domains_info = domains_info
|
||||
self._icons = {}
|
||||
self._icon_size = 16
|
||||
self._theme = Gtk.IconTheme.get_default()
|
||||
self._create_entries()
|
||||
|
||||
def _get_icon(self, name):
|
||||
if name not in self._icons:
|
||||
try:
|
||||
icon = self._theme.load_icon(name, self._icon_size, 0)
|
||||
except GLib.Error: # pylint: disable=catching-non-exception
|
||||
icon = self._theme.load_icon("edit-find", self._icon_size, 0)
|
||||
|
||||
self._icons[name] = icon
|
||||
|
||||
return self._icons[name]
|
||||
|
||||
def _create_entries(self):
|
||||
for name, vm in self._domains_info.items():
|
||||
if name.startswith('@dispvm:'):
|
||||
vm_name = name[len('@dispvm:'):]
|
||||
dispvm = True
|
||||
else:
|
||||
vm_name = name
|
||||
dispvm = False
|
||||
sanitize_domain_name(vm_name, assert_sanitized=True)
|
||||
|
||||
icon = self._get_icon(vm.get('icon', None))
|
||||
|
||||
if dispvm:
|
||||
display_name = 'Disposable VM ({})'.format(vm_name)
|
||||
else:
|
||||
display_name = vm_name
|
||||
self._entries[display_name] = {
|
||||
'api_name': name,
|
||||
'icon': icon,
|
||||
'vm': vm}
|
||||
|
||||
def _get_valid_qube_name(self, combo, entry_box, whitelist):
|
||||
name = None
|
||||
|
||||
if combo and combo.get_active_id():
|
||||
selected = combo.get_active_id()
|
||||
|
||||
if selected in self._entries and \
|
||||
self._entries[selected]['api_name'] in whitelist:
|
||||
name = selected
|
||||
|
||||
if not name and entry_box:
|
||||
typed = entry_box.get_text()
|
||||
|
||||
if typed in self._entries and \
|
||||
self._entries[typed]['api_name'] in whitelist:
|
||||
name = typed
|
||||
|
||||
return name
|
||||
|
||||
def _combo_change(self, selection_trigger, combo, entry_box, whitelist):
|
||||
data = None
|
||||
name = self._get_valid_qube_name(combo, entry_box, whitelist)
|
||||
|
||||
if name:
|
||||
entry = self._entries[name]
|
||||
|
||||
data = entry['api_name']
|
||||
|
||||
if entry_box:
|
||||
entry_box.set_icon_from_pixbuf(
|
||||
Gtk.EntryIconPosition.PRIMARY, entry['icon'])
|
||||
else:
|
||||
if entry_box:
|
||||
entry_box.set_icon_from_stock(
|
||||
Gtk.EntryIconPosition.PRIMARY, "gtk-find")
|
||||
|
||||
if selection_trigger:
|
||||
selection_trigger(data)
|
||||
|
||||
def _entry_activate(self, activation_trigger, combo, entry_box, whitelist):
|
||||
name = self._get_valid_qube_name(combo, entry_box, whitelist)
|
||||
|
||||
if name:
|
||||
activation_trigger(entry_box)
|
||||
|
||||
def apply_model(self, destination_object, vm_list,
|
||||
selection_trigger=None, activation_trigger=None):
|
||||
if isinstance(destination_object, Gtk.ComboBox):
|
||||
list_store = Gtk.ListStore(int, str, GdkPixbuf.Pixbuf, str)
|
||||
|
||||
for entry_no, display_name in zip(itertools.count(),
|
||||
sorted(self._entries)):
|
||||
entry = self._entries[display_name]
|
||||
if entry['api_name'] in vm_list:
|
||||
list_store.append([
|
||||
entry_no,
|
||||
display_name,
|
||||
entry['icon'],
|
||||
entry['api_name'],
|
||||
])
|
||||
|
||||
destination_object.set_model(list_store)
|
||||
destination_object.set_id_column(1)
|
||||
|
||||
icon_column = Gtk.CellRendererPixbuf()
|
||||
destination_object.pack_start(icon_column, False)
|
||||
destination_object.add_attribute(icon_column, "pixbuf", 2)
|
||||
destination_object.set_entry_text_column(1)
|
||||
|
||||
if destination_object.get_has_entry():
|
||||
entry_box = destination_object.get_child()
|
||||
|
||||
area = Gtk.CellAreaBox()
|
||||
area.pack_start(icon_column, False, False, False)
|
||||
area.add_attribute(icon_column, "pixbuf", 2)
|
||||
|
||||
completion = Gtk.EntryCompletion.new_with_area(area)
|
||||
completion.set_inline_selection(True)
|
||||
completion.set_inline_completion(True)
|
||||
completion.set_popup_completion(True)
|
||||
completion.set_popup_single_match(False)
|
||||
completion.set_model(list_store)
|
||||
completion.set_text_column(1)
|
||||
|
||||
entry_box.set_completion(completion)
|
||||
if activation_trigger:
|
||||
entry_box.connect("activate",
|
||||
lambda entry: self._entry_activate(
|
||||
activation_trigger,
|
||||
destination_object,
|
||||
entry,
|
||||
vm_list))
|
||||
|
||||
# A Combo with an entry has a text column already
|
||||
text_column = destination_object.get_cells()[0]
|
||||
destination_object.reorder(text_column, 1)
|
||||
else:
|
||||
entry_box = None
|
||||
|
||||
text_column = Gtk.CellRendererText()
|
||||
destination_object.pack_start(text_column, False)
|
||||
destination_object.add_attribute(text_column, "text", 1)
|
||||
|
||||
changed_function = lambda combo: self._combo_change(
|
||||
selection_trigger,
|
||||
combo,
|
||||
entry_box,
|
||||
vm_list)
|
||||
|
||||
destination_object.connect("changed", changed_function)
|
||||
changed_function(destination_object)
|
||||
|
||||
else:
|
||||
raise TypeError(
|
||||
"Only expecting Gtk.ComboBox objects to want our model.")
|
||||
|
||||
def apply_icon(self, entry, qube_name):
|
||||
if isinstance(entry, Gtk.Entry):
|
||||
if qube_name in self._entries:
|
||||
entry.set_icon_from_pixbuf(
|
||||
Gtk.EntryIconPosition.PRIMARY,
|
||||
self._entries[qube_name]['icon'])
|
||||
else:
|
||||
raise ValueError("The specified source qube does not exist!")
|
||||
else:
|
||||
raise TypeError(
|
||||
"Only expecting Gtk.Entry objects to want our icon.")
|
||||
|
||||
|
||||
class GtkOneTimerHelper:
|
||||
# pylint: disable=too-few-public-methods
|
||||
def __init__(self, wait_seconds):
|
||||
self._wait_seconds = wait_seconds
|
||||
self._current_timer_id = 0
|
||||
self._timer_completed = False
|
||||
|
||||
def _invalidate_timer_completed(self):
|
||||
self._timer_completed = False
|
||||
|
||||
def _invalidate_current_timer(self):
|
||||
self._current_timer_id += 1
|
||||
|
||||
def _timer_check_run(self, timer_id):
|
||||
if self._current_timer_id == timer_id:
|
||||
self._timer_run(timer_id)
|
||||
self._timer_completed = True
|
||||
else:
|
||||
pass
|
||||
|
||||
def _timer_run(self, timer_id):
|
||||
raise NotImplementedError("Not yet implemented")
|
||||
|
||||
def _timer_schedule(self):
|
||||
self._invalidate_current_timer()
|
||||
GObject.timeout_add(int(round(self._wait_seconds * 1000)),
|
||||
self._timer_check_run,
|
||||
self._current_timer_id)
|
||||
|
||||
def _timer_has_completed(self):
|
||||
return self._timer_completed
|
||||
|
||||
|
||||
class FocusStealingHelper(GtkOneTimerHelper):
|
||||
def __init__(self, window, target_button, wait_seconds=1):
|
||||
GtkOneTimerHelper.__init__(self, wait_seconds)
|
||||
self._window = window
|
||||
self._target_button = target_button
|
||||
|
||||
self._window.connect("window-state-event", self._window_state_event)
|
||||
|
||||
self._target_sensitivity = False
|
||||
self._target_button.set_sensitive(self._target_sensitivity)
|
||||
|
||||
def _window_changed_focus(self, window_is_focused):
|
||||
self._target_button.set_sensitive(False)
|
||||
self._invalidate_timer_completed()
|
||||
|
||||
if window_is_focused:
|
||||
self._timer_schedule()
|
||||
else:
|
||||
self._invalidate_current_timer()
|
||||
|
||||
def _window_state_event(self, window, event):
|
||||
assert window == self._window, \
|
||||
'Window state callback called with wrong window'
|
||||
|
||||
changed_focus = event.changed_mask & Gdk.WindowState.FOCUSED
|
||||
window_focus = event.new_window_state & Gdk.WindowState.FOCUSED
|
||||
|
||||
if changed_focus:
|
||||
self._window_changed_focus(window_focus != 0)
|
||||
|
||||
# Propagate event further
|
||||
return False
|
||||
|
||||
def _timer_run(self, timer_id):
|
||||
self._target_button.set_sensitive(self._target_sensitivity)
|
||||
|
||||
def request_sensitivity(self, sensitivity):
|
||||
if self._timer_has_completed() or not sensitivity:
|
||||
self._target_button.set_sensitive(sensitivity)
|
||||
|
||||
self._target_sensitivity = sensitivity
|
||||
|
||||
def can_perform_action(self):
|
||||
return self._timer_has_completed()
|
@ -1,82 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2017 Marek Marczykowski-Górecki
|
||||
# <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
|
||||
import pkg_resources
|
||||
|
||||
# pylint: disable=import-error,wrong-import-position
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
# pylint: enable=import-error
|
||||
|
||||
class PolicyCreateConfirmationWindow:
|
||||
# pylint: disable=too-few-public-methods
|
||||
_source_file = pkg_resources.resource_filename('qubespolicy',
|
||||
os.path.join('glade', "PolicyCreateConfirmationWindow.glade"))
|
||||
_source_id = {'window': "PolicyCreateConfirmationWindow",
|
||||
'ok': "okButton",
|
||||
'cancel': "cancelButton",
|
||||
'source': "sourceEntry",
|
||||
'service': "serviceEntry",
|
||||
'confirm': "confirmEntry",
|
||||
}
|
||||
|
||||
def __init__(self, source, service):
|
||||
self._gtk_builder = Gtk.Builder()
|
||||
self._gtk_builder.add_from_file(self._source_file)
|
||||
self._window = self._gtk_builder.get_object(
|
||||
self._source_id['window'])
|
||||
self._rpc_ok_button = self._gtk_builder.get_object(
|
||||
self._source_id['ok'])
|
||||
self._rpc_cancel_button = self._gtk_builder.get_object(
|
||||
self._source_id['cancel'])
|
||||
self._service_entry = self._gtk_builder.get_object(
|
||||
self._source_id['service'])
|
||||
self._source_entry = self._gtk_builder.get_object(
|
||||
self._source_id['source'])
|
||||
self._confirm_entry = self._gtk_builder.get_object(
|
||||
self._source_id['confirm'])
|
||||
|
||||
self._source_entry.set_text(source)
|
||||
self._service_entry.set_text(service)
|
||||
|
||||
# make OK button the default
|
||||
ok_button = self._window.get_widget_for_response(Gtk.ResponseType.OK)
|
||||
ok_button.set_can_default(True)
|
||||
ok_button.grab_default()
|
||||
|
||||
def run(self):
|
||||
self._window.set_keep_above(True)
|
||||
self._window.connect("delete-event", Gtk.main_quit)
|
||||
self._window.show_all()
|
||||
|
||||
response = self._window.run()
|
||||
|
||||
self._window.hide()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
return self._confirm_entry.get_text() == 'YES'
|
||||
return False
|
||||
|
||||
def confirm(source, service):
|
||||
window = PolicyCreateConfirmationWindow(source, service)
|
||||
|
||||
return window.run()
|
@ -1,212 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# The Qubes OS Project, https://www.qubes-os.org/
|
||||
#
|
||||
# Copyright (C) 2017 boring-stuff <boring-stuff@users.noreply.github.com>
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import os
|
||||
from gi.repository import Gtk, Gdk, GLib # pylint: disable=import-error
|
||||
import pkg_resources
|
||||
|
||||
from qubespolicy.gtkhelpers import VMListModeler, FocusStealingHelper
|
||||
from qubespolicy.utils import sanitize_domain_name, \
|
||||
sanitize_service_name
|
||||
|
||||
|
||||
class RPCConfirmationWindow:
|
||||
# pylint: disable=too-few-public-methods
|
||||
_source_file = pkg_resources.resource_filename('qubespolicy',
|
||||
os.path.join('glade', "RPCConfirmationWindow.glade"))
|
||||
_source_id = {'window': "RPCConfirmationWindow",
|
||||
'ok': "okButton",
|
||||
'cancel': "cancelButton",
|
||||
'source': "sourceEntry",
|
||||
'rpc_label': "rpcLabel",
|
||||
'target': "TargetCombo",
|
||||
'error_bar': "ErrorBar",
|
||||
'error_message': "ErrorMessage",
|
||||
}
|
||||
|
||||
def _clicked_ok(self, source):
|
||||
assert source is not None, \
|
||||
'Called the clicked ok callback from no source object'
|
||||
|
||||
if self._can_perform_action():
|
||||
self._confirmed = True
|
||||
self._close()
|
||||
|
||||
def _clicked_cancel(self, button):
|
||||
assert button == self._rpc_cancel_button, \
|
||||
'Called the clicked cancel callback through the wrong button'
|
||||
|
||||
if self._can_perform_action():
|
||||
self._confirmed = False
|
||||
self._close()
|
||||
|
||||
def _key_pressed(self, window, key):
|
||||
assert window == self._rpc_window, \
|
||||
'Key pressed callback called with wrong window'
|
||||
|
||||
if self._can_perform_action():
|
||||
if key.keyval == Gdk.KEY_Escape:
|
||||
self._confirmed = False
|
||||
self._close()
|
||||
|
||||
def _update_ok_button_sensitivity(self, data):
|
||||
valid = (data is not None)
|
||||
|
||||
if valid:
|
||||
self._target_name = data
|
||||
else:
|
||||
self._target_name = None
|
||||
|
||||
self._focus_helper.request_sensitivity(valid)
|
||||
|
||||
def _show_error(self, error_message):
|
||||
self._error_message.set_text(error_message)
|
||||
self._error_bar.set_visible(True)
|
||||
|
||||
def _close_error(self, error_bar, response):
|
||||
assert error_bar == self._error_bar, \
|
||||
'Closed the error bar with the wrong error bar as parameter'
|
||||
assert response is not None, \
|
||||
'Closed the error bar with None as a response'
|
||||
|
||||
self._error_bar.set_visible(False)
|
||||
|
||||
def _set_initial_target(self, source, target):
|
||||
if target is not None:
|
||||
if target == source:
|
||||
self._show_error(
|
||||
"Source and target domains must not be the same.")
|
||||
else:
|
||||
model = self._rpc_combo_box.get_model()
|
||||
|
||||
found = False
|
||||
for item in model:
|
||||
if item[3] == target:
|
||||
found = True
|
||||
|
||||
self._rpc_combo_box.set_active_iter(
|
||||
model.get_iter(item.path))
|
||||
|
||||
break
|
||||
|
||||
if not found:
|
||||
self._show_error("Domain '%s' doesn't exist." % target)
|
||||
|
||||
def _can_perform_action(self):
|
||||
return self._focus_helper.can_perform_action()
|
||||
|
||||
@staticmethod
|
||||
def _escape_and_format_rpc_text(rpc_operation):
|
||||
escaped = GLib.markup_escape_text(rpc_operation)
|
||||
|
||||
partitioned = escaped.partition('.')
|
||||
formatted = partitioned[0] + partitioned[1]
|
||||
|
||||
if partitioned[2]:
|
||||
formatted += "<b>" + partitioned[2] + "</b>"
|
||||
else:
|
||||
formatted = "<b>" + formatted + "</b>"
|
||||
|
||||
return formatted
|
||||
|
||||
def _connect_events(self):
|
||||
self._rpc_window.connect("key-press-event", self._key_pressed)
|
||||
self._rpc_ok_button.connect("clicked", self._clicked_ok)
|
||||
self._rpc_cancel_button.connect("clicked", self._clicked_cancel)
|
||||
|
||||
self._error_bar.connect("response", self._close_error)
|
||||
|
||||
def __init__(self, entries_info, source, rpc_operation, targets_list,
|
||||
target=None):
|
||||
sanitize_domain_name(source, assert_sanitized=True)
|
||||
sanitize_service_name(source, assert_sanitized=True)
|
||||
|
||||
self._gtk_builder = Gtk.Builder()
|
||||
self._gtk_builder.add_from_file(self._source_file)
|
||||
self._rpc_window = self._gtk_builder.get_object(
|
||||
self._source_id['window'])
|
||||
self._rpc_ok_button = self._gtk_builder.get_object(
|
||||
self._source_id['ok'])
|
||||
self._rpc_cancel_button = self._gtk_builder.get_object(
|
||||
self._source_id['cancel'])
|
||||
self._rpc_label = self._gtk_builder.get_object(
|
||||
self._source_id['rpc_label'])
|
||||
self._source_entry = self._gtk_builder.get_object(
|
||||
self._source_id['source'])
|
||||
self._rpc_combo_box = self._gtk_builder.get_object(
|
||||
self._source_id['target'])
|
||||
self._error_bar = self._gtk_builder.get_object(
|
||||
self._source_id['error_bar'])
|
||||
self._error_message = self._gtk_builder.get_object(
|
||||
self._source_id['error_message'])
|
||||
self._target_name = None
|
||||
|
||||
self._focus_helper = self._new_focus_stealing_helper()
|
||||
|
||||
self._rpc_label.set_markup(
|
||||
self._escape_and_format_rpc_text(rpc_operation))
|
||||
|
||||
self._entries_info = entries_info
|
||||
list_modeler = self._new_vm_list_modeler()
|
||||
|
||||
list_modeler.apply_model(self._rpc_combo_box, targets_list,
|
||||
selection_trigger=self._update_ok_button_sensitivity,
|
||||
activation_trigger=self._clicked_ok)
|
||||
|
||||
self._source_entry.set_text(source)
|
||||
list_modeler.apply_icon(self._source_entry, source)
|
||||
|
||||
self._confirmed = None
|
||||
|
||||
self._set_initial_target(source, target)
|
||||
|
||||
self._connect_events()
|
||||
|
||||
def _close(self):
|
||||
self._rpc_window.close()
|
||||
|
||||
def _show(self):
|
||||
self._rpc_window.set_keep_above(True)
|
||||
self._rpc_window.connect("delete-event", Gtk.main_quit)
|
||||
self._rpc_window.show_all()
|
||||
|
||||
Gtk.main()
|
||||
|
||||
def _new_vm_list_modeler(self):
|
||||
return VMListModeler(self._entries_info)
|
||||
|
||||
def _new_focus_stealing_helper(self):
|
||||
return FocusStealingHelper(
|
||||
self._rpc_window,
|
||||
self._rpc_ok_button,
|
||||
1)
|
||||
|
||||
def confirm_rpc(self):
|
||||
self._show()
|
||||
|
||||
if self._confirmed:
|
||||
return self._target_name
|
||||
return False
|
||||
|
||||
|
||||
def confirm_rpc(entries_info, source, rpc_operation, targets_list, target=None):
|
||||
window = RPCConfirmationWindow(entries_info, source, rpc_operation,
|
||||
targets_list, target)
|
||||
|
||||
return window.confirm_rpc()
|
@ -1,919 +0,0 @@
|
||||
# -*- encoding: utf8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2017 Marek Marczykowski-Górecki
|
||||
# <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
|
||||
import os
|
||||
import socket
|
||||
import unittest.mock
|
||||
|
||||
import shutil
|
||||
|
||||
import qubes.tests
|
||||
import qubespolicy
|
||||
|
||||
tmp_policy_dir = '/tmp/policy'
|
||||
|
||||
system_info = {
|
||||
'domains': {
|
||||
'dom0': {
|
||||
'tags': ['dom0-tag'],
|
||||
'type': 'AdminVM',
|
||||
'default_dispvm': 'default-dvm',
|
||||
'template_for_dispvms': False,
|
||||
},
|
||||
'test-vm1': {
|
||||
'tags': ['tag1', 'tag2'],
|
||||
'type': 'AppVM',
|
||||
'default_dispvm': 'default-dvm',
|
||||
'template_for_dispvms': False,
|
||||
},
|
||||
'test-vm2': {
|
||||
'tags': ['tag2'],
|
||||
'type': 'AppVM',
|
||||
'default_dispvm': 'default-dvm',
|
||||
'template_for_dispvms': False,
|
||||
},
|
||||
'test-vm3': {
|
||||
'tags': ['tag3'],
|
||||
'type': 'AppVM',
|
||||
'default_dispvm': 'default-dvm',
|
||||
'template_for_dispvms': True,
|
||||
},
|
||||
'default-dvm': {
|
||||
'tags': [],
|
||||
'type': 'AppVM',
|
||||
'default_dispvm': 'default-dvm',
|
||||
'template_for_dispvms': True,
|
||||
},
|
||||
'test-invalid-dvm': {
|
||||
'tags': ['tag1', 'tag2'],
|
||||
'type': 'AppVM',
|
||||
'default_dispvm': 'test-vm1',
|
||||
'template_for_dispvms': False,
|
||||
},
|
||||
'test-no-dvm': {
|
||||
'tags': ['tag1', 'tag2'],
|
||||
'type': 'AppVM',
|
||||
'default_dispvm': None,
|
||||
'template_for_dispvms': False,
|
||||
},
|
||||
'test-template': {
|
||||
'tags': ['tag1', 'tag2'],
|
||||
'type': 'TemplateVM',
|
||||
'default_dispvm': 'default-dvm',
|
||||
'template_for_dispvms': False,
|
||||
},
|
||||
'test-standalone': {
|
||||
'tags': ['tag1', 'tag2'],
|
||||
'type': 'StandaloneVM',
|
||||
'default_dispvm': 'default-dvm',
|
||||
'template_for_dispvms': False,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TC_00_PolicyRule(qubes.tests.QubesTestCase):
|
||||
def test_000_verify_target_value(self):
|
||||
self.assertTrue(
|
||||
qubespolicy.verify_target_value(system_info, 'test-vm1'))
|
||||
self.assertTrue(
|
||||
qubespolicy.verify_target_value(system_info, 'default-dvm'))
|
||||
self.assertTrue(
|
||||
qubespolicy.verify_target_value(system_info, '@dispvm'))
|
||||
self.assertTrue(
|
||||
qubespolicy.verify_target_value(system_info, '@dispvm:default-dvm'))
|
||||
self.assertTrue(
|
||||
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(
|
||||
qubespolicy.verify_target_value(system_info,
|
||||
'@dispvm:test-invalid-dvm'))
|
||||
self.assertFalse(
|
||||
qubespolicy.verify_target_value(system_info, '@dispvm:test-vm1'))
|
||||
self.assertFalse(
|
||||
qubespolicy.verify_target_value(system_info, ''))
|
||||
self.assertFalse(
|
||||
qubespolicy.verify_target_value(system_info, '@default'))
|
||||
self.assertFalse(
|
||||
qubespolicy.verify_target_value(system_info, '@anyvm'))
|
||||
self.assertFalse(
|
||||
qubespolicy.verify_target_value(system_info, '@tag:tag1'))
|
||||
self.assertFalse(
|
||||
qubespolicy.verify_target_value(system_info, '@dispvm:@tag:tag1'))
|
||||
self.assertFalse(
|
||||
qubespolicy.verify_target_value(system_info, '@invalid'))
|
||||
|
||||
def test_010_verify_special_value(self):
|
||||
self.assertTrue(qubespolicy.verify_special_value('@tag:tag',
|
||||
for_target=False))
|
||||
self.assertTrue(qubespolicy.verify_special_value('@tag:other-tag',
|
||||
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.assertTrue(qubespolicy.verify_special_value('@dispvm:some-vm',
|
||||
for_target=True))
|
||||
self.assertTrue(qubespolicy.verify_special_value('@dispvm:@tag:tag1',
|
||||
for_target=True))
|
||||
self.assertFalse(qubespolicy.verify_special_value('@default',
|
||||
for_target=False))
|
||||
self.assertFalse(qubespolicy.verify_special_value('@dispvm',
|
||||
for_target=False))
|
||||
self.assertFalse(qubespolicy.verify_special_value('@dispvm:some-vm',
|
||||
for_target=False))
|
||||
self.assertFalse(qubespolicy.verify_special_value('@dispvm:@tag:tag1',
|
||||
for_target=False))
|
||||
self.assertFalse(qubespolicy.verify_special_value('@invalid',
|
||||
for_target=False))
|
||||
self.assertFalse(qubespolicy.verify_special_value('vm-name',
|
||||
for_target=False))
|
||||
self.assertFalse(qubespolicy.verify_special_value('@tag:',
|
||||
for_target=False))
|
||||
self.assertFalse(qubespolicy.verify_special_value('@type:',
|
||||
for_target=False))
|
||||
|
||||
def test_020_line_simple(self):
|
||||
line = qubespolicy.PolicyRule('@anyvm @anyvm ask', '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, '@anyvm')
|
||||
self.assertEqual(line.full_action, 'ask')
|
||||
self.assertIsNone(line.override_target)
|
||||
self.assertIsNone(line.override_user)
|
||||
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',
|
||||
'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.override_target, 'test-vm2')
|
||||
self.assertEqual(line.override_user, 'user')
|
||||
self.assertIsNone(line.default_target)
|
||||
|
||||
def test_022_line_simple(self):
|
||||
line = qubespolicy.PolicyRule(
|
||||
'@anyvm @default allow,target=@dispvm:test-vm2',
|
||||
'filename', 12)
|
||||
self.assertEqual(line.filename, 'filename')
|
||||
self.assertEqual(line.lineno, 12)
|
||||
self.assertEqual(line.action, qubespolicy.Action.allow)
|
||||
self.assertEqual(line.source, '@anyvm')
|
||||
self.assertEqual(line.target, '@default')
|
||||
self.assertEqual(line.full_action, 'allow,target=@dispvm:test-vm2')
|
||||
self.assertEqual(line.override_target, '@dispvm:test-vm2')
|
||||
self.assertIsNone(line.override_user)
|
||||
self.assertIsNone(line.default_target)
|
||||
|
||||
def test_023_line_simple(self):
|
||||
line = qubespolicy.PolicyRule(
|
||||
'@anyvm @default ask,default_target=test-vm1',
|
||||
'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, '@default')
|
||||
self.assertEqual(line.full_action, 'ask,default_target=test-vm1')
|
||||
self.assertIsNone(line.override_target)
|
||||
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
|
||||
'@default @default allow', # @default can't be a source
|
||||
'@anyvm @default allow,target=@dispvm:@tag:tag1', # @dispvm:@tag
|
||||
# as override target
|
||||
'@anyvm @default allow,target=@tag:tag1', # @tag as override target
|
||||
'@anyvm @default deny,target=test-vm1', # target= used with deny
|
||||
'@anyvm @anyvm deny,default_target=test-vm1', # default_target=
|
||||
# with deny
|
||||
'@anyvm @anyvm deny,user=user', # user= with deny
|
||||
'@anyvm @anyvm invalid', # invalid action
|
||||
'@anyvm @anyvm allow,invalid=xx', # invalid option
|
||||
'@anyvm @anyvm', # missing action
|
||||
'@anyvm @anyvm allow,default_target=test-vm1', # default_target=
|
||||
# with allow
|
||||
'@invalid @anyvm allow', # invalid source
|
||||
'@anyvm @invalid deny', # invalid target
|
||||
'', # empty line
|
||||
'@anyvm @anyvm allow extra', # trailing words
|
||||
'@anyvm @default allow', # @default allow without target=
|
||||
]
|
||||
for line in invalid_lines:
|
||||
with self.subTest(line):
|
||||
with self.assertRaises(qubespolicy.PolicySyntaxError):
|
||||
qubespolicy.PolicyRule(line, 'filename', 12)
|
||||
|
||||
def test_040_match_single(self):
|
||||
is_match_single = qubespolicy.PolicyRule.is_match_single
|
||||
self.assertTrue(is_match_single(system_info, '@anyvm', 'test-vm1'))
|
||||
self.assertTrue(is_match_single(system_info, '@anyvm', '@default'))
|
||||
self.assertTrue(is_match_single(system_info, '@default', '@default'))
|
||||
self.assertTrue(is_match_single(system_info, '@tag:tag1', 'test-vm1'))
|
||||
self.assertTrue(is_match_single(system_info, '@type:AppVM', 'test-vm1'))
|
||||
self.assertTrue(is_match_single(system_info,
|
||||
'@type:TemplateVM', 'test-template'))
|
||||
self.assertTrue(is_match_single(system_info, '@anyvm', '@dispvm'))
|
||||
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,
|
||||
'@dispvm:@tag:tag3', '@dispvm:test-vm3'))
|
||||
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'))
|
||||
self.assertTrue(is_match_single(system_info, '@anyvm', '@dispvm'))
|
||||
self.assertTrue(is_match_single(system_info, '@anyvm', 'test-vm1'))
|
||||
self.assertTrue(is_match_single(system_info, '@anyvm', 'test-vm1'))
|
||||
self.assertTrue(is_match_single(system_info, '@anyvm', 'test-vm1'))
|
||||
|
||||
self.assertFalse(is_match_single(system_info, '@default', 'test-vm1'))
|
||||
self.assertFalse(is_match_single(system_info, '@tag:tag1', 'test-vm3'))
|
||||
self.assertFalse(is_match_single(system_info, '@anyvm', 'no-such-vm'))
|
||||
# test-vm1.template_for_dispvms=False
|
||||
self.assertFalse(is_match_single(system_info,
|
||||
'@anyvm', '@dispvm:test-vm1'))
|
||||
# test-vm1.template_for_dispvms=False
|
||||
self.assertFalse(is_match_single(system_info,
|
||||
'@dispvm:test-vm1', '@dispvm:test-vm1'))
|
||||
self.assertFalse(is_match_single(system_info,
|
||||
'@dispvm:@tag:tag1', '@dispvm:test-vm1'))
|
||||
# test-vm3 has not tag1
|
||||
self.assertFalse(is_match_single(system_info,
|
||||
'@dispvm:@tag:tag1', '@dispvm:test-vm3'))
|
||||
# default-dvm has no tag3
|
||||
self.assertFalse(is_match_single(system_info,
|
||||
'@dispvm:@tag:tag3', '@dispvm:default-dvm'))
|
||||
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'))
|
||||
self.assertFalse(is_match_single(system_info, '@anyvm', '@invalid'))
|
||||
self.assertFalse(is_match_single(system_info, '@invalid', '@invalid'))
|
||||
self.assertFalse(is_match_single(system_info, '@anyvm', 'no-such-vm'))
|
||||
self.assertFalse(is_match_single(system_info,
|
||||
'no-such-vm', 'no-such-vm'))
|
||||
self.assertFalse(is_match_single(system_info, '@dispvm', 'test-vm1'))
|
||||
self.assertFalse(is_match_single(system_info, '@dispvm', 'default-dvm'))
|
||||
self.assertFalse(is_match_single(system_info,
|
||||
'@dispvm:default-dvm', 'default-dvm'))
|
||||
self.assertFalse(is_match_single(system_info, '@anyvm', 'test-vm1\n'))
|
||||
self.assertFalse(is_match_single(system_info, '@anyvm', 'test-vm1 '))
|
||||
|
||||
def test_050_match(self):
|
||||
line = qubespolicy.PolicyRule('@anyvm @anyvm allow')
|
||||
self.assertTrue(line.is_match(system_info, 'test-vm1', 'test-vm2'))
|
||||
line = qubespolicy.PolicyRule('@anyvm @anyvm allow')
|
||||
self.assertFalse(line.is_match(system_info, 'no-such-vm', 'test-vm2'))
|
||||
line = qubespolicy.PolicyRule('@anyvm @anyvm allow')
|
||||
self.assertFalse(line.is_match(system_info, 'test-vm1', 'no-such-vm'))
|
||||
line = qubespolicy.PolicyRule('@anyvm @dispvm allow')
|
||||
self.assertTrue(line.is_match(system_info, 'test-vm1', '@dispvm'))
|
||||
line = qubespolicy.PolicyRule('@anyvm @dispvm allow')
|
||||
self.assertFalse(line.is_match(system_info,
|
||||
'test-vm1', '@dispvm:default-dvm'))
|
||||
line = qubespolicy.PolicyRule('@anyvm @dispvm:default-dvm allow')
|
||||
self.assertTrue(line.is_match(system_info, 'test-vm1', '@dispvm'))
|
||||
line = qubespolicy.PolicyRule('@anyvm @dispvm:default-dvm allow')
|
||||
self.assertTrue(line.is_match(system_info,
|
||||
'test-vm1', '@dispvm:default-dvm'))
|
||||
line = qubespolicy.PolicyRule('@anyvm @dispvm:@tag:tag3 allow')
|
||||
self.assertTrue(line.is_match(system_info,
|
||||
'test-vm1', '@dispvm:test-vm3'))
|
||||
|
||||
def test_060_expand_target(self):
|
||||
lines = {
|
||||
'@anyvm @anyvm allow': ['test-vm1', 'test-vm2', 'test-vm3',
|
||||
'@dispvm:test-vm3',
|
||||
'default-dvm', '@dispvm:default-dvm', 'test-invalid-dvm',
|
||||
'test-no-dvm', 'test-template', 'test-standalone', '@dispvm'],
|
||||
'@anyvm @dispvm allow': ['@dispvm'],
|
||||
'@anyvm @dispvm:default-dvm allow': ['@dispvm:default-dvm'],
|
||||
# no DispVM from test-vm1 allowed
|
||||
'@anyvm @dispvm:test-vm1 allow': [],
|
||||
'@anyvm @dispvm:test-vm3 allow': ['@dispvm:test-vm3'],
|
||||
'@anyvm @dispvm:@tag:tag1 allow': [],
|
||||
'@anyvm @dispvm:@tag:tag3 allow': ['@dispvm:test-vm3'],
|
||||
'@anyvm test-vm1 allow': ['test-vm1'],
|
||||
'@anyvm @type:AppVM allow': ['test-vm1', 'test-vm2', 'test-vm3',
|
||||
'default-dvm', 'test-invalid-dvm', 'test-no-dvm'],
|
||||
'@anyvm @type:TemplateVM allow': ['test-template'],
|
||||
'@anyvm @tag:tag1 allow': ['test-vm1', 'test-invalid-dvm',
|
||||
'test-template', 'test-standalone', 'test-no-dvm'],
|
||||
'@anyvm @tag:tag2 allow': ['test-vm1', 'test-vm2',
|
||||
'test-invalid-dvm', 'test-template', 'test-standalone',
|
||||
'test-no-dvm'],
|
||||
'@anyvm @tag:no-such-tag allow': [],
|
||||
}
|
||||
for line in lines:
|
||||
with self.subTest(line):
|
||||
policy_line = qubespolicy.PolicyRule(line)
|
||||
self.assertCountEqual(list(policy_line.expand_target(system_info)),
|
||||
lines[line])
|
||||
|
||||
def test_070_expand_override_target(self):
|
||||
line = qubespolicy.PolicyRule(
|
||||
'@anyvm @anyvm allow,target=test-vm2')
|
||||
self.assertEqual(
|
||||
line.expand_override_target(system_info, 'test-vm1'),
|
||||
'test-vm2')
|
||||
|
||||
def test_071_expand_override_target_dispvm(self):
|
||||
line = qubespolicy.PolicyRule(
|
||||
'@anyvm @anyvm allow,target=@dispvm')
|
||||
self.assertEqual(
|
||||
line.expand_override_target(system_info, 'test-vm1'),
|
||||
'@dispvm:default-dvm')
|
||||
|
||||
def test_072_expand_override_target_dispvm_specific(self):
|
||||
line = qubespolicy.PolicyRule(
|
||||
'@anyvm @anyvm allow,target=@dispvm:test-vm3')
|
||||
self.assertEqual(
|
||||
line.expand_override_target(system_info, 'test-vm1'),
|
||||
'@dispvm:test-vm3')
|
||||
|
||||
def test_073_expand_override_target_dispvm_none(self):
|
||||
line = qubespolicy.PolicyRule(
|
||||
'@anyvm @anyvm allow,target=@dispvm')
|
||||
self.assertEqual(
|
||||
line.expand_override_target(system_info, 'test-no-dvm'),
|
||||
None)
|
||||
|
||||
def test_074_expand_override_target_dom0(self):
|
||||
line = qubespolicy.PolicyRule(
|
||||
'@anyvm @anyvm allow,target=dom0')
|
||||
self.assertEqual(
|
||||
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):
|
||||
rule = qubespolicy.PolicyRule('@anyvm @anyvm deny')
|
||||
with self.assertRaises(qubespolicy.AccessDenied):
|
||||
qubespolicy.PolicyAction('test.service', 'test-vm1', 'test-vm2',
|
||||
rule, 'test-vm2')
|
||||
|
||||
def test_001_init(self):
|
||||
rule = qubespolicy.PolicyRule('@anyvm @anyvm ask')
|
||||
action = qubespolicy.PolicyAction('test.service', 'test-vm1',
|
||||
None, rule, 'test-vm2', ['test-vm2', 'test-vm3'])
|
||||
self.assertEqual(action.service, 'test.service')
|
||||
self.assertEqual(action.source, 'test-vm1')
|
||||
self.assertIsNone(action.target)
|
||||
self.assertEqual(action.original_target, 'test-vm2')
|
||||
self.assertEqual(action.targets_for_ask, ['test-vm2', 'test-vm3'])
|
||||
self.assertEqual(action.rule, rule)
|
||||
self.assertEqual(action.action, qubespolicy.Action.ask)
|
||||
|
||||
def test_002_init_invalid(self):
|
||||
rule_ask = qubespolicy.PolicyRule('@anyvm @anyvm ask')
|
||||
rule_allow = qubespolicy.PolicyRule('@anyvm @anyvm allow')
|
||||
with self.assertRaises(qubespolicy.AccessDenied):
|
||||
qubespolicy.PolicyAction('test.service', 'test-vm1',
|
||||
None, rule_allow, 'test-vm2', None)
|
||||
with self.assertRaises(qubespolicy.AccessDenied):
|
||||
qubespolicy.PolicyAction('test.service', 'test-vm1',
|
||||
'test-vm2', rule_allow, 'test-vm2', ['test-vm2', 'test-vm3'])
|
||||
|
||||
with self.assertRaises(qubespolicy.AccessDenied):
|
||||
qubespolicy.PolicyAction('test.service', 'test-vm1',
|
||||
None, rule_ask, 'test-vm2', None)
|
||||
|
||||
def test_003_init_default_target(self):
|
||||
rule_ask = qubespolicy.PolicyRule('@anyvm @anyvm ask')
|
||||
|
||||
action = qubespolicy.PolicyAction('test.service', 'test-vm1',
|
||||
'test-vm1', rule_ask, 'test-vm2', ['test-vm2'])
|
||||
self.assertIsNone(action.target)
|
||||
|
||||
action = qubespolicy.PolicyAction('test.service', 'test-vm1',
|
||||
'test-vm2', rule_ask, 'test-vm2', ['test-vm2'])
|
||||
self.assertEqual(action.target, 'test-vm2')
|
||||
|
||||
def test_010_handle_user_response(self):
|
||||
rule = qubespolicy.PolicyRule('@anyvm @anyvm ask')
|
||||
action = qubespolicy.PolicyAction('test.service', 'test-vm1',
|
||||
None, rule, 'test-vm2', ['test-vm2', 'test-vm3'])
|
||||
action.handle_user_response(True, 'test-vm2')
|
||||
self.assertEqual(action.action, qubespolicy.Action.allow)
|
||||
self.assertEqual(action.target, 'test-vm2')
|
||||
|
||||
def test_011_handle_user_response(self):
|
||||
rule = qubespolicy.PolicyRule('@anyvm @anyvm ask')
|
||||
action = qubespolicy.PolicyAction('test.service', 'test-vm1',
|
||||
None, rule, 'test-vm2', ['test-vm2', 'test-vm3'])
|
||||
with self.assertRaises(AssertionError):
|
||||
action.handle_user_response(True, 'test-no-dvm')
|
||||
|
||||
def test_012_handle_user_response(self):
|
||||
rule = qubespolicy.PolicyRule('@anyvm @anyvm ask')
|
||||
action = qubespolicy.PolicyAction('test.service', 'test-vm1',
|
||||
None, rule, 'test-vm2', ['test-vm2', 'test-vm3'])
|
||||
with self.assertRaises(qubespolicy.AccessDenied):
|
||||
action.handle_user_response(False, None)
|
||||
self.assertEqual(action.action, qubespolicy.Action.deny)
|
||||
|
||||
def test_013_handle_user_response_with_default_target(self):
|
||||
rule = qubespolicy.PolicyRule(
|
||||
'@anyvm @anyvm ask,default_target=test-vm2')
|
||||
action = qubespolicy.PolicyAction('test.service', 'test-vm1',
|
||||
None, rule, 'test-vm2', ['test-vm2', 'test-vm3'])
|
||||
action.handle_user_response(True, 'test-vm2')
|
||||
self.assertEqual(action.action, qubespolicy.Action.allow)
|
||||
self.assertEqual(action.target, 'test-vm2')
|
||||
|
||||
@unittest.mock.patch('qubespolicy.qubesd_call')
|
||||
@unittest.mock.patch('subprocess.call')
|
||||
def test_020_execute(self, mock_subprocess, mock_qubesd_call):
|
||||
rule = qubespolicy.PolicyRule('@anyvm @anyvm allow')
|
||||
action = qubespolicy.PolicyAction('test.service', 'test-vm1',
|
||||
'test-vm2', rule, 'test-vm2')
|
||||
action.execute('some-ident')
|
||||
self.assertEqual(mock_qubesd_call.mock_calls,
|
||||
[unittest.mock.call('test-vm2', 'admin.vm.Start')])
|
||||
self.assertEqual(mock_subprocess.mock_calls,
|
||||
[unittest.mock.call([qubespolicy.QREXEC_CLIENT, '-d', 'test-vm2',
|
||||
'-c', 'some-ident', 'DEFAULT:QUBESRPC test.service test-vm1'])])
|
||||
|
||||
@unittest.mock.patch('qubespolicy.qubesd_call')
|
||||
@unittest.mock.patch('subprocess.call')
|
||||
def test_021_execute_dom0(self, mock_subprocess, mock_qubesd_call):
|
||||
rule = qubespolicy.PolicyRule('@anyvm dom0 allow')
|
||||
action = qubespolicy.PolicyAction('test.service', 'test-vm1',
|
||||
'dom0', rule, 'dom0')
|
||||
action.execute('some-ident')
|
||||
self.assertEqual(mock_qubesd_call.mock_calls, [])
|
||||
self.assertEqual(mock_subprocess.mock_calls,
|
||||
[unittest.mock.call([qubespolicy.QREXEC_CLIENT, '-d', 'dom0',
|
||||
'-c', 'some-ident',
|
||||
'QUBESRPC test.service test-vm1 name dom0'])])
|
||||
|
||||
@unittest.mock.patch('qubespolicy.qubesd_call')
|
||||
@unittest.mock.patch('subprocess.call')
|
||||
def test_021_execute_dom0_keyword(self, mock_subprocess, mock_qubesd_call):
|
||||
rule = qubespolicy.PolicyRule('@anyvm dom0 allow')
|
||||
action = qubespolicy.PolicyAction('test.service', 'test-vm1',
|
||||
'dom0', rule, '@adminvm')
|
||||
action.execute('some-ident')
|
||||
self.assertEqual(mock_qubesd_call.mock_calls, [])
|
||||
self.assertEqual(mock_subprocess.mock_calls,
|
||||
[unittest.mock.call([qubespolicy.QREXEC_CLIENT, '-d', 'dom0',
|
||||
'-c', 'some-ident',
|
||||
'QUBESRPC test.service test-vm1 keyword adminvm'])])
|
||||
|
||||
@unittest.mock.patch('qubespolicy.qubesd_call')
|
||||
@unittest.mock.patch('subprocess.call')
|
||||
def test_022_execute_dispvm(self, mock_subprocess, mock_qubesd_call):
|
||||
rule = qubespolicy.PolicyRule('@anyvm @dispvm:default-dvm allow')
|
||||
action = qubespolicy.PolicyAction('test.service', 'test-vm1',
|
||||
'@dispvm:default-dvm', rule, '@dispvm:default-dvm')
|
||||
mock_qubesd_call.side_effect = (lambda target, call:
|
||||
b'dispvm-name' if call == 'admin.vm.CreateDisposable' else
|
||||
unittest.mock.DEFAULT)
|
||||
action.execute('some-ident')
|
||||
self.assertEqual(mock_qubesd_call.mock_calls,
|
||||
[unittest.mock.call('default-dvm', 'admin.vm.CreateDisposable'),
|
||||
unittest.mock.call('dispvm-name', 'admin.vm.Start'),
|
||||
unittest.mock.call('dispvm-name', 'admin.vm.Kill')])
|
||||
self.assertEqual(mock_subprocess.mock_calls,
|
||||
[unittest.mock.call([qubespolicy.QREXEC_CLIENT, '-d', 'dispvm-name',
|
||||
'-c', 'some-ident', '-W',
|
||||
'DEFAULT:QUBESRPC test.service test-vm1'])])
|
||||
|
||||
@unittest.mock.patch('qubespolicy.qubesd_call')
|
||||
@unittest.mock.patch('subprocess.call')
|
||||
def test_023_execute_already_running(self, mock_subprocess,
|
||||
mock_qubesd_call):
|
||||
rule = qubespolicy.PolicyRule('@anyvm @anyvm allow')
|
||||
action = qubespolicy.PolicyAction('test.service', 'test-vm1',
|
||||
'test-vm2', rule, 'test-vm2')
|
||||
mock_qubesd_call.side_effect = \
|
||||
qubespolicy.QubesMgmtException('QubesVMNotHaltedError')
|
||||
action.execute('some-ident')
|
||||
self.assertEqual(mock_qubesd_call.mock_calls,
|
||||
[unittest.mock.call('test-vm2', 'admin.vm.Start')])
|
||||
self.assertEqual(mock_subprocess.mock_calls,
|
||||
[unittest.mock.call([qubespolicy.QREXEC_CLIENT, '-d', 'test-vm2',
|
||||
'-c', 'some-ident', 'DEFAULT:QUBESRPC test.service test-vm1'])])
|
||||
|
||||
@unittest.mock.patch('qubespolicy.qubesd_call')
|
||||
@unittest.mock.patch('subprocess.call')
|
||||
def test_024_execute_startup_error(self, mock_subprocess,
|
||||
mock_qubesd_call):
|
||||
rule = qubespolicy.PolicyRule('@anyvm @anyvm allow')
|
||||
action = qubespolicy.PolicyAction('test.service', 'test-vm1',
|
||||
'test-vm2', rule, 'test-vm2')
|
||||
mock_qubesd_call.side_effect = \
|
||||
qubespolicy.QubesMgmtException('QubesVMError')
|
||||
with self.assertRaises(qubespolicy.QubesMgmtException):
|
||||
action.execute('some-ident')
|
||||
self.assertEqual(mock_qubesd_call.mock_calls,
|
||||
[unittest.mock.call('test-vm2', 'admin.vm.Start')])
|
||||
self.assertEqual(mock_subprocess.mock_calls, [])
|
||||
|
||||
class TC_20_Policy(qubes.tests.QubesTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TC_20_Policy, self).setUp()
|
||||
if not os.path.exists(tmp_policy_dir):
|
||||
os.mkdir(tmp_policy_dir)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(tmp_policy_dir)
|
||||
super(TC_20_Policy, self).tearDown()
|
||||
|
||||
def test_000_load(self):
|
||||
with open(os.path.join(tmp_policy_dir, 'test.service'), 'w') as f:
|
||||
f.write('test-vm1 test-vm2 allow\n')
|
||||
f.write('\n')
|
||||
f.write('# comment\n')
|
||||
f.write('test-vm2 test-vm3 ask\n')
|
||||
f.write(' # comment \n')
|
||||
f.write('@anyvm @anyvm ask\n')
|
||||
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')
|
||||
self.assertEqual(policy.policy_rules[0].target, 'test-vm2')
|
||||
self.assertEqual(policy.policy_rules[0].action,
|
||||
qubespolicy.Action.allow)
|
||||
|
||||
def test_001_not_existent(self):
|
||||
with self.assertRaises(qubespolicy.AccessDenied):
|
||||
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:
|
||||
f.write('test-vm1 test-vm2 allow\n')
|
||||
f.write('@include:test.service2\n')
|
||||
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', 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')
|
||||
self.assertEqual(policy.policy_rules[0].target, 'test-vm2')
|
||||
self.assertEqual(policy.policy_rules[0].action,
|
||||
qubespolicy.Action.allow)
|
||||
self.assertEqual(policy.policy_rules[0].filename,
|
||||
tmp_policy_dir + '/test.service')
|
||||
self.assertEqual(policy.policy_rules[0].lineno, 1)
|
||||
self.assertEqual(policy.policy_rules[1].source, 'test-vm3')
|
||||
self.assertEqual(policy.policy_rules[1].target, '@default')
|
||||
self.assertEqual(policy.policy_rules[1].action,
|
||||
qubespolicy.Action.allow)
|
||||
self.assertEqual(policy.policy_rules[1].filename,
|
||||
tmp_policy_dir + '/test.service2')
|
||||
self.assertEqual(policy.policy_rules[1].lineno, 1)
|
||||
self.assertEqual(policy.policy_rules[2].source, '@anyvm')
|
||||
self.assertEqual(policy.policy_rules[2].target, '@anyvm')
|
||||
self.assertEqual(policy.policy_rules[2].action,
|
||||
qubespolicy.Action.deny)
|
||||
self.assertEqual(policy.policy_rules[2].filename,
|
||||
tmp_policy_dir + '/test.service')
|
||||
self.assertEqual(policy.policy_rules[2].lineno, 3)
|
||||
|
||||
def test_003_load_convert(self):
|
||||
with open(os.path.join(tmp_policy_dir, 'test.service'), 'w') as f:
|
||||
f.write('test-vm2 test-vm3 ask\n')
|
||||
f.write(' # comment \n')
|
||||
f.write('$anyvm $dispvm ask,default_target=$dispvm\n')
|
||||
policy = qubespolicy.Policy('test.service', tmp_policy_dir)
|
||||
self.assertEqual(policy.service, 'test.service')
|
||||
self.assertEqual(len(policy.policy_rules), 2)
|
||||
self.assertEqual(policy.policy_rules[1].source, '@anyvm')
|
||||
self.assertEqual(policy.policy_rules[1].target, '@dispvm')
|
||||
self.assertEqual(policy.policy_rules[1].action,
|
||||
qubespolicy.Action.ask)
|
||||
self.assertEqual(policy.policy_rules[1].default_target,
|
||||
'@dispvm')
|
||||
|
||||
def test_010_find_rule(self):
|
||||
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 @anyvm ask\n')
|
||||
f.write('test-vm2 @tag:tag1 deny\n')
|
||||
f.write('test-vm2 @tag:tag2 allow\n')
|
||||
f.write('test-vm2 @dispvm:@tag:tag3 allow\n')
|
||||
f.write('test-vm2 @dispvm:@tag:tag2 allow\n')
|
||||
f.write('test-vm2 @dispvm:default-dvm 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', 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(
|
||||
system_info, 'test-vm1', 'test-vm3'), policy.policy_rules[1])
|
||||
self.assertEqual(policy.find_matching_rule(
|
||||
system_info, 'test-vm2', 'test-vm2'), policy.policy_rules[3])
|
||||
self.assertEqual(policy.find_matching_rule(
|
||||
system_info, 'test-vm2', 'test-no-dvm'), policy.policy_rules[2])
|
||||
# @anyvm matches @default too
|
||||
self.assertEqual(policy.find_matching_rule(
|
||||
system_info, 'test-vm1', '@default'), policy.policy_rules[1])
|
||||
self.assertEqual(policy.find_matching_rule(
|
||||
system_info, 'test-vm2', '@default'), policy.policy_rules[7])
|
||||
self.assertEqual(policy.find_matching_rule(
|
||||
system_info, 'test-no-dvm', 'test-vm3'), policy.policy_rules[8])
|
||||
self.assertEqual(policy.find_matching_rule(
|
||||
system_info, 'test-vm2', '@dispvm:test-vm3'),
|
||||
policy.policy_rules[4])
|
||||
self.assertEqual(policy.find_matching_rule(
|
||||
system_info, 'test-vm2', '@dispvm'),
|
||||
policy.policy_rules[6])
|
||||
with self.assertRaises(qubespolicy.AccessDenied):
|
||||
policy.find_matching_rule(
|
||||
system_info, 'test-no-dvm', 'test-standalone')
|
||||
with self.assertRaises(qubespolicy.AccessDenied):
|
||||
policy.find_matching_rule(system_info, 'test-no-dvm', '@dispvm')
|
||||
with self.assertRaises(qubespolicy.AccessDenied):
|
||||
policy.find_matching_rule(
|
||||
system_info, 'test-standalone', '@default')
|
||||
|
||||
def test_020_collect_targets_for_ask(self):
|
||||
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 @anyvm ask\n')
|
||||
f.write('test-vm2 @tag:tag1 deny\n')
|
||||
f.write('test-vm2 @tag:tag2 allow\n')
|
||||
f.write('test-no-dvm @type:AppVM deny\n')
|
||||
f.write('@type:AppVM @default allow,target=test-vm3\n')
|
||||
f.write('@tag:tag1 @type:AppVM allow\n')
|
||||
f.write('test-no-dvm @dispvm allow\n')
|
||||
f.write('test-standalone @dispvm allow\n')
|
||||
f.write('test-standalone @adminvm allow\n')
|
||||
policy = qubespolicy.Policy('test.service', tmp_policy_dir)
|
||||
self.assertCountEqual(policy.collect_targets_for_ask(system_info,
|
||||
'test-vm1'), ['test-vm2', 'test-vm3',
|
||||
'@dispvm:test-vm3',
|
||||
'default-dvm', '@dispvm:default-dvm', 'test-invalid-dvm',
|
||||
'test-no-dvm', 'test-template', 'test-standalone'])
|
||||
self.assertCountEqual(policy.collect_targets_for_ask(system_info,
|
||||
'test-vm2'), ['test-vm3'])
|
||||
self.assertCountEqual(policy.collect_targets_for_ask(system_info,
|
||||
'test-vm3'), [])
|
||||
self.assertCountEqual(policy.collect_targets_for_ask(system_info,
|
||||
'test-standalone'), ['test-vm1', 'test-vm2', 'test-vm3',
|
||||
'default-dvm', 'test-no-dvm', 'test-invalid-dvm',
|
||||
'@dispvm:default-dvm', 'dom0'])
|
||||
self.assertCountEqual(policy.collect_targets_for_ask(system_info,
|
||||
'test-no-dvm'), [])
|
||||
|
||||
def test_030_eval_simple(self):
|
||||
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', 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)
|
||||
self.assertEqual(action.target, 'test-vm2')
|
||||
self.assertEqual(action.original_target, 'test-vm2')
|
||||
self.assertEqual(action.service, 'test.service')
|
||||
self.assertIsNone(action.targets_for_ask)
|
||||
with self.assertRaises(qubespolicy.AccessDenied):
|
||||
policy.evaluate(system_info, 'test-vm2', '@default')
|
||||
|
||||
def test_031_eval_default(self):
|
||||
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 @default allow,target=test-vm2\n')
|
||||
f.write('@tag:tag1 test-vm2 ask\n')
|
||||
f.write('@tag:tag2 @anyvm allow\n')
|
||||
f.write('test-vm3 @anyvm deny\n')
|
||||
|
||||
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)
|
||||
self.assertEqual(action.target, 'test-vm2')
|
||||
self.assertEqual(action.original_target, '@default')
|
||||
self.assertEqual(action.service, 'test.service')
|
||||
self.assertIsNone(action.targets_for_ask)
|
||||
with self.assertRaises(qubespolicy.AccessDenied):
|
||||
# action allow should hit, but no target specified (either by
|
||||
# caller or policy)
|
||||
policy.evaluate(system_info, 'test-standalone', '@default')
|
||||
|
||||
def test_032_eval_ask(self):
|
||||
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 @default allow,target=test-vm2\n')
|
||||
f.write('@tag:tag1 test-vm2 ask\n')
|
||||
f.write('@tag:tag1 test-vm3 ask,default_target=test-vm3\n')
|
||||
f.write('@tag:tag2 @anyvm allow\n')
|
||||
f.write('test-vm3 @anyvm deny\n')
|
||||
|
||||
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)
|
||||
self.assertIsNone(action.target)
|
||||
self.assertEqual(action.original_target, 'test-vm2')
|
||||
self.assertEqual(action.service, 'test.service')
|
||||
self.assertCountEqual(action.targets_for_ask,
|
||||
['test-vm1', 'test-vm2', 'test-vm3', '@dispvm:test-vm3',
|
||||
'default-dvm', '@dispvm:default-dvm', 'test-invalid-dvm',
|
||||
'test-no-dvm', 'test-template'])
|
||||
|
||||
def test_033_eval_ask(self):
|
||||
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 @default allow,target=test-vm2\n')
|
||||
f.write('@tag:tag1 test-vm2 ask\n')
|
||||
f.write('@tag:tag1 test-vm3 ask,default_target=test-vm3\n')
|
||||
f.write('@tag:tag2 @anyvm allow\n')
|
||||
f.write('test-vm3 @anyvm deny\n')
|
||||
|
||||
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)
|
||||
self.assertEqual(action.target, 'test-vm3')
|
||||
self.assertEqual(action.original_target, 'test-vm3')
|
||||
self.assertEqual(action.service, 'test.service')
|
||||
self.assertCountEqual(action.targets_for_ask,
|
||||
['test-vm1', 'test-vm2', 'test-vm3', '@dispvm:test-vm3',
|
||||
'default-dvm', '@dispvm:default-dvm', 'test-invalid-dvm',
|
||||
'test-no-dvm', 'test-template'])
|
||||
|
||||
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)
|
||||
|
||||
def test_035_eval_resolve_dispvm_fail(self):
|
||||
with open(os.path.join(tmp_policy_dir, 'test.service'), 'w') as f:
|
||||
f.write('test-no-dvm @dispvm allow\n')
|
||||
|
||||
policy = qubespolicy.Policy('test.service', tmp_policy_dir)
|
||||
with self.assertRaises(qubespolicy.AccessDenied):
|
||||
policy.evaluate(system_info, 'test-no-dvm', '@dispvm')
|
||||
|
||||
def test_036_eval_invalid_override_target(self):
|
||||
with open(os.path.join(tmp_policy_dir, 'test.service'), 'w') as f:
|
||||
f.write('test-vm3 @anyvm allow,target=no-such-vm\n')
|
||||
|
||||
policy = qubespolicy.Policy('test.service', tmp_policy_dir)
|
||||
with self.assertRaises(qubespolicy.AccessDenied):
|
||||
policy.evaluate(system_info, 'test-vm3', '@default')
|
||||
|
||||
def test_037_eval_ask_no_targets(self):
|
||||
with open(os.path.join(tmp_policy_dir, 'test.service'), 'w') as f:
|
||||
f.write('test-vm3 @default ask\n')
|
||||
|
||||
policy = qubespolicy.Policy('test.service', tmp_policy_dir)
|
||||
with self.assertRaises(qubespolicy.AccessDenied):
|
||||
policy.evaluate(system_info, 'test-vm3', '@default')
|
||||
|
||||
|
||||
class TC_30_Misc(qubes.tests.QubesTestCase):
|
||||
@unittest.mock.patch('socket.socket')
|
||||
def test_000_qubesd_call(self, mock_socket):
|
||||
mock_config = {
|
||||
'return_value.makefile.return_value.read.return_value': b'0\x00data'
|
||||
}
|
||||
mock_socket.configure_mock(**mock_config)
|
||||
result = qubespolicy.qubesd_call('test', 'internal.method')
|
||||
self.assertEqual(result, b'data')
|
||||
self.assertEqual(mock_socket.mock_calls, [
|
||||
unittest.mock.call(socket.AF_UNIX, socket.SOCK_STREAM),
|
||||
unittest.mock.call().connect(qubespolicy.QUBESD_INTERNAL_SOCK),
|
||||
unittest.mock.call().sendall(b'dom0'),
|
||||
unittest.mock.call().sendall(b'\x00'),
|
||||
unittest.mock.call().sendall(b'internal.method'),
|
||||
unittest.mock.call().sendall(b'\x00'),
|
||||
unittest.mock.call().sendall(b'test'),
|
||||
unittest.mock.call().sendall(b'\x00'),
|
||||
unittest.mock.call().sendall(b'\x00'),
|
||||
unittest.mock.call().shutdown(socket.SHUT_WR),
|
||||
unittest.mock.call().makefile('rb'),
|
||||
unittest.mock.call().makefile().read(),
|
||||
])
|
||||
|
||||
@unittest.mock.patch('socket.socket')
|
||||
def test_001_qubesd_call_arg_payload(self, mock_socket):
|
||||
mock_config = {
|
||||
'return_value.makefile.return_value.read.return_value': b'0\x00data'
|
||||
}
|
||||
mock_socket.configure_mock(**mock_config)
|
||||
result = qubespolicy.qubesd_call('test', 'internal.method', 'arg',
|
||||
b'payload')
|
||||
self.assertEqual(result, b'data')
|
||||
self.assertEqual(mock_socket.mock_calls, [
|
||||
unittest.mock.call(socket.AF_UNIX, socket.SOCK_STREAM),
|
||||
unittest.mock.call().connect(qubespolicy.QUBESD_INTERNAL_SOCK),
|
||||
unittest.mock.call().sendall(b'dom0'),
|
||||
unittest.mock.call().sendall(b'\x00'),
|
||||
unittest.mock.call().sendall(b'internal.method'),
|
||||
unittest.mock.call().sendall(b'\x00'),
|
||||
unittest.mock.call().sendall(b'test'),
|
||||
unittest.mock.call().sendall(b'\x00'),
|
||||
unittest.mock.call().sendall(b'arg'),
|
||||
unittest.mock.call().sendall(b'\x00'),
|
||||
unittest.mock.call().sendall(b'payload'),
|
||||
unittest.mock.call().shutdown(socket.SHUT_WR),
|
||||
unittest.mock.call().makefile('rb'),
|
||||
unittest.mock.call().makefile().read(),
|
||||
])
|
||||
|
||||
@unittest.mock.patch('socket.socket')
|
||||
def test_002_qubesd_call_exception(self, mock_socket):
|
||||
mock_config = {
|
||||
'return_value.makefile.return_value.read.return_value':
|
||||
b'2\x00SomeError\x00traceback\x00message\x00'
|
||||
}
|
||||
mock_socket.configure_mock(**mock_config)
|
||||
with self.assertRaises(qubespolicy.QubesMgmtException) as e:
|
||||
qubespolicy.qubesd_call('test', 'internal.method')
|
||||
self.assertEqual(e.exception.exc_type, 'SomeError')
|
||||
self.assertEqual(mock_socket.mock_calls, [
|
||||
unittest.mock.call(socket.AF_UNIX, socket.SOCK_STREAM),
|
||||
unittest.mock.call().connect(qubespolicy.QUBESD_INTERNAL_SOCK),
|
||||
unittest.mock.call().sendall(b'dom0'),
|
||||
unittest.mock.call().sendall(b'\x00'),
|
||||
unittest.mock.call().sendall(b'internal.method'),
|
||||
unittest.mock.call().sendall(b'\x00'),
|
||||
unittest.mock.call().sendall(b'test'),
|
||||
unittest.mock.call().sendall(b'\x00'),
|
||||
unittest.mock.call().sendall(b'\x00'),
|
||||
unittest.mock.call().shutdown(socket.SHUT_WR),
|
||||
unittest.mock.call().makefile('rb'),
|
||||
unittest.mock.call().makefile().read(),
|
||||
])
|
||||
|
@ -1,343 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2017 Marek Marczykowski-Górecki
|
||||
# <marmarek@invisiblethingslab.com>
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
|
||||
import os
|
||||
import tempfile
|
||||
import unittest.mock
|
||||
|
||||
import shutil
|
||||
|
||||
import qubes.tests
|
||||
import qubespolicy
|
||||
import qubespolicy.cli
|
||||
import qubespolicy.tests
|
||||
|
||||
|
||||
|
||||
class TC_00_qrexec_policy(qubes.tests.QubesTestCase):
|
||||
def setUp(self):
|
||||
super(TC_00_qrexec_policy, self).setUp()
|
||||
self.policy_patch = unittest.mock.patch('qubespolicy.Policy')
|
||||
self.policy_mock = self.policy_patch.start()
|
||||
|
||||
self.system_info_patch = unittest.mock.patch(
|
||||
'qubespolicy.get_system_info')
|
||||
self.system_info_mock = self.system_info_patch.start()
|
||||
|
||||
self.system_info = {
|
||||
'domains': {'dom0': {'icon': 'black', 'template_for_dispvms': False},
|
||||
'test-vm1': {'icon': 'red', 'template_for_dispvms': False},
|
||||
'test-vm2': {'icon': 'red', 'template_for_dispvms': False},
|
||||
'test-vm3': {'icon': 'green', 'template_for_dispvms': True}, }}
|
||||
self.system_info_mock.return_value = self.system_info
|
||||
|
||||
self.dbus_patch = unittest.mock.patch('pydbus.SystemBus')
|
||||
self.dbus_mock = self.dbus_patch.start()
|
||||
|
||||
self.policy_dir = tempfile.TemporaryDirectory()
|
||||
self.policydir_patch = unittest.mock.patch('qubespolicy.POLICY_DIR',
|
||||
self.policy_dir.name)
|
||||
self.policydir_patch.start()
|
||||
|
||||
def tearDown(self):
|
||||
self.policydir_patch.stop()
|
||||
self.policy_dir.cleanup()
|
||||
self.dbus_patch.start()
|
||||
self.system_info_patch.stop()
|
||||
self.policy_patch.stop()
|
||||
super(TC_00_qrexec_policy, self).tearDown()
|
||||
|
||||
def test_000_allow(self):
|
||||
self.policy_mock.configure_mock(**{
|
||||
'return_value.evaluate.return_value.action':
|
||||
qubespolicy.Action.allow,
|
||||
})
|
||||
retval = qubespolicy.cli.main(
|
||||
['source-id', 'source', 'target', 'service', 'process_ident'])
|
||||
self.assertEqual(retval, 0)
|
||||
self.assertEqual(self.policy_mock.mock_calls, [
|
||||
('', ('service',), {}),
|
||||
('().evaluate', (self.system_info, 'source',
|
||||
'target'), {}),
|
||||
('().evaluate().target.__str__', (), {}),
|
||||
('().evaluate().execute', ('process_ident,source,source-id', ), {}),
|
||||
])
|
||||
self.assertEqual(self.dbus_mock.mock_calls, [])
|
||||
|
||||
def test_010_ask_allow(self):
|
||||
self.policy_mock.configure_mock(**{
|
||||
'return_value.evaluate.return_value.action':
|
||||
qubespolicy.Action.ask,
|
||||
'return_value.evaluate.return_value.target':
|
||||
None,
|
||||
'return_value.evaluate.return_value.targets_for_ask':
|
||||
['test-vm1', 'test-vm2'],
|
||||
})
|
||||
self.dbus_mock.configure_mock(**{
|
||||
'return_value.get.return_value.Ask.return_value': 'test-vm1'
|
||||
})
|
||||
retval = qubespolicy.cli.main(
|
||||
['source-id', 'source', 'target', 'service', 'process_ident'])
|
||||
self.assertEqual(retval, 0)
|
||||
self.assertEqual(self.policy_mock.mock_calls, [
|
||||
('', ('service',), {}),
|
||||
('().evaluate', (self.system_info, 'source',
|
||||
'target'), {}),
|
||||
('().evaluate().handle_user_response', (True, 'test-vm1'), {}),
|
||||
('().evaluate().execute', ('process_ident,source,source-id', ), {}),
|
||||
])
|
||||
icons = {
|
||||
'dom0': 'black',
|
||||
'test-vm1': 'red',
|
||||
'test-vm2': 'red',
|
||||
'test-vm3': 'green',
|
||||
'@dispvm:test-vm3': 'green',
|
||||
}
|
||||
self.assertEqual(self.dbus_mock.mock_calls, [
|
||||
('', (), {}),
|
||||
('().get', ('org.qubesos.PolicyAgent',
|
||||
'/org/qubesos/PolicyAgent'), {}),
|
||||
('().get().Ask', ('source', 'service', ['test-vm1', 'test-vm2'],
|
||||
'', icons), {}),
|
||||
])
|
||||
|
||||
def test_011_ask_deny(self):
|
||||
self.policy_mock.configure_mock(**{
|
||||
'return_value.evaluate.return_value.action':
|
||||
qubespolicy.Action.ask,
|
||||
'return_value.evaluate.return_value.target':
|
||||
None,
|
||||
'return_value.evaluate.return_value.targets_for_ask':
|
||||
['test-vm1', 'test-vm2'],
|
||||
'return_value.evaluate.return_value.handle_user_response'
|
||||
'.side_effect':
|
||||
qubespolicy.AccessDenied,
|
||||
})
|
||||
self.dbus_mock.configure_mock(**{
|
||||
'return_value.get.return_value.Ask.return_value': ''
|
||||
})
|
||||
retval = qubespolicy.cli.main(
|
||||
['source-id', 'source', 'target', 'service', 'process_ident'])
|
||||
self.assertEqual(retval, 1)
|
||||
self.assertEqual(self.policy_mock.mock_calls, [
|
||||
('', ('service',), {}),
|
||||
('().evaluate', (self.system_info, 'source',
|
||||
'target'), {}),
|
||||
('().evaluate().handle_user_response', (False,), {}),
|
||||
])
|
||||
icons = {
|
||||
'dom0': 'black',
|
||||
'test-vm1': 'red',
|
||||
'test-vm2': 'red',
|
||||
'test-vm3': 'green',
|
||||
'@dispvm:test-vm3': 'green',
|
||||
}
|
||||
self.assertEqual(self.dbus_mock.mock_calls, [
|
||||
('', (), {}),
|
||||
('().get', ('org.qubesos.PolicyAgent',
|
||||
'/org/qubesos/PolicyAgent'), {}),
|
||||
('().get().Ask', ('source', 'service', ['test-vm1', 'test-vm2'],
|
||||
'', icons), {}),
|
||||
])
|
||||
|
||||
def test_012_ask_default_target(self):
|
||||
self.policy_mock.configure_mock(**{
|
||||
'return_value.evaluate.return_value.action':
|
||||
qubespolicy.Action.ask,
|
||||
'return_value.evaluate.return_value.target':
|
||||
'test-vm1',
|
||||
'return_value.evaluate.return_value.targets_for_ask':
|
||||
['test-vm1', 'test-vm2'],
|
||||
})
|
||||
self.dbus_mock.configure_mock(**{
|
||||
'return_value.get.return_value.Ask.return_value': 'test-vm1'
|
||||
})
|
||||
retval = qubespolicy.cli.main(
|
||||
['source-id', 'source', 'target', 'service', 'process_ident'])
|
||||
self.assertEqual(retval, 0)
|
||||
self.assertEqual(self.policy_mock.mock_calls, [
|
||||
('', ('service',), {}),
|
||||
('().evaluate', (self.system_info, 'source',
|
||||
'target'), {}),
|
||||
('().evaluate().handle_user_response', (True, 'test-vm1'), {}),
|
||||
('().evaluate().execute', ('process_ident,source,source-id',), {}),
|
||||
])
|
||||
icons = {
|
||||
'dom0': 'black',
|
||||
'test-vm1': 'red',
|
||||
'test-vm2': 'red',
|
||||
'test-vm3': 'green',
|
||||
'@dispvm:test-vm3': 'green',
|
||||
}
|
||||
self.assertEqual(self.dbus_mock.mock_calls, [
|
||||
('', (), {}),
|
||||
('().get', ('org.qubesos.PolicyAgent',
|
||||
'/org/qubesos/PolicyAgent'), {}),
|
||||
('().get().Ask', ('source', 'service', ['test-vm1', 'test-vm2'],
|
||||
'test-vm1', icons), {}),
|
||||
])
|
||||
|
||||
def test_020_deny(self):
|
||||
self.policy_mock.configure_mock(**{
|
||||
'return_value.evaluate.return_value.action':
|
||||
qubespolicy.Action.deny,
|
||||
'return_value.evaluate.return_value.execute.side_effect':
|
||||
qubespolicy.AccessDenied,
|
||||
})
|
||||
retval = qubespolicy.cli.main(
|
||||
['source-id', 'source', 'target', 'service', 'process_ident'])
|
||||
self.assertEqual(retval, 1)
|
||||
self.assertEqual(self.policy_mock.mock_calls, [
|
||||
('', ('service',), {}),
|
||||
('().evaluate', (self.system_info, 'source',
|
||||
'target'), {}),
|
||||
('().evaluate().target.__str__', (), {}),
|
||||
('().evaluate().execute', ('process_ident,source,source-id',), {}),
|
||||
])
|
||||
self.assertEqual(self.dbus_mock.mock_calls, [])
|
||||
|
||||
def test_030_just_evaluate_allow(self):
|
||||
self.policy_mock.configure_mock(**{
|
||||
'return_value.evaluate.return_value.action':
|
||||
qubespolicy.Action.allow,
|
||||
})
|
||||
retval = qubespolicy.cli.main(
|
||||
['--just-evaluate',
|
||||
'source-id', 'source', 'target', 'service', 'process_ident'])
|
||||
self.assertEqual(retval, 0)
|
||||
self.assertEqual(self.policy_mock.mock_calls, [
|
||||
('', ('service',), {}),
|
||||
('().evaluate', (self.system_info, 'source',
|
||||
'target'), {}),
|
||||
])
|
||||
self.assertEqual(self.dbus_mock.mock_calls, [])
|
||||
|
||||
def test_031_just_evaluate_deny(self):
|
||||
self.policy_mock.configure_mock(**{
|
||||
'return_value.evaluate.return_value.action':
|
||||
qubespolicy.Action.deny,
|
||||
})
|
||||
retval = qubespolicy.cli.main(
|
||||
['--just-evaluate',
|
||||
'source-id', 'source', 'target', 'service', 'process_ident'])
|
||||
self.assertEqual(retval, 1)
|
||||
self.assertEqual(self.policy_mock.mock_calls, [
|
||||
('', ('service',), {}),
|
||||
('().evaluate', (self.system_info, 'source',
|
||||
'target'), {}),
|
||||
])
|
||||
self.assertEqual(self.dbus_mock.mock_calls, [])
|
||||
|
||||
def test_032_just_evaluate_ask(self):
|
||||
self.policy_mock.configure_mock(**{
|
||||
'return_value.evaluate.return_value.action':
|
||||
qubespolicy.Action.ask,
|
||||
})
|
||||
retval = qubespolicy.cli.main(
|
||||
['--just-evaluate',
|
||||
'source-id', 'source', 'target', 'service', 'process_ident'])
|
||||
self.assertEqual(retval, 1)
|
||||
self.assertEqual(self.policy_mock.mock_calls, [
|
||||
('', ('service',), {}),
|
||||
('().evaluate', (self.system_info, 'source',
|
||||
'target'), {}),
|
||||
])
|
||||
self.assertEqual(self.dbus_mock.mock_calls, [])
|
||||
|
||||
def test_033_just_evaluate_ask_assume_yes(self):
|
||||
self.policy_mock.configure_mock(**{
|
||||
'return_value.evaluate.return_value.action':
|
||||
qubespolicy.Action.ask,
|
||||
})
|
||||
retval = qubespolicy.cli.main(
|
||||
['--just-evaluate', '--assume-yes-for-ask',
|
||||
'source-id', 'source', 'target', 'service', 'process_ident'])
|
||||
self.assertEqual(retval, 0)
|
||||
self.assertEqual(self.policy_mock.mock_calls, [
|
||||
('', ('service',), {}),
|
||||
('().evaluate', (self.system_info, 'source',
|
||||
'target'), {}),
|
||||
])
|
||||
self.assertEqual(self.dbus_mock.mock_calls, [])
|
||||
|
||||
def test_040_create_policy(self):
|
||||
self.policy_mock.configure_mock(**{
|
||||
'side_effect':
|
||||
[qubespolicy.PolicyNotFound('service'), unittest.mock.DEFAULT],
|
||||
'return_value.evaluate.return_value.action':
|
||||
qubespolicy.Action.allow,
|
||||
})
|
||||
self.dbus_mock.configure_mock(**{
|
||||
'return_value.get.return_value.ConfirmPolicyCreate.return_value':
|
||||
True
|
||||
})
|
||||
retval = qubespolicy.cli.main(
|
||||
['source-id', 'source', 'target', 'service', 'process_ident'])
|
||||
self.assertEqual(retval, 0)
|
||||
self.assertEqual(self.policy_mock.mock_calls, [
|
||||
('', ('service',), {}),
|
||||
('', ('service',), {}),
|
||||
('().evaluate', (self.system_info, 'source',
|
||||
'target'), {}),
|
||||
('().evaluate().target.__str__', (), {}),
|
||||
('().evaluate().execute', ('process_ident,source,source-id',), {}),
|
||||
])
|
||||
self.assertEqual(self.dbus_mock.mock_calls, [
|
||||
('', (), {}),
|
||||
('().get', ('org.qubesos.PolicyAgent',
|
||||
'/org/qubesos/PolicyAgent'), {}),
|
||||
('().get().ConfirmPolicyCreate', ('source', 'service'), {}),
|
||||
])
|
||||
policy_path = os.path.join(self.policy_dir.name, 'service')
|
||||
self.assertTrue(os.path.exists(policy_path))
|
||||
with open(policy_path) as policy_file:
|
||||
self.assertEqual(policy_file.read(),
|
||||
"## Policy file automatically created on first service call.\n"
|
||||
"## Feel free to edit.\n"
|
||||
"## Note that policy parsing stops at the first match\n"
|
||||
"\n"
|
||||
"## Please use a single # to start your custom comments\n"
|
||||
"\n"
|
||||
"@anyvm @anyvm ask\n")
|
||||
|
||||
def test_041_create_policy_abort(self):
|
||||
self.policy_mock.configure_mock(**{
|
||||
'side_effect':
|
||||
[qubespolicy.PolicyNotFound('service'), unittest.mock.DEFAULT],
|
||||
'return_value.evaluate.return_value.action':
|
||||
qubespolicy.Action.deny,
|
||||
})
|
||||
self.dbus_mock.configure_mock(**{
|
||||
'return_value.get.return_value.ConfirmPolicyCreate.return_value':
|
||||
False
|
||||
})
|
||||
retval = qubespolicy.cli.main(
|
||||
['source-id', 'source', 'target', 'service', 'process_ident'])
|
||||
self.assertEqual(retval, 1)
|
||||
self.assertEqual(self.policy_mock.mock_calls, [
|
||||
('', ('service',), {}),
|
||||
])
|
||||
self.assertEqual(self.dbus_mock.mock_calls, [
|
||||
('', (), {}),
|
||||
('().get', ('org.qubesos.PolicyAgent',
|
||||
'/org/qubesos/PolicyAgent'), {}),
|
||||
('().get().ConfirmPolicyCreate', ('source', 'service'), {}),
|
||||
])
|
||||
policy_path = os.path.join(self.policy_dir.name, 'service')
|
||||
self.assertFalse(os.path.exists(policy_path))
|
@ -1,409 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# The Qubes OS Project, https://www.qubes-os.org/
|
||||
#
|
||||
# Copyright (C) 2017 boring-stuff <boring-stuff@users.noreply.github.com>
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import gi # isort:skip
|
||||
gi.require_version('Gtk', '3.0') # isort:skip
|
||||
from gi.repository import Gtk # isort:skip pylint:
|
||||
|
||||
from qubes.tests import skipUnlessEnv
|
||||
|
||||
from qubespolicy.gtkhelpers import VMListModeler, GtkOneTimerHelper, \
|
||||
FocusStealingHelper
|
||||
|
||||
mock_domains_info = {
|
||||
'dom0': {'icon': 'black', 'type': 'AdminVM'},
|
||||
'test-red1': {'icon': 'red', 'type': 'AppVM'},
|
||||
'test-red2': {'icon': 'red', 'type': 'AppVM'},
|
||||
'test-red3': {'icon': 'red', 'type': 'AppVM'},
|
||||
'test-source': {'icon': 'green', 'type': 'AppVM'},
|
||||
'test-target': {'icon': 'orange', 'type': 'AppVM'},
|
||||
'@dispvm:test-disp6': {'icon': 'red', 'type': 'DispVM'},
|
||||
}
|
||||
|
||||
mock_whitelist = ["test-red1", "test-red2", "test-red3",
|
||||
"test-target", "@dispvm:test-disp6"]
|
||||
|
||||
class MockComboEntry:
|
||||
def __init__(self, text):
|
||||
self._text = text
|
||||
|
||||
def get_active_id(self):
|
||||
return self._text
|
||||
|
||||
def get_text(self):
|
||||
return self._text
|
||||
|
||||
|
||||
class GtkTestCase(unittest.TestCase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
unittest.TestCase.__init__(self, *args, **kwargs)
|
||||
self._smallest_wait = 0.01
|
||||
|
||||
def flush_gtk_events(self, wait_seconds=0):
|
||||
start = time.time()
|
||||
iterations = 0
|
||||
remaining_wait = wait_seconds
|
||||
time_length = 0
|
||||
|
||||
if wait_seconds < 0:
|
||||
raise ValueError("Only non-negative intervals are allowed.")
|
||||
|
||||
while remaining_wait >= 0:
|
||||
while Gtk.events_pending():
|
||||
Gtk.main_iteration_do(blocking=False)
|
||||
iterations += 1
|
||||
|
||||
time_length = time.time() - start
|
||||
remaining_wait = wait_seconds - time_length
|
||||
|
||||
if remaining_wait > 0:
|
||||
time.sleep(self._smallest_wait)
|
||||
|
||||
return iterations, time_length
|
||||
|
||||
|
||||
@skipUnlessEnv('DISPLAY')
|
||||
class VMListModelerTest(VMListModeler, unittest.TestCase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
unittest.TestCase.__init__(self, *args, **kwargs)
|
||||
VMListModeler.__init__(self, mock_domains_info)
|
||||
|
||||
def test_entries_gets_loaded(self):
|
||||
self.assertIsNotNone(self._entries)
|
||||
|
||||
def test_valid_qube_name(self):
|
||||
self.apply_model(Gtk.ComboBox(), list(mock_domains_info.keys()))
|
||||
|
||||
for name in ["test-red1", "test-red2", "test-red3",
|
||||
"test-target", "Disposable VM (test-disp6)"]:
|
||||
|
||||
mock = MockComboEntry(name)
|
||||
self.assertEquals(name,
|
||||
self._get_valid_qube_name(mock, mock, mock_whitelist))
|
||||
self.assertEquals(name,
|
||||
self._get_valid_qube_name(None, mock, mock_whitelist))
|
||||
self.assertEquals(name,
|
||||
self._get_valid_qube_name(mock, None, mock_whitelist))
|
||||
self.assertIsNone(
|
||||
self._get_valid_qube_name(None, None, mock_whitelist))
|
||||
|
||||
def test_valid_qube_name_whitelist(self):
|
||||
list_exc = ["@dispvm:test-disp6", "test-red2"]
|
||||
|
||||
whitelist = [name for name in mock_whitelist if name not in list_exc]
|
||||
self.apply_model(Gtk.ComboBox(), whitelist)
|
||||
|
||||
for name in list_exc:
|
||||
mock = MockComboEntry(name)
|
||||
self.assertIsNone(self._get_valid_qube_name(mock, mock, whitelist))
|
||||
self.assertIsNone(self._get_valid_qube_name(None, mock, whitelist))
|
||||
self.assertIsNone(self._get_valid_qube_name(mock, None, whitelist))
|
||||
|
||||
def test_invalid_qube_name(self):
|
||||
self.apply_model(Gtk.ComboBox(), mock_whitelist)
|
||||
|
||||
for name in ["test-nonexistant", None, "", 1]:
|
||||
|
||||
mock = MockComboEntry(name)
|
||||
self.assertIsNone(
|
||||
self._get_valid_qube_name(mock, mock, mock_whitelist))
|
||||
self.assertIsNone(
|
||||
self._get_valid_qube_name(None, mock, mock_whitelist))
|
||||
self.assertIsNone(
|
||||
self._get_valid_qube_name(mock, None, mock_whitelist))
|
||||
|
||||
def test_apply_model(self):
|
||||
new_object = Gtk.ComboBox()
|
||||
self.assertIsNone(new_object.get_model())
|
||||
|
||||
self.apply_model(new_object, mock_whitelist)
|
||||
|
||||
self.assertIsNotNone(new_object.get_model())
|
||||
|
||||
def test_apply_model_with_entry(self):
|
||||
new_object = Gtk.ComboBox.new_with_entry()
|
||||
|
||||
self.assertIsNone(new_object.get_model())
|
||||
|
||||
self.apply_model(new_object, [])
|
||||
|
||||
self.assertIsNotNone(new_object.get_model())
|
||||
|
||||
def test_apply_model_only_combobox(self):
|
||||
invalid_types = [1, "One", u'1', {'1': "one"}, VMListModeler(
|
||||
mock_domains_info)]
|
||||
|
||||
for invalid_type in invalid_types:
|
||||
with self.assertRaises(TypeError):
|
||||
self.apply_model(invalid_type, [])
|
||||
|
||||
def test_apply_model_whitelist(self):
|
||||
combo = Gtk.ComboBox()
|
||||
|
||||
self.apply_model(combo, list(mock_domains_info.keys()))
|
||||
self.assertEquals(7, len(combo.get_model()))
|
||||
|
||||
names = [entry['api_name'] for entry in self._entries.values()]
|
||||
|
||||
self.apply_model(combo, [names[0]])
|
||||
self.assertEquals(1, len(combo.get_model()))
|
||||
|
||||
self.apply_model(combo, [names[0], names[1]])
|
||||
self.assertEquals(2, len(combo.get_model()))
|
||||
|
||||
def test_apply_icon(self):
|
||||
new_object = Gtk.Entry()
|
||||
|
||||
self.assertIsNone(
|
||||
new_object.get_icon_pixbuf(Gtk.EntryIconPosition.PRIMARY))
|
||||
|
||||
self.apply_icon(new_object, "Disposable VM (test-disp6)")
|
||||
|
||||
self.assertIsNotNone(
|
||||
new_object.get_icon_pixbuf(Gtk.EntryIconPosition.PRIMARY))
|
||||
|
||||
def test_apply_icon_only_entry(self):
|
||||
invalid_types = [1, "One", u'1', {'1': "one"}, Gtk.ComboBox()]
|
||||
|
||||
for invalid_type in invalid_types:
|
||||
with self.assertRaises(TypeError):
|
||||
self.apply_icon(invalid_type, "test-disp6")
|
||||
|
||||
def test_apply_icon_only_existing(self):
|
||||
new_object = Gtk.Entry()
|
||||
|
||||
for name in ["test-red1", "test-red2", "test-red3",
|
||||
"test-target", "Disposable VM (test-disp6)"]:
|
||||
self.apply_icon(new_object, name)
|
||||
|
||||
for name in ["test-nonexistant", None, "", 1]:
|
||||
with self.assertRaises(ValueError):
|
||||
self.apply_icon(new_object, name)
|
||||
|
||||
|
||||
class GtkOneTimerHelperTest(GtkOneTimerHelper, GtkTestCase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
GtkTestCase.__init__(self, *args, **kwargs)
|
||||
|
||||
self._test_time = 0.1
|
||||
|
||||
GtkOneTimerHelper.__init__(self, self._test_time)
|
||||
self._run_timers = []
|
||||
|
||||
def _timer_run(self, timer_id):
|
||||
self._run_timers.append(timer_id)
|
||||
|
||||
def test_nothing_runs_automatically(self):
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertEquals([], self._run_timers)
|
||||
self.assertEquals(0, self._current_timer_id)
|
||||
self.assertFalse(self._timer_has_completed())
|
||||
|
||||
def test_schedule_one_task(self):
|
||||
self._timer_schedule()
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertEquals([1], self._run_timers)
|
||||
self.assertEquals(1, self._current_timer_id)
|
||||
self.assertTrue(self._timer_has_completed())
|
||||
|
||||
def test_invalidate_completed(self):
|
||||
self._timer_schedule()
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertEquals([1], self._run_timers)
|
||||
self.assertEquals(1, self._current_timer_id)
|
||||
|
||||
self.assertTrue(self._timer_has_completed())
|
||||
self._invalidate_timer_completed()
|
||||
self.assertFalse(self._timer_has_completed())
|
||||
|
||||
def test_schedule_and_cancel_one_task(self):
|
||||
self._timer_schedule()
|
||||
self._invalidate_current_timer()
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertEquals([], self._run_timers)
|
||||
self.assertEquals(2, self._current_timer_id)
|
||||
self.assertFalse(self._timer_has_completed())
|
||||
|
||||
def test_two_tasks(self):
|
||||
self._timer_schedule()
|
||||
self.flush_gtk_events(self._test_time/4)
|
||||
self._timer_schedule()
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertEquals([2], self._run_timers)
|
||||
self.assertEquals(2, self._current_timer_id)
|
||||
self.assertTrue(self._timer_has_completed())
|
||||
|
||||
def test_more_tasks(self):
|
||||
num = 0
|
||||
for num in range(1, 10):
|
||||
self._timer_schedule()
|
||||
self.flush_gtk_events(self._test_time/4)
|
||||
self.flush_gtk_events(self._test_time*1.75)
|
||||
self.assertEquals([num], self._run_timers)
|
||||
self.assertEquals(num, self._current_timer_id)
|
||||
self.assertTrue(self._timer_has_completed())
|
||||
|
||||
def test_more_tasks_cancel(self):
|
||||
num = 0
|
||||
for num in range(1, 10):
|
||||
self._timer_schedule()
|
||||
self.flush_gtk_events(self._test_time/4)
|
||||
self._invalidate_current_timer()
|
||||
self.flush_gtk_events(int(self._test_time*1.75))
|
||||
self.assertEquals([], self._run_timers)
|
||||
self.assertEquals(num+1, self._current_timer_id)
|
||||
self.assertFalse(self._timer_has_completed())
|
||||
|
||||
def test_subsequent_tasks(self):
|
||||
self._timer_schedule() # 1
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertEquals([1], self._run_timers)
|
||||
self.assertEquals(1, self._current_timer_id)
|
||||
self.assertTrue(self._timer_has_completed())
|
||||
|
||||
self._timer_schedule() # 2
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertEquals([1, 2], self._run_timers)
|
||||
self.assertEquals(2, self._current_timer_id)
|
||||
self.assertTrue(self._timer_has_completed())
|
||||
|
||||
self._invalidate_timer_completed()
|
||||
self._timer_schedule() # 3
|
||||
self._invalidate_current_timer() # 4
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertEquals([1, 2], self._run_timers)
|
||||
self.assertEquals(4, self._current_timer_id)
|
||||
self.assertFalse(self._timer_has_completed())
|
||||
|
||||
self._timer_schedule() # 5
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertEquals([1, 2, 5], self._run_timers)
|
||||
self.assertEquals(5, self._current_timer_id)
|
||||
self.assertTrue(self._timer_has_completed())
|
||||
|
||||
|
||||
class FocusStealingHelperMock(FocusStealingHelper):
|
||||
def simulate_focus(self):
|
||||
self._window_changed_focus(True)
|
||||
|
||||
|
||||
@skipUnlessEnv('DISPLAY')
|
||||
class FocusStealingHelperTest(FocusStealingHelperMock, GtkTestCase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
GtkTestCase.__init__(self, *args, **kwargs)
|
||||
|
||||
self._test_time = 0.1
|
||||
self._test_button = Gtk.Button()
|
||||
self._test_window = Gtk.Window()
|
||||
|
||||
FocusStealingHelperMock.__init__(self, self._test_window,
|
||||
self._test_button, self._test_time)
|
||||
|
||||
def test_nothing_runs_automatically(self):
|
||||
self.assertFalse(self.can_perform_action())
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertFalse(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
def test_nothing_runs_automatically_with_request(self):
|
||||
self.request_sensitivity(True)
|
||||
self.assertFalse(self.can_perform_action())
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertFalse(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
def _simulate_focus(self, focused):
|
||||
self._window_changed_focus(focused)
|
||||
|
||||
def test_focus_with_request(self):
|
||||
self.request_sensitivity(True)
|
||||
self._simulate_focus(True)
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertTrue(self.can_perform_action())
|
||||
self.assertTrue(self._test_button.get_sensitive())
|
||||
|
||||
def test_focus_with_late_request(self):
|
||||
self._simulate_focus(True)
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertTrue(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
self.request_sensitivity(True)
|
||||
self.assertTrue(self._test_button.get_sensitive())
|
||||
|
||||
def test_immediate_defocus(self):
|
||||
self.request_sensitivity(True)
|
||||
self._simulate_focus(True)
|
||||
self._simulate_focus(False)
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertFalse(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
def test_focus_then_unfocus(self):
|
||||
self.request_sensitivity(True)
|
||||
self._simulate_focus(True)
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertTrue(self.can_perform_action())
|
||||
self.assertTrue(self._test_button.get_sensitive())
|
||||
|
||||
self._simulate_focus(False)
|
||||
self.assertFalse(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
def test_focus_cycle(self):
|
||||
self.request_sensitivity(True)
|
||||
|
||||
self._simulate_focus(True)
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertTrue(self.can_perform_action())
|
||||
self.assertTrue(self._test_button.get_sensitive())
|
||||
|
||||
self._simulate_focus(False)
|
||||
self.assertFalse(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
self._simulate_focus(True)
|
||||
self.assertFalse(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertTrue(self.can_perform_action())
|
||||
self.assertTrue(self._test_button.get_sensitive())
|
||||
|
||||
self.request_sensitivity(False)
|
||||
self.assertTrue(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
self._simulate_focus(False)
|
||||
self.assertFalse(self.can_perform_action())
|
||||
|
||||
self._simulate_focus(True)
|
||||
self.assertFalse(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assertTrue(self.can_perform_action())
|
||||
self.assertFalse(self._test_button.get_sensitive())
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,359 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# The Qubes OS Project, https://www.qubes-os.org/
|
||||
#
|
||||
# Copyright (C) 2017 boring-stuff <boring-stuff@users.noreply.github.com>
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from qubespolicy.tests.gtkhelpers import GtkTestCase, FocusStealingHelperMock
|
||||
from qubespolicy.tests.gtkhelpers import mock_domains_info, mock_whitelist
|
||||
|
||||
from qubespolicy.gtkhelpers import VMListModeler
|
||||
from qubespolicy.rpcconfirmation import RPCConfirmationWindow
|
||||
|
||||
|
||||
class MockRPCConfirmationWindow(RPCConfirmationWindow):
|
||||
def _new_vm_list_modeler(self):
|
||||
return VMListModeler(mock_domains_info)
|
||||
|
||||
def _new_focus_stealing_helper(self):
|
||||
return FocusStealingHelperMock(
|
||||
self._rpc_window,
|
||||
self._rpc_ok_button,
|
||||
self._focus_stealing_seconds)
|
||||
|
||||
def __init__(self, source, rpc_operation, whitelist,
|
||||
target=None, focus_stealing_seconds=1):
|
||||
self._focus_stealing_seconds = focus_stealing_seconds
|
||||
|
||||
RPCConfirmationWindow.__init__(
|
||||
self, mock_domains_info, source, rpc_operation, whitelist,
|
||||
target)
|
||||
|
||||
def is_error_visible(self):
|
||||
return self._error_bar.get_visible()
|
||||
|
||||
def get_shown_domains(self):
|
||||
model = self._rpc_combo_box.get_model()
|
||||
model_iter = model.get_iter_first()
|
||||
domains = []
|
||||
|
||||
while model_iter is not None:
|
||||
domain_name = model.get_value(model_iter, 1)
|
||||
|
||||
domains += [domain_name]
|
||||
|
||||
model_iter = model.iter_next(model_iter)
|
||||
|
||||
return domains
|
||||
|
||||
|
||||
class RPCConfirmationWindowTestBase(MockRPCConfirmationWindow, GtkTestCase):
|
||||
def __init__(self, test_method, source_name="test-source",
|
||||
rpc_operation="test.Operation", whitelist=mock_whitelist,
|
||||
target_name=None):
|
||||
GtkTestCase.__init__(self, test_method)
|
||||
self.test_source_name = source_name
|
||||
self.test_rpc_operation = rpc_operation
|
||||
self.test_target_name = target_name
|
||||
|
||||
self._test_time = 0.1
|
||||
|
||||
self.test_called_close = False
|
||||
self.test_called_show = False
|
||||
|
||||
self.test_clicked_ok = False
|
||||
self.test_clicked_cancel = False
|
||||
|
||||
MockRPCConfirmationWindow.__init__(self,
|
||||
self.test_source_name,
|
||||
self.test_rpc_operation,
|
||||
whitelist,
|
||||
self.test_target_name,
|
||||
focus_stealing_seconds=self._test_time)
|
||||
|
||||
def _can_perform_action(self):
|
||||
return True
|
||||
|
||||
def _close(self):
|
||||
self.test_called_close = True
|
||||
|
||||
def _show(self):
|
||||
self.test_called_show = True
|
||||
|
||||
def _clicked_ok(self, button):
|
||||
MockRPCConfirmationWindow._clicked_ok(self, button)
|
||||
self.test_clicked_ok = True
|
||||
|
||||
def _clicked_cancel(self, button):
|
||||
MockRPCConfirmationWindow._clicked_cancel(self, button)
|
||||
self.test_clicked_cancel = True
|
||||
|
||||
def test_has_linked_the_fields(self):
|
||||
self.assertIsNotNone(self._rpc_window)
|
||||
self.assertIsNotNone(self._rpc_ok_button)
|
||||
self.assertIsNotNone(self._rpc_cancel_button)
|
||||
self.assertIsNotNone(self._rpc_label)
|
||||
self.assertIsNotNone(self._source_entry)
|
||||
self.assertIsNotNone(self._rpc_combo_box)
|
||||
self.assertIsNotNone(self._error_bar)
|
||||
self.assertIsNotNone(self._error_message)
|
||||
|
||||
def test_is_showing_source(self):
|
||||
self.assertTrue(self.test_source_name in self._source_entry.get_text())
|
||||
|
||||
def test_is_showing_operation(self):
|
||||
self.assertTrue(self.test_rpc_operation in self._rpc_label.get_text())
|
||||
|
||||
def test_escape_and_format_rpc_text(self):
|
||||
self.assertEquals("qubes.<b>Test</b>",
|
||||
self._escape_and_format_rpc_text("qubes.Test"))
|
||||
self.assertEquals("custom.<b>Domain</b>",
|
||||
self._escape_and_format_rpc_text("custom.Domain"))
|
||||
self.assertEquals("<b>nodomain</b>",
|
||||
self._escape_and_format_rpc_text("nodomain"))
|
||||
self.assertEquals("domain.<b>Sub.Operation</b>",
|
||||
self._escape_and_format_rpc_text("domain.Sub.Operation"))
|
||||
self.assertEquals("<b></b>",
|
||||
self._escape_and_format_rpc_text(""))
|
||||
self.assertEquals("<b>.</b>",
|
||||
self._escape_and_format_rpc_text("."))
|
||||
self.assertEquals("inject.<b><script></b>",
|
||||
self._escape_and_format_rpc_text("inject.<script>"))
|
||||
self.assertEquals("<script>.<b>inject</b>",
|
||||
self._escape_and_format_rpc_text("<script>.inject"))
|
||||
|
||||
def test_lifecycle_open_select_ok(self):
|
||||
self._lifecycle_start(select_target=True)
|
||||
self._lifecycle_click(click_type="ok")
|
||||
|
||||
def test_lifecycle_open_select_cancel(self):
|
||||
self._lifecycle_start(select_target=True)
|
||||
self._lifecycle_click(click_type="cancel")
|
||||
|
||||
def test_lifecycle_open_select_exit(self):
|
||||
self._lifecycle_start(select_target=True)
|
||||
self._lifecycle_click(click_type="exit")
|
||||
|
||||
def test_lifecycle_open_cancel(self):
|
||||
self._lifecycle_start(select_target=False)
|
||||
self._lifecycle_click(click_type="cancel")
|
||||
|
||||
def test_lifecycle_open_exit(self):
|
||||
self._lifecycle_start(select_target=False)
|
||||
self._lifecycle_click(click_type="exit")
|
||||
|
||||
def _lifecycle_click(self, click_type):
|
||||
if click_type == "ok":
|
||||
self._rpc_ok_button.clicked()
|
||||
|
||||
self.assertTrue(self.test_clicked_ok)
|
||||
self.assertFalse(self.test_clicked_cancel)
|
||||
self.assertTrue(self._confirmed)
|
||||
self.assertIsNotNone(self._target_name)
|
||||
elif click_type == "cancel":
|
||||
self._rpc_cancel_button.clicked()
|
||||
|
||||
self.assertFalse(self.test_clicked_ok)
|
||||
self.assertTrue(self.test_clicked_cancel)
|
||||
self.assertFalse(self._confirmed)
|
||||
elif click_type == "exit":
|
||||
self._close()
|
||||
|
||||
self.assertFalse(self.test_clicked_ok)
|
||||
self.assertFalse(self.test_clicked_cancel)
|
||||
self.assertIsNone(self._confirmed)
|
||||
|
||||
self.assertTrue(self.test_called_close)
|
||||
|
||||
|
||||
def _lifecycle_start(self, select_target):
|
||||
self.assertFalse(self.test_called_close)
|
||||
self.assertFalse(self.test_called_show)
|
||||
|
||||
self.assert_initial_state(False)
|
||||
self.assertTrue(isinstance(self._focus_helper, FocusStealingHelperMock))
|
||||
|
||||
# Need the following because of pylint's complaints
|
||||
if isinstance(self._focus_helper, FocusStealingHelperMock):
|
||||
FocusStealingHelperMock.simulate_focus(self._focus_helper)
|
||||
|
||||
self.flush_gtk_events(self._test_time*2)
|
||||
self.assert_initial_state(True)
|
||||
|
||||
try:
|
||||
# We expect the call to exit immediately, since no window is opened
|
||||
self.confirm_rpc()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.assertFalse(self.test_called_close)
|
||||
self.assertTrue(self.test_called_show)
|
||||
|
||||
self.assert_initial_state(True)
|
||||
|
||||
if select_target:
|
||||
self._rpc_combo_box.set_active(1)
|
||||
|
||||
self.assertTrue(self._rpc_ok_button.get_sensitive())
|
||||
|
||||
self.assertIsNotNone(self._target_name)
|
||||
|
||||
self.assertFalse(self.test_called_close)
|
||||
self.assertTrue(self.test_called_show)
|
||||
self.assertFalse(self.test_clicked_ok)
|
||||
self.assertFalse(self.test_clicked_cancel)
|
||||
self.assertFalse(self._confirmed)
|
||||
|
||||
def assert_initial_state(self, after_focus_timer):
|
||||
self.assertIsNone(self._target_name)
|
||||
self.assertFalse(self.test_clicked_ok)
|
||||
self.assertFalse(self.test_clicked_cancel)
|
||||
self.assertFalse(self._confirmed)
|
||||
self.assertFalse(self._rpc_ok_button.get_sensitive())
|
||||
self.assertFalse(self._error_bar.get_visible())
|
||||
|
||||
if after_focus_timer:
|
||||
self.assertTrue(self._focus_helper.can_perform_action())
|
||||
else:
|
||||
self.assertFalse(self._focus_helper.can_perform_action())
|
||||
|
||||
|
||||
class RPCConfirmationWindowTestWithTarget(RPCConfirmationWindowTestBase):
|
||||
def __init__(self, test_method):
|
||||
RPCConfirmationWindowTestBase.__init__(self, test_method,
|
||||
source_name="test-source", rpc_operation="test.Operation",
|
||||
target_name="test-target")
|
||||
|
||||
def test_lifecycle_open_ok(self):
|
||||
self._lifecycle_start(select_target=False)
|
||||
self._lifecycle_click(click_type="ok")
|
||||
|
||||
def assert_initial_state(self, after_focus_timer):
|
||||
self.assertIsNotNone(self._target_name)
|
||||
self.assertFalse(self.test_clicked_ok)
|
||||
self.assertFalse(self.test_clicked_cancel)
|
||||
self.assertFalse(self._confirmed)
|
||||
if after_focus_timer:
|
||||
self.assertTrue(self._rpc_ok_button.get_sensitive())
|
||||
self.assertTrue(self._focus_helper.can_perform_action())
|
||||
self.assertEqual(self._target_name, 'test-target')
|
||||
else:
|
||||
self.assertFalse(self._rpc_ok_button.get_sensitive())
|
||||
self.assertFalse(self._focus_helper.can_perform_action())
|
||||
|
||||
def _lifecycle_click(self, click_type):
|
||||
RPCConfirmationWindowTestBase._lifecycle_click(self, click_type)
|
||||
self.assertIsNotNone(self._target_name)
|
||||
|
||||
|
||||
class RPCConfirmationWindowTestWithDispVMTarget(RPCConfirmationWindowTestBase):
|
||||
def __init__(self, test_method):
|
||||
RPCConfirmationWindowTestBase.__init__(self, test_method,
|
||||
source_name="test-source", rpc_operation="test.Operation",
|
||||
target_name="@dispvm:test-disp6")
|
||||
|
||||
def test_lifecycle_open_ok(self):
|
||||
self._lifecycle_start(select_target=False)
|
||||
self._lifecycle_click(click_type="ok")
|
||||
|
||||
def assert_initial_state(self, after_focus_timer):
|
||||
self.assertIsNotNone(self._target_name)
|
||||
self.assertFalse(self.test_clicked_ok)
|
||||
self.assertFalse(self.test_clicked_cancel)
|
||||
self.assertFalse(self._confirmed)
|
||||
if after_focus_timer:
|
||||
self.assertTrue(self._rpc_ok_button.get_sensitive())
|
||||
self.assertTrue(self._focus_helper.can_perform_action())
|
||||
self.assertEqual(self._target_name, '@dispvm:test-disp6')
|
||||
else:
|
||||
self.assertFalse(self._rpc_ok_button.get_sensitive())
|
||||
self.assertFalse(self._focus_helper.can_perform_action())
|
||||
|
||||
|
||||
class RPCConfirmationWindowTestWithTargetInvalid(unittest.TestCase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
unittest.TestCase.__init__(self, *args, **kwargs)
|
||||
|
||||
def test_unknown(self):
|
||||
self.assert_raises_error(True, "test-source", "test-wrong-target")
|
||||
|
||||
def test_empty(self):
|
||||
self.assert_raises_error(True, "test-source", "")
|
||||
|
||||
def test_equals_source(self):
|
||||
self.assert_raises_error(True, "test-source", "test-source")
|
||||
|
||||
def assert_raises_error(self, expect, source, target):
|
||||
rpcWindow = MockRPCConfirmationWindow(source, "test.Operation",
|
||||
mock_whitelist, target=target)
|
||||
self.assertEquals(expect, rpcWindow.is_error_visible())
|
||||
|
||||
|
||||
class RPCConfirmationWindowTestWhitelist(unittest.TestCase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
unittest.TestCase.__init__(self, *args, **kwargs)
|
||||
|
||||
def test_no_domains(self):
|
||||
self._assert_whitelist([], [])
|
||||
|
||||
def test_all_red_domains(self):
|
||||
self._assert_whitelist(["test-red1", "test-red2", "test-red3"],
|
||||
["test-red1", "test-red2", "test-red3"])
|
||||
|
||||
def test_all_red_domains_plus_nonexistent(self):
|
||||
self._assert_whitelist(
|
||||
["test-red1", "test-red2", "test-red3",
|
||||
"test-blue1", "test-blue2", "test-blue3"],
|
||||
["test-red1", "test-red2", "test-red3"])
|
||||
|
||||
def test_all_allowed_domains(self):
|
||||
self._assert_whitelist(
|
||||
["test-red1", "test-red2", "test-red3",
|
||||
"test-target", "@dispvm:test-disp6", "test-source", "dom0"],
|
||||
["test-red1", "test-red2", "test-red3",
|
||||
"test-target", "Disposable VM (test-disp6)", "test-source",
|
||||
"dom0"])
|
||||
|
||||
def _assert_whitelist(self, whitelist, expected):
|
||||
rpcWindow = MockRPCConfirmationWindow(
|
||||
"test-source", "test.Operation", whitelist)
|
||||
|
||||
domains = rpcWindow.get_shown_domains()
|
||||
|
||||
self.assertCountEqual(domains, expected)
|
||||
|
||||
if __name__ == '__main__':
|
||||
test = False
|
||||
window = False
|
||||
|
||||
if len(sys.argv) == 1 or sys.argv[1] == '-t':
|
||||
test = True
|
||||
elif sys.argv[1] == '-w':
|
||||
window = True
|
||||
else:
|
||||
print("Usage: " + __file__ + " [-t|-w]")
|
||||
|
||||
if window:
|
||||
print(MockRPCConfirmationWindow("test-source",
|
||||
"qubes.Filecopy",
|
||||
mock_whitelist,
|
||||
"test-red1").confirm_rpc())
|
||||
elif test:
|
||||
unittest.main(argv=[sys.argv[0]])
|
@ -1,57 +0,0 @@
|
||||
# -*- encoding: utf8 -*-
|
||||
#
|
||||
# The Qubes OS Project, http://www.qubes-os.org
|
||||
#
|
||||
# Copyright (C) 2017 boring-stuff <boring-stuff@users.noreply.github.com>
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library 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
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
def _sanitize_char(input_char, extra_allowed_characters):
|
||||
input_char_ord = ord(input_char)
|
||||
|
||||
if (ord('a') <= input_char_ord <= ord('z')) \
|
||||
or (ord('A') <= input_char_ord <= ord('Z')) \
|
||||
or (ord('0') <= input_char_ord <= ord('9')) \
|
||||
or (input_char in ['@', '_', '-', '.']) \
|
||||
or (input_char in extra_allowed_characters):
|
||||
result = input_char
|
||||
else:
|
||||
result = '_'
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# This function needs to be synchronized with qrexec-daemon.c's sanitize_name()
|
||||
# from the qubes-core-admin-linux repository.
|
||||
#
|
||||
# See https://github.com/QubesOS/qubes-core-admin-linux/blob/
|
||||
# 4f0878ccbf8a95f8264b54d2b6f4dc433ca0793a/qrexec/qrexec-daemon.c#L627-L646
|
||||
#
|
||||
def _sanitize_name(input_string, extra_allowed_characters, assert_sanitized):
|
||||
result = ''.join(_sanitize_char(character, extra_allowed_characters)
|
||||
for character in input_string)
|
||||
|
||||
if assert_sanitized:
|
||||
assert input_string == result, \
|
||||
'Input string was expected to be sanitized, but was not.'
|
||||
return result
|
||||
|
||||
|
||||
def sanitize_domain_name(input_string, assert_sanitized=False):
|
||||
return _sanitize_name(input_string, {}, assert_sanitized)
|
||||
|
||||
|
||||
def sanitize_service_name(input_string, assert_sanitized=False):
|
||||
return _sanitize_name(input_string, {'+'}, assert_sanitized)
|
@ -67,7 +67,6 @@ Requires: python3
|
||||
Requires: python3-docutils
|
||||
Requires: python3-jinja2
|
||||
Requires: python3-lxml
|
||||
Requires: python3-pydbus
|
||||
Requires: python3-qubesdb
|
||||
Requires: python3-setuptools
|
||||
Requires: python3-PyYAML
|
||||
@ -76,6 +75,7 @@ Requires: libvirt-python3
|
||||
|
||||
Requires: pciutils
|
||||
Requires: qubes-core-dom0-linux >= 4.0.11
|
||||
Requires: qubes-core-qrexec-dom0
|
||||
Requires: qubes-db-dom0
|
||||
# TODO: R: qubes-gui-dom0 >= 2.1.11
|
||||
Conflicts: qubes-gui-dom0 < 1.1.13
|
||||
@ -190,18 +190,13 @@ fi
|
||||
%files
|
||||
%defattr(-,root,root,-)
|
||||
%config(noreplace) %attr(0664,root,qubes) %{_sysconfdir}/qubes/qmemman.conf
|
||||
%config(noreplace) /etc/dbus-1/system.d/org.qubesos.PolicyAgent.conf
|
||||
%config(noreplace) /etc/logrotate.d/qubes
|
||||
%attr(770,root,qubes) %dir /etc/qubes/backup
|
||||
/usr/bin/qvm-*
|
||||
/usr/bin/qubes-*
|
||||
/usr/bin/qmemmand
|
||||
/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*
|
||||
%{_mandir}/man1/qvm-*.1*
|
||||
|
||||
@ -360,30 +355,6 @@ fi
|
||||
%{python3_sitelib}/qubes/qmemman/algo.py
|
||||
%{python3_sitelib}/qubes/qmemman/client.py
|
||||
|
||||
%dir %{python3_sitelib}/qubespolicy
|
||||
%dir %{python3_sitelib}/qubespolicy/__pycache__
|
||||
%{python3_sitelib}/qubespolicy/__pycache__/*
|
||||
%{python3_sitelib}/qubespolicy/__init__.py
|
||||
%{python3_sitelib}/qubespolicy/cli.py
|
||||
%{python3_sitelib}/qubespolicy/agent.py
|
||||
%{python3_sitelib}/qubespolicy/gtkhelpers.py
|
||||
%{python3_sitelib}/qubespolicy/policycreateconfirmation.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__
|
||||
%{python3_sitelib}/qubespolicy/tests/__pycache__/*
|
||||
%{python3_sitelib}/qubespolicy/tests/__init__.py
|
||||
%{python3_sitelib}/qubespolicy/tests/cli.py
|
||||
%{python3_sitelib}/qubespolicy/tests/gtkhelpers.py
|
||||
%{python3_sitelib}/qubespolicy/tests/rpcconfirmation.py
|
||||
|
||||
%dir %{python3_sitelib}/qubespolicy/glade
|
||||
%{python3_sitelib}/qubespolicy/glade/PolicyCreateConfirmationWindow.glade
|
||||
%{python3_sitelib}/qubespolicy/glade/RPCConfirmationWindow.glade
|
||||
|
||||
/usr/lib/qubes/cleanup-dispvms
|
||||
/usr/lib/qubes/fix-dir-perms.sh
|
||||
/usr/lib/qubes/startup-misc.sh
|
||||
@ -434,10 +405,8 @@ fi
|
||||
/etc/qubes-rpc/qubes.GetRandomizedTime
|
||||
/etc/qubes-rpc/qubes.NotifyTools
|
||||
/etc/qubes-rpc/qubes.NotifyUpdates
|
||||
/etc/qubes-rpc/policy.RegisterArgument
|
||||
%attr(2770,root,qubes) %dir /var/log/qubes
|
||||
%attr(0770,root,qubes) %dir /var/run/qubes
|
||||
/etc/xdg/autostart/qrexec-policy-agent.desktop
|
||||
|
||||
/usr/share/doc/qubes/relaxng/*.rng
|
||||
|
||||
|
6
setup.py
6
setup.py
@ -10,9 +10,6 @@ import setuptools.command.install
|
||||
# don't import: import * is unreliable and there is no need, since this is
|
||||
# compile time and we have source files
|
||||
def get_console_scripts():
|
||||
yield 'qrexec-policy', 'qubespolicy.cli'
|
||||
yield 'qrexec-policy-agent', 'qubespolicy.agent'
|
||||
yield 'qrexec-policy-graph', 'qubespolicy.graph'
|
||||
for filename in os.listdir('./qubes/tools'):
|
||||
basename, ext = os.path.splitext(os.path.basename(filename))
|
||||
if basename == '__init__' or ext != '.py':
|
||||
@ -51,9 +48,6 @@ if __name__ == '__main__':
|
||||
license='GPL2+',
|
||||
url='https://www.qubes-os.org/',
|
||||
packages=setuptools.find_packages(exclude=('core*', 'tests')),
|
||||
package_data = {
|
||||
'qubespolicy': ['glade/*.glade'],
|
||||
},
|
||||
cmdclass={
|
||||
'install': CustomInstall,
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user