Deleting user folders in labs on a schedule

In multiuser labs on campus we often have people log in once, and never log in again. Sometimes they pick a machine and always use that, sure, but inevitably at some point in the semester we start to run low on disk space. For that reason I have a policy to run once a week on our lab systems that runs a script to check if a user profile has been logged into within a variable number of days. For most systems I’ll use 28 days.

All lab systems are subject to replacement in the event of hardware or software failure, and as such generally a student (or faculty member) should be using network storage or google drive. At the same time we want to provide the convenience to users who may use the same system every class or study period.

This script is based on a script from a coworker (who appears to have pulled it from a GitHub and modified it). Its fairly simple but searches based on the last modified and excludes any user currently logged in. I use a variable (#4) to assign different ages based on the locations.

## Checks if the variable has been provided
if [[ -z $days ]]; then
	read -p "Profile Age:" days
## new shorter bash method to get logged in user
loggedInUser=$( scutil <<< "show State:/Users/ConsoleUser" | awk '/Name ꞉/ && ! /loginwindow/ { print $3 }' )
## Do not delete the current user or the shared folder, add additional users if needed
permanent=("Shared" "$loggedInUser")
## Verify script is being run as root
if [[ $UID -ne 0 ]]; then echo "$0 must be run as root." && exit 1; fi
## users that have not been active in the specified number of days
allusers=`/usr/bin/find /Users -type d -maxdepth 1 -mindepth 1 -not -name "." -mtime +$days`
echo "deleting inactive users"
## iterate through each inactive user, check if they are in the permanent list, then delete 
for username in $allusers; do
	if ! [[ ${permanent[*]} =~ "$username" ]]; then
		echo "Deleting inactive (over $days days) account" $username
		# delete user
		/usr/bin/dscl . delete $username > /dev/null 2>&1
		# find and delete home folder (in case it is not in /Users/)
		home=$( dscl . read /Users/${username} NFSHomeDirectory )
		/bin/rm -r $home
		echo "skip" $username
echo "complete"
exit $error

Edit: I made some changes to the above script, including using a method to get the current logged in user via bash found here,-in-Bash/ and some errors pointed out in a comment in my logic where I conflated username and home folder in a variable.

Edit 2: it seems the codeblock above had a strange WordPress induced bug in line 10 where it was displaying a colon backslash as an emoji. I have worked around this to display correctly, but please make sure you copy from the correct script located on GitHub here.

Loaner and Lab “Reimage”

We have found ourselves in need of more loaner laptops than ever before with growing numbers of students and faculty attending class remotely. In an effort to come up with a simple way to prepare these laptops after they are returned and sanitized I went looking for how I can set up with the least interactivity required.

After some research, I found this JamfNation thread. I can launch Jamf’s jamfhelper application and run at the login window, but only if its opened from a launchdaemon. That led me to this script which builds a launchdaemon to run jamfhelper and run through our prep workflow.

Luckily, as I described in this post from my JNUC2020 presentation, I have most of our software set up with policies on custom triggers to run with DEPNotify for our standard and zero touch deployment strategy. I took above script and customized it, and created a new prestage enrollment to skip account creation then run this script on enrollment. Of course I had to assign each serial number to this prestage enrollment so I utilized the MUT‘s Groups and Prestages template to upload the list of serial numbers to the newly created prestage for this refresh process.

With that configuration, we can have a student or help desk associate go through the setup on a lab or prepare a recently returned loaner using an erase install progress in Self Service. This allows us to set things up quickly without needing any passwords shared or requiring someone to go through the user setup process.

While not as pretty as DEPNotify, this does show a useful progression

Making Panopto Available in Self Service

As most of the US is now doing remote instruction to practice better social distancing, we’ve found ourselves using new tools more frequently. Panopto is a tool that is for recording classes and being able to distribute later – it lets you record from a camera and microphone and screen share, and it can be uploaded for students to reference.

We’ve been using Panopto for some time, but now we are getting requests to install in en masse. The Panopto installer does some unusual stuff that caused complications when trying to deploy with Jamf’s Self Service. First, it creates a hidden user account to manage uploads to the server. That user needs specific permissions, and in Mojave and Catalina it can cause some issues. If you are following certain security protocols you may need further exceptions for this user account for it to work. It also assumes the installer is being run by the user. We found that when Jamf installed Panopto it was either not creating the account, or it was creating it but failing to give it appropriate permissions.

We saw multiple errors, mostly an error 84 which we couldn’t find documentation for, or an error uploading. It seemed the issues with these were either that the user Panopto creates, panopto_upload, did not have permission to the folder or was not created correctly.

In order to make this work in Self Service I worked out the following steps:

  • Due to failed and incorrectly configured installs the first step is a script I wrote based on the instructions to manually uninstall Panopto. This helps as there were case where Panopto was installed and nonfunctional. The script uses the $3 variable in Jamf for Self Service, but it can be run manually and will use the logged in user if $3 is not specified.
  • Second, we install a profile to ensure the correct hostname is added in settings. Panopto’s installer is supposed to use a hostname in the installer’s filename but I’ve found that to be inconsistent when deployed with Jamf. This is a profile using the Application & Custom Settings payload in Jamf.
  • A script originally written by @macdude22 on the macadmins slack to create the Panopto user correctly before install.
  • Installing the Panopto 7.3 package as provided by Panopto’s support.
  • Running a permissions fix for the user in Self Service.

In testing with Mojave and Catalina, the Self Service installer now runs correctly for the user in Self Service. For now, this covers most of our use case. There will be an additional requirement on shared systems to ensure that the panopto_upload user has permissions for each user who logs in. Tentatively that will run using outset in a login-once script.

Migrating existing mobile AD accounts to Local accounts with NoMAD in Self Service

Moving from mostly on campus and AD bound machines with mobile accounts to working remotely for an unspecified time brings a lot of challenges. one of them is ensuring your password correctly stays in sync when changed, and ensuring your FileVault access and login keychains remain with your login password is important too. We’d had issues with those already, and were testing NoMAD already on campus as a tool to avoid this. NoMAD does not require a bind to AD, but can still sync your password with your AD account and ensure the FileVault and login keychain passwords update correctly.

As part of my test I had a Self Service policy with a script to turn a mobile account into a local account, then install NoMAD and the required profile. This was initially so we could take advantage of the bootstrap token, and use in conjunction with a Self Service policy to enable encryption.

The main part of the process is, based on Rich Trouton’s interactive script but intended for use in Self Service. It uses the #3 variable to pull the user logged in to Self Service and migrate that account if it is in fact a mobile account, and retains admin permissions as it is configured in our environment. The policy is configured to run 3 things; this script to migrate the account, install the NoMAD profile and then the NoMAD application and launchagent.

Alternately I could modify the script to run on the currently logged in account to the OS for Self Service. In our case I don’t believe this will be any more effective. It could be used as part of a policy to migrate systems to NoMAD in the background. Currently in Self Service we can scope to just the machines we are testing with so we have control over the test as we move to a wider scale.

Running Jamf policy custom triggers via script

At first glance, my script may seem too simple. It just calls a Jamf policy by a custom trigger. But this can be very useful in a few workflows.

if [[ -z $event ]]; then
	read -p "Jamf Custom Trigger:" event

jamf policy -event $event

I use this script to create Self Service policies that are scoped specifically to the required OS or department. They refer back to a policy that actually installs the software that is only triggered by the custom event. I can also create another policy to automatically install on newly DEP enrolled machines during their provisioning workflow. I can have multiple policies that refer back to the primary one. This means I only need to edit one policy if something changes in the installer, package name, or choices XML modifications.

I also use this to kickstart our DEP Provisioning workflow in Self Service because the enrollmentComplete trigger from Jamf can be unreliable. If the network connection is interrupted during the enrollment, it may never see the trigger. So when setting up a machine, if I don’t see the notice that it’s starting the process I can click it from Self Service.

If Jamf changed the way to call a custom event it also would mean only changing the primary script which just makes things slightly easier, as opposed to using the “Execute Command” feature located under Files and Processes in a Jamf Policy.

Add and Remove from Static Groups with the Jamf API

I have 2 scripts I’ve found useful with scoping policies and profiles to a group that isn’t easily configured by other information in a device’s record. A MacBook Pro that is shared for a lab is difficult encrypt, but the same model in the same department is assigned to a specific user and that we do want to encrypt. We can scope a policy to a static group and use a Self Service policy to add a machine to that group, then trigger the policy by a custom event to kick off. grabs the serial number of the device and adds it to a static group by that groups ID number. You can easily locate the ID number of a group by checking the URL in the JSS. In the example below, the number is in the ?id= portion, represented by XX.

Writing the script with these variables allows for reuse, and if an update to the parameters is made it will be easy to update one script instead of every policy that may use it.

curl \
	-s \
	-f \
	-u ${apiUser}:${apiPass} \
	-X PUT \
	-H "Content-Type: text/xml" \
	-d "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>${apiData}" ${jssHost}/JSSResource/computergroups/id/${groupID}

A few usage examples for this would be to add a machine to an exception group to temporarily remove a profile for testing at the help desk, or adding to a group that has an encryption policy scoped to it. is similarly used to remove a machine from a static group. One of the ways I’ve used this is to remove a machine from groups when repurposing a machine with an erase install policy. It could similarly be used in conjunction with the add to static group script for help desk, with a self service policy to add and remove a profile or policy for testing purposes.

curl \
	-s \
	-f \
	-u ${apiUser}:${apiPass} \
	-X PUT \
	-H "Content-Type: text/xml" \
	-d "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>${apiData}" ${jssHost}/JSSResource/computergroups/id/${groupID}

Help Desk Scripts

I have added a couple items to Jamf Self Service that can be useful for Help Desk. They are simple items but can empower users and help desk staff to help when there are security obstacles, like a firmware password or out of sync user password. does some minor clearing of caches and performs an NVRAM reset. This is useful on macOS systems with firmware passwords; the method to hold down CMD+Option+P+R will not traditionally work when there is a firmware password set. Clicking a Self Service policy to run this script and rebooting can potentially alleviate some issues.

update_dyld_shared_cache -root / -force -debug
/usr/libexec/xpchelper --rebuild-cache
nvram -c will simply remove an old keychain for a user with a keychain they can no longer access. This occasionally happens with AD mobile accounts when the password is changed on another system and then authenticated to AD updating the password. In macOS if you bypass the password change mechanism some items cannot be updated, like the Keychain password or Filevault password. This script will get rid of the issue when attempting to remember the old password is not working.

rm -rf "$home"/Library/Keychains/*

I’ve also added an installer for Recovery Selector from Two Canoes. Again firmware passwords can be a hindrance for some items, like rebooting to run diagnostics. When we have macOS devices that need diagnostics run I’ve added a Self Service policy to install this application to the /Applications/Utilities folder. The system still requires the firmware password, but you do not need to remove it just for diagnostics. In some cases you will still need to remove it to complete a repair, but this can make things quicker.

edit 2021-03-17: updated script links

Jamf API Scripts on Github

I’ve been maintaining a Github with useful Jamf Pro scripts I’ve found helpful, and some I featured in a presentation at the 2019 Penn State MacAdmins Conference.

I specifically featured a few scripts that I use in our DEP provisioning workflow. This workflow relies heavily on using the Inventory Preload feature in Jamf Pro. It allows me to assign a machine to a user, assign it an asset tag, and put it in a department and building. The first time a machine checks in, Jamf will assign these items to the computer’s record in Jamf. These are all ways I can sort computers in Jamf Pro, but I can communicate with the Jamf API to pull some of these items and use them while preparing a new system.

For security purposes I have the username and password sent as a variable to the script. I also made the account with limited permissions to only make the call to pull computer data to mitigate concerns. is a simple script that grabs the user assigned in Jamf Pro and adds that user as an administrator. It first gets the serial number, and uses that to perform a lookup to find the assigned user via API call. It then uses a dscl command to set that user as an admin. This script would primarily be useful if you have machines prepped in advance by IT instead of initial configuration by a user.

username=$(/usr/bin/curl -H "Accept: text/xml" -sfku "${apiUser}:${apiPass}" "${jssHost}/JSSResource/computers/serialnumber/${serialNumber}/subset/location" | xmllint --format - 2>/dev/null | awk -F'>|<' '/<username>/{print $3}')
dscl . -append /Groups/admin GroupMembership $username could easily be customized, but this allows for a unique firmware password to be set per machine, utilizing the Asset Tag field in this case. As the firmwarepasswd command is interactive it uses the expect shell to respond. Again we get the serial number and use that to pull the asset tag from the computer’s inventory record. We use the expect shell to interact with a command line and send a unique password with a password scheme, assuming one isn’t already set. There are potential security concerns with setting a firmware password with a script. It will run in plain text and be cached to the Jamf Waiting Room, but in the case of IT preparing machines it is unlikely that any information would still be cached when the user gets the system.

doesexist=`firmwarepasswd -check`
barcode=$(/usr/bin/curl -H "Accept: text/xml" -sfku "${apiUser}:${apiPass}" "${jssHost}/JSSResource/computers/serialnumber/${serialNumber}/subset/general" | xmllint --format - 2>/dev/null | awk -F'>|<' '/<asset_tag>/{print $3}')

if [ "$doesexist" = "Password Enabled: No" ]; then
	/usr/bin/expect <<- DONE
		spawn firmwarepasswd -setpasswd
		expect "Enter new password:"
		send "$firmware\r";
		expect "Re-enter new password:"
		send "$firmware\r";
		expect EOF
	echo "Firmware Password Already Exists"
fi similarly pulls the Asset Tag from the computer’s record in Jamf, again by using the serial number to perform a lookup. This sets each of the names to the same name. In this case we use the asset tag proceeded by the type of device , Mac-##### in the script. This can be coupled with a Jamf policy to keep computer name if you want to ensure you can find a machine on the network.

barcode=$(/usr/bin/curl -H "Accept: text/xml" -sfku "${apiUser}:${apiPass}" "${jssHost}/JSSResource/computers/serialnumber/${serialNumber}/subset/general" | xmllint --format - 2>/dev/null | awk -F'>|<' '/<asset_tag>/{print $3}')
scutil --set ComputerName $hostname
scutil --set HostName $hostname
scutil --set LocalHostName $hostname