How do I use setuid in Python?

1.2k views Asked by At

I read this but I couldn't solve my problem. I don't know how do I use setuid

I have a small python app that runs python commands and bash command in linux machine. I want only this part below to run as normal user. App run with sudo python3 app.py because do some commands that need sudo privilege. I upload only the part I want to run as normal user.

How can I do that?

import alsaaudio

m = alsaaudio.Mixer('Capture')
m.setvolume(10) # set volume
vol = m.getvolume() # get volume float value
1

There are 1 answers

14
jsbueno On

One can't simply change the owner of a process back and forth at will (ok, one can with the proper call: os.seteuid, I learned after starting this answer. Either way, there are more things required for this particular question than just setting the UID - the bottom part of the answer has a subprocess approach): just the root user can do that, and once its done, it is no longer root to switch back. So the trick is to call it in a process that is exclusive to run the limited tasks.

You can fork the process and call os.setuid on the child, and then terminate it:

import alsaaudio
import os
import sys
...

if not os.fork():
    # this block just runs on the child process: fork returns a non-0 PID on the parent
    os.setuid(<usernumber>)
    m = alsaaudio.Mixer('Capture')
    m.setvolume(10) # set volume
    vol = m.getvolume() # get volume float value
    sys.exit(0)  # terminates the child process. 

# normal code that will just run on the parent process (as root)
# continues:
...

Of course, this won't make the vol variable avaliable on the parent process - you have to setup a way to pass the value along.

In that case, one can use Python multiprocessing, instead of fork, with multiprocessing.Queue to send vol value, and add the needed pauses to destroy the other user process: that is good if you are writing "production quality" code that will need to handle a lot of corner cases in third party computers. If your object is a simple script for setting up stuff in your box, writting the value to a file, and reading it on parent will be easier:

import alsaaudio
import os
import sys
import tempfile
import time
import pickle


...

_, file_ = tempfile.mkstemp()

if not os.fork():
    # this block just runs on the child process: fork returns a non-0 PID on the parent
    os.setuid(<usernumber>)
    m = alsaaudio.Mixer('Capture')
    m.setvolume(10) # set volume
    vol = m.getvolume() # get volume float value
    with open(file_, "wb") as file:
         pickle.dump(vol, file)
    sys.exit(0)  # terminates the child process. 

# normal code that will just run on the parent process (as root)
# continues:

time.sleep(0.1)  # may need calibration
vol = pickle.load(open(file_, "rb"))
os.unlink(file_)
...

Given the OP comments, this is not just a matter of changing the effective UID, but also environment variables, and reimporting the alsaaudio module - a "fork" won't cut it (as env vars are not changed, and changing os.environment entries on the Pythonside will probably not be reflected on the native-code side of alsalib as it is initialized.

In this case, running the process with subprocess.Popen and ensuring the correct enviroment prior to it can attain the desired effects. Interprocess comunication can be resolved by capturing the subprocess stdout - so we can refrain from both the pickle.Also, the default subprocess call is synchronous, and no need for an arbitrary pause for the target child to initialize alsa and pick its value: the main process will continue just after the child is done, in the default call.

I am not trying alsaaudio here, it may be that more environment variables than the 2 I set are needed on the subprocess. If that is the case, just go on adding then to the env={ ...} part of the code as needed.

This sample just do what you asked, and pass the value for "vol" back - if you ever need more data, drop the "encoding" argument to the subprocess, and pickle arbitrary data to sys.stdout on the child process - you can then retriev it with pickle.loads on proc.stdout

import alsaaudio
import os
import sys

import tempfile
import time
import pickle

vol = None

def child_code():
    import alsaaudio 
    # no need to reload: when running as a subprocess, the top-level import (if any)
    # will also run as the new user
    m = alsaaudio.Mixer('Capture')
    m.setvolume(10) # set volume
    vol = m.getvolume() # get volume float value
    print(vol)
    
    ...


def parent_code():

    import subprocess
    import pwd
    target_user = 1000
    user_data = os.getpwuid(target_user).pw_dir()
    # if alsalib fails, you may need to setup other variables
    env = {"HOME": user_data, "PWD": user_data, }
    # __file__ bellow may have to be changed if this code
    # is in a submodule in a larger project.
    proc = subprocess.run(
        ["/usr/bin/env", "python3", __file__  ], 
        env=env,
        user=target_user,
        capture_output=True,
        encoding="utf-8"
    )
    vol = literal_eval(proc.stdout)
    return vol
    ...
    
if os.getuid() == 0:
    # runs  on parent, UID==1, process
    vol = parent_code()
else:
    # runs on subprocess.
    child_code()
    sys.exit(0)

...