#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-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-client-bootstrap-$Version.zip`: registra un certificado mTLS único, instala y valida el Credential Provider antes de unir el equipo al dominio, habilita RDP/WinRM y se repara al arranque. - `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 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() }