Using Self Service for Temporary Admin Rights

With a combination of bash, AppleScript, and your MDM's self service tool, you can allow your users to grant themselves admin rights when they need it.

Using Self Service for Temporary Admin Rights

Different companies have different guidelines for whether they want their end-users to have standard or administrator accounts. It can be tempting to just give your users admin rights to lower support desk request volume, though you do run a higher risk of users authenticating malware or improperly modifying their workstations. If you keep your users as standard, you'll need to regularly handle requests from them to authenticate settings changes, app installs, and updates. This can also generate some perceived friction with them, feeling reliant on your support desk and making some of their work tasks burdensome.

One possible solution to this quandary is to allow your users to self-remediate - keep them as standard users, but allow them a way to temporarily promote themselves. The lowest-tech solution to this would be helping them create local admin accounts that they don't log into, but whose credentials they can use to authenticate admin requests. Next would be giving them the ability to temporarily promote themselves to administrator status for a brief period of time. One common tool for this is Rich Trouton's Privileges, a phenomenal little app that can handle these kinds of requests, and even log the requests to a server.

So, with such a great tool like that available, why did I decide to put something together myself? I had the following checklist of things wants:

  • I'd like to embed the script into our MDM's self-service app, to encourage our users to engage with it in the future
  • I'd like the script to be interactive - where a user can restore their account to standard when they're done, or passively allow it to time out
  • The tool should be resilient enough to recover itself should the user try to circumvent the restoration of standard account status
  • I'd like to have a record of admin promotion requests, so we have a paper trail for auditing purposes

So, I did a thing. And Addigy liked it enough to write a KB article about it.

Grant Me Admin Access

Here's how it works

  1. The user opens MacManage (Addigy's self-service application), locates the script, and launches it
    01---MM-item_sanitized
  2. The user confirms a popup verifying they'd like to promote themselves temporarily
    02---Verify
  3. The user is prompted to submit a reason for the promotion
    03a---Justification
  4. The user's request is packaged as a support ticket and sent to the support desk for review and archiving. Note if the ticket fails to send, the workflow halts, asking them to reach out to the support desk by phone
  5. If the ticket is sent successfully, the user is temporarily promoted and shown a popup with a timer, letting them know they'll be restored to a standard user in 10 minutes. They can also click a button when they're done
    04---Promoted-and-Dismiss
  6. When 10 minutes elapse, or the user dismisses the popup, they're restored to a standard account

One quick note - the version of the script hosted by Addigy does not include the logic to generate a support ticket. Some teams don't like the extra notifications, so it makes sense - when originally authoring this, I figured the ticket workflow made sense, as the MacManage library will not load unless the computer's online, so may as well take advantage of it.

The prompts

Note: These prompts aren't included in the above script if you copy it directly from Addigy's kbase article.

Addigy includes some tools to allow administrators to leverage MacManage to create popup notification windows (not via Notification Center) with interactive buttons. I start by using these to get user consent to proceed with the promotion.

# Present user with info on what they're about to do - offer bailout
title="Grant me admin access"
description="You are requesting administrative privileges on your Mac. You must supply a reason for the request before proceeding."
acceptText="OK"
closeText="Cancel"
timeOut="600"

if /Library/Addigy/macmanage/MacManage.app/Contents/MacOS/MacManage action=notify title="${title}" description="${description}" closeLabel="${closeText}" acceptLabel="${acceptText}" timeout="$timeOut" forefront="true"; then
    echo "User proceeding"
else
    echo "User cancelled, exiting 1"
    exit 1
fi
MacManage notification allowing user to cancel or proceed

Next, an AppleScript popup is used to capture the user's reason for admin access to submit it as a support ticket. There's a lot that goes into this type of prompt - I'll go into those details in another post.

# 1 - Popup request for justification window
# Popup variables
PROMPT_TITLE="Grant me admin access"
PROMPT_TEXT="Tell us why you're requesting temporary administrator access"
FAIL_TEXT="Unable to process your request - please contact your support desk for assistance."

# Popup logo - requires macOS version comparison
currentOS=$(sw_vers -productVersion)
# Usage: Is $1 at least $2
is_at_least () {
	if [[ $1 == $2 ]]; then
		return 0
	fi
	local IFS=.
	local i ver1=($1) ver2=($2)
	for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)); do
		ver1[i]=0
	done
	for ((i=0; i<${#ver1[@]}; i++)); do
		if [[ -z ${ver2[i]} ]]; then
			ver2[i]=0
		fi
		if ((10#${ver1[i]} > 10#${ver2[i]})); then
			return 0
		fi
		if ((10#${ver1[i]} < 10#${ver2[i]})); then
			return 1
		fi
	done
	return 0
}

if is_at_least $(/usr/bin/sw_vers -productVersion) "$targetOS"; then
  LOGO="/System/Applications/Utilities/Keychain Access.app/Contents/Resources/AppIcon.icns" #10.15, 11.x
else
  LOGO="/Applications/Utilities/Keychain Access.app/Contents/Resources/AppIcon.icns" # 10.14 and earlier
fi
# End macOS Version Check #

LOGO_POSIX="$(/usr/bin/osascript -e 'tell application "System Events" to return POSIX file "'"$LOGO"'" as text')"

response1="$(/bin/launchctl asuser "$uid" sudo -u "$USER" /usr/bin/osascript -e 'display dialog "'"${PROMPT_TEXT//\"/\\\"}"'" default answer "" with title "'"${PROMPT_TITLE//\"/\\\"}"'" with text buttons {"Cancel","OK"} default button {"OK"} with icon file "'"${LOGO_POSIX//\"/\\\"}"'"' -e 'return text returned of result')"

echo "$timestamp [REQUEST] User supplied the following justification for promotion: $response1" >> "${logPath}" 2>&1
AppleScript popup asking the user to justify their promotion request

The script then sanitizes the request, including a failsafe for the user not typing anything, a maximum character limit, and a filter to remove illegal characters that could interfere with the ticket creation script.

# Catch invalid user input
if [[ "$response1" == "" ]]; then
  response1="User left text box empty"
fi

# Remove bad characters
# TO REMOVE: Double quotes, single quotes, backslashes
# TO KEEP: Forward slashes, colon, exclamation point, dollar sign, asterisk, parentheses, brackets, braces
response1="$(echo "$response1" | tr -d \'\"\\ )"
echo "User response with special characters removed: $response1"

# Set maximum character length to 1024
if [ ${#response1} -ge 1024 ]; then
  echo "Shortening string to 1024 characters"
  response1=$( echo "$response1" | cut -c 1-1024)
  echo "Truncated response: $response1"
fi
Sanitizing the user input

We now create a support ticket using Addigy's go-agent binary. The user's sanitized response is loaded into a ticketDescription variable and some useful identifying info is harvested from the device to fill out the submitted ticket.

# 2 - Submit ticket - submit $response1
ticketDescription="--- EVENT --- \n User ran Admin Promotion script with the following request: \n \n $response1"
# Harvest ticket information
ticketDevice=$(hostname | awk -F'.' '{print $1}')
ticketSerialNumber="$(system_profiler SPHardwareDataType | awk '/Serial/ {print $4}')"
ticketUserName="$(/usr/bin/stat -f%Su /dev/console)"
     echo "ticketUserName is $ticketUserName"
ticketTime="$(date +%r)"
ticketDate="$(date +%x)"

# Create ticket
ticketRequest="$(curl -X POST https://$(/Library/Addigy/go-agent agent realm).addigy.com/submit_ticket/ -H 'content-type: application/json' -d "{\"agentid\": \"$(/Library/Addigy/go-agent agent agentid)\", \"orgid\":\"$(/Library/Addigy/go-agent agent orgid)\", \"name\":\"$ticketUserName\", \"description\":\"\n ____________________________________________________________ \n Device name: $ticketDevice \n Serial number: $ticketSerialNumber \n Username: $ticketUserName \n Date: $ticketDate, $ticketTime \n ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ \n $ticketDescription \"}")"
Creating a support ticket using Addigy's go-agent

The ticket attempts to upload - since users can't trigger this tool while offline, we decided it was worthwhile to require the ticket to upload before proceeding. This section contains the necessary bailout to make sure that we halt if the ticket can't upload for any reason.

# If ticket creation fails, halt - user will have to contact suppport Support Desk
if [[ "$ticketRequest" == *"Something went wrong, we are looking into this issue."* ]]; then
  echo "$timestamp [RESULT] Ticket not sent, display error and exit"
  title="Ticket submission failed"
  description="$FAIL_TEXT"
  closeText="OK"
  timeOut="600"

  if /Library/Addigy/macmanage/MacManage.app/Contents/MacOS/MacManage action=notify title="${title}" description="${description}"  closeLabel="${closeText}" timeout="$timeOut" forefront="true"; then
      # These commands can be changed to detemine what happens when the user clicks the "Accept" label.
      echo "$timestamp [Ticket creation failed, halting]" >> "${logPath}" 2>&1
      exit 1
    else
      echo "$timestamp [Ticket creation failed, halting]" >> "${logPath}" 2>&1
      exit 1
  fi
  exit 1
else
  echo "$timestamp [RESULT] Ticket created successfully, promoting." >> "${logPath}" 2>&1
fi
Bailout if the ticket was unable to upload & notify the user

Once the ticket is successfully generated, the user is finally promoted to admin. As soon as this is done, we also touch a hidden flag file to mark this user account as temporarily promoted.

#find current user
loggedInUser="$(stat -f "%Su" /dev/console)"
uid=$(id -u "$loggedInUser")

#Set current user to admin
sudo dscl . -merge /Groups/admin GroupMembership $loggedInUser
echo "[Promotion complete]"

# SETUP SAFEGUARDS
# Create failsafe flag. If flag detected in maintenance script, account will be demoted.
touch /Users/$loggedInUser/.tempPromoted
echo "Created flag file"
Add the current user to the admin group

The safeguards

While testing, I had to figure out how to ensure users wouldn't abuse this tool and use it to permanently promote their accounts. At worst, I had to be prepared for users to force a shutdown on their computer via an SMC reset - the script needed to be able to recover from that and return them to their original status elegantly.

So, immediately as the user promotion occurs, the script authors two files to be used to ensure the user will be returned to standard status.

First, we make a shellscript that we can use to return the user to normal. This script includes all our cleanup tasks: user demotion, cleanup of the flag file, and then deleting itself along with the launchAgent in the next step. The file is created and given broad permissions for execution:

# Create the demotion shellscript
shellscriptPath="/Users/$loggedInUser/Library/Application Support/maintenance_demotion.sh"

echo '#!/bin/bash
# Remove temporary admin status if detected
# Ross Matsuda | Ntiva, Inc | December 2020

# Perform action on all detected user accounts
for user in $(dscl . list /Users UniqueID | awk '$2 >= 500 {print $1}'); do
    userHome=$(dscl . read /Users/"$user" NFSHomeDirectory | sed 's/NFSHomeDirectory://' | grep "/" | sed 's/^[ \t]*//')
    echo "$user:$userHome"
    FILE="$userHome/.tempPromoted"
    if [[ -f "$FILE" ]]; then
        echo "$FILE exists, demoting and removing flag"
        sudo dseditgroup -o edit -d $user -t user admin
        rm "$FILE"
        launchctl unload "$pathPlist" &>/dev/null
        rm "$shellscriptPath"
        rm "$pathPlist"
      else
        echo "$FILE not found"
    fi
done
' > "$shellscriptPath"

# Set the correct permissions for shell script

chmod 777 "$shellscriptPath"
chmod a+x "$shellscriptPath"
echo "Created shellscript"
The demotion script

Next, a LaunchAgent is created to leverage the above demotion script. This will launch and persist immediately upon a user logging in, ensuring that even if a user tries to force their computer off while promoted, they'll be returned to normal the next time they log in.

# Create LaunchAgent
pathPlist="/Users/$loggedInUser/Library/LaunchAgents/com.user.tempPromoted.plist"

# Ensure destination directory exists
userLA="/Users/$loggedInUser/Library/LaunchAgents"
if [ -d "$userLA" ]; then
    echo "User launchAgent directory detected"
else
    echo "User launchAgent directory not detected, creating"
    mkdir -p "$userLA"
    chmod 777 "$userLA"
fi

# Create the LaunchAgent.

cat >> "$pathPlist" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
   <key>Label</key>
   <string>com.user.loginscript</string>
   <key>ProgramArguments</key>
   <array><string>$shellscriptPath</string></array>
   <key>RunAtLoad</key>
   <true/>
</dict>
</plist>
EOF

# Set the correct permissions and load current LaunchAgent.
chmod 644 "$pathPlist"
launchctl load "$pathPlist" &>/dev/null
echo "Created launchagent"
The LaunchAgent to trigger the demotion script at login

Wrapping up

The user is presented a popup so they can restore their account to standard when they're done. If they don't proactively dismiss the window, it will time out after 10 minutes (you can change the timer in the script). Either way, once the user's account is restored to normal status, the LaunchAgent is unloaded and deleted along with the demotion .sh file and the flag file.

# Update the text fields for the final notification window
title="Admin Status Enabled"
description="Once you've authenticated your installer, settings change, or update, please click Restore"
# acceptText="Restore"
closeText="Restore"
timeOut="600"

if /Library/Addigy/macmanage/MacManage.app/Contents/MacOS/MacManage action=notify title="${title}" description="${description}"  closeLabel="${closeText}" forefront="true" timeout="$timeOut"; then
    # These commands can be changed to detemine what happens when the user
    #   clicks the "Accept" label.
    sudo dseditgroup -o edit -d $loggedInUser -t user admin
    rm /Users/$loggedInUser/.tempPromoted
    launchctl unload "$pathPlist" &>/dev/null
    rm "$shellscriptPath"
    rm "$pathPlist"
    echo "[Demotion complete]"
    exit 0
else
    # These commands can be changed to detemine what happens when the user
    #   clicks the "Close" label.
    sudo dseditgroup -o edit -d $loggedInUser -t user admin
    rm /Users/$loggedInUser/.tempPromoted
    launchctl unload "$pathPlist" &>/dev/null
    rm "$shellscriptPath"
    rm "$pathPlist"
    echo "[Demotion complete]"
    exit 0
fi
Action to take when dismissing the promotion window (cleanup and demotion)

That's our full workflow. This script has allowed our team to work with our clients on finding a middle ground where their users can benefit from the added security of running as standard accounts while still having the flexibility of gaining administrative access on demand. We always ask our clients to update their employee acceptable use policies to indicate that users are responsible for the actions they take while promoting themselves.

The benefits of this script have been wonderful. The support desk only gets involved if a ticket appears with a justification like "Apple Support called me on the phone and asked me for my credit card number and to install LogMeIn". They can also use the archived tickets for forensic purposes when troubleshooting future issues. Users get introduced to MacManage, a repository for great single-click software installers that they can use without needing admin credentials and other self-remediation resources. This builds their self-efficacy as users of technology, lowers the friction they'd feel if they had to reach out to us every time they needed to change their time zone or install an app, and respects them as responsible stewards of their equipment.

EDIT: Thanks to the Command-Control-Power podcast for shouting out this article in episode 494!