azurepowershellmicrosoft-graph-apiexchange-servermicrosoft-entra-id

Get-MgGroupMember by display name instead of userID


I am currently in the process of replacing an old script that used Exchange Online Powershell.

The old script was very simple and only performed 2 tasks.

  1. Downloaded user photos that had custom attribute 3 set to 'employee' + Had a picture from EXO
  2. upload the downloaded user photo to local AD.

The old script was using an app registration for connecting to exchange online powershell through entra. Now that Get-UserPhoto along with other cmdlets from EXO Powershell are depricated, I have started learning how to use MSGraph Powershell instead.

I have gotten the user photos to download based on being in the 'Staff' group in Azure + having a photo available to download. However, I have only been able to query users by User ID, then name the download photos as that same User ID. This would solve part 1 of the old script of downloading the photo from azure.

The issue I am running into is with part 2 of the old script. I cant use the photos named with User ID to upload into local AD. Microsoft only allows certain attriubtes when using Set-ADUser -Identity such as princal name or upn.

How can I get the photos downloaded to download using the displayName, PrincapalName, or UPN of the user instead of the UserID

tl;dr - I need the user photos downloaded from Azure to be named by displayName, userPrincipalName, or UPN.

Old Script

Function LogWrite
{
   Param ([string]$logstring)

   Add-content $logpath -value $logstring
}

## Get a list of users who have a picture and are an employee ##
Write-Host "Getting a list of users who have a photo and extensionAttribute3 set to employee. This takes a few minutes" |Out-File -Append $logpath
$UsersWithPics = Get-Mailbox -Identity * -ResultSize Unlimited | Where-Object {$_.HasPicture -eq "True" -and $_.CustomAttribute3 -eq "Employee"}

## Download photos from O365
foreach ($Usr in $UsersWithPics) {
    If($Usr.UserPrincipalName){
        $path = $folderPath+$Usr.UserPrincipalName+".jpg"
        $photo = Get-Userphoto -identity $Usr.UserPrincipalName -ErrorAction SilentlyContinue
            If($photo.PictureData -ne $null){
                [io.file]::WriteAllBytes($path,$photo.PictureData)
                Write-Host "Success: Downloaded Pic for $Usr"
                LogWrite "Success: Downloaded Pic for $Usr"
        }
    }   
}


## Update Local AD profile pic with ones from O365
###Shows users who have a local ad pic: get-aduser -Filter 'thumbnailPhoto -like "*"' -Properties thumbnailPhoto |Select Name,thumbnailPhoto |Measure-Object
$LocalUsersWithPics = $UsersWithPics | ForEach-Object { $UPN = $_.UserPrincipalName; Get-ADUser -Filter { UserPrincipalName -Eq $UPN } }
foreach ($Usr in $LocalUsersWithPics) {
    $photopath = $folderpath+$Usr.UserPrincipalName+".jpg"
    $photo = [byte[]](Get-Content $photopath -Encoding byte)
    Set-Aduser $Usr.SamAccountName -Replace @{thumbnailPhoto=$photo}
    Write-host "Setting Photo: " $photopath "-" "User Getting Photo:" $Usr.SamAccountName
    LogWrite "Setting Photo: $photopath - User Getting Photo: $Usr"
}


## Properlly close exchange online connection ##
Disconnect-ExchangeOnline -Confirm:$false

## Remove Logs older than $logLimitDays days ##
Get-childitem -path $logpathDir -Recurse -Force |? { !$_.PSIsContainer -and $_.CreationTime -lt $logLimitDays} |Remove-Item -Force

New Script (In Progress, functionally finds users in the Staff Azure group, checks if they have a photo, then downloads the photo while naming it the long UserID String of text)

# Define a LogWrite function to append logs to a file
Function LogWrite {
    Param ([string]$logstring)
    Add-content $logpath -value $logstring
}

Write-Host "Getting a list of users who are members of the Staff group. This takes a few minutes" |Out-File -Append $logpath

# Define the group ID for the staff group
$staffGroupId = "*Group ID*"

# Get all members of the staff group
## $groupMembers = Get-MgGroupMember -GroupId $staffGroupId -All -Property "id,displayName,onPremisesExtensionAttributes"
$groupMembers = Get-MgGroupMember -GroupId $staffGroupId -All | Select-Object Id, DisplayName, UserPrincipalName

# Assuming $groupMembers contains the members of the "Staff" group
foreach ($member in $groupMembers) {
    $userId = $member.Id
    try {
        # Define the file path for the photo
        $filePath = Join-Path $folderpath "$userid.jpeg"
        
        # Use the Outfile parameter to specify where the photo should be saved
        Get-MgUserPhotoContent -UserId $userId -Outfile $filePath -ErrorAction Stop
        LogWrite "User $userId photo has been downloaded to $filePath."
    } catch {
        LogWrite "User $userId does not have a profile photo or an error occurred."
    }
}

Things I have tried

I had found a pretty odd workaround to get userprincalname to appear in the query with Get-MgGroupMember, but I was able to pass the userprincalname through to name the photo

$groupMembers = Get-MgGroupMember -GroupId $staffGroupID -All | Select @{label="DisplayName";expression = {$.AdditionalProperties.displayName} }, Id, @{label="Mail";expression = {$.AdditionalProperties.mail} }, @{label="UserPrincipalName";expression = {$_.AdditionalProperties.userPrincipalName} }

I tried changing the file path and file name using this $filePath = Join-Path $folderpath "$($member.DisplayName).jpeg". I continually got the error WARNING: C:\'filepath'\.jpeg already exists. The file will be overridden.


Solution

  • From feedback in comments I understand the issue is that Get-MgGroupMember is not giving you the user's displayName or userPrincipalName so that you can use one of these values as a file name. I'm personally not aware of how this cmdlet behaves, I don't use any cmdlet from the Graph Module, none of them are worth something, except for Invoke-MgGraphRequest for direct API calls.

    If you query directly the List group members endpoint you will get these 2 required properties, you can also use what is known as OData cast to get only members of the group that are of type microsoft.graph.user to make the query more efficient. From there, for each user member you can make a call to the Get profilePhoto endpoint to extract their profile pic.

    This is how the code would look:

    $staffGroupId = 'xxxx-xxxx-xxxx-xxxx'
    $uri = "v1.0/groups/$staffGroupId/members/microsoft.graph.user?`$select=id, displayName, userPrincipalName"
    
    do {
        $groupMembers = Invoke-MgGraphRequest GET $uri
        $uri = $groupMembers['@odata.nextLink']
    
        foreach ($user in $groupMembers['value']) {
            $upn = $user['userPrincipalName'] # here you can choose `$user['displayName']` too
    
            $invokeMgGraphRequestSplat = @{
                OutputFilePath = "$upn.jpg"
                Method         = 'GET'
                Uri            = "v1.0/users/$upn/photo/`$value"
            }
            Invoke-MgGraphRequest @invokeMgGraphRequestSplat
        }
    }
    while ($uri)