Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20dae3caa9 | ||
|
|
7d78a1f515 | ||
|
|
8290e347f5 | ||
|
|
4478753457 | ||
|
|
80ab99cc8a | ||
|
|
106be26e7c | ||
|
|
01e0ee5ac4 | ||
|
|
c47913e81d | ||
|
|
da310e4213 | ||
|
|
bda8b354b7 | ||
|
|
7986b76e35 |
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,16 @@
|
||||
# SGU Credential Provider 0.6.9
|
||||
|
||||
Esta versión completa la autoridad de contraseñas del Auth Broker en la
|
||||
interfaz de Windows.
|
||||
|
||||
- La GPO `SGU - User session restrictions` habilita la directiva **Remove
|
||||
Change Password** para todas las cuentas bajo `OU=Usuarios-SGU`.
|
||||
- La opción **Cambiar una contraseña** deja de aparecer en la pantalla de
|
||||
seguridad de Ctrl+Alt+Supr.
|
||||
- La protección real continúa en Active Directory mediante las denegaciones
|
||||
del derecho extendido `Change Password`; la GPO únicamente evita mostrar una
|
||||
acción que esas cuentas no pueden completar.
|
||||
|
||||
El Auth Broker conserva el derecho administrativo separado `Reset Password`
|
||||
para sincronizar la contraseña institucional después de una autenticación SGU
|
||||
válida.
|
||||
@@ -18,6 +18,13 @@ 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.
|
||||
- The `SGU - User session restrictions` GPO also hides the **Change a
|
||||
password** command from the Windows Ctrl+Alt+Delete security screen for
|
||||
managed users. This is a user-interface complement to the directory ACL,
|
||||
not a substitute for it.
|
||||
- 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
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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 } }
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ param(
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$policyKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System'
|
||||
$policyValueName = 'DisableLockWorkstation'
|
||||
$disableChangePasswordValueName = 'DisableChangePassword'
|
||||
$desktopPolicyKey = 'HKCU\Software\Policies\Microsoft\Windows\Control Panel\Desktop'
|
||||
$themeKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize'
|
||||
|
||||
@@ -76,6 +77,17 @@ if ($PSCmdlet.ShouldProcess($GpoName, 'Prevent SGU users from manually locking w
|
||||
-Type DWord `
|
||||
-Value 1 | Out-Null
|
||||
|
||||
# The directory ACL remains the security boundary. This user policy also
|
||||
# removes the unusable Change a password action from Ctrl+Alt+Delete.
|
||||
Set-GPRegistryValue `
|
||||
-Name $GpoName `
|
||||
-Domain $domainName `
|
||||
-Server $DomainController `
|
||||
-Key $policyKey `
|
||||
-ValueName $disableChangePasswordValueName `
|
||||
-Type DWord `
|
||||
-Value 1 | Out-Null
|
||||
|
||||
Set-GPRegistryValue `
|
||||
-Name $GpoName `
|
||||
-Domain $domainName `
|
||||
@@ -135,6 +147,12 @@ $configuredValue = Get-GPRegistryValue `
|
||||
-Server $DomainController `
|
||||
-Key $policyKey `
|
||||
-ValueName $policyValueName
|
||||
$disableChangePasswordValue = Get-GPRegistryValue `
|
||||
-Name $GpoName `
|
||||
-Domain $domainName `
|
||||
-Server $DomainController `
|
||||
-Key $policyKey `
|
||||
-ValueName $disableChangePasswordValueName
|
||||
$screenSaverValue = Get-GPRegistryValue `
|
||||
-Name $GpoName `
|
||||
-Domain $domainName `
|
||||
@@ -175,6 +193,7 @@ if ($WallpaperPath) {
|
||||
TargetOu = $TargetOuDn
|
||||
LinkEnabled = [bool]$linkEnabled
|
||||
DisableLockWorkstation = [int]$configuredValue.Value
|
||||
DisableChangePassword = [int]$disableChangePasswordValue.Value
|
||||
ScreenSaverDisabled = [string]$screenSaverValue.Value -eq '0'
|
||||
DarkMode = ([int]$appsThemeValue.Value -eq 0) -and ([int]$systemThemeValue.Value -eq 0)
|
||||
Wallpaper = $configuredWallpaper
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,28 @@
|
||||
$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'
|
||||
$userPolicyPath = Join-Path $repositoryRoot 'scripts\Set-SguDomainUserPolicies.ps1'
|
||||
$synchronizer = Get-Content -LiteralPath $synchronizerPath -Raw
|
||||
$deploy = Get-Content -LiteralPath $deployPath -Raw
|
||||
$userPolicy = Get-Content -LiteralPath $userPolicyPath -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'
|
||||
}
|
||||
|
||||
It 'removes Change Password from the Windows security screen for managed users' {
|
||||
$userPolicy | Should Match "disableChangePasswordValueName = 'DisableChangePassword'"
|
||||
$userPolicy | Should Match '(?s)-ValueName \$disableChangePasswordValueName.*-Type DWord.*-Value 1'
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user