<# .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 }