Monday, June 4, 2012

Authorization Rules in polkit

For the past couple of weeks, I've been working on rewriting the part of polkit that actually makes the authorization decision: the polkit authority.

Some history

In its current implementation, the so-called polkit local authority ended up being one of these things that never really worked well for the (relatively small subset of) users with a need to configure it. First of all the local authority was never really supposed to be the main polkit authority... back then, we envisioned that in an Enterprise setting, you'd have something like FreeIPA to handle permissions and authorization and this would in turn provide a polkit backend responsible for answering authorization questions from applications (sadly this haven't materialized yet but with this new work it could be we're a lot closer).

Second of all, the local authority had some serious usability problems, in fact so many that I early on re-purposed a bug for the rewrite. I think I basically concluded that it's just too hard to even do simple things and the key-file based format and priority rule scheme for the .pkla file format wasn't really helping. It's also really hard to test .pkla files.

Defining the problem

After thinking about this problem on and off for a while (while working on other things, mostly udisks/GNOME Disks and all the GDBus stuff), I identified the following requirements:

  1. There is no one-size-fits-all - for example some admins want to allow all users in a group to do the action xyz, while other admins want the opposite (forbid users in a group to do the action xyz). Similarly, some admins want black-lists ("allow anything but action xyz") and some want white-lists ("only allow actions xyz, abc, ..."). Some admins even want to have the result depend on the time ("don't allow xyz on school nights").
  2. It would be good to have more information than just the action available when making a decision. For example for mounting or formatting a disk, it would be nice to have the device file or serial number or name and e.g. make the decision depend on this. For connecting to a wireless access point, having access to its ESSID would be great (to only allow connections to ESSID's in a whitelist) and so on.
  3. Some admins may want to use external programs so there should be some facility available for this. Although this can't be the primary interface because forking a process every time someone calls CheckAuthorization() is a recipe for disaster [1].
  4. Ideally, it should be easy test authorization rules - after all, this has to do with security so being able to easily (and automatically) test that your rules Does The Right Thing(tm) would be, uhm, great

A lot of these requirements are actually similar to the requirements you have for udev rules. And a lot of the constraints are similar as well, especially the fork-fest part.

Another thing that's important to identify is who is going to end up using this. The answer here is mostly "Enterprise Admin" although in reality a lot hobbyist end up using it because, well, they like to tinker around. Some users of Linux distributions with broken defaults (see below) may also need to undo the damage done by the distribution so they can actually use their computer. All in all, one can probably assume that the target user here is relatively skilled, ie. can read the provided documentation and is at least capable of copy-pasting some snippet from a website into a file in /etc as root and check that it has an effect.

[1] : History lesson: before we had udevd(8), the kernel forked /sbin/hotplug for every hotplug event. And /sbin/hotplug, being a shell script and all, itself forked another ten shell scripts or so in /etc/hotplug.d/and these forked other shell-scripts and... the result was that hotplugging a USB hub full of devices could easily take minutes because tens of thousands of /bin/sh-instances were forked. Awesometown. Today udevd(8) does the same in less than a second without forking a lot of extraneous processes.

Just embed JavaScript

The first requirement clearly indicates that we want some kind of programming language. However, inventing your own programming language is rarely a good idea so I decided to just embed a JavaScript interpreter (specifically SpiderMonkey) and try that out.

For the second requirement, I just exposed information we already have to the rules engine. I also rewrote the "Writing polkit applications" chapter to mention that mechanisms should use this feature as well as a ton of other advice. [2]

For the third requirement, spawning programs, I added a simple polkit.spawn() method.

The fourth requirement, testing, is fulfilled by just observing that the polkit authorization rules are JavaScript files that the user can test any way they want by trivially mocking the Polkit, Action and Subject types via Duck Typing in their favorite server-side JavaScript environment (gjs, node.js, seed etc.).

After a couple of prototyping attempts, I ended up with something that isn't too awful - see for yourself in the polkit(8) man page in the AUTHORIZATION RULES section. I also ended up adding a number of tests for this, including a test to ensure that even runaway-scripts are terminated. Over all, I'm very satisfied with the result.

[2] : I also included common-sense advice like "don't ask for a root password for adding printer queues" in this section

Wait, isn't embedding a JS interpreter inherently dangerous?

Embedding a JS interpreter is actually perfectly safe. First of all, it all runs inside the polkitd(8) system daemon (which runs without privileges, see below). Second of all, all data passed to the rules are either trusted or from a process that is trusted (except where designated untrusted, e.g. pkexec(1)'s command_line variable). Third, being an interpreted language, we can actually sensibly terminate runaway scripts.

Hmm, OK, but you are bloating Linux anyway. You suck.

The only new dependency here is which in turn depends on the C++ runtime and NSPR (which NSS also depends on and most Linux installs have this library). Note that you also already need a JS interpreter for proxy server auto-configuration. It's also possible (or will be, at least) to just not install polkitd(8) at all (or disable/mask it using systemd) while still having the client-side libraries installed.

Other features

Apart from the new authorization rules engine, I also made the polkitd(8) system daemon run as the unprivileged polkitd user instead of root (much safer for obvious reasons). The main reason why this hadn't been done before had to do with the fact that polkitd(8) was loading backends via an extension system and we didn't really know if some future backend had to run as root. Since I decided nuke the extension system, we no longer need to make such assumptions so changing it was straightforward.

A while ago, I also added the pkttyagent(1) command on request from Lennart for optional use in systemctl(1). Optional here means that it's a soft dependency - systemctl(1) works fine even when polkit is not installed.

Next steps

I've been doing all this work on the wip/js-rule-files branch and today I merged this branch to the master branch. I plan to do a new 0.106 release shortly and put it in what will end up being Fedora 18.

Once that's available, I plan to file bugs against the most important mechanisms (such as NetworkManager) so they can start exporting variables to be used in authorization rules and also properly document this.