从长时间运行的Python进程以不同用户运行子进程

56 投票
4 回答
70189 浏览
提问于 2025-04-15 16:12

我有一个长时间运行的Python程序,它会在某些事件发生时使用子进程来启动新的子进程。这个长时间运行的程序是由一个有超级用户权限的用户启动的。我需要它启动的子进程以不同的用户身份运行(比如说“nobody”),同时保持父进程的超级用户权限。

我现在使用的是

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

但这个方法感觉比较复杂,而且结束时不太干净。

有没有办法通过编程的方式来实现这个,而不是使用su命令?我在看os.set*uid这些方法,但Python标准库在这方面的文档内容比较少。

4 个回答

12

有一个叫做 os.setuid() 的方法。你可以用它来改变这个脚本当前的用户。

一种解决办法是在子进程开始的地方,调用 os.setuid()os.setgid() 来改变用户和组的身份,然后再调用其中一个 os.exec* 方法来生成一个新的子进程。这个新生成的子进程会以权限较低的用户身份运行,无法再变成权限更高的用户。

另一种方法是在守护进程(主进程)启动时就进行设置,这样所有新生成的进程都会以同样的用户身份运行。

想了解更多信息,可以查看 setuid 的手册页

17

从Python 3.9开始,新版本支持 usergroup 选项,这个功能是直接可以使用的,具体可以查看官方文档

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

新版本还提供了一个叫 subprocess.run 的函数。这个函数其实是对 subprocess.Popen 的一个简单封装。subprocess.Popen 是在后台运行命令,而 subprocess.run 则是运行命令后会等到命令执行完毕再继续。

所以我们还可以这样做:

subprocess.run(args, user=username)
102

因为你提到了守护进程,我可以推测你是在使用类Unix操作系统。这很重要,因为实现方法取决于操作系统的类型。这个回答只适用于Unix,包括Linux和Mac OS X。

  1. 定义一个函数,用来设置正在运行的进程的组ID(gid)和用户ID(uid)。
  2. 将这个函数作为preexec_fn参数传递给subprocess.Popen。

subprocess.Popen会使用fork/exec模型来调用你的preexec_fn。这就相当于依次调用os.fork()、preexec_fn()(在子进程中)和os.exec()(在子进程中)。因为os.setuid、os.setgid和preexec_fn这些功能只在Unix上支持,所以这个方法不能在其他类型的操作系统上使用。

下面的代码是一个脚本(Python 2.4及以上版本),演示了如何做到这一点:

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()

你可以这样调用这个脚本:

以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

在子进程中变成非root身份...

(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

当子进程退出时,父进程会回到root身份...

(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

注意,父进程等待子进程退出只是为了演示目的。我这样做是为了让父进程和子进程可以共享一个终端。守护进程通常没有终端,也很少会等待子进程退出。

撰写回答