Reputation: 301
If we open a python interpreter normally and enter the following:
import dbus
bus = dbus.SessionBus()
bus.list_names()
We see all the services on the user's session dbus. Now suppose we wanted to do some root-only things in the same script to determine information to pass through dbus, so we run the interpreter with sudo python
and run the same thing, we only see a short list of items on the root user's session dbus, and attempting to connect to anything that was on the user dbus with get_object
produces a not found error accordingly.
So far I've tried inserting
import os
os.seteuid(int(os.environ['SUDO_UID']))
But this only makes SessionBus()
give a org.freedesktop.DBus.Error.NoReply
so this is probably nonsense. Is there a way to connect to a user's dbus service as a super user, with the python dbus bindings?
Upvotes: 4
Views: 3575
Reputation: 11
Bluehorn's answer helped me. I figured I'd share my solution. I'm only a few months into learning Python (coming from just shell scripting) so if this is really wrong and just happens to work on my system please let me know.
These are parts from a daemon I wrote to monitor CPU temps and control the fan speeds in Linux so it needs root permissions. Not sure how well it would work if ran as a regular user when multiple users are logged in. I'm guessing it wouldn't...
import os, pwd
from dbus import SessionBus, Interface
from dbus.bus import BusConnection
# Subclassing dbus.Interface because why not
class Dbus(Interface):
def __init__(self, uid):
method = 'org.freedesktop.Notifications'
path = '/' + method.replace('.', '/')
if os.getuid() == uid:
obj = SessionBus().get_object( method, path )
else:
os.seteuid(uid)
obj = BusConnection( "unix:path=/run/user/" + str(uid) + "/bus" )
obj.get_object( method, path )
super().__init__(obj, method)
# Did this so my notifications would work
# when running as root or non root
class DbusNotificationHandler:
app_icon = r"/path/to/my/apps/icon.png"
name = "MacFanD"
def __init__(self):
loggedIn, users = [ os.getlogin() ], []
for login in pwd.getpwall():
if login.pw_name in loggedIn:
users.append( login.pw_uid )
self.users = []
for i in users:
self.users.append( Dbus(i) )
def notification(self, msg, mtype=None):
if not isinstance(msg, list) or len(msg) < 2:
raise TypeError("Expecting a list of 2 for 'msg' parameter")
hint = {}
if mtype == 'temp':
icon = 'dialog-warning'
hint['urgency'] = 2
db_id = 498237618
timeout = 0
elif mtype == 'warn':
icon = 'dialog-warning'
hint['urgency'] = 2
db_id = 0
timeout = 5000
else:
icon = self.app_icon
hint['urgency'] = 1
db_id = 0
timeout = 5000
for db in self.users:
db.Notify( self.name, db_id, icon, msg[0], msg[1:], [], hint, timeout )
handler = DbusNotificationHandler()
notify = handler.notification
msg = [ "Daemon Started", "Daemon is now running - %s"%os.getpid() ]
notify(msg)
temp = "95 Celsius"
msg = [ "High Temp Warning", "CPU temperature has reached %s"%temp ]
notify(msg, 'warn')
Upvotes: 1
Reputation: 3091
I have little knowledge about DBus, but that question got me curious.
TL;DR: Use dbus.bus.BusConnection
with the socket address for the target user and seteuid
for gaining access.
First question: What socket does DBus connect to for the session bus?
$ cat list_bus.py
import dbus
print(dbus.SessionBus().list_names())
$ strace -o list_bus.trace python3 list_bus.py
$ grep ^connect list_bus.trace
connect(3, {sa_family=AF_UNIX, sun_path="/run/user/1000/bus"}, 20) = 0
Maybe it relies on environment variables for this? Gotcha!
$ env|grep /run/user/1000/bus
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
Stracing the behaviour from the root account it appears that it does not know the address to connect to. Googling for the variable name got me to the D-Bus Specification, section "Well-known Message Bus Instances".
Second question: Can we connect directly to the socket without having the D-Bus library guess the right address? The dbus-python tutorial states:
For special purposes, you might use a non-default Bus, or a connection which isn’t a Bus at all, using some new API added in dbus-python 0.81.0.
Looking at the changelog, this appears to refer to these:
Bus has a superclass dbus.bus.BusConnection (a connection to a bus daemon, but without the shared-connection semantics or any deprecated API) for the benefit of those wanting to subclass bus daemon connections
Let's try this out:
$ python3
Python 3.9.2 (default, Feb 28 2021, 17:03:44)
>>> from dbus.bus import BusConnection
>>> len(BusConnection("unix:path=/run/user/1000/bus").list_names())
145
How about root access?
# python3
>>> from dbus.bus import BusConnection
>>> len(BusConnection("unix:path=/run/user/1000/bus").list_names())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3/dist-packages/dbus/bus.py", line 124, in __new__
bus = cls._new_for_bus(address_or_type, mainloop=mainloop)
dbus.exceptions.DBusException: org.freedesktop.DBus.Error.NoReply: Did not
receive a reply. Possible causes include: the remote application did not send
a reply, the message bus security policy blocked the reply, the reply timeout
expired, or the network connection was broken.
>>> import os
>>> os.seteuid(1000)
>>> len(BusConnection("unix:path=/run/user/1000/bus").list_names())
143
So this answers the question: Use BusConnection
instead of SessionBus
and specify the address explicitly, combined with seteuid
to gain access.
Still I'd like to know if it is possible
to access the bus directly as root user, without resorting to seteuid
. After
a few search queries, I found a systemd ticket
with this remark:
dbus-daemon is the component enforcing access ... (but you can drop an xml policy file in, to make it so).
This led me to an askubuntu question discussing how to modify the site local session bus policy.
Just to play with it, I ran this in one terminal:
$ cp /usr/share/dbus-1/session.conf session.conf
$ (edit session.conf to modify the include for local customization)
$ diff /usr/share/dbus-1/session.conf session.conf
50c50
< <include ignore_missing="yes">/etc/dbus-1/session-local.conf</include>
---
> <include ignore_missing="yes">session-local.conf</include>
$ cat > session-local.conf
<busconfig>
<policy context="mandatory">
<allow user="root"/>
</policy>
</busconfig>
$ dbus-daemon --config-file session.conf --print-address
unix:abstract=/tmp/dbus-j0r67hLIuh,guid=d100052e45d06f248242109262325b98
$ dbus-daemon --config-file session.conf --print-address
unix:abstract=/tmp/dbus-j0r67hLIuh,guid=d100052e45d06f248242109262325b98
In another terminal, I can not attach to this bus as a root user:
# python3
Python 3.9.2 (default, Feb 28 2021, 17:03:44)
>>> from dbus.bus import BusConnection
>>> address = "unix:abstract=/tmp/dbus-j0r67hLIuh,guid=d100052e45d06f248242109262325b98"
>>> BusConnection(address).list_names()
dbus.Array([dbus.String('org.freedesktop.DBus'), dbus.String(':1.0')], signature=dbus.Signature('s'))
This should also enable accessing all session busses on the system, when installing session-local.conf
globally:
# cp session-local.conf /etc/dbus-1/session-local.conf
# kill -HUP 1865 # reload config of my users session dbus-daemon
# python3
>>> from dbus.bus import BusConnection
>>> len(BusConnection("unix:path=/run/user/1000/bus").list_names())
143
And it works - now root can connect to any session bus without resorting to seteuid
. Don't forget to
# rm /etc/dbus-1/session-local.conf
if your root user does not need this power.
Upvotes: 9
Reputation: 424
You can set DBUS_SESSION_BUS_ADDRESS
environment variable to choose the dbus session you want to connect to.
Incorrect permissions (i.e., missing the seteuid
) causes an immediate NoReply
, and not defining DBUS_SESSION_BUS_ADDRESS
responded with Using X11 for dbus-daemon autolaunch was disabled at compile time, set your DBUS_SESSION_BUS_ADDRESS instead
.
Here's the test code I used:
import os
import dbus
# become user
uid = os.environ["SUDO_UID"]
print(f"I'm {os.geteuid()}, becoming {uid}")
os.seteuid(int(uid))
# set the dbus address
os.environ["DBUS_SESSION_BUS_ADDRESS"] = f"unix:path=/run/user/{uid}/bus"
bus = dbus.SessionBus()
print(bus.list_names())
# I'm 0, becoming 1000
# dbus.Array([dbus.String('org.freedesktop.DBus'), dbus.String('org.fr .. <snip>
Upvotes: 0