[CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory)] [string]$PublishPath, [Parameter(Mandatory)] [string]$ServerCertificateSubject, [ValidatePattern('^[0-9A-Fa-f ]{40,59}$')] [string[]]$AllowedClientThumbprints = @(), [string]$NtlmEndpoint = 'https://sgu.ulsa.edu.mx/', [string[]]$AllowedNtlmRedirectHosts = @('sgu.ulsa.edu.mx'), [ValidatePattern('^/')] [string]$AuthenticationPath = '/psulsa/', [ValidatePattern('^/')] [string]$AdministrativeProfilePath = '/psulsa/gadmon/capitalhumano/controlincidencias/incidencias.aspx', [ValidatePattern('^/')] [string]$AdministrativePersonalProfilePath = '/psulsa/gadmon/capitalhumano/datos/personales.aspx', [ValidatePattern('^/')] [string]$AdministrativeLocationProfilePath = '/psulsa/gadmon/capitalhumano/datos/ubicacion.aspx', [ValidatePattern('^/')] [string]$StudentProfilePath = '/psulsa/alumnos/consultainformacionalumnos/consultainformacion.aspx', [ValidatePattern('^/')] [string]$MenuProfilePath = '/psulsa/menu.aspx', [ValidateRange(32768, 2097152)] [int]$MaxProfileBytes = 524288, [string]$LdapHost = 'localhost', [string]$BaseDn = 'DC=lci,DC=lasalle,DC=mx', [string]$DomainNetbios = 'LCI', [string]$UpnSuffix = 'lci.lasalle.mx', [string]$RemoteDesktopGroupDn = '', [ValidateLength(1, 64)] [string]$DefaultCompany = 'La Salle', [ValidateRange(10, 60)] [int]$NtlmTimeoutSeconds = 20, [ValidateRange(2, 90)] [int]$ProfileTimeoutSeconds = 90, [switch]$CreateMissingOus, [switch]$DisableCertificateRevocationCheckForLab ) $ErrorActionPreference = 'Stop' $serviceName = 'SGUAuthBroker' $installPath = Join-Path $env:ProgramFiles 'SGU\AuthBroker' $normalizedClientThumbprints = @($AllowedClientThumbprints | ForEach-Object { $_ -replace ' ', '' }) if ($normalizedClientThumbprints.Where({ $_.Length -ne 40 }).Count -gt 0) { throw 'Client certificate thumbprints must contain exactly 40 hexadecimal characters.' } $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($identity) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Run this script from an elevated PowerShell session on the broker server.' } $serverCertificate = Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Subject -like "*$ServerCertificateSubject*" -and $_.HasPrivateKey } | Sort-Object NotAfter -Descending | Select-Object -First 1 if (-not $serverCertificate) { throw 'The HTTPS server certificate with private key was not found in LocalMachine\My.' } if (-not $serverCertificate.Verify()) { throw 'The HTTPS server certificate chain is not trusted or is outside its validity period. Import the issuing CA chain; for a self-signed lab certificate, trust its public .cer in LocalMachine\Root.' } if ($CreateMissingOus) { Import-Module ActiveDirectory -ErrorAction Stop $usersOuName = 'Usuarios-SGU' $usersOuDn = "OU=$usersOuName,$BaseDn" if (-not (Get-ADOrganizationalUnit -LDAPFilter "(ou=$usersOuName)" -SearchBase $BaseDn -SearchScope OneLevel -Server $LdapHost -ErrorAction SilentlyContinue)) { New-ADOrganizationalUnit -Name $usersOuName -Path $BaseDn -ProtectedFromAccidentalDeletion $true -Server $LdapHost | Out-Null } foreach ($ouName in @('Docentes', 'Alumnos', 'Administrativos')) { $targetOu = Get-ADOrganizationalUnit -LDAPFilter "(ou=$ouName)" -SearchBase $usersOuDn -SearchScope OneLevel -Properties ProtectedFromAccidentalDeletion -Server $LdapHost -ErrorAction SilentlyContinue if ($targetOu) { if (-not $targetOu.ProtectedFromAccidentalDeletion) { $targetOuDn = [string]$targetOu.DistinguishedName Set-ADOrganizationalUnit -Identity $targetOuDn -ProtectedFromAccidentalDeletion $true -Server $LdapHost -Confirm:$false } continue } $legacyOu = Get-ADOrganizationalUnit -LDAPFilter "(ou=$ouName)" -SearchBase $BaseDn -SearchScope OneLevel -Properties ProtectedFromAccidentalDeletion -Server $LdapHost -ErrorAction SilentlyContinue if ($legacyOu) { $legacyOuDn = [string]$legacyOu.DistinguishedName try { if ($legacyOu.ProtectedFromAccidentalDeletion) { Set-ADOrganizationalUnit -Identity $legacyOuDn -ProtectedFromAccidentalDeletion $false -Server $LdapHost -Confirm:$false Start-Sleep -Seconds 1 } Move-ADObject -Identity $legacyOuDn -TargetPath $usersOuDn -Server $LdapHost -Confirm:$false -ErrorAction Stop } finally { $currentOu = Get-ADOrganizationalUnit -LDAPFilter "(ou=$ouName)" -SearchBase $usersOuDn -SearchScope OneLevel -Properties ProtectedFromAccidentalDeletion -Server $LdapHost -ErrorAction SilentlyContinue if (-not $currentOu) { $currentOu = Get-ADOrganizationalUnit -LDAPFilter "(ou=$ouName)" -SearchBase $BaseDn -SearchScope OneLevel -Properties ProtectedFromAccidentalDeletion -Server $LdapHost -ErrorAction SilentlyContinue } if ($currentOu) { $currentOuDn = [string]$currentOu.DistinguishedName Set-ADOrganizationalUnit -Identity $currentOuDn -ProtectedFromAccidentalDeletion $true -Server $LdapHost -Confirm:$false } } } else { New-ADOrganizationalUnit -Name $ouName -Path $usersOuDn -ProtectedFromAccidentalDeletion $true -Server $LdapHost | Out-Null } } } if ($RemoteDesktopGroupDn) { Import-Module ActiveDirectory -ErrorAction Stop $remoteDesktopGroup = Get-ADGroup -Identity $RemoteDesktopGroupDn -Server $LdapHost -ErrorAction Stop if ($remoteDesktopGroup.GroupCategory -ne 'Security' -or -not $remoteDesktopGroup.DistinguishedName.EndsWith(",$BaseDn", [StringComparison]::OrdinalIgnoreCase)) { throw 'RemoteDesktopGroupDn must identify a security group beneath BaseDn.' } } foreach ($file in @('SGU.AuthBroker.exe', 'SGU.AuthBroker.dll', 'appsettings.json')) { if (-not (Test-Path -LiteralPath (Join-Path $PublishPath $file))) { throw "PublishPath is missing $file." } } $productionSettings = @{ Kestrel = @{ Endpoints = @{ Https = @{ Url = 'https://0.0.0.0:8443' Certificate = @{ Subject = $ServerCertificateSubject Store = 'My' Location = 'LocalMachine' AllowInvalid = $false } } } } Broker = @{ Tls = @{ AllowedClientThumbprints = $normalizedClientThumbprints CheckCertificateRevocation = -not $DisableCertificateRevocationCheckForLab } Ntlm = @{ Endpoint = $NtlmEndpoint Domain = '' TimeoutSeconds = $NtlmTimeoutSeconds ProfileTimeoutSeconds = $ProfileTimeoutSeconds MaxRedirects = 5 AuthenticationPath = $AuthenticationPath AdministrativeProfilePath = $AdministrativeProfilePath AdministrativePersonalProfilePath = $AdministrativePersonalProfilePath AdministrativeLocationProfilePath = $AdministrativeLocationProfilePath StudentProfilePath = $StudentProfilePath MenuProfilePath = $MenuProfilePath MaxProfileBytes = $MaxProfileBytes AllowedRedirectHosts = $AllowedNtlmRedirectHosts } Directory = @{ LdapHost = $LdapHost BaseDn = $BaseDn DomainNetbios = $DomainNetbios UpnSuffix = $UpnSuffix ProfessorOuDn = "OU=Docentes,OU=Usuarios-SGU,$BaseDn" StudentOuDn = "OU=Alumnos,OU=Usuarios-SGU,$BaseDn" AdministrativeOuDn = "OU=Administrativos,OU=Usuarios-SGU,$BaseDn" RemoteDesktopGroupDn = $RemoteDesktopGroupDn DefaultCompany = $DefaultCompany CreateMissingOus = [bool]$CreateMissingOus } } } if ($PSCmdlet.ShouldProcess($installPath, 'Install the SGU Authentication Broker Windows service')) { if (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) { Stop-Service -Name $serviceName -Force (Get-Service -Name $serviceName).WaitForStatus( [System.ServiceProcess.ServiceControllerStatus]::Stopped, [TimeSpan]::FromSeconds(15)) # A self-contained .NET process can briefly retain mapped runtime files # after SCM reports Stopped. Give Windows time to release those handles. Start-Sleep -Seconds 2 } New-Item -ItemType Directory -Path $installPath -Force | Out-Null Copy-Item -Path (Join-Path $PublishPath '*') -Destination $installPath -Recurse -Force $settingsJson = $productionSettings | ConvertTo-Json -Depth 8 $utf8WithoutBom = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText( (Join-Path $installPath 'appsettings.Production.json'), $settingsJson, $utf8WithoutBom) if (-not (Get-Service -Name $serviceName -ErrorAction SilentlyContinue)) { New-Service -Name $serviceName ` -DisplayName 'SGU Authentication Broker' ` -Description 'Validates SGU NTLM credentials and synchronizes Active Directory accounts.' ` -BinaryPathName ('"{0}"' -f (Join-Path $installPath 'SGU.AuthBroker.exe')) ` -StartupType Automatic } else { Set-Service -Name $serviceName -StartupType Automatic } & sc.exe failure $serviceName 'reset=' '86400' 'actions=' 'restart/5000/restart/15000/restart/60000' | Out-Null if ($LASTEXITCODE -ne 0) { throw 'Could not configure automatic recovery for SGUAuthBroker.' } & sc.exe failureflag $serviceName '1' | Out-Null if ($LASTEXITCODE -ne 0) { throw 'Could not enable recovery for non-crash SGUAuthBroker failures.' } if (-not (Get-NetFirewallRule -DisplayName 'SGU Authentication Broker (mTLS)' -ErrorAction SilentlyContinue)) { New-NetFirewallRule -DisplayName 'SGU Authentication Broker (mTLS)' ` -Direction Inbound -Action Allow -Protocol TCP -LocalPort 8443 -Profile Domain | Out-Null } Start-Service -Name $serviceName } Get-Service -Name $serviceName | Select-Object Name, Status, StartType