Add branded default provider and enforced enrollment
This commit is contained in:
@@ -35,6 +35,7 @@ Operational documentation:
|
|||||||
|
|
||||||
- [Broker location, health, timeout, and recovery](docs/broker-operations.md)
|
- [Broker location, health, timeout, and recovery](docs/broker-operations.md)
|
||||||
- [Windows domain join and remote-access onboarding](docs/windows-client-onboarding.md)
|
- [Windows domain join and remote-access onboarding](docs/windows-client-onboarding.md)
|
||||||
|
- [Required Credential Provider client enrollment](docs/client-enrollment.md)
|
||||||
- [Decision: do not persist password verifiers in Redis](docs/decisions/0001-no-password-cache.md)
|
- [Decision: do not persist password verifiers in Redis](docs/decisions/0001-no-password-cache.md)
|
||||||
|
|
||||||
| Prefix | Role | Default OU |
|
| Prefix | Role | Default OU |
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ still contain the Unicode replacement character are not written to AD.
|
|||||||
When `RemoteDesktopGroupDn` is configured, the broker also adds each successfully
|
When `RemoteDesktopGroupDn` is configured, the broker also adds each successfully
|
||||||
synchronized SGU user to that dedicated AD security group. The laboratory
|
synchronized SGU user to that dedicated AD security group. The laboratory
|
||||||
Windows client maps the group into its local **Remote Desktop Users** group.
|
Windows client maps the group into its local **Remote Desktop Users** group.
|
||||||
|
Every synchronized user also receives `Universidad La Salle` in the AD
|
||||||
|
`company` attribute unless deployment configuration supplies another default.
|
||||||
|
|
||||||
|
The generic SGU credential is rendered as a dedicated branded tile instead of
|
||||||
|
being grouped below the anonymous **Other user** tile. Machine policy assigns
|
||||||
|
the SGU CLSID as the default provider while retaining the built-in Microsoft
|
||||||
|
password provider.
|
||||||
|
|
||||||
The managed hierarchy is rooted at `OU=Usuarios-SGU`: `Docentes`, `Alumnos`,
|
The managed hierarchy is rooted at `OU=Usuarios-SGU`: `Docentes`, `Alumnos`,
|
||||||
and `Administrativos` are direct child OUs beneath it.
|
and `Administrativos` are direct child OUs beneath it.
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# Enrolamiento obligatorio de clientes SGU
|
||||||
|
|
||||||
|
El flujo administrado instala y valida el Credential Provider **antes** de
|
||||||
|
ejecutar `Add-Computer`. La pertenencia al dominio es el último cambio; si falta
|
||||||
|
el runtime, un certificado, el registro COM, la directiva predeterminada o la
|
||||||
|
salud mTLS del broker, el script rechaza la unión.
|
||||||
|
|
||||||
|
Una cuenta administrativa todavía puede modificar Windows fuera de este flujo.
|
||||||
|
Por eso el enrolamiento también instala la tarea
|
||||||
|
`SGU-CredentialProvider-EnrollmentGuard`, ejecutada como `SYSTEM` al iniciar y
|
||||||
|
diariamente. La tarea conserva un paquete local protegido, repara el registro o
|
||||||
|
los binarios y vuelve a habilitar RDP/WinRM en equipos unidos al dominio. No
|
||||||
|
guarda contraseñas.
|
||||||
|
|
||||||
|
## 1. Preparar el nuevo Windows Enterprise
|
||||||
|
|
||||||
|
En la VM nueva, antes de unirla al dominio:
|
||||||
|
|
||||||
|
1. copiar `artifacts\credential-provider` y todos los scripts de `scripts`;
|
||||||
|
2. copiar el instalador offline más reciente de .NET 10 x64;
|
||||||
|
3. crear el certificado cliente no exportable:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass `
|
||||||
|
-File .\New-LabCertificate.ps1 `
|
||||||
|
-Role CredentialProviderClient
|
||||||
|
```
|
||||||
|
|
||||||
|
Copiar únicamente el `.cer` público al broker y registrarlo:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass `
|
||||||
|
-File .\Register-SguClientCertificate.ps1 `
|
||||||
|
-CertificatePath C:\SGUDeploy\new-client.cer
|
||||||
|
```
|
||||||
|
|
||||||
|
Exportar únicamente el certificado público actual del broker e importarlo en
|
||||||
|
el cliente con `Import-LabPeerCertificate.ps1`. Nunca copiar claves privadas.
|
||||||
|
|
||||||
|
## 2. Instalar, comprobar y unir en una sola operación
|
||||||
|
|
||||||
|
Ejecutar como administrador local dentro de la VM nueva. La credencial del
|
||||||
|
dominio se solicita de forma segura si no se proporciona como objeto
|
||||||
|
`PSCredential`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass `
|
||||||
|
-File .\Enroll-SguDomainClient.ps1 `
|
||||||
|
-PublishPath C:\SGUDeploy\credential-provider `
|
||||||
|
-BrokerEndpoint https://sgu-auth.lci.lasalle.mx:8443/v1/authenticate `
|
||||||
|
-ClientCertificateThumbprint CLIENT_CERT_THUMBPRINT `
|
||||||
|
-ServerCertificateThumbprint SERVER_CERT_THUMBPRINT `
|
||||||
|
-DotNetRuntimeInstallerPath C:\SGUDeploy\dotnet-runtime-10-win-x64.exe `
|
||||||
|
-NewComputerName LAB-W11-001
|
||||||
|
```
|
||||||
|
|
||||||
|
Orden de la transacción:
|
||||||
|
|
||||||
|
1. instala .NET y los binarios versionados;
|
||||||
|
2. registra COM y configura SGU como proveedor predeterminado;
|
||||||
|
3. instala el guard de autorreparación;
|
||||||
|
4. exige health mTLS del broker y ejecuta las comprobaciones locales;
|
||||||
|
5. configura DNS del dominio;
|
||||||
|
6. sólo entonces ejecuta `Add-Computer` y reinicia;
|
||||||
|
7. al arrancar, el guard habilita RDP, NLA y WinRM y comprueba el estado final.
|
||||||
|
|
||||||
|
La directiva de Windows **Assign a default credential provider** selecciona SGU
|
||||||
|
por defecto. No se instala ningún filtro: el proveedor de contraseña de
|
||||||
|
Microsoft permanece disponible para cuentas creadas manualmente y recuperación.
|
||||||
|
|
||||||
|
## 3. Verificación y reparación
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Test-SguClientEnrollment.ps1 `
|
||||||
|
-RequireDomainJoined `
|
||||||
|
-RequireRemoteAccess `
|
||||||
|
-RequireBrokerHealth `
|
||||||
|
-Enforce
|
||||||
|
|
||||||
|
Get-ScheduledTask -TaskName SGU-CredentialProvider-EnrollmentGuard
|
||||||
|
Start-ScheduledTask -TaskName SGU-CredentialProvider-EnrollmentGuard
|
||||||
|
```
|
||||||
|
|
||||||
|
Un resultado válido exige simultáneamente binario y registro COM, configuración,
|
||||||
|
certificados, .NET 10, proveedor SGU predeterminado y proveedor de contraseña de
|
||||||
|
Microsoft preservado. El script de reparación se encuentra en
|
||||||
|
`C:\ProgramData\SGU\Enrollment` con ACL exclusiva para `SYSTEM` y
|
||||||
|
administradores.
|
||||||
|
|
||||||
|
## 4. Experiencia de LogonUI
|
||||||
|
|
||||||
|
El proveedor usa un mosaico genérico dedicado, no una credencial asociada al SID
|
||||||
|
anónimo de **Other user**. Windows puede mostrar ahora la imagen institucional y
|
||||||
|
la etiqueta `Universidad La Salle · Acceso SGU`, además del encabezado
|
||||||
|
`Acceso institucional SGU`. El orden visual exacto de los iconos de opciones lo
|
||||||
|
decide Windows; lo administrable y exigido es cuál aparece seleccionado por
|
||||||
|
defecto.
|
||||||
|
|
||||||
|
Referencias de Microsoft:
|
||||||
|
|
||||||
|
- <https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-admx-credentialproviders#defaultcredentialprovider>
|
||||||
|
- <https://learn.microsoft.com/en-us/windows/win32/api/credentialprovider/nf-credentialprovider-icredentialprovidercredential2-getusersid>
|
||||||
+6
-1
@@ -18,7 +18,8 @@ put an institutional password on a command line or in a script file.
|
|||||||
Set-Location C:\Users\alex\Documents\projects\SGU-CredentialProvider
|
Set-Location C:\Users\alex\Documents\projects\SGU-CredentialProvider
|
||||||
dotnet restore .\SGU-CredentialProvider.sln
|
dotnet restore .\SGU-CredentialProvider.sln
|
||||||
dotnet build .\SGU-CredentialProvider.sln -c Release --no-restore
|
dotnet build .\SGU-CredentialProvider.sln -c Release --no-restore
|
||||||
dotnet test .\SGU-CredentialProvider.sln -c Release --no-build --no-restore
|
dotnet test --project .\tests\SGU.AuthBroker.Core.Tests\SGU.AuthBroker.Core.Tests.csproj -c Release
|
||||||
|
dotnet test --project .\tests\SGU.CredentialProvider.Tests\SGU.CredentialProvider.Tests.csproj -c Release
|
||||||
.\scripts\Publish-Lab.ps1
|
.\scripts\Publish-Lab.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -111,6 +112,10 @@ OU on Windows Server. Use separate authorized test accounts for `DO`, `AL`, and
|
|||||||
|
|
||||||
## 5. Install the Credential Provider
|
## 5. Install the Credential Provider
|
||||||
|
|
||||||
|
For a new client, prefer the provider-first transaction in
|
||||||
|
[`client-enrollment.md`](client-enrollment.md). The direct installation below is
|
||||||
|
kept for repair and existing domain members.
|
||||||
|
|
||||||
On Windows 10:
|
On Windows 10:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ Resolve-DnsName -Type SRV _ldap._tcp.dc._msdcs.lci.lasalle.mx
|
|||||||
|
|
||||||
## 2. Unir el equipo al dominio
|
## 2. Unir el equipo al dominio
|
||||||
|
|
||||||
|
Para equipos SGU nuevos, no ejecutar `Add-Computer` directamente. Usar el flujo
|
||||||
|
obligatorio de [`client-enrollment.md`](client-enrollment.md), que instala y
|
||||||
|
valida primero el Credential Provider. Los comandos siguientes quedan como guía
|
||||||
|
de recuperación o para equipos que no pertenecen al alcance SGU.
|
||||||
|
|
||||||
Abrir Windows PowerShell como administrador. El prompt solicita la contraseña
|
Abrir Windows PowerShell como administrador. El prompt solicita la contraseña
|
||||||
de forma interactiva y evita ponerla en el historial:
|
de forma interactiva y evita ponerla en el historial:
|
||||||
|
|
||||||
@@ -85,6 +90,10 @@ contenido. De esta forma se puede preparar una actualización aunque
|
|||||||
`LogonUI.exe` todavía tenga cargada la DLL anterior; el reinicio obligatorio
|
`LogonUI.exe` todavía tenga cargada la DLL anterior; el reinicio obligatorio
|
||||||
activa la nueva versión.
|
activa la nueva versión.
|
||||||
|
|
||||||
|
El instalador también asigna SGU como proveedor predeterminado mediante la
|
||||||
|
directiva de equipo de Windows. No modifica ni oculta el proveedor de contraseña
|
||||||
|
de Microsoft.
|
||||||
|
|
||||||
Bloquear el equipo, abrir **Sign-in options** y elegir el icono azul con llave
|
Bloquear el equipo, abrir **Sign-in options** y elegir el icono azul con llave
|
||||||
del acceso SGU. El proveedor de contraseña de Microsoft debe permanecer visible.
|
del acceso SGU. El proveedor de contraseña de Microsoft debe permanecer visible.
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ param(
|
|||||||
[string]$DomainNetbios = 'LCI',
|
[string]$DomainNetbios = 'LCI',
|
||||||
[string]$UpnSuffix = 'lci.lasalle.mx',
|
[string]$UpnSuffix = 'lci.lasalle.mx',
|
||||||
[string]$RemoteDesktopGroupDn = '',
|
[string]$RemoteDesktopGroupDn = '',
|
||||||
|
[ValidateLength(1, 64)]
|
||||||
|
[string]$DefaultCompany = 'Universidad La Salle',
|
||||||
[ValidateRange(10, 60)]
|
[ValidateRange(10, 60)]
|
||||||
[int]$NtlmTimeoutSeconds = 15,
|
[int]$NtlmTimeoutSeconds = 15,
|
||||||
[switch]$CreateMissingOus,
|
[switch]$CreateMissingOus,
|
||||||
@@ -152,6 +154,7 @@ $productionSettings = @{
|
|||||||
StudentOuDn = "OU=Alumnos,OU=Usuarios-SGU,$BaseDn"
|
StudentOuDn = "OU=Alumnos,OU=Usuarios-SGU,$BaseDn"
|
||||||
AdministrativeOuDn = "OU=Administrativos,OU=Usuarios-SGU,$BaseDn"
|
AdministrativeOuDn = "OU=Administrativos,OU=Usuarios-SGU,$BaseDn"
|
||||||
RemoteDesktopGroupDn = $RemoteDesktopGroupDn
|
RemoteDesktopGroupDn = $RemoteDesktopGroupDn
|
||||||
|
DefaultCompany = $DefaultCompany
|
||||||
CreateMissingOus = [bool]$CreateMissingOus
|
CreateMissingOus = [bool]$CreateMissingOus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
[CmdletBinding(SupportsShouldProcess)]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$PublishPath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[ValidatePattern('^https://')]
|
||||||
|
[string]$BrokerEndpoint,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[ValidatePattern('^[0-9A-Fa-f ]{40,59}$')]
|
||||||
|
[string]$ClientCertificateThumbprint,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[ValidatePattern('^[0-9A-Fa-f ]{40,59}$')]
|
||||||
|
[string]$ServerCertificateThumbprint,
|
||||||
|
|
||||||
|
[PSCredential]$DomainCredential,
|
||||||
|
[string]$DomainName = 'lci.lasalle.mx',
|
||||||
|
[string]$DomainNetbios = 'LCI',
|
||||||
|
[string]$NewComputerName,
|
||||||
|
[string]$NetworkInterfaceAlias = 'Ethernet',
|
||||||
|
[string[]]$DomainDnsServerAddresses = @('192.168.50.10'),
|
||||||
|
[string]$RemoteDesktopPrincipal = 'LCI\SG-Laboratorio-Usuarios-RDP',
|
||||||
|
[string]$DotNetRuntimeInstallerPath,
|
||||||
|
[switch]$SkipRestart
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$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 Windows PowerShell session.'
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($scriptName in @(
|
||||||
|
'Install-CredentialProvider.ps1',
|
||||||
|
'Install-SguEnrollmentGuard.ps1',
|
||||||
|
'Test-SguClientEnrollment.ps1',
|
||||||
|
'Repair-SguClientEnrollment.ps1',
|
||||||
|
'Enable-LabRemoteAccess.ps1')) {
|
||||||
|
if (-not (Test-Path -LiteralPath (Join-Path $PSScriptRoot $scriptName) -PathType Leaf)) {
|
||||||
|
throw "$scriptName must be beside Enroll-SguDomainClient.ps1."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$computer = Get-CimInstance Win32_ComputerSystem
|
||||||
|
if ($computer.PartOfDomain -and $computer.Domain -ne $DomainName) {
|
||||||
|
throw "The computer is already joined to the unexpected domain $($computer.Domain)."
|
||||||
|
}
|
||||||
|
|
||||||
|
$installParams = @{
|
||||||
|
PublishPath = $PublishPath
|
||||||
|
BrokerEndpoint = $BrokerEndpoint
|
||||||
|
ClientCertificateThumbprint = $ClientCertificateThumbprint
|
||||||
|
ServerCertificateThumbprint = $ServerCertificateThumbprint
|
||||||
|
DomainNetbios = $DomainNetbios
|
||||||
|
TimeoutSeconds = 20
|
||||||
|
}
|
||||||
|
if ($DotNetRuntimeInstallerPath) {
|
||||||
|
$installParams.InstallDotNetRuntime = $true
|
||||||
|
$installParams.DotNetRuntimeInstallerPath = $DotNetRuntimeInstallerPath
|
||||||
|
}
|
||||||
|
|
||||||
|
$guardParams = @{
|
||||||
|
PublishPath = $PublishPath
|
||||||
|
BrokerEndpoint = $BrokerEndpoint
|
||||||
|
ClientCertificateThumbprint = $ClientCertificateThumbprint
|
||||||
|
ServerCertificateThumbprint = $ServerCertificateThumbprint
|
||||||
|
DomainNetbios = $DomainNetbios
|
||||||
|
TimeoutSeconds = 20
|
||||||
|
RemoteDesktopPrincipal = $RemoteDesktopPrincipal
|
||||||
|
DotNetRuntimeInstallerPath = $DotNetRuntimeInstallerPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Install and verify SGU before joining the domain')) {
|
||||||
|
& (Join-Path $PSScriptRoot 'Install-CredentialProvider.ps1') @installParams | Out-Null
|
||||||
|
& (Join-Path $PSScriptRoot 'Install-SguEnrollmentGuard.ps1') @guardParams | Out-Null
|
||||||
|
|
||||||
|
$preJoin = & (Join-Path $PSScriptRoot 'Test-SguClientEnrollment.ps1') `
|
||||||
|
-RequireBrokerHealth
|
||||||
|
if (-not $preJoin.IsValid) {
|
||||||
|
throw "Domain join refused because SGU enrollment is invalid: $($preJoin.Issues -join ' ')"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($computer.PartOfDomain) {
|
||||||
|
& (Join-Path $PSScriptRoot 'Enable-LabRemoteAccess.ps1') `
|
||||||
|
-RemoteDesktopPrincipal $RemoteDesktopPrincipal `
|
||||||
|
-EnableAdministrativeFirewallGroups | Out-Null
|
||||||
|
return & (Join-Path $PSScriptRoot 'Test-SguClientEnrollment.ps1') `
|
||||||
|
-RequireDomainJoined `
|
||||||
|
-RequireRemoteAccess `
|
||||||
|
-RemoteDesktopPrincipal $RemoteDesktopPrincipal
|
||||||
|
}
|
||||||
|
|
||||||
|
Set-DnsClientServerAddress `
|
||||||
|
-InterfaceAlias $NetworkInterfaceAlias `
|
||||||
|
-ServerAddresses $DomainDnsServerAddresses
|
||||||
|
Resolve-DnsName -Type SRV "_ldap._tcp.dc._msdcs.$DomainName" -ErrorAction Stop | Out-Null
|
||||||
|
|
||||||
|
if (-not $DomainCredential) {
|
||||||
|
$DomainCredential = Get-Credential `
|
||||||
|
-UserName "$DomainNetbios\Administrator" `
|
||||||
|
-Message "Credential permitted to join this computer to $DomainName"
|
||||||
|
}
|
||||||
|
|
||||||
|
$joinParams = @{
|
||||||
|
DomainName = $DomainName
|
||||||
|
Credential = $DomainCredential
|
||||||
|
Force = $true
|
||||||
|
}
|
||||||
|
if ($NewComputerName) {
|
||||||
|
$joinParams.NewName = $NewComputerName
|
||||||
|
}
|
||||||
|
Add-Computer @joinParams
|
||||||
|
|
||||||
|
if (-not $SkipRestart) {
|
||||||
|
Restart-Computer -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[pscustomobject]@{
|
||||||
|
ComputerName = if ($NewComputerName) { $NewComputerName } else { $env:COMPUTERNAME }
|
||||||
|
DomainName = $DomainName
|
||||||
|
ProviderValidatedBeforeJoin = $true
|
||||||
|
RestartRequired = [bool]$SkipRestart
|
||||||
|
}
|
||||||
@@ -20,6 +20,8 @@ param(
|
|||||||
[ValidateRange(2, 60)]
|
[ValidateRange(2, 60)]
|
||||||
[int]$TimeoutSeconds = 20,
|
[int]$TimeoutSeconds = 20,
|
||||||
|
|
||||||
|
[switch]$DoNotSetAsDefaultCredentialProvider,
|
||||||
|
|
||||||
[switch]$InstallDotNetRuntime,
|
[switch]$InstallDotNetRuntime,
|
||||||
|
|
||||||
[string]$DotNetRuntimeInstallerPath
|
[string]$DotNetRuntimeInstallerPath
|
||||||
@@ -31,6 +33,7 @@ $installRoot = Join-Path $env:ProgramFiles 'SGU\CredentialProvider'
|
|||||||
$settingsPath = Join-Path $env:ProgramData 'SGU\CredentialProvider\settings.json'
|
$settingsPath = Join-Path $env:ProgramData 'SGU\CredentialProvider\settings.json'
|
||||||
$providerRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\$providerClassId"
|
$providerRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\$providerClassId"
|
||||||
$classRegistryPath = "HKLM:\SOFTWARE\Classes\CLSID\$providerClassId\InprocServer32"
|
$classRegistryPath = "HKLM:\SOFTWARE\Classes\CLSID\$providerClassId\InprocServer32"
|
||||||
|
$defaultProviderPolicyPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System'
|
||||||
|
|
||||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
||||||
@@ -175,7 +178,27 @@ if ($PSCmdlet.ShouldProcess($installPath, 'Install and register the SGU Credenti
|
|||||||
New-ItemProperty -Path $classRegistryPath -Name ThreadingModel -Value Both -PropertyType String -Force | Out-Null
|
New-ItemProperty -Path $classRegistryPath -Name ThreadingModel -Value Both -PropertyType String -Force | Out-Null
|
||||||
|
|
||||||
New-Item -Path $providerRegistryPath -Force | Out-Null
|
New-Item -Path $providerRegistryPath -Force | Out-Null
|
||||||
Set-Item -Path $providerRegistryPath -Value 'SGU Institutional Login'
|
Set-Item -Path $providerRegistryPath -Value 'Universidad La Salle · Acceso SGU'
|
||||||
|
|
||||||
|
if (-not $DoNotSetAsDefaultCredentialProvider) {
|
||||||
|
New-Item -Path $defaultProviderPolicyPath -Force | Out-Null
|
||||||
|
New-ItemProperty -Path $defaultProviderPolicyPath `
|
||||||
|
-Name DefaultCredentialProvider `
|
||||||
|
-Value $providerClassId `
|
||||||
|
-PropertyType String `
|
||||||
|
-Force | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaultProviderConfigured = $false
|
||||||
|
try {
|
||||||
|
$defaultProviderConfigured = (Get-ItemPropertyValue `
|
||||||
|
-LiteralPath $defaultProviderPolicyPath `
|
||||||
|
-Name DefaultCredentialProvider `
|
||||||
|
-ErrorAction Stop) -eq $providerClassId
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# An explicitly opted-out installation has no default-provider policy.
|
||||||
}
|
}
|
||||||
|
|
||||||
[pscustomobject]@{
|
[pscustomobject]@{
|
||||||
@@ -183,5 +206,6 @@ if ($PSCmdlet.ShouldProcess($installPath, 'Install and register the SGU Credenti
|
|||||||
InstallPath = $installPath
|
InstallPath = $installPath
|
||||||
SettingsPath = $settingsPath
|
SettingsPath = $settingsPath
|
||||||
Registered = Test-Path -LiteralPath $providerRegistryPath
|
Registered = Test-Path -LiteralPath $providerRegistryPath
|
||||||
|
DefaultProviderConfigured = $defaultProviderConfigured
|
||||||
SystemPasswordProviderPreserved = $true
|
SystemPasswordProviderPreserved = $true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
[CmdletBinding(SupportsShouldProcess)]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$PublishPath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[ValidatePattern('^https://')]
|
||||||
|
[string]$BrokerEndpoint,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[ValidatePattern('^[0-9A-Fa-f ]{40,59}$')]
|
||||||
|
[string]$ClientCertificateThumbprint,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[ValidatePattern('^[0-9A-Fa-f ]{40,59}$')]
|
||||||
|
[string]$ServerCertificateThumbprint,
|
||||||
|
|
||||||
|
[string]$DomainNetbios = 'LCI',
|
||||||
|
[ValidateRange(2, 60)]
|
||||||
|
[int]$TimeoutSeconds = 20,
|
||||||
|
[string]$RemoteDesktopPrincipal = 'LCI\SG-Laboratorio-Usuarios-RDP',
|
||||||
|
[string]$DotNetRuntimeInstallerPath
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$taskName = 'SGU-CredentialProvider-EnrollmentGuard'
|
||||||
|
$enrollmentRoot = Join-Path $env:ProgramData 'SGU\Enrollment'
|
||||||
|
$sourceScripts = @(
|
||||||
|
'Install-CredentialProvider.ps1',
|
||||||
|
'Enable-LabRemoteAccess.ps1',
|
||||||
|
'Test-SguClientEnrollment.ps1',
|
||||||
|
'Repair-SguClientEnrollment.ps1'
|
||||||
|
)
|
||||||
|
|
||||||
|
$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 Windows PowerShell session.'
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($scriptName in $sourceScripts) {
|
||||||
|
if (-not (Test-Path -LiteralPath (Join-Path $PSScriptRoot $scriptName) -PathType Leaf)) {
|
||||||
|
throw "$scriptName must be beside Install-SguEnrollmentGuard.ps1."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$requiredProviderFile = Join-Path $PublishPath 'SGU.CredentialProvider.comhost.dll'
|
||||||
|
if (-not (Test-Path -LiteralPath $requiredProviderFile -PathType Leaf)) {
|
||||||
|
throw 'PublishPath does not contain the Credential Provider package.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($DotNetRuntimeInstallerPath -and
|
||||||
|
-not (Test-Path -LiteralPath $DotNetRuntimeInstallerPath -PathType Leaf)) {
|
||||||
|
throw 'DotNetRuntimeInstallerPath does not exist.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($PSCmdlet.ShouldProcess($enrollmentRoot, 'Install the SGU enrollment repair guard')) {
|
||||||
|
New-Item -ItemType Directory -Path $enrollmentRoot -Force | Out-Null
|
||||||
|
$packageId = '{0}-{1}' -f (Get-Date -Format 'yyyyMMddHHmmss'), ([Guid]::NewGuid().ToString('N').Substring(0, 8))
|
||||||
|
$guardPublishPath = Join-Path $enrollmentRoot "packages\$packageId"
|
||||||
|
New-Item -ItemType Directory -Path $guardPublishPath -Force | Out-Null
|
||||||
|
Copy-Item -Path (Join-Path $PublishPath '*') -Destination $guardPublishPath -Recurse -Force
|
||||||
|
|
||||||
|
foreach ($scriptName in $sourceScripts) {
|
||||||
|
Copy-Item -LiteralPath (Join-Path $PSScriptRoot $scriptName) `
|
||||||
|
-Destination (Join-Path $enrollmentRoot $scriptName) -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
$guardRuntimeInstaller = $null
|
||||||
|
if ($DotNetRuntimeInstallerPath) {
|
||||||
|
$runtimeDirectory = Join-Path $enrollmentRoot 'prerequisites'
|
||||||
|
New-Item -ItemType Directory -Path $runtimeDirectory -Force | Out-Null
|
||||||
|
$guardRuntimeInstaller = Join-Path $runtimeDirectory (Split-Path $DotNetRuntimeInstallerPath -Leaf)
|
||||||
|
Copy-Item -LiteralPath $DotNetRuntimeInstallerPath -Destination $guardRuntimeInstaller -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
$guardConfiguration = [ordered]@{
|
||||||
|
PublishPath = $guardPublishPath
|
||||||
|
BrokerEndpoint = $BrokerEndpoint
|
||||||
|
ClientCertificateThumbprint = ($ClientCertificateThumbprint -replace ' ', '')
|
||||||
|
ServerCertificateThumbprint = ($ServerCertificateThumbprint -replace ' ', '')
|
||||||
|
DomainNetbios = $DomainNetbios
|
||||||
|
TimeoutSeconds = $TimeoutSeconds
|
||||||
|
RemoteDesktopPrincipal = $RemoteDesktopPrincipal
|
||||||
|
DotNetRuntimeInstallerPath = $guardRuntimeInstaller
|
||||||
|
}
|
||||||
|
$configurationPath = Join-Path $enrollmentRoot 'enrollment.json'
|
||||||
|
[IO.File]::WriteAllText(
|
||||||
|
$configurationPath,
|
||||||
|
($guardConfiguration | ConvertTo-Json),
|
||||||
|
[Text.UTF8Encoding]::new($false))
|
||||||
|
|
||||||
|
$acl = Get-Acl -LiteralPath $enrollmentRoot
|
||||||
|
$acl.SetAccessRuleProtection($true, $false)
|
||||||
|
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
|
||||||
|
'SYSTEM', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
|
||||||
|
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
|
||||||
|
'BUILTIN\Administrators', 'FullControl', 'ContainerInherit,ObjectInherit', 'None', 'Allow'))
|
||||||
|
Set-Acl -LiteralPath $enrollmentRoot -AclObject $acl
|
||||||
|
|
||||||
|
$repairScript = Join-Path $enrollmentRoot 'Repair-SguClientEnrollment.ps1'
|
||||||
|
$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||||
|
$action = New-ScheduledTaskAction -Execute $powerShell `
|
||||||
|
-Argument "-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$repairScript`""
|
||||||
|
$startupTrigger = New-ScheduledTaskTrigger -AtStartup
|
||||||
|
# Give domain networking and Group Policy time to initialize before the
|
||||||
|
# repair script resolves domain principals and validates remote access.
|
||||||
|
$startupTrigger.Delay = 'PT1M'
|
||||||
|
$triggers = @(
|
||||||
|
$startupTrigger,
|
||||||
|
(New-ScheduledTaskTrigger -Daily -At '3:00 AM')
|
||||||
|
)
|
||||||
|
$settings = New-ScheduledTaskSettingsSet `
|
||||||
|
-StartWhenAvailable `
|
||||||
|
-ExecutionTimeLimit (New-TimeSpan -Minutes 5) `
|
||||||
|
-RestartCount 3 `
|
||||||
|
-RestartInterval (New-TimeSpan -Minutes 1)
|
||||||
|
Register-ScheduledTask -TaskName $taskName `
|
||||||
|
-Action $action `
|
||||||
|
-Trigger $triggers `
|
||||||
|
-Settings $settings `
|
||||||
|
-User 'SYSTEM' `
|
||||||
|
-RunLevel Highest `
|
||||||
|
-Force | Out-Null
|
||||||
|
|
||||||
|
& $repairScript -ConfigurationPath $configurationPath | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
[pscustomobject]@{
|
||||||
|
EnrollmentRoot = $enrollmentRoot
|
||||||
|
TaskName = $taskName
|
||||||
|
TaskState = (Get-ScheduledTask -TaskName $taskName).State
|
||||||
|
ConfigurationPath = Join-Path $enrollmentRoot 'enrollment.json'
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
[CmdletBinding(SupportsShouldProcess)]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$CertificatePath,
|
||||||
|
[string]$BrokerConfigurationPath = 'C:\Program Files\SGU\AuthBroker\appsettings.Production.json'
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$serviceName = 'SGUAuthBroker'
|
||||||
|
$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.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $CertificatePath -PathType Leaf)) {
|
||||||
|
throw 'CertificatePath does not exist.'
|
||||||
|
}
|
||||||
|
if (-not (Test-Path -LiteralPath $BrokerConfigurationPath -PathType Leaf)) {
|
||||||
|
throw 'The broker production configuration does not exist.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidate = [Security.Cryptography.X509Certificates.X509Certificate2]::new($CertificatePath)
|
||||||
|
if ($candidate.NotBefore -gt (Get-Date) -or $candidate.NotAfter -lt (Get-Date)) {
|
||||||
|
throw 'The client certificate is outside its validity period.'
|
||||||
|
}
|
||||||
|
$clientAuthenticationOid = '1.3.6.1.5.5.7.3.2'
|
||||||
|
$ekuExtension = $candidate.Extensions |
|
||||||
|
Where-Object { $_.Oid.Value -eq '2.5.29.37' } |
|
||||||
|
Select-Object -First 1
|
||||||
|
if (-not $ekuExtension -or
|
||||||
|
$ekuExtension.EnhancedKeyUsages.Value -notcontains $clientAuthenticationOid) {
|
||||||
|
throw 'The certificate is not valid for TLS client authentication.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($PSCmdlet.ShouldProcess($candidate.Thumbprint, 'Trust and allow the SGU client certificate')) {
|
||||||
|
$trustedCertificate = Import-Certificate `
|
||||||
|
-FilePath $CertificatePath `
|
||||||
|
-CertStoreLocation Cert:\LocalMachine\Root |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
$configuration = Get-Content -LiteralPath $BrokerConfigurationPath -Raw | ConvertFrom-Json
|
||||||
|
$allowed = @($configuration.Broker.Tls.AllowedClientThumbprints |
|
||||||
|
ForEach-Object { $_ -replace ' ', '' })
|
||||||
|
if ($allowed -notcontains $trustedCertificate.Thumbprint) {
|
||||||
|
$configuration.Broker.Tls.AllowedClientThumbprints = @($allowed + $trustedCertificate.Thumbprint)
|
||||||
|
$backupPath = "$BrokerConfigurationPath.before-$($trustedCertificate.Thumbprint.Substring(0, 12)).bak"
|
||||||
|
Copy-Item -LiteralPath $BrokerConfigurationPath -Destination $backupPath -Force
|
||||||
|
[IO.File]::WriteAllText(
|
||||||
|
$BrokerConfigurationPath,
|
||||||
|
($configuration | ConvertTo-Json -Depth 8),
|
||||||
|
[Text.UTF8Encoding]::new($false))
|
||||||
|
}
|
||||||
|
|
||||||
|
Restart-Service -Name $serviceName -Force
|
||||||
|
(Get-Service -Name $serviceName).WaitForStatus(
|
||||||
|
[System.ServiceProcess.ServiceControllerStatus]::Running,
|
||||||
|
[TimeSpan]::FromSeconds(20))
|
||||||
|
}
|
||||||
|
|
||||||
|
[pscustomobject]@{
|
||||||
|
Subject = $candidate.Subject
|
||||||
|
Thumbprint = $candidate.Thumbprint
|
||||||
|
NotAfter = $candidate.NotAfter
|
||||||
|
Allowed = @((Get-Content -LiteralPath $BrokerConfigurationPath -Raw | ConvertFrom-Json).Broker.Tls.AllowedClientThumbprints) -contains $candidate.Thumbprint
|
||||||
|
ServiceStatus = (Get-Service -Name $serviceName).Status
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$ConfigurationPath = "$env:ProgramData\SGU\Enrollment\enrollment.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$configuration = Get-Content -LiteralPath $ConfigurationPath -Raw | ConvertFrom-Json
|
||||||
|
$enrollmentRoot = Split-Path $ConfigurationPath -Parent
|
||||||
|
$testScript = Join-Path $enrollmentRoot 'Test-SguClientEnrollment.ps1'
|
||||||
|
$installScript = Join-Path $enrollmentRoot 'Install-CredentialProvider.ps1'
|
||||||
|
$remoteAccessScript = Join-Path $enrollmentRoot 'Enable-LabRemoteAccess.ps1'
|
||||||
|
|
||||||
|
$before = & $testScript
|
||||||
|
if (-not $before.IsValid) {
|
||||||
|
$installParams = @{
|
||||||
|
PublishPath = [string]$configuration.PublishPath
|
||||||
|
BrokerEndpoint = [string]$configuration.BrokerEndpoint
|
||||||
|
ClientCertificateThumbprint = [string]$configuration.ClientCertificateThumbprint
|
||||||
|
ServerCertificateThumbprint = [string]$configuration.ServerCertificateThumbprint
|
||||||
|
DomainNetbios = [string]$configuration.DomainNetbios
|
||||||
|
TimeoutSeconds = [int]$configuration.TimeoutSeconds
|
||||||
|
}
|
||||||
|
if ($configuration.DotNetRuntimeInstallerPath) {
|
||||||
|
$installParams.InstallDotNetRuntime = $true
|
||||||
|
$installParams.DotNetRuntimeInstallerPath = [string]$configuration.DotNetRuntimeInstallerPath
|
||||||
|
}
|
||||||
|
& $installScript @installParams | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$computer = Get-CimInstance Win32_ComputerSystem
|
||||||
|
if ($computer.PartOfDomain) {
|
||||||
|
& $remoteAccessScript `
|
||||||
|
-RemoteDesktopPrincipal ([string]$configuration.RemoteDesktopPrincipal) `
|
||||||
|
-EnableAdministrativeFirewallGroups | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$verificationParams = @{}
|
||||||
|
if ($computer.PartOfDomain) {
|
||||||
|
$verificationParams.RequireDomainJoined = $true
|
||||||
|
$verificationParams.RequireRemoteAccess = $true
|
||||||
|
$verificationParams.RemoteDesktopPrincipal = [string]$configuration.RemoteDesktopPrincipal
|
||||||
|
}
|
||||||
|
$after = & $testScript @verificationParams
|
||||||
|
$after
|
||||||
|
if (-not $after.IsValid) {
|
||||||
|
throw "SGU enrollment repair did not reach a valid state: $($after.Issues -join ' ')"
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[switch]$RequireDomainJoined,
|
||||||
|
[switch]$RequireRemoteAccess,
|
||||||
|
[switch]$RequireBrokerHealth,
|
||||||
|
[string]$RemoteDesktopPrincipal = 'LCI\SG-Laboratorio-Usuarios-RDP',
|
||||||
|
[switch]$Enforce
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$providerClassId = '{D789CFD8-5AD4-489F-9B83-7EB5D9D09335}'
|
||||||
|
$passwordProviderClassId = '{60B78E88-EAD8-445C-9CFD-0B87F74EA6CD}'
|
||||||
|
$providerRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\$providerClassId"
|
||||||
|
$passwordProviderRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\$passwordProviderClassId"
|
||||||
|
$classRegistryPath = "HKLM:\SOFTWARE\Classes\CLSID\$providerClassId\InprocServer32"
|
||||||
|
$defaultProviderPolicyPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System'
|
||||||
|
$settingsPath = Join-Path $env:ProgramData 'SGU\CredentialProvider\settings.json'
|
||||||
|
$issues = [Collections.Generic.List[string]]::new()
|
||||||
|
|
||||||
|
$computer = Get-CimInstance Win32_ComputerSystem
|
||||||
|
if ($RequireDomainJoined -and -not $computer.PartOfDomain) {
|
||||||
|
$issues.Add('The computer is not joined to a domain.')
|
||||||
|
}
|
||||||
|
|
||||||
|
$providerRegistered = Test-Path -LiteralPath $providerRegistryPath
|
||||||
|
if (-not $providerRegistered) {
|
||||||
|
$issues.Add('The SGU Credential Provider registration is missing.')
|
||||||
|
}
|
||||||
|
|
||||||
|
$registeredDll = $null
|
||||||
|
if (Test-Path -LiteralPath $classRegistryPath) {
|
||||||
|
$registeredDll = (Get-Item -LiteralPath $classRegistryPath).GetValue('')
|
||||||
|
}
|
||||||
|
$providerBinaryPresent = $registeredDll -and (Test-Path -LiteralPath $registeredDll -PathType Leaf)
|
||||||
|
if (-not $providerBinaryPresent) {
|
||||||
|
$issues.Add('The registered SGU COM binary is missing.')
|
||||||
|
}
|
||||||
|
|
||||||
|
$configuredDefault = $null
|
||||||
|
try {
|
||||||
|
$configuredDefault = Get-ItemPropertyValue `
|
||||||
|
-LiteralPath $defaultProviderPolicyPath `
|
||||||
|
-Name DefaultCredentialProvider `
|
||||||
|
-ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# Report the missing or unreadable policy as a failed enrollment check.
|
||||||
|
}
|
||||||
|
$defaultProviderConfigured = $configuredDefault -eq $providerClassId
|
||||||
|
if (-not $defaultProviderConfigured) {
|
||||||
|
$issues.Add('The SGU provider is not assigned as the machine default credential provider.')
|
||||||
|
}
|
||||||
|
|
||||||
|
$passwordProviderPreserved = Test-Path -LiteralPath $passwordProviderRegistryPath
|
||||||
|
if (-not $passwordProviderPreserved) {
|
||||||
|
$issues.Add('The built-in Microsoft password provider registration is missing.')
|
||||||
|
}
|
||||||
|
|
||||||
|
$settings = $null
|
||||||
|
try {
|
||||||
|
$settings = Get-Content -LiteralPath $settingsPath -Raw | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$issues.Add('The SGU provider settings file is missing or invalid.')
|
||||||
|
}
|
||||||
|
|
||||||
|
$clientCertificatePresent = $false
|
||||||
|
$serverCertificateTrusted = $false
|
||||||
|
$brokerHealth = $null
|
||||||
|
if ($settings) {
|
||||||
|
$clientCertificate = Get-ChildItem Cert:\LocalMachine\My |
|
||||||
|
Where-Object Thumbprint -eq $settings.ClientCertificateThumbprint |
|
||||||
|
Select-Object -First 1
|
||||||
|
$clientCertificatePresent = $clientCertificate -and $clientCertificate.HasPrivateKey
|
||||||
|
if (-not $clientCertificatePresent) {
|
||||||
|
$issues.Add('The client mTLS certificate with private key is missing.')
|
||||||
|
}
|
||||||
|
|
||||||
|
$serverCertificate = Get-ChildItem Cert:\LocalMachine\Root,Cert:\LocalMachine\CA |
|
||||||
|
Where-Object Thumbprint -eq $settings.ServerCertificateThumbprint |
|
||||||
|
Select-Object -First 1
|
||||||
|
$serverCertificateTrusted = [bool]$serverCertificate
|
||||||
|
if (-not $serverCertificateTrusted) {
|
||||||
|
$issues.Add('The broker certificate is not trusted by LocalMachine.')
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($RequireBrokerHealth -and $clientCertificatePresent) {
|
||||||
|
try {
|
||||||
|
$healthUri = ([Uri]$settings.BrokerEndpoint).GetLeftPart([UriPartial]::Authority) + '/health/live'
|
||||||
|
$health = Invoke-RestMethod -Uri $healthUri -Certificate $clientCertificate -TimeoutSec 10
|
||||||
|
$brokerHealth = [string]$health.status
|
||||||
|
if ($brokerHealth -ne 'ok') {
|
||||||
|
$issues.Add('The broker health endpoint did not return ok.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$issues.Add("The broker health check failed: $($_.Exception.Message)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$dotNetRuntimePresent = $false
|
||||||
|
$dotnetCandidates = @(
|
||||||
|
(Get-Command dotnet -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source -ErrorAction SilentlyContinue),
|
||||||
|
(Join-Path $env:ProgramFiles 'dotnet\dotnet.exe')
|
||||||
|
) | Where-Object { $_ -and (Test-Path -LiteralPath $_ -PathType Leaf) } | Select-Object -Unique
|
||||||
|
foreach ($dotnet in $dotnetCandidates) {
|
||||||
|
if (& $dotnet --list-runtimes | Select-String '^Microsoft\.NETCore\.App 10\.') {
|
||||||
|
$dotNetRuntimePresent = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $dotNetRuntimePresent) {
|
||||||
|
$issues.Add('The Microsoft .NET 10 x64 runtime is missing.')
|
||||||
|
}
|
||||||
|
|
||||||
|
$remoteAccessReady = $null
|
||||||
|
if ($RequireRemoteAccess) {
|
||||||
|
$remoteDesktopUsersSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-555')
|
||||||
|
$remoteDesktopUsersGroup = ($remoteDesktopUsersSid.Translate([Security.Principal.NTAccount]).Value -split '\\', 2)[1]
|
||||||
|
$rdpMembers = @(Get-LocalGroupMember -Group $remoteDesktopUsersGroup -ErrorAction SilentlyContinue)
|
||||||
|
$remoteAccessReady =
|
||||||
|
(Get-Service TermService).Status -eq 'Running' -and
|
||||||
|
(Get-Service WinRM).Status -eq 'Running' -and
|
||||||
|
(Get-ItemPropertyValue 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name fDenyTSConnections) -eq 0 -and
|
||||||
|
$rdpMembers.Name -contains $RemoteDesktopPrincipal
|
||||||
|
if (-not $remoteAccessReady) {
|
||||||
|
$issues.Add('RDP/WinRM or the authorized domain group is not fully configured.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [pscustomobject]@{
|
||||||
|
ComputerName = $env:COMPUTERNAME
|
||||||
|
Domain = $computer.Domain
|
||||||
|
DomainJoined = [bool]$computer.PartOfDomain
|
||||||
|
ProviderRegistered = $providerRegistered
|
||||||
|
ProviderBinary = $registeredDll
|
||||||
|
ProviderBinaryPresent = [bool]$providerBinaryPresent
|
||||||
|
DefaultProviderConfigured = $defaultProviderConfigured
|
||||||
|
PasswordProviderPreserved = $passwordProviderPreserved
|
||||||
|
SettingsPresent = [bool]$settings
|
||||||
|
ClientCertificatePresent = [bool]$clientCertificatePresent
|
||||||
|
ServerCertificateTrusted = $serverCertificateTrusted
|
||||||
|
DotNetRuntimePresent = $dotNetRuntimePresent
|
||||||
|
BrokerHealth = $brokerHealth
|
||||||
|
RemoteAccessReady = $remoteAccessReady
|
||||||
|
IsValid = $issues.Count -eq 0
|
||||||
|
Issues = $issues.ToArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
$result
|
||||||
|
if ($Enforce -and -not $result.IsValid) {
|
||||||
|
throw "SGU client enrollment is invalid: $($issues -join ' ')"
|
||||||
|
}
|
||||||
@@ -8,14 +8,37 @@ $providerClassId = '{D789CFD8-5AD4-489F-9B83-7EB5D9D09335}'
|
|||||||
$installPath = Join-Path $env:ProgramFiles 'SGU\CredentialProvider'
|
$installPath = Join-Path $env:ProgramFiles 'SGU\CredentialProvider'
|
||||||
$providerRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\$providerClassId"
|
$providerRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\$providerClassId"
|
||||||
$classRegistryPath = "HKLM:\SOFTWARE\Classes\CLSID\$providerClassId"
|
$classRegistryPath = "HKLM:\SOFTWARE\Classes\CLSID\$providerClassId"
|
||||||
|
$defaultProviderPolicyPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System'
|
||||||
|
$enrollmentTaskName = 'SGU-CredentialProvider-EnrollmentGuard'
|
||||||
|
$enrollmentPath = Join-Path $env:ProgramData 'SGU\Enrollment'
|
||||||
|
|
||||||
if ($PSCmdlet.ShouldProcess($providerClassId, 'Unregister the SGU Credential Provider')) {
|
if ($PSCmdlet.ShouldProcess($providerClassId, 'Unregister the SGU Credential Provider')) {
|
||||||
|
if (Get-ScheduledTask -TaskName $enrollmentTaskName -ErrorAction SilentlyContinue) {
|
||||||
|
Unregister-ScheduledTask -TaskName $enrollmentTaskName -Confirm:$false
|
||||||
|
}
|
||||||
|
|
||||||
Remove-Item -LiteralPath $providerRegistryPath -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $providerRegistryPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
Remove-Item -LiteralPath $classRegistryPath -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $classRegistryPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
$configuredDefault = $null
|
||||||
|
try {
|
||||||
|
$configuredDefault = Get-ItemPropertyValue `
|
||||||
|
-LiteralPath $defaultProviderPolicyPath `
|
||||||
|
-Name DefaultCredentialProvider `
|
||||||
|
-ErrorAction Stop
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
# Nothing to remove when the policy is absent.
|
||||||
|
}
|
||||||
|
if ($configuredDefault -eq $providerClassId) {
|
||||||
|
Remove-ItemProperty -LiteralPath $defaultProviderPolicyPath `
|
||||||
|
-Name DefaultCredentialProvider -Force
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($RemoveFiles -and $PSCmdlet.ShouldProcess($installPath, 'Remove Credential Provider files')) {
|
if ($RemoveFiles -and $PSCmdlet.ShouldProcess($installPath, 'Remove Credential Provider files')) {
|
||||||
Remove-Item -LiteralPath $installPath -Recurse -Force -ErrorAction SilentlyContinue
|
Remove-Item -LiteralPath $installPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item -LiteralPath $enrollmentPath -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Output 'The built-in Windows password Credential Provider was not changed.'
|
Write-Output 'The built-in Windows password Credential Provider was not changed.'
|
||||||
|
|||||||
@@ -85,6 +85,11 @@ public sealed class BrokerOptions
|
|||||||
{
|
{
|
||||||
throw new InvalidOperationException("RemoteDesktopGroupDn must identify a group beneath BaseDn.");
|
throw new InvalidOperationException("RemoteDesktopGroupDn must identify a group beneath BaseDn.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Directory.DefaultCompany) || Directory.DefaultCompany.Length > 64)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("DefaultCompany is required and must not exceed 64 characters.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsCertificateThumbprint(string value)
|
private static bool IsCertificateThumbprint(string value)
|
||||||
@@ -139,6 +144,8 @@ public sealed class ActiveDirectoryOptions
|
|||||||
|
|
||||||
public string RemoteDesktopGroupDn { get; init; } = string.Empty;
|
public string RemoteDesktopGroupDn { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string DefaultCompany { get; init; } = "Universidad La Salle";
|
||||||
|
|
||||||
public bool CreateMissingOus { get; init; }
|
public bool CreateMissingOus { get; init; }
|
||||||
|
|
||||||
public string GetOuDn(InstitutionalRole role) => role switch
|
public string GetOuDn(InstitutionalRole role) => role switch
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ public sealed class ActiveDirectorySynchronizer(BrokerOptions options) : IActive
|
|||||||
user.Properties["pwdLastSet"].Value = -1;
|
user.Properties["pwdLastSet"].Value = -1;
|
||||||
user.CommitChanges();
|
user.CommitChanges();
|
||||||
|
|
||||||
TryApplyProfile(user, identity, profile);
|
TryApplyProfile(user, identity, profile, options.DefaultCompany);
|
||||||
TryEnsureRemoteDesktopGroupMembership(user);
|
TryEnsureRemoteDesktopGroupMembership(user);
|
||||||
|
|
||||||
return new DirectorySyncResult(
|
return new DirectorySyncResult(
|
||||||
@@ -119,14 +119,13 @@ public sealed class ActiveDirectorySynchronizer(BrokerOptions options) : IActive
|
|||||||
private static void TryApplyProfile(
|
private static void TryApplyProfile(
|
||||||
DirectoryEntry user,
|
DirectoryEntry user,
|
||||||
UserIdentity identity,
|
UserIdentity identity,
|
||||||
InstitutionalProfile? profile)
|
InstitutionalProfile? profile,
|
||||||
|
string defaultCompany)
|
||||||
{
|
{
|
||||||
if (profile is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
SetOptionalProperty(user, "company", defaultCompany);
|
||||||
|
if (profile is not null)
|
||||||
{
|
{
|
||||||
SetOptionalProperty(user, "displayName", profile.DisplayName);
|
SetOptionalProperty(user, "displayName", profile.DisplayName);
|
||||||
SetOptionalProperty(user, "mail", profile.Email);
|
SetOptionalProperty(user, "mail", profile.Email);
|
||||||
@@ -137,6 +136,7 @@ public sealed class ActiveDirectorySynchronizer(BrokerOptions options) : IActive
|
|||||||
{
|
{
|
||||||
SetOptionalProperty(user, "employeeID", profile.EmployeeNumber);
|
SetOptionalProperty(user, "employeeID", profile.EmployeeNumber);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
user.CommitChanges();
|
user.CommitChanges();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
"StudentOuDn": "OU=Alumnos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx",
|
"StudentOuDn": "OU=Alumnos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx",
|
||||||
"AdministrativeOuDn": "OU=Administrativos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx",
|
"AdministrativeOuDn": "OU=Administrativos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx",
|
||||||
"RemoteDesktopGroupDn": "",
|
"RemoteDesktopGroupDn": "",
|
||||||
|
"DefaultCompany": "Universidad La Salle",
|
||||||
"CreateMissingOus": false
|
"CreateMissingOus": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ internal static class ControlKeys
|
|||||||
{
|
{
|
||||||
public const string ProviderLabel = "ProviderLabel";
|
public const string ProviderLabel = "ProviderLabel";
|
||||||
public const string ProviderLogo = "ProviderLogo";
|
public const string ProviderLogo = "ProviderLogo";
|
||||||
|
public const string UserTile = "UserTile";
|
||||||
|
public const string Heading = "Heading";
|
||||||
public const string InformationLabel = "InformationLabel";
|
public const string InformationLabel = "InformationLabel";
|
||||||
public const string UserName = "UserName";
|
public const string UserName = "UserName";
|
||||||
public const string Password = "Password";
|
public const string Password = "Password";
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace SGU.CredentialProvider;
|
|||||||
internal static class ProviderTileIcon
|
internal static class ProviderTileIcon
|
||||||
{
|
{
|
||||||
public const int Size = 72;
|
public const int Size = 72;
|
||||||
|
public const int UserTileSize = 128;
|
||||||
|
|
||||||
public static Bitmap Create()
|
public static Bitmap Create()
|
||||||
{
|
{
|
||||||
@@ -30,4 +31,29 @@ internal static class ProviderTileIcon
|
|||||||
|
|
||||||
return bitmap;
|
return bitmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Bitmap CreateUserTile()
|
||||||
|
{
|
||||||
|
Bitmap bitmap = new(UserTileSize, UserTileSize, PixelFormat.Format32bppArgb);
|
||||||
|
using Graphics graphics = Graphics.FromImage(bitmap);
|
||||||
|
graphics.SmoothingMode = SmoothingMode.AntiAlias;
|
||||||
|
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||||
|
graphics.Clear(Color.FromArgb(0, 83, 155));
|
||||||
|
|
||||||
|
using Pen border = new(Color.FromArgb(190, 221, 241), 5f);
|
||||||
|
using Pen key = new(Color.White, 9f)
|
||||||
|
{
|
||||||
|
StartCap = LineCap.Round,
|
||||||
|
EndCap = LineCap.Round,
|
||||||
|
LineJoin = LineJoin.Round
|
||||||
|
};
|
||||||
|
|
||||||
|
graphics.DrawEllipse(border, 7, 7, 113, 113);
|
||||||
|
graphics.DrawEllipse(key, 25, 25, 43, 43);
|
||||||
|
graphics.DrawLine(key, 62, 62, 99, 99);
|
||||||
|
graphics.DrawLine(key, 82, 82, 95, 69);
|
||||||
|
graphics.DrawLine(key, 93, 93, 106, 80);
|
||||||
|
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,18 @@ public sealed class SguCredentialProvider : CredentialProviderBase
|
|||||||
|
|
||||||
public override IEnumerable<ControlBase> GetControls(UsageScenario cpus)
|
public override IEnumerable<ControlBase> GetControls(UsageScenario cpus)
|
||||||
{
|
{
|
||||||
yield return new CredentialProviderLabelControl(ControlKeys.ProviderLabel, "Acceso institucional SGU");
|
yield return new CredentialProviderLabelControl(
|
||||||
|
ControlKeys.ProviderLabel,
|
||||||
|
"Universidad La Salle · Acceso SGU");
|
||||||
yield return new CredentialProviderLogoControl(
|
yield return new CredentialProviderLogoControl(
|
||||||
ControlKeys.ProviderLogo,
|
ControlKeys.ProviderLogo,
|
||||||
"Acceso institucional SGU",
|
"Acceso institucional SGU",
|
||||||
ProviderTileIcon.Create());
|
ProviderTileIcon.Create());
|
||||||
|
yield return new UserTileControl(
|
||||||
|
ControlKeys.UserTile,
|
||||||
|
"Universidad La Salle",
|
||||||
|
ProviderTileIcon.CreateUserTile());
|
||||||
|
yield return new LargeLabelControl(ControlKeys.Heading, "Acceso institucional SGU");
|
||||||
yield return new SmallLabelControl(
|
yield return new SmallLabelControl(
|
||||||
ControlKeys.InformationLabel,
|
ControlKeys.InformationLabel,
|
||||||
"Usa tu clave institucional (DO, AL o AD + 6 dígitos) y contraseña.");
|
"Usa tu clave institucional (DO, AL o AD + 6 dígitos) y contraseña.");
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ internal sealed class SguCredentialTile : CredentialTile2
|
|||||||
public SguCredentialTile(CredentialProviderBase credentialProvider)
|
public SguCredentialTile(CredentialProviderBase credentialProvider)
|
||||||
: base(credentialProvider)
|
: base(credentialProvider)
|
||||||
{
|
{
|
||||||
|
GenericTileDisplayMode = GenericTileDisplayMode.DisplayAsDedicatedTile;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SguCredentialTile(CredentialProviderBase credentialProvider, CredentialProviderUser user)
|
public SguCredentialTile(CredentialProviderBase credentialProvider, CredentialProviderUser user)
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ internal static class Program
|
|||||||
|
|
||||||
private static readonly string[] ExpectedLabels =
|
private static readonly string[] ExpectedLabels =
|
||||||
[
|
[
|
||||||
|
"Universidad La Salle · Acceso SGU",
|
||||||
"Acceso institucional SGU",
|
"Acceso institucional SGU",
|
||||||
|
"Universidad La Salle",
|
||||||
"Acceso institucional SGU",
|
"Acceso institucional SGU",
|
||||||
"Usa tu clave institucional (DO, AL o AD + 6 dígitos) y contraseña.",
|
"Usa tu clave institucional (DO, AL o AD + 6 dígitos) y contraseña.",
|
||||||
"Clave institucional",
|
"Clave institucional",
|
||||||
@@ -55,6 +57,7 @@ internal static class Program
|
|||||||
|
|
||||||
List<string> labels = [];
|
List<string> labels = [];
|
||||||
bool providerLogoPresent = false;
|
bool providerLogoPresent = false;
|
||||||
|
bool userTilePresent = false;
|
||||||
for (uint index = 0; index < fieldCount; index++)
|
for (uint index = 0; index < fieldCount; index++)
|
||||||
{
|
{
|
||||||
ThrowIfFailed(provider.GetFieldDescriptorAt(index, out IntPtr descriptorPointer), "GetFieldDescriptorAt");
|
ThrowIfFailed(provider.GetFieldDescriptorAt(index, out IntPtr descriptorPointer), "GetFieldDescriptorAt");
|
||||||
@@ -69,6 +72,8 @@ internal static class Program
|
|||||||
labels.Add(descriptor.Label ?? string.Empty);
|
labels.Add(descriptor.Label ?? string.Empty);
|
||||||
providerLogoPresent |= descriptor.FieldType == FieldType.TileImage &&
|
providerLogoPresent |= descriptor.FieldType == FieldType.TileImage &&
|
||||||
descriptor.FieldTypeGuid == ProviderLogoFieldType;
|
descriptor.FieldTypeGuid == ProviderLogoFieldType;
|
||||||
|
userTilePresent |= descriptor.FieldType == FieldType.TileImage &&
|
||||||
|
descriptor.FieldTypeGuid == Guid.Empty;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -87,6 +92,7 @@ internal static class Program
|
|||||||
credentialCount == 1 &&
|
credentialCount == 1 &&
|
||||||
credential != IntPtr.Zero &&
|
credential != IntPtr.Zero &&
|
||||||
providerLogoPresent &&
|
providerLogoPresent &&
|
||||||
|
userTilePresent &&
|
||||||
labels.SequenceEqual(ExpectedLabels, StringComparer.Ordinal);
|
labels.SequenceEqual(ExpectedLabels, StringComparer.Ordinal);
|
||||||
|
|
||||||
if (mode != "enumeration" && passed)
|
if (mode != "enumeration" && passed)
|
||||||
@@ -103,6 +109,7 @@ internal static class Program
|
|||||||
fieldCount,
|
fieldCount,
|
||||||
labels,
|
labels,
|
||||||
providerLogoPresent,
|
providerLogoPresent,
|
||||||
|
userTilePresent,
|
||||||
credentialCount,
|
credentialCount,
|
||||||
defaultIndex,
|
defaultIndex,
|
||||||
autoLogon = autoLogon != 0
|
autoLogon = autoLogon != 0
|
||||||
|
|||||||
@@ -31,4 +31,20 @@ public sealed class ProviderTileIconTests
|
|||||||
|
|
||||||
Assert.InRange(lightPixels, 200, 2_000);
|
Assert.InRange(lightPixels, 200, 2_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProviderPublishesABrandedDedicatedUserTileAndHeading()
|
||||||
|
{
|
||||||
|
SguCredentialProvider provider = new();
|
||||||
|
ControlBase[] controls = provider.GetControls(UsageScenario.Logon).ToArray();
|
||||||
|
|
||||||
|
UserTileControl userTile = Assert.Single(controls.OfType<UserTileControl>());
|
||||||
|
LargeLabelControl heading = Assert.Single(controls.OfType<LargeLabelControl>());
|
||||||
|
SguCredentialTile tile = Assert.IsType<SguCredentialTile>(provider.CreateGenericTile());
|
||||||
|
|
||||||
|
Assert.Equal(ProviderTileIcon.UserTileSize, userTile.Bitmap.Width);
|
||||||
|
Assert.Equal(ProviderTileIcon.UserTileSize, userTile.Bitmap.Height);
|
||||||
|
Assert.Equal("Acceso institucional SGU", heading.Label);
|
||||||
|
Assert.Equal(GenericTileDisplayMode.DisplayAsDedicatedTile, tile.GenericTileDisplayMode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user