Managing the Authorization Database with Munki

Have you ever wished you didn’t have to take calls from your users to unlock various parts of System Preferences? That standard users could unlock Energy Saver or Date and Time preferences? Well dear reader, this is the article for you.

If, for some strange reason you can’t be bothered to read this overly long article (I do love to procrastinate), you can head over to my macscripts repo on GitHub for the scripts and resulting pkginfo files I’ve made for this.

Before we start, let’s get one thing out of the way - Munki isn’t at heart a configuration management system. I’ve traditionally preferred Puppet for these tasks, but as there is at the time of writing a bug open on modifying this with Puppet, I took it upon myself to make this work in my environment. I spent a couple of days trying to get my sub-par Ruby skills to match my aspirations, so I moved onto a much more comfortable technology for me: Python and Munki.

To tackle this issue, I’m going to be using the same Philosophy as Puppet:

  • Check if the resource exists and what it’s current value is
  • If required, change the value
  • And be able to revert back to how things were

These translate quite nicely into installcheck_script, postinstall_script and uninstall_script rolled into a nopkg pkginfo (for a good intro into how nopkg pkginfos work, see how to manage printers with them over on the Munki wiki). We could do this with a payload free package and an installcheck_script just as easily, but as we’re already putting code into our pkginfo, we might as well keep it all in one place.

This isn’t intended to be a tutorial on the theory of OS X’s authorization database - there are already excellent resources available.

installcheck_script

Our installcheck_script is going to be very basic. To first open up the root system.preferences right, we just need to make sure that the group is set to everyone rather than admin. If you want to use another group, just substitute it in the group variable in the installcheck_script and the postinstall_script.

#!/usr/bin/env python

import subprocess
import sys
import plistlib

# Group System Preferences should be opened to
group = 'everyone'

command = ['/usr/bin/security', 'authorizationdb', 'read', 'system.preferences']

task = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = task.communicate()

formatted = plistlib.readPlistFromString(out)
# if group matches, exit 1 as we don't need to install
if formatted['group'] == group:
    sys.exit(1)
else:
    # if it doesn't we're exiting with 0 as we need to perform the install
    sys.exit(0)

postinstall_script

The postinstall_script is just an extension of the installcheck_script - but we’re going to make use of Python’s built-in plistlib to modify the plist and feed it back into security authorizationdb to set our desired settings.

#!/usr/bin/env python

import subprocess
import sys
import plistlib

# Group System Preferences should be opened to
group = 'everyone'

command = ['/usr/bin/security', 'authorizationdb', 'read', 'system.preferences']

task = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = task.communicate()
formatted = plistlib.readPlistFromString(out)

# If the group doesn't match, we're going to correct it.
if formatted['group'] != group:
    #input_plist = {}
    formatted['group'] = group
    # Convert back to plist
    input_plist = plistlib.writePlistToString(formatted)
    # Write the plist back to the authorizationdb
    command = ['/usr/bin/security', 'authorizationdb', 'write', 'system.preferences']
    task = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    (out, err) = task.communicate(input=input_plist)

uninstall_script

We should be good admins and clean up after ourselves, so we’ll include an uninstall script.

#!/usr/bin/env python

import subprocess
import sys
import plistlib

# Set the group back to admin
group = 'admin'

command = ['/usr/bin/security', 'authorizationdb', 'read', 'system.preferences']

task = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = task.communicate()
formatted = plistlib.readPlistFromString(out)

# If the group doesn't match, we're going to correct it.
if formatted['group'] != group:
    formatted['group'] = group
    # Convert back to plist
    input_plist = plistlib.writePlistToString(formatted)
    # Write the plist back to the authorizationdb
    command = ['/usr/bin/security', 'authorizationdb', 'write', 'system.preferences']
    task = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    (out, err) = task.communicate(input=input_plist)

Getting it into Munki

Now we’ve got our three scripts, we need to get them together into a pkginfo file. Assuming the scripts you’ve just made live in ~/src/macscripts/Munki/Auth:

$ cd ~/src/macscripts/Munki/Auth
$ /usr/local/munki/makepkginfo --installcheck_script=installcheck.py --postinstall_script=postinstall.py --uninstall_script=uninstall.py > OpenSysPrefs-1.0.plist

Which will produce the bare bones of a pkginfo file, but there are a few other things we need to add into it. Modify OpenSysPref-1.0.plist to look like the below. For further documentation on what we’re doing here, have a look at the Munki wiki. The important parts you’ll need to add / modify are:

  • autoremove
  • catalog
  • description
  • display_name
  • name
  • installer_type
  • minimum_os_version
  • version
  • unattended_install (if you want it to apply in the background)
  • uninstall_method
  • uninstallable
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>autoremove</key>
    <false/>
    <key>catalogs</key>
    <array>
        <string>production</string>
    </array>
    <key>description</key>
    <string>Opens System Preferences to Everyone</string>
    <key>display_name</key>
    <string>Open System Preferences</string>
    <key>name</key>
    <string>OpenSysPrefs</string>
    <key>installer_type</key>
    <string>nopkg</string>
    <key>minimum_os_version</key>
    <string>10.8.0</string>
    <key>unattended_install</key>
    <true/>
    <key>version</key>
    <string>1.0</string>
	<key>installcheck_script</key>
	<string>#!/usr/bin/env python

import subprocess
import sys
import plistlib

# Group System Preferences should be opened to
group = 'everyone'

command = ['/usr/bin/security', 'authorizationdb', 'read', 'system.preferences']

task = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = task.communicate()

formatted = plistlib.readPlistFromString(out)

# if group matches, exit 1 as we don't need to install
if formatted['group'] == group:
    sys.exit(1)
else:
    # if it doesn't we're exiting with 0 as we need to perform the install
    sys.exit(0)</string>
	<key>postinstall_script</key>
	<string>#!/usr/bin/env python

import subprocess
import sys
import plistlib

# Group System Preferences should be opened to
group = 'everyone'

command = ['/usr/bin/security', 'authorizationdb', 'read', 'system.preferences']

task = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = task.communicate()
formatted = plistlib.readPlistFromString(out)

# If the group doesn't match, we're going to correct it.
if formatted['group'] != group:
    #input_plist = {}
    formatted['group'] = group
    # Convert back to plist
    input_plist = plistlib.writePlistToString(formatted)
    # Write the plist back to the authorizationdb
    command = ['/usr/bin/security', 'authorizationdb', 'write', 'system.preferences']
    task = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    (out, err) = task.communicate(input=input_plist)</string>

	<key>uninstall_method</key>
	<string>uninstall_script</string>
	<key>uninstallable</key>
	<true/>
	<key>uninstall_script</key>
	<string>#!/usr/bin/env python

import subprocess
import sys
import plistlib

# Set the group back to admin
group = 'admin'

command = ['/usr/bin/security', 'authorizationdb', 'read', 'system.preferences']

task = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = task.communicate()
formatted = plistlib.readPlistFromString(out)

# If the group doesn't match, we're going to correct it.
if formatted['group'] != group:
    formatted['group'] = group
    # Convert back to plist
    input_plist = plistlib.writePlistToString(formatted)
    # Write the plist back to the authorizationdb
    command = ['/usr/bin/security', 'authorizationdb', 'write', 'system.preferences']
    task = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    (out, err) = task.communicate(input=input_plist)</string>
</dict>
</plist>

At this point, you should be able to add this pkginfo to your Munki repository, include it in a manifest and - well, nothing will happen, as this only unlocks the top level of System Preferences. If you want to do more, you’ll need to unlock additional parts as well - the scripts to do this can be found in my macscripts repository. I’ve specified that OpenSysPrefs is required in all of these - this means I can include only the needed modifications in the manifest and not worry about the top level being unlocked.

Also remember that Munki has conditional items built right in - you might only want to unlock the Network pane on laptops so they can install VPN profiles etc using something like this:

<key>conditional_items</key>
<array>
  <dict>
    <key>condition</key>
    <string>machine_type == "laptop"</string>
    <key>managed_installs</key>
    <array>
      <string>UnlockNetwork</string>
    </array>
  </dict>
</array>