Demystifying `root` on macOS, Part 3 — `root` and Scripting

sudo is very useful when working interactively in the shell. However, when you are scripting workflows then you don’t want to encounter the interactive prompt for authentication.

sudo in Scripts

You will write scripts that require root privileges to perform their tasks. Many novice scripters will simply add the sudo command to their scripts. For example:

#!/bin/bash

timeserver="time.apple.com"

echo "Setting Time Server to: $timeserver"

# note: sudo is superfluous
sudo systemsetup -setnetworktimeserver "$timeserver"
sudo systemsetup -setusingnetworktime on

When you invoke this script from the command line, it will actually work as expected, since the first sudo will prompt the password and the second sudo will use the cached credentials.

In many cases where the script already has root privileges, it will work also, because sudo when run as root will not prompt for authorization again.

However, when this script is sent with Remote Desktop or run in a different context without user interaction to authorize sudo, the script will stall and eventually fail.

In most contexts, the scripts should already be running with root privileges so sudo in the script is not necessary.

#!/bin/bash

timeserver="time.apple.com"

echo "Setting Time Server to: $timeserver"

systemsetup -setnetworktimeserver "$timeserver"
systemsetup -setusingnetworktime on

You should not use sudo in your management scripts. When commands inside a script require root privileges, you should invoke the entire script with sudo:

$ sudo ./settimeserver.sh

or you deploy and execute the script through other methods that provide root privileges. (Remote Desktop: run as user root, installation scripts, LaunchDaemons, management systems, etc.)

Testing for root Privileges in Scripts

When you write scripts that require root privileges, you may want to test whether it is actually running as root early in the script or exit gracefully with an error or warning. As usual with Unix, there are many ways to achieve this, but the recommended one is to check the EUID (effective user id) environment variable. For the root user the EUID is 0.

#!/bin/bash

# test if root
if [[ $EUID -ne 0 ]]; then
    >&2 echo "script requires super user privileges, exiting..."
    exit 1
fi

# continue with important things here
echo "I am root"

Running a Process as Another User

Most administrator scripts are run in a context where they run with super user privileges. Often they require root privileges.

On the other hand, when you need to affect settings or processes in a user’s context or login session, you may need to run commands as a specific user from a script running as root. It’s the opposite problem of gaining root privileges.

You can use sudo -u user command or su user -c command to run a command as a different user. However, the launchd man page warns us:

On Darwin platforms, a user environment includes a specific Mach boot strap subset, audit session and other characteristics not recognized by POSIX. Therefore, making the appropriate setuid(2) and setgid(2) system calls is not sufficient to completely assume the identity for a given user. Running a service as a launchd agent or a per-user XPC service is the only way to run a process with a complete identity of that user.

So it is safest use the Darwin/macOS native mechanism to launch a process as another user. Enter launchctl. You can use the the launchctl asuser verb to execute a script or command as a different user.

uid=$(id -u "$username")
launchctl asuser "$uid" /path/to/command arguments

The launchctl command takes a user’s numerical ID or UID rather than the short name as an argument. You can get a user’s UID with the id -u username command.

Note that launchctl asuser does not launch the command in the context of a new shell environment. You cannot rely on environment variables being set in that context. This is especially relevant for the PATH.

Note also that the asuser verb of launchctl is listed among the deprecated functions of launchctl. However, there is no replacement for the functionality yet.

Getting the Current User

Most of the time you will not know which user you need to run as, when you write a script, but need to run as the ‘currently logged in user’. There are many unix-y ways of determining the current user. However, once again there are edge cases in macOS where some of the traditional methods fail. (Mainly concerning Fast User Switching)

The ‘official’, method is to use the SystemConfiguration framework, and from a script this is easiest from within python:

loggedInUser=$(/usr/bin/python -c 'from SystemConfiguration import SCDynamicStoreCopyConsoleUser; import sys; username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0]; username = [username,""][username in [u"loginwindow", None, u""]]; sys.stdout.write(username + "\n");')

(Enduring thanks to ‘macmule’ who documented this years ago.)

This will return the currently active user, even when multiple users are logged in with Fast User Switching. When no user is logged and the system is logging in the value returned will be "". Your scripts have to cover this case as well.

You can use this code snippet as a template:

# get the current user
loggedInUser=$(/usr/bin/python -c 'from SystemConfiguration import SCDynamicStoreCopyConsoleUser; import sys; username = (SCDynamicStoreCopyConsoleUser(None, None, None) or [None])[0]; username = [username,""][username in [u"loginwindow", None, u""]]; sys.stdout.write(username + "\n");')

# test if a user is logged in
if [[ $loggedInUser != "" ]]; then
    # get the uid
    uid=$(id -u "$loggedInUser")
    # do what you need to do
    launchctl asuser "$uid" /path/to/command arguments
fi

AppleScript

AppleScript has a special function when invoking shell commands for gaining super user privileges. When you run a shell command from AppleScript with the do shell script command you can add with administrator privileges to have the script prompt for admin user authentication and run the command as root:

do shell script "whoami"
do shell script "whoami" with administrator privileges

Will run like this:

    do shell script "whoami"
       --> "armin"
    do shell script "whoami" with administrator privileges
     --> "root"

Like with sudo the credentials for the first command run with administrator privileges will also be cached, so you will not get mulitple prompts, unless the script runs for a long time.

This is useful for providing tools and workflows from within AppleScript for interactive use. However, as with sudo you have to keep in mind that some contexts in which a script may be run does not allow for user interaction and then you have to find other means of elevating privileges.

AppleScript is often used to communicate with other applications and/or the user interface. However, when run with root privileges AppleScripts are often prohibited from connecting to other process or present user interface, such as dialogs. When you really need to do this, you have to use launchctl asuser to change the user the script is run as:

launchctl asuser "$uid" /usr/bin/osascript -e 'display dialog "Do you really want to do this?"

Beyond the Shell

sudo allows you to gain super user privileges from the interactive shell. LaunchDaemons, installer Packages, management systems and other tools will run scripts as root when required. This covers many cases where administrators need to influence the system on macOS.

However, for most users, macOS is exclusively the graphical interface. Users can authorize to perform certain tasks when they are administrator users, like unlocking a Preference Pane or running an installer package.

These tasks are not controlled by sudo but by a separate mechanism. The data for that mechanism is stored in the authorization database, which we will cover in the next post.

Leave a Reply

Your email address will not be published. Required fields are marked *