From ee6e98b18936eebbcfca649c4d591d8f2864f389 Mon Sep 17 00:00:00 2001 From: Anselm Kruis Date: Sun, 15 May 2016 20:04:07 +0200 Subject: [PATCH 1/4] Add an optional timeout argument to SystemBus and SessionBus Previously the initial value of timeout was card coded to 1000ms --- pydbus/bus.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pydbus/bus.py b/pydbus/bus.py index b010ed0..b21cbb4 100644 --- a/pydbus/bus.py +++ b/pydbus/bus.py @@ -18,11 +18,11 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, traceback): self.con = None -def SystemBus(): - return Bus(Bus.Type.SYSTEM) +def SystemBus(timeout=1000): + return Bus(Bus.Type.SYSTEM, timeout=timeout) -def SessionBus(): - return Bus(Bus.Type.SESSION) +def SessionBus(timeout=1000): + return Bus(Bus.Type.SESSION, timeout=timeout) if __name__ == "__main__": import sys From 2f5f79625e2784f29873dc67d55c78db31d50342 Mon Sep 17 00:00:00 2001 From: Anselm Kruis Date: Mon, 16 May 2016 02:30:56 +0200 Subject: [PATCH 2/4] Asynchronous execution and authorization for exported methods This commit enhances registration.ObjectWrapper to support asynchronous method calls. This commit adds a method decorator, that authorizes a call with Polkit. --- pydbus/authorization.py | 134 +++++++++++++++++++++++++ pydbus/registration.py | 68 +++++++++---- pydbus/tests/publish_async.py | 181 ++++++++++++++++++++++++++++++++++ 3 files changed, 364 insertions(+), 19 deletions(-) create mode 100644 pydbus/authorization.py create mode 100644 pydbus/tests/publish_async.py diff --git a/pydbus/authorization.py b/pydbus/authorization.py new file mode 100644 index 0000000..a3271ee --- /dev/null +++ b/pydbus/authorization.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Anselm Kruis +# +# 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# + +""" +A decorator for Polkit authorization +""" + +from __future__ import print_function, absolute_import + +from gi.repository import GObject, GLib, Polkit, Gio +import traceback +import inspect +import functools +from pydbus import registration + +__all__ = ['PolkitAuthorization'] + + +class PolkitAuthorization(object): + @staticmethod + def NotAuthorizedError(message="Not authorized to perform operation"): + return GLib.Error.new_literal(Polkit.error_quark(), message, Polkit.Error.NOT_AUTHORIZED) + + def __init__(self, action_id, details=None, flags=Polkit.CheckAuthorizationFlags.ALLOW_USER_INTERACTION): + if callable(action_id): + raise TypeError("PolkitAuthorization needs an action id") + self.action_id = action_id + self.details = details + self.flags = flags + + def __call__(self, func): + # check, if func needs the dbus_bus and dbus_method_invokation kw args + try: + method_args = inspect.getargspec(func)[0] + except TypeError: + # not a function + method_args = () + is_async = getattr(func, "async", None) + needs_dbus_method_invocation = ("dbus_method_invocation" in method_args or + getattr(func, "arg_dbus_method_invocation", None) or + is_async) + needs_dbus_bus = ("dbus_bus" in method_args or getattr(func, "arg_dbus_bus", None)) + needs_polkit_is_authorized = ("polkit_is_authorized" in method_args or getattr(func, "arg_polkit_is_authorized", None)) + + @functools.wraps(func) + def wrapper(*args, **kw): + bus = kw['dbus_bus'] if needs_dbus_bus else kw.pop('dbus_bus') + method_invocation = kw['dbus_method_invocation'] if needs_dbus_method_invocation else kw.pop('dbus_method_invocation') + try: + cancellable = Gio.Cancellable() + state = dict(bus=bus, + method_invocation=method_invocation, + func=func, + is_async=is_async, + needs_polkit_is_authorized=needs_polkit_is_authorized, + cancellable=cancellable, + args=args, + kw=kw) + timeout = bus.timeout + if timeout == -1: + # -1 indicates a default value + timeout = 25000 # DBus default + + GLib.timeout_add(timeout, cancellable.cancel) + Polkit.Authority.get_async(cancellable, self._cb_get_authority_ready, state) + except Exception as e: + traceback.print_exc() # FIXME: better error reporting. logging? + method_invocation.return_exception(e) + + wrapper.async = True + wrapper.arg_dbus_bus = True + wrapper.arg_dbus_method_invocation = True + return wrapper + + def _cb_get_authority_ready(self, source_object, res, state): + # source_object is None + method_invocation = state['method_invocation'] + try: + authority = Polkit.Authority.get_finish(res) + assert authority is not None + + sender = method_invocation.get_sender() + dbus_service = state['bus'].get('.DBus')[''] + sender_pid = dbus_service.GetConnectionUnixProcessID(sender) # synchronous call for now + subject = Polkit.UnixProcess.new(sender_pid) + + authority.check_authorization(subject, + self.action_id, + self.details, + self.flags, + state['cancellable'], + self._cb_check_authorization_ready, + state) + except Exception as e: + traceback.print_exc() # FIXME: better error reporting. logging? + method_invocation.return_exception(e) + + def _cb_check_authorization_ready(self, authority, res, state): + method_invocation = state['method_invocation'] + needs_polkit_is_authorized = state['needs_polkit_is_authorized'] + try: + result = authority.check_authorization_finish(res) + is_authorized = result.get_is_authorized() + if not needs_polkit_is_authorized and not is_authorized: + method_invocation.return_exception(self.NotAuthorizedError()) + return + + if needs_polkit_is_authorized: + state['kw']['polkit_is_authorized'] = is_authorized + + result = state['func'](*state['args'], **state['kw']) + if not (state['is_async'] or result is registration.METHOD_IS_ASYNC): + method_invocation.return_value(result) + + except Exception as e: + traceback.print_exc() # FIXME: better error reporting. logging? + method_invocation.return_exception(e) diff --git a/pydbus/registration.py b/pydbus/registration.py index ccdbc9d..b9b444a 100644 --- a/pydbus/registration.py +++ b/pydbus/registration.py @@ -1,14 +1,18 @@ from __future__ import print_function -import sys, traceback +import sys, traceback, inspect from gi.repository import GLib, Gio from . import generic from .exitable import ExitableWithAliases +METHOD_IS_ASYNC = object() + + class ObjectWrapper(ExitableWithAliases("unwrap")): - __slots__ = ["object", "outargs", "property_types"] + __slots__ = ["object", "outargs", "property_types", "bus"] - def __init__(self, object, interfaces): + def __init__(self, object, interfaces, bus): self.object = object + self.bus = bus self.outargs = {} for iface in interfaces: @@ -40,30 +44,56 @@ def onPropertiesChanged(iface, changed, invalidated): SignalEmitted = generic.signal() def call_method(self, connection, sender, object_path, interface_name, method_name, parameters, invocation): + def return_exception(exc): + if isinstance(exc, GLib.GError): + # the much simpler invocation.return_gerror(exc) raises TypeError. + invocation.return_error_literal(GLib.quark_from_string(exc.domain), exc.code, exc.message) + else: + # TODO Think of a better way to translate Python exception types to DBus error types. + e_type = type(exc).__name__ + if "." not in e_type: + e_type = "unknown." + e_type + invocation.return_dbus_error(e_type, str(exc)) + try: outargs = self.outargs[interface_name + "." + method_name] + loutargs = len(outargs) soutargs = "(" + "".join(outargs) + ")" - method = getattr(self.object, method_name) + invocation.return_value_raw = return_value_raw = invocation.return_value - result = method(*parameters) + def return_value(result): + if loutargs == 0: + return_value_raw(None) + elif loutargs == 1: + return_value_raw(GLib.Variant(soutargs, (result,))) + else: + return_value_raw(GLib.Variant(soutargs, result)) + return False - #if len(outargs) == 1: - # result = (result,) + invocation.return_value = return_value + invocation.return_exception = return_exception - if len(outargs) == 0: - invocation.return_value(None) - elif len(outargs) == 1: - invocation.return_value(GLib.Variant(soutargs, (result,))) - else: - invocation.return_value(GLib.Variant(soutargs, result)) + method = getattr(self.object, method_name) + is_async = getattr(method, "async", None) + try: + method_args = inspect.getargspec(method)[0] + except TypeError: + # not a function + method_args = () + + kw = {} + if "dbus_bus" in method_args or getattr(method, "arg_dbus_bus", None): + kw["dbus_bus"] = self.bus + if "dbus_method_invocation" in method_args or getattr(method, "arg_dbus_bus", None) or is_async: + kw["dbus_method_invocation"] = invocation + + result = method(*parameters, **kw) + if not (is_async or result is METHOD_IS_ASYNC): + return_value(result) except Exception as e: - #TODO Think of a better way to translate Python exception types to DBus error types. - e_type = type(e).__name__ - if not "." in e_type: - e_type = "unknown." + e_type - invocation.return_dbus_error(e_type, str(e)) + return_exception(e) def get_property(self, connection, sender, object_path, interface_name, property_name): # Note: It's impossible to correctly return an exception, as @@ -119,5 +149,5 @@ def register_object(self, path, object, node_info): node_info = [Gio.DBusNodeInfo.new_for_xml(ni) for ni in node_info] interfaces = sum((ni.interfaces for ni in node_info), []) - wrapper = ObjectWrapper(object, interfaces) + wrapper = ObjectWrapper(object, interfaces, self) return ObjectRegistration(self.con, path, interfaces, wrapper, own_wrapper=True) diff --git a/pydbus/tests/publish_async.py b/pydbus/tests/publish_async.py new file mode 100644 index 0000000..415dcc5 --- /dev/null +++ b/pydbus/tests/publish_async.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Anselm Kruis +# +# 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# + +""" +A variant of publish, that works asynchronously and authorizes the service using Polkit +""" + + +from __future__ import print_function, absolute_import +from pydbus import SessionBus +from pydbus.authorization import PolkitAuthorization +from gi.repository import GObject, GLib, Polkit, Gio +from threading import Thread +import sys +import traceback + +dot_count = 0 +done = 0 +loop = GObject.MainLoop() + + +class TestObject(object): + ''' + + + + + + + + + + + + + + + ''' + + def __init__(self, id): + self.id = id + + def do_cancel(self, cancellable): + print("Timeout, canceling operation") + sys.stdout.flush() + cancellable.cancel() + + # the simple way + @PolkitAuthorization("org.freedesktop.policykit.exec") + def HelloWorld1(self, a, b, polkit_is_authorized): + global done + done += 1 + if done == 2: + GLib.idle_add(loop.quit) + if not polkit_is_authorized: + raise PolkitAuthorization.NotAuthorizedError("You are not authorized!") + res = self.id + ": " + a + str(b) + print(res) + return res + + # use the low level API, otherwise the same as HelloWorld1 + def HelloWorld2(self, a, b, dbus_method_invocation, dbus_bus): + try: + cancellable = Gio.Cancellable() + state = dict(a=a, + b=b, + method_invocation=dbus_method_invocation, + bus=dbus_bus, + cancellable=cancellable) + GLib.timeout_add(dbus_bus.timeout, self.do_cancel, cancellable) + Polkit.Authority.get_async(cancellable, self._cb_get_authority_ready, state) + except Exception as e: + traceback.print_exc() + dbus_method_invocation.return_exception(e) + HelloWorld2.async = True + + def _cb_get_authority_ready(self, source_object, res, state): + # source_object is None + dbus_bus = state['bus'] + dbus_method_invocation = state['method_invocation'] + cancellable = state['cancellable'] + + try: + authority = Polkit.Authority.get_finish(res) + assert authority is not None + + sender = dbus_method_invocation.get_sender() + dbus_service = dbus_bus.get('.DBus')[''] + sender_pid = dbus_service.GetConnectionUnixProcessID(sender) # synchronous call for now + state['sender_pid'] = sender_pid + subject = Polkit.UnixProcess.new(sender_pid) + + authority.check_authorization(subject, + "org.freedesktop.policykit.exec", # uses auth_admin + None, + Polkit.CheckAuthorizationFlags.ALLOW_USER_INTERACTION, + cancellable, + self._cb_check_authorization_ready, + state) + except Exception as e: + traceback.print_exc() + dbus_method_invocation.return_exception(e) + + def _cb_check_authorization_ready(self, authority, res, state): + dbus_method_invocation = state['method_invocation'] + a = state['a'] + b = state['b'] + sender_pid = state['sender_pid'] + try: + global done + done += 1 + if done == 2: + GLib.idle_add(loop.quit) + + result = authority.check_authorization_finish(res) + if not result.get_is_authorized(): + dbus_method_invocation.return_dbus_error("unknown.PermissionDenied", "You are not authorized!") + return + + res = self.id + ": " + a + str(b) + " sender pid " + str(sender_pid) + print(res) + dbus_method_invocation.return_value(res) + + except Exception as e: + traceback.print_exc() + dbus_method_invocation.return_exception(e) + + +def print_dots(): + """Print one dot per second, if the mainloop is running""" + global dot_count + print(".", end='') + dot_count += 1 + if dot_count >= 80: + print("") + dot_count = 0 + sys.stdout.flush() + return True + +with SessionBus(timeout=30 * 1000) as bus: + with bus.publish("net.lew21.pydbus.Test", TestObject("Main"), ("Lol", TestObject("Lol"))): + remoteMain = bus.get("net.lew21.pydbus.Test") + remoteLol = bus.get("net.lew21.pydbus.Test", "Lol") + + def t1_func(): + print(remoteMain.HelloWorld1("t", 1)) + + def t2_func(): + print(remoteLol.HelloWorld2("t", 2)) + + t1 = Thread(None, t1_func) + t2 = Thread(None, t2_func) + t1.daemon = True + t2.daemon = True + + t1.start() + t2.start() + + GLib.timeout_add_seconds(1, print_dots) + + loop.run() + + t1.join() + t2.join() From 6cc95658e4b112957b00f9d3dd1b02219e47fda2 Mon Sep 17 00:00:00 2001 From: Anselm Kruis Date: Tue, 17 May 2016 01:00:55 +0200 Subject: [PATCH 3/4] Eliminate duplicated code from registration and authorization Create a new function generic.inspect_function and use it in registration and authorization. --- pydbus/authorization.py | 30 +++++++++-------------- pydbus/generic.py | 53 +++++++++++++++++++++++++++++++++++++++++ pydbus/registration.py | 21 ++++++++-------- 3 files changed, 75 insertions(+), 29 deletions(-) diff --git a/pydbus/authorization.py b/pydbus/authorization.py index a3271ee..0093bb7 100644 --- a/pydbus/authorization.py +++ b/pydbus/authorization.py @@ -24,11 +24,10 @@ from __future__ import print_function, absolute_import -from gi.repository import GObject, GLib, Polkit, Gio -import traceback -import inspect +from gi.repository import GLib, Polkit, Gio import functools -from pydbus import registration +from pydbus import registration, generic +# import traceback __all__ = ['PolkitAuthorization'] @@ -46,18 +45,11 @@ def __init__(self, action_id, details=None, flags=Polkit.CheckAuthorizationFlags self.flags = flags def __call__(self, func): - # check, if func needs the dbus_bus and dbus_method_invokation kw args - try: - method_args = inspect.getargspec(func)[0] - except TypeError: - # not a function - method_args = () - is_async = getattr(func, "async", None) - needs_dbus_method_invocation = ("dbus_method_invocation" in method_args or - getattr(func, "arg_dbus_method_invocation", None) or - is_async) - needs_dbus_bus = ("dbus_bus" in method_args or getattr(func, "arg_dbus_bus", None)) - needs_polkit_is_authorized = ("polkit_is_authorized" in method_args or getattr(func, "arg_polkit_is_authorized", None)) + func_info = generic.inspect_function(func, flag_names=('async',), arg_names=('dbus_bus', 'dbus_method_invocation', "polkit_is_authorized")) + is_async = func_info['async'] + needs_dbus_method_invocation = func_info["dbus_method_invocation"] + needs_dbus_bus = func_info["dbus_bus"] + needs_polkit_is_authorized = func_info["polkit_is_authorized"] @functools.wraps(func) def wrapper(*args, **kw): @@ -81,7 +73,7 @@ def wrapper(*args, **kw): GLib.timeout_add(timeout, cancellable.cancel) Polkit.Authority.get_async(cancellable, self._cb_get_authority_ready, state) except Exception as e: - traceback.print_exc() # FIXME: better error reporting. logging? + # traceback.print_exc() # FIXME: better error reporting. logging? method_invocation.return_exception(e) wrapper.async = True @@ -109,7 +101,7 @@ def _cb_get_authority_ready(self, source_object, res, state): self._cb_check_authorization_ready, state) except Exception as e: - traceback.print_exc() # FIXME: better error reporting. logging? + # traceback.print_exc() # FIXME: better error reporting. logging? method_invocation.return_exception(e) def _cb_check_authorization_ready(self, authority, res, state): @@ -130,5 +122,5 @@ def _cb_check_authorization_ready(self, authority, res, state): method_invocation.return_value(result) except Exception as e: - traceback.print_exc() # FIXME: better error reporting. logging? + # traceback.print_exc() # FIXME: better error reporting. logging? method_invocation.return_exception(e) diff --git a/pydbus/generic.py b/pydbus/generic.py index abeb7ce..0c895dc 100644 --- a/pydbus/generic.py +++ b/pydbus/generic.py @@ -4,6 +4,9 @@ on dbus, they can be used everywhere. """ +import inspect + + class subscription(object): __slots__ = ("callback_list", "callback") @@ -103,3 +106,53 @@ def __repr__(self): return "" bound_method = type(signal().emit) # TODO find a prettier way to get this type + + +def inspect_function(func, flag_names, arg_names): + """Inspect a function or method. + + This function inspects *func* and returns + boolean flags and information, whether *func* wants a partiucular + arguments. + + The flag *NAME* is set, if *func* has an attribute *NAME*, whose + value is true in a boolean context. + + The function wants the argument *NAME*, it *NAME* is in the list of + named arguments or if *func* has an attribute ``arg_``*NAME*, whose + value is true in a boolean context. + + :parameter func: a callable object. + :parameter flag_names: an iterable, that yields flag names (strings) + :type flag_names: :class:`~collections.Iterable` + :parameter arg_names: an iterable, that yields potential argument names. + :type arg_names: :class:`~collections.Iterable` + :returns: a dictionary, that contains a boolean value for each flag-name + and arg-name. + :rtype: dict + """ + result = {} + for name in flag_names: + try: + value = bool(getattr(func, name)) # be careful, func can be anything + except Exception: + value = False + result[name] = value + + if arg_names: + try: + func_args = inspect.getargspec(func)[0] + except TypeError: + # not a function + func_args = () + for name in arg_names: + if name in func_args: + result[name] = True + continue + try: + value = bool(getattr(func, "arg_" + name)) + except Exception: + value = False + result[name] = value + + return result diff --git a/pydbus/registration.py b/pydbus/registration.py index b9b444a..26317df 100644 --- a/pydbus/registration.py +++ b/pydbus/registration.py @@ -1,5 +1,5 @@ from __future__ import print_function -import sys, traceback, inspect +import sys, traceback from gi.repository import GLib, Gio from . import generic from .exitable import ExitableWithAliases @@ -75,21 +75,22 @@ def return_value(result): invocation.return_exception = return_exception method = getattr(self.object, method_name) - is_async = getattr(method, "async", None) - try: - method_args = inspect.getargspec(method)[0] - except TypeError: - # not a function - method_args = () + method_info = generic.inspect_function(method, flag_names=('async',), arg_names=('dbus_bus', 'dbus_method_invocation')) + + if method_info["async"] and not method_info["dbus_method_invocation"]: + # for now, we require a asynchronous method to accept the argument 'dbus_method_invocation' + # and to return its result itself. + raise TypeError("an asynchronous method must accept the argument 'dbus_method_invocation'") kw = {} - if "dbus_bus" in method_args or getattr(method, "arg_dbus_bus", None): + if method_info["dbus_bus"]: kw["dbus_bus"] = self.bus - if "dbus_method_invocation" in method_args or getattr(method, "arg_dbus_bus", None) or is_async: + if method_info["dbus_method_invocation"]: kw["dbus_method_invocation"] = invocation result = method(*parameters, **kw) - if not (is_async or result is METHOD_IS_ASYNC): + if not (result is METHOD_IS_ASYNC or method_info["async"]): + # func is synchronous and returned its result. Send it back to the remote caller. return_value(result) except Exception as e: From a4a31e8d075382d3579c29be95e66529ef0ee2b5 Mon Sep 17 00:00:00 2001 From: Anselm Kruis Date: Tue, 17 May 2016 14:38:44 +0200 Subject: [PATCH 4/4] silence a warning (python 3.5, ubuntu 16.04) --- pydbus/authorization.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pydbus/authorization.py b/pydbus/authorization.py index 0093bb7..9a8510d 100644 --- a/pydbus/authorization.py +++ b/pydbus/authorization.py @@ -23,7 +23,8 @@ """ from __future__ import print_function, absolute_import - +import gi +gi.require_version('Polkit', '1.0') from gi.repository import GLib, Polkit, Gio import functools from pydbus import registration, generic