linuxsudoapplication-serverpython-keyring

How to access files owned by a user in an application


Context

Imagine I have a application with a Python server running as user 'python-app' on Linux and a Python client also running as user 'python-app'. The user 'python-app' does not have root permissions. Thus, the Python application cannot open files it does not have access to.

The purpose of the application is to allow users to perform some task on a file they own.

Another user 'user' (who exists on the Linux host running the application) accesses the application and authenticates themselves somehow (e.g., OAuth2, username/password, whatever, not relevant for this question). That user successfully authenticates.

Now, I need the application (running as user 'python-app') to perform actions on behalf of the user 'user' using the 'user' permission set.

Question

How do I access files as the application on behalf of the user without doing sudo -u 'user' <command> and potentially having to re-enter credentials every time?

What I would love is:

Is this not possible? I feel like I am missing something and I just don't know what. I've reviewed other enterprise applications that do this, such as RStudio Server (relevant source), but I don't conceptually understand what is happening.


Solution

  • I figured out a solution. It requires that some aspect of the application run as root with sudo. The solution here is to:

    1. Run a "master service" as root (elevated privileges)
      • sudo service
    2. User accesses the "master service" from some client
    3. User authenticates (via Oauth2, PAM, whatever) themselves with "master service" via the client
    4. Upon successful authentication, the "master service":
      1. Creates a child process
      2. De-escalates or demotes privileges of the child process by:
        1. Setting the child process's user id (uid)
        2. Setting the child process's group ids to all relevant group ids (gid) for that user
      3. (Optionally) Redirects client or UI to this new child process
      4. Detaches from the child process

    A minimally working example.

    import os
    import subprocess
    import pam
    import pwd
    import grp
    from getpass import getpass
    
    def authenticate(user, pwd):
        return pam.authenticate(user, pwd, service='common-auth')
    
    def report_ids(msg):
        print(msg)
        print('uid', os.getuid())
        print('gids', os.getgroups())
    
    def spawn_process(user):
        pw_record = pwd.getpwnam(user)
        def demote():
            def result():
                report_ids('starting demotion')
    
                # set multiple group ids instead of just one
                # os.setgid(pw_record.pw_uid)
                groups = [g.gr_gid for g in grp.getgrall() if user in g.gr_mem]
                gid = pwd.getpwnam(user).pw_gid
                groups.append(grp.getgrgid(gid).gr_gid)
                try:
                    groups.remove(27) # sudo group
                except ValueError:
                    pass # Silently ignore if element not found
                os.setgroups(groups)
    
                os.setuid(pw_record.pw_gid)
                report_ids('finished demotion')
            return result
    
            # Some generic bash script with a different owner and group id
            # $ ls -l process.sh
            # -rwxrwx--- 1 user  test 28 Jan  8 15:17 process.sh
            # $ cat process.sh
            # #!/bin/bash
            # ls /home/other-user
            subprocess.Popen(["./process.sh"], preexec_fn=demote())
    
    
    
    if __name__ == "__main__":
        user = input("Username: ")
        passwd = getpass("Password: ")
        if authenticate(user, passwd) is False:
            print("Failed to authenticate user")
            exit(1)
    
        # Launch process as another user
        spawn_process(user)
        exit(0)
    

    Resources:

    I would be curious to know if others find any problems with this approach