<# ===================================================================== Bulk-Freigabe "Kontakte" fuer Entra-Sicherheitsgruppe - Laeuft in Windows PowerShell 5.1 (auch ISE) mit Minimal-Import - Robust gegen leeres $PSScriptRoot, gemischte DirectoryObject-Typen, fehlendes @odata.type; nutzt typisierte Abfrage & Fallback-Resolve ===================================================================== #> # ------------------------------- # KONFIGURATION # ------------------------------- $GroupName = "sysvoice Users" # Entra (Azure AD) Sicherheitsgruppe (DisplayName) $ReaderIdentity = "admin@kundendomain.onmicrosoft.com" # Konto ODER mailaktivierte Gruppe, die Lesen (Reviewer) soll $AccessRight = "Reviewer" # Lesen reicht fuer Yeastar $WhatIf = $false # $true = Simulation ohne Aenderungen $IncludeTransitiveMembers = $true # $true: Mitglieder verschachtelter Gruppen ebenfalls laden # Log-Pfade (Fallback, falls in Konsole/ohne Datei gestartet) $LogFolder = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path } $TimeStamp = (Get-Date -Format "yyyyMMdd_HHmmss") $LogPath = Join-Path $LogFolder "ContactsShare_$($GroupName -replace '\W','_')_$TimeStamp.csv" # ------------------------------- # HILFSFUNKTIONEN (Ausgaben) # ------------------------------- function Write-Step($msg) { Write-Host "[STEP] $msg" -ForegroundColor Cyan } function Write-Info($msg) { Write-Host "[INFO] $msg" -ForegroundColor Gray } function Write-Ok($msg) { Write-Host "[OK] $msg" -ForegroundColor Green } function Write-Warn2($msg) { Write-Warning $msg } function Write-Err2($msg) { Write-Host "[ERROR] $msg" -ForegroundColor Red } function Write-Debug2($msg) { Write-Host "[DEBUG] $msg" -ForegroundColor Yellow } # ------------------------------- # 1) Microsoft Graph minimal laden & verbinden # ------------------------------- Write-Step "Lade Microsoft Graph Teilmodule (Authentication, Users, Groups)..." $needed = @( "Microsoft.Graph.Authentication", "Microsoft.Graph.Users", "Microsoft.Graph.Groups" ) foreach ($m in $needed) { if (-not (Get-Module -ListAvailable -Name $m)) { Write-Info "Installiere Modul: $m (Scope=CurrentUser)..." Install-Module $m -Scope CurrentUser -Force -ErrorAction Stop } Import-Module $m -ErrorAction Stop Write-Ok "Modul geladen: $m" } Write-Step "Verbinde zu Microsoft Graph (Scopes: Group.Read.All, User.Read.All)..." try { Connect-MgGraph -Scopes "Group.Read.All","User.Read.All" -ErrorAction Stop | Out-Null try { $ctx = Get-MgContext Write-Ok "Graph verbunden als: $($ctx.Account) / Tenant: $($ctx.TenantId)" } catch { Write-Info "Graph-Kontextpruefung nicht verfuegbar - fahre fort." } } catch { Write-Err2 "Graph-Verbindung fehlgeschlagen: $($_.Exception.Message)" throw } # ------------------------------- # 2) Gruppe finden # ------------------------------- Write-Step "Suche Entra Gruppe mit DisplayName: '$GroupName'..." $group = $null try { $group = Get-MgGroup -All -Filter "displayName eq '$GroupName'" -ErrorAction Stop } catch { } if (-not $group) { Write-Info "Wechsle in Suchmodus (ConsistencyLevel eventual)..." try { $group = Get-MgGroup -Search ('"'+$GroupName+'"') -ConsistencyLevel eventual -ErrorAction Stop | Where-Object { $_.DisplayName -eq $GroupName } } catch { } } if (-not $group) { Write-Info "Hole alle Gruppen und filtere lokal (dies kann dauern)..." $group = Get-MgGroup -All | Where-Object DisplayName -eq $GroupName } if (-not $group) { Write-Err2 "Gruppe '$GroupName' nicht gefunden."; throw "Gruppe '$GroupName' nicht gefunden." } if ($group.Count -gt 1) { Write-Warn2 "Mehrere Gruppen gefunden - verwende die erste: $($group[0].Id)"; $group = $group[0] } Write-Ok "Gruppe gefunden: $($group.DisplayName) (Id: $($group.Id))" # ------------------------------- # 3) Mitglieder (nur Benutzer) laden - robust # ------------------------------- Write-Step ("Lade {0}Mitglieder der Gruppe..." -f ($(if($IncludeTransitiveMembers){"transitive "}else{""}))) $rawMembers = @() try { if ($IncludeTransitiveMembers) { # TYPISIERT (direkt nur Benutzer) Write-Info "Versuche typisierte Abfrage: Get-MgGroupTransitiveMemberAsUser..." $rawMembers = Get-MgGroupTransitiveMemberAsUser -GroupId $group.Id -All -ErrorAction Stop } else { Write-Info "Versuche typisierte Abfrage: Get-MgGroupMemberAsUser..." $rawMembers = Get-MgGroupMemberAsUser -GroupId $group.Id -All -ErrorAction Stop } Write-Ok ("Typisierte Benutzer-Mitglieder geladen: {0}" -f $rawMembers.Count) } catch { Write-Warn2 "Typisierte Abfrage nicht verfuegbar/fehlgeschlagen: $($_.Exception.Message)" } # Fallback, wenn typisiert nichts brachte: untypisiert laden und selbst aufloesen if (-not $rawMembers -or $rawMembers.Count -eq 0) { Write-Info "Fallback: Lade DirectoryObjects und loese pro Id zu User auf..." try { if ($IncludeTransitiveMembers) { $dirObjs = Get-MgGroupTransitiveMember -GroupId $group.Id -All -ErrorAction Stop } else { $dirObjs = Get-MgGroupMember -GroupId $group.Id -All -ErrorAction Stop } Write-Ok ("DirectoryObjects geladen: {0}" -f $dirObjs.Count) $rawMembers = foreach ($o in $dirObjs) { try { $u2 = Get-MgUser -UserId $o.Id -ErrorAction Stop $u2 # nur echte Benutzer durchreichen } catch { } } Write-Ok ("Als Benutzer aufgeloest: {0}" -f $rawMembers.Count) } catch { Write-Err2 "Mitgliederabfrage fehlgeschlagen: $($_.Exception.Message)" throw } } if (-not $rawMembers -or $rawMembers.Count -eq 0) { Write-Err2 "Keine Benutzer-Mitglieder gefunden." throw "Keine Benutzer-Mitglieder gefunden." } # In ein einheitliches Objekt-Set projizieren Write-Step "Projiziere Benutzerobjekte..." $users = foreach ($m in $rawMembers) { # $m kann typisiertes User-Objekt sein $mail = $null; $upn = $null; $display = $null; $id = $null try { $mail = $m.Mail } catch {} try { $upn = $m.UserPrincipalName } catch {} try { $display = $m.DisplayName } catch {} try { $id = $m.Id } catch {} if (-not $display -and $m.AdditionalProperties) { $display = $m.AdditionalProperties.displayName } if (-not $mail -and $m.AdditionalProperties) { $mail = $m.AdditionalProperties.mail } if (-not $upn -and $m.AdditionalProperties) { $upn = $m.AdditionalProperties.userPrincipalName } [pscustomobject]@{ DisplayName = $display PrimaryAddressHint = if ($mail) { $mail } else { $upn } UPN = $upn ObjectId = $id } } Write-Ok ("Gefundene Benutzer: {0}" -f $users.Count) Write-Debug2 "Beispiel-Eintrag:`n$($users | Select-Object -First 1 | Format-List | Out-String)" # ------------------------------- # 4) Exchange Online verbinden # ------------------------------- Write-Step "Verbinde Exchange Online..." try { if (-not (Get-Module -ListAvailable -Name ExchangeOnlineManagement)) { Write-Info "Installiere Modul ExchangeOnlineManagement..." Install-Module ExchangeOnlineManagement -Scope CurrentUser -Force -ErrorAction Stop } Import-Module ExchangeOnlineManagement -ErrorAction Stop Connect-ExchangeOnline -ShowProgress:$true -ErrorAction Stop | Out-Null Write-Ok "Exchange Online verbunden." } catch { Write-Err2 "EXO-Verbindung fehlgeschlagen: $($_.Exception.Message)" throw } # ------------------------------- # 5) Mailbox-Check (nur UserMailbox zulassen) # ------------------------------- Write-Step "Pruefe Postfaecher fuer alle Benutzer..." $mailUsers = foreach ($u in $users) { $mbx = $null $probe1 = $u.PrimaryAddressHint $probe2 = $u.UPN try { $mbx = Get-EXOMailbox -Identity $probe1 -ErrorAction Stop } catch { try { $mbx = Get-EXOMailbox -Identity $probe2 -ErrorAction Stop } catch { $mbx = $null } } if ($mbx -and $mbx.RecipientTypeDetails -eq "UserMailbox") { Write-Info "OK: $($u.DisplayName) -> $($mbx.PrimarySmtpAddress)" [pscustomobject]@{ DisplayName=$u.DisplayName; Smtp=$mbx.PrimarySmtpAddress.ToString() } } else { Write-Warn2 "KEIN UserMailbox: $($u.DisplayName) (uebersprungen)" [pscustomobject]@{ DisplayName=$u.DisplayName; Smtp=$null } } } $withMailbox = $mailUsers | Where-Object { $_.Smtp } $withoutMailbox = $mailUsers | Where-Object { -not $_.Smtp } Write-Ok ("Mit UserMailbox: {0} | Ohne UserMailbox (skip): {1}" -f $withMailbox.Count, $withoutMailbox.Count) # ------------------------------- # 6) Kontakte-Ordner ermitteln (lokalisiert) & Rechte setzen # ------------------------------- function Get-ContactsFolderPath { param([Parameter(Mandatory)][string]$MailboxSmtp) $f = Get-MailboxFolderStatistics -Identity $MailboxSmtp -FolderScope Contacts ` | Where-Object { $_.FolderType -eq "Contacts" } ` | Select-Object -First 1 if ($null -eq $f) { return $null } return "$MailboxSmtp" + ":\$($f.Name)" } Write-Step "Setze Ordnerberechtigungen (ReaderIdentity=$ReaderIdentity, Access=$AccessRight)..." $results = foreach ($m in $withMailbox) { $path = Get-ContactsFolderPath -MailboxSmtp $m.Smtp if (-not $path) { Write-Warn2 "Kein Kontakte-Ordner fuer $($m.DisplayName) ($($m.Smtp)) - SKIP" [pscustomobject]@{User=$m.DisplayName; Mail=$m.Smtp; Result="SKIP"; Note="Kein Kontakte-Ordner"} continue } Write-Info "Bearbeite: $($m.DisplayName) | Ordner: $path" try { $existing = $null try { $existing = Get-MailboxFolderPermission -Identity $path -User $ReaderIdentity -ErrorAction Stop } catch {} if ($existing) { if ($existing.AccessRights -notcontains $AccessRight) { if ($WhatIf) { Write-Info "WOULD-UPDATE -> $AccessRight" [pscustomobject]@{User=$m.DisplayName; Mail=$m.Smtp; Result="WOULD-UPDATE"; Note="$path -> $AccessRight"} } else { Set-MailboxFolderPermission -Identity $path -User $ReaderIdentity -AccessRights $AccessRight -ErrorAction Stop Write-Ok "UPDATED -> $AccessRight" [pscustomobject]@{User=$m.DisplayName; Mail=$m.Smtp; Result="UPDATED"; Note="$path -> $AccessRight"} } } else { Write-Ok "OK (bereits $AccessRight)" [pscustomobject]@{User=$m.DisplayName; Mail=$m.Smtp; Result="OK"; Note="bereits $AccessRight auf $path"} } } else { if ($WhatIf) { Write-Info "WOULD-ADD -> $AccessRight" [pscustomobject]@{User=$m.DisplayName; Mail=$m.Smtp; Result="WOULD-ADD"; Note="$path -> $AccessRight"} } else { Add-MailboxFolderPermission -Identity $path -User $ReaderIdentity -AccessRights $AccessRight -ErrorAction Stop Write-Ok "ADDED -> $AccessRight" [pscustomobject]@{User=$m.DisplayName; Mail=$m.Smtp; Result="ADDED"; Note="$path -> $AccessRight"} } } } catch { Write-Err2 "Fehler bei $($m.DisplayName): $($_.Exception.Message)" [pscustomobject]@{User=$m.DisplayName; Mail=$m.Smtp; Result="ERROR"; Note=$_.Exception.Message} } } # ------------------------------- # 7) Ergebnis anzeigen & speichern # ------------------------------- Write-Step "Ergebnisse zusammenfassen..." $summary = $results | Group-Object Result | Select-Object Name,Count $summary | ForEach-Object { Write-Ok ("{0}: {1}" -f $_.Name, $_.Count) } Write-Step "CSV-Report speichern: $LogPath" try { $results | Export-Csv -Path $LogPath -NoTypeInformation -Encoding UTF8 Write-Ok "Report gespeichert." } catch { Write-Warn2 "Konnte CSV nicht speichern: $($_.Exception.Message)" } Write-Step "Fertig." $results | Sort-Object Result, User | Format-Table -AutoSize