Baja de empleados desde CSV
Procesa un CSV de bajas de personal: bloquea el inicio de sesión de cada cuenta y revoca sus sesiones, y deja un reporte de lo que pasó con cada una.
Ejemplo en México
Maquiladora en Tijuana, 1,500 personas con turnos rotativos
Cada quincena Recursos Humanos entrega una lista de bajas y hacerlas una por una en el portal deja cuentas activas por días.
.\set-baja-empleados-csv.ps1 -RutaCsvBajas .\bajas.csv -RutaCsv .\resultado.csv -WhatIfLa simulación muestra 32 cuentas por bloquear y dos que no existen. Corrige el archivo con Recursos Humanos, lo corre sin -WhatIf y archiva el reporte de la quincena.
Cómo implementarlo
- Prepara un CSV separado por comas con una columna llamada Cuenta y el UPN de cada baja, y pide a Recursos Humanos que lo revise.
- Corre el script con -RutaCsvBajas, -RutaCsv y -WhatIf, y revisa el reporte de la simulación.
- Corre de nuevo sin -WhatIf y confirma cada cambio, o agrega -Confirm:$false si ya revisaste la lista.
- Abre el reporte y atiende cualquier fila con Error o con una nota, y sigue con tu proceso de buzón, licencias y dispositivos.
Ver el código completo de set-baja-empleados-csv.ps1
<#
.SYNOPSIS
Bloquea el inicio de sesión y revoca las sesiones de las cuentas de un CSV de bajas.
.DESCRIPTION
Lee un CSV con la columna Cuenta (UPN). Por cada cuenta pone accountEnabled en false y revoca sus sesiones,
en ese orden, y deja un reporte con el resultado de cada una. Una falla no detiene a las demás.
No borra cuentas, buzones ni licencias. Prueba siempre primero con -WhatIf.
CAMBIA EL TENANT: bloquea cuentas en masa.
.PARAMETER RutaCsvBajas
CSV de entrada, separado por comas, con una columna llamada Cuenta.
.PARAMETER RutaCsv
Ruta del reporte de resultados. Si se omite, solo se muestra en pantalla.
.EXAMPLE
.\set-baja-empleados-csv.ps1 -RutaCsvBajas .\bajas.csv -RutaCsv .\resultado.csv -WhatIf
.EXAMPLE
.\set-baja-empleados-csv.ps1 -RutaCsvBajas .\bajas.csv -RutaCsv .\resultado.csv
.NOTES
Permisos: User.EnableDisableAccount.All, User.Read.All, User.RevokeSessions.All
Rol mínimo: Administrador de usuarios (User Administrator). Para bloquear administradores: Administrador de autenticación con privilegios (Privileged Authentication Administrator)
Licencia: Microsoft Entra ID Free
Licencia del código: MIT. Se entrega TAL CUAL, sin garantía. Pruébalo primero en un tenant de pruebas.
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
[Parameter(Mandatory)][string]$RutaCsvBajas,
[string]$RutaCsv
)
$ErrorActionPreference = 'Stop'
$permisos = @('User.EnableDisableAccount.All', 'User.Read.All', 'User.RevokeSessions.All')
if (-not (Get-Command Connect-MgGraph -ErrorAction SilentlyContinue)) {
throw 'Falta el módulo. Instálalo con: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser'
}
$entrada = @(Import-Csv -Path $RutaCsvBajas)
if (-not $entrada) { throw "El CSV $RutaCsvBajas está vacío." }
if ('Cuenta' -notin $entrada[0].PSObject.Properties.Name) {
throw "El CSV debe tener una columna llamada Cuenta, separada por comas. Columnas encontradas: $($entrada[0].PSObject.Properties.Name -join ', ')"
}
$cuentas = @($entrada | ForEach-Object { "$($_.Cuenta)".Trim().ToLowerInvariant() } | Where-Object { $_ } | Select-Object -Unique)
$sinCuenta = @($entrada | Where-Object { -not "$($_.Cuenta)".Trim() }).Count
if (-not $cuentas) { throw "El CSV $RutaCsvBajas no trae ninguna cuenta en la columna Cuenta." }
Connect-MgGraph -Scopes $permisos -NoWelcome
$base = 'https://graph.microsoft.com/v1.0'
$yo = (Get-MgContext).Account
$filas = foreach ($cuenta in $cuentas) {
$fila = [pscustomobject]@{ Cuenta = $cuenta; Nombre = $null; Bloqueo = 'Sin cambios'; Sesiones = 'Sin cambios'; Nota = $null }
$fila
# El valor viene de un archivo y termina en la URI.
if ($cuenta -notmatch '^[^\s/\\?]+@[^\s/\\?]+$') { $fila.Nota = 'No parece un UPN válido'; continue }
if ($yo -and $cuenta -eq $yo) { $fila.Nota = 'Omitida: es la cuenta con la que estás conectado'; continue }
$usuario = $null
try {
$usuario = Invoke-MgGraphRequest -Method GET -Uri "$base/users/$([uri]::EscapeDataString($cuenta))?`$select=id,displayName,accountEnabled,userType,onPremisesSyncEnabled"
} catch {
if ("$_" -notmatch 'NotFound|Not Found|404') { $fila.Nota = "Error al leer: $_"; continue }
}
if (-not $usuario.id) { $fila.Nota = 'Cuenta no encontrada'; continue }
$fila.Nombre = $usuario.displayName
$notas = @()
if ($usuario.onPremisesSyncEnabled) { $notas += 'Cuenta sincronizada: deshabilítala también en Active Directory' }
if ($usuario.userType -eq 'Guest') { $notas += 'Invitado: la revocación no aplica a sus sesiones de su tenant de origen' }
if ($usuario.accountEnabled -eq $false) {
$fila.Bloqueo = 'Ya estaba bloqueada'
} elseif ($PSCmdlet.ShouldProcess($cuenta, 'Bloquear el inicio de sesión')) {
try {
Invoke-MgGraphRequest -Method PATCH -Uri "$base/users/$($usuario.id)" -Body (@{ accountEnabled = $false } | ConvertTo-Json) -ContentType 'application/json' | Out-Null
$fila.Bloqueo = 'Bloqueada'
} catch { $fila.Bloqueo = "Error: $_" }
} else {
$fila.Bloqueo = 'Simulado'
}
# Se revoca aunque el bloqueo falle: cierra las sesiones abiertas mientras se resuelve.
if ($PSCmdlet.ShouldProcess($cuenta, 'Revocar las sesiones')) {
try {
Invoke-MgGraphRequest -Method POST -Uri "$base/users/$($usuario.id)/revokeSignInSessions" | Out-Null
$fila.Sesiones = 'Revocadas'
} catch { $fila.Sesiones = "Error: $_" }
} else {
$fila.Sesiones = 'Simulado'
}
$fila.Nota = $notas -join '. '
}
$filas = @($filas) + @(for ($i = 0; $i -lt $sinCuenta; $i++) {
[pscustomobject]@{ Cuenta = ''; Nombre = $null; Bloqueo = 'Sin cambios'; Sesiones = 'Sin cambios'; Nota = 'Sin UPN en el CSV' }
})
$fallas = @($filas | Where-Object { $_.Bloqueo -like 'Error*' -or $_.Sesiones -like 'Error*' -or $_.Nota -match '^(Error|Cuenta no|No parece|Omitida|Sin UPN)' })
Write-Host "Cuentas procesadas: $(@($filas).Count). Con falla o sin procesar: $($fallas.Count)"
if ($RutaCsv) {
# Excel ejecutaría como fórmula un nombre que empiece con estos caracteres.
foreach ($f in $filas) { foreach ($p in $f.PSObject.Properties) { if ($p.Value -is [string] -and $p.Value -match '^[=+\-@]') { $p.Value = "'" + $p.Value } } }
# El reporte se escribe también con -WhatIf: es el resultado de la simulación, no un cambio en el tenant.
$filas | Export-Csv -Path $RutaCsv -NoTypeInformation -Encoding utf8BOM -WhatIf:$false -Confirm:$false
Write-Host "CSV guardado en $RutaCsv"
} else {
$filas | Format-Table -AutoSize
}