Configuring Printers Programmatically for AirPrint Part 2: Now with Icons!

Several years ago, I wrote a post about the challenges around using lpd:// for printing on Macs running 10.14 Mojave and later. While I came up with a workaround involving programmatically configuring printers to use AirPrint, I found a minor (but annoying) issue where configuring a printer in this way when run as root (which any management tool would do) would produce a PPD file without a printer icon. Running the same ipp2ppd binary as the logged in user did correctly include the printer icon.

A fellow MacAdmin came up with a clever workaround using outset for running the necessary script as the logged-in user, but I had hoped that my post would nudge others in the community to come up with a workaround to allow the inclusion of the printer icon rather than force others to implement another (albeit useful & powerful) tool. Unfortunately, this did not come to pass. The topic of the #printers-n-cups MacAdmins Slack channel is apt: Abandon all hope, ye who enter here

Part of the ipp2ppd binary processing involves downloading all of the available printer icons (.png) from the printer and converting these into a single .icns file. As I learned years ago, these icons are discoverable using the ipptool and searching for printer-icons from the output.

Since the only difference between a root triggered call of ipp2ppd and a regular user call is this icon file reference in the PPD, it occurred to me that so long as it was possible to query and download just one of these PNG icons that this could be subsequently appended to the PPD file in order to install a printer in this programmatic way with an icon.

Sure enough, some tweaking later and I can confirmed the below script would successfully add several different AirPrint printers with their appropriate icons!

#!/bin/bash
# Use the built-in ipp2ppd tool to create a PPD file for AirPrint
# Add icon to PPD (if we can get one) and install the printer
# Required printer info
readonly PRINTER_IP='XXX.XXX.XXX.XXX'
readonly PRINTER_NAME='PRINTER_NAME'
readonly PRINTER_DISPLAY_NAME='PRINTER NAME'
readonly PRINTER_LOCATION='PRINTER LOCATION'
# Requiring icon will prevent install if we can't get it
readonly REQUIRE_ICON=true
#readonly REQUIRE_ICON=false
# Number of seconds to wait for TCP verification before exiting
readonly CHECK_TIMEOUT=2
# Custom PPD info
readonly PPD_PATH='/tmp'
readonly PPD="${PPD_PATH}/${PRINTER_NAME}.ppd"
# Base info
readonly AIR_PPD='/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/PrintCore.framework/Versions/A/Resources/AirPrint.ppd'
readonly EXE='/System/Library/Printers/Libraries/ipp2ppd'
readonly ICON_PATH='/Library/Printers/Icons'
readonly ICON="${ICON_PATH}/${PRINTER_NAME}.icns"
AppendPPDIcon() {
# Verify we have a file
if [ ! -f "$ICON" ] && [ "$ICON_AVAILABLE" != "false" ]; then
/bin/echo "Don't have an icon. Exiting…"
exit 1
fi
/bin/echo "Appending ${ICON} to ${PPD}"
# Append the icon to the PPD
/bin/echo "*APPrinterIconPath: \"${ICON}\"" >> "${PPD}"
}
CheckPrinter() {
# Verify we can communicat with the printer via the AirPrint port via TCP
local CHECK=$(/usr/bin/nc -G ${CHECK_TIMEOUT} -z ${PRINTER_IP} 631 2&> /dev/null; /bin/echo $?)
if [ "$CHECK" != 0 ]; then
/bin/echo "Cannot communicate with ${PRINTER_IP} on port 631/tcp. Exiting…"
exit 1
fi
}
CheckIcon() {
# Query & parse printer icon, largest usually last?
readonly PRINTER_ICON=$(/usr/bin/ipptool -tv ipp://${PRINTER_IP}/ipp/print get-printer-attributes.test \
| /usr/bin/awk -F, '/printer-icons/ {print $NF}')
# Verify we have an icon to download
if [ -z "$PRINTER_ICON" ] && [ "$REQUIRE_ICON" = "true" ]; then
/bin/echo "Did not successfully query a printer icon. Will not install printer. Exiting…"
exit 1
elif [ -z "$PRINTER_ICON" ]; then
/bin/echo "Did not successfully query printer icon. Will continue with printer install…"
readonly ICON_AVAILABLE=false
fi
/bin/echo "Downloading printer icon from ${PRINTER_ICON} to ${ICON}"
# Download the PNG icon and make it an .icns file
/usr/bin/curl -skL "$PRINTER_ICON" -o "$ICON"
}
CreatePPD() {
# Create the PPD file
/bin/echo "Creating the .ppd file at ${PPD}"
$EXE ipp://${PRINTER_IP} "$AIR_PPD" > "$PPD"
}
InstallPrinter() {
/bin/echo "Installing printer…"
/usr/sbin/lpadmin -p ${PRINTER_NAME} -D "${PRINTER_DISPLAY_NAME}" -L "${PRINTER_LOCATION}" -E -v ipp://${PRINTER_IP} -P ${PPD} -o printer-is-shared=false
}
main() {
CheckPrinter
CreatePPD
CheckIcon
AppendPPDIcon
InstallPrinter
}
main "@"

To use the above script effectively, the following is required:

  1. Your printer information:
    1. Name
    2. Display Name
    3. IP Address
    4. Location
  2. Specifying whether to forcibly fail the printer install if no icon is downloaded
  3. Specifying a timeout period where failure to communicate with the printer via TCP port 631 (used for ipp) will automatically cancel the install (default is 2 seconds)

Hopefully this discovery and script helps others to scratch that itch of wanting to both programmatically install their AirPrint printers and to include their icons without third-party tools!

Enabling, Restricting, and Hiding Third-Party System Preference Panes on Mac

As time goes on, it’s staggering to think about the amount of things we learn and then later happily (but at times also unhappily) forget when it becomes no longer relevant or useful. Much to my surprise, something I had long forgotten (mostly unpleasant memories) came flooding back to me from a relatively simple question: can third-party System Preferences panes be managed via profile?

Back when the only Jamf Pro option was on-premises, Jamf provided this Jamfnation Article as a guide for admins to natively manage third-party preference panes. By default, only a very small (and now very dated) native list of preference panes could be enabled, restricted, or hidden entirely via the System Preferences payload. Without too too much effort (if you knew what you’re doing) you could add your own checkboxes with the desired display name and the corresponding CFBundleIdentifier for the preference pane in order to selectively allow, deny, or hide these preference panes right from within the native Jamf profile builder.

Due to the fact the method of adding checkbox options involved changing the actual XML code of a particular JSS file, which got reset to its original state after every JSS upgrade, this required making the desired changes after every upgrade… Not a fun time! Shoutout to my fellow MacAdmins who can relate.

I’d even forgotten I’d written a Feature Request to add this functionality, which sadly like many a good Jamfnation FR:

To be fair, it’s not completely Jamf’s fault as it is a relatively trivial problem to solve and not necessarily one that’s needed to be made by a vendor when so many great open source solutions available…

All we need then is a way to create a System Preferences profile payload, have a list of our needed preference pane bundle ids, and have these listed under either the appropriate allowed, denied, or hidden list. Thankfully, I had taken the time to put some of this old work into code that with a little digging revealed a number of good options to start with.

So as of this writing, the ProfileManifests project which provides both ProfileCreator and iMazing’s Profile Editor with its magical powers now includes a number of third-party preference panes in the System Preferences payload! At the moment this is limited to a handful of items that I personally cared about (native Apple preference panes sorted alphabetically first, followed by third-party options), but it’s trivial to add more.

Don’t see an option you need, file an issue!

So how do you use this functionality with your own MDM? First, you must decide one of 3 paths. Do you want to:

  • Enable/Allow only the selected preference panes and disable all others
  • Disable/Restrict only the selected preference panes and enable all others
  • Hide only the selected preference panes and show all others

The best way to choose from the above options is to determine how you’d want to handle a new (perhaps an unknown, untrusted, or untested) preference pane addition. Would you want this enabled or disabled by default?

If you want to auto disable, I’d recommend the Enabled Preference Panes option which requires you to select everything in the list you expressly want to be enabled. If you want to auto enable, use the Disabled Preference Panes option instead. I have personally not tested the hide route, or combining any of these options, so test test test before if for whatever reason you decide to go that route.

To be clear, you don’t have to go with a one size fits all approach for your Mac fleet in this regard. You can certainly configure multiple profiles to auto allow or deny preference panes and target them to different groups of users or machines. Don’t want people to sign in to iCloud but don’t want to restrict this for your CEO? Make a special profile just for them.

Once you have your System Preferences profile settings the way you want them, export your profile and import that sucker in your MDM! No need have your MDM vendor build in native support for this…

That said, if your MDM vendor is Jamf don’t forget to sign your profile before you upload it.

Happy preference pane managing.

Okta LDAP User Group Membership with Jamf Pro: The Missing Link

For some time now, I’ve had the annoying problem of being unable to use LDAP group membership from Okta within Jamf Pro. Following this guide https://jrdsgl.com/integrating-okta-ldap-to-jamf-pro/ (although there are a number of others out on the interwebs) I was able to successfully query users and groups, but critically not user group membership.

Having previously worked at an organization that still bound Macs to Active Directory (say what you will), I had long enjoyed the administrative benefits of this functionality by being able to assign LDAP groups to limitations and exclusions within automated and Self Service policy scopes. This provides two very useful features:

  1. It allowed me to limit sensitive CLI and Self Service policies only for IT technician use on any device.
  2. I could provide conditional access to select users based on their group membership, regardless of the Mac they might use.

Through the LDAP test mechanism within Jamf, I was able to query my and other users successfully (returning the object ID) but for the life of me could never query a given user and group (which the user was a part of) and return “Yes”.

After much trial and error, it occurred to me that guides consistently showed setting the ‘Membership Location’ option to ‘Group Object’ and that instead I might base this on the ‘User Object’ instead. If you’re at all familiar with Active Directory, when you access a user record there is a ‘Member Of’ tab which lists all the groups the user is part of. Sure enough, using the ‘User Object’ option and setting the value to memberOf correctly marked users as members of the appropriate groups!

If you have your LDAP configured correctly, you can achieve the previously mentioned benefits by configuring any Jamf policies with a Scope of ‘All Computers’ with Limitations based on the desired LDAP User groups. If you happen to have local admin users for specific management tasks, you can specify these individual usernames as well.

A potential undesirable side effect (depending on how you look at it) is that if you remotely connect to your users’ Macs and attempt trigger policies manually, simply running sudo jamf policy and a custom trigger will use the logged in user’s username by default. As a result, it may be necessary to run sudo jamf policy -username <user> where <user> is a user that’s a member of an LDAP group or is explicitly specified under your policy’s Limitations. While this solves a potential problem for IT staff, this leaves open an opportunity for clever users to take advantage of this (as I’ve written recently, the list of things a local admin user can’t do on an institutionally owned and managed Mac grows smaller and smaller…). In my own testing, use of the -username argument:

  • …with a valid LDAP username that is a member of an LDAP Group listed as a policy limitation will successfully trigger a policy.
  • …with a valid LDAP username this is not a member of an LDAP Group listed as a policy limitation will not trigger a policy.
  • …with a non-existent LDAP username will instead behave as if you hadn’t specified -username at all and use the logged-in user’s username.