Enrich professor profiles and harden client policy

This commit is contained in:
2026-09-03 15:40:40 -06:00
parent 403132f869
commit 742ae9c2b5
12 changed files with 338 additions and 33 deletions
+6 -4
View File
@@ -34,10 +34,12 @@ photo, and manager fields are ignored. Student enrichment targets the read-only
student information page and reads only the matching student number, structured
name, email, career, and postal address. The career becomes an AD title in the
form `Estudiante de ...`; faculty/department remains unset because the verified
page does not expose it. Professors retain the menu display-name fallback until
a richer role-specific page is verified. Missing or changed presentation HTML
never blocks authentication or password synchronization after the lightweight
NTLM root has accepted the credential.
page does not expose it. Professors use the portal menu name as their safe base
profile and then attempt the same personal and location modules as administrative
staff. A missing route, non-success response, timeout, or absent element ID merely
skips that optional field. Missing or changed presentation HTML never blocks
authentication or password synchronization after the lightweight NTLM root has
accepted the credential.
Operational documentation:
+16 -7
View File
@@ -28,9 +28,11 @@ administrative incident overview for `AD`, the student information page for
`AL`, or the portal menu for `DO`. After the incident page confirms an `AD`
employee number, two additional GETs in the same in-memory session read the
structured name from `datos/personales.aspx` and the address from
`datos/ubicacion.aspx`. A supplemental failure preserves fields already
collected, and a profile timeout does not invalidate an already authenticated
credential. NTLM may still require its normal
`datos/ubicacion.aspx`. Docentes keep the menu name as a base and attempt those
same two shared staff modules without requiring them to exist. A supplemental
404, changed/missing element ID, other failure, or timeout preserves fields
already collected, and a profile timeout does not invalidate an already
authenticated credential. NTLM may still require its normal
challenge/response round trips on the connection. Transient portal cookies are
kept only in an in-memory per-request container and are never persisted or
returned to the client.
@@ -60,9 +62,12 @@ updates the applicable `displayName`, `givenName`, `sn`, `mail`, `title`,
`postalCode` attributes. Administrative and student numbers must match the six
numeric digits of the requested identity before any role-specific metadata is
trusted. Administrative personal and location pages are accepted only after
that incident-page match. Student faculty/department is deliberately left unset because the
verified page does not expose it. Missing metadata does not clear existing AD
values and never changes the password outcome.
that incident-page match. A docente's supplemental fields remain tied to the
fresh NTLM-authenticated portal session and are optional; the menu display name
remains usable if neither shared page is available. Student faculty/department
is deliberately left unset because the verified page does not expose it.
Missing metadata does not clear existing AD values and never changes the
password outcome.
Human-readable SGU values are decoded with BOM/header/meta detection, strict
UTF-8 validation, and a Windows-1252 fallback for the legacy portal. Names and
@@ -91,7 +96,11 @@ synthetic Windows account to imitate that title or ordering.
The managed hierarchy is rooted at `OU=Usuarios-SGU`: `Docentes`, `Alumnos`,
and `Administrativos` are direct child OUs beneath it. The domain GPO
`SGU - User session restrictions` is linked to this root and enables the
per-user `DisableLockWorkstation` policy for the complete hierarchy.
per-user `DisableLockWorkstation` policy and disables screen savers for the
complete hierarchy. The computer GPO `SGU - Windows client experience` is linked
to `OU=Laboratorio`; it suppresses first-logon/privacy/diagnostic prompts,
disables location, and enforces always-on display, sleep, and hibernation
settings for managed clients.
Per-user synchronization is serialized inside the broker to prevent concurrent
create/reset races. Production deployments should run the broker as a gMSA with
+3 -2
View File
@@ -50,8 +50,9 @@ Eso es comportamiento esperado, no una caída del servicio.
- El broker permite hasta **20 segundos** para el desafío NTLM ligero de
`/psulsa/`. El enriquecimiento usa el límite total independiente
`ProfileTimeoutSeconds` —**90 segundos** en la configuración del laboratorio—
y conserva los campos que alcance a obtener si una página administrativa se
retrasa o falla. El Credential Provider mantiene su propio límite de **90
y conserva los campos que alcance a obtener si una página de personal se
retrasa, no existe o cambia sus IDs. Esto incluye los módulos opcionales de
nombre y ubicación para docentes. El Credential Provider mantiene su propio límite de **90
segundos**: si SGU excede ese presupuesto, Windows continúa por el fallback
normal de AD o credenciales de dominio en caché.
- El instalador configura recuperación del servicio con reinicios a los 5, 15
+21 -4
View File
@@ -82,17 +82,29 @@ siguiente arranque síncrono y el fondo se aplica al siguiente inicio de sesión
del usuario SGU.
En el controlador de dominio, aplicar una vez la directiva de usuario al árbol
completo `Usuarios-SGU`:
completo `Usuarios-SGU` y la directiva de equipo a `Laboratorio`:
```powershell
.\Set-SguDomainUserPolicies.ps1
.\Set-SguDomainComputerPolicies.ps1
```
El script crea y vincula de forma idempotente el GPO
`SGU - User session restrictions` y configura `DisableLockWorkstation=1` para
Administrativos, Alumnos y Docentes. La directiva elimina la acción manual de
bloqueo; no sustituye otras directivas independientes de protector de pantalla,
inactividad o desconexión de RDP.
Administrativos, Alumnos y Docentes; también deshabilita el protector de
pantalla. El segundo crea `SGU - Windows client experience` y, antes del primer
inicio de sesión de dominio, suprime la experiencia de privacidad, las
notificaciones y controles de telemetría, la animación **Hi / Preparing
Windows**, y el acceso a ubicación. Además fija en **Nunca** el apagado de
pantalla, suspensión, hibernación y suspensión híbrida, conectado a corriente o
batería. El guard de enrolamiento vuelve a aplicar `powercfg /hibernate off` y
los tiempos en cero al inicio y diariamente.
`HideEULAPage` no forma parte de esta GPO: es una opción de archivo Unattend para
la fase OOBE y Microsoft la reserva para pruebas de OEM/System Builder. La GPO
usa las alternativas soportadas `DisablePrivacyExperience=1` y
`EnableFirstLogonAnimation=0`, que son las que afectan a cada cuenta de dominio
nueva.
## 3. Verificación y reparación
@@ -134,5 +146,10 @@ 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/client-management/mdm/policy-csp-windowslogon#enumeratelocalusersondomainjoinedcomputers>
- <https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-admx-ctrlaltdel#disablelockcomputer>
- <https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-privacy#disableprivacyexperience>
- <https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-windowslogon#enablefirstlogonanimation>
- <https://learn.microsoft.com/en-us/windows/privacy/manage-connections-from-windows-operating-system-components-to-microsoft-services#182-location>
- <https://learn.microsoft.com/en-us/windows-hardware/design/device-experiences/powercfg-command-line-options>
- <https://learn.microsoft.com/en-us/windows-hardware/customize/desktop/automate-oobe>
- <https://learn.microsoft.com/en-us/windows/win32/api/credentialprovider/ne-credentialprovider-credential_provider_field_type>
- <https://learn.microsoft.com/en-us/windows/win32/api/credentialprovider/nf-credentialprovider-icredentialprovidercredential2-getusersid>
+8 -4
View File
@@ -4,8 +4,8 @@ Validated lab inventory:
- Domain controller/broker: Windows Server 2025 Standard,
`WIN-1AIQMMA1EPR.lci.lasalle.mx`, `192.168.50.10`.
- Client: Windows 10 Pro 22H2,
`DESKTOP-U1I3BNN.lci.lasalle.mx`, `192.168.50.20`.
- Client: no active Windows client VM. The next enrollment target is a clean
Windows 11 Enterprise VM placed in `OU=Laboratorio`.
- Domain: `lci.lasalle.mx` / `LCI`.
- Private switch: `Laboratorio AD`.
@@ -133,14 +133,18 @@ Use Lithnet's `Invoke-CredUI` test utility when available, or sign out/restart
the VM and select **Acceso institucional SGU**. Keep the built-in Windows
password provider available through **Other user**.
On the domain controller, apply the SGU user policy once:
On the domain controller, apply the SGU user and computer policies once:
```powershell
.\Set-SguDomainUserPolicies.ps1
.\Set-SguDomainComputerPolicies.ps1
```
It links `SGU - User session restrictions` to `OU=Usuarios-SGU` and prevents
managed users from manually locking the workstation.
managed users from manually locking the workstation or starting a screen saver.
It also links `SGU - Windows client experience` to `OU=Laboratorio` to suppress
first-logon privacy/diagnostic/location screens and keep display, sleep, and
hibernation disabled.
Before testing through Hyper-V Enhanced Session/RDP, enable the dedicated lab
group and Windows PowerShell Remoting:
+5 -1
View File
@@ -43,12 +43,16 @@
city/municipality, state, and postal code from known element IDs.
- Student CURP, birth date, sex, blood type, marital status, telephone, mobile,
guardian, medical, financial, and academic-history values are ignored.
- Professor enrichment keeps the menu display name as its base and optionally
reads only the same name and postal-address element IDs used by staff pages.
A missing professor route or element never makes authentication fail.
- Incident details, calendars, photographs, manager names, and manager positions
are deliberately ignored.
- The employee or student number must match the authenticated `AD` or `AL` key
before role-specific metadata is synchronized. The two supplemental
administrative pages are never requested unless the incident page supplied
the matching employee number.
the matching employee number. Professor supplemental data comes from the
same fresh, request-scoped NTLM session as its menu fallback.
- If SGU changes its HTML, authentication and exact-password synchronization
continue without enrichment; existing AD metadata is not erased.
- Slow profile pages cannot change an accepted credential into a rejection. The
+8
View File
@@ -63,6 +63,8 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass `
El script realiza de forma idempotente lo siguiente:
- fija pantalla, suspensión e hibernación en **Nunca** para corriente y batería,
y ejecuta `powercfg /hibernate off`;
- habilita RDP y conserva Network Level Authentication;
- habilita las reglas RDP solo para el perfil Domain;
- autoriza el grupo SGU dedicado;
@@ -121,6 +123,12 @@ contraseña incorrecta. Comprobar que la cuenta pertenece a
`SG-Laboratorio-Usuarios-RDP` y que ese grupo aparece en **Remote Desktop Users**.
Las directivas de grupo de dominio prevalecen sobre la política local.
En el controlador de dominio, `Set-SguDomainComputerPolicies.ps1` crea y vincula
la GPO `SGU - Windows client experience` a `OU=Laboratorio`. Esa GPO refuerza la
configuración siempre activa y evita las experiencias iniciales de privacidad,
telemetría, ubicación y **Hi / Preparing Windows** antes de que un usuario SGU
entre por primera vez.
Microsoft documenta este derecho en:
<https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-userrights#allowlogonthroughremotedesktop>
y PowerShell Remoting en:
+18
View File
@@ -20,6 +20,23 @@ $remoteDesktopUsersSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-
$remoteDesktopUsersGroup = ($remoteDesktopUsersSid.Translate([Security.Principal.NTAccount]).Value -split '\\', 2)[1]
if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Enable RDP and grant $RemoteDesktopPrincipal access")) {
foreach ($powerChange in @(
@('monitor-timeout-ac', '0'),
@('monitor-timeout-dc', '0'),
@('standby-timeout-ac', '0'),
@('standby-timeout-dc', '0'),
@('hibernate-timeout-ac', '0'),
@('hibernate-timeout-dc', '0'))) {
& powercfg.exe /change $powerChange[0] $powerChange[1]
if ($LASTEXITCODE -ne 0) {
throw "powercfg /change $($powerChange[0]) failed with exit code $LASTEXITCODE."
}
}
& powercfg.exe /hibernate off
if ($LASTEXITCODE -ne 0) {
throw "powercfg /hibernate off failed with exit code $LASTEXITCODE."
}
Set-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' `
-Name fDenyTSConnections -Type DWord -Value 0
Set-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' `
@@ -87,4 +104,5 @@ $rdpMembers = @(Get-LocalGroupMember -Group $remoteDesktopUsersGroup -ErrorActio
TermService = (Get-Service TermService).Status
WinRM = (Get-Service WinRM).Status
FirewallProfile = 'Domain'
AlwaysOnPowerPolicyApplied = $true
}
+124
View File
@@ -0,0 +1,124 @@
[CmdletBinding(SupportsShouldProcess)]
param(
[string]$TargetOuDn = 'OU=Laboratorio,DC=lci,DC=lasalle,DC=mx',
[string]$GpoName = 'SGU - Windows client experience',
[string]$DomainController = $env:COMPUTERNAME
)
$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 on a domain controller or management host.'
}
Import-Module ActiveDirectory -ErrorAction Stop
Import-Module GroupPolicy -ErrorAction Stop
$targetOu = Get-ADOrganizationalUnit `
-Identity $TargetOuDn `
-Server $DomainController `
-ErrorAction Stop
$domainDn = ($targetOu.DistinguishedName -split ',DC=', 2)[1]
if (-not $domainDn) {
throw 'TargetOuDn does not contain a domain distinguished name.'
}
$domainName = ($domainDn -replace ',DC=', '.')
$gpo = Get-GPO -Name $GpoName -Domain $domainName -Server $DomainController -ErrorAction SilentlyContinue
if (-not $gpo -and $PSCmdlet.ShouldProcess($GpoName, 'Create the SGU Windows client policy GPO')) {
$gpo = New-GPO -Name $GpoName -Domain $domainName -Server $DomainController
}
if (-not $gpo) {
throw "The GPO '$GpoName' does not exist and was not created."
}
$existingLink = @(Get-GPInheritance -Target $TargetOuDn -Domain $domainName -Server $DomainController).GpoLinks |
Where-Object DisplayName -eq $GpoName |
Select-Object -First 1
$existingLinkEnabled = $existingLink -and (
$existingLink.Enabled -eq $true -or
[string]$existingLink.Enabled -eq 'Yes')
if (-not $existingLink) {
if ($PSCmdlet.ShouldProcess($TargetOuDn, "Link and enable '$GpoName'")) {
New-GPLink `
-Name $GpoName `
-Target $TargetOuDn `
-Domain $domainName `
-Server $DomainController `
-LinkEnabled Yes | Out-Null
}
}
elseif (-not $existingLinkEnabled -and
$PSCmdlet.ShouldProcess($TargetOuDn, "Enable the '$GpoName' link")) {
Set-GPLink `
-Name $GpoName `
-Target $TargetOuDn `
-Domain $domainName `
-Server $DomainController `
-LinkEnabled Yes | Out-Null
}
$dataCollectionKey = 'HKLM\Software\Policies\Microsoft\Windows\DataCollection'
$powerPolicyRoot = 'HKLM\Software\Policies\Microsoft\Power\PowerSettings'
$policies = @(
@{ Key = $dataCollectionKey; Name = 'AllowTelemetry'; Value = 0 },
@{ Key = $dataCollectionKey; Name = 'DisableTelemetryOptInSettingsUx'; Value = 1 },
@{ Key = $dataCollectionKey; Name = 'DisableTelemetryOptInChangeNotification'; Value = 1 },
@{ Key = $dataCollectionKey; Name = 'DisableDiagnosticDataViewer'; Value = 1 },
@{ Key = 'HKLM\Software\Policies\Microsoft\Windows\OOBE'; Name = 'DisablePrivacyExperience'; Value = 1 },
@{ Key = 'HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System'; Name = 'EnableFirstLogonAnimation'; Value = 0 },
@{ Key = 'HKLM\Software\Policies\Microsoft\Windows\LocationAndSensors'; Name = 'DisableLocation'; Value = 1 },
@{ Key = 'HKLM\Software\Policies\Microsoft\Windows\AppPrivacy'; Name = 'LetAppsAccessLocation'; Value = 2 }
)
$powerSettingIds = @(
'3c0bc021-c8a8-4e07-a973-6b14cbcb2b7e', # Turn off display after
'29f6c1db-86da-48c5-9fdb-f2b67b1f44da', # Sleep after
'9d7815a6-7ee4-497e-8888-515a05f02364', # Hibernate after
'94ac6d29-73ce-41a6-809f-6363ba21b47e' # Allow hybrid sleep
)
foreach ($settingId in $powerSettingIds) {
$settingKey = "$powerPolicyRoot\$settingId"
$policies += @{ Key = $settingKey; Name = 'ACSettingIndex'; Value = 0 }
$policies += @{ Key = $settingKey; Name = 'DCSettingIndex'; Value = 0 }
}
foreach ($policy in $policies) {
if ($PSCmdlet.ShouldProcess($GpoName, "Set $($policy.Key)\$($policy.Name)=$($policy.Value)")) {
Set-GPRegistryValue `
-Name $GpoName `
-Domain $domainName `
-Server $DomainController `
-Key $policy.Key `
-ValueName $policy.Name `
-Type DWord `
-Value $policy.Value | Out-Null
}
}
$configuredPolicies = [ordered]@{}
foreach ($policy in $policies) {
$configured = Get-GPRegistryValue `
-Name $GpoName `
-Domain $domainName `
-Server $DomainController `
-Key $policy.Key `
-ValueName $policy.Name
$configuredPolicies[$policy.Name + '@' + $policy.Key] = [int]$configured.Value
}
$link = @(Get-GPInheritance -Target $TargetOuDn -Domain $domainName -Server $DomainController).GpoLinks |
Where-Object DisplayName -eq $GpoName |
Select-Object -First 1
$linkEnabled = $link -and (
$link.Enabled -eq $true -or
[string]$link.Enabled -eq 'Yes')
[pscustomobject]@{
GpoName = $GpoName
GpoId = $gpo.Id
TargetOu = $TargetOuDn
LinkEnabled = [bool]$linkEnabled
PolicyCount = $configuredPolicies.Count
Policies = [pscustomobject]$configuredPolicies
}
+17
View File
@@ -8,6 +8,7 @@ param(
$ErrorActionPreference = 'Stop'
$policyKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System'
$policyValueName = 'DisableLockWorkstation'
$desktopPolicyKey = 'HKCU\Software\Policies\Microsoft\Windows\Control Panel\Desktop'
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
@@ -71,6 +72,15 @@ if ($PSCmdlet.ShouldProcess($GpoName, 'Prevent SGU users from manually locking w
-ValueName $policyValueName `
-Type DWord `
-Value 1 | Out-Null
Set-GPRegistryValue `
-Name $GpoName `
-Domain $domainName `
-Server $DomainController `
-Key $desktopPolicyKey `
-ValueName 'ScreenSaveActive' `
-Type String `
-Value '0' | Out-Null
}
$configuredValue = Get-GPRegistryValue `
@@ -79,6 +89,12 @@ $configuredValue = Get-GPRegistryValue `
-Server $DomainController `
-Key $policyKey `
-ValueName $policyValueName
$screenSaverValue = Get-GPRegistryValue `
-Name $GpoName `
-Domain $domainName `
-Server $DomainController `
-Key $desktopPolicyKey `
-ValueName 'ScreenSaveActive'
$link = @(Get-GPInheritance -Target $TargetOuDn -Domain $domainName -Server $DomainController).GpoLinks |
Where-Object DisplayName -eq $GpoName |
Select-Object -First 1
@@ -92,4 +108,5 @@ $linkEnabled = $link -and (
TargetOu = $TargetOuDn
LinkEnabled = [bool]$linkEnabled
DisableLockWorkstation = [int]$configuredValue.Value
ScreenSaverDisabled = [string]$screenSaverValue.Value -eq '0'
}
@@ -325,15 +325,19 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
response,
identity,
timeout.Token).ConfigureAwait(false);
if (identity.Role == InstitutionalRole.Administrative &&
string.Equals(
profile?.EmployeeNumber,
identity.NumericId,
StringComparison.Ordinal))
bool mayEnrichStaffProfile =
(identity.Role == InstitutionalRole.Administrative &&
string.Equals(
profile?.EmployeeNumber,
identity.NumericId,
StringComparison.Ordinal)) ||
(identity.Role == InstitutionalRole.Professor && profile is not null);
if (mayEnrichStaffProfile)
{
profile = await TryEnrichAdministrativeProfileAsync(
profile = await TryEnrichStaffProfileAsync(
client,
profile!,
identity.Role,
allowedHosts,
timeout.Token,
cancellationToken,
@@ -394,15 +398,16 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
return null;
}
private async Task<InstitutionalProfile> TryEnrichAdministrativeProfileAsync(
private async Task<InstitutionalProfile> TryEnrichStaffProfileAsync(
HttpClient client,
InstitutionalProfile verifiedProfile,
InstitutionalProfile baseProfile,
InstitutionalRole role,
HashSet<string> allowedHosts,
CancellationToken timeoutToken,
CancellationToken requestCancellationToken,
Stopwatch elapsed)
{
InstitutionalProfile profile = verifiedProfile;
InstitutionalProfile profile = baseProfile;
(string Path, Func<string, InstitutionalProfile?> Parser)[] pages =
[
(options.AdministrativePersonalProfilePath, SguProfileParser.ParseAdministrativePersonal),
@@ -423,7 +428,8 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
catch (OperationCanceledException) when (!requestCancellationToken.IsCancellationRequested)
{
logger.LogWarning(
"SGU administrative profile enrichment reached its total timeout after {ElapsedMilliseconds} ms; preserving fields already collected.",
"SGU optional staff profile enrichment for role {Role} reached its total timeout after {ElapsedMilliseconds} ms; preserving fields already collected.",
role,
elapsed.ElapsedMilliseconds);
break;
}
@@ -431,7 +437,8 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
{
logger.LogWarning(
exception,
"An optional SGU administrative profile page failed after {ElapsedMilliseconds} ms; preserving fields already collected.",
"An optional SGU staff profile page for role {Role} failed after {ElapsedMilliseconds} ms; preserving fields already collected.",
role,
elapsed.ElapsedMilliseconds);
}
}
@@ -23,6 +23,12 @@ public sealed class NtlmCredentialValidatorTests
"123456",
InstitutionalRole.Student);
private static readonly UserIdentity Professor = new(
"DO123456",
"DO",
"123456",
InstitutionalRole.Professor);
[Fact]
public async Task RedirectAndSuccessWithoutChallengeNeverAuthorizes()
{
@@ -192,6 +198,94 @@ public sealed class NtlmCredentialValidatorTests
Assert.Equal(3, handler.RequestPaths.Count);
}
[Fact]
public async Task ProfessorProfileIsEnrichedFromSharedStaffPages()
{
SequenceHandler handler = new(
Challenge(),
Response(HttpStatusCode.OK),
Response(
HttpStatusCode.OK,
"""
<span id="ctl00_lblNombreUsuario">MARÍA DEL CARMEN</span>
"""),
Response(
HttpStatusCode.OK,
"""
<input id="ctl00_contenedor_txtNombre" value="MARÍA DEL CARMEN" />
<input id="ctl00_contenedor_txtApaterno" value="DE LA FUENTE" />
<input id="ctl00_contenedor_txtAmaterno" value="O'CONNOR" />
"""),
Response(
HttpStatusCode.OK,
"""
<input id="ctl00_contenedor_txtCalle" value="CALLE DEL SOL" />
<input id="ctl00_contenedor_txtNoExt" value="15" />
<input id="ctl00_contenedor_txtCP" value="01000" />
<select id="ctl00_contenedor_ddlEstado">
<option selected="selected">CIUDAD DE MÉXICO</option>
</select>
<select id="ctl00_contenedor_ddlLocalidad">
<option selected="selected">ÁLVARO OBREGÓN</option>
</select>
<select id="ctl00_contenedor_ddlColonia">
<option selected="selected">FLORIDA</option>
</select>
"""));
NtlmCredentialValidator validator = CreateValidator(handler);
NtlmValidationResult result = await validator.ValidateAsync(
Professor,
"test-password",
TestContext.Current.CancellationToken);
Assert.Equal(NtlmValidationStatus.Valid, result.Status);
Assert.NotNull(result.Profile);
Assert.Equal("María del Carmen de la Fuente O'Connor", result.Profile.DisplayName);
Assert.Equal("María del Carmen", result.Profile.GivenName);
Assert.Equal("de la Fuente O'Connor", result.Profile.Surname);
Assert.Equal("Calle del Sol 15\r\nFlorida", result.Profile.StreetAddress);
Assert.Equal("Álvaro Obregón", result.Profile.City);
Assert.Equal("Ciudad de México", result.Profile.State);
Assert.Equal("01000", result.Profile.PostalCode);
Assert.Equal(
[
"/psulsa/",
"/psulsa/",
"/psulsa/menu.aspx",
"/psulsa/gadmon/capitalhumano/datos/personales.aspx",
"/psulsa/gadmon/capitalhumano/datos/ubicacion.aspx"
],
handler.RequestPaths);
}
[Fact]
public async Task MissingProfessorStaffPageOrElementIdsPreservesMenuProfile()
{
SequenceHandler handler = new(
Challenge(),
Response(HttpStatusCode.OK),
Response(
HttpStatusCode.OK,
"""
<span id="ctl00_lblNombreUsuario">MIGUEL DE CERVANTES</span>
"""),
Response(HttpStatusCode.OK, "<html><body>Unrecognized layout</body></html>"),
Response(HttpStatusCode.NotFound));
NtlmCredentialValidator validator = CreateValidator(handler);
NtlmValidationResult result = await validator.ValidateAsync(
Professor,
"test-password",
TestContext.Current.CancellationToken);
Assert.Equal(NtlmValidationStatus.Valid, result.Status);
Assert.NotNull(result.Profile);
Assert.Equal("Miguel de Cervantes", result.Profile.DisplayName);
Assert.Null(result.Profile.StreetAddress);
Assert.Equal(5, handler.RequestPaths.Count);
}
private static NtlmCredentialValidator CreateValidator(SequenceHandler handler)
{
BrokerOptions options = new()