Entra ID Directory Picture Sync with macOS

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.All graph 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

Bash
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# 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’

Bash
########################################################################################
#
# 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:

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

Bash
########################################################################################
#
# 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

Bash
########################################################################################
#
# 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

Bash
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# 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

Bash
#!/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

Leave a Reply

Discover more from Tech IT Out

Subscribe now to keep reading and get access to the full archive.

Continue reading