<# .SYNOPSIS Responde quién tiene acceso a qué buzón, o a qué buzones tiene acceso una persona. .DESCRIPTION Lista los permisos FullAccess, ChangePermission, ChangeOwner, SendAs y SendOnBehalf asignados directamente en cada buzón. No revisa permisos de carpetas, como el calendario. Excluye NT AUTHORITY\SELF, las cuentas de NT AUTHORITY y los permisos heredados del sistema. Con -Usuario responde la pregunta típica al dar de baja a alguien. Un acceso recibido por grupo aparece a nombre del grupo, no de la persona. Si un buzón falla, queda en el reporte y el script termina con error al final, después de guardar el CSV. Solo lectura: no cambia nada en el tenant. .PARAMETER Usuario UPN o correo de la persona. Muestra solo los buzones donde tiene permisos. Si no existe, el script se detiene. .PARAMETER Tipo Tipo de buzón: UserMailbox, SharedMailbox, RoomMailbox, EquipmentMailbox o Todos (por defecto). .PARAMETER RutaCsv Ruta del CSV de salida. Si se omite, solo se muestra en pantalla. .EXAMPLE .\get-permisos-delegados.ps1 -Usuario ana.lopez@empresa.example -RutaCsv .\accesos-ana.csv .NOTES Permisos: ninguno de Graph; usa ExchangeOnlineManagement. Rol mínimo: Lector global (View-Only Organization Management en Exchange Online). Licencia: Exchange Online Plan 1 o superior, incluido en Microsoft 365 Business Premium. Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en un tenant de pruebas. #> #Requires -Version 7.0 [CmdletBinding()] param( [ValidateNotNullOrEmpty()] [string]$Usuario, [ValidateSet('UserMailbox', 'SharedMailbox', 'RoomMailbox', 'EquipmentMailbox', 'Todos')] [string]$Tipo = 'Todos', [string]$RutaCsv ) $ErrorActionPreference = 'Stop' if (-not (Get-Command Connect-ExchangeOnline -ErrorAction SilentlyContinue)) { throw 'Falta el módulo. Instálalo con: Install-Module ExchangeOnlineManagement -Scope CurrentUser' } function Limpiar-Fila($fila) { foreach ($p in $fila.PSObject.Properties) { if ($p.Value -is [string] -and $p.Value -match '^[=+\-@\t\r]') { $p.Value = "'$($p.Value)" } } $fila } # El CSV se vacía desde el inicio: si algo truena o no hay resultados, no queda uno viejo que parezca actual. if ($RutaCsv) { Set-Content -LiteralPath $RutaCsv -Value $null -Encoding utf8BOM } $tipos = if ($Tipo -eq 'Todos') { 'UserMailbox', 'SharedMailbox', 'RoomMailbox', 'EquipmentMailbox' } else { $Tipo } $filas = [System.Collections.Generic.List[object]]::new() $fallas = [System.Collections.Generic.List[string]]::new() Connect-ExchangeOnline -ShowBanner:$false try { $identidades = @() if ($Usuario) { # GrantSendOnBehalfTo no trae siempre el correo, así que se compara contra todas las formas de nombrar a la persona. $persona = Get-EXORecipient -Identity $Usuario -Properties DisplayName, Alias, PrimarySmtpAddress, EmailAddresses -ErrorAction Stop if (-not $persona) { throw "No se encontró al destinatario $Usuario. Sin él no se puede afirmar a qué buzones tiene acceso." } # El permiso suele guardarse a nombre del UPN, que puede ser distinto del correo. $upn = $null try { $upn = (Get-User -Identity $Usuario -ErrorAction Stop).UserPrincipalName } catch { Write-Warning "No se pudo leer el UPN de $Usuario : pueden faltar permisos guardados a ese nombre. $_" } $identidades = @($Usuario, $upn, $persona.Name, $persona.Alias, $persona.DisplayName, $persona.PrimarySmtpAddress, $persona.ExternalDirectoryObjectId) + @(@($persona.EmailAddresses) | Where-Object { "$_" -like 'smtp:*' } | ForEach-Object { "$_".Substring(5) }) | Where-Object { $_ } } foreach ($b in @(Get-EXOMailbox -RecipientTypeDetails $tipos -ResultSize Unlimited -Properties GrantSendOnBehalfTo -ErrorAction Stop)) { try { $delegados = @( Get-EXOMailboxPermission -Identity $b.UserPrincipalName -ResultSize Unlimited -ErrorAction Stop | Where-Object { "$($_.AccessRights)" -match 'FullAccess|ChangePermission|ChangeOwner' -and $_.IsInherited -ne $true -and $_.Deny -ne $true -and "$($_.User)" -notlike 'NT AUTHORITY\*' } | ForEach-Object { [pscustomobject]@{ Delegado = "$($_.User)"; Permiso = ([regex]::Matches("$($_.AccessRights)", 'FullAccess|ChangePermission|ChangeOwner').Value -join ', ') } } Get-EXORecipientPermission -Identity $b.UserPrincipalName -ResultSize Unlimited -ErrorAction Stop | Where-Object { "$($_.AccessRights)" -match 'SendAs' -and $_.AccessControlType -ne 'Deny' -and $_.IsInherited -ne $true -and "$($_.Trustee)" -notlike 'NT AUTHORITY\*' } | ForEach-Object { [pscustomobject]@{ Delegado = "$($_.Trustee)"; Permiso = 'SendAs' } } @($b.GrantSendOnBehalfTo) | Where-Object { $_ } | ForEach-Object { [pscustomobject]@{ Delegado = "$_"; Permiso = 'SendOnBehalf' } } ) } catch { $fallas.Add("$($b.PrimarySmtpAddress)") $filas.Add([pscustomobject]@{ Buzon = $b.DisplayName; Correo = $b.PrimarySmtpAddress; TipoBuzon = $b.RecipientTypeDetails; Delegado = ''; Permiso = "No se pudo consultar: $_" }) continue } foreach ($d in $delegados) { if ($Usuario -and $d.Delegado -notin $identidades) { continue } $filas.Add([pscustomobject]@{ Buzon = $b.DisplayName; Correo = $b.PrimarySmtpAddress; TipoBuzon = $b.RecipientTypeDetails; Delegado = $d.Delegado; Permiso = $d.Permiso }) } } } finally { Disconnect-ExchangeOnline -Confirm:$false } $salida = @($filas | Sort-Object Correo, Permiso, Delegado) Write-Host "Permisos en el reporte: $($salida.Count)" if ($RutaCsv) { if ($salida.Count) { # UTF-8 con BOM para que Excel en español muestre bien los acentos. $salida | ForEach-Object { Limpiar-Fila $_ } | Export-Csv -LiteralPath $RutaCsv -NoTypeInformation -Encoding utf8BOM } Write-Host "CSV guardado en $RutaCsv" } else { $salida | Format-Table -AutoSize } if ($fallas.Count) { throw "No se pudieron consultar $($fallas.Count) buzones: $($fallas -join ', '). Revisa las filas marcadas en el reporte." }