2017-02-22 11:14:50 +00:00
|
|
|
#!/usr/bin/python3
|
|
|
|
|
2016-08-16 01:09:16 +00:00
|
|
|
import argparse
|
2013-03-20 05:24:17 +00:00
|
|
|
import sys
|
|
|
|
import os
|
|
|
|
import os.path
|
|
|
|
import subprocess
|
2016-02-11 01:16:01 +00:00
|
|
|
import qubes
|
2015-10-10 23:52:40 +00:00
|
|
|
import libvirt
|
2013-03-20 05:24:17 +00:00
|
|
|
from optparse import OptionParser
|
|
|
|
import fcntl
|
2016-08-16 00:57:17 +00:00
|
|
|
from PyQt4.QtGui import QApplication, QMessageBox
|
2016-02-11 01:16:01 +00:00
|
|
|
|
2016-08-16 00:57:17 +00:00
|
|
|
POLICY_FILE_DIR = "/etc/qubes-rpc/policy"
|
2013-03-20 05:24:17 +00:00
|
|
|
# XXX: Backward compatibility, to be removed soon
|
2016-08-16 00:57:17 +00:00
|
|
|
DEPRECATED_POLICY_FILE_DIR = "/etc/qubes_rpc/policy"
|
|
|
|
QREXEC_CLIENT = "/usr/lib/qubes/qrexec-client"
|
|
|
|
QUBES_RPC_MULTIPLEXER_PATH = "/usr/lib/qubes/qubes-rpc-multiplexer"
|
|
|
|
|
2013-03-20 05:24:17 +00:00
|
|
|
|
|
|
|
class UserChoice:
|
2016-08-16 00:57:17 +00:00
|
|
|
ALLOW = 0
|
|
|
|
DENY = 1
|
|
|
|
ALWAYS_ALLOW = 2
|
|
|
|
|
2013-03-20 05:24:17 +00:00
|
|
|
|
2016-02-11 01:16:01 +00:00
|
|
|
def prepare_app():
|
|
|
|
app = QApplication(sys.argv)
|
|
|
|
app.setOrganizationName("The Qubes Project")
|
|
|
|
app.setOrganizationDomain("http://qubes-os.org")
|
|
|
|
app.setApplicationName("Qubes")
|
|
|
|
return app
|
|
|
|
|
2016-08-16 00:57:17 +00:00
|
|
|
|
2016-02-11 01:16:01 +00:00
|
|
|
def ask(text, title="Question", yestoall=False):
|
2016-08-17 15:54:43 +00:00
|
|
|
qtapp = prepare_app()
|
2016-02-11 01:16:01 +00:00
|
|
|
|
|
|
|
buttons = QMessageBox.Yes | QMessageBox.No
|
|
|
|
if yestoall:
|
|
|
|
buttons |= QMessageBox.YesToAll
|
|
|
|
|
2016-08-16 00:57:17 +00:00
|
|
|
reply = QMessageBox.question(None, title, text, buttons,
|
|
|
|
defaultButton=QMessageBox.Yes)
|
2016-02-11 01:16:01 +00:00
|
|
|
if reply == QMessageBox.Yes:
|
|
|
|
return 0
|
|
|
|
elif reply == QMessageBox.No:
|
|
|
|
return 1
|
|
|
|
elif reply == QMessageBox.YesToAll:
|
|
|
|
return 2
|
|
|
|
else:
|
2016-08-16 00:57:17 +00:00
|
|
|
# ?!
|
2016-02-11 01:16:01 +00:00
|
|
|
return 127
|
|
|
|
|
2016-08-16 00:57:17 +00:00
|
|
|
|
2013-03-20 05:24:17 +00:00
|
|
|
def line_to_dict(line):
|
2016-08-16 00:57:17 +00:00
|
|
|
tokens = line.split()
|
2013-03-20 05:24:17 +00:00
|
|
|
if len(tokens) < 3:
|
|
|
|
return None
|
|
|
|
|
|
|
|
if tokens[0][0] == '#':
|
2013-10-27 18:22:47 +00:00
|
|
|
return None
|
2013-03-20 05:24:17 +00:00
|
|
|
|
2016-08-16 00:57:17 +00:00
|
|
|
policy_dict = {
|
|
|
|
'source': tokens[0],
|
|
|
|
'dest': tokens[1],
|
|
|
|
'full-action': tokens[2],
|
|
|
|
}
|
2013-10-27 18:22:47 +00:00
|
|
|
|
2016-08-16 00:57:17 +00:00
|
|
|
action_list = tokens[2].split(',')
|
|
|
|
policy_dict['action'] = action_list.pop(0)
|
2013-10-27 18:22:47 +00:00
|
|
|
|
2016-08-16 00:57:17 +00:00
|
|
|
for action_iter in action_list:
|
|
|
|
paramval = action_iter.split("=")
|
|
|
|
policy_dict["action." + paramval[0]] = paramval[1]
|
2013-10-27 18:22:47 +00:00
|
|
|
|
2013-12-27 19:29:50 +00:00
|
|
|
# Warn if we're ignoring extra data after a space, such as:
|
|
|
|
# vm1 vm2 allow, user=foo
|
|
|
|
if len(tokens) > 3:
|
2017-02-22 11:14:50 +00:00
|
|
|
print("Trailing data ignored in %s" % line, file=sys.stderr)
|
2013-12-27 19:29:50 +00:00
|
|
|
|
2016-08-16 00:57:17 +00:00
|
|
|
return policy_dict
|
2013-03-20 05:24:17 +00:00
|
|
|
|
|
|
|
|
2013-12-27 04:38:16 +00:00
|
|
|
def read_policy_file(service_name):
|
2016-03-27 02:27:37 +00:00
|
|
|
policy_file = os.path.join(POLICY_FILE_DIR, service_name)
|
2013-03-20 05:24:17 +00:00
|
|
|
if not os.path.isfile(policy_file):
|
2016-03-27 02:27:37 +00:00
|
|
|
# fallback to policy without specific argument set (if any)
|
|
|
|
policy_file = os.path.join(POLICY_FILE_DIR, service_name.split("+")[0])
|
|
|
|
if not os.path.isfile(policy_file):
|
|
|
|
policy_file = os.path.join(DEPRECATED_POLICY_FILE_DIR, service_name)
|
2013-03-20 05:24:17 +00:00
|
|
|
if not os.path.isfile(policy_file):
|
|
|
|
return None
|
2017-02-22 11:14:50 +00:00
|
|
|
print(sys.stderr,
|
|
|
|
"RPC service '%s' uses deprecated policy location, "
|
|
|
|
"please move to %s" % (service_name, POLICY_FILE_DIR),
|
|
|
|
file=sys.stderr)
|
2016-08-16 00:57:17 +00:00
|
|
|
policy_list = list()
|
2013-03-20 05:24:17 +00:00
|
|
|
f = open(policy_file)
|
|
|
|
fcntl.flock(f, fcntl.LOCK_SH)
|
2016-08-16 00:57:17 +00:00
|
|
|
for policy_iter in f.readlines():
|
|
|
|
policy_item = line_to_dict(policy_iter)
|
|
|
|
if policy_item is not None:
|
|
|
|
policy_list.append(policy_item)
|
2013-03-20 05:24:17 +00:00
|
|
|
f.close()
|
|
|
|
return policy_list
|
|
|
|
|
2016-08-16 00:57:17 +00:00
|
|
|
|
2016-08-17 00:37:37 +00:00
|
|
|
def is_match(config_term, item):
|
|
|
|
if config_term == '$anyvm':
|
|
|
|
# match anything but dom0
|
|
|
|
return item != "dom0"
|
|
|
|
else:
|
|
|
|
if isinstance(item, qubes.vm.qubesvm.QubesVM):
|
|
|
|
return config_term == item.name
|
|
|
|
else:
|
|
|
|
return config_term == item
|
2016-08-16 00:57:17 +00:00
|
|
|
|
2013-03-20 05:24:17 +00:00
|
|
|
|
|
|
|
def get_default_policy():
|
2016-08-16 00:57:17 +00:00
|
|
|
return {"action": "deny"}
|
2013-10-27 18:22:47 +00:00
|
|
|
|
2013-03-20 05:24:17 +00:00
|
|
|
|
2016-08-17 00:37:37 +00:00
|
|
|
def find_policy(policy, source_domain, target, target_domain=None):
|
2016-08-16 00:57:17 +00:00
|
|
|
for policy_iter in policy:
|
2016-08-17 00:37:37 +00:00
|
|
|
if not is_match(policy_iter["source"], source_domain):
|
2013-03-20 05:24:17 +00:00
|
|
|
continue
|
2016-08-17 00:37:37 +00:00
|
|
|
if not is_match(policy_iter["dest"], target_domain or target):
|
2013-03-20 05:24:17 +00:00
|
|
|
continue
|
2016-08-16 00:57:17 +00:00
|
|
|
return policy_iter
|
2013-03-20 05:24:17 +00:00
|
|
|
return get_default_policy()
|
|
|
|
|
2016-08-16 00:57:17 +00:00
|
|
|
|
2016-08-17 00:37:37 +00:00
|
|
|
def validate_target(app, target):
|
2016-09-05 12:31:50 +00:00
|
|
|
"""Validate target name. Throw KeYError for invalid name.
|
|
|
|
|
|
|
|
:param app: Qubes app object
|
|
|
|
:param target: target name to validate
|
|
|
|
:return: QubesVM object or None (in case of spacial target)
|
|
|
|
"""
|
2013-08-14 22:01:56 +00:00
|
|
|
# special targets
|
2016-08-17 00:37:37 +00:00
|
|
|
if target == '$dispvm' or target.startswith('$dispvm:'):
|
2016-09-05 12:31:50 +00:00
|
|
|
return None
|
2013-08-14 22:01:56 +00:00
|
|
|
|
2016-02-11 01:16:01 +00:00
|
|
|
return app.domains[target]
|
2013-10-27 18:22:47 +00:00
|
|
|
|
2016-08-16 00:57:17 +00:00
|
|
|
|
2015-10-10 23:52:40 +00:00
|
|
|
def spawn_target_if_necessary(vm):
|
|
|
|
if vm.is_running():
|
2013-03-20 05:24:17 +00:00
|
|
|
return
|
2016-08-17 00:37:37 +00:00
|
|
|
# TODO: tray notification
|
|
|
|
vm.start()
|
2016-08-16 00:57:17 +00:00
|
|
|
|
2015-10-10 23:52:40 +00:00
|
|
|
def do_execute(domain, target, user, service_name, process_ident, vm=None):
|
2016-08-17 00:39:16 +00:00
|
|
|
dispvm = False
|
2013-12-27 04:38:16 +00:00
|
|
|
if target == "$dispvm":
|
2016-08-17 00:39:16 +00:00
|
|
|
if domain.default_dispvm is None:
|
2017-02-22 11:14:50 +00:00
|
|
|
print("No default DispVM set, aborting!", file=sys.stderr)
|
2016-08-17 00:39:16 +00:00
|
|
|
exit(1)
|
|
|
|
target = "$dispvm:" + domain.default_dispvm.name
|
|
|
|
if target.startswith("$dispvm:"):
|
|
|
|
dispvm_tpl_name = target[len("$dispvm:"):]
|
|
|
|
vm = qubes.vm.dispvm.DispVM.from_appvm(dispvm_tpl_name)
|
|
|
|
dispvm = True
|
|
|
|
# at this point we should also have some VM *object*
|
|
|
|
assert vm is not None
|
|
|
|
try:
|
|
|
|
spawn_target_if_necessary(vm)
|
2013-12-27 04:38:16 +00:00
|
|
|
if target == "dom0":
|
2016-08-17 00:37:37 +00:00
|
|
|
cmd = QUBES_RPC_MULTIPLEXER_PATH + " " + service_name + " " + \
|
|
|
|
domain.name
|
2013-12-27 04:38:16 +00:00
|
|
|
else:
|
2016-08-17 00:37:37 +00:00
|
|
|
cmd = user + ":QUBESRPC " + service_name + " " + domain.name
|
2016-08-17 00:39:16 +00:00
|
|
|
qrexec_opts = ["-d", vm.name, "-c", process_ident]
|
|
|
|
if dispvm:
|
|
|
|
# wait for qrexec connection end
|
|
|
|
qrexec_opts.append("-W")
|
|
|
|
subprocess.call([QREXEC_CLIENT] + qrexec_opts + [cmd])
|
|
|
|
finally:
|
|
|
|
if dispvm:
|
|
|
|
vm.cleanup()
|
2013-12-27 04:38:16 +00:00
|
|
|
|
|
|
|
def confirm_execution(domain, target, service_name):
|
2016-08-16 00:57:17 +00:00
|
|
|
text = "Do you allow domain \"" + domain + "\" to execute " + service_name
|
|
|
|
text += " operation on the domain \"" + target + "\"?<br>"
|
|
|
|
text += " \"Yes to All\" option will automatically allow this " \
|
|
|
|
"operation in the future."
|
2016-08-17 15:54:43 +00:00
|
|
|
return ask(text, yestoall=True)
|
2013-03-20 05:24:17 +00:00
|
|
|
|
2016-08-16 00:57:17 +00:00
|
|
|
|
2013-12-27 04:38:16 +00:00
|
|
|
def add_always_allow(domain, target, service_name, options):
|
2016-08-16 00:57:17 +00:00
|
|
|
policy_file = POLICY_FILE_DIR + "/" + service_name
|
2013-03-20 05:24:17 +00:00
|
|
|
if not os.path.isfile(policy_file):
|
|
|
|
return None
|
|
|
|
f = open(policy_file, 'r+')
|
|
|
|
fcntl.flock(f, fcntl.LOCK_EX)
|
|
|
|
lines = []
|
|
|
|
for l in f.readlines():
|
|
|
|
lines.append(l)
|
|
|
|
lines.insert(0, "%s\t%s\tallow%s\n" % (domain, target, options))
|
|
|
|
f.seek(0)
|
|
|
|
f.write("".join(lines))
|
|
|
|
f.close()
|
|
|
|
|
2016-08-16 00:57:17 +00:00
|
|
|
|
2015-10-10 23:55:46 +00:00
|
|
|
def info_dialog(msg_type, text):
|
|
|
|
if msg_type not in ['info', 'warning', 'error']:
|
|
|
|
raise ValueError("Invalid msg_type value")
|
|
|
|
try:
|
|
|
|
subprocess.call(["/usr/bin/zenity", "--{}".format(msg_type), "--text",
|
2016-08-16 00:57:17 +00:00
|
|
|
text])
|
2015-10-10 23:55:46 +00:00
|
|
|
except OSError:
|
|
|
|
kdialog_msg_type = {
|
|
|
|
'info': 'msgbox',
|
|
|
|
'warning': 'sorry',
|
|
|
|
'error': 'error'
|
|
|
|
}[msg_type]
|
|
|
|
subprocess.call(["/usr/bin/kdialog", "--{}".format(kdialog_msg_type),
|
2016-08-16 00:57:17 +00:00
|
|
|
text])
|
|
|
|
|
2015-10-10 23:55:46 +00:00
|
|
|
|
2016-08-16 00:57:17 +00:00
|
|
|
# noinspection PyUnusedLocal
|
2013-12-27 04:38:16 +00:00
|
|
|
def policy_editor(domain, target, service_name):
|
|
|
|
text = "No policy definition found for " + service_name + " action. "
|
2016-08-16 00:57:17 +00:00
|
|
|
text += "Please create a policy file in Dom0 in " + POLICY_FILE_DIR + "/" + service_name
|
2015-10-10 23:55:46 +00:00
|
|
|
info_dialog("warning", text)
|
2013-03-20 05:24:17 +00:00
|
|
|
|
2016-08-16 00:57:17 +00:00
|
|
|
|
2013-03-20 05:24:17 +00:00
|
|
|
def main():
|
2016-08-16 01:09:16 +00:00
|
|
|
parser = argparse.ArgumentParser(description="Evaluate qrexec policy")
|
|
|
|
parser.add_argument("--assume-yes-for-ask", action="store_true",
|
2016-08-16 00:57:17 +00:00
|
|
|
dest="assume_yes_for_ask", default=False,
|
|
|
|
help="Allow run of service without confirmation if policy say 'ask'")
|
2016-08-16 01:09:16 +00:00
|
|
|
parser.add_argument("--just-evaluate", action="store_true",
|
2016-08-16 00:57:17 +00:00
|
|
|
dest="just_evaluate", default=False,
|
|
|
|
help="Do not run the service, only evaluate policy; "
|
|
|
|
"retcode=0 means 'allow'")
|
2016-08-16 01:09:16 +00:00
|
|
|
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='proces-ident',
|
|
|
|
help='Qrexec process identifier - for connecting data channel')
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
process_ident = args.process_ident
|
2013-12-27 04:38:16 +00:00
|
|
|
|
|
|
|
# Add source domain information, required by qrexec-client for establishing
|
|
|
|
# connection
|
2016-08-16 01:09:16 +00:00
|
|
|
process_ident += "," + args.domain + "," + args.domain_id
|
2013-10-27 18:22:47 +00:00
|
|
|
|
2016-08-17 00:37:37 +00:00
|
|
|
app = qubes.Qubes()
|
|
|
|
|
2016-02-11 01:16:01 +00:00
|
|
|
try:
|
2016-08-17 00:37:37 +00:00
|
|
|
source_vm = app.domains[args.domain]
|
|
|
|
except KeyError:
|
2017-02-22 11:14:50 +00:00
|
|
|
print("Rpc failed (unknown source domain): ",
|
|
|
|
args.domain, args.target, args.service_name, file=sys.stderr)
|
2016-08-17 00:37:37 +00:00
|
|
|
text = "Domain '%s' doesn't exist (service %s called to domain %s)." % (
|
|
|
|
args.domain, args.service_name, args.target)
|
|
|
|
info_dialog("error", text)
|
|
|
|
return 1
|
|
|
|
|
|
|
|
try:
|
|
|
|
target_vm = validate_target(app, args.target)
|
2016-02-11 01:16:01 +00:00
|
|
|
except KeyError:
|
2017-02-22 11:14:50 +00:00
|
|
|
print("Rpc failed (unknown domain):",
|
|
|
|
args.domain, args.target, args.service_name, file=sys.stderr)
|
2014-10-28 04:28:13 +00:00
|
|
|
text = "Domain '%s' doesn't exist (service %s called by domain %s)." % (
|
2016-08-16 01:09:16 +00:00
|
|
|
args.target, args.service_name, args.domain)
|
2015-10-10 23:55:46 +00:00
|
|
|
info_dialog("error", text)
|
2016-08-16 01:20:36 +00:00
|
|
|
return 1
|
2013-08-14 22:01:56 +00:00
|
|
|
|
2016-08-16 01:09:16 +00:00
|
|
|
policy_list = read_policy_file(args.service_name)
|
2016-08-16 00:57:17 +00:00
|
|
|
if policy_list is None:
|
2016-08-16 01:09:16 +00:00
|
|
|
policy_editor(args.domain, args.target, args.service_name)
|
|
|
|
policy_list = read_policy_file(args.service_name)
|
2016-08-16 00:57:17 +00:00
|
|
|
if policy_list is None:
|
|
|
|
policy_list = list()
|
2013-03-20 05:24:17 +00:00
|
|
|
|
2016-08-17 00:37:37 +00:00
|
|
|
policy_dict = find_policy(policy_list, source_vm, args.target, target_vm)
|
2013-03-20 05:24:17 +00:00
|
|
|
|
2016-08-16 01:09:16 +00:00
|
|
|
if policy_dict["action"] == "ask" and args.assume_yes_for_ask:
|
2013-03-20 05:24:17 +00:00
|
|
|
policy_dict["action"] = "allow"
|
2013-10-27 18:22:47 +00:00
|
|
|
|
2013-03-20 05:24:17 +00:00
|
|
|
if policy_dict["action"] == "ask":
|
2016-08-16 01:09:16 +00:00
|
|
|
user_choice = confirm_execution(args.domain, args.target, args.service_name)
|
2013-03-20 05:24:17 +00:00
|
|
|
if user_choice == UserChoice.ALWAYS_ALLOW:
|
2016-08-16 01:09:16 +00:00
|
|
|
add_always_allow(args.domain, args.target, args.service_name,
|
2016-08-16 00:57:17 +00:00
|
|
|
policy_dict["full-action"].lstrip('ask'))
|
2013-03-20 05:24:17 +00:00
|
|
|
policy_dict["action"] = "allow"
|
|
|
|
elif user_choice == UserChoice.ALLOW:
|
|
|
|
policy_dict["action"] = "allow"
|
|
|
|
else:
|
|
|
|
policy_dict["action"] = "deny"
|
|
|
|
|
2016-08-16 01:09:16 +00:00
|
|
|
if args.just_evaluate:
|
2013-03-20 05:24:17 +00:00
|
|
|
if policy_dict["action"] == "allow":
|
2016-08-16 01:20:36 +00:00
|
|
|
return 0
|
2013-03-20 05:24:17 +00:00
|
|
|
else:
|
2016-08-16 01:20:36 +00:00
|
|
|
return 1
|
2013-03-20 05:24:17 +00:00
|
|
|
|
|
|
|
if policy_dict["action"] == "allow":
|
2016-08-16 00:57:17 +00:00
|
|
|
if "action.target" in policy_dict:
|
2016-08-16 01:09:16 +00:00
|
|
|
args.target = policy_dict["action.target"]
|
2016-08-16 00:57:17 +00:00
|
|
|
if "action.user" in policy_dict:
|
|
|
|
user = policy_dict["action.user"]
|
2013-03-20 05:24:17 +00:00
|
|
|
else:
|
2016-08-16 00:57:17 +00:00
|
|
|
user = "DEFAULT"
|
2017-02-22 11:14:50 +00:00
|
|
|
print("Rpc allowed:", args.domain, args.target,
|
|
|
|
args.service_name, file=sys.stderr)
|
2016-08-17 00:37:37 +00:00
|
|
|
do_execute(source_vm, args.target, user, args.service_name, process_ident,
|
|
|
|
vm=target_vm)
|
|
|
|
return 0
|
2017-02-22 11:14:50 +00:00
|
|
|
print("Rpc denied:", args.domain, args.target, args.service_name,
|
|
|
|
file=sys.stderr)
|
2016-08-16 01:20:36 +00:00
|
|
|
return 1
|
2013-03-20 05:24:17 +00:00
|
|
|
|
2016-08-16 01:20:36 +00:00
|
|
|
if __name__ == '__main__':
|
|
|
|
sys.exit(main())
|