Enrich professor profiles and harden client policy
This commit is contained in:
@@ -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
|
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
|
name, email, career, and postal address. The career becomes an AD title in the
|
||||||
form `Estudiante de ...`; faculty/department remains unset because the verified
|
form `Estudiante de ...`; faculty/department remains unset because the verified
|
||||||
page does not expose it. Professors retain the menu display-name fallback until
|
page does not expose it. Professors use the portal menu name as their safe base
|
||||||
a richer role-specific page is verified. Missing or changed presentation HTML
|
profile and then attempt the same personal and location modules as administrative
|
||||||
never blocks authentication or password synchronization after the lightweight
|
staff. A missing route, non-success response, timeout, or absent element ID merely
|
||||||
NTLM root has accepted the credential.
|
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:
|
Operational documentation:
|
||||||
|
|
||||||
|
|||||||
+16
-7
@@ -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`
|
`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
|
employee number, two additional GETs in the same in-memory session read the
|
||||||
structured name from `datos/personales.aspx` and the address from
|
structured name from `datos/personales.aspx` and the address from
|
||||||
`datos/ubicacion.aspx`. A supplemental failure preserves fields already
|
`datos/ubicacion.aspx`. Docentes keep the menu name as a base and attempt those
|
||||||
collected, and a profile timeout does not invalidate an already authenticated
|
same two shared staff modules without requiring them to exist. A supplemental
|
||||||
credential. NTLM may still require its normal
|
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
|
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
|
kept only in an in-memory per-request container and are never persisted or
|
||||||
returned to the client.
|
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
|
`postalCode` attributes. Administrative and student numbers must match the six
|
||||||
numeric digits of the requested identity before any role-specific metadata is
|
numeric digits of the requested identity before any role-specific metadata is
|
||||||
trusted. Administrative personal and location pages are accepted only after
|
trusted. Administrative personal and location pages are accepted only after
|
||||||
that incident-page match. Student faculty/department is deliberately left unset because the
|
that incident-page match. A docente's supplemental fields remain tied to the
|
||||||
verified page does not expose it. Missing metadata does not clear existing AD
|
fresh NTLM-authenticated portal session and are optional; the menu display name
|
||||||
values and never changes the password outcome.
|
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
|
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
|
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`,
|
The managed hierarchy is rooted at `OU=Usuarios-SGU`: `Docentes`, `Alumnos`,
|
||||||
and `Administrativos` are direct child OUs beneath it. The domain GPO
|
and `Administrativos` are direct child OUs beneath it. The domain GPO
|
||||||
`SGU - User session restrictions` is linked to this root and enables the
|
`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
|
Per-user synchronization is serialized inside the broker to prevent concurrent
|
||||||
create/reset races. Production deployments should run the broker as a gMSA with
|
create/reset races. Production deployments should run the broker as a gMSA with
|
||||||
|
|||||||
@@ -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
|
- El broker permite hasta **20 segundos** para el desafío NTLM ligero de
|
||||||
`/psulsa/`. El enriquecimiento usa el límite total independiente
|
`/psulsa/`. El enriquecimiento usa el límite total independiente
|
||||||
`ProfileTimeoutSeconds` —**90 segundos** en la configuración del laboratorio—
|
`ProfileTimeoutSeconds` —**90 segundos** en la configuración del laboratorio—
|
||||||
y conserva los campos que alcance a obtener si una página administrativa se
|
y conserva los campos que alcance a obtener si una página de personal se
|
||||||
retrasa o falla. El Credential Provider mantiene su propio límite de **90
|
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
|
segundos**: si SGU excede ese presupuesto, Windows continúa por el fallback
|
||||||
normal de AD o credenciales de dominio en caché.
|
normal de AD o credenciales de dominio en caché.
|
||||||
- El instalador configura recuperación del servicio con reinicios a los 5, 15
|
- El instalador configura recuperación del servicio con reinicios a los 5, 15
|
||||||
|
|||||||
@@ -82,17 +82,29 @@ siguiente arranque síncrono y el fondo se aplica al siguiente inicio de sesión
|
|||||||
del usuario SGU.
|
del usuario SGU.
|
||||||
|
|
||||||
En el controlador de dominio, aplicar una vez la directiva de usuario al árbol
|
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
|
```powershell
|
||||||
.\Set-SguDomainUserPolicies.ps1
|
.\Set-SguDomainUserPolicies.ps1
|
||||||
|
.\Set-SguDomainComputerPolicies.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
El script crea y vincula de forma idempotente el GPO
|
El script crea y vincula de forma idempotente el GPO
|
||||||
`SGU - User session restrictions` y configura `DisableLockWorkstation=1` para
|
`SGU - User session restrictions` y configura `DisableLockWorkstation=1` para
|
||||||
Administrativos, Alumnos y Docentes. La directiva elimina la acción manual de
|
Administrativos, Alumnos y Docentes; también deshabilita el protector de
|
||||||
bloqueo; no sustituye otras directivas independientes de protector de pantalla,
|
pantalla. El segundo crea `SGU - Windows client experience` y, antes del primer
|
||||||
inactividad o desconexión de RDP.
|
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
|
## 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-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-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-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/ne-credentialprovider-credential_provider_field_type>
|
||||||
- <https://learn.microsoft.com/en-us/windows/win32/api/credentialprovider/nf-credentialprovider-icredentialprovidercredential2-getusersid>
|
- <https://learn.microsoft.com/en-us/windows/win32/api/credentialprovider/nf-credentialprovider-icredentialprovidercredential2-getusersid>
|
||||||
|
|||||||
+8
-4
@@ -4,8 +4,8 @@ Validated lab inventory:
|
|||||||
|
|
||||||
- Domain controller/broker: Windows Server 2025 Standard,
|
- Domain controller/broker: Windows Server 2025 Standard,
|
||||||
`WIN-1AIQMMA1EPR.lci.lasalle.mx`, `192.168.50.10`.
|
`WIN-1AIQMMA1EPR.lci.lasalle.mx`, `192.168.50.10`.
|
||||||
- Client: Windows 10 Pro 22H2,
|
- Client: no active Windows client VM. The next enrollment target is a clean
|
||||||
`DESKTOP-U1I3BNN.lci.lasalle.mx`, `192.168.50.20`.
|
Windows 11 Enterprise VM placed in `OU=Laboratorio`.
|
||||||
- Domain: `lci.lasalle.mx` / `LCI`.
|
- Domain: `lci.lasalle.mx` / `LCI`.
|
||||||
- Private switch: `Laboratorio AD`.
|
- 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
|
the VM and select **Acceso institucional SGU**. Keep the built-in Windows
|
||||||
password provider available through **Other user**.
|
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
|
```powershell
|
||||||
.\Set-SguDomainUserPolicies.ps1
|
.\Set-SguDomainUserPolicies.ps1
|
||||||
|
.\Set-SguDomainComputerPolicies.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
It links `SGU - User session restrictions` to `OU=Usuarios-SGU` and prevents
|
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
|
Before testing through Hyper-V Enhanced Session/RDP, enable the dedicated lab
|
||||||
group and Windows PowerShell Remoting:
|
group and Windows PowerShell Remoting:
|
||||||
|
|||||||
+5
-1
@@ -43,12 +43,16 @@
|
|||||||
city/municipality, state, and postal code from known element IDs.
|
city/municipality, state, and postal code from known element IDs.
|
||||||
- Student CURP, birth date, sex, blood type, marital status, telephone, mobile,
|
- Student CURP, birth date, sex, blood type, marital status, telephone, mobile,
|
||||||
guardian, medical, financial, and academic-history values are ignored.
|
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
|
- Incident details, calendars, photographs, manager names, and manager positions
|
||||||
are deliberately ignored.
|
are deliberately ignored.
|
||||||
- The employee or student number must match the authenticated `AD` or `AL` key
|
- The employee or student number must match the authenticated `AD` or `AL` key
|
||||||
before role-specific metadata is synchronized. The two supplemental
|
before role-specific metadata is synchronized. The two supplemental
|
||||||
administrative pages are never requested unless the incident page supplied
|
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
|
- If SGU changes its HTML, authentication and exact-password synchronization
|
||||||
continue without enrichment; existing AD metadata is not erased.
|
continue without enrichment; existing AD metadata is not erased.
|
||||||
- Slow profile pages cannot change an accepted credential into a rejection. The
|
- Slow profile pages cannot change an accepted credential into a rejection. The
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass `
|
|||||||
|
|
||||||
El script realiza de forma idempotente lo siguiente:
|
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 RDP y conserva Network Level Authentication;
|
||||||
- habilita las reglas RDP solo para el perfil Domain;
|
- habilita las reglas RDP solo para el perfil Domain;
|
||||||
- autoriza el grupo SGU dedicado;
|
- 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**.
|
`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.
|
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:
|
Microsoft documenta este derecho en:
|
||||||
<https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-userrights#allowlogonthroughremotedesktop>
|
<https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-userrights#allowlogonthroughremotedesktop>
|
||||||
y PowerShell Remoting en:
|
y PowerShell Remoting en:
|
||||||
|
|||||||
@@ -20,6 +20,23 @@ $remoteDesktopUsersSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-32-
|
|||||||
$remoteDesktopUsersGroup = ($remoteDesktopUsersSid.Translate([Security.Principal.NTAccount]).Value -split '\\', 2)[1]
|
$remoteDesktopUsersGroup = ($remoteDesktopUsersSid.Translate([Security.Principal.NTAccount]).Value -split '\\', 2)[1]
|
||||||
|
|
||||||
if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Enable RDP and grant $RemoteDesktopPrincipal access")) {
|
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' `
|
Set-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' `
|
||||||
-Name fDenyTSConnections -Type DWord -Value 0
|
-Name fDenyTSConnections -Type DWord -Value 0
|
||||||
Set-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' `
|
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
|
TermService = (Get-Service TermService).Status
|
||||||
WinRM = (Get-Service WinRM).Status
|
WinRM = (Get-Service WinRM).Status
|
||||||
FirewallProfile = 'Domain'
|
FirewallProfile = 'Domain'
|
||||||
|
AlwaysOnPowerPolicyApplied = $true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ param(
|
|||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$policyKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System'
|
$policyKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System'
|
||||||
$policyValueName = 'DisableLockWorkstation'
|
$policyValueName = 'DisableLockWorkstation'
|
||||||
|
$desktopPolicyKey = 'HKCU\Software\Policies\Microsoft\Windows\Control Panel\Desktop'
|
||||||
|
|
||||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
||||||
@@ -71,6 +72,15 @@ if ($PSCmdlet.ShouldProcess($GpoName, 'Prevent SGU users from manually locking w
|
|||||||
-ValueName $policyValueName `
|
-ValueName $policyValueName `
|
||||||
-Type DWord `
|
-Type DWord `
|
||||||
-Value 1 | Out-Null
|
-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 `
|
$configuredValue = Get-GPRegistryValue `
|
||||||
@@ -79,6 +89,12 @@ $configuredValue = Get-GPRegistryValue `
|
|||||||
-Server $DomainController `
|
-Server $DomainController `
|
||||||
-Key $policyKey `
|
-Key $policyKey `
|
||||||
-ValueName $policyValueName
|
-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 |
|
$link = @(Get-GPInheritance -Target $TargetOuDn -Domain $domainName -Server $DomainController).GpoLinks |
|
||||||
Where-Object DisplayName -eq $GpoName |
|
Where-Object DisplayName -eq $GpoName |
|
||||||
Select-Object -First 1
|
Select-Object -First 1
|
||||||
@@ -92,4 +108,5 @@ $linkEnabled = $link -and (
|
|||||||
TargetOu = $TargetOuDn
|
TargetOu = $TargetOuDn
|
||||||
LinkEnabled = [bool]$linkEnabled
|
LinkEnabled = [bool]$linkEnabled
|
||||||
DisableLockWorkstation = [int]$configuredValue.Value
|
DisableLockWorkstation = [int]$configuredValue.Value
|
||||||
|
ScreenSaverDisabled = [string]$screenSaverValue.Value -eq '0'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -325,15 +325,19 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
|
|||||||
response,
|
response,
|
||||||
identity,
|
identity,
|
||||||
timeout.Token).ConfigureAwait(false);
|
timeout.Token).ConfigureAwait(false);
|
||||||
if (identity.Role == InstitutionalRole.Administrative &&
|
bool mayEnrichStaffProfile =
|
||||||
|
(identity.Role == InstitutionalRole.Administrative &&
|
||||||
string.Equals(
|
string.Equals(
|
||||||
profile?.EmployeeNumber,
|
profile?.EmployeeNumber,
|
||||||
identity.NumericId,
|
identity.NumericId,
|
||||||
StringComparison.Ordinal))
|
StringComparison.Ordinal)) ||
|
||||||
|
(identity.Role == InstitutionalRole.Professor && profile is not null);
|
||||||
|
if (mayEnrichStaffProfile)
|
||||||
{
|
{
|
||||||
profile = await TryEnrichAdministrativeProfileAsync(
|
profile = await TryEnrichStaffProfileAsync(
|
||||||
client,
|
client,
|
||||||
profile!,
|
profile!,
|
||||||
|
identity.Role,
|
||||||
allowedHosts,
|
allowedHosts,
|
||||||
timeout.Token,
|
timeout.Token,
|
||||||
cancellationToken,
|
cancellationToken,
|
||||||
@@ -394,15 +398,16 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<InstitutionalProfile> TryEnrichAdministrativeProfileAsync(
|
private async Task<InstitutionalProfile> TryEnrichStaffProfileAsync(
|
||||||
HttpClient client,
|
HttpClient client,
|
||||||
InstitutionalProfile verifiedProfile,
|
InstitutionalProfile baseProfile,
|
||||||
|
InstitutionalRole role,
|
||||||
HashSet<string> allowedHosts,
|
HashSet<string> allowedHosts,
|
||||||
CancellationToken timeoutToken,
|
CancellationToken timeoutToken,
|
||||||
CancellationToken requestCancellationToken,
|
CancellationToken requestCancellationToken,
|
||||||
Stopwatch elapsed)
|
Stopwatch elapsed)
|
||||||
{
|
{
|
||||||
InstitutionalProfile profile = verifiedProfile;
|
InstitutionalProfile profile = baseProfile;
|
||||||
(string Path, Func<string, InstitutionalProfile?> Parser)[] pages =
|
(string Path, Func<string, InstitutionalProfile?> Parser)[] pages =
|
||||||
[
|
[
|
||||||
(options.AdministrativePersonalProfilePath, SguProfileParser.ParseAdministrativePersonal),
|
(options.AdministrativePersonalProfilePath, SguProfileParser.ParseAdministrativePersonal),
|
||||||
@@ -423,7 +428,8 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
|
|||||||
catch (OperationCanceledException) when (!requestCancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (!requestCancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
logger.LogWarning(
|
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);
|
elapsed.ElapsedMilliseconds);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -431,7 +437,8 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
|
|||||||
{
|
{
|
||||||
logger.LogWarning(
|
logger.LogWarning(
|
||||||
exception,
|
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);
|
elapsed.ElapsedMilliseconds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ public sealed class NtlmCredentialValidatorTests
|
|||||||
"123456",
|
"123456",
|
||||||
InstitutionalRole.Student);
|
InstitutionalRole.Student);
|
||||||
|
|
||||||
|
private static readonly UserIdentity Professor = new(
|
||||||
|
"DO123456",
|
||||||
|
"DO",
|
||||||
|
"123456",
|
||||||
|
InstitutionalRole.Professor);
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RedirectAndSuccessWithoutChallengeNeverAuthorizes()
|
public async Task RedirectAndSuccessWithoutChallengeNeverAuthorizes()
|
||||||
{
|
{
|
||||||
@@ -192,6 +198,94 @@ public sealed class NtlmCredentialValidatorTests
|
|||||||
Assert.Equal(3, handler.RequestPaths.Count);
|
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)
|
private static NtlmCredentialValidator CreateValidator(SequenceHandler handler)
|
||||||
{
|
{
|
||||||
BrokerOptions options = new()
|
BrokerOptions options = new()
|
||||||
|
|||||||
Reference in New Issue
Block a user