""" Open a file via sudo and unix socket If you only need root privileges to open one file, then you can spawn a helper program with sudo to open the file, then pass the file descriptor back to your original process via a Unix socket. This module imports a drop-in replacement for os.open (except for the dir_fd argument) based on this idea: stephen at wrath in ~/repos/opener $ ls -l secret.txt -rw------- 1 root root 24 Aug 15 21:11 secret.txt stephen at wrath in ~/repos/opener $ python Python 3.11.3 (main, Jun 5 2023, 09:32:32) [GCC 13.1.1 20230429] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import os, opener >>> fd = opener.open_via_sudo("secret.txt") [sudo] password for stephen: >>> os.fdopen(fd, "rt").read() os.fdopen(fd, "rt").read() 'the very secret contents' >>> There's some funny business I have yet to unwind with the terminal settings. Sudo seems to change a number of settings around, and I had to run "stty sane" after invoking sudo. It doesn't completely resolve the issue: see the extra copy of the line reading "os.fdopen(...)"? That shouldn't be there... """ import argparse import array import os import socket import subprocess import sys import tempfile from pathlib import Path from typing import Union def recv_fd(sock: socket.socket) -> int: """Receive a file descripter on the socket""" # Borrows heavily from the Python docs for sock.recvmsg() fds = array.array("i") msg, ancdata, flags, addr = sock.recvmsg(4096, socket.CMSG_SPACE(fds.itemsize)) if msg == b"success": level, typ, data = ancdata[0] assert level == socket.SOL_SOCKET assert typ == socket.SCM_RIGHTS data = data[:fds.itemsize] fds.frombytes(data) return fds[0] raise Exception(msg.decode()) def send_fd(sock: socket.socket, fd: int) -> None: """Send a file descripter on the socket""" # Borrows heavily from the Python docs for sock.sendmsg() fds = array.array("i", [fd]) sock.sendmsg( [b"success"], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fds)], ) def open_via_sudo( path: Union[Path, str], flags: int = os.O_RDONLY, mode: int = 0o777, ) -> int: """Implements os.open() using sudo to get permissions""" # Currently does not support dir_fd argument path = str(path) with tempfile.TemporaryDirectory() as td: sockpath = Path(td) / "sock" sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) sock.bind(str(sockpath)) try: proc = subprocess.Popen( [ "sudo", sys.executable, "-B", __file__, str(sockpath), path, str(flags), str(mode), ], ) # TODO: handle errors / timeout / wait return recv_fd(sock) finally: # sudo tends to mess up the terminal, get it into a sane state subprocess.run(["stty", "sane"]) def main(): parser = argparse.ArgumentParser() parser.add_argument( "socket", type=Path, help="path to unix domain socket", ) parser.add_argument( "file", type=Path, help="filename to open", ) parser.add_argument( "flags", type=int, help="numeric flags", ) parser.add_argument( "mode", type=int, help="numeric mode", ) args = parser.parse_args() fd = os.open(args.file, args.flags, args.mode) sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) sock.connect(str(args.socket)) send_fd(sock, fd) if __name__ == "__main__": main()