Compare commits

...
10 Commits
36 changed files with 1342 additions and 173 deletions
+7 -4
View File
@@ -61,7 +61,7 @@ Operational documentation:
- [Self-hosted RustDesk server and managed Windows remote access](docs/rustdesk-operations.md)
- [Domain monitoring, usage reports, and six-month retention](docs/monitoring.md)
- [Laboratorio desktop wallpaper protection](docs/laboratorio-wallpaper-policy.md)
- [Azure user roaming by role with Azure Files and FSLogix](docs/user-roaming.md)
- [Optional Azure user roaming by role with Azure Files and FSLogix](docs/user-roaming.md)
- [Decision: do not persist password verifiers in Redis](docs/decisions/0001-no-password-cache.md)
| Prefix | Role | Default OU | Security group in the same OU |
@@ -109,9 +109,12 @@ Follow [docs/lab-runbook.md](docs/lab-runbook.md). Review
For a public Azure VM, use
[docs/azure-vpn-deployment.md](docs/azure-vpn-deployment.md). It supports an
optional Azure P2S gateway or direct enrollment restricted to explicit public
source CIDRs. P2S deployments also provision private Azure Files storage for
role-based roaming: `AL` redirects only Documents/Desktop, while `AD` and `DO`
can mount an FSLogix VHDX profile after the post-domain setup step.
source CIDRs. Every SGU role uses an ordinary local Windows profile by default:
Desktop and Documents stay under `C:\Users` and no profile share is mapped.
Azure Files/FSLogix roaming remains available only as an experimental,
explicit opt-in that requires acknowledgement of its login, sign-out, storage,
cache and recovery risks. See [docs/user-roaming.md](docs/user-roaming.md) for
the opt-in and rollback procedures.
Never disable the built-in Microsoft password Credential Provider. It is the
supported recovery path if a third-party provider fails to load.
Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 80 KiB

+22 -9
View File
@@ -62,6 +62,15 @@ plus six digits. It searches `BaseDn` by `sAMAccountName`, creates the user when
absent, moves it to the mapped OU when required, sets `userPrincipalName`, and
passes the submitted password directly to ADSI `SetPassword`.
Before an account becomes usable, the broker applies explicit deny ACEs for the
Active Directory `Change Password` extended right to SELF and Everyone. Users
beneath `OU=Usuarios-SGU` therefore cannot replace the synchronized password
from Windows, Ctrl+Alt+Delete, LDAP or another client. The broker's
administrative `SetPassword` operation uses the separate `Reset Password` right
and remains able to synchronize the current institutional credential after each
successful SGU authentication. Repeated synchronizations detect the existing
ACEs and do not duplicate them.
When the authenticated HTML exposes recognized stable IDs, the broker also
updates the applicable `displayName`, `givenName`, `sn`, `mail`, `title`,
`department`, `employeeType`, `employeeID`, `streetAddress`, `l`, `st`, and
@@ -78,16 +87,20 @@ 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.
## Azure user roaming
## Optional Azure user roaming
Azure P2S deployments can add a dedicated, private Azure Files account. Student
accounts (`AL`) keep a non-authoritative local profile and redirect only
Documents/Desktop. Administrative and professor accounts (`AD`/`DO`) receive an
FSLogix VHDX profile container through object-specific settings keyed to their
AD security-group SIDs. The storage account uses AD DS Kerberos with AES-256;
share-root ACLs use `CREATOR OWNER` so users cannot browse one another's data.
The bootstrap staging account is deliberately separate. See
[user-roaming.md](user-roaming.md) for rollout and recovery constraints.
The default architecture uses local Windows profiles for every role. Desktop,
Documents and the remainder of the profile live under `C:\Users`; enrollment
does not map a profile share and the domain GPO keeps FSLogix disabled.
An experimental opt-in can add a dedicated Azure Files account. Student
accounts (`AL`) redirect only Documents/Desktop, while administrative and
professor accounts (`AD`/`DO`) receive an FSLogix VHDX profile container through
object-specific settings. Enabling it is never implicit: both infrastructure
deployment and policy configuration require an explicit risk acknowledgement.
The storage account uses AD DS Kerberos with AES-256 and share-root ACLs use
`CREATOR OWNER`. See [user-roaming.md](user-roaming.md) for the observed
sign-out/cache failure modes, staged rollout requirements and rollback.
Every synchronization also enforces one idempotent security-group membership
from the classified institutional prefix: `AL` to `SGU-Alumnos`, `AD` to
+12 -9
View File
@@ -14,8 +14,8 @@ La plantilla crea:
- VPN Gateway opcional `VpnGw1AZ` con IKEv2/OpenVPN y autenticación por certificados;
- pool P2S `172.30.0.0/24`, autorizado en los firewalls SGU;
- DNS de la NIC del servidor apuntando a `10.77.0.4`;
- Azure Files privado para user roaming: `AL` conserva sólo Escritorio/Documentos
y `AD`/`DO` usa contenedores de perfil FSLogix.
- perfiles locales para todos los usuarios por omisión; Azure Files/FSLogix es
una característica opcional que debe habilitarse y aceptarse explícitamente.
Los prefijos privados deben ser RFC1918 y no deben solaparse con las redes usadas
por Hyper-V, el `Default Switch`, Wi-Fi o Ethernet locales. Los prefijos de
@@ -84,9 +84,10 @@ pública actual:
No utilice `0.0.0.0/0`. El despliegue de un VPN Gateway suele tardar bastante
más que la VM; omitirlo reduce tiempo y costo. El comando espera hasta que Azure
entregue un resultado final. User roaming se crea por omisión y requiere la ruta
privada del P2S; para un despliegue de enrolamiento público directo indique
`-DeployUserRoaming $false`.
entregue un resultado final. User roaming está desactivado por omisión. Sólo se
provisiona al indicar juntos `-DeployUserRoaming $true` y
`-AcknowledgeUserRoamingRisks`, después de revisar
[sus riesgos y recuperación](user-roaming.md).
## 3. Conectarse al servidor
@@ -141,10 +142,12 @@ El JSON debe indicar `NetworkConfigurationMode = PlatformManaged`, el pool P2S
en `TrustedClientNetworks` cuando exista VPN y los CIDR directos en
`PublicEnrollmentNetworks` cuando se hayan habilitado.
Después de terminar la promoción, una Azure Files al nuevo bosque y publique
las políticas por rol con `Enable-SguAzureUserRoaming.ps1`. Esta segunda fase
requiere Azure PowerShell y el módulo oficial AzFilesHybrid porque el SID y GUID
del bosque todavía no existen durante el despliegue Bicep. Consulte
Después de terminar la promoción no se necesita ninguna acción de perfiles: las
cuentas usarán archivos locales. Si se decide realizar un piloto explícito de
Azure Files, publique las políticas ejecutando
`Enable-SguAzureUserRoaming.ps1` con `-AcknowledgeUserRoamingRisks`. Esa fase opcional requiere Azure PowerShell y el
módulo oficial AzFilesHybrid porque el SID y GUID del bosque todavía no existen
durante el despliegue Bicep. Consulte
[user roaming por rol en Azure](user-roaming.md).
## 5. Enrolar cada VM Hyper-V
+4
View File
@@ -36,6 +36,10 @@ contrario, el contenedor de equipos configurado en AD. Los parámetros
explícitamente. Para otra cuenta, editar el usuario sugerido como
`DOMINIO\usuario` o `usuario@dominio`. No se guardan contraseñas.
El enrolamiento también oculta las entradas de Cambio rápido de usuario mediante
una política de equipo. Docentes, administrativos y alumnos conservan la opción
de cerrar sesión, pero no pueden dejar una sesión abierta para cambiar a otra.
Si la IPv4 proporcionada es pública, el mismo flujo configura automáticamente
la conectividad directa. Después de autenticar WinRM, el servidor crea o reutiliza
un certificado DoH, publica DNS cifrado en TCP 443 y devuelve únicamente su
+39
View File
@@ -0,0 +1,39 @@
# SGU Credential Provider 0.6.1
Continúa la serie publicada después de `0.6.0`.
## Cambios
- El configurador de roaming admite explícitamente Azure Files mediante endpoint
público con `-EndpointAccess Public`; el modo privado sigue siendo el valor
predeterminado.
- Se corrige la generación del nombre de la identidad de almacenamiento para
respetar el límite de 15 caracteres de una cuenta de equipo de Active
Directory.
- Las ACL de `profiles` y `redirected` aplican mínimo privilegio en la raíz: los
grupos autorizados pueden crear su directorio, pero no modificar ni eliminar
directorios ajenos; `CREATOR OWNER` conserva el control dentro del directorio
propio.
- Se documenta la secuencia de despliegue con firewall de Azure Storage en modo
`Deny`, autorizando finalmente sólo la red institucional `200.13.89.0/24`.
## Validación del despliegue
- Azure Files usa autenticación AD DS con Kerberos AES-256.
- `AF-02` y `AF-03` validaron ticket Kerberos, TCP 445 y lectura/escritura en los
shares `profiles` y `redirected`.
- FSLogix 26.08 (`3.26.826.17182`) quedó instalado y activo en ambos clientes.
- La GPO de FSLogix mantiene una base deshabilitada y se habilita sólo para los
SID de `SGU-Docentes` y `SGU-Administrativos`.
- La GPO de Alumnos redirige únicamente Escritorio y Documentos.
## Descargas
- `sgu-windows-client-bootstrap-0.6.1.zip`
- `sgu-server-bootstrap-0.6.1.zip`
- `sgu-linux-client-bootstrap-0.6.1.zip`
- `sgu-azure-infrastructure-0.6.1.zip`
- `SHA256SUMS-0.6.1.txt`
Verifique los hashes SHA-256 antes de ejecutar los paquetes. No se incluyen
credenciales, claves de almacenamiento ni artefactos temporales del despliegue.
+36
View File
@@ -0,0 +1,36 @@
# SGU Credential Provider 0.6.2
Continúa la serie publicada después de `0.6.1`.
## Cambios
- Se agrega despliegue automático e idempotente de FSLogix 26.08 mediante una
GPO de inicio de equipo ligada a `OU=Laboratorio`.
- El publicador verifica la firma Authenticode de Microsoft y el SHA-256 del
instalador antes de copiarlo al SYSVOL.
- Los equipos que estaban apagados reciben e instalan FSLogix en su siguiente
arranque; no se emplean tareas programadas.
- El script queda incluido en el paquete de servidor y la guía de roaming
documenta su publicación y comprobación.
## Validación del despliegue
- La GPO `SGU - FSLogix client deployment` está enlazada y sus versiones de AD
y SYSVOL coinciden.
- El instalador publicado conserva firma válida de Microsoft y SHA-256
`0BAF7FE8195571060F0361B351E58FE6CD422AE521A96BAB3E66D900544E8A30`.
- `AF-02` y `AF-03` aplicaron la GPO y ejecutan `frxsvc` con FSLogix
`3.26.826.17182`.
- Azure Storage permanece en `defaultAction=Deny` y sólo autoriza
`200.13.89.0/24`.
## Descargas
- `sgu-windows-client-bootstrap-0.6.2.zip`
- `sgu-server-bootstrap-0.6.2.zip`
- `sgu-linux-client-bootstrap-0.6.2.zip`
- `sgu-azure-infrastructure-0.6.2.zip`
- `SHA256SUMS-0.6.2.txt`
Verifique los hashes SHA-256 antes de ejecutar los paquetes. No se incluyen
credenciales, claves de almacenamiento ni artefactos temporales del despliegue.
+44
View File
@@ -0,0 +1,44 @@
# SGU Credential Provider 0.6.3
Corrige el primer inicio de sesión de perfiles completos `AD`/`DO` cuando Azure
Files se consume mediante su endpoint público.
## Cambios
- `Enable-SguAzureUserRoaming.ps1` selecciona automáticamente FSLogix Cloud
Cache para endpoints públicos y conserva el montaje directo para endpoints
privados.
- Cloud Cache realiza el I/O del perfil sobre el VHDX local y sincroniza con
Azure Files, evitando que la latencia WAN de SMB bloquee cada operación.
- La GPO elimina de forma efectiva `VHDLocations` al activar `CCDLocations`,
incluyendo equipos que habían aplicado la política directa anterior.
- Se impide el retorno silencioso a un perfil local o temporal cuando FSLogix
no puede registrar el contenedor.
- El cierre de sesión deja de esperar indefinidamente por un proveedor remoto:
se limita a 60 segundos y conserva la caché local si vence el plazo.
- La documentación agrega capacidad de disco, validación del registro efectivo
y rutas de diagnóstico para ambos modos.
## Validación del despliegue
- Se eliminó únicamente el perfil piloto fallido de `AD017045` en `AF-03`; la
cuenta de Active Directory se conservó.
- La GPO `SGU - AD-DO FSLogix profiles` quedó en Cloud Cache para los SID de
`SGU-Administrativos` y `SGU-Docentes`; `VHDLocations` ya no aparece en el
registro efectivo de `AF-03`.
- El nuevo inicio de `AD017045` creó la caché y el proxy locales, agregó la
redirección del perfil al VHDX y completó `LoadProfile` en 4.649 segundos, sin
error 258, perfil temporal ni retorno local.
- Pasaron 70 pruebas Pester, 41 pruebas de `SGU.AuthBroker.Core`, 16 de
`SGU.AuthBroker` y 6 del Credential Provider. La solución compiló sin errores.
## Descargas
- `sgu-windows-client-bootstrap-0.6.3.zip`
- `sgu-server-bootstrap-0.6.3.zip`
- `sgu-linux-client-bootstrap-0.6.3.zip`
- `sgu-azure-infrastructure-0.6.3.zip`
- `SHA256SUMS-0.6.3.txt`
Verifique los hashes SHA-256 antes de ejecutar los paquetes. No se incluyen
credenciales, claves de almacenamiento ni artefactos temporales del despliegue.
+27
View File
@@ -0,0 +1,27 @@
# SGU Credential Provider 0.6.4
Esta versión revierte el despliegue de perfiles móviles y deja los perfiles
locales como comportamiento predeterminado para todo el dominio SGU.
## Cambio operativo
- `AL`, `AD`, `DO` y cualquier otra cuenta usan un perfil local bajo `C:\Users`.
- Escritorio y Documentos vuelven a las rutas locales del usuario.
- No se configura `profilePath`, `homeDirectory` ni `homeDrive` en Active Directory.
- Las políticas deshabilitan FSLogix Profiles, ODFC, `frxsvc` y `frxccds`.
- El enlace de instalación automática de FSLogix queda deshabilitado.
- Los recursos Azure Files existentes no se destruyen y pueden conservarse para
recuperación o para un piloto futuro.
## Herramientas
`Disable-SguAzureUserRoaming.ps1` aplica el rollback idempotente en el dominio.
`Deploy-SguAzureInfrastructure.ps1` y Bicep usan
`DeployUserRoaming=false` por omisión. Tanto el aprovisionamiento como la
publicación de políticas exigen `AcknowledgeUserRoamingRisks` para volver a
activar la función opcional.
La documentación registra los tiempos prolongados de inicio/cierre, estados
pendientes de VHDX, caché obsoleta y recuperación mediante reinicio observados
durante el piloto. User roaming se considera experimental y nunca se habilita de
forma implícita.
+26
View File
@@ -0,0 +1,26 @@
# SGU Credential Provider 0.6.5
Esta versión actualiza los fondos de bienvenida de Windows y Linux sin agregar
dependencias ni trabajo de composición durante el inicio de sesión.
## Identidad visual
- El fondo base azul incorpora de forma permanente el logotipo blanco oficial
de Universidad La Salle México.
- El generador utiliza directamente ese JPEG: no abre, redimensiona ni compone
un segundo archivo de logotipo cada vez que inicia una sesión.
- Se conservan las dimensiones institucionales de `1600x1000` y el área central
disponible para el saludo personalizado.
## Texto de ubicación
- La sala aparece en una primera línea de mayor tamaño.
- La dependencia aparece debajo con una tipografía menor.
- La redacción neutral usa `Acceso a...`; cuando Active Directory contiene el
sexo enriquecido, usa `Estás ubicado...` o `Estás ubicada...`.
- La misma lógica está incluida en los enrolamientos de Windows y Linux.
- Si faltan el sexo o los metadatos de ubicación, permanecen los fallbacks
neutrales documentados y el inicio de sesión no se bloquea.
Los perfiles de usuario locales y el rollback de roaming introducido en 0.6.4
no cambian en esta versión.
+16
View File
@@ -0,0 +1,16 @@
# SGU Credential Provider 0.6.6
Esta versión ajusta la composición del fondo institucional introducido en
0.6.5.
- El logotipo blanco oficial de Universidad La Salle México queda integrado en
la esquina inferior derecha.
- Su tamaño se reduce ligeramente y se conserva un margen interior respecto de
ambos bordes para acercarlo visualmente al centro.
- El fondo original de `1600x1000`, el área central del saludo y la composición
dinámica de nombre y ubicación permanecen sin cambios.
- El generador sigue sin cargar ni componer un logotipo durante el inicio de
sesión.
No se modifican la autenticación, el enrolamiento, las políticas del dominio ni
el comportamiento de perfiles locales establecido en 0.6.4.
+17
View File
@@ -0,0 +1,17 @@
# SGU Credential Provider 0.6.7
Esta versión separa la imagen de la pantalla de bloqueo de la base utilizada
para generar el escritorio personalizado.
- `darkblue-lockscreen.jpg` contiene únicamente el fondo azul institucional,
sin logotipo ni datos personales.
- `darkblue.jpg` continúa como base del escritorio de cada sesión y conserva el
logotipo oficial integrado.
- El logo de escritorio se reduce nuevamente y se desplaza ligeramente hacia
arriba, sin interferir con el saludo central.
- El instalador, la GPO clásica, `PersonalizationCSP` y los paquetes de servidor
y cliente utilizan explícitamente el archivo correspondiente a cada función.
- Linux continúa usando sólo la base de escritorio personalizada.
No se modifican la autenticación, los perfiles locales ni las reglas de
enrolamiento.
+17
View File
@@ -0,0 +1,17 @@
# SGU Credential Provider 0.6.8
Esta versión declara al Auth Broker como la única autoridad de contraseñas para
todas las cuentas administradas bajo `OU=Usuarios-SGU`.
- Cada creación, movimiento o actualización de una cuenta agrega de forma
idempotente las denegaciones SELF y Everyone para el derecho extendido de
Active Directory `Change Password`.
- El usuario no puede cambiar la contraseña desde Windows, Ctrl+Alt+Delete,
LDAP ni herramientas equivalentes.
- El broker conserva el derecho administrativo separado `Reset Password` y
continúa sincronizando la contraseña institucional exacta mediante
`SetPassword` después de una autenticación SGU válida.
- El despliegue del broker recorre todas las cuentas existentes en
`Usuarios-SGU` y corrige aquellas que todavía permiten cambios directos.
No se modifica la contraseña institucional ni se almacena una copia adicional.
+3
View File
@@ -18,6 +18,9 @@ the same source list. RDP uses a separate allowlist. See
`Marshal.ZeroFreeGlobalAllocUnicode`; managed references are released as soon
as each request completes.
- The broker uses the exact received value for both NTLM and AD `SetPassword`.
- Managed `Usuarios-SGU` accounts deny the SELF and Everyone `Change Password`
extended right. Only an administrator or the broker through the separate
`Reset Password` right can replace the AD password.
- There is no HMAC password, pepper, local password cache, Supabase password, or
other derived credential in this Windows path.
- Neither application logs request bodies or passwords. Deployment configuration
+122 -86
View File
@@ -1,41 +1,70 @@
# User roaming por rol en Azure
# User roaming por rol en Azure (opcional)
El despliegue Azure puede crear almacenamiento privado y políticas distintas para
cada tipo de cuenta SGU:
> **Estado predeterminado:** desactivado. Todas las cuentas `AL`, `AD`, `DO` y
> cualquier otra cuenta de dominio usan un perfil local normal bajo `C:\Users`.
> Escritorio y Documentos permanecen en el disco del equipo; no se mapean shares
> de perfiles ni se monta un contenedor FSLogix.
La implementación se conserva para un posible piloto futuro, pero no forma parte
del enrolamiento normal. Activarla requiere dos decisiones explícitas: desplegar
la infraestructura con `-DeployUserRoaming $true
-AcknowledgeUserRoamingRisks` y después ejecutar el configurador con
`-AcknowledgeUserRoamingRisks`.
## Diseño opcional
Cuando se habilita expresamente, el comportamiento es:
| Rol | Datos conservados | Implementación |
| --- | --- | --- |
| `AL` | `Desktop` y `Documents` | Redirección de carpetas a Azure Files; el resto del perfil local no es autoritativo y no se copia al iniciar o cerrar sesión. |
| `AD` / `DO` | Perfil completo | Un contenedor VHDX único de FSLogix montado desde Azure Files. |
| `AL` | `Desktop` y `Documents` | Redirección de carpetas a Azure Files; el resto del perfil sigue local. |
| `AD` / `DO` | Perfil completo | FSLogix VHDX: montaje directo con endpoint privado o Cloud Cache con endpoint público. |
Linux conserva su solución nativa de directorios personales; no intenta montar
un perfil de Windows.
Linux conserva su directorio personal nativo y no monta un perfil de Windows.
## Arquitectura y seguridad
`infra/azure/main.bicep` puede crear una cuenta `StorageV2` dedicada con shares
`profiles` y `redirected`, soft delete de 14 días, private endpoint y zona
`privatelink.file.<sufijo-de-Azure>`. Su valor predeterminado
`deployUserRoaming=false` evita crear estos recursos. La cuenta de staging usada
para paquetes no se reutiliza automáticamente como almacenamiento de perfiles.
`infra/azure/main.bicep` crea, por omisión, una cuenta `StorageV2` dedicada con
dos recursos SMB (`profiles` y `redirected`), soft delete de 14 días, un private
endpoint y la zona `privatelink.file.<sufijo-de-Azure>` ligada a la VNet. El
endpoint público queda deshabilitado. La VNet entrega el DNS del controlador de
dominio a los clientes P2S para que el nombre de Azure Files resuelva a la IP
privada. La cuenta de staging usada para transferir paquetes no se reutiliza ni
se convierte en almacenamiento de perfiles.
Los clientes de un endpoint privado requieren P2S u otra ruta privada a la VNet.
Un endpoint público exige resolución DNS, salida TCP 445 y reglas de red de
Storage limitadas a redes institucionales. El modo `Auto` selecciona `Direct`
para endpoint privado y `CloudCache` para uno público. `CCDLocations` y
`VHDLocations` son mutuamente excluyentes.
Los clientes deben usar el P2S de Azure o disponer de otra ruta privada a la
VNet. El modo de enrolamiento público directo no puede usar estos recursos y el
script de despliegue exige `-DeployUserRoaming $false` cuando no se despliega el
gateway P2S.
Azure Files se integra con AD DS mediante Kerberos AES-256. Las ACL de la raíz
usan `CREATOR OWNER` para aislar los directorios de usuarios. La clave de la
cuenta se usa sólo en memoria durante la creación de ACL y no se guarda en GPO.
Azure Files se integra con el AD DS SGU, no con cuentas o claves guardadas en
cada cliente. El script configura AES-256 para Kerberos, una ACL de mínimo
privilegio con `CREATOR OWNER`, administradores y los grupos del rol, y permiso
SMB predeterminado para identidades autenticadas. La ACL NTFS mantiene aislado
el contenido de cada usuario. La clave de la cuenta se usa en memoria sólo para
crear las ACL iniciales y no se escribe en el dominio, en GPO ni en disco.
## Riesgos que deben aceptarse
## 1. Desplegar la infraestructura
El piloto de septiembre de 2026 confirmó que esta función puede aumentar de
forma considerable el tiempo de inicio y cierre de sesión. Antes de activarla se
deben aceptar y probar, como mínimo, estos escenarios:
El soporte está activado por omisión para despliegues P2S:
- latencia o indisponibilidad de Azure Files que mantiene `Welcome` o `Sign out`
durante varios minutos;
- VHDX remoto en estado abierto, pendiente de eliminación o de creación, con
fallos como `0x80070050`;
- caché o proxy local obsoleto de Cloud Cache y fallos de registro como
`0xE000000B`;
- recuperación que puede exigir detener por fuerza los servicios FSLogix y
reiniciar el equipo;
- datos todavía no sincronizados, caché local insuficiente o divergencia si se
fuerza una recuperación;
- dependencia de DNS, Kerberos, TCP 445, la red y el servicio de Storage tanto
para registrar como para desmontar el perfil.
No se debe desplegar a todos los usuarios sin un piloto de inicio, cierre,
desconexión, cuota, restauración y pérdida temporal del proveedor. Cloud Cache
reduce I/O WAN síncrono, pero no elimina la dependencia al cerrar sesión.
## 1. Desplegar la infraestructura opcional
El despliegue normal no incluye user roaming. Para crear deliberadamente sus
recursos:
```powershell
$azure = .\scripts\Deploy-SguAzureInfrastructure.ps1 `
@@ -43,97 +72,103 @@ $azure = .\scripts\Deploy-SguAzureInfrastructure.ps1 `
-ResourceGroupName 'rg-sgu-lab' `
-Location 'centralus' `
-AdministratorUsername 'azureadmin' `
-P2sRootCertificatePath $p2s.RootCertificatePath
$azure.UserRoamingStorageAccountName
$azure.FsLogixProfilesSharePath
$azure.RedirectedFoldersSharePath
$azure.UserRoamingSetupArguments
-P2sRootCertificatePath $p2s.RootCertificatePath `
-DeployUserRoaming $true `
-AcknowledgeUserRoamingRisks
```
Puede definirse un nombre globalmente único con
`-UserRoamingStorageAccountName`. Para omitir completamente el almacenamiento:
```powershell
-DeployUserRoaming $false
```
Puede definirse un nombre con `-UserRoamingStorageAccountName`. Si no se pasan
los dos parámetros de aceptación, la infraestructura de perfiles no se crea.
## 2. Unir Azure Files al dominio y publicar las GPO
Primero termine `Initialize-SguDomainController.ps1`. Después, en Windows
PowerShell 5.1 elevado dentro del DC:
1. Instale Azure PowerShell actual (`Az.Accounts` y `Az.Storage` 8.1 o posterior).
2. Descargue y extraiga el módulo oficial
[AzFilesHybrid](https://github.com/Azure-Samples/azure-files-samples/releases).
3. Ejecute el configurador incluido en el paquete de servidor:
PowerShell 5.1 elevado dentro del DC, instale Azure PowerShell y el módulo oficial
[AzFilesHybrid](https://github.com/Azure-Samples/azure-files-samples/releases),
y ejecute:
```powershell
$storageAccountName = '<valor UserRoamingStorageAccountName del despliegue>'
$parameters = @{
SubscriptionId = '00000000-0000-0000-0000-000000000000'
ResourceGroupName = 'rg-sgu-lab'
StorageAccountName = $storageAccountName
StorageAccountName = '<cuenta de perfiles>'
FsLogixProfilesShareName = 'profiles'
RedirectedFoldersShareName = 'redirected'
AzFilesHybridModulePath = 'C:\Admin\AzFilesHybrid'
UseDeviceAuthentication = $true
AcknowledgeUserRoamingRisks = $true
}
.\payload\scripts\Enable-SguAzureUserRoaming.ps1 @parameters
```
La operación es idempotente. Crea `OU=AzureFilesConfig`, une la cuenta de
almacenamiento al bosque y publica dos GPO:
Para una cuenta autorizada con endpoint público:
- `SGU - AL redirected folders`, ligada a `OU=Alumnos`;
- `SGU - AD-DO FSLogix profiles`, ligada a `OU=Laboratorio` y habilitada sólo
mediante configuración `ObjectSpecific` para los SID de `SGU-Docentes` y
`SGU-Administrativos`.
Por seguridad, el script no borra perfiles locales existentes de AD/DO. Antes
de un piloto real, migre o respalde su contenido y elimínelos manualmente. El
switch `-DeleteExistingStaffLocalProfiles` habilita la opción de FSLogix que los
elimina al aplicar el contenedor; úselo sólo después de respaldar y validar.
La identidad de Azure Files queda con contraseña sin expiración en la OU
dedicada para evitar que una cuenta de equipo sin agente pierda Kerberos a los
30 días. Si la política exige rotación, use periódicamente el procedimiento
oficial `Update-AzStorageAccountADObjectPassword` y no cambie sólo un lado.
## 3. Instalar FSLogix en las imágenes Windows
FSLogix no forma parte de Windows y su licencia debe validarse para las ediciones
y suscripciones usadas. Descargue `FSLogixAppsSetup.exe` directamente de
Microsoft y agréguelo a la imagen base, o páselo como quinto argumento al
enrolamiento Azure:
```bat
Start-SguAzureClientEnrollment.cmd 10.77.0.4 C:\SGU\vpn.zip C:\SGU\client.pfx C:\SGU\root.cer C:\SGU\FSLogixAppsSetup.exe
```powershell
$parameters.EndpointAccess = 'Public'
$parameters.StaffProfileStorageMode = 'Auto'
.\payload\scripts\Enable-SguAzureUserRoaming.ps1 @parameters
```
`Install-SguFsLogix.ps1` exige una firma Authenticode válida de Microsoft,
instala silenciosamente sin reiniciar y comprueba `frxsvc` y `frx.exe`. Si la
imagen ya contiene FSLogix, el instalador puede omitirse. Microsoft recomienda
mantener la versión más reciente; FSLogix no se actualiza mediante Windows
Update.
El script crea las GPO `SGU - AL redirected folders` y `SGU - AD-DO FSLogix
profiles`, y vuelve a habilitar el enlace de instalación de FSLogix si ya existe.
No borra perfiles locales existentes salvo que se proporcione deliberadamente
`-DeleteExistingStaffLocalProfiles`.
## 4. Validar un piloto
## 3. Instalar FSLogix sólo para un piloto
Después de `gpupdate /force` y reiniciar un cliente:
FSLogix no forma parte de Windows. Valide su licencia y descargue
`FSLogixAppsSetup.exe` directamente de Microsoft. Puede pasarse como quinto
argumento al enrolamiento Azure o publicarse con:
```powershell
.\payload\scripts\Publish-SguFsLogixClientDeployment.ps1
```
El publicador valida firma y SHA-256 y crea `SGU - FSLogix client deployment`.
El rollback descrito abajo deshabilita su enlace para que no se instale en más
equipos.
## 4. Desactivar y volver a perfiles locales
Ejecute desde Windows PowerShell 5.1 elevado en el controlador de dominio:
```powershell
.\payload\scripts\Disable-SguAzureUserRoaming.ps1
```
Es idempotente y realiza lo siguiente:
- fija `Profiles\Enabled=0` y `ODFC\Enabled=0`;
- deshabilita `frxsvc` y `frxccds` mediante GPO;
- elimina `CCDLocations`, `VHDLocations` y valores de contenedor específicos de
los grupos `SGU-Administrativos` y `SGU-Docentes`;
- restaura Escritorio y Documentos de alumnos a
`%USERPROFILE%\Desktop` y `%USERPROFILE%\Documents`;
- deshabilita el enlace de despliegue del cliente FSLogix;
- limpia `profilePath`, `homeDirectory` y `homeDrive` de los usuarios de las OU
SGU, salvo que se use `-PreserveLegacyUserMappings`.
Después aplique `gpupdate /force` y reinicie cada Windows administrado antes de
probar un nuevo inicio de sesión. El primer ingreso vuelve a crear o reutilizar
un perfil local. El script no borra automáticamente VHDX, shares de Azure ni
cachés existentes: conservarlos permite recuperación y evita destrucción de
datos. Su eliminación debe ser una decisión separada y posterior a la validación.
## 5. Validar un piloto futuro
Después de `gpupdate /force` y reiniciar un cliente de prueba:
```powershell
Resolve-DnsName "$($azure.UserRoamingStorageAccountName).file.core.windows.net"
Test-NetConnection "$($azure.UserRoamingStorageAccountName).file.core.windows.net" -Port 445
Get-Service frxsvc
Get-ItemProperty 'HKLM:\SOFTWARE\FSLogix\Profiles'
Get-ItemProperty 'HKLM:\SOFTWARE\FSLogix\Profiles\ObjectSpecific\<SID-del-grupo>'
& 'C:\Program Files\FSLogix\Apps\frx.exe' list-redirects
```
La resolución debe devolver la IP privada del endpoint. Inicie primero con un
`AL` nuevo y confirme las ubicaciones de Escritorio/Documentos; luego pruebe un
`AD` o `DO` sin perfil local previo y confirme la creación de un VHDX en
`profiles`. No active el borrado de perfiles locales ni despliegue a todos los
usuarios hasta probar inicio, cierre, desconexión de VPN, cuota y restauración.
Revise `%ProgramData%\FSLogix\Logs\Profile`, pruebe inicio y cierre varias veces,
simule indisponibilidad y valide restauración. No active el borrado de perfiles
locales ni amplíe el alcance antes de superar esas pruebas.
Referencias de Microsoft:
@@ -141,4 +176,5 @@ Referencias de Microsoft:
- [Permisos SMB para FSLogix](https://learn.microsoft.com/fslogix/how-to-configure-storage-permissions)
- [Contenedores de perfil FSLogix](https://learn.microsoft.com/fslogix/how-to-configure-profile-containers)
- [Configuración ObjectSpecific de FSLogix](https://learn.microsoft.com/fslogix/how-to-configure-object-specific-settings)
- [Cloud Cache de FSLogix](https://learn.microsoft.com/fslogix/concepts-fslogix-cloud-cache)
- [Redirección de carpetas](https://learn.microsoft.com/windows-server/storage/folder-redirection/deploy-folder-redirection)
+20 -8
View File
@@ -1,9 +1,15 @@
# Fondo de bienvenida personalizado
El enrolamiento instala un fondo base azul, las familias `Indivisa Text Sans` y
`Indivisa Text Serif`, y un generador local. La GPO de equipos
El enrolamiento instala dos fondos base azules. El escritorio personalizado usa
`assets/branding/darkblue.jpg`, que contiene el logotipo blanco oficial de
Universidad La Salle México, pequeño y elevado en el sector inferior derecho.
La pantalla de bloqueo usa `assets/branding/darkblue-lockscreen.jpg`, que sólo
contiene el fondo azul y nunca muestra el logo. También instala las familias
`Indivisa Text Sans` y `Indivisa Text Serif`, y un generador local. El logotipo
está horneado únicamente en la base de escritorio: el generador no carga,
redimensiona ni compone otro logo durante el inicio de sesión. La GPO de equipos
`SGU - Windows client experience` ejecuta el generador al abrir cada sesión y
mantiene el fondo base en la pantalla de bloqueo.
mantiene la base limpia independiente en la pantalla de bloqueo.
Windows no conoce todavía la identidad que se autenticará mientras muestra la
pantalla previa al inicio de sesión. Por ello, esa pantalla utiliza el fondo base
@@ -28,12 +34,18 @@ restaura el comando genérico de inicio.
El saludo usa `Bienvenido/ubicado` para `Male` y `Bienvenida/ubicada` para
`Female`. Cuando el enriquecimiento no produjo este dato, utiliza la redacción
neutral `Te damos la bienvenida` y `Ubicación:`. El texto secundario sigue estas
reglas:
neutral `Te damos la bienvenida` y `Acceso`. La ubicación se divide en dos
líneas: la sala ocupa la primera y la dependencia aparece debajo con un tamaño
menor. Sigue estas reglas:
1. Con `location` y OU: `Acceso al Aula Flexible del Centro de Experiencia Digital.`
2. Con sólo uno de los datos: muestra únicamente el dato disponible.
3. Sin ambos: el texto adaptado `Bienvenido/Bienvenida al Laboratorio...`; sin
1. Sin sexo: `Acceso a la Sala de Aplicaciones`, seguido por
`del Laboratorio de Cómputo de Ingeniería.`
2. Masculino: `Estás ubicado en el Aula Flexible`, seguido por
`del Centro de Experiencia Digital.`
3. Femenino: `Estás ubicada en la Sala de Redes`, seguido por
`del Laboratorio de Cómputo de Ingeniería.`
4. Con sólo uno de los datos: muestra únicamente el dato disponible.
5. Sin ambos: el texto adaptado `Bienvenido/Bienvenida al Laboratorio...`; sin
sexo disponible, la forma neutral `Acceso al Laboratorio de Cómputo de Ingeniería.`
La ausencia de AD, de un atributo o de una tipografía nunca bloquea la sesión.
+1 -1
View File
@@ -56,7 +56,7 @@ param publicEnrollmentSourceAddressPrefixes array = []
param administratorSourceAddressPrefix string = ''
@description('Deploy private Azure Files shares for SGU user roaming. Clients require P2S or another private route into the VNet.')
param deployUserRoaming bool = true
param deployUserRoaming bool = false
@description('Optional globally unique storage account name for roaming data. Leave empty to derive a stable name from the resource group.')
param userRoamingStorageAccountName string = ''
+15 -1
View File
@@ -201,6 +201,19 @@ if ($RemoteDesktopGroupDn) {
}
}
$managedUsersPasswordChangeCorrected = 0
if ($PSCmdlet.ShouldProcess($usersOuDn, 'Deny direct password changes for every managed SGU user')) {
$managedUsers = @(Get-ADUser -Filter * -SearchBase $usersOuDn -SearchScope Subtree `
-Properties CannotChangePassword -Server $LdapHost -ErrorAction Stop)
foreach ($managedUser in $managedUsers) {
if (-not $managedUser.CannotChangePassword) {
Set-ADAccountControl -Identity $managedUser.DistinguishedName `
-CannotChangePassword $true -Server $LdapHost -Confirm:$false
$managedUsersPasswordChangeCorrected++
}
}
}
foreach ($file in @('SGU.AuthBroker.exe', 'SGU.AuthBroker.dll', 'appsettings.json')) {
if (-not (Test-Path -LiteralPath (Join-Path $PublishPath $file))) {
throw "PublishPath is missing $file."
@@ -356,4 +369,5 @@ if ($PSCmdlet.ShouldProcess($installPath, 'Install the SGU Authentication Broker
}
Get-Service -Name $serviceName | Select-Object Name, Status, StartType,
@{ Name = 'EventLog'; Expression = { $brokerEventLogName } }
@{ Name = 'EventLog'; Expression = { $brokerEventLogName } },
@{ Name = 'ExistingUsersPasswordChangeDenied'; Expression = { $managedUsersPasswordChangeCorrected } }
+5 -1
View File
@@ -18,7 +18,8 @@ param(
[string]$VpnClientAddressPoolPrefix = '172.30.0.0/24',
[string[]]$PublicEnrollmentSourceAddressPrefixes = @(),
[string]$AdministratorSourceAddressPrefix = '',
[bool]$DeployUserRoaming = $true,
[bool]$DeployUserRoaming = $false,
[switch]$AcknowledgeUserRoamingRisks,
[ValidatePattern('^$|^[a-z0-9]{3,24}$')]
[string]$UserRoamingStorageAccountName = '',
[ValidatePattern('^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$')]
@@ -47,6 +48,9 @@ if (-not $AdministratorPassword) {
if ($DeployUserRoaming -and -not $DeployVpnGateway) {
throw 'Azure user roaming requires the P2S gateway deployed by this template so clients can reach the private Azure Files endpoint. Use -DeployUserRoaming $false with direct public enrollment.'
}
if ($DeployUserRoaming -and -not $AcknowledgeUserRoamingRisks) {
throw 'User roaming is disabled by default. To provision it, also pass -AcknowledgeUserRoamingRisks after reviewing the sign-in, sign-out, storage-availability, stale-cache, and recovery risks in docs/user-roaming.md.'
}
foreach ($shareName in @($FsLogixProfilesShareName, $RedirectedFoldersShareName)) {
if ($shareName.Contains('--')) {
throw "Azure Files share names cannot contain consecutive hyphens: $shareName"
+210
View File
@@ -0,0 +1,210 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]
param(
[string]$DomainController = $env:COMPUTERNAME,
[string]$StudentOuName = 'Alumnos',
[string]$ProfessorOuName = 'Docentes',
[string]$AdministrativeOuName = 'Administrativos',
[string]$LaboratoryOuName = 'Laboratorio',
[string]$StudentGroupName = 'SGU-Alumnos',
[string]$ProfessorGroupName = 'SGU-Docentes',
[string]$AdministrativeGroupName = 'SGU-Administrativos',
[string]$StudentGpoName = 'SGU - AL redirected folders',
[string]$StaffGpoName = 'SGU - AD-DO FSLogix profiles',
[string]$FsLogixDeploymentGpoName = 'SGU - FSLogix client deployment',
[switch]$PreserveLegacyUserMappings
)
$ErrorActionPreference = 'Stop'
function Assert-Administrator {
$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 5.1 session on the SGU domain controller.'
}
}
function Set-SguPolicyValue {
param(
[Parameter(Mandatory)][string]$GpoName,
[Parameter(Mandatory)][string]$DomainName,
[Parameter(Mandatory)][string]$Server,
[Parameter(Mandatory)][string]$Key,
[Parameter(Mandatory)][string]$ValueName,
[Parameter(Mandatory)][ValidateSet('DWord','String','ExpandString')][string]$Type,
[Parameter(Mandatory)]$Value
)
if ($PSCmdlet.ShouldProcess("$GpoName :: $Key\\$ValueName", "Set rollback value $Value")) {
Remove-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
-Key $Key -ValueName "**del.$ValueName" -ErrorAction SilentlyContinue | Out-Null
Set-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
-Key $Key -ValueName $ValueName -Type $Type -Value $Value | Out-Null
}
}
function Remove-SguPolicyValue {
param(
[Parameter(Mandatory)][string]$GpoName,
[Parameter(Mandatory)][string]$DomainName,
[Parameter(Mandatory)][string]$Server,
[Parameter(Mandatory)][string]$Key,
[Parameter(Mandatory)][string]$ValueName
)
if ($PSCmdlet.ShouldProcess("$GpoName :: $Key\\$ValueName", 'Remove roaming value')) {
Remove-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
-Key $Key -ValueName $ValueName -ErrorAction SilentlyContinue | Out-Null
Set-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
-Key $Key -ValueName "**del.$ValueName" -Type String -Value '' | Out-Null
}
}
Assert-Administrator
Import-Module ActiveDirectory -ErrorAction Stop
Import-Module GroupPolicy -ErrorAction Stop
$domain = Get-ADDomain -Server $DomainController
$domainName = $domain.DNSRoot
$domainDn = $domain.DistinguishedName
$laboratoryOuDn = "OU=$LaboratoryOuName,$domainDn"
$usersOuDn = "OU=Usuarios-SGU,$domainDn"
$studentOuDn = "OU=$StudentOuName,$usersOuDn"
$professorOuDn = "OU=$ProfessorOuName,$usersOuDn"
$administrativeOuDn = "OU=$AdministrativeOuName,$usersOuDn"
foreach ($ou in $laboratoryOuDn,$studentOuDn,$professorOuDn,$administrativeOuDn) {
Get-ADOrganizationalUnit -Identity $ou -Server $DomainController -ErrorAction Stop | Out-Null
}
$staffGpo = Get-GPO -Name $StaffGpoName -Domain $domainName -Server $DomainController `
-ErrorAction SilentlyContinue
if (-not $staffGpo -and $PSCmdlet.ShouldProcess($StaffGpoName, 'Create local-profile enforcement GPO')) {
$staffGpo = New-GPO -Name $StaffGpoName -Domain $domainName -Server $DomainController
}
if (-not $staffGpo) {
throw "The GPO '$StaffGpoName' does not exist and was not created."
}
$staffLink = @(Get-GPInheritance -Target $laboratoryOuDn -Domain $domainName `
-Server $DomainController).GpoLinks |
Where-Object DisplayName -eq $StaffGpoName | Select-Object -First 1
if (-not $staffLink -and $PSCmdlet.ShouldProcess($laboratoryOuDn, "Link '$StaffGpoName'")) {
New-GPLink -Name $StaffGpoName -Target $laboratoryOuDn -Domain $domainName `
-Server $DomainController -LinkEnabled Yes | Out-Null
}
elseif ($staffLink -and -not [bool]$staffLink.Enabled -and
$PSCmdlet.ShouldProcess($laboratoryOuDn, "Enable '$StaffGpoName' rollback link")) {
Set-GPLink -Name $StaffGpoName -Target $laboratoryOuDn -Domain $domainName `
-Server $DomainController -LinkEnabled Yes | Out-Null
}
$profilesRoot = 'HKLM\SOFTWARE\FSLogix\Profiles'
Set-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName -Server $DomainController `
-Key $profilesRoot -ValueName Enabled -Type DWord -Value 0
Set-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName -Server $DomainController `
-Key 'HKLM\SOFTWARE\FSLogix\ODFC' -ValueName Enabled -Type DWord -Value 0
foreach ($serviceName in 'frxsvc','frxccds') {
Set-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName -Server $DomainController `
-Key "HKLM\SYSTEM\CurrentControlSet\Services\$serviceName" `
-ValueName Start -Type DWord -Value 4
}
Remove-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName -Server $DomainController `
-Key 'HKLM\SOFTWARE\FSLogix\Apps' -ValueName VHDCompactDisk
$profileValueNames = @(
'CCDLocations','VHDLocations','CcdUnregisterTimeout','ClearCacheOnForcedUnregister',
'ClearCacheOnLogoff','HealthyProvidersRequiredForRegister',
'HealthyProvidersRequiredForUnregister','PreventLoginWithFailure',
'PreventLoginWithTempProfile','DeleteLocalProfileWhenVHDShouldApply',
'FlipFlopProfileDirectoryName','IsDynamic','LockedRetryCount','LockedRetryInterval',
'ProfileType','ReAttachIntervalSeconds','ReAttachRetryCount','SizeInMBs','VolumeType',
'VHDCompactDisk'
)
$roleGroups = @(
Get-ADGroup -Identity "CN=$ProfessorGroupName,$professorOuDn" -Server $DomainController
Get-ADGroup -Identity "CN=$AdministrativeGroupName,$administrativeOuDn" -Server $DomainController
)
foreach ($group in $roleGroups) {
$key = "$profilesRoot\ObjectSpecific\$($group.SID.Value)"
Set-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName -Server $DomainController `
-Key $key -ValueName Enabled -Type DWord -Value 0
foreach ($valueName in $profileValueNames) {
Remove-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName `
-Server $DomainController -Key $key -ValueName $valueName
}
}
Set-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName -Server $DomainController `
-Key 'HKLM\SOFTWARE\SGU\UserRoaming' -ValueName StaffProfileStorageMode `
-Type String -Value Disabled
$studentGpo = Get-GPO -Name $StudentGpoName -Domain $domainName -Server $DomainController `
-ErrorAction SilentlyContinue
if ($studentGpo) {
$studentLink = @(Get-GPInheritance -Target $studentOuDn -Domain $domainName `
-Server $DomainController).GpoLinks |
Where-Object DisplayName -eq $StudentGpoName | Select-Object -First 1
if ($studentLink -and -not [bool]$studentLink.Enabled -and
$PSCmdlet.ShouldProcess($studentOuDn, "Enable '$StudentGpoName' local-path rollback")) {
Set-GPLink -Name $StudentGpoName -Target $studentOuDn -Domain $domainName `
-Server $DomainController -LinkEnabled Yes | Out-Null
}
$shellFoldersKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders'
Set-SguPolicyValue -GpoName $StudentGpoName -DomainName $domainName -Server $DomainController `
-Key $shellFoldersKey -ValueName Desktop -Type ExpandString `
-Value '%USERPROFILE%\Desktop'
Set-SguPolicyValue -GpoName $StudentGpoName -DomainName $domainName -Server $DomainController `
-Key $shellFoldersKey -ValueName Personal -Type ExpandString `
-Value '%USERPROFILE%\Documents'
Remove-SguPolicyValue -GpoName $StudentGpoName -DomainName $domainName `
-Server $DomainController -Key 'HKCU\Software\Policies\Microsoft\Windows\NetCache' `
-ValueName DisableFRAdminPin
}
$deploymentGpo = Get-GPO -Name $FsLogixDeploymentGpoName -Domain $domainName `
-Server $DomainController -ErrorAction SilentlyContinue
if ($deploymentGpo) {
$deploymentLink = @(Get-GPInheritance -Target $laboratoryOuDn -Domain $domainName `
-Server $DomainController).GpoLinks |
Where-Object DisplayName -eq $FsLogixDeploymentGpoName | Select-Object -First 1
if ($deploymentLink -and [bool]$deploymentLink.Enabled -and
$PSCmdlet.ShouldProcess($laboratoryOuDn, "Disable '$FsLogixDeploymentGpoName'")) {
Set-GPLink -Name $FsLogixDeploymentGpoName -Target $laboratoryOuDn `
-Domain $domainName -Server $DomainController -LinkEnabled No | Out-Null
}
}
$clearedUsers = [Collections.Generic.List[string]]::new()
if (-not $PreserveLegacyUserMappings) {
foreach ($roleOu in $studentOuDn,$professorOuDn,$administrativeOuDn) {
foreach ($user in Get-ADUser -SearchBase $roleOu -SearchScope Subtree -Filter * `
-Server $DomainController -Properties profilePath,homeDirectory,homeDrive) {
if ($user.profilePath -or $user.homeDirectory -or $user.homeDrive) {
if ($PSCmdlet.ShouldProcess($user.SamAccountName, 'Clear AD profile and home-drive mappings')) {
Set-ADUser -Identity $user -Server $DomainController `
-Clear profilePath,homeDirectory,homeDrive
$clearedUsers.Add($user.SamAccountName)
}
}
}
}
}
$staffGpo = Get-GPO -Name $StaffGpoName -Domain $domainName -Server $DomainController
$studentGpo = Get-GPO -Name $StudentGpoName -Domain $domainName -Server $DomainController `
-ErrorAction SilentlyContinue
$deploymentLink = @(Get-GPInheritance -Target $laboratoryOuDn -Domain $domainName `
-Server $DomainController).GpoLinks |
Where-Object DisplayName -eq $FsLogixDeploymentGpoName | Select-Object -First 1
[pscustomobject]@{
Domain = $domainName
UserRoamingEnabled = $false
StaffGpoVersion = "$($staffGpo.Computer.DSVersion)/$($staffGpo.Computer.SysVolVersion)"
StudentGpoVersion = if ($studentGpo) {
"$($studentGpo.User.DSVersion)/$($studentGpo.User.SysVolVersion)"
} else { $null }
FsLogixDeploymentLinkEnabled = [bool]$deploymentLink.Enabled
StaffProfilesEnabled = 0
StudentDesktop = '%USERPROFILE%\Desktop'
StudentDocuments = '%USERPROFILE%\Documents'
ClearedAdUsers = @($clearedUsers)
RebootManagedWindowsClients = $true
}
+139 -19
View File
@@ -21,14 +21,23 @@ param(
[string]$AdministrativeGroupName = 'SGU-Administrativos',
[string]$StudentGpoName = 'SGU - AL redirected folders',
[string]$StaffGpoName = 'SGU - AD-DO FSLogix profiles',
[ValidateSet('Private', 'Public')]
[string]$EndpointAccess = 'Private',
[ValidateSet('Auto', 'Direct', 'CloudCache')]
[string]$StaffProfileStorageMode = 'Auto',
[ValidateRange(1024, 1048576)]
[int]$FsLogixProfileSizeMiB = 30000,
[string]$AzFilesHybridModulePath,
[switch]$UseDeviceAuthentication,
[switch]$DeleteExistingStaffLocalProfiles
[switch]$DeleteExistingStaffLocalProfiles,
[switch]$AcknowledgeUserRoamingRisks,
[string]$FsLogixDeploymentGpoName = 'SGU - FSLogix client deployment'
)
$ErrorActionPreference = 'Stop'
if (-not $AcknowledgeUserRoamingRisks -and -not $WhatIfPreference) {
throw 'User roaming is an optional high-risk feature. Pass -AcknowledgeUserRoamingRisks only after reviewing docs/user-roaming.md, testing sign-in and sign-out, and accepting the documented storage and recovery failure modes.'
}
function Assert-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
@@ -76,13 +85,13 @@ function Import-SguAzFilesHybrid {
function Get-SguStorageSamAccountName {
param([Parameter(Mandatory)][string]$StorageName)
if ($StorageName.Length -le 20) {
if ($StorageName.Length -le 15) {
return $StorageName
}
$sha256 = [Security.Cryptography.SHA256]::Create()
try {
$hash = $sha256.ComputeHash([Text.Encoding]::UTF8.GetBytes($StorageName))
$suffix = ([BitConverter]::ToString($hash) -replace '-', '').Substring(0, 15).ToLowerInvariant()
$suffix = ([BitConverter]::ToString($hash) -replace '-', '').Substring(0, 10).ToLowerInvariant()
return "sgufs$suffix"
}
finally {
@@ -154,11 +163,43 @@ function Set-SguGpoRegistryValue {
)
if ($PSCmdlet.ShouldProcess("$GpoName :: $Key\\$ValueName", "Set $Type policy value")) {
Remove-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
-Key $Key -ValueName "**del.$ValueName" -ErrorAction SilentlyContinue | Out-Null
Set-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
-Key $Key -ValueName $ValueName -Type $Type -Value $Value | Out-Null
}
}
function Remove-SguGpoRegistryValue {
param(
[Parameter(Mandatory)][string]$GpoName,
[Parameter(Mandatory)][string]$DomainName,
[Parameter(Mandatory)][string]$Server,
[Parameter(Mandatory)][string]$Key,
[Parameter(Mandatory)][string]$ValueName
)
if ($PSCmdlet.ShouldProcess("$GpoName :: $Key\$ValueName", 'Remove obsolete policy value')) {
Remove-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
-Key $Key -ValueName $ValueName -ErrorAction SilentlyContinue
}
}
function Set-SguGpoRegistryValueDeletion {
param(
[Parameter(Mandatory)][string]$GpoName,
[Parameter(Mandatory)][string]$DomainName,
[Parameter(Mandatory)][string]$Server,
[Parameter(Mandatory)][string]$Key,
[Parameter(Mandatory)][string]$ValueName
)
Remove-SguGpoRegistryValue -GpoName $GpoName -DomainName $DomainName -Server $Server `
-Key $Key -ValueName $ValueName
Set-SguGpoRegistryValue -GpoName $GpoName -DomainName $DomainName -Server $Server `
-Key $Key -ValueName "**del.$ValueName" -Type String -Value ''
}
function Get-SguUnusedDriveName {
$used = @(Get-PSDrive -PSProvider FileSystem | Select-Object -ExpandProperty Name)
foreach ($name in @('Z', 'Y', 'X', 'W', 'V')) {
@@ -322,15 +363,19 @@ if ($PSCmdlet.ShouldProcess($StorageAccountName, 'Grant authenticated AD identit
-DefaultSharePermission StorageFileDataSmbShareContributor
}
$privateAddresses = @(Resolve-DnsName -Name $fileEndpointHost -Type A -ErrorAction Stop |
$endpointAddresses = @(Resolve-DnsName -Name $fileEndpointHost -Type A -ErrorAction Stop |
Where-Object IPAddress | Select-Object -ExpandProperty IPAddress)
if ($privateAddresses.Count -eq 0 -or @($privateAddresses | Where-Object {
$_ -match '^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)'
}).Count -eq 0) {
if ($endpointAddresses.Count -eq 0) {
throw "$fileEndpointHost did not resolve to an IPv4 address."
}
$privateAddresses = @($endpointAddresses | Where-Object {
$_ -match '^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)'
})
if ($EndpointAccess -eq 'Private' -and $privateAddresses.Count -eq 0) {
throw "$fileEndpointHost did not resolve to a private endpoint. Verify the privatelink.file.core.windows.net VNet link and the DC Azure DNS forwarder."
}
if (-not (Test-NetConnection -ComputerName $fileEndpointHost -Port 445 -InformationLevel Quiet)) {
throw "The domain controller cannot reach $fileEndpointHost on TCP 445 through the private endpoint."
throw "The domain controller cannot reach $fileEndpointHost on TCP 445 using the selected $EndpointAccess endpoint."
}
$storageKey = @(Get-AzStorageAccountKey -ResourceGroupName $ResourceGroupName `
@@ -344,23 +389,30 @@ $storageCredential = [PSCredential]::new(
(ConvertTo-SecureString -String $storageKey -AsPlainText -Force))
$profilesSharePath = "\\$fileEndpointHost\$FsLogixProfilesShareName"
$redirectedFoldersSharePath = "\\$fileEndpointHost\$RedirectedFoldersShareName"
$resolvedStaffProfileStorageMode = if ($StaffProfileStorageMode -eq 'Auto') {
if ($EndpointAccess -eq 'Public') { 'CloudCache' } else { 'Direct' }
}
else {
$StaffProfileStorageMode
}
$perUserRootRights = [Security.AccessControl.FileSystemRights]::CreateDirectories -bor
[Security.AccessControl.FileSystemRights]::ListDirectory -bor
[Security.AccessControl.FileSystemRights]::ReadAttributes -bor
[Security.AccessControl.FileSystemRights]::ReadExtendedAttributes -bor
[Security.AccessControl.FileSystemRights]::ReadPermissions -bor
[Security.AccessControl.FileSystemRights]::Traverse -bor
[Security.AccessControl.FileSystemRights]::Synchronize
try {
if ($PSCmdlet.ShouldProcess($profilesSharePath, 'Apply isolated FSLogix root ACLs')) {
Set-SguAzureFileRootAcl -UncPath $profilesSharePath -Credential $storageCredential `
-DomainAdminsSid $domainAdminsSid `
-ContributorSids @($professorGroup.SID, $administrativeGroup.SID)
-ContributorSids @($professorGroup.SID, $administrativeGroup.SID) `
-ContributorRights $perUserRootRights
}
if ($PSCmdlet.ShouldProcess($redirectedFoldersSharePath, 'Apply isolated student-folder root ACLs')) {
$studentRootRights = [Security.AccessControl.FileSystemRights]::CreateDirectories -bor
[Security.AccessControl.FileSystemRights]::ListDirectory -bor
[Security.AccessControl.FileSystemRights]::ReadAttributes -bor
[Security.AccessControl.FileSystemRights]::ReadExtendedAttributes -bor
[Security.AccessControl.FileSystemRights]::ReadPermissions -bor
[Security.AccessControl.FileSystemRights]::Traverse -bor
[Security.AccessControl.FileSystemRights]::Synchronize
Set-SguAzureFileRootAcl -UncPath $redirectedFoldersSharePath -Credential $storageCredential `
-DomainAdminsSid $domainAdminsSid -ContributorSids @($studentGroup.SID) `
-ContributorRights $studentRootRights
-ContributorRights $perUserRootRights
}
}
finally {
@@ -386,6 +438,24 @@ $staffGpo = Ensure-SguGpoLink -Name $StaffGpoName -TargetOuDn $laboratoryOuDn `
$fsLogixRoot = 'HKLM\SOFTWARE\FSLogix\Profiles'
Set-SguGpoRegistryValue -GpoName $staffGpo.DisplayName -DomainName $domainName `
-Server $DomainController -Key $fsLogixRoot -ValueName 'Enabled' -Type DWord -Value 0
Set-SguGpoRegistryValue -GpoName $staffGpo.DisplayName -DomainName $domainName `
-Server $DomainController -Key 'HKLM\SYSTEM\CurrentControlSet\Services\frxsvc' `
-ValueName 'Start' -Type DWord -Value 2
Set-SguGpoRegistryValue -GpoName $staffGpo.DisplayName -DomainName $domainName `
-Server $DomainController -Key 'HKLM\SYSTEM\CurrentControlSet\Services\frxccds' `
-ValueName 'Start' -Type DWord -Value 2
$deploymentGpo = Get-GPO -Name $FsLogixDeploymentGpoName -Domain $domainName `
-Server $DomainController -ErrorAction SilentlyContinue
if ($deploymentGpo) {
$deploymentLink = @(Get-GPInheritance -Target $laboratoryOuDn -Domain $domainName `
-Server $DomainController).GpoLinks |
Where-Object DisplayName -eq $FsLogixDeploymentGpoName | Select-Object -First 1
if ($deploymentLink -and -not [bool]$deploymentLink.Enabled -and
$PSCmdlet.ShouldProcess($laboratoryOuDn, "Enable '$FsLogixDeploymentGpoName'")) {
Set-GPLink -Name $FsLogixDeploymentGpoName -Target $laboratoryOuDn `
-Domain $domainName -Server $DomainController -LinkEnabled Yes | Out-Null
}
}
$fsLogixValues = [ordered]@{
Enabled = @{ Type = 'DWord'; Value = 1 }
DeleteLocalProfileWhenVHDShouldApply = @{
@@ -400,12 +470,54 @@ $fsLogixValues = [ordered]@{
ReAttachIntervalSeconds = @{ Type = 'DWord'; Value = 15 }
ReAttachRetryCount = @{ Type = 'DWord'; Value = 3 }
SizeInMBs = @{ Type = 'DWord'; Value = $FsLogixProfileSizeMiB }
VHDLocations = @{ Type = 'String'; Value = $profilesSharePath }
VolumeType = @{ Type = 'String'; Value = 'VHDX' }
}
$cloudCacheOnlyValues = @(
'CCDLocations',
'CcdUnregisterTimeout',
'ClearCacheOnForcedUnregister',
'ClearCacheOnLogoff',
'HealthyProvidersRequiredForRegister',
'HealthyProvidersRequiredForUnregister',
'PreventLoginWithFailure',
'PreventLoginWithTempProfile'
)
if ($resolvedStaffProfileStorageMode -eq 'CloudCache') {
$fsLogixValues.CCDLocations = @{
Type = 'String'
Value = "type=smb,name=`"SGU Azure Files`",connectionString=$profilesSharePath"
}
$fsLogixValues.CcdUnregisterTimeout = @{ Type = 'DWord'; Value = 60 }
$fsLogixValues.ClearCacheOnForcedUnregister = @{ Type = 'DWord'; Value = 0 }
$fsLogixValues.ClearCacheOnLogoff = @{ Type = 'DWord'; Value = 0 }
$fsLogixValues.HealthyProvidersRequiredForRegister = @{ Type = 'DWord'; Value = 1 }
$fsLogixValues.HealthyProvidersRequiredForUnregister = @{ Type = 'DWord'; Value = 1 }
$fsLogixValues.PreventLoginWithFailure = @{ Type = 'DWord'; Value = 1 }
$fsLogixValues.PreventLoginWithTempProfile = @{ Type = 'DWord'; Value = 1 }
}
else {
$fsLogixValues.VHDLocations = @{ Type = 'String'; Value = $profilesSharePath }
}
Set-SguGpoRegistryValue -GpoName $staffGpo.DisplayName -DomainName $domainName `
-Server $DomainController -Key 'HKLM\SOFTWARE\FSLogix\Apps' `
-ValueName 'VHDCompactDisk' -Type DWord -Value 0
foreach ($staffGroup in @($professorGroup, $administrativeGroup)) {
$objectSpecificKey = "$fsLogixRoot\ObjectSpecific\$($staffGroup.SID.Value)"
Set-SguGpoRegistryValueDeletion -GpoName $staffGpo.DisplayName -DomainName $domainName `
-Server $DomainController -Key $objectSpecificKey -ValueName 'VHDCompactDisk'
if ($resolvedStaffProfileStorageMode -eq 'CloudCache') {
Set-SguGpoRegistryValueDeletion -GpoName $staffGpo.DisplayName -DomainName $domainName `
-Server $DomainController -Key $objectSpecificKey -ValueName 'VHDLocations'
}
else {
foreach ($obsoleteValue in $cloudCacheOnlyValues) {
Set-SguGpoRegistryValueDeletion -GpoName $staffGpo.DisplayName -DomainName $domainName `
-Server $DomainController -Key $objectSpecificKey -ValueName $obsoleteValue
}
}
foreach ($setting in $fsLogixValues.GetEnumerator()) {
Remove-SguGpoRegistryValue -GpoName $staffGpo.DisplayName -DomainName $domainName `
-Server $DomainController -Key $objectSpecificKey -ValueName "**del.$($setting.Key)"
Set-SguGpoRegistryValue -GpoName $staffGpo.DisplayName -DomainName $domainName `
-Server $DomainController -Key $objectSpecificKey -ValueName $setting.Key `
-Type $setting.Value.Type -Value $setting.Value.Value
@@ -415,6 +527,9 @@ foreach ($staffGroup in @($professorGroup, $administrativeGroup)) {
[pscustomobject]@{
StorageAccountName = $StorageAccountName
FileEndpoint = $fileEndpointHost
EndpointAccess = $EndpointAccess
StaffProfileStorageMode = $resolvedStaffProfileStorageMode
EndpointAddresses = $endpointAddresses
PrivateEndpointAddresses = $privateAddresses
DirectoryService = $directoryService
KerberosEncryption = 'AES256'
@@ -425,6 +540,11 @@ foreach ($staffGroup in @($professorGroup, $administrativeGroup)) {
StudentPolicy = $studentGpo.DisplayName
StaffPolicy = $staffGpo.DisplayName
StudentBehavior = 'Local non-authoritative profile; Documents and Desktop redirected without Offline Files pinning.'
StaffBehavior = 'FSLogix VHDX profile container for SGU-Docentes and SGU-Administrativos only.'
StaffBehavior = if ($resolvedStaffProfileStorageMode -eq 'CloudCache') {
'FSLogix Cloud Cache profile container for SGU-Docentes and SGU-Administrativos only; local I/O is synchronized to Azure Files.'
}
else {
'Direct FSLogix VHDX profile container for SGU-Docentes and SGU-Administrativos only.'
}
ExistingStaffLocalProfilesDeleted = [bool]$DeleteExistingStaffLocalProfiles
}
+13
View File
@@ -38,6 +38,7 @@ $interactiveLogonPolicyPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\P
$accountPictureSourcePath = Join-Path $PublishPath 'branding\user.png'
$accountPictureDirectory = Join-Path $env:ProgramData 'Microsoft\User Account Pictures'
$welcomeWallpaperSourcePath = Join-Path $PublishPath 'branding\darkblue.jpg'
$welcomeLockScreenSourcePath = Join-Path $PublishPath 'branding\darkblue-lockscreen.jpg'
$welcomeWallpaperScriptSourcePath = Join-Path $PublishPath 'branding\Set-SguWelcomeWallpaper.ps1'
$welcomeFontsSourcePath = Join-Path $PublishPath 'branding\fonts'
$welcomeWallpaperDirectory = Join-Path $env:ProgramData 'SGU\Branding'
@@ -128,6 +129,7 @@ function Install-DefaultAccountPicture {
function Install-WelcomeWallpaperAssets {
if (-not (Test-Path -LiteralPath $welcomeWallpaperSourcePath -PathType Leaf) -or
-not (Test-Path -LiteralPath $welcomeLockScreenSourcePath -PathType Leaf) -or
-not (Test-Path -LiteralPath $welcomeWallpaperScriptSourcePath -PathType Leaf)) {
return $false
}
@@ -135,6 +137,8 @@ function Install-WelcomeWallpaperAssets {
New-Item -ItemType Directory -Path $welcomeWallpaperDirectory -Force | Out-Null
Copy-Item -LiteralPath $welcomeWallpaperSourcePath `
-Destination (Join-Path $welcomeWallpaperDirectory 'darkblue.jpg') -Force
Copy-Item -LiteralPath $welcomeLockScreenSourcePath `
-Destination (Join-Path $welcomeWallpaperDirectory 'darkblue-lockscreen.jpg') -Force
Copy-Item -LiteralPath $welcomeWallpaperScriptSourcePath `
-Destination (Join-Path $welcomeWallpaperDirectory 'Set-SguWelcomeWallpaper.ps1') -Force
if (Test-Path -LiteralPath $welcomeFontsSourcePath -PathType Container) {
@@ -314,6 +318,11 @@ if ($PSCmdlet.ShouldProcess($installPath, 'Install and register the SGU Credenti
-Value 1 `
-PropertyType DWord `
-Force | Out-Null
New-ItemProperty -Path $interactiveLogonPolicyPath `
-Name HideFastUserSwitching `
-Value 1 `
-PropertyType DWord `
-Force | Out-Null
if (-not (Test-Path -LiteralPath $defaultProviderPolicyPath)) {
New-Item -Path $defaultProviderPolicyPath -Force | Out-Null
}
@@ -344,11 +353,15 @@ catch {
LastSignedInUserHidden = (Get-ItemPropertyValue `
-LiteralPath $interactiveLogonPolicyPath `
-Name DontDisplayLastUserName) -eq 1
FastUserSwitchingHidden = (Get-ItemPropertyValue `
-LiteralPath $interactiveLogonPolicyPath `
-Name HideFastUserSwitching) -eq 1
LocalUserEnumerationDisabled = (Get-ItemPropertyValue `
-LiteralPath $defaultProviderPolicyPath `
-Name EnumerateLocalUsers) -eq 0
SystemPasswordProviderPreserved = $true
WelcomeWallpaperAssetsInstalled =
(Test-Path -LiteralPath (Join-Path $welcomeWallpaperDirectory 'darkblue.jpg') -PathType Leaf) -and
(Test-Path -LiteralPath (Join-Path $welcomeWallpaperDirectory 'darkblue-lockscreen.jpg') -PathType Leaf) -and
(Test-Path -LiteralPath (Join-Path $welcomeWallpaperDirectory 'Set-SguWelcomeWallpaper.ps1') -PathType Leaf)
}
+6
View File
@@ -143,6 +143,8 @@ Copy-RequiredFile -Source (Join-Path $repositoryRoot 'assets\branding\lasalle-ma
-Destination (Join-Path $clientRoot 'payload\credential-provider\branding\user.png')
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'assets\branding\darkblue.jpg') `
-Destination (Join-Path $clientRoot 'payload\credential-provider\branding\darkblue.jpg')
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'assets\branding\darkblue-lockscreen.jpg') `
-Destination (Join-Path $clientRoot 'payload\credential-provider\branding\darkblue-lockscreen.jpg')
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Set-SguWelcomeWallpaper.ps1') `
-Destination (Join-Path $clientRoot 'payload\credential-provider\branding\Set-SguWelcomeWallpaper.ps1')
foreach ($fontName in $welcomeFontNames) {
@@ -196,11 +198,13 @@ Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguAzureServerBootstra
-Destination (Join-Path $serverRoot 'Start-SguAzureServerBootstrap.cmd')
$serverScripts = @(
'Deploy-AuthBroker.ps1',
'Disable-SguAzureUserRoaming.ps1',
'Enable-SguAzureUserRoaming.ps1',
'Enable-SguServerRemoteManagement.ps1',
'Get-SguUsageReport.ps1',
'Get-SguBrokerLog.ps1',
'Install-SguDomainMonitoring.ps1',
'Publish-SguFsLogixClientDeployment.ps1',
'Install-SguRustDeskClient.ps1',
'Install-SguRustDeskLinuxEnrollment.ps1',
'Install-SguRustDeskServer.ps1',
@@ -236,6 +240,8 @@ Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Set-SguWelcomeWallpaper.ps1'
-Destination (Join-Path $serverContentTarget 'welcome-wallpaper\Set-SguWelcomeWallpaper.ps1')
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'assets\branding\darkblue.jpg') `
-Destination (Join-Path $serverContentTarget 'welcome-wallpaper\darkblue.jpg')
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'assets\branding\darkblue-lockscreen.jpg') `
-Destination (Join-Path $serverContentTarget 'welcome-wallpaper\darkblue-lockscreen.jpg')
foreach ($fontName in $welcomeFontNames) {
Copy-RequiredFile -Source (Join-Path $repositoryRoot "assets\branding\fonts\$fontName") `
-Destination (Join-Path $serverContentTarget "welcome-wallpaper\fonts\$fontName")
@@ -0,0 +1,140 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]
param(
[string]$GpoName = 'SGU - FSLogix client deployment',
[string]$LaboratoryOuDn,
[uri]$InstallerZipUri = 'https://download.microsoft.com/download/ae6d2014-e692-45fa-a88b-ee552567cdc1/FSLogix_26.08.zip',
[string]$ExpectedInstallerSha256 = '0BAF7FE8195571060F0361B351E58FE6CD422AE521A96BAB3E66D900544E8A30',
[string]$CacheRoot = "$env:ProgramData\SGU\FSLogixDeployment\26.08"
)
$ErrorActionPreference = 'Stop'
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run the FSLogix GPO deployment from an elevated Windows PowerShell session.'
}
Import-Module ActiveDirectory -ErrorAction Stop
Import-Module GroupPolicy -ErrorAction Stop
$domain = Get-ADDomain
$domainName = $domain.DNSRoot
$domainDn = $domain.DistinguishedName
if (-not $LaboratoryOuDn) { $LaboratoryOuDn = "OU=Laboratorio,$domainDn" }
Get-ADOrganizationalUnit -Identity $LaboratoryOuDn -ErrorAction Stop | Out-Null
$zipPath = Join-Path $CacheRoot 'FSLogix_26.08.zip'
$extractRoot = Join-Path $CacheRoot 'expanded'
$installerPath = Join-Path $extractRoot 'x64\Release\FSLogixAppsSetup.exe'
if (-not (Test-Path -LiteralPath $installerPath -PathType Leaf)) {
if (-not $PSCmdlet.ShouldProcess($InstallerZipUri.AbsoluteUri, "Download official FSLogix package to $zipPath")) { return }
New-Item -ItemType Directory -Path $CacheRoot,$extractRoot -Force | Out-Null
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest -Uri $InstallerZipUri -OutFile $zipPath -UseBasicParsing
Expand-Archive -LiteralPath $zipPath -DestinationPath $extractRoot -Force
}
if (-not (Test-Path -LiteralPath $installerPath -PathType Leaf)) {
throw "The FSLogix x64 installer is missing after extraction: $installerPath"
}
$signature = Get-AuthenticodeSignature -LiteralPath $installerPath
if ($signature.Status -ne [Management.Automation.SignatureStatus]::Valid -or
-not $signature.SignerCertificate -or
$signature.SignerCertificate.Subject -notmatch '(^|,\s*)CN=Microsoft Corporation(,|$)') {
throw 'FSLogixAppsSetup.exe does not have a valid Microsoft Corporation signature.'
}
$installerHash = (Get-FileHash -LiteralPath $installerPath -Algorithm SHA256).Hash
if ($installerHash -ne $ExpectedInstallerSha256) {
throw "FSLogixAppsSetup.exe SHA-256 mismatch. Expected $ExpectedInstallerSha256; received $installerHash."
}
$gpo = Get-GPO -Name $GpoName -Domain $domainName -ErrorAction SilentlyContinue
if (-not $gpo) { $gpo = New-GPO -Name $GpoName -Domain $domainName }
$links = @(Get-GPInheritance -Target $LaboratoryOuDn -Domain $domainName).GpoLinks
if (-not ($links | Where-Object DisplayName -eq $GpoName)) {
New-GPLink -Name $GpoName -Target $LaboratoryOuDn -Domain $domainName -LinkEnabled Yes | Out-Null
}
# Set one ordinary machine value through the supported cmdlet. Besides providing
# a deployment marker, this initializes the computer half of the GPO and its
# Registry client-side extension before the Scripts extension is added below.
Set-GPRegistryValue -Name $GpoName -Domain $domainName `
-Key 'HKLM\SOFTWARE\SGU\FSLogixDeployment' -ValueName 'Version' `
-Type String -Value '26.08-3.26.826.17182' | Out-Null
$gpo = Get-GPO -Name $GpoName -Domain $domainName
$guid = $gpo.Id.ToString('B').ToUpperInvariant()
$gpoRoot = "\\$domainName\SYSVOL\$domainName\Policies\$guid"
$scriptsRoot = Join-Path $gpoRoot 'Machine\Scripts'
$startupRoot = Join-Path $scriptsRoot 'Startup'
New-Item -ItemType Directory -Path $startupRoot -Force | Out-Null
Copy-Item -LiteralPath $installerPath -Destination (Join-Path $startupRoot 'FSLogixAppsSetup.exe') -Force
$startupPowerShell = @'
$ErrorActionPreference = 'Stop'
$appsRoot = Join-Path $env:ProgramFiles 'FSLogix\Apps'
$frx = Join-Path $appsRoot 'frx.exe'
if ((Get-Service frxsvc -ErrorAction SilentlyContinue) -and (Test-Path -LiteralPath $frx)) { exit 0 }
$source = Join-Path $PSScriptRoot 'FSLogixAppsSetup.exe'
$signature = Get-AuthenticodeSignature -LiteralPath $source
if ($signature.Status -ne 'Valid' -or -not $signature.SignerCertificate -or
$signature.SignerCertificate.Subject -notmatch '(^|,\s*)CN=Microsoft Corporation(,|$)') { exit 10 }
$targetRoot = Join-Path $env:ProgramData 'SGU\FSLogix'
New-Item -ItemType Directory -Path $targetRoot -Force | Out-Null
$target = Join-Path $targetRoot 'FSLogixAppsSetup.exe'
Copy-Item -LiteralPath $source -Destination $target -Force
$log = Join-Path $targetRoot 'install.log'
$process = Start-Process -FilePath $target -ArgumentList @('/install','/quiet','/norestart','/log',"`"$log`"") -Wait -PassThru
if ($process.ExitCode -notin @(0,1641,3010)) { exit $process.ExitCode }
if (-not (Get-Service frxsvc -ErrorAction SilentlyContinue) -or -not (Test-Path -LiteralPath $frx)) { exit 11 }
exit 0
'@
[IO.File]::WriteAllText(
(Join-Path $startupRoot 'Install-SguFsLogix-Startup.ps1'),
$startupPowerShell,
[Text.UTF8Encoding]::new($true))
$startupCommand = '@echo off' + [Environment]::NewLine +
'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "%~dp0Install-SguFsLogix-Startup.ps1"' + [Environment]::NewLine +
'exit /b %ERRORLEVEL%' + [Environment]::NewLine
[IO.File]::WriteAllText(
(Join-Path $startupRoot 'Install-SguFsLogix.cmd'),
$startupCommand,
[Text.Encoding]::ASCII)
$scriptsIni = "[Startup]`r`n0CmdLine=Install-SguFsLogix.cmd`r`n0Parameters=`r`n"
[IO.File]::WriteAllText(
(Join-Path $scriptsRoot 'scripts.ini'),
$scriptsIni,
[Text.Encoding]::Unicode)
$policyDn = "CN=$guid,CN=Policies,CN=System,$domainDn"
$policy = Get-ADObject -Identity $policyDn -Properties versionNumber,gPCMachineExtensionNames
$scriptExtension = '[{42B5FAAE-6536-11D2-AE5A-0000F87571E3}{40B6664F-4972-11D1-A7CA-0000F87571E3}]'
$extensions = @([regex]::Matches([string]$policy.gPCMachineExtensionNames, '\[\{[0-9A-Fa-f-]{36}\}\{[0-9A-Fa-f-]{36}\}\]') |
ForEach-Object Value)
if ($extensions -notcontains $scriptExtension) { $extensions += $scriptExtension }
$extensionValue = ($extensions | Sort-Object { $_.Substring(1, 38) }) -join ''
$currentVersion = [int64]$policy.versionNumber
$machineVersion = $currentVersion -band 0xFFFF
if ($machineVersion -ge 65535) { throw 'The computer GPO version cannot be incremented further.' }
$newVersion = ($currentVersion -band 0xFFFF0000) -bor ($machineVersion + 1)
$gptIniPath = Join-Path $gpoRoot 'gpt.ini'
$gptLines = @(Get-Content -LiteralPath $gptIniPath)
$versionFound = $false
$gptLines = @($gptLines | ForEach-Object {
if ($_ -match '^Version=') { $versionFound = $true; "Version=$newVersion" } else { $_ }
})
if (-not $versionFound) { $gptLines += "Version=$newVersion" }
[IO.File]::WriteAllLines($gptIniPath, $gptLines, [Text.Encoding]::ASCII)
Set-ADObject -Identity $policy -Replace @{
gPCMachineExtensionNames = $extensionValue
versionNumber = [int]$newVersion
}
$verifiedGpo = Get-GPO -Name $GpoName -Domain $domainName
[pscustomobject]@{
GpoName = $verifiedGpo.DisplayName
GpoId = $verifiedGpo.Id
LaboratoryOuDn = $LaboratoryOuDn
MachineVersion = $verifiedGpo.Computer.DSVersion
InstallerVersion = '3.26.826.17182'
InstallerSha256 = $installerHash
StartupScript = Join-Path $startupRoot 'Install-SguFsLogix.cmd'
}
+7 -5
View File
@@ -5,7 +5,8 @@ param(
[string]$DomainController = $env:COMPUTERNAME,
[string]$EventCollectorFqdn,
[string]$WelcomeWallpaperScriptPath = 'C:\ProgramData\SGU\Branding\Set-SguWelcomeWallpaper.ps1',
[string]$WelcomeWallpaperBasePath = 'C:\ProgramData\SGU\Branding\darkblue.jpg'
[string]$WelcomeWallpaperBasePath = 'C:\ProgramData\SGU\Branding\darkblue.jpg',
[string]$WelcomeLockScreenBasePath = 'C:\ProgramData\SGU\Branding\darkblue-lockscreen.jpg'
)
$ErrorActionPreference = 'Stop'
@@ -99,6 +100,7 @@ $policies = @(
@{ Key = $credentialProviderPolicyKey; Name = 'DefaultCredentialProvider'; Type = 'String'; Value = $providerClassId },
@{ Key = $credentialProviderPolicyKey; Name = 'EnumerateLocalUsers'; Type = 'DWord'; Value = 0 },
@{ Key = $interactiveLogonPolicyKey; Name = 'DontDisplayLastUserName'; Type = 'DWord'; Value = 1 },
@{ Key = $interactiveLogonPolicyKey; Name = 'HideFastUserSwitching'; Type = 'DWord'; Value = 1 },
# Use Windows' native default account image for named user tiles. LogonUI
# retains ownership of the anonymous Other user tile and its circular mask.
@@ -112,14 +114,14 @@ $policies = @(
# The machine GPO remains the authority for every interactive session. The
# local payload lets the first desktop render without depending on SMB.
@{ Key = $runPolicyKey; Name = 'SGUWelcomeWallpaper'; Type = 'String'; Value = $welcomeWallpaperCommand },
@{ Key = $personalizationPolicyKey; Name = 'LockScreenImage'; Type = 'String'; Value = $WelcomeWallpaperBasePath },
@{ Key = $personalizationPolicyKey; Name = 'LockScreenImage'; Type = 'String'; Value = $WelcomeLockScreenBasePath },
@{ Key = $personalizationPolicyKey; Name = 'NoChangingLockScreen'; Type = 'DWord'; Value = 1 },
# Windows 11 Pro can ignore the legacy lock-screen policy even though it is
# present in the registry. PersonalizationCSP provides the same local image
# to Pro while remaining harmless on Enterprise and LTSC editions.
@{ Key = $personalizationCspKey; Name = 'LockScreenImagePath'; Type = 'String'; Value = $WelcomeWallpaperBasePath },
@{ Key = $personalizationCspKey; Name = 'LockScreenImageUrl'; Type = 'String'; Value = $WelcomeWallpaperBasePath },
@{ Key = $personalizationCspKey; Name = 'LockScreenImagePath'; Type = 'String'; Value = $WelcomeLockScreenBasePath },
@{ Key = $personalizationCspKey; Name = 'LockScreenImageUrl'; Type = 'String'; Value = $WelcomeLockScreenBasePath },
@{ Key = $personalizationCspKey; Name = 'LockScreenImageStatus'; Type = 'DWord'; Value = 1 },
@{ Key = $cloudContentPolicyKey; Name = 'DisableWindowsSpotlightFeatures'; Type = 'DWord'; Value = 1 },
@{ Key = $cloudContentPolicyKey; Name = 'DisableWindowsSpotlightOnLockScreen'; Type = 'DWord'; Value = 1 }
@@ -175,6 +177,6 @@ $linkEnabled = $link -and (
PolicyCount = $configuredPolicies.Count
EventCollector = $EventCollectorFqdn
WelcomeWallpaperCommand = $welcomeWallpaperCommand
LockScreenImage = $WelcomeWallpaperBasePath
LockScreenImage = $WelcomeLockScreenBasePath
Policies = [pscustomobject]$configuredPolicies
}
+37 -9
View File
@@ -189,7 +189,7 @@ function Get-WelcomeLocationText {
$located = switch ($Gender) {
'Male' { 'Est{0}s ubicado en' -f [char]0x00E1 }
'Female' { 'Est{0}s ubicada en' -f [char]0x00E1 }
default { 'Ubicaci{0}n:' -f [char]0x00F3 }
default { $null }
}
$engineeringLab = switch ($Gender) {
'Male' { 'Bienvenido al Laboratorio de C{0}mputo de Ingenier{1}a.' -f [char]0x00F3,[char]0x00ED }
@@ -202,7 +202,10 @@ function Get-WelcomeLocationText {
if ($Room -and $OuName) {
$roomArticle = Get-SpanishArticle -Value $Room
$ouArticle = Get-SpanishArticle -Value $OuName
$roomPhrase = if ($roomArticle -eq 'el') {
$roomPhrase = if ($Gender) {
if ($roomArticle) { "$roomArticle $Room" } else { $Room }
}
elseif ($roomArticle -eq 'el') {
"al $Room"
}
elseif ($roomArticle) {
@@ -212,17 +215,26 @@ function Get-WelcomeLocationText {
"a $Room"
}
$ouPhrase = if ($ouArticle -eq 'el') { "del $OuName" } elseif ($ouArticle) { "de $ouArticle $OuName" } else { "de $OuName" }
return "Acceso $roomPhrase $ouPhrase."
$primary = if ($Gender) { "$located $roomPhrase" } else { "Acceso $roomPhrase" }
return "$primary`n$ouPhrase."
}
if ($Room) {
$article = Get-SpanishArticle -Value $Room
$phrase = if ($article) { "$article $Room" } else { $Room }
return "$located $phrase."
if ($Gender) {
$phrase = if ($article) { "$article $Room" } else { $Room }
return "$located $phrase."
}
$destination = if ($article -eq 'el') { "al $Room" } elseif ($article) { "a $article $Room" } else { "a $Room" }
return "Acceso $destination."
}
if ($OuName) {
$article = Get-SpanishArticle -Value $OuName
$phrase = if ($article) { "$article $OuName" } else { $OuName }
return "$located $phrase."
if ($Gender) {
$phrase = if ($article) { "$article $OuName" } else { $OuName }
return "$located $phrase."
}
$destination = if ($article -eq 'el') { "al $OuName" } elseif ($article) { "a $article $OuName" } else { "a $OuName" }
return "Acceso $destination."
}
return $engineeringLab
}
@@ -350,6 +362,9 @@ if (-not $PSBoundParameters.ContainsKey('OrganizationalUnit')) {
}
}
$locationText = Get-WelcomeLocationText -Room $Location -OuName $OrganizationalUnit -Gender $Gender
$locationLines = @($locationText -split '\r?\n', 2)
$locationPrimaryText = $locationLines[0]
$locationSecondaryText = if ($locationLines.Count -gt 1) { $locationLines[1] } else { $null }
if (-not $CanvasWidth -or -not $CanvasHeight) {
try {
@@ -436,6 +451,8 @@ try {
-PreferredStyle ([Drawing.FontStyle]::Bold -bor [Drawing.FontStyle]::Italic)
$locationFont = New-WelcomeFont -Family $sansFamily -Size ([single](27 * $scale)) `
-PreferredStyle ([Drawing.FontStyle]::Regular)
$locationSecondaryFont = New-WelcomeFont -Family $sansFamily -Size ([single](20 * $scale)) `
-PreferredStyle ([Drawing.FontStyle]::Regular)
$format = [Drawing.StringFormat]::new()
$format.Alignment = [Drawing.StringAlignment]::Center
$format.LineAlignment = [Drawing.StringAlignment]::Center
@@ -447,11 +464,20 @@ try {
Draw-CenteredText -Graphics $graphics -Text $DisplayName -Font $nameFont `
-Brush $whiteBrush -Bounds ([Drawing.RectangleF]::new($panelX + 30*$scale, $panelY + 64*$scale, $panelWidth - 60*$scale, 105*$scale)) -Format $format
$graphics.DrawLine($linePen, $panelX + 150*$scale, $panelY + 180*$scale, $panelX + $panelWidth - 150*$scale, $panelY + 180*$scale)
Draw-CenteredText -Graphics $graphics -Text $locationText -Font $locationFont `
-Brush $accentBrush -Bounds ([Drawing.RectangleF]::new($panelX + 60*$scale, $panelY + 190*$scale, $panelWidth - 120*$scale, 94*$scale)) -Format $format
if ($locationSecondaryText) {
Draw-CenteredText -Graphics $graphics -Text $locationPrimaryText -Font $locationFont `
-Brush $accentBrush -Bounds ([Drawing.RectangleF]::new($panelX + 60*$scale, $panelY + 190*$scale, $panelWidth - 120*$scale, 46*$scale)) -Format $format
Draw-CenteredText -Graphics $graphics -Text $locationSecondaryText -Font $locationSecondaryFont `
-Brush $accentBrush -Bounds ([Drawing.RectangleF]::new($panelX + 60*$scale, $panelY + 235*$scale, $panelWidth - 120*$scale, 38*$scale)) -Format $format
}
else {
Draw-CenteredText -Graphics $graphics -Text $locationPrimaryText -Font $locationFont `
-Brush $accentBrush -Bounds ([Drawing.RectangleF]::new($panelX + 60*$scale, $panelY + 195*$scale, $panelWidth - 120*$scale, 78*$scale)) -Format $format
}
}
finally {
$format.Dispose()
$locationSecondaryFont.Dispose()
$locationFont.Dispose()
$nameFont.Dispose()
$welcomeFont.Dispose()
@@ -519,6 +545,8 @@ Write-WelcomeLog -Message ("OK computer={0}; gender={1}; location={2}; ou={3}; o
Gender = $Gender
WelcomeHeading = $welcomeHeading
LocationText = $locationText
LocationPrimaryText = $locationPrimaryText
LocationSecondaryText = $locationSecondaryText
OutputPath = $OutputPath
Applied = -not $SkipApply
}
+55 -13
View File
@@ -129,8 +129,8 @@ fi
article_for() {
local value=${1,,}
case "$value" in
sala*|aula*|facultad*|unidad*|biblioteca*|oficina*|coordinación*) printf la ;;
laboratorio*|centro*|edificio*|campus*|taller*|auditorio*) printf el ;;
sala*|facultad*|unidad*|biblioteca*|oficina*|coordinación*) printf la ;;
aula*|laboratorio*|centro*|edificio*|campus*|taller*|auditorio*) printf el ;;
*) printf '' ;;
esac
}
@@ -159,29 +159,60 @@ case "$gender" in
;;
*)
welcome_text='Te damos la bienvenida,'
located_text='Ubicación:'
located_text=''
engineering_lab_text='Acceso al Laboratorio de Cómputo de Ingeniería.'
;;
esac
location_primary=''
location_secondary=''
if [[ -n $location && -n $organizational_unit ]]; then
room_phrase=$(with_article "$location")
room_article=$(article_for "$location")
if [[ -n $gender ]]; then
room_phrase=$(with_article "$location")
location_primary="${located_text} ${room_phrase}"
elif [[ $room_article == el ]]; then
location_primary="Acceso al ${location}"
elif [[ -n $room_article ]]; then
location_primary="Acceso a ${room_article} ${location}"
else
location_primary="Acceso a ${location}"
fi
ou_article=$(article_for "$organizational_unit")
if [[ $ou_article == el ]]; then
ou_phrase="del ${organizational_unit}"
location_secondary="del ${organizational_unit}."
elif [[ -n $ou_article ]]; then
ou_phrase="de ${ou_article} ${organizational_unit}"
location_secondary="de ${ou_article} ${organizational_unit}."
else
ou_phrase="de ${organizational_unit}"
location_secondary="de ${organizational_unit}."
fi
location_text="${located_text} ${room_phrase} ${ou_phrase}."
elif [[ -n $location ]]; then
location_text="${located_text} $(with_article "$location")."
room_article=$(article_for "$location")
if [[ -n $gender ]]; then
location_primary="${located_text} $(with_article "$location")."
elif [[ $room_article == el ]]; then
location_primary="Acceso al ${location}."
elif [[ -n $room_article ]]; then
location_primary="Acceso a ${room_article} ${location}."
else
location_primary="Acceso a ${location}."
fi
elif [[ -n $organizational_unit ]]; then
location_text="${located_text} $(with_article "$organizational_unit")."
ou_article=$(article_for "$organizational_unit")
if [[ -n $gender ]]; then
location_primary="${located_text} $(with_article "$organizational_unit")."
elif [[ $ou_article == el ]]; then
location_primary="Acceso al ${organizational_unit}."
elif [[ -n $ou_article ]]; then
location_primary="Acceso a ${ou_article} ${organizational_unit}."
else
location_primary="Acceso a ${organizational_unit}."
fi
else
location_text=$engineering_lab_text
location_primary=$engineering_lab_text
fi
location_text=$location_primary
[[ -n $location_secondary ]] && location_text+=$'\n'"$location_secondary"
width=1600
height=1000
@@ -202,6 +233,7 @@ scale=$(( height * 100 / 1000 ))
welcome_size=$(( 34 * scale / 100 ))
name_size=$(( 70 * scale / 100 ))
location_size=$(( 27 * scale / 100 ))
location_secondary_size=$(( 20 * scale / 100 ))
panel_width=$(( width * 76 / 100 ))
panel_height=$(( 310 * scale / 100 ))
panel_x1=$(( (width - panel_width) / 2 ))
@@ -232,6 +264,17 @@ if [[ $serif_font == DejaVu-Serif ]] && command -v fc-list >/dev/null 2>&1; then
fi
fi
location_annotations=(
-font "$sans_font" -weight 400 -style Normal -pointsize "$location_size"
-fill '#D3E2FF' -stroke none -annotate "+0+$(( 76 * scale / 100 ))" "$location_primary"
)
if [[ -n $location_secondary ]]; then
location_annotations+=(
-font "$sans_font" -weight 400 -style Normal -pointsize "$location_secondary_size"
-fill '#D3E2FF' -stroke none -annotate "+0+$(( 116 * scale / 100 ))" "$location_secondary"
)
fi
if ! "${image_command[@]}" "$BASE_IMAGE" \
-resize "${width}x${height}^" -gravity center -extent "${width}x${height}" \
-fill 'rgba(0,13,58,0.30)' -draw "rectangle ${panel_x1},${panel_y1} ${panel_x2},${panel_y2}" \
@@ -241,8 +284,7 @@ if ! "${image_command[@]}" "$BASE_IMAGE" \
-annotate "+0-$(( 92 * scale / 100 ))" "$welcome_text" \
-font "$serif_font" -weight 700 -style Italic -pointsize "$name_size" \
-fill white -annotate "+0-$(( 22 * scale / 100 ))" "$display_name" \
-font "$sans_font" -weight 400 -style Normal -pointsize "$location_size" \
-fill '#D3E2FF' -annotate "+0+$(( 88 * scale / 100 ))" "$location_text" \
"${location_annotations[@]}" \
-quality 94 "$output_path" 2>>"$log_path"; then
fail_softly 'ImageMagick could not render the welcome wallpaper.'
fi
+15
View File
@@ -92,6 +92,20 @@ if (-not $lastSignedInUserHidden) {
$issues.Add('The last signed-in user is not hidden from LogonUI.')
}
$fastUserSwitchingHidden = $false
try {
$fastUserSwitchingHidden = (Get-ItemPropertyValue `
-LiteralPath $interactiveLogonPolicyPath `
-Name HideFastUserSwitching `
-ErrorAction Stop) -eq 1
}
catch {
# Report the missing or unreadable policy as a failed enrollment check.
}
if (-not $fastUserSwitchingHidden) {
$issues.Add('Fast User Switching entry points are not hidden.')
}
$localUserEnumerationDisabled = $false
try {
$localUserEnumerationDisabled = (Get-ItemPropertyValue `
@@ -279,6 +293,7 @@ $result = [pscustomobject]@{
ProviderBinaryPresent = [bool]$providerBinaryPresent
DefaultProviderConfigured = $defaultProviderConfigured
LastSignedInUserHidden = $lastSignedInUserHidden
FastUserSwitchingHidden = $fastUserSwitchingHidden
LocalUserEnumerationDisabled = $localUserEnumerationDisabled
PasswordProviderPreserved = $passwordProviderPreserved
StandardLocalUserPresent = $standardLocalUserPresent
+1
View File
@@ -22,4 +22,5 @@ internal static class BrokerEventIds
internal static readonly EventId DirectoryOptionalMetadataFailure = new(1301, nameof(DirectoryOptionalMetadataFailure));
internal static readonly EventId DirectoryGroupMembershipFailure = new(1302, nameof(DirectoryGroupMembershipFailure));
internal static readonly EventId DirectoryRoleGroupMembershipAdded = new(1303, nameof(DirectoryRoleGroupMembershipAdded));
internal static readonly EventId DirectoryPasswordChangeDenied = new(1304, nameof(DirectoryPasswordChangeDenied));
}
@@ -1,5 +1,7 @@
using System.Collections.Concurrent;
using System.DirectoryServices;
using System.Security.AccessControl;
using System.Security.Principal;
using SGU.AuthBroker.Core.Directory;
using SGU.AuthBroker.Core.Identity;
using SGU.AuthBroker.Core.Profiles;
@@ -15,6 +17,12 @@ public sealed class ActiveDirectorySynchronizer(
private const int InfoAttributeMaximumLength = 1024;
private const int AccountDisabled = 0x0002;
private const int NormalAccount = 0x0200;
private static readonly Guid ChangePasswordExtendedRight =
new("AB721A53-1E2F-11D0-9819-00AA0040529B");
private static readonly SecurityIdentifier EveryoneSid =
new(WellKnownSidType.WorldSid, null);
private static readonly SecurityIdentifier SelfSid =
new(WellKnownSidType.SelfSid, null);
private static readonly AuthenticationTypes BindFlags =
AuthenticationTypes.Secure | AuthenticationTypes.Signing | AuthenticationTypes.Sealing;
@@ -117,6 +125,12 @@ public sealed class ActiveDirectorySynchronizer(
// account without its required classification.
EnsureRoleGroupMembership(user, identity);
// SGU remains the password authority. Deny the user's Change
// Password extended right before making the account usable. This
// does not deny the broker's administrative Reset Password right,
// which ADSI SetPassword uses for each successful authentication.
EnsureCannotChangePassword(user, identity.UserName);
// The exact institutional password received by the broker is passed to AD.
// It is not derived, transformed, written to disk, or included in logs.
user.Invoke("SetPassword", [password]);
@@ -143,6 +157,52 @@ public sealed class ActiveDirectorySynchronizer(
}
}
private void EnsureCannotChangePassword(DirectoryEntry user, string institutionalUser)
{
user.Options!.SecurityMasks = SecurityMasks.Dacl;
ActiveDirectorySecurity security = user.ObjectSecurity;
if (!EnsureCannotChangePassword(security))
{
return;
}
user.ObjectSecurity = security;
user.CommitChanges();
logger.LogInformation(
BrokerEventIds.DirectoryPasswordChangeDenied,
"Denied direct password changes for managed Active Directory user {InstitutionalUser}; SGU Auth Broker remains the password authority.",
institutionalUser);
}
internal static bool EnsureCannotChangePassword(ActiveDirectorySecurity security)
{
bool changed = false;
foreach (SecurityIdentifier identity in new[] { EveryoneSid, SelfSid })
{
bool exists = security
.GetAccessRules(includeExplicit: true, includeInherited: false, typeof(SecurityIdentifier))
.OfType<ActiveDirectoryAccessRule>()
.Any(rule =>
rule.AccessControlType == AccessControlType.Deny &&
rule.IdentityReference.Equals(identity) &&
rule.ObjectType == ChangePasswordExtendedRight &&
(rule.ActiveDirectoryRights & ActiveDirectoryRights.ExtendedRight) != 0);
if (exists)
{
continue;
}
security.AddAccessRule(new ActiveDirectoryAccessRule(
identity,
ActiveDirectoryRights.ExtendedRight,
AccessControlType.Deny,
ChangePasswordExtendedRight));
changed = true;
}
return changed;
}
private static void TryApplyProfile(
DirectoryEntry user,
UserIdentity identity,
@@ -0,0 +1,21 @@
$repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$synchronizerPath = Join-Path $repositoryRoot 'src\SGU.AuthBroker\Services\ActiveDirectorySynchronizer.cs'
$deployPath = Join-Path $repositoryRoot 'scripts\Deploy-AuthBroker.ps1'
$synchronizer = Get-Content -LiteralPath $synchronizerPath -Raw
$deploy = Get-Content -LiteralPath $deployPath -Raw
Describe 'SGU Auth Broker password authority' {
It 'denies the Change Password extended right to SELF and Everyone before SetPassword' {
$synchronizer | Should Match 'AB721A53-1E2F-11D0-9819-00AA0040529B'
$synchronizer | Should Match 'WellKnownSidType\.WorldSid'
$synchronizer | Should Match 'WellKnownSidType\.SelfSid'
$synchronizer | Should Match 'AccessControlType\.Deny'
$synchronizer.IndexOf('EnsureCannotChangePassword(user', [StringComparison]::Ordinal) |
Should BeLessThan $synchronizer.IndexOf('user.Invoke("SetPassword"', [StringComparison]::Ordinal)
}
It 'repairs every existing account below Usuarios-SGU during broker deployment' {
$deploy | Should Match 'Get-ADUser.*-SearchBase \$usersOuDn.*-SearchScope Subtree'
$deploy | Should Match '(?s)Set-ADAccountControl.*-CannotChangePassword \$true'
}
}
+22
View File
@@ -4,6 +4,8 @@ $enrollmentTestScriptPath = Join-Path $repositoryRoot 'scripts\Test-SguClientEnr
$packageScriptPath = Join-Path $repositoryRoot 'scripts\New-SguBootstrapPackages.ps1'
$releaseScriptPath = Join-Path $repositoryRoot 'scripts\Publish-GiteaRelease.ps1'
$azureLauncherPath = Join-Path $repositoryRoot 'scripts\Start-SguAzureClientEnrollment.cmd'
$credentialProviderInstallerPath = Join-Path $repositoryRoot 'scripts\Install-CredentialProvider.ps1'
$computerPolicyScriptPath = Join-Path $repositoryRoot 'scripts\Set-SguDomainComputerPolicies.ps1'
$tokens = $null
$parseErrors = $null
@@ -72,4 +74,24 @@ Describe 'SGU Windows client enrollment scripts' {
Should Be $true
$azureLauncher | Should Match '-PauseOnError'
}
It 'hides Fast User Switching during enrollment and through computer policy' {
(Get-Content -LiteralPath $credentialProviderInstallerPath -Raw) |
Should Match 'HideFastUserSwitching'
(Get-Content -LiteralPath $enrollmentTestScriptPath -Raw) |
Should Match 'FastUserSwitchingHidden'
(Get-Content -LiteralPath $computerPolicyScriptPath -Raw) |
Should Match "Name = 'HideFastUserSwitching'; Type = 'DWord'; Value = 1"
}
It 'separates the clean lock screen base from the branded desktop base' {
$installerSource = Get-Content -LiteralPath $credentialProviderInstallerPath -Raw
$policySource = Get-Content -LiteralPath $computerPolicyScriptPath -Raw
$packageSource = Get-Content -LiteralPath $packageScriptPath -Raw
$installerSource | Should Match 'darkblue-lockscreen\.jpg'
$packageSource | Should Match 'darkblue-lockscreen\.jpg'
$policySource | Should Match "LockScreenImage.*WelcomeLockScreenBasePath"
$policySource | Should Match "LockScreenImagePath.*WelcomeLockScreenBasePath"
$policySource | Should Match "LockScreenImageUrl.*WelcomeLockScreenBasePath"
}
}
@@ -1,3 +1,6 @@
using System.DirectoryServices;
using System.Security.AccessControl;
using System.Security.Principal;
using SGU.AuthBroker.Core.Profiles;
using SGU.AuthBroker.Services;
using Xunit;
@@ -31,4 +34,29 @@ public sealed class ActiveDirectorySynchronizerTests
Assert.Null(updated);
}
[Fact]
public void CannotChangePasswordRulesAreCompleteAndIdempotent()
{
ActiveDirectorySecurity security = new();
Assert.True(ActiveDirectorySynchronizer.EnsureCannotChangePassword(security));
Assert.False(ActiveDirectorySynchronizer.EnsureCannotChangePassword(security));
Guid changePasswordRight = new("AB721A53-1E2F-11D0-9819-00AA0040529B");
ActiveDirectoryAccessRule[] rules = security
.GetAccessRules(includeExplicit: true, includeInherited: false, typeof(SecurityIdentifier))
.OfType<ActiveDirectoryAccessRule>()
.Where(rule =>
rule.AccessControlType == AccessControlType.Deny &&
rule.ObjectType == changePasswordRight &&
(rule.ActiveDirectoryRights & ActiveDirectoryRights.ExtendedRight) != 0)
.ToArray();
Assert.Equal(2, rules.Length);
Assert.Contains(rules, rule => rule.IdentityReference.Equals(
new SecurityIdentifier(WellKnownSidType.WorldSid, null)));
Assert.Contains(rules, rule => rule.IdentityReference.Equals(
new SecurityIdentifier(WellKnownSidType.SelfSid, null)));
}
}
+82 -6
View File
@@ -2,7 +2,9 @@ $repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$bicepPath = Join-Path $repositoryRoot 'infra\azure\main.bicep'
$deploymentPath = Join-Path $repositoryRoot 'scripts\Deploy-SguAzureInfrastructure.ps1'
$configurationPath = Join-Path $repositoryRoot 'scripts\Enable-SguAzureUserRoaming.ps1'
$rollbackPath = Join-Path $repositoryRoot 'scripts\Disable-SguAzureUserRoaming.ps1'
$installerPath = Join-Path $repositoryRoot 'scripts\Install-SguFsLogix.ps1'
$gpoDeploymentPath = Join-Path $repositoryRoot 'scripts\Publish-SguFsLogixClientDeployment.ps1'
$clientBootstrapPath = Join-Path $repositoryRoot 'scripts\Invoke-SguClientBootstrap.ps1'
$azureLauncherPath = Join-Path $repositoryRoot 'scripts\Start-SguAzureClientEnrollment.cmd'
$packagePath = Join-Path $repositoryRoot 'scripts\New-SguBootstrapPackages.ps1'
@@ -10,7 +12,9 @@ $packagePath = Join-Path $repositoryRoot 'scripts\New-SguBootstrapPackages.ps1'
foreach ($scriptPath in @(
$deploymentPath,
$configurationPath,
$rollbackPath,
$installerPath,
$gpoDeploymentPath,
$clientBootstrapPath,
$packagePath)) {
$tokens = $null
@@ -38,9 +42,9 @@ $samFunction = $configurationAst.Find({
Invoke-Expression $samFunction.Extent.Text
Describe 'SGU Azure user-roaming infrastructure' {
It 'deploys dedicated private Azure Files resources by default' {
It 'keeps roaming disabled by default while retaining optional private Azure Files resources' {
$source = Get-Content -LiteralPath $bicepPath -Raw
$source | Should Match 'param deployUserRoaming bool = true'
$source | Should Match 'param deployUserRoaming bool = false'
$source | Should Match "purpose: 'SGU-user-roaming'"
$source | Should Match "publicNetworkAccess: 'Disabled'"
$source | Should Match "privateLinkServiceId: userRoamingStorageAccount.id"
@@ -61,6 +65,8 @@ Describe 'SGU Azure user-roaming infrastructure' {
$source = Get-Content -LiteralPath $deploymentPath -Raw
$source | Should Match '\$DeployUserRoaming -and -not \$DeployVpnGateway'
$source | Should Match 'private Azure Files endpoint'
$source | Should Match '\$DeployUserRoaming -and -not \$AcknowledgeUserRoamingRisks'
$source | Should Match '\[bool\]\$DeployUserRoaming = \$false'
}
It 'returns every post-domain setup value from the Azure deployment' {
@@ -77,6 +83,40 @@ Describe 'SGU Azure user-roaming infrastructure' {
}
Describe 'SGU role-specific roaming policies' {
It 'requires an explicit risk acknowledgement before enabling roaming' {
$source = Get-Content -LiteralPath $configurationPath -Raw
$source | Should Match '\[switch\]\$AcknowledgeUserRoamingRisks'
$source | Should Match 'optional high-risk feature'
}
It 'supports an explicitly selected public SMB endpoint while keeping private as the default' {
(((Get-Command $configurationPath).Parameters['EndpointAccess'].Attributes |
Where-Object { $_ -is [Management.Automation.ValidateSetAttribute] }).ValidValues -join ',') |
Should Be 'Private,Public'
$source = Get-Content -LiteralPath $configurationPath -Raw
$source | Should Match ([regex]::Escape("[string]`$EndpointAccess = 'Private'"))
$source | Should Match ([regex]::Escape("`$EndpointAccess -eq 'Private'"))
$source | Should Match 'selected \$EndpointAccess endpoint'
}
It 'uses local Cloud Cache automatically for public WAN profile storage' {
(((Get-Command $configurationPath).Parameters['StaffProfileStorageMode'].Attributes |
Where-Object { $_ -is [Management.Automation.ValidateSetAttribute] }).ValidValues -join ',') |
Should Be 'Auto,Direct,CloudCache'
$source = Get-Content -LiteralPath $configurationPath -Raw
$source | Should Match "EndpointAccess -eq 'Public'\) \{ 'CloudCache' \}"
$source | Should Match 'CCDLocations'
$source | Should Match 'type=smb,name='
$source | Should Match "ValueName 'VHDLocations'"
$source | Should Match '\*\*del\.\$ValueName'
$source | Should Match 'HealthyProvidersRequiredForRegister'
$source | Should Match 'CcdUnregisterTimeout.*60'
$source | Should Match 'ClearCacheOnForcedUnregister'
$source | Should Match 'PreventLoginWithFailure'
$source | Should Match "'HKLM\\SOFTWARE\\FSLogix\\Apps'"
$source | Should Match "ValueName 'VHDCompactDisk'"
}
It 'uses an AD computer identity with AES-256 Kerberos' {
$source = Get-Content -LiteralPath $configurationPath -Raw
$source | Should Match "DomainAccountType = 'ComputerAccount'"
@@ -90,6 +130,9 @@ Describe 'SGU role-specific roaming policies' {
$source | Should Match "SecurityIdentifier\]::new\('S-1-3-0'\)"
$source | Should Match 'PropagationFlags\]::InheritOnly'
$source | Should Match 'SetAccessRuleProtection\(\$true, \$false\)'
$source | Should Match '\$perUserRootRights'
$source | Should Match 'CreateDirectories'
$source | Should Match 'ContributorRights \$perUserRootRights'
}
It 'redirects only the student Desktop and Documents folders' {
@@ -118,11 +161,11 @@ Describe 'SGU role-specific roaming policies' {
It 'derives valid deterministic AD names for long storage account names' {
$name = Get-SguStorageSamAccountName -StorageName 'abcdefghijklmnopqrstuvwx'
$name.Length | Should Be 20
$name | Should Match '^sgufs[0-9a-f]{15}$'
$name.Length | Should Be 15
$name | Should Match '^sgufs[0-9a-f]{10}$'
(Get-SguStorageSamAccountName -StorageName 'abcdefghijklmnopqrstuvwx') | Should Be $name
(Get-SguStorageSamAccountName -StorageName 'sguroam1234567890123') |
Should Be 'sguroam1234567890123'
(Get-SguStorageSamAccountName -StorageName 'sguroam1234567') |
Should Be 'sguroam1234567'
}
}
@@ -148,6 +191,39 @@ Describe 'SGU FSLogix image enrollment' {
It 'packages both roaming setup scripts' {
$source = Get-Content -LiteralPath $packagePath -Raw
$source | Should Match "'Install-SguFsLogix\.ps1'"
$source | Should Match "'Disable-SguAzureUserRoaming\.ps1'"
$source | Should Match "'Enable-SguAzureUserRoaming\.ps1'"
$source | Should Match "'Publish-SguFsLogixClientDeployment\.ps1'"
}
It 'publishes a signed idempotent FSLogix computer startup deployment' {
$source = Get-Content -LiteralPath $gpoDeploymentPath -Raw
$source | Should Match 'Get-AuthenticodeSignature'
$source | Should Match 'ExpectedInstallerSha256'
$source | Should Match "'Machine\\Scripts'"
$source | Should Match "'Startup'"
$source | Should Match 'scripts\.ini'
$source | Should Match '42B5FAAE-6536-11D2-AE5A-0000F87571E3'
$source | Should Match 'Install-SguFsLogix-Startup\.ps1'
}
}
Describe 'SGU local-profile rollback' {
It 'disables FSLogix containers and their services through computer policy' {
$source = Get-Content -LiteralPath $rollbackPath -Raw
$source | Should Match "'HKLM\\SOFTWARE\\FSLogix\\Profiles'"
$source | Should Match "'HKLM\\SOFTWARE\\FSLogix\\ODFC'"
$source | Should Match '-ValueName Enabled -Type DWord -Value 0'
$source | Should Match "'frxsvc','frxccds'"
$source | Should Match '-ValueName Start -Type DWord -Value 4'
$source | Should Match 'StaffProfileStorageMode[\s\S]*-Value Disabled'
}
It 'restores student shell folders to the local profile and disables deployment' {
$source = Get-Content -LiteralPath $rollbackPath -Raw
$source | Should Match '%USERPROFILE%\\Desktop'
$source | Should Match '%USERPROFILE%\\Documents'
$source | Should Match "-LinkEnabled No"
$source | Should Match 'profilePath,homeDirectory,homeDrive'
}
}
+73 -2
View File
@@ -89,7 +89,29 @@ Describe 'Welcome wallpaper with AD metadata' {
$result = Invoke-WelcomeFixture -DirectoryGender $null `
-DirectoryLocation 'Aula Flexible' `
-DirectoryOu 'Centro de Experiencia Digital'
$result.LocationText | Should Be 'Acceso al Aula Flexible del Centro de Experiencia Digital.'
$result.LocationText | Should Be "Acceso al Aula Flexible`ndel Centro de Experiencia Digital."
$result.LocationPrimaryText | Should Be 'Acceso al Aula Flexible'
$result.LocationSecondaryText | Should Be 'del Centro de Experiencia Digital.'
}
It 'uses the requested neutral, masculine and feminine location wording' {
$neutral = Invoke-WelcomeFixture -DirectoryGender $null `
-DirectoryLocation 'Sala de Aplicaciones' `
-DirectoryOu 'Laboratorio de Cómputo de Ingeniería'
$neutral.LocationPrimaryText | Should Be 'Acceso a la Sala de Aplicaciones'
$neutral.LocationSecondaryText | Should Be 'del Laboratorio de Cómputo de Ingeniería.'
$male = Invoke-WelcomeFixture -DirectoryGender 'Male' `
-DirectoryLocation 'Aula Flexible' `
-DirectoryOu 'Centro de Experiencia Digital'
$male.LocationPrimaryText | Should Be 'Estás ubicado en el Aula Flexible'
$male.LocationSecondaryText | Should Be 'del Centro de Experiencia Digital.'
$female = Invoke-WelcomeFixture -DirectoryGender 'Female' `
-DirectoryLocation 'Sala de Redes' `
-DirectoryOu 'Laboratorio de Cómputo de Ingeniería'
$female.LocationPrimaryText | Should Be 'Estás ubicada en la Sala de Redes'
$female.LocationSecondaryText | Should Be 'del Laboratorio de Cómputo de Ingeniería.'
}
It 'keeps machine location metadata when the GPO command has no location arguments' {
@@ -98,6 +120,55 @@ Describe 'Welcome wallpaper with AD metadata' {
-DirectoryOu 'Laboratorio' `
-MachineLocation 'Aula Flexible' `
-MachineOu 'Centro de Experiencia Digital'
$result.LocationText | Should Be 'Acceso al Aula Flexible del Centro de Experiencia Digital.'
$result.LocationText | Should Be "Acceso al Aula Flexible`ndel Centro de Experiencia Digital."
}
It 'ships the logo at the lower right of the base image instead of compositing it at runtime' {
$source | Should Not Match 'lasalle-logo-blanco|DrawImage.*logo|composite.*logo'
$bitmap = [Drawing.Bitmap]::FromFile((Join-Path $repositoryRoot 'assets\branding\darkblue.jpg'))
try {
$upperLeftWhitePixels = 0
for ($x = 60; $x -lt 390; $x += 2) {
for ($y = 45; $y -lt 175; $y += 2) {
$pixel = $bitmap.GetPixel($x, $y)
if ($pixel.R -gt 220 -and $pixel.G -gt 220 -and $pixel.B -gt 220) {
$upperLeftWhitePixels++
}
}
}
$lowerRightWhitePixels = 0
for ($x = 1260; $x -lt 1510; $x += 2) {
for ($y = 740; $y -lt 860; $y += 2) {
$pixel = $bitmap.GetPixel($x, $y)
if ($pixel.R -gt 220 -and $pixel.G -gt 220 -and $pixel.B -gt 220) {
$lowerRightWhitePixels++
}
}
}
($upperLeftWhitePixels -lt 25) | Should Be $true
($lowerRightWhitePixels -gt 250) | Should Be $true
}
finally {
$bitmap.Dispose()
}
}
It 'keeps the lock screen base free of the desktop logo' {
$bitmap = [Drawing.Bitmap]::FromFile((Join-Path $repositoryRoot 'assets\branding\darkblue-lockscreen.jpg'))
try {
$whitePixels = 0
for ($x = 0; $x -lt $bitmap.Width; $x += 4) {
for ($y = 0; $y -lt $bitmap.Height; $y += 4) {
$pixel = $bitmap.GetPixel($x, $y)
if ($pixel.R -gt 220 -and $pixel.G -gt 220 -and $pixel.B -gt 220) {
$whitePixels++
}
}
}
($whitePixels -lt 10) | Should Be $true
}
finally {
$bitmap.Dispose()
}
}
}