从长时间运行的Python进程以不同用户运行子进程
我有一个长时间运行的Python程序,它会在某些事件发生时使用子进程来启动新的子进程。这个长时间运行的程序是由一个有超级用户权限的用户启动的。我需要它启动的子进程以不同的用户身份运行(比如说“nobody”),同时保持父进程的超级用户权限。
我现在使用的是
su -m nobody -c <program to execute as a child>
但这个方法感觉比较复杂,而且结束时不太干净。
有没有办法通过编程的方式来实现这个,而不是使用su命令?我在看os.set*uid这些方法,但Python标准库在这方面的文档内容比较少。
4 个回答
有一个叫做 os.setuid()
的方法。你可以用它来改变这个脚本当前的用户。
一种解决办法是在子进程开始的地方,调用 os.setuid()
和 os.setgid()
来改变用户和组的身份,然后再调用其中一个 os.exec* 方法来生成一个新的子进程。这个新生成的子进程会以权限较低的用户身份运行,无法再变成权限更高的用户。
另一种方法是在守护进程(主进程)启动时就进行设置,这样所有新生成的进程都会以同样的用户身份运行。
想了解更多信息,可以查看 setuid 的手册页。
从Python 3.9开始,新版本支持 user
和 group
选项,这个功能是直接可以使用的,具体可以查看官方文档。
process = subprocess.Popen(args, user=username)
新版本还提供了一个叫 subprocess.run
的函数。这个函数其实是对 subprocess.Popen
的一个简单封装。subprocess.Popen
是在后台运行命令,而 subprocess.run
则是运行命令后会等到命令执行完毕再继续。
所以我们还可以这样做:
subprocess.run(args, user=username)
因为你提到了守护进程,我可以推测你是在使用类Unix操作系统。这很重要,因为实现方法取决于操作系统的类型。这个回答只适用于Unix,包括Linux和Mac OS X。
- 定义一个函数,用来设置正在运行的进程的组ID(gid)和用户ID(uid)。
- 将这个函数作为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
注意,父进程等待子进程退出只是为了演示目的。我这样做是为了让父进程和子进程可以共享一个终端。守护进程通常没有终端,也很少会等待子进程退出。