Saltar al contenido
Microsoft scripts

Scripts de Entra

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.

Antes de correrloBloquea cuentas en masa: un CSV equivocado deja a gente sin trabajar, así que usa siempre -WhatIf primero. El acceso no se corta al instante: la revocación tarda unos minutos y un token de acceso ya emitido sirve hasta que vence. En cuentas sincronizadas con Active Directory hay que deshabilitarlas también allá. La revocación no cierra sesiones de invitados en su tenant de origen. No borra la cuenta ni quita licencias; tampoco deshabilita sus dispositivos. Las filas sin cuenta aparecen en el reporte como "Sin UPN en el CSV", y tu propia cuenta se omite y cuenta como sin procesar.

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 -WhatIf

La 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

  1. 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.
  2. Corre el script con -RutaCsvBajas, -RutaCsv y -WhatIf, y revisa el reporte de la simulación.
  3. Corre de nuevo sin -WhatIf y confirma cada cambio, o agrega -Confirm:$false si ya revisaste la lista.
  4. 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
}