修改python进程的uid和gid

1. uid和gid

在linux系统下,对于用户的权限的管理,是通过uid来识别和鉴权的。你登录时用的用户名,在系统里有一个uid与之相对应,不同的用户名可以对应到同一个uid,反之则不可以,gid也是如此,他们都是一串数字。

系统并不认我们的用户名和组名,它只认uid和gid,你可以通过修改password文件实现两个不同的用户拥有相同的uid,不过这样做除了会造成一些混乱外,没有什么实际意义。

2.setuid, setgid

本文想要讨论的是,如何修改python进程的uid和gid。

在研究如何修改进程的uid之前,先考虑一下修改进程uid是否有实践意义。如果你是root用户,执行的程序是某个普通用户的程序,那么这个程序在执行期间就拥有了root用户的权限,这是一个危险的事情,如果能够将进程的uid,gid修改为普通用户,那么就可以消除这种危险。

我现在root用户下新建一个1.txt文件,这个文件,hadoop用户是无权修改的,接下来,我要编写一个脚本,由root用户启动,但是在启动后,将进程的uid修改为hadoop的uid,脚本里尝试修改1.txt文件,如果所有操作正常,文件将无法正常打开。

import os
import grp
import pwd


def change_user(username):
    user = pwd.getpwnam(username)
    uid = user.pw_uid       # uid
    gid = user.pw_gid       # gid
    home = user.pw_dir      # 用户home目录
    gids = [g.gr_gid for g in grp.getgrall() if username in g.gr_mem]       # 用户组

    os.setgid(gid)
    try:
        os.setgroups(gids)
    except Exception as e:
        print('Failed to set groups %s' % e)

    os.setuid(uid)
    #os.chdir(home)


change_user('hadoop')
with open('1.txt', 'w')as f:
    f.write('bbb')

程序果然报错

PermissionError: [Errno 13] Permission denied: '1.txt'

3. jupyterhub管理子进程权限

上面的例子,需要普通用户的程序配合才行,似乎实用性不强,那么,咱们来看jupyterhub这个实用性比较强的例子。

jupyterhub实现了多租户管理, 可以通过进程的方式启动notebook,每个hub的用户在机器上有一个同名的系统用户。

hub采用的是PAM认证方法来管理用户登录,
notebook是hub启动的,而hub是root账户启动的,在notebook启动时,必须修改notebook进程的uid,否则notebook进程将拥有同root一样的权限。

这意味着,当一个用户,在notebook上编写代码时,自然的拥有了root账户的权限,这是十分危险的。

为了控制notebook进程的权限,jupyterhub所采用的就是修改进程uid的方法,不同于我上面所举的例子,notebook进程是hub的子进程,那么hub是如何设置子进程的uid的呢?

self.proc = Popen(cmd, **popen_kwargs)

hub使用subprocess模块的Popen方法来启动notebook子进程,秘密就藏在参数popen_kwargs里

popen_kwargs = dict(
            preexec_fn=self.make_preexec_fn(self.user.name),
            start_new_session=True,  # don't forward signals
        )

preexec_fn参数的值是一个函数,在子进程启动后执行,hub就是在这里修改了子进程的uid

def make_preexec_fn(self, name):
    return set_user_setuid(name)
    
def _try_setcwd(path):
    while path != '/':
        try:
            os.chdir(path)
        except OSError as e:
            exc = e  # break exception instance out of except scope
            print("Couldn't set CWD to %s (%s)" % (path, e), file=sys.stderr)
            path, _ = os.path.split(path)
        else:
            return
    print("Couldn't set CWD at all (%s), using temp dir" % exc, file=sys.stderr)
    td = mkdtemp()
    os.chdir(td)


def set_user_setuid(username, chdir=True):
    import grp
    import pwd
    
    user = pwd.getpwnam(username)
    uid = user.pw_uid
    gid = user.pw_gid
    home = user.pw_dir
    gids = [g.gr_gid for g in grp.getgrall() if username in g.gr_mem]
    
    def preexec():
        """Set uid/gid of current process
    
        Executed after fork but before exec by python.
    
        Also try to chdir to the user's home directory.
        """
        os.setgid(gid)
        try:
            os.setgroups(gids)
        except Exception as e:
            print('Failed to set groups %s' % e, file=sys.stderr)
        os.setuid(uid)
    
        # start in the user's home dir
        if chdir:
            _try_setcwd(home)
    
    return preexec

功能的核心实现在set_user_setuid里,这个函数返回preexec,作为preexec_fn的实参,在子进程创建以后执行,这样就改变了notebook子进程的权限,从root用户变为普通用户。

扫描关注, 与我技术互动

QQ交流群: 211426309

加入知识星球, 每天收获更多精彩内容

分享日常研究的python技术和遇到的问题及解决方案