Files
SGU-CredentialProvider/scripts/Publish-GiteaRelease.ps1
T

186 lines
10 KiB
PowerShell

#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[ValidatePattern('^\d+\.\d+\.\d+([-.][0-9A-Za-z.-]+)?$')]
[string]$Version,
[string]$ReleaseDirectory = (Join-Path $PSScriptRoot '..\artifacts\releases'),
[uri]$GiteaBaseUri = 'https://gitea.lci.ulsa.mx',
[string]$Owner = 'alexrg',
[string]$Repository = 'SGU-CredentialProvider',
[string]$TargetCommitish = 'main',
[switch]$Draft
)
$ErrorActionPreference = 'Stop'
$tagName = "v$Version"
$assetPaths = @(
(Join-Path $ReleaseDirectory "sgu-windows10-legacy-client-bootstrap-$Version.zip"),
(Join-Path $ReleaseDirectory "sgu-windows11-client-bootstrap-$Version.zip"),
(Join-Path $ReleaseDirectory "sgu-server-bootstrap-$Version.zip"),
(Join-Path $ReleaseDirectory "sgu-linux-client-bootstrap-$Version.zip"),
(Join-Path $ReleaseDirectory "sgu-azure-infrastructure-$Version.zip"),
(Join-Path $ReleaseDirectory "SHA256SUMS-$Version.txt")
)
foreach ($assetPath in $assetPaths) {
if (-not (Test-Path -LiteralPath $assetPath -PathType Leaf)) {
throw "Release asset is missing: $assetPath"
}
}
$token = $env:GITEA_TOKEN
$authorizationScheme = 'token'
$authorizationParameter = $token
if (-not $authorizationParameter) {
$credentialInput = "protocol=$($GiteaBaseUri.Scheme)`nhost=$($GiteaBaseUri.Host)`n`n"
$credentialOutput = $credentialInput | & git credential fill
if ($LASTEXITCODE -ne 0) {
throw 'Git Credential Manager could not supply Gitea credentials. Set GITEA_TOKEN for this process.'
}
$credentialValues = @{}
foreach ($line in $credentialOutput) {
$parts = $line -split '=', 2
if ($parts.Count -eq 2) {
$credentialValues[$parts[0]] = $parts[1]
}
}
if ($credentialValues.username -and $credentialValues.password) {
$authorizationScheme = 'Basic'
$basicCredential = '{0}:{1}' -f $credentialValues.username,$credentialValues.password
$authorizationParameter = [Convert]::ToBase64String(
[Text.Encoding]::UTF8.GetBytes($basicCredential))
$basicCredential = $null
}
}
if (-not $authorizationParameter) {
throw 'No Gitea credential is available. Set GITEA_TOKEN for this process or sign in through Git Credential Manager.'
}
Add-Type -AssemblyName System.Net.Http
$handler = [Net.Http.HttpClientHandler]::new()
$client = [Net.Http.HttpClient]::new($handler)
$client.BaseAddress = [uri]($GiteaBaseUri.AbsoluteUri.TrimEnd('/') + '/')
$client.DefaultRequestHeaders.Authorization = [Net.Http.Headers.AuthenticationHeaderValue]::new(
$authorizationScheme,
$authorizationParameter)
$client.DefaultRequestHeaders.UserAgent.ParseAdd('SGU-CredentialProvider-Release/1.0')
function Invoke-GiteaJson {
param(
[Parameter(Mandatory)][Net.Http.HttpMethod]$Method,
[Parameter(Mandatory)][string]$RelativeUri,
[object]$Body,
[switch]$AllowNotFound
)
$request = [Net.Http.HttpRequestMessage]::new($Method, $RelativeUri)
if ($null -ne $Body) {
$json = $Body | ConvertTo-Json -Depth 6
$request.Content = [Net.Http.StringContent]::new($json, [Text.Encoding]::UTF8, 'application/json')
}
try {
$response = $client.SendAsync($request).GetAwaiter().GetResult()
if ($AllowNotFound -and $response.StatusCode -eq [Net.HttpStatusCode]::NotFound) {
return $null
}
$content = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
if (-not $response.IsSuccessStatusCode) {
throw "Gitea returned HTTP $([int]$response.StatusCode): $content"
}
if ($content) {
return $content | ConvertFrom-Json
}
}
finally {
$request.Dispose()
}
}
try {
$encodedOwner = [Uri]::EscapeDataString($Owner)
$encodedRepository = [Uri]::EscapeDataString($Repository)
$encodedTag = [Uri]::EscapeDataString($tagName)
$repositoryPath = "api/v1/repos/$encodedOwner/$encodedRepository"
$existingRelease = Invoke-GiteaJson -Method ([Net.Http.HttpMethod]::Get) `
-RelativeUri "$repositoryPath/releases/tags/$encodedTag" -AllowNotFound
if ($existingRelease) {
throw "Release $tagName already exists. Choose a new version."
}
$releaseNotes = @"
Bootstrap reproducible para el laboratorio SGU.
- **Advertencia:** el bootstrap de servidor crea un bosque nuevo. No restaura los SID, contraseñas ni relaciones de confianza del bosque anterior; para conservarlos se requiere una recuperación de bosque desde una copia de estado del sistema.
- `sgu-server-bootstrap-$Version.zip`: crea el bosque AD/DNS, OUs, grupo RDP, GPO, recurso `Packages`, broker mTLS y administración remota; se reanuda solo después del reinicio.
- `sgu-windows10-legacy-client-bootstrap-$Version.zip`: perfil directo para Windows 10 de laboratorio, con las correcciones de NIC privada, límites de cuentas locales y compatibilidad de sus APIs heredadas.
- `sgu-windows11-client-bootstrap-$Version.zip`: perfil completo para Windows 11; conserva el enrolamiento directo y añade Azure P2S con certificado de máquina y entrada previa al inicio de sesión.
- Ambos clientes comparten los mismos binarios, seguridad mTLS, Credential Provider, cuenta estándar, RustDesk, supervisión y autorreparación; el manifiesto impide ejecutar accidentalmente el paquete de la otra versión de Windows.
- En clientes Hyper-V con dos NIC, el bootstrap selecciona la red privada sin puerta de enlace, solicita o acepta la IP fija del cliente, espera a que la dirección y WinRM estén disponibles y conserva en pantalla y archivo cualquier error de enrolamiento.
- `sgu-linux-client-bootstrap-$Version.zip`: une clientes Debian/Ubuntu o RHEL/Fedora/Rocky/AlmaLinux con realmd, Kerberos y SSSD. Solicita interactivamente la contraseña de unión y no instala el Credential Provider de Windows.
- `sgu-azure-infrastructure-$Version.zip`: despliega mediante Bicep una VM Windows Server 2025, red privada, IP pública protegida por NSG y Azure VPN Gateway P2S; también genera certificados por equipo y descarga el perfil de cliente.
- El bootstrap Azure conserva la IP privada administrada por la NIC de Azure, autoriza el pool P2S en los firewalls SGU y nunca publica LDAP, Kerberos, SMB, RPC, WinRM ni el Auth Broker directamente a Internet.
- Los Windows 11 Pro pueden instalar un perfil IKEv2 de todos los usuarios con certificado de máquina, DNS dividido para `lci.lasalle.mx` y ejecutarlo desde la pantalla de inicio de sesión antes de autenticar una cuenta de dominio nueva.
- El Auth Broker clasifica sin tareas programadas cada cuenta autenticada: `AL` se agrega a `SGU-Alumnos`, `AD` a `SGU-Administrativos` y `DO` a `SGU-Docentes`; el bootstrap crea cada grupo dentro de la OU de su rol y migra idempotentemente cualquier grupo heredado sin cambiar su SID.
- El Auth Broker resuelve la dirección guardada de administrativos y docentes mediante `GetDireccion`, `GetLocalidadListado` y `GetColoniasListado`, evitando conservar los valores transitorios `Seleccione...` de los controles dinámicos de SGU.
- El enrolamiento y la reparación de clientes Windows crean y verifican idempotentemente la cuenta local estándar `alumno`, sin pertenencia al grupo de administradores.
- La descripción de la cuenta local administrada respeta el límite de 48 caracteres de Windows 10 Enterprise.
- La validación de expiración de contraseña usa el indicador de cuenta compatible con Windows 10 y 11, en lugar de una propiedad que Windows 10 no expone.
- El enriquecimiento obtiene el sexo de los módulos SGU de personal/alumnos, lo conserva como la línea administrada `SGU-Gender: Male|Female` en Notas de AD y adapta el fondo de Windows/Linux; cuando falta utiliza redacción neutral.
- El servidor configura WEF/WEC para registrar sesiones y fallos, inventariar el estado alcanzable de las máquinas cada cinco minutos y conservar durante 183 días tanto esos eventos como el diagnóstico estructurado del Auth Broker.
- Windows Home se detecta y se rechaza con una explicación, ya que no admite unión a Active Directory ni RDP host.
Las contraseñas se solicitan de forma interactiva y no se escriben en archivos ni en la línea de comandos. Verifique los ZIP con `SHA256SUMS-$Version.txt`.
"@
$releaseBody = [ordered]@{
tag_name = $tagName
target_commitish = $TargetCommitish
name = "SGU Credential Provider $Version"
body = $releaseNotes
draft = [bool]$Draft
prerelease = $Version -match '-'
}
if (-not $PSCmdlet.ShouldProcess("$Owner/$Repository $tagName", 'Create Gitea release and upload bootstrap assets')) {
return
}
$release = Invoke-GiteaJson -Method ([Net.Http.HttpMethod]::Post) `
-RelativeUri "$repositoryPath/releases" -Body $releaseBody
foreach ($assetPath in $assetPaths) {
$assetName = Split-Path $assetPath -Leaf
$uploadUri = "$repositoryPath/releases/$($release.id)/assets?name=$([Uri]::EscapeDataString($assetName))"
$stream = [IO.File]::OpenRead((Resolve-Path -LiteralPath $assetPath).Path)
$multipart = [Net.Http.MultipartFormDataContent]::new()
$fileContent = [Net.Http.StreamContent]::new($stream)
$fileContent.Headers.ContentType = [Net.Http.Headers.MediaTypeHeaderValue]::new('application/octet-stream')
$multipart.Add($fileContent, 'attachment', $assetName)
try {
$response = $client.PostAsync($uploadUri, $multipart).GetAwaiter().GetResult()
$content = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
if (-not $response.IsSuccessStatusCode) {
throw "Gitea asset upload returned HTTP $([int]$response.StatusCode): $content"
}
}
finally {
$multipart.Dispose()
$stream.Dispose()
}
}
[pscustomobject]@{
Tag = $tagName
ReleaseName = $release.name
ReleaseUrl = $release.html_url
Draft = [bool]$release.draft
Assets = $assetPaths | ForEach-Object { Split-Path $_ -Leaf }
}
}
finally {
$token = $null
$authorizationParameter = $null
$credentialValues = $null
$client.Dispose()
$handler.Dispose()
}