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)
|
||||
- [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)
|
||||
|
||||
| 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
|
||||
synchronized SGU user to that dedicated AD security group. The laboratory
|
||||
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`,
|
||||
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
|
||||
dotnet restore .\SGU-CredentialProvider.sln
|
||||
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
|
||||
```
|
||||
|
||||
@@ -111,6 +112,10 @@ OU on Windows Server. Use separate authorized test accounts for `DO`, `AL`, and
|
||||
|
||||
## 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:
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -21,6 +21,11 @@ Resolve-DnsName -Type SRV _ldap._tcp.dc._msdcs.lci.lasalle.mx
|
||||
|
||||
## 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
|
||||
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
|
||||
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
|
||||
del acceso SGU. El proveedor de contraseña de Microsoft debe permanecer visible.
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ param(
|
||||
[string]$DomainNetbios = 'LCI',
|
||||
[string]$UpnSuffix = 'lci.lasalle.mx',
|
||||
[string]$RemoteDesktopGroupDn = '',
|
||||
[ValidateLength(1, 64)]
|
||||
[string]$DefaultCompany = 'Universidad La Salle',
|
||||
[ValidateRange(10, 60)]
|
||||
[int]$NtlmTimeoutSeconds = 15,
|
||||
[switch]$CreateMissingOus,
|
||||
@@ -152,6 +154,7 @@ $productionSettings = @{
|
||||
StudentOuDn = "OU=Alumnos,OU=Usuarios-SGU,$BaseDn"
|
||||
AdministrativeOuDn = "OU=Administrativos,OU=Usuarios-SGU,$BaseDn"
|
||||
RemoteDesktopGroupDn = $RemoteDesktopGroupDn
|
||||
DefaultCompany = $DefaultCompany
|
||||
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)]
|
||||
[int]$TimeoutSeconds = 20,
|
||||
|
||||
[switch]$DoNotSetAsDefaultCredentialProvider,
|
||||
|
||||
[switch]$InstallDotNetRuntime,
|
||||
|
||||
[string]$DotNetRuntimeInstallerPath
|
||||
@@ -31,6 +33,7 @@ $installRoot = Join-Path $env:ProgramFiles 'SGU\CredentialProvider'
|
||||
$settingsPath = Join-Path $env:ProgramData 'SGU\CredentialProvider\settings.json'
|
||||
$providerRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\$providerClassId"
|
||||
$classRegistryPath = "HKLM:\SOFTWARE\Classes\CLSID\$providerClassId\InprocServer32"
|
||||
$defaultProviderPolicyPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System'
|
||||
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$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-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]@{
|
||||
@@ -183,5 +206,6 @@ if ($PSCmdlet.ShouldProcess($installPath, 'Install and register the SGU Credenti
|
||||
InstallPath = $installPath
|
||||
SettingsPath = $settingsPath
|
||||
Registered = Test-Path -LiteralPath $providerRegistryPath
|
||||
DefaultProviderConfigured = $defaultProviderConfigured
|
||||
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'
|
||||
$providerRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\Credential Providers\$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 (Get-ScheduledTask -TaskName $enrollmentTaskName -ErrorAction SilentlyContinue) {
|
||||
Unregister-ScheduledTask -TaskName $enrollmentTaskName -Confirm:$false
|
||||
}
|
||||
|
||||
Remove-Item -LiteralPath $providerRegistryPath -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')) {
|
||||
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.'
|
||||
|
||||
@@ -85,6 +85,11 @@ public sealed class BrokerOptions
|
||||
{
|
||||
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)
|
||||
@@ -139,6 +144,8 @@ public sealed class ActiveDirectoryOptions
|
||||
|
||||
public string RemoteDesktopGroupDn { get; init; } = string.Empty;
|
||||
|
||||
public string DefaultCompany { get; init; } = "Universidad La Salle";
|
||||
|
||||
public bool CreateMissingOus { get; init; }
|
||||
|
||||
public string GetOuDn(InstitutionalRole role) => role switch
|
||||
|
||||
@@ -100,7 +100,7 @@ public sealed class ActiveDirectorySynchronizer(BrokerOptions options) : IActive
|
||||
user.Properties["pwdLastSet"].Value = -1;
|
||||
user.CommitChanges();
|
||||
|
||||
TryApplyProfile(user, identity, profile);
|
||||
TryApplyProfile(user, identity, profile, options.DefaultCompany);
|
||||
TryEnsureRemoteDesktopGroupMembership(user);
|
||||
|
||||
return new DirectorySyncResult(
|
||||
@@ -119,23 +119,23 @@ public sealed class ActiveDirectorySynchronizer(BrokerOptions options) : IActive
|
||||
private static void TryApplyProfile(
|
||||
DirectoryEntry user,
|
||||
UserIdentity identity,
|
||||
InstitutionalProfile? profile)
|
||||
InstitutionalProfile? profile,
|
||||
string defaultCompany)
|
||||
{
|
||||
if (profile is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
SetOptionalProperty(user, "displayName", profile.DisplayName);
|
||||
SetOptionalProperty(user, "mail", profile.Email);
|
||||
SetOptionalProperty(user, "title", profile.JobTitle);
|
||||
SetOptionalProperty(user, "department", profile.Department);
|
||||
SetOptionalProperty(user, "employeeType", profile.EmployeeType);
|
||||
if (string.Equals(profile.EmployeeNumber, identity.NumericId, StringComparison.Ordinal))
|
||||
SetOptionalProperty(user, "company", defaultCompany);
|
||||
if (profile is not null)
|
||||
{
|
||||
SetOptionalProperty(user, "employeeID", profile.EmployeeNumber);
|
||||
SetOptionalProperty(user, "displayName", profile.DisplayName);
|
||||
SetOptionalProperty(user, "mail", profile.Email);
|
||||
SetOptionalProperty(user, "title", profile.JobTitle);
|
||||
SetOptionalProperty(user, "department", profile.Department);
|
||||
SetOptionalProperty(user, "employeeType", profile.EmployeeType);
|
||||
if (string.Equals(profile.EmployeeNumber, identity.NumericId, StringComparison.Ordinal))
|
||||
{
|
||||
SetOptionalProperty(user, "employeeID", profile.EmployeeNumber);
|
||||
}
|
||||
}
|
||||
|
||||
user.CommitChanges();
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"StudentOuDn": "OU=Alumnos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx",
|
||||
"AdministrativeOuDn": "OU=Administrativos,OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx",
|
||||
"RemoteDesktopGroupDn": "",
|
||||
"DefaultCompany": "Universidad La Salle",
|
||||
"CreateMissingOus": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ internal static class ControlKeys
|
||||
{
|
||||
public const string ProviderLabel = "ProviderLabel";
|
||||
public const string ProviderLogo = "ProviderLogo";
|
||||
public const string UserTile = "UserTile";
|
||||
public const string Heading = "Heading";
|
||||
public const string InformationLabel = "InformationLabel";
|
||||
public const string UserName = "UserName";
|
||||
public const string Password = "Password";
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace SGU.CredentialProvider;
|
||||
internal static class ProviderTileIcon
|
||||
{
|
||||
public const int Size = 72;
|
||||
public const int UserTileSize = 128;
|
||||
|
||||
public static Bitmap Create()
|
||||
{
|
||||
@@ -30,4 +31,29 @@ internal static class ProviderTileIcon
|
||||
|
||||
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)
|
||||
{
|
||||
yield return new CredentialProviderLabelControl(ControlKeys.ProviderLabel, "Acceso institucional SGU");
|
||||
yield return new CredentialProviderLabelControl(
|
||||
ControlKeys.ProviderLabel,
|
||||
"Universidad La Salle · Acceso SGU");
|
||||
yield return new CredentialProviderLogoControl(
|
||||
ControlKeys.ProviderLogo,
|
||||
"Acceso institucional SGU",
|
||||
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(
|
||||
ControlKeys.InformationLabel,
|
||||
"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)
|
||||
: base(credentialProvider)
|
||||
{
|
||||
GenericTileDisplayMode = GenericTileDisplayMode.DisplayAsDedicatedTile;
|
||||
}
|
||||
|
||||
public SguCredentialTile(CredentialProviderBase credentialProvider, CredentialProviderUser user)
|
||||
|
||||
@@ -14,7 +14,9 @@ internal static class Program
|
||||
|
||||
private static readonly string[] ExpectedLabels =
|
||||
[
|
||||
"Universidad La Salle · Acceso SGU",
|
||||
"Acceso institucional SGU",
|
||||
"Universidad La Salle",
|
||||
"Acceso institucional SGU",
|
||||
"Usa tu clave institucional (DO, AL o AD + 6 dígitos) y contraseña.",
|
||||
"Clave institucional",
|
||||
@@ -55,6 +57,7 @@ internal static class Program
|
||||
|
||||
List<string> labels = [];
|
||||
bool providerLogoPresent = false;
|
||||
bool userTilePresent = false;
|
||||
for (uint index = 0; index < fieldCount; index++)
|
||||
{
|
||||
ThrowIfFailed(provider.GetFieldDescriptorAt(index, out IntPtr descriptorPointer), "GetFieldDescriptorAt");
|
||||
@@ -69,6 +72,8 @@ internal static class Program
|
||||
labels.Add(descriptor.Label ?? string.Empty);
|
||||
providerLogoPresent |= descriptor.FieldType == FieldType.TileImage &&
|
||||
descriptor.FieldTypeGuid == ProviderLogoFieldType;
|
||||
userTilePresent |= descriptor.FieldType == FieldType.TileImage &&
|
||||
descriptor.FieldTypeGuid == Guid.Empty;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -87,6 +92,7 @@ internal static class Program
|
||||
credentialCount == 1 &&
|
||||
credential != IntPtr.Zero &&
|
||||
providerLogoPresent &&
|
||||
userTilePresent &&
|
||||
labels.SequenceEqual(ExpectedLabels, StringComparer.Ordinal);
|
||||
|
||||
if (mode != "enumeration" && passed)
|
||||
@@ -103,6 +109,7 @@ internal static class Program
|
||||
fieldCount,
|
||||
labels,
|
||||
providerLogoPresent,
|
||||
userTilePresent,
|
||||
credentialCount,
|
||||
defaultIndex,
|
||||
autoLogon = autoLogon != 0
|
||||
|
||||
@@ -31,4 +31,20 @@ public sealed class ProviderTileIconTests
|
||||
|
||||
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