Creating a D-Bus Service with dbus-python and Polkit Authentication

Vincent Wang
9 min readNov 9, 2019

D-Bus is the standard for inter-process communication for Linux desktop applications. Both Qt and GLib have high-level abstractions for D-Bus communication, and many of the desktop services we rely on export D-Bus protocols. However, D-Bus has its shortcomings — namely a lack of documentation. Let’s explore how to write our own D-Bus Service in Python and connect it to Freedesktop.org’s PolicyKit API to provide user authentication.

What is D-Bus?

D-Bus is a standard IPC/RPC protocol introduced by Freedesktop.org as a way of unifying the messy landscape of inter-process communication on Linux desktops under one standard. In other words, it’s a way for programs to communicate with each other.

How is D-Bus organized?

D-Bus is organized into objects. These objects can be published on one of two “buses” (the system bus, in which there is one object for the whole system, or the session bus, in which each user session can have its own object). Objects play a double role as both an RPC object (you can call methods on the object) and as a publish/subscribe interface (you can subscribe to signals on the object). Each object defines interfaces, which describe and organize what each object can do.

Objects published on a bus are identified by a unique bus name (often written in reverse-DNS format, e.g. “org.freedesktop.NetworkManager”) and an object path, describing (e.g. “/org/freedesktop/NetworkManager”). If you’re confused about the difference between an object path and a bus name, this probably explains it better than me. In this article, we’ll explore how to export our own object onto a bus under an object path of our choosing.

Why use D-Bus?

While D-Bus has its shortcomings, it still remains the standard for Linux desktops today. All of Freedesktop.org’s APIs (that is to say, maybe half of the average Linux desktop in general) are published through D-Bus. D-Bus is well suited to the needs of an IPC system for the Linux desktop; it can handle publish/subscribe interfaces as well as methods, allowing for robust architectures.

Okay, can we build the thing?

Yes.

We’ll be using dbus-python to build our service. This means it’s primarily targeted towards GLib/GTK apps. If you’re looking to integrate your Qt app with D-Bus, there’s great first-party docs on that.

Step 1 is to install the dbus-python bindings. Most package managers should have a package for it; it might already be installed by something else. If your package manager’s dbus-python version is out of date, you can always pip install it.

While you’re at it, install D-Feet for easy GUI D-Bus debugging.

Oh, and you’ll also want to grab PyGObject so the service has a mainloop, otherwise it won’t listen continuously.

Now that we have the dbus-python bindings, let’s start by creating a simple service:

service.py

import dbus
import dbus.service
import dbus.mainloop.glib
from gi.repository import GLibclass HelloWorld(dbus.service.Object):
def __init__(self, conn=None, object_path=None, bus_name=None):
dbus.service.Object.__init__(self, conn, object_path, bus_name)
if __name__ == "__main__":
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SessionBus()
name = dbus.service.BusName("com.example.HelloWorld", bus)
helloworld = HelloWorld(bus, "/HelloWorld")
mainloop = GLib.MainLoop()
mainloop.run()

Let’s break it down line by line:

1–5: imports

7: We declare a subclass of dbus.service.Object. This will define the object we export onto the bus.

8: We define an init function. It doesn’t have to follow this as long as the superclass constructor in line 9 gets the proper arguments, but I think this is probably the cleanest way.

9: We call the superclass constructor. We pass in self , conn (the bus connection), object_path (the object path we want to use, as str), and the bus name we want to export under.

11: It’s an if __name__ == "__main__" block. Python devs should probably know what this is.

12: We tell D-Bus to use a mainloop. This allows the service to listen for requests.

13: We get a connection to the Session Bus (if you want SystemBus instead, just replace SessionBus with SystemBus; everything’s the same).

14: We create a BusName to export our object under (“com.example.HelloWorld”) under the Session Bus.

15: We create the actual service object, passing in the bus connection, the object path and the bus name.

That is basically all you need to create a D-Bus service. Open up D-Feet and you should see something like this:

As you can see, our object shows up under /HelloWorld in com.example.HelloWorld, but it doesn’t have any content in it (besides the default Introspectable interface). Let’s fix that by adding a method to our class:

@dbus.service.method(dbus_interface="com.example.HelloWorldInterface", in_signature="s", out_signature="s", sender_keyword="sender", connection_keyword="conn")
def SayHello(self, name, sender=None, conn=None):
return "Hello " + name

It’s technically only 3 lines, but there’s a lot to break down here. Let’s go line by line again:

1: the @dbus.service.method decorator tells D-Bus that this is a callable method that can be called on the object. This decorator takes several named arguments:

  • dbus_interface is the interface name we publish the method under. For methods, interfaces are merely a way of grouping functionality.
  • in_signature is a string of characters representing the datatypes of the parameters passed in; for a comprehensive guide, see the official D-Bus docs. Each piece of data is passed in as an argument to the method; in this example, we take in 1 string argument, name .
  • out_signature is the same as in_signature, but it represents return datatype instead of parameter datatype.
  • sender_keyword and connection_keyword are optional; basically, sender_keyword=“sender” basically means the ID of the user who called the method will be stored in the sender argument, and connection_keyword means the connection will be stored in conn .

2: Here we actually declare the function. Note that the types have to match the in_signature declared in the decorator. (P.S. In most D-Bus services, methods are in PascalCase; that’s just how it is, don’t ask me why.)

3: We return a result. Note that the type has to match the out_signature declared in the decorator.

Let’s see the result:

The SayHello method shows up under com.example.HelloWorldInterface as expected and takes the input and output expected. Great!

Now, let’s spice things up a bit.

Polkit Authentication

For the uninitiated, PolicyKit (Polkit for short) is the authorization system used by most of the Linux desktop today. Opened your software center and got prompted for a password? That’s polkit. Tried running GParted and you need root privileges? Polkit. Polkit automatically manages showing those nice authorization popups, so it’s less work for the dev and more ease of use for the user.

How do I use Polkit?

This is where the docs start to fall short. There’s not a lot of actual documentation on integrating your own app with Polkit; you could hunt through the docs all day to find out how it works but TL;DR: it’s a D-Bus API (and a C library, but of course we want to do Python, so D-Bus it is).

First, you need to configure a polkit auth level. Write this file to /usr/share/polkit-1/actions :

com.example.HelloWorld.policy

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
<policyconfig>
<vendor>Example</vendor>
<vendor_url>https://example.com/example</vendor_url>
<action id="com.example.HelloWorld.auth">
<description gettext-domain="systemd">Authorization</description>
<message gettext-domain="systemd">Authentication is needed to perform this action.</message>
<defaults>
<!--These describe the auth level needed to do this.
Auth_admin, the current one, requires admin authentication every time.
Auth_admin_keep behaves like sudo, saving the password for a few minutes.
Allow_inactive allows it to be accessed from SSH etc. Allow_active allows it to be accessed from the desktop.
Allow_any is a combo of both.
-->
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
</action>
</policyconfig>

This XML configuration basically defines a polkit privilege called com.example.HelloWorld.auth which requires the user to enter admin credentials to gain authorization.

Of course, you also need to write the code to check privileges. To save time, here’s a convenience function for checking polkit privileges:

Pastebin

Vim screenshot because I couldn’t get it to format

Make sure to add self.dbus_info = None and self.polkit = None to the __init__ function.

Here’s a general summary:

1–8: Get a reference to the DBus object and use it to get the Unix Process ID of the user who called the method (we’ll need this for polkit).

10–15: Get a reference to the Polkit object.

17–32: Use the Polkit object to check authorization. If it times out, try again. The polkit CheckAuthorization method takes these arguments:

  • Subject (aka type — “unix-process” here — and a dictionary of details (pid as UInt32 and start time of 0 here ))
  • Privilege — a string describing the Polkit privilege we defined earlier.
  • Details — details describing the action; for example, you can set a message for the authorization dialog that overrides the default one (here we leave it empty)
  • CheckAuthorizationFlags — aka 0 = no attempt to authenticate user, or 1 = show them an authentication dialog
  • CancellationID — just leave this one empty
  • timeout=seconds until timeout

CheckAuthorization will return a set of values in the format (bool is_authorized, bool is_challenge, dict details). is_authorized is likely the one you want to look at; it tells you whether the user is authenticated or not. We return false if is_auth is false, true otherwise; if we choose, we can also substitute returning true or false for raising an exception when is_auth is false.

Let’s try it out:

Whoops — it gives us an error. Only services running as root or running as an action owner (that we have to specify in the config file) can actually check for authorization, which makes sense given that non-privileged services would have little use for authorization. We could just run the service as root, but because it’s on a SessionBus, the service won’t be accessible to regular users, which ruins the point of authentication. We’ll need to convert this service into a SystemBus service.

Converting to SystemBus

SystemBus services are a little more involved than SessionBus services. Unlike SessionBus services, SystemBus services require a config file to specify the service; if you don’t have it, you’ll get an error. Write this file to /etc/dbus-1/system.d/ :

com.example.HelloWorld.conf

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<type>system</type>
<!-- Only root can own the service -->
<policy user="root">
<allow own="com.example.HelloWorld"/>
<allow send_destination="com.example.HelloWorld"/>
<allow send_interface="com.example.HelloWorld"/>
</policy>
<!-- Allow anyone to invoke methods on the interfaces -->
<policy context="default">
<allow send_destination="com.example.HelloWorld"/>
<allow send_interface="com.example.HelloWorld"/>
</policy>
</busconfig>

Once we have written the XML config file, D-Bus should allow us to run our service with sudo: sudo python3 service.py

Now does it work?

Yes it does.

Congratulations — you’ve just built a D-Bus service with polkit authentication! If you want further D-Bus guidance, check out the official dbus-python docs.

Gotchas — debugging

Polkit can be finicky sometimes; I lost several hours debugging why I wasn’t getting an authorization dialog before I realized I needed to have my authentication agent running first (to be fair, that particular case was more my own fault than polkit’s, but the point stands). Some useful tips for debugging a polkit service:

  • Print out is_challenge. If is_challenge is true, you probably don’t have your authentication agent running; while this shouldn’t be a problem on most major DEs, you may have issues if you configured your own WM setup.
  • Print out details; details can show you things you might have missed, such as an incorrect argument placement.

Credits

A huge thanks to this guy on Ubuntu Forums from 2009 for providing example code; it really helped me figure out how polkit and D-Bus work (the _check_polkit_privilege() function is mostly copied from his code, with some modifications).

Also thanks to the D-Bus and dbus-python official documentation.

--

--