将子进程作为不同用户从长时间运行的Python进程运行

时间:2009-11-20 12:39:10

标签: python fork subprocess setuid

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

我正在使用

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

但这似乎很重,并且不会非常干净地死去。

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

4 个答案:

答案 0 :(得分:76)

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

  1. 定义一个函数,用于设置正在运行的进程的gid和uid。
  2. 将此函数作为preexec_fn参数传递给subprocess.Popen
  3. 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
    

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

答案 1 :(得分:11)

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

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

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

有关信息,请查看manpage for setuid

答案 2 :(得分:1)

实际上,preexec_fn的示例对我不起作用。
我的解决方案正常工作,从另一个用户运行一些shell命令并获得其输出:

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

然后,如果你需要从进程stdout中读取:

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

希望,这不仅仅适用于我的情况。

答案 3 :(得分:-6)

在sudo上启用用户,无需密码

username ALL=(ALL) NOPASSWD: ALL

然后使用sudo调用root函数,例如:

import pexpect
child = pexpect.spawn('sudo apachectl restart')
for i in child: print i #if you want to see the output from the process