178 lines
8.6 KiB
PowerShell
178 lines
8.6 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-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()
|
|
}
|