views:

121

answers:

5

I have a Python script that will be doing a lot of things that would require root-level privileges, such as moving files in /etc, installing with apt-get, and so on. I currently have:

if os.geteuid() != 0:
    exit("You need to have root privileges to run this script.\nPlease try again, this time using 'sudo'. Exiting.")

Is this the best way to do the check? Are there other best practices?

+4  A: 

os.geteuid gets the effective user id, which is exactly what you want, so I can't think of any better way to perform such a check. The one bit that's uncertain is that "root-like' in the title: your code checks for exactly root, no "like" about it, and indeed I wouldn't know what "root-like but not root" would mean -- so, if you mean something different than "exactly root", perhaps you can clarify, thanks!

Alex Martelli
I am assuming Paul is worried about different systems using different uids for administrators. Normally id(root) == 0, but it is not a must and on some systems it's really not equal.
Stan
@Stan: then EAFP would indeed be better
msw
The assumption that effective UID == 0 means "root" is pretty deeply ingrained into UNIX code (including throughout most UNIX-like kernel sources).Technically under Linux that assumption is NOT necessarily correct. The Linux "capabilities" model allows the system to run with more fine-grained controls delegated via process inheritance (lcap2 wrappers) or potentially through extended filesystem attributes. Also SELinux features could play havoc with such assumptions.For 99% of the systems out there, geteuid() == 0 is sufficient; for the rest try: ... except: is your friend.
Jim Dennis
+8  A: 

Under the "Easier to Ask Forgiveness than Permission" principle:

try:
    os.rename('/etc/foo', '/etc/bar')
except IOError as e:
    if (e[0] == errno.EPERM):
       print >> sys.stderr, "You need root permissions to do this, laterz!"
       sys.exit(1)

If you are concerned about the non-portability of os.geteuid() you probably shouldn't be mucking with /etc anyway.

msw
This is the most portable way and even works under a bit more exotic circumstances, like with SELinux limiting root privileges.
Florian Diesch
This may actually be worse depending on the situation. Hmm but Florian has a point.
Longpoke
@Longpoke - are you referring to the situation where a user may have rename permissions in /etc but - say lack the ability to remove a file from that path? If that is what you mean, then trying to determine what capabilities the user does have in advance is a fool's errand. Better to structure your code for "transactional" semantics where A and B can be rolled-back if C fails.
msw
@msw: I initially thought, "you should just check if you're root or not and exit", to avoid any transactional consequence, but SELinux, ACLs, and capabilities definately break that, so I guess the only way to really do it right is what you suggested.
Longpoke
Using try/except is fine for os.rename, but isn't flexible enough for the other thing I mentioned, "installing with apt-get". I fully agree that euid could fail on permissions-based systems, which is why I asked the question in the first place. However, I don't think that try/except covers all the issues either.
Paul Hoffman
Agreed about apt-get, see below.
msw
I can only mark one answer as "the correct one", but your addition below (and other riffs on it that I can think of) nail it. Yes, I should be checking for errno.EPERM on each action.
Paul Hoffman
A: 

It all depends how portable you want you app to be. If you mean bussiness, the we have to assume that administrator account does not always equal 0. This means that checking for euid 0 is not enough. Problem is, there are situations where one command will behave as if you are root and next will fail with permission denied (think SELinux & co.). Therefore it's really better to fail gracefully and check for EPERM errno whenever it's appropriate.

Stan
+2  A: 

If you really want your code to be robust across a wide variety of Linux configurations I'd suggest that you consider the corner cases where someone may be using SELinux, or filesystem ACLs, or the "capabilities" features that have been in the Linux kernel since v. 2.2 or so. Your process might be running under some wrapper that has used SELinux, or some Linux capabilities library, such as libcap2 libcap-ng, or fscaps or elfcap via something more exotic like Niels Provos' wonderful and sadly under-appreciated systrace system.

All of these are ways that you code might be running as non-root and yet your process might have been delegated the necessary access to perform its work without EUID==0.

So I'd suggest that you consider writing your code more Pythonically, by wrapping operations that may fail due to permissions or other issues with exception handling code. If you're shelling out to perform various operations (e.g. using the subprocess module) you might offer to prefix all such calls with sudo (as a command line, environment, or .rc file option, for example). If it's being run interactively you can offer to re-execute any commands that raise permissions related exceptions using sudo (optionally only if you find sudo on the os.environ['PATH']).

Overall it's true that most Linux and UNIX systems still have most of their administration done by a 'root' privileged user. However, it's old school and we, as programmers, should try to support newer models. Trying your operations and letting the exception handling do its job allows your code to work under any system that transparently permits the operations you need, and being aware of and ready to use sudo is a nice touch (as it is, by far, the most widespread tool for controlled delegation of system privileges).

Jim Dennis
+1  A: 

Answer to the second part of the question

(sorry the comment box was too small)

Paul Hoffman, you are correct, I only addressed one part of your question dealing with intrinsics, but it wouldn't be a worthy scripting language if it couldn't handle apt-get. The preferred library is a tad verbose but it does the job:

>>> apt_get = ['/usr/bin/apt-get', 'install', 'python']
>>> p = subprocess.Popen(apt_get, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
>>> p.wait()
100                 # Houston, we have a problem.
>>> p.stderr.read()
'E: Could not open lock file /var/lib/dpkg/lock - open (13: Permission denied)'
'E: Unable to lock the administration directory (/var/lib/dpkg/), are you root?\n'

But Popen is a generalized tool and can be wrapped for convenience:

$ cat apt.py
import errno
import subprocess

def get_install(package):
    cmd = '/usr/bin/apt-get install'.split()
    cmd.append(package)
    output_kw = {'stdout': subprocess.PIPE, 'stderr': subprocess.PIPE}
    p = subprocess.Popen(cmd, **output_kw)
    status = p.wait()
    error = p.stderr.read().lower()
    if status and 'permission denied' in error:
        raise OSError(errno.EACCES, 'Permission denied running apt-get')
    # other conditions here as you require
$ python
>>> import apt
>>> apt.get_install('python')
Traceback ...
OSError: [Errno 13] Permission denied running apt-get

And now we're back to exception handling. I will decline to comment on the Java-like over-generality of the subprocess module.

msw