
Introduction
With a mixed environment of Windows and Mac computers, often the user experience will differ based on the platform of choice. At my current organization, we are working to minimize these differences for the end users. One noticeable difference between the two platforms that we began work on was dealing with user account pictures. On a Windows computer, the user account picture is pulled from Entra and set as the user account picture, this does not happen natively on Mac. The Mac has a separate local account created on it that is created with the help of Jamf Connect to make sure that the username is consistent with our account names in Entra. Jamf Connect also helps keep the user’s Entra account password in sync with their local Mac account, but there is no directory picture sync.
Speaking to a networking engineer friend of mine at a different organization, he stated “Man, the addition of a picture seems so small but would make such a nice impression/touch.” This is true, the user account picture is not a mission-critical item, but it does add to the overall user experience, so why not?
When looking for a solution to this I did find the following article: https://nosari20.github.io/posts/macos-m365-picture/. In this article, the author lays out a combination of using PowerShell Azure Functions and bash scripting on the local client to do this work. I set out to have an all-in-one bash script that could do the job.
Prequisite
For this bash script to work, you will need to create an Azure Enterprise application. The Tenant ID, Client ID, and ClientSecret of the app will be used for authenticating to the Microsoft Graph API. This application will need to have User.Read.All API permissions.
EntraID_Picture_Sync.bash
Assumptions for this script:
- Enterprise Application in Azure with
User.Read.Allgraph API Permissions - User accounts on Mac matches usernames in Entra, i.e.,
- local Mac username: tony.hawk
- Entra UPN: Tony.Hawk@techitout.xyz
Using The Script
Change the variable for:
- scriptLog
- tenantID
- clientID
- clientSecret
- domain
Script Breakdown
Variables and Pre-Flight
Variables:
Change these variables to fit your organizational needs
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Script Version and Application Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
scriptVersion="1.0.1"
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
scriptLog="/var/log/xyz.techitout.log"
# Azure Enterprise App Variables
tenantID="597dfe-538-7g58-867e-f8dd9dd75b19"
clientID="4217369b-3141-563f-dd61-1c07b1g7dd9e"
clientSecret="rRQ8Q~CgEdke.Q4M47FJc1HXXMrFam~7B-0jGheeY"
domain="techitout.xyz"
exitCode="0"Exit Codes:
Refer to these exit codes for script status’
########################################################################################
#
# Exit Codes
#
########################################################################################
# Exit Codes:
# 0: Clean exit, script complete
# 1: Script not ran by root or script not run in bash
# 2: Unable to generate an access token
# 3: Logged-In User's Entra user_id not found
# 4: Current user's picture is already downloadedPre-Flight:
Conducts the following tests:
– Is there a script log? If not, create one
– Defines function for updating the script log
– Defines a function for getting the current logged-in user
– Ensures that the script is running in bash
– Ensures the script is running as root
– Ensures the logged-in user is not a system account
– Caffeinate the script
########################################################################################
#
# Pre-flight Checks
#
########################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Client-side Logging
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if [[ ! -f "${scriptLog}" ]]; then
touch "${scriptLog}"
fi
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Client-side Script Logging Function
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function updateScriptLog() {
echo -e "$( date +%Y-%m-%d\ %H:%M:%S ) - ${1}" | tee -a "${scriptLog}"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Current Logged-in User Function
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function currentLoggedInUser() {
loggedInUser=$( echo "show State:/Users/ConsoleUser" | scutil | awk '/Name :/ { print $3 }' )
updateScriptLog "PRE-FLIGHT CHECK: Current Logged-in User: ${loggedInUser}"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Logging Preamble
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
updateScriptLog "\n\n###\n# Entra ID Picture Sync (${scriptVersion})\n# https://techitout.xyz\n###\n"
updateScriptLog "PRE-FLIGHT CHECK: Initiating …"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Confirm script is running under bash
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if [[ "$BASH" != "/bin/bash" ]] ; then
updateScriptLog "PRE-FLIGHT CHECK: This script must be run under 'bash', please do not run it using 'sh', 'zsh', etc.; exiting."
exit 1
fi
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Confirm script is running as root
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if [[ $(id -u) -ne 0 ]]; then
updateScriptLog "PRE-FLIGHT CHECK: This script must be run as root; exiting."
exit 1
fi
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Validate Logged-in System Accounts
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
updateScriptLog "PRE-FLIGHT CHECK: Check for Logged-in System Accounts …"
currentLoggedInUser
counter="1"
until { [[ "${loggedInUser}" != "_mbsetupuser" ]] || [[ "${counter}" -gt "180" ]]; } && { [[ "${loggedInUser}" != "loginwindow" ]] || [[ "${counter}" -gt "30" ]]; } ; do
updateScriptLog "PRE-FLIGHT CHECK: Logged-in User Counter: ${counter}"
currentLoggedInUser
sleep 2
((counter++))
done
loggedInUserFullname=$( id -F "${loggedInUser}" )
loggedInUserID=$( id -u "${loggedInUser}" )
loggedInUserUPN="${loggedInUser}@${domain}"
updateScriptLog "PRE-FLIGHT CHECK: Current Logged-in User ID: ${loggedInUserID}"
updateScriptLog "PRE-FLIGHT CHECK: Current Logged-in User UPN: ${loggedInUserUPN}"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Ensure computer does not go to sleep during EPS (thanks, @grahampugh!)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
epsPID="$$"
updateScriptLog "PRE-FLIGHT CHECK: Caffeinating this script (PID: $epsPID)"
caffeinate -dimsu -w $epsPID &
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Complete
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
updateScriptLog "PRE-FLIGHT CHECK: Complete"Functions:
This is the work that the script is doing.
– Obtaining an access token
– Obtaining the user’s directory id
– Obtaining the metadata from the user’s directory picture
– Checking to see if the directory picture exists or has been modified
– Obtaining the user’s directory picture
– Syncing the user’s local user account picture to the current directory picture
– Kill any process (i.e., caffeine)
– Clean up with the QuitScript function
########################################################################################
#
# Functions
#
########################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Obtain access token from Graph
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function get_access_token() {
updateScriptLog "INFO: Obtaining access token"
authToken=$( curl -X POST "https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "client_id=${clientID}" \
--data-urlencode 'scope=https://graph.microsoft.com/.default' \
--data-urlencode "client_secret=${clientSecret}" \
--data-urlencode 'grant_type=client_credentials' \
--silent
)
token=$(echo "${authToken}" | sed -n 's/.*"access_token":"\([^"]*\).*/\1/p')
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Get the User ID from the Graph API
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function get_user_id() {
local upn=$1
local response=$(curl -s -H "Authorization: Bearer $token" \
"https://graph.microsoft.com/beta/users('$upn')?$select=id")
local user_id=$(echo "${response}" | sed -n 's/.*"id":"\([^"]*\).*/\1/p')
echo "$user_id"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Get the User's picture metadata
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function get_picture_metadata() {
local user_id=$1
local response=$(curl -s -H "Authorization: Bearer $token" \
"https://graph.microsoft.com/v1.0/users/$user_id/photo/")
echo "$response"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Check if picture needs to be updated
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function check_picture() {
updateScriptLog "INFO: Checking to see if picture needs to be replaced or created"
pictureFolder="/Library/User Pictures/Pictures"
if [[ ! -d "${pictureFolder}/${loggedInUser}" ]]; then
updateScriptLog "INFO: Creating user picture directory"
mkdir -p "${pictureFolder}/${loggedInUser}"
updateScriptLog "INFO: Picture needs to downloaded, continuing ..."
return
fi
# Picture Folder exists, evaluate if picture needs to be replaced
if [[ -f "${pictureFolder}/${loggedInUser}/${metaTag}.jpeg" ]]; then
updateScriptLog "INFO: Picture exists, exiting ..."
exitCode="4"
quitScript
fi
updateScriptLog "INFO: Removing old picture from folder"
rm -r "${pictureFolder}/${loggedInUser}"/*
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Download the User's Picture
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function download_picture() {
updateScriptLog "INFO: Downloading picture to /tmp/$metaTag.jpeg"
curl -X GET "https://graph.microsoft.com/v1.0/users/$user_id/photo/\$value" -sS -o /tmp/$metaTag.jpeg \
-H "Content-Type: $contentType" \
-H "Authorization: Bearer $token"
updateScriptLog "INFO: Copying directory picture to user's picture folder"
cp "/tmp/$metaTag.jpeg" "${pictureFolder}/${loggedInUser}/${metaTag}.jpeg"
chmod a+rx "${pictureFolder}/${loggedInUser}/${metaTag}.jpeg"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Set the User Picture
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function set_user_picture() {
updateScriptLog "INFO: Setting picture for $loggedInUser"
updateScriptLog "INFO: Removing old user picture"
dscl . delete /Users/$loggedInUser JPEGPhoto ||
dscl . delete /Users/$loggedInUser Picture ||
updateScriptLog "INFO: Creating new user picture"
dscl . create /Users/$loggedInUser Picture "${pictureFolder}/${loggedInUser}/${metaTag}.jpeg"
picImport="$(mktemp /tmp/${loggedInUser}_dsimport.XXXXXX)"
mappings='0x0A 0x5C 0x3A 0x2C'
attrs='dsRecTypeStandard:Users 2 dsAttrTypeStandard:RecordName externalbinary:dsAttrTypeStandard:JPEGPhoto'
printf "%s %s \n%s:%s" "${mappings}" "${attrs}" "${loggedInUser}" "${pictureFolder}/${loggedInUser}/${metaTag}.jpeg" > "${picImport}"
/usr/bin/dsimport "${picImport}" /Local/Default M
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Kill a specified process (thanks, @grahampugh!)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function killProcess() {
process="$1"
if process_pid=$( pgrep -a "${process}" 2>/dev/null ) ; then
updateScriptLog "INFO: Attempting to terminate the '$process' process …"
updateScriptLog "INFO: (Termination message indicates success.)"
kill "$process_pid" 2> /dev/null
if pgrep -a "$process" >/dev/null ; then
updateScriptLog "WARNING: '$process' could not be terminated."
fi
else
updateScriptLog "INFO: The '$process' process isn't running."
fi
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Quit Script
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function quitScript() {
updateScriptLog "QUIT SCRIPT: Exiting …"
# Stop `caffeinate` process
updateScriptLog "QUIT SCRIPT: De-caffeinate …"
killProcess "caffeinate"
# Remove temp picture file
if [[ -e /tmp/$metaTag.jpeg ]]; then
updateScriptLog "QUIT SCRIPT: Removing /tmp/$metaTag.jpeg …"
rm "/tmp/$metaTag.jpeg"
fi
if [[ -e "${picImport}" ]]; then
updateScriptLog "QUIT SCRIPT: Removing ${picImport} ..."
rm "${picImport}"
fi
updateScriptLog "QUIT SCRIPT: Exiting with exit code: ${exitCode}"
exit "${exitCode}"
}Main Function:
The main function of the script calls all the other work functions and provides some checks along the way
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Main
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function main() {
# Get an bearer token
get_access_token
# Verify access token was received
if [ -z "$token" ]; then
updateScriptLog "FAILURE: Failed to acquire token"
exitCode="2"
quitScript
else
updateScriptLog "INFO: Token acquired successfully"
fi
user_id=$(get_user_id "${loggedInUserUPN}")
# Check to see if we got a valid user_id
if [[ -z "$user_id" ]]; then
updateScriptLog "WARNING: No user ID found"
exitCode="3"
quitScript
else
updateScriptLog "INFO: Found user ID, continuing ..."
fi
updateScriptLog "INFO: User ID: $user_id"
metadata=$(get_picture_metadata "${user_id}")
contentType=$(echo "$metadata" | sed -n 's/.*"@odata.mediaContentType":"\([^"]*\).*/\1/p')
metaTag=$(echo "$metadata" | sed -n 's/.*"@odata.mediaEtag":"W\/\\\"\(.*\)\\\".*/\1/p' )
updateScriptLog "INFO: Metadata: $metadata"
updateScriptLog "INFO: Metatag: $metaTag"
updateScriptLog "INFO: Content Type: $contentType"
check_picture
download_picture
set_user_picture
quitScript
}Putting it all together
#!/bin/bash
###########################################################################################
#
# Entra ID Picture Sync
# Sync Local User Picture to Entra Directory Picture
# https://techitout.xyz
#
###########################################################################################
#
# HISTORY
#
# Version 1.0.0, 05.07.2024 @robjschroeder
# - Original script creation (adopted from: https://nosari20.github.io/posts/macos-m365-picture/)
# - ** Note ** Enterprise Application in Azure needs Microsoft Graph API permissions
# - User.Read.All
#
# Version 1.0.1, 05.08.2024 @robjschroeder
# - Added check to exit if user_id is not found
# - Added checks to see if picture already exists or needs to be replaced
#
###########################################################################################
###########################################################################################
#
# Global Variables
#
###########################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Script Version and Application Variables
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
scriptVersion="1.0.1"
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
scriptLog="/var/log/xyz.techitout.log"
# Azure Enterprise App Variables
tenantID="597dfe-538-7g58-867e-f8dd9dd75b19"
clientID="4217369b-3141-563f-dd61-1c07b1g7dd9e"
clientSecret="rRQ8Q~CgEdke.Q4M47FJc1HXXMrFam~7B-0jGheeY"
domain="techitout.xyz"
exitCode="0"
###########################################################################################
#
# Exit Codes
#
###########################################################################################
# Exit Codes:
# 0: Clean exit, script complete
# 1: Script not ran by root or script not run in bash
# 2: Unable to generate an access token
# 3: Logged-In User's Entra user_id not found
# 4: Current user's picture is already downloaded
###########################################################################################
#
# Pre-flight Checks
#
###########################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Client-side Logging
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if [[ ! -f "${scriptLog}" ]]; then
touch "${scriptLog}"
fi
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Client-side Script Logging Function
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function updateScriptLog() {
echo -e "$( date +%Y-%m-%d\ %H:%M:%S ) - ${1}" | tee -a "${scriptLog}"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Current Logged-in User Function
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function currentLoggedInUser() {
loggedInUser=$( echo "show State:/Users/ConsoleUser" | scutil | awk '/Name :/ { print $3 }' )
updateScriptLog "PRE-FLIGHT CHECK: Current Logged-in User: ${loggedInUser}"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Logging Preamble
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
updateScriptLog "\n\n###\n# Entra ID Picture Sync (${scriptVersion})\n# https://techitout.xyz\n###\n"
updateScriptLog "PRE-FLIGHT CHECK: Initiating …"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Confirm script is running under bash
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if [[ "$BASH" != "/bin/bash" ]] ; then
updateScriptLog "PRE-FLIGHT CHECK: This script must be run under 'bash', please do not run it using 'sh', 'zsh', etc.; exiting."
exit 1
fi
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Confirm script is running as root
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
if [[ $(id -u) -ne 0 ]]; then
updateScriptLog "PRE-FLIGHT CHECK: This script must be run as root; exiting."
exit 1
fi
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Validate Logged-in System Accounts
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
updateScriptLog "PRE-FLIGHT CHECK: Check for Logged-in System Accounts …"
currentLoggedInUser
counter="1"
until { [[ "${loggedInUser}" != "_mbsetupuser" ]] || [[ "${counter}" -gt "180" ]]; } && { [[ "${loggedInUser}" != "loginwindow" ]] || [[ "${counter}" -gt "30" ]]; } ; do
updateScriptLog "PRE-FLIGHT CHECK: Logged-in User Counter: ${counter}"
currentLoggedInUser
sleep 2
((counter++))
done
loggedInUserFullname=$( id -F "${loggedInUser}" )
loggedInUserID=$( id -u "${loggedInUser}" )
loggedInUserUPN="${loggedInUser}@${domain}"
updateScriptLog "PRE-FLIGHT CHECK: Current Logged-in User ID: ${loggedInUserID}"
updateScriptLog "PRE-FLIGHT CHECK: Current Logged-in User UPN: ${loggedInUserUPN}"
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Ensure computer does not go to sleep during EPS (thanks, @grahampugh!)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
epsPID="$$"
updateScriptLog "PRE-FLIGHT CHECK: Caffeinating this script (PID: $epsPID)"
caffeinate -dimsu -w $epsPID &
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Pre-flight Check: Complete
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
updateScriptLog "PRE-FLIGHT CHECK: Complete"
###########################################################################################
#
# Functions
#
###########################################################################################
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Obtain access token from Graph
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function get_access_token() {
updateScriptLog "INFO: Obtaining access token"
authToken=$( curl -X POST "https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "client_id=${clientID}" \
--data-urlencode 'scope=https://graph.microsoft.com/.default' \
--data-urlencode "client_secret=${clientSecret}" \
--data-urlencode 'grant_type=client_credentials' \
--silent
)
token=$(echo "${authToken}" | sed -n 's/.*"access_token":"\([^"]*\).*/\1/p')
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Get the User ID from the Graph API
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function get_user_id() {
local upn=$1
local response=$(curl -s -H "Authorization: Bearer $token" \
"https://graph.microsoft.com/beta/users('$upn')?$select=id")
local user_id=$(echo "${response}" | sed -n 's/.*"id":"\([^"]*\).*/\1/p')
echo "$user_id"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Get the User's picture metadata
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function get_picture_metadata() {
local user_id=$1
local response=$(curl -s -H "Authorization: Bearer $token" \
"https://graph.microsoft.com/v1.0/users/$user_id/photo/")
echo "$response"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Check if picture needs to be updated
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function check_picture() {
updateScriptLog "INFO: Checking to see if picture needs to be replaced or created"
pictureFolder="/Library/User Pictures/Pictures"
if [[ ! -d "${pictureFolder}/${loggedInUser}" ]]; then
updateScriptLog "INFO: Creating user picture directory"
mkdir -p "${pictureFolder}/${loggedInUser}"
updateScriptLog "INFO: Picture needs to downloaded, continuing ..."
return
fi
# Picture Folder exists, evaluate if picture needs to be replaced
if [[ -f "${pictureFolder}/${loggedInUser}/${metaTag}.jpeg" ]]; then
updateScriptLog "INFO: Picture exists, exiting ..."
exitCode="4"
quitScript
fi
updateScriptLog "INFO: Removing old picture from folder"
rm -r "${pictureFolder}/${loggedInUser}"/*
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Download the User's Picture
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function download_picture() {
updateScriptLog "INFO: Downloading picture to /tmp/$metaTag.jpeg"
curl -X GET "https://graph.microsoft.com/v1.0/users/$user_id/photo/\$value" -sS -o /tmp/$metaTag.jpeg \
-H "Content-Type: $contentType" \
-H "Authorization: Bearer $token"
updateScriptLog "INFO: Copying directory picture to user's picture folder"
cp "/tmp/$metaTag.jpeg" "${pictureFolder}/${loggedInUser}/${metaTag}.jpeg"
chmod a+rx "${pictureFolder}/${loggedInUser}/${metaTag}.jpeg"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Set the User Picture
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function set_user_picture() {
updateScriptLog "INFO: Setting picture for $loggedInUser"
updateScriptLog "INFO: Removing old user picture"
dscl . delete /Users/$loggedInUser JPEGPhoto ||
dscl . delete /Users/$loggedInUser Picture ||
updateScriptLog "INFO: Creating new user picture"
dscl . create /Users/$loggedInUser Picture "${pictureFolder}/${loggedInUser}/${metaTag}.jpeg"
picImport="$(mktemp /tmp/${loggedInUser}_dsimport.XXXXXX)"
mappings='0x0A 0x5C 0x3A 0x2C'
attrs='dsRecTypeStandard:Users 2 dsAttrTypeStandard:RecordName externalbinary:dsAttrTypeStandard:JPEGPhoto'
printf "%s %s \n%s:%s" "${mappings}" "${attrs}" "${loggedInUser}" "${pictureFolder}/${loggedInUser}/${metaTag}.jpeg" > "${picImport}"
/usr/bin/dsimport "${picImport}" /Local/Default M
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Kill a specified process (thanks, @grahampugh!)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function killProcess() {
process="$1"
if process_pid=$( pgrep -a "${process}" 2>/dev/null ) ; then
updateScriptLog "INFO: Attempting to terminate the '$process' process …"
updateScriptLog "INFO: (Termination message indicates success.)"
kill "$process_pid" 2> /dev/null
if pgrep -a "$process" >/dev/null ; then
updateScriptLog "WARNING: '$process' could not be terminated."
fi
else
updateScriptLog "INFO: The '$process' process isn't running."
fi
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Quit Script
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function quitScript() {
updateScriptLog "QUIT SCRIPT: Exiting …"
# Stop `caffeinate` process
updateScriptLog "QUIT SCRIPT: De-caffeinate …"
killProcess "caffeinate"
# Remove temp picture file
if [[ -e /tmp/$metaTag.jpeg ]]; then
updateScriptLog "QUIT SCRIPT: Removing /tmp/$metaTag.jpeg …"
rm "/tmp/$metaTag.jpeg"
fi
if [[ -e "${picImport}" ]]; then
updateScriptLog "QUIT SCRIPT: Removing ${picImport} ..."
rm "${picImport}"
fi
updateScriptLog "QUIT SCRIPT: Exiting with exit code: ${exitCode}"
exit "${exitCode}"
}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Main
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
function main() {
# Get an bearer token
get_access_token
# Verify access token was received
if [ -z "$token" ]; then
updateScriptLog "FAILURE: Failed to acquire token"
exitCode="2"
quitScript
else
updateScriptLog "INFO: Token acquired successfully"
fi
user_id=$(get_user_id "${loggedInUserUPN}")
# Check to see if we got a valid user_id
if [[ -z "$user_id" ]]; then
updateScriptLog "WARNING: No user ID found"
exitCode="3"
quitScript
else
updateScriptLog "INFO: Found user ID, continuing ..."
fi
updateScriptLog "INFO: User ID: $user_id"
metadata=$(get_picture_metadata "${user_id}")
contentType=$(echo "$metadata" | sed -n 's/.*"@odata.mediaContentType":"\([^"]*\).*/\1/p')
metaTag=$(echo "$metadata" | sed -n 's/.*"@odata.mediaEtag":"W\/\\\"\(.*\)\\\".*/\1/p' )
updateScriptLog "INFO: Metadata: $metadata"
updateScriptLog "INFO: Metatag: $metaTag"
updateScriptLog "INFO: Content Type: $contentType"
check_picture
download_picture
set_user_picture
quitScript
}
main