Run child processes as different user from a long running Python process
Asked Answered
J

4

56

I've got a long running, daemonized Python process that uses subprocess to spawn new child processes when certain events occur. The long running process is started by a user with super user privileges. I need the child processes it spawns to run as a different user (e.g., "nobody") while retaining the super user privileges for the parent process.

I'm currently using

su -m nobody -c <program to execute as a child>

but this seems heavyweight and doesn't die very cleanly.

Is there a way to accomplish this programmatically instead of using su? I'm looking at the os.set*uid methods, but the doc in the Python std lib is quite sparse in that area.

Junie answered 20/11, 2009 at 12:39 Comment(0)
P
102

Since you mentioned a daemon, I can conclude that you are running on a Unix-like operating system. This matters, because how to do this depends on the kind operating system. This answer applies only to Unix, including Linux, and Mac OS X.

  1. Define a function that will set the gid and uid of the running process.
  2. Pass this function as the preexec_fn parameter to subprocess.Popen

subprocess.Popen will use the fork/exec model to use your preexec_fn. That is equivalent to calling os.fork(), preexec_fn() (in the child process), and os.exec() (in the child process) in that order. Since os.setuid, os.setgid, and preexec_fn are all only supported on Unix, this solution is not portable to other kinds of operating systems.

The following code is a script (Python 2.4+) that demonstrates how to do this:

import os
import pwd
import subprocess
import sys


def main(my_args=None):
    if my_args is None: my_args = sys.argv[1:]
    user_name, cwd = my_args[:2]
    args = my_args[2:]
    pw_record = pwd.getpwnam(user_name)
    user_name      = pw_record.pw_name
    user_home_dir  = pw_record.pw_dir
    user_uid       = pw_record.pw_uid
    user_gid       = pw_record.pw_gid
    env = os.environ.copy()
    env[ 'HOME'     ]  = user_home_dir
    env[ 'LOGNAME'  ]  = user_name
    env[ 'PWD'      ]  = cwd
    env[ 'USER'     ]  = user_name
    report_ids('starting ' + str(args))
    process = subprocess.Popen(
        args, preexec_fn=demote(user_uid, user_gid), cwd=cwd, env=env
    )
    result = process.wait()
    report_ids('finished ' + str(args))
    print 'result', result


def demote(user_uid, user_gid):
    def result():
        report_ids('starting demotion')
        os.setgid(user_gid)
        os.setuid(user_uid)
        report_ids('finished demotion')
    return result


def report_ids(msg):
    print 'uid, gid = %d, %d; %s' % (os.getuid(), os.getgid(), msg)


if __name__ == '__main__':
    main()

You can invoke this script like this:

Start as root...

(hale)/tmp/demo$ sudo bash --norc
(root)/tmp/demo$ ls -l
total 8
drwxr-xr-x  2 hale  wheel    68 May 17 16:26 inner
-rw-r--r--  1 hale  staff  1836 May 17 15:25 test-child.py

Become non-root in a child process...

(root)/tmp/demo$ python test-child.py hale inner /bin/bash --norc
uid, gid = 0, 0; starting ['/bin/bash', '--norc']
uid, gid = 0, 0; starting demotion
uid, gid = 501, 20; finished demotion
(hale)/tmp/demo/inner$ pwd
/tmp/demo/inner
(hale)/tmp/demo/inner$ whoami
hale

When the child process exits, we go back to root in parent ...

(hale)/tmp/demo/inner$ exit
exit
uid, gid = 0, 0; finished ['/bin/bash', '--norc']
result 0
(root)/tmp/demo$ pwd
/tmp/demo
(root)/tmp/demo$ whoami
root

Note that having the parent process wait around for the child process to exit is for demonstration purposes only. I did this so that the parent and child could share a terminal. A daemon would have no terminal and would seldom wait around for a child process to exit.

Previse answered 17/5, 2011 at 21:41 Comment(5)
"seldom wait around for a child process to exit" may lead to many zombie processes (long-lived parent, short-lived child processes).Discommodity
It's probably obvious, (wasn't for me), but ... you should change the gid first as shown in the example!Cinda
Note the above example only uses the user's primary gid if you want to use ALL of the user's groups then you can use os.initgroups(user_name, user_gid) instead of os.setgid. This requires passing the username into demote()Tartuffery
Is that possible to still run the python script under my own account, switch to root within the process, and then call the command as another user?Boles
I feel compelled to reiterate @Cinda comment. If you get exception: SubprocessError Exception occurred in preexec_fn, make sure that in the def result() function os.setgid(user_gid) comes before os.setuid(user_uid). On Fedora running Python 3.7.7, order didn't matter (i.e. it worked either way); but on Ubuntu Bionic running Python 3.7.7, it mattered! Strange but true. So be safe and just use the order above. Hope that helps other searchers.Glaser
C
17

The new versions of Python (3.9 onwards) support user and group option out of the box:

process = subprocess.Popen(args, user=username)

The new versions also provide a subprocess.run function. It is a simple wrapper around subprocess.Popen. While suprocess.Popen runs the commands in the background, subprocess.run runs the commands and wait for their completion.

Thus we can also do:

subprocess.run(args, user=username)
Codicil answered 13/10, 2021 at 17:52 Comment(1)
Note that this solution is POSIX only. (for those of us looking for windows solutions)Tobietobin
C
12

There is an os.setuid() method. You can use it to change the current user for this script.

One solution is, somewhere where the child starts, to call os.setuid() and os.setgid() to change the user and group id and after that call one of the os.exec* methods to spawn a new child. The newly spawned child will run with the less powerful user without the ability to become a more powerful one again.

Another is to do it when the daemon (the master process) starts and then all newly spawned processes will have run under the same user.

For information look at the manpage for setuid.

Cellulous answered 20/11, 2009 at 12:49 Comment(6)
You may also need os.setgroups() if you're switching to a user with additional gids. Other than that, yeah, it's pretty straightforward.Avitzur
Followup: The processes intended to run as nobody are untrusted, third-party apps. I can't rely on them switching to another uid/gid. I also can't switch the daemon process to another uid/gid permanently when it starts because it still needs super user privileges for some ops other than launching these child processes. Is the following possible? 1. Start the daemon as a super user. 2. When the daemon is about to launch a child, drop to the nobody user. Ensure that the child can't become a super user again. 3. After launching the child, switch the daemon back to super user privileges.Junie
No. Once you become a less powerful user there is no way back. I've edited the post above that should work for you - look at the first option.Cellulous
The subprocess.Popen function has a preexec_func parameter which can be used to accomplish the double child spawn suggested by Emil. The preexec_func can call os.setgid and os.setuid in the context of the first launched child which will then launch the second as that user.Junie
This is not true on Linux. Set the effective UID and then set it back to the real user ID when you are done.Cooney
I've done this from a daemon using multiprocessing module to run a python function as a child process before calling subprocess.Popen(). Look at the multiprocessing example here: #2582317. Within the target() function, add your calls to os.setgid() & os.setuid().Springbok
B
3

Actually, example with preexec_fn did not work for me.
My solution that is working fine to run some shell command from another user and get its output is:

apipe=subprocess.Popen('sudo -u someuser /execution',shell=True,stdout=subprocess.PIPE)

Then, if you need to read from the process stdout:

cond=True
while (cond):
  line=apipe.stdout.getline()
  if (....):
    cond=False

Hope, it is useful not only in my case.

Brink answered 17/6, 2016 at 21:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.