54

我有一个长时间运行的守护进程 Python 进程,当某些事件发生时,它使用子进程生成新的子进程。长时间运行的进程由具有超级用户权限的用户启动。我需要它产生的子进程作为不同的用户(例如“nobody”)运行,同时保留父进程的超级用户权限。

我目前正在使用

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

但这似乎是重量级的,并且不会很干净地死去。

有没有办法以编程方式完成此操作而不是使用 su?我正在查看 os.set*uid 方法,但 Python std lib 中的文档在该区域非常稀疏。

4

5 回答 5

92

既然你提到了一个守护进程,我可以断定你正在一个类 Unix 操作系统上运行。这很重要,因为如何做到这一点取决于操作系统的种类。此答案仅适用Unix 包括 Linux 和 Mac OS X。

  1. 定义一个函数来设置正在运行的进程的 gid 和 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

当子进程退出时,我们回到父进程的根...

(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

请注意,让父进程等待子进程退出仅用于演示目的。我这样做是为了让父母和孩子可以共享一个终端。守护进程没有终端,并且很少等待子进程退出。

于 2011-05-17T21:41:18.973 回答
12

有一种os.setuid()方法。您可以使用它来更改此脚本的当前用户。

一种解决方案是,在孩子开始的地方,调用os.setuid()os.setgid()更改用户和组 ID,然后调用os.exec*方法之一来生成一个新的孩子。新生成的孩子将与较弱的用户一起运行,而无法再次成为更强大的用户。

另一种方法是在守护进程(主进程)启动时执行此操作,然后所有新生成的进程都将在同一用户下运行。

有关信息,请查看setuid 的手册页

于 2009-11-20T12:49:01.603 回答
3

实际上, preexec_fn 的示例对我不起作用。
我的解决方案可以很好地从另一个用户运行一些 shell 命令并获取其输出:

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

然后,如果您需要从进程标准输出中读取:

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

希望,它不仅对我有用。

于 2016-06-17T21:16:23.830 回答
2

新版本的 Python(3.9 及更高版本)支持user开箱即用group选项:

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

新版本还提供了一个subprocess.run功能。它是一个简单的包装器subprocess.Popen。在suprocess.Popen后台运行命令时,subprocess.run运行命令等待它们完成。

因此我们也可以这样做:

subprocess.run(args, user=username)
于 2021-10-13T17:52:21.020 回答
-8

在 sudo 上启用用户不需要密码

username ALL=(ALL) NOPASSWD: ALL

然后用 调用根函数sudo,例如:

import pexpect
child = pexpect.spawn('sudo apachectl restart')
for i in child: print i #if you want to see the output from the process
于 2014-04-14T07:44:30.777 回答