Dodawanie zdjęć użytkowników w Active Directory i Exchange 2019

Przeczytasz to w: 6 minut

Nie wiem jak Wam, ale mi się taki klimat podoba. Niestandardowe tło z logiem firmy, zdjęcie pracownika na ekranie logowania przy koncie użytkownika…to sprawia, że jak ktoś patrzy na takie środowisko z boku to myśli, że wdrażał to jakiś profesjonalista, który dba o wszystkie szczegóły i w sumie w mojej pracy to jest to, do czego dążę, czyli być najlepszym w swoim rodzaju. Okej, ale dosyć lizania własnych jajek, przejdźmy do konkretów, czyli do tego jak to wdrożyć – zdjęcia użytkowników w AD i w Exchange 2019.
Exchange to taki dodatek do zdjęć użytkowników – tutaj ponownie poczta wygląda profesjonalnie w organizacji, gdy jest wszystko tak poukładane. Efekt w OWA (Outlook Web Access) wygląda tak:

Do tego będą nam potrzebne zdjęcia naszych użytkowników i te zdjęcia muszą być kwadratowe, czyli np. być rozmiaru 500x500px. Nie ma znaczenia czy te zdjęcie jest duże czy małe (byle nie za małe, bo wtedy będzie pikseloza, a nie twarze użytkowników) dlatego, bo i tak te zdjęcia zostaną w pewnym stopniu skompresowane. Zdjęcia, które ja wykorzystałem są z Unsplash i przedstawiają modelów w moim testowym środowisku. Do wycięcia zdjęć można użyć program wg własnego uznania, np. IrfanView czy Adobe Photoshop. Następnie takie pliki użytkowników należy jakoś nazwać. Najlepszym rozwiązaniem jest nazwanie plików imieniem i nazwiskiem, ponieważ wtedy możemy je wykorzystać lepiej w programie, którym będziemy przypisywać zdjęcia do użytkowników w AD, przykład: Radosław.Serba.jpg. Ja skorzystałem z nazwy sAMAccountName, czyli w moim przypadku rserba.jpg (niestety ta opcja nie pozwala na automatyczne dopasowanie obrazków do kont użytkowników w CodeTwo Active Directory Photos.

Zdjęcia w AD

Ogólnie zdjęcie użytkownika jest przechowywane w atrybucie użytkownika thumbnailPhoto i w plaintext można zobaczyć tylko w krótkim fragmencie:

Get-ADUser -Identity Administrator -Properties thumbnailPhoto


DistinguishedName : CN=Administrator,CN=Users,DC=serba,DC=local
Enabled           : True
GivenName         :
Name              : Administrator
ObjectClass       : user
ObjectGUID        : 9dfaf3bb-fcc9-4f09-be3f-b35b95f7f278
SamAccountName    : Administrator
SID               : S-1-5-21-723521058-2419329218-2805022534-500
Surname           :
thumbnailPhoto    : {137, 80, 78, 71...}
UserPrincipalName : Administrator@serba.local

W takim razie jak ustawić taki atrybut? Są dwa rozwiązania: przez PowerShell lub za pomocą fajnej aplikacji zrobionej przez CodeTwo.

PowerShell:

Set-ADUser rserba -Replace @{thumbnailPhoto=([byte[]](Get-Content "C:\zdjecia\rserba.jpg" -Encoding byte))}

CodeTwo Active Directory Photos:

Sytuacja jest prosta: instalujemy program z linka, odpalamy, wybieramy OU, w którym chcemy ustawić zdjęcia i zaznaczamy konta, w których chcemy ustawić avatarki, a następnie klikamy Import.

Tutaj wszystko zależy od tego jaki mamy schemat nazw plików obrazków. Jeśli trzymaliśmy się schematu, który proponowałem na początku to wystarczy, że pomiędzy {First name} i {Last name} wstawimy kropkę, więc pole będzie wypełnione tak:
{First name}.{Last name}
Po tym możemy kliknąć przycisk Automatch >.

Co prawda, ja tu już zdjęcia mam, ale to, co mamy tutaj na ekranie to scenariusz, w którym zdjęcia mogły nie być dopasowane i musimy je dopasować ręcznie, więc wystarczy przeciągnąć obrazki z prawej strony do odpowiedniego użytkownika, a następnie kliknąć Next >. Po tym wystarczy sprawdzić, czy zmiany, które chcemy zastosować nam pasują i zaznaczyć checkbox Apply the above settings to all selected images. Wartość jest ustawiona na 100 KB nieprzypadkowo, ponieważ to jest limit wielkości obrazka przechowywany w atrybcie thumbnailPhoto użytkownika.

Gdy to zatwierdzimy to zdjęcia się zaimportują i jedną część mamy gotową. Drugą częścią jest podpięcie gotowego skryptu, który będzie pobierał zdjęcia użytkowników przy wylogowywaniu się. Bez niego obrazki będą się importowały nam na maszynach tylko i wyłącznie, gdy posiadamy na nich lokalne uprawnienia administracyjne, czyli w sumie będzie to działać tylko dla adminów. Mowa tutaj o skrypcie, który także jest na stronie CodeTwo:

[CmdletBinding(SupportsShouldProcess = $true)]Param()
function Test-Null($InputObject) { return !([bool]$InputObject) }
Function ResizeImage() {
    param([String]$ImagePath, [Int]$Quality = 90, [Int]$targetSize, [String]$OutputLocation)
    Add-Type -AssemblyName "System.Drawing"
    $img = [System.Drawing.Image]::FromFile($ImagePath)
    $CanvasWidth = $targetSize
    $CanvasHeight = $targetSize
    #Encoder parameter for image quality
    $ImageEncoder = [System.Drawing.Imaging.Encoder]::Quality
    $encoderParams = New-Object System.Drawing.Imaging.EncoderParameters(1)
    $encoderParams.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter($ImageEncoder, $Quality)
    # get codec
    $Codec = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where { $_.MimeType -eq 'image/jpeg' }
    #compute the final ratio to use
    $ratioX = $CanvasWidth / $img.Width;
    $ratioY = $CanvasHeight / $img.Height;
    $ratio = $ratioY
    if ($ratioX -le $ratioY) {
        $ratio = $ratioX
    }
    $newWidth = [int] ($img.Width * $ratio)
    $newHeight = [int] ($img.Height * $ratio)
    $bmpResized = New-Object System.Drawing.Bitmap($newWidth, $newHeight)
    $graph = [System.Drawing.Graphics]::FromImage($bmpResized)
    $graph.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
    $graph.Clear([System.Drawing.Color]::White)
    $graph.DrawImage($img, 0, 0, $newWidth, $newHeight)
    #save to file
    $bmpResized.Save($OutputLocation, $Codec, $($encoderParams))
    $bmpResized.Dispose()
    $img.Dispose()
}
#get sid and photo for current user
$user = ([ADSISearcher]"(&(objectCategory=User)(SAMAccountName=$env:username))").FindOne().Properties
$user_photo = $user.thumbnailphoto
$user_sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value
Write-Verbose "Updating account picture for $($user.displayname)..."
#continue if an image was returned
If ((Test-Null $user_photo) -eq $false) {
    Write-Verbose "Success. Photo exists in Active Directory."
    #set up image sizes and base path
    $image_sizes = @(32, 40, 48, 96, 192, 200, 240, 448)
    $image_mask = "Image{0}.jpg"
    $image_base = "C:\ProgramData\AccountPictures"
    #set up registry
    $reg_base = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AccountPicture\Users\{0}"
    $reg_key = [string]::format($reg_base, $user_sid)
    $reg_value_mask = "Image{0}"
    If ((Test-Path -Path $reg_key) -eq $false) { New-Item -Path $reg_key }
    #save images, set reg keys
    ForEach ($size in $image_sizes) {
        #create hidden directory, if it doesn't exist
        $dir = $image_base + "\" + $user_sid
        If ((Test-Path -Path $dir) -eq $false) { $(mkdir $dir).Attributes = "Hidden" }
        #save photo to disk, overwrite existing files
        $file_name = ([string]::format($image_mask, $size))
        $pathtmp = $dir + "\_" + $file_name
        $path = $dir + "\" + $file_name
        Write-Verbose " saving: $file_name"
        $user_photo | Set-Content -Path $pathtmp -Encoding Byte -Force
        ResizeImage $pathtmp $size $size $path
        Remove-Item $pathtmp
        #save the path in registry, overwrite existing entries
        $name = [string]::format($reg_value_mask, $size)
        $value = New-ItemProperty -Path $reg_key -Name $name -Value $path -Force
    }
    Write-Verbose "Done."
}
else { Write-Error "No photo found in Active Directory for $env:username" }

Taki skrypt zapisujemy w pliku, ja nazwałem go UserProfilePicture.ps1 i umieściłem go w folderze NETLOGON. Ten folder jest synchronizowany pomiędzy wszystkimi kontrolerami domeny. Następnie należy stworzyć politykę GPO, w której definiujemy ten skrypt jako skrypt uruchamiany przy wylogowaniu użytkownika, więc należy przejść do Konfiguracja użytkownika > Ustawienia systemu Windows > Skrypty (logowanie/wylogowywanie) > Wylogowywanie.

Następnie, w zakładce Skrypty PowerShell należy wskazać ścieżkę do pliku. Jeśli plik jest umieszczony bezpośrednio w NETLOGON, ścieżka powinna być //<nazwa-domeny>/NETLOGON/<nazwa-skryptu> i tak też jest w moim przypadku.

Po dodaniu można zapisać i zamknąć wszystkie okna, a następnie poczekać, aż polityki się zaaktualizują na komputerach. Po tym kwestia zdjęć na komputerach jest załatwiona.

Zdjęcia w Exchange 2019

Obstawiam, że sposób działania zdjęć jest mniej więcej taki sam w starszych wersjach Exchange 2019 (mam na myśli Exchange 2016 i 2013, bo w 2010 zmiany są ponoć spore). Do wrzucenia zdjęć możemy wykorzystać poniższy skrypt, lecz będziemy w nim musieli zmienić główną gałąź naszego OU, w którym przechowujemy użytkowników (linia 3) oraz ścieżkę do folderu, w którym przechowujemy pliki (linia 7). Poniżej opcja skryptu dla wykorzystywania nazw sAMAccountName:

Import-Module activedirectory

$users = Get-ADUser -Filter "ObjectClass -eq 'user'" -SearchBase 'OU=it.supra.tf,DC=serba,DC=local'
foreach($user in $users)
{
    $samaccountname = $user.SamAccountName
    Set-UserPhoto -Identity $user.SamAccountName -PictureData ([System.IO.File]::ReadAllBytes("C:\Users\rserba.SERBA\Desktop\it.supra.tf\$samaccountname.jpg")) -Confirm:$false
}

Ponadto druga opcja z imionami i nazwiskami oddzielonymi kropką:

Import-Module activedirectory

$users = Get-ADUser -Filter "ObjectClass -eq 'user'" -SearchBase 'OU=it.supra.tf,DC=serba,DC=local'
foreach($user in $users)
{
    $firstname = $user.firstname
    $lastname = $user.lastname
    Set-UserPhoto -Identity $user.SamAccountName -PictureData ([System.IO.File]::ReadAllBytes("C:\Users\rserba.SERBA\Desktop\it.supra.tf\$firstname.$lastname.jpg")) -Confirm:$false
}

Taki skrypt odpalamy w Exchange Management Shell. Jeśli popełniliśmy błędy w nazwie plików lub one nie istnieją, zostaniemy o tym poinformowani przez skrypt:

[PS] C:\Users\rserba.SERBA\desktop>.\UpdateExchangePictureProfile.ps1
Exception calling "ReadAllBytes" with "1" argument(s): "Nie można odnaleźć pliku 'C:\Users\rserba.SERBA\Desktop\it.supra.tf\ferexio.jpg'."
At C:\Users\rserba.SERBA\desktop\UpdateExchangePictureProfile.ps1:7 char:5
+     Set-UserPhoto -Identity $user.SamAccountName -PictureData ([Syste ...
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : FileNotFoundException

Exception calling "ReadAllBytes" with "1" argument(s): "Nie można odnaleźć pliku 'C:\Users\rserba.SERBA\Desktop\it.supra.tf\kszymocha.jpg'."
At C:\Users\rserba.SERBA\desktop\UpdateExchangePictureProfile.ps1:7 char:5
+     Set-UserPhoto -Identity $user.SamAccountName -PictureData ([Syste ...
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : FileNotFoundException

Efekty widać na początku posta 😊