diff --git a/Makefile b/Makefile index 60c6126b..a0dc2837 100644 --- a/Makefile +++ b/Makefile @@ -37,8 +37,8 @@ release-cp: release-dir cp -a tuned.py tuned.spec tuned.service tuned.tmpfiles Makefile tuned-adm.py \ tuned-adm.bash dbus.conf recommend.conf tuned-main.conf 00_tuned \ - bootcmdline com.redhat.tuned.gui.policy tuned-gui.py tuned-gui.glade \ - tuned-gui.desktop $(VERSIONED_NAME) + bootcmdline com.redhat.tuned.policy com.redhat.tuned.gui.policy \ + tuned-gui.py tuned-gui.glade tuned-gui.desktop $(VERSIONED_NAME) cp -a doc experiments libexec man profiles systemtap tuned contrib icons \ $(VERSIONED_NAME) @@ -144,6 +144,7 @@ install: install-dirs install -Dpm 0755 00_tuned $(DESTDIR)/etc/grub.d/00_tuned # polkit configuration + install -Dpm 0644 com.redhat.tuned.policy $(DESTDIR)$(DATADIR)/polkit-1/actions/com.redhat.tuned.policy install -Dpm 0644 com.redhat.tuned.gui.policy $(DESTDIR)$(DATADIR)/polkit-1/actions/com.redhat.tuned.gui.policy # manual pages diff --git a/com.redhat.tuned.policy b/com.redhat.tuned.policy new file mode 100644 index 00000000..0ada6110 --- /dev/null +++ b/com.redhat.tuned.policy @@ -0,0 +1,129 @@ + + + + + Tuned + https://fedorahosted.org/tuned/ + tuned + + + Show active profile + Authentication is required to show active profile + + yes + yes + yes + + + + + Disable Tuned + Authentication is required to disable Tuned + + auth_admin + auth_admin + yes + + + + + Check whether Tuned is running + Authentication is required to check whether Tuned is running + + yes + yes + yes + + + + + Show information about Tuned profile + Authentication is required to show information about Tuned profile + + yes + yes + yes + + + + + List Tuned profiles + Authentication is required to list Tuned profiles + + yes + yes + yes + + + + + List Tuned profiles + Authentication is required to list Tuned profiles + + yes + yes + yes + + + + + Show Tuned profile name which is recommended for your system + Authentication is required to show recommended profile name + + yes + yes + yes + + + + + Reload Tuned configuration + Authentication is required to reload Tuned configuration + + auth_admin + auth_admin + yes + + + + + Start Tuned daemon + Authentication is required to start Tuned daemon + + auth_admin + auth_admin + yes + + + + + Stop Tuned daemon + Authentication is required to stop Tuned daemon + + auth_admin + auth_admin + yes + + + + + Switch Tuned profile + Authentication is required to switch Tuned profile + + auth_admin + auth_admin + yes + + + + + Verify Tuned profile + Authentication is required to verify Tuned profile + + yes + yes + yes + + + diff --git a/dbus.conf b/dbus.conf index 120778a8..c3b42779 100644 --- a/dbus.conf +++ b/dbus.conf @@ -4,18 +4,13 @@ - - + - - - - diff --git a/tuned.spec b/tuned.spec index 76f5c099..321028c7 100644 --- a/tuned.spec +++ b/tuned.spec @@ -256,6 +256,7 @@ fi %{_sbindir}/tuned-gui %{python_sitelib}/tuned/gtk %{_datadir}/tuned/ui +%{_datadir}/polkit-1/actions/com.redhat.tuned.policy %{_datadir}/polkit-1/actions/com.redhat.tuned.gui.policy %{_datadir}/icons/hicolor/scalable/apps/tuned.svg %{_datadir}/applications/tuned-gui.desktop diff --git a/tuned/admin/admin.py b/tuned/admin/admin.py index 3a774526..f4f4426a 100644 --- a/tuned/admin/admin.py +++ b/tuned/admin/admin.py @@ -24,7 +24,7 @@ def __init__(self, dbus = True, debug = False, async = False, timeout = consts.A self._daemon_action_errstr = "" self._controller = None if self._dbus: - self._controller = tuned.admin.DBusController(consts.DBUS_BUS, consts.DBUS_OBJECT, consts.DBUS_INTERFACE, debug) + self._controller = tuned.admin.DBusController(consts.DBUS_BUS, consts.DBUS_INTERFACE, consts.DBUS_OBJECT, debug) try: self._controller.set_signal_handler(consts.DBUS_SIGNAL_PROFILE_CHANGED, self._signal_profile_changed_cb) except TunedAdminDBusException as e: diff --git a/tuned/admin/dbus_controller.py b/tuned/admin/dbus_controller.py index c7305200..1b11b189 100644 --- a/tuned/admin/dbus_controller.py +++ b/tuned/admin/dbus_controller.py @@ -13,6 +13,7 @@ def __init__(self, bus_name, interface_name, object_name, debug = False): self._interface_name = interface_name self._object_name = object_name self._proxy = None + self._interface = None self._debug = debug self._main_loop = None self._action = None @@ -29,7 +30,8 @@ def _init_proxy(self): DBusGMainLoop(set_as_default=True) self._main_loop = GLib.MainLoop() bus = dbus.SystemBus() - self._proxy = bus.get_object(self._bus_name, self._interface_name, self._object_name) + self._proxy = bus.get_object(self._bus_name, self._object_name) + self._interface = dbus.Interface(self._proxy, dbus_interface = self._interface_name) except dbus.exceptions.DBusException: raise TunedAdminDBusException("Cannot talk to Tuned daemon via DBus. Is Tuned daemon running?") @@ -67,7 +69,7 @@ def _call(self, method_name, *args, **kwargs): self._init_proxy() try: - method = self._proxy.get_dbus_method(method_name) + method = self._interface.get_dbus_method(method_name) return method(*args, **kwargs) except dbus.exceptions.DBusException as dbus_exception: err_str = "DBus call to Tuned daemon failed" diff --git a/tuned/consts.py b/tuned/consts.py index 482702c7..e3658e9d 100644 --- a/tuned/consts.py +++ b/tuned/consts.py @@ -3,7 +3,8 @@ PROFILE_FILE = "tuned.conf" AUTODETECT_FILE = "recommend.conf" DAEMONIZE_PARENT_TIMEOUT = 5 -DBUS_BUS = "com.redhat.tuned" +NAMESPACE = "com.redhat.tuned" +DBUS_BUS = NAMESPACE DBUS_INTERFACE = "com.redhat.tuned.control" DBUS_OBJECT = "/Tuned" DEFAULT_PROFILE = "balanced" diff --git a/tuned/daemon/controller.py b/tuned/daemon/controller.py index 75b31b0b..346ca3a1 100644 --- a/tuned/daemon/controller.py +++ b/tuned/daemon/controller.py @@ -45,8 +45,15 @@ def terminate(self): def profile_changed(self, profile_name, result, errstr): pass + # exports decorator checks the authorization (currently through polkit), caller is None if + # no authorization was performed (i.e. the call should process as authorized), string + # identifying caller (with DBus it's the caller bus name) if authorized and empty + # string if not authorized, caller must be the last argument + @exports.export("", "b") - def start(self): + def start(self, caller = None): + if caller == "": + return False if self._global_config.get_bool(consts.CFG_DAEMON, consts.CFG_DEF_DAEMON): if self._daemon.is_running(): return True @@ -55,21 +62,27 @@ def start(self): return self._daemon.start() @exports.export("", "b") - def stop(self): + def stop(self, caller = None): + if caller == "": + return False if not self._daemon.is_running(): return True else: return self._daemon.stop() @exports.export("", "b") - def reload(self): + def reload(self, caller = None): + if caller == "": + return False if not self._daemon.is_running(): return False else: return self.stop() and self.start() @exports.export("s", "(bs)") - def switch_profile(self, profile_name): + def switch_profile(self, profile_name, caller = None): + if caller == "": + return (False, "Unauthorized") was_running = self._daemon.is_running() msg = "OK" success = True @@ -88,14 +101,18 @@ def switch_profile(self, profile_name): return (success, msg) @exports.export("", "s") - def active_profile(self): + def active_profile(self, caller = None): + if caller == "": + return "" if self._daemon.profile is not None: return self._daemon.profile.name else: return "" @exports.export("", "b") - def disable(self): + def disable(self, caller = None): + if caller == "": + return False if self._daemon.is_running(): self._daemon.stop() if self._daemon.is_enabled(): @@ -103,31 +120,45 @@ def disable(self): return True @exports.export("", "b") - def is_running(self): + def is_running(self, caller = None): + if caller == "": + return False return self._daemon.is_running() @exports.export("", "as") - def profiles(self): + def profiles(self, caller = None): + if caller == "": + return [] return self._daemon.profile_loader.profile_locator.get_known_names() @exports.export("", "a(ss)") - def profiles2(self): + def profiles2(self, caller = None): + if caller == "": + return [] return self._daemon.profile_loader.profile_locator.get_known_names_summary() @exports.export("s", "(bsss)") - def profile_info(self, profile_name): + def profile_info(self, profile_name, caller = None): + if caller == "": + return tuple(False, "", "", "") if profile_name is None or profile_name == "": profile_name = self.active_profile() return tuple(self._daemon.profile_loader.profile_locator.get_profile_attrs(profile_name, [consts.PROFILE_ATTR_SUMMARY, consts.PROFILE_ATTR_DESCRIPTION], [""])) @exports.export("", "s") - def recommend_profile(self): + def recommend_profile(self, caller = None): + if caller == "": + return "" return self._cmd.recommend_profile(hardcoded = not self._global_config.get_bool(consts.CFG_RECOMMEND_COMMAND, consts.CFG_DEF_RECOMMEND_COMMAND)) @exports.export("", "b") - def verify_profile(self): + def verify_profile(self, caller = None): + if caller == "": + return False return self._daemon.verify_profile(ignore_missing = False) @exports.export("", "b") - def verify_profile_ignore_missing(self): + def verify_profile_ignore_missing(self, caller = None): + if caller == "": + return False return self._daemon.verify_profile(ignore_missing = True) diff --git a/tuned/exports/dbus_exporter.py b/tuned/exports/dbus_exporter.py index 6338bc4e..616729cd 100644 --- a/tuned/exports/dbus_exporter.py +++ b/tuned/exports/dbus_exporter.py @@ -2,11 +2,17 @@ import decorator import dbus.service import dbus.mainloop.glib +import dbus.exceptions import inspect import threading import signal +import tuned.logs +import tuned.consts as consts +from tuned.utils.polkit import polkit from gi.repository import GObject as gobject +log = tuned.logs.get() + class DBusExporter(interfaces.ExporterInterface): """ Export method calls through DBus Interface. @@ -19,6 +25,7 @@ class DBusExporter(interfaces.ExporterInterface): def __init__(self, bus_name, interface_name, object_name): gobject.threads_init() + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) self._dbus_object_cls = None self._dbus_object = None @@ -30,6 +37,7 @@ def __init__(self, bus_name, interface_name, object_name): self._object_name = object_name self._thread = None self._bus_object = None + self._polkit = polkit() # dirty hack that fixes KeyboardInterrupt handling # the hack is needed because PyGObject / GTK+-3 developers are morons @@ -58,10 +66,22 @@ def export(self, method, in_signature, out_signature): raise Exception("Method with this name is already exported.") def wrapper(wrapped, owner, *args, **kwargs): + action_id = consts.NAMESPACE + "." + method.__name__ + caller = args[-1] + log.debug("checking authorization for for action '%s' requested by caller '%s'" % (action_id, caller)) + try: + if self._polkit.check_authorization(caller, action_id): + log.debug("action '%s' requested by caller '%s' was successfully authorized by polkit" % (action_id, caller)) + else: + log.info("action '%s' requested by caller '%s' wasn't authorized by polkit, ignoring the request" % (action_id, caller)) + args[-1] = "" + except (dbus.exceptions.DBusException, ValueError) as e: + log.error("unable to query polkit to authorize action '%s' requested by caller '%s': %s, ignoring the request" % (action_id, caller, e)) + args[-1] = "" return method(*args, **kwargs) wrapper = decorator.decorator(wrapper, method.im_func) - wrapper = dbus.service.method(self._interface_name, in_signature, out_signature)(wrapper) + wrapper = dbus.service.method(self._interface_name, in_signature, out_signature, sender_keyword = "caller")(wrapper) self._dbus_methods[method_name] = wrapper @@ -119,8 +139,6 @@ def stop(self): self._thread = None def _thread_code(self): - dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) - bus = dbus.SystemBus() bus_name = dbus.service.BusName(self._bus_name, bus) self._bus_object = self._dbus_object_cls(bus, self._object_name, bus_name) @@ -128,4 +146,3 @@ def _thread_code(self): self._main_loop.run() del self._bus_object self._bus_object = None - diff --git a/tuned/utils/polkit.py b/tuned/utils/polkit.py new file mode 100644 index 00000000..319b5538 --- /dev/null +++ b/tuned/utils/polkit.py @@ -0,0 +1,16 @@ +import dbus + +class polkit(): + def __init__(self): + bus = dbus.SystemBus() + proxy = bus.get_object('org.freedesktop.PolicyKit1', '/org/freedesktop/PolicyKit1/Authority') + self._authority = dbus.Interface(proxy, dbus_interface='org.freedesktop.PolicyKit1.Authority') + + def check_authorization(self, sender, action_id): + if sender is None or action_id is None: + return False + details = {} + flags = 1 # AllowUserInteraction flag + cancellation_id = '' # No cancellation id + subject = ('system-bus-name', {'name' : sender}) + return self._authority.CheckAuthorization(subject, action_id, details, flags, cancellation_id)[0]