Payton Flint's Tech Blog
Menu
  • Home
  • Blog
  • Categories
  • Resources
  • About
  • Contact
Menu

Cloud- Synchronize Profile Photos Between Federated Domains (Google, Azure)

Posted on February 24, 2024February 24, 2024 by paytonflint

Synchronization of profile photos between Google’s G Suite and Azure is not supported by the built-in Azure Provisioning engine. This must be accomplished by some other means. In my case, of course, I look to employ PowerShell for such automation tasks.

As a prerequisite, you should have already followed PSGSuite’s intial setup procedure documented on their website (https://psgsuite.io/Initial%20Setup/). This is to both set up PSGSuite application, and to configure your device to communicate with it properly. Additionally, you must have permissions within Azure to use Microsoft Graph, and have installed the Microsoft.Graph module.

This script works unidirectionally from Google > Azure. It downloads the profile photo for each user using PSGSuite. Then, it stores the photo in a local repository. No worries, though, these photos are highly compressed and low-resolution. Storage space was on the order of MB, not GB for a very large domain. The purpose of the local repository is to enable the script to evaluate for any changes, and only upload the delta to Azure/Entra via Microsoft Graph. It accomplishes this by generating a hash for the new photo and the corresponding photo in the local repository by the same name, then compares the two. This keeps us from making unnecessary uploads via Microsoft Graph.

A disclaimer, this script takes quite a while to run for a larger domain. The limiting factor here is the rate that either Google or PSGSuite supplies the images, everything else is pretty well negligible.

Here is the script, and the GitHub link: https://github.com/p8nflnt/Cloud-Toolbox/blob/main/Sync-GSuiteUserImagesToAzure.ps1

<#
.SYNOPSIS
    Synchronize user profile photos from GSuite to Azure. Requires PSGSuite/Microsoft.Graph

.NOTES
    Name: Sync-GSuiteUserImagesToAzure
    Author: Payton Flint
    Version: 1.0
    DateCreated: 2024-February

.LINK
    https://github.com/p8nflnt/Cloud-Toolbox/blob/main/Sync-GSuiteUserImagesToAzure.ps1
    
Cloud- Synchronize Profile Photos Between Federated Domains (Google, Azure)
#> # clear variables for repeatability Get-Variable -Exclude PWD,*Preference | Remove-Variable -EA 0 # identify location of script $scriptPath = Split-Path ($MyInvocation.MyCommand.Source) -Parent #==================================================================================================================== # specify domain portion found in target user principal names $upnDomain = "[email protected]" # connect to ms graph w/ appropriate permissions Connect-MgGraph -Scopes 'User.ReadWrite.All' -NoWelcome #==================================================================================================================== # set inbound image cache path $inboundImgCache = Join-Path $scriptPath "inboundImgCache" # remove inbound image cache directory if it exists If (Test-Path $inboundImgCache) { Remove-Item -Path $inboundImgCache -Recurse -Force -ErrorAction SilentlyContinue } # create inbound image cache directory New-Item -Path $inboundImgCache -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null # set image repository path $imgRepo = Join-Path $ScriptPath "imgRepo" # create image repository directory if it does not exist If (!(Test-Path $imgRepo)) { New-Item -Path $imgRepo -ItemType Directory -Force -ErrorAction SilentlyContinue } #==================================================================================================================== # function to compare image hashes function Compare-ImageHash { param ( [string]$ImagePath1, [string]$ImagePath2, [string]$Algorithm = "SHA256" ) $hasher = [System.Security.Cryptography.HashAlgorithm]::Create($Algorithm) $hash1Bytes = $hasher.ComputeHash([System.IO.File]::ReadAllBytes($ImagePath1)) $hash1 = -join ($hash1Bytes | ForEach-Object { $_.ToString("x2") }) $hash2Bytes = $hasher.ComputeHash([System.IO.File]::ReadAllBytes($ImagePath2)) $hash2 = -join ($hash2Bytes | ForEach-Object { $_.ToString("x2") }) # directly returns true if the hashes match (images are identical), false otherwise return $hash1 -eq $hash2 } # end Compare-ImageHash function # function to handle updated images function HandleImageUpdate { param ( [Parameter(Mandatory=$true)] [string]$cachedImgPath, # Path to the cached image [Parameter(Mandatory=$true)] [string]$cachedImgRepoPath, # Destination path in the image repository [Parameter(Mandatory=$true)] [string]$userUPN, # user principal name [Parameter(Mandatory=$true)] [string]$fileExt # image format file extension ) # prod ms graph url $graphUrl = "https://graph.microsoft.com/v1.0" # get entra user id via ms graph, silence errors $userId = (Get-MgUser -UserId $userUPN 2>$null) | Select-Object -ExpandProperty Id # if userId found in destination domain... if ($userId -ne $null) { # copy image from inbound cache to image repository, overwriting it if present Copy-Item -Path $cachedImgPath -Destination $cachedImgRepoPath -Force -ErrorAction SilentlyContinue # upload cached image as user profile photo via ms graph Invoke-MgGraphRequest -Method "PUT" -Uri $graphUrl/users/$userId/photo/`$value -InputFilePath $cachedImgPath -ContentType "image/$fileExt" # remove cached image Remove-Item -Path $cachedImgPath -Force -ErrorAction SilentlyContinue # if no user id found in destination domain... } else { # remove cached image Remove-Item -Path $cachedImgPath -Force -ErrorAction SilentlyContinue } } # end HandleImageUpdate function #==================================================================================================================== # get UPNs from source domain $srcUPNs = Get-GSUserList | Select-Object -ExpandProperty User # percentage calculation variables $total = $srcUPNs.Count $processed = 0 # for each UPN... $srcUPNs | ForEach-Object { # Increment the processed count $processed++ # Calculate the percentage of completion $percentage = ($processed / $total) * 100 # Update the progress bar using Write-Progress Write-Progress -Activity "Processing Users" -Status "$($percentage.ToString("0.00"))% Complete" -PercentComplete $percentage # get image for UPN and place in inbound image cache Get-GSUserPhoto -User $_ -OutFilePath $inboundImgCache -ErrorAction SilentlyContinue # get cached image filename (if any) $cachedImgName = (Get-ChildItem -Path $inboundImgCache).Name # if a user photo is returned to the cache... if ($cachedImgName -ne $null) { # get file extension $cachedImgExt = [System.IO.Path]::GetExtension($cachedImgName).TrimStart('.') # build cached image file path $cachedImgPath = Join-Path $inboundImgCache $cachedImgName # build file path to check image repository for cached image filename $cachedImgRepoPath = Join-Path $imgRepo $cachedImgName # check if cached image filename exists in image repository if (Test-Path $cachedImgRepoPath) { # compare cached image to repository image $imgCompareResult = Compare-ImageHash -ImagePath1 $cachedImgPath -ImagePath2 $cachedImgRepoPath # if cached image is identical to the repository image... if ($imgCompareResult -eq $true) { # remove cached image Remove-Item -Path $cachedImgPath -Force -ErrorAction SilentlyContinue # if cached image is not identical to the repository image... } else { # copy image to repository (potentially overwriting existing files by the same name), upload photo via ms graph, and clear the inbound cache HandleImageUpdate -cachedImgPath $cachedImgPath -cachedImgRepoPath $cachedImgRepoPath -userUPN $_ -fileExt $cachedImgExt } # if cached image does not exist in image repository } else { # copy image to repository (potentially overwriting existing files by the same name), upload photo via ms graph, and clear the inbound cache HandleImageUpdate -cachedImgPath $cachedImgPath -cachedImgRepoPath $cachedImgRepoPath -userUPN $_ -fileExt $cachedImgExt } } }

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

About The Author

Author's Portrait

In my journey as a technologist and 11 years of experience as an IT professional, I have found my niche as Director of Infrastructure Services; developing my skillsets in management, scripting, cloud infrastructure, identity management, and networking.

I have experience as a Systems Administrator and Engineer for large enterprises including the DoD, government agencies, and a nuclear-generation site.

I've been blessed to collaborate with engineers at esteemed Fortune 50 corporations, and one of Africa's largest, to ensure successful implementation of my work.

GitHub Button

Credentials

M365 Endpoint Administrator Associate
M365 Fundamentals
Microsoft AZ-900
CompTIA CSIS
CompTIA CIOS
CompTIA Security+
CompTIA Network+
CompTIA A+
  • April 2025
  • December 2024
  • November 2024
  • October 2024
  • September 2024
  • August 2024
  • May 2024
  • April 2024
  • March 2024
  • February 2024
  • January 2024
  • December 2023
  • November 2023
  • October 2023
  • September 2023
  • August 2023
  • July 2023
  • June 2023
  • May 2023
  • April 2023
  • March 2023
  • February 2023
  • January 2023
  • December 2022
  • November 2022
  • October 2022
  • September 2022
  • August 2022
© 2022 Payton Flint | The views and opinions expressed on this website belong solely to the author/owner and do not represent the perspectives of any individuals, institutions, or organizations, whether affiliated personally or professionally, unless explicitly stated otherwise. The content and products on this website are provided as-is with no warranties or guaranties, are for informational/demonstrative purposes only, do not constitute professional advice, and are not to be used maliciously. The author/owner is not responsible for any consequences arising from actions taken based on information provided on this website, nor from the use/misuse of products from this site. All trademarks are the property of their respective owners.