From 68bb447a0f9c2a10ace4d86aa07dba41831a6873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20=C5=A0karvada?= Date: Fri, 10 Jun 2016 11:57:33 +0200 Subject: [PATCH] Implemented polkit authorization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dropped dbus at_console policy and implemented polkit authorization. By the default policy the query actions are allowed for all users, actions which do modification to the system settings or Tuned state are by default allowed only for active users that have console, others need admin authorization. Resolves: rhbz#1095142 Signed-off-by: Jaroslav Škarvada --- Makefile | 5 +- com.redhat.tuned.policy | 129 +++++++++++++++++++++++++++++++++ dbus.conf | 7 +- tuned.spec | 1 + tuned/admin/admin.py | 2 +- tuned/admin/dbus_controller.py | 6 +- tuned/consts.py | 3 +- tuned/daemon/controller.py | 57 +++++++++++---- tuned/exports/dbus_exporter.py | 25 ++++++- tuned/utils/polkit.py | 16 ++++ 10 files changed, 222 insertions(+), 29 deletions(-) create mode 100644 com.redhat.tuned.policy create mode 100644 tuned/utils/polkit.py diff --git a/Makefile b/Makefile index 60c6126..a0dc283 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 0000000..0ada611 --- /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 120778a..c3b4277 100644 --- a/dbus.conf +++ b/dbus.conf @@ -4,18 +4,13 @@ - - + - - - - diff --git a/tuned.spec b/tuned.spec index 76f5c09..321028c 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 3a77452..f4f4426 100644 --- a/tuned/admin/admin.py +++ b/tuned/admin/admin.py @@ -24,7 +24,7 @@ class Admin(object): 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 c730520..1b11b18 100644 --- a/tuned/admin/dbus_controller.py +++ b/tuned/admin/dbus_controller.py @@ -13,6 +13,7 @@ class DBusController(object): 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 @@ class DBusController(object): 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 @@ class DBusController(object): 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 482702c..e3658e9 100644 --- a/tuned/consts.py +++ b/tuned/consts.py @@ -3,7 +3,8 @@ ACTIVE_PROFILE_FILE = "/etc/tuned/active_profile" 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 75b31b0..346ca3a 100644 --- a/tuned/daemon/controller.py +++ b/tuned/daemon/controller.py @@ -45,8 +45,15 @@ class Controller(tuned.exports.interfaces.ExportableInterface): 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 @@ class Controller(tuned.exports.interfaces.ExportableInterface): 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 @@ class Controller(tuned.exports.interfaces.ExportableInterface): 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 @@ class Controller(tuned.exports.interfaces.ExportableInterface): 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 6338bc4..616729c 100644 --- a/tuned/exports/dbus_exporter.py +++ b/tuned/exports/dbus_exporter.py @@ -2,11 +2,17 @@ import interfaces 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 @@ class DBusExporter(interfaces.ExporterInterface): 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 @@ class DBusExporter(interfaces.ExporterInterface): 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 @@ class DBusExporter(interfaces.ExporterInterface): 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 @@ class DBusExporter(interfaces.ExporterInterface): 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 0000000..319b553 --- /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]