Permisos delegados en buzones
Lista quién tiene FullAccess, SendAs y SendOnBehalf en cada buzón, sin ruido de permisos del sistema. Con -Usuario responde a qué buzones tiene acceso una persona, la pregunta típica al darla de baja.
Solo lecturaSin permisos de GraphProbado con respuestas simuladas. Sin ejecutar en un tenant real.
Ejemplo en México
Despacho contable en Querétaro, 45 personas
Una contadora deja el despacho en pleno cierre fiscal. Antes de dar de baja la cuenta necesitan saber a qué buzones de clientes entraba.
.\get-permisos-delegados.ps1 -Usuario ana.lopez@empresa.example -RutaCsv .\accesos-ana.csvObtiene los buzones donde tiene FullAccess, SendAs o SendOnBehalf y reasigna esos accesos a quien toma su cartera antes de la baja.
Cómo implementarlo
- Instala el módulo si no lo tienes: Install-Module ExchangeOnlineManagement -Scope CurrentUser.
- Ejecuta el script con -RutaCsv para todo el tenant, o con -Usuario y el UPN de la persona que se va.
- Inicia sesión con una cuenta que tenga el rol indicado y espera: revisa cada buzón uno por uno.
- Revisa el CSV, quita los permisos que ya no procedan y revisa también los grupos de la persona, porque un acceso por grupo sale a nombre del grupo.
Ver el código completo de get-permisos-delegados.ps1
<#
.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." }