Compare commits

...
10 Commits
32 changed files with 2129 additions and 20 deletions
+8 -1
View File
@@ -60,6 +60,8 @@ Operational documentation:
- [Linux client enrollment with realmd and SSSD](docs/linux-client-enrollment.md) - [Linux client enrollment with realmd and SSSD](docs/linux-client-enrollment.md)
- [Self-hosted RustDesk server and managed Windows remote access](docs/rustdesk-operations.md) - [Self-hosted RustDesk server and managed Windows remote access](docs/rustdesk-operations.md)
- [Domain monitoring, usage reports, and six-month retention](docs/monitoring.md) - [Domain monitoring, usage reports, and six-month retention](docs/monitoring.md)
- [Laboratorio desktop wallpaper protection](docs/laboratorio-wallpaper-policy.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) - [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 | | Prefix | Role | Default OU | Security group in the same OU |
@@ -107,7 +109,12 @@ Follow [docs/lab-runbook.md](docs/lab-runbook.md). Review
For a public Azure VM, use For a public Azure VM, use
[docs/azure-vpn-deployment.md](docs/azure-vpn-deployment.md). It supports an [docs/azure-vpn-deployment.md](docs/azure-vpn-deployment.md). It supports an
optional Azure P2S gateway or direct enrollment restricted to explicit public optional Azure P2S gateway or direct enrollment restricted to explicit public
source CIDRs. 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 Never disable the built-in Microsoft password Credential Provider. It is the
supported recovery path if a third-party provider fails to load. supported recovery path if a third-party provider fails to load.
+15
View File
@@ -78,6 +78,21 @@ is deliberately left unset because the verified page does not expose it.
Missing metadata does not clear existing AD values and never changes the Missing metadata does not clear existing AD values and never changes the
password outcome. password outcome.
## Optional Azure user roaming
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 Every synchronization also enforces one idempotent security-group membership
from the classified institutional prefix: `AL` to `SGU-Alumnos`, `AD` to from the classified institutional prefix: `AL` to `SGU-Alumnos`, `AD` to
`SGU-Administrativos`, and `DO` to `SGU-Docentes`. Each role group is stored `SGU-Administrativos`, and `DO` to `SGU-Docentes`. Each role group is stored
+20 -3
View File
@@ -13,7 +13,9 @@ La plantilla crea:
- IP pública Standard para la VM, protegida por NSG; - IP pública Standard para la VM, protegida por NSG;
- VPN Gateway opcional `VpnGw1AZ` con IKEv2/OpenVPN y autenticación por certificados; - VPN Gateway opcional `VpnGw1AZ` con IKEv2/OpenVPN y autenticación por certificados;
- pool P2S `172.30.0.0/24`, autorizado en los firewalls SGU; - pool P2S `172.30.0.0/24`, autorizado en los firewalls SGU;
- DNS de la NIC del servidor apuntando a `10.77.0.4`. - DNS de la NIC del servidor apuntando a `10.77.0.4`;
- 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 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 por Hyper-V, el `Default Switch`, Wi-Fi o Ethernet locales. Los prefijos de
@@ -82,7 +84,10 @@ pública actual:
No utilice `0.0.0.0/0`. El despliegue de un VPN Gateway suele tardar bastante 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 más que la VM; omitirlo reduce tiempo y costo. El comando espera hasta que Azure
entregue un resultado final. 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 ## 3. Conectarse al servidor
@@ -137,6 +142,14 @@ El JSON debe indicar `NetworkConfigurationMode = PlatformManaged`, el pool P2S
en `TrustedClientNetworks` cuando exista VPN y los CIDR directos en en `TrustedClientNetworks` cuando exista VPN y los CIDR directos en
`PublicEnrollmentNetworks` cuando se hayan habilitado. `PublicEnrollmentNetworks` cuando se hayan habilitado.
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 ## 5. Enrolar cada VM Hyper-V
En la estación administrativa, emita una credencial distinta por equipo: En la estación administrativa, emita una credencial distinta por equipo:
@@ -155,7 +168,7 @@ Copie a la VM Windows 10/11 de Hyper-V:
Desde la carpeta extraída del bootstrap de cliente, instale P2S y enrole: Desde la carpeta extraída del bootstrap de cliente, instale P2S y enrole:
```bat ```bat
Start-SguAzureClientEnrollment.cmd 10.77.0.4 C:\SGU\sgu-azure-vpn-client.zip C:\SGU\sgu-azure-p2s-Windows11.pfx C:\SGU\sgu-azure-p2s-root.cer Start-SguAzureClientEnrollment.cmd 10.77.0.4 C:\SGU\sgu-azure-vpn-client.zip C:\SGU\sgu-azure-p2s-Windows11.pfx C:\SGU\sgu-azure-p2s-root.cer C:\SGU\FSLogixAppsSetup.exe
``` ```
En una sola ejecución el comando: En una sola ejecución el comando:
@@ -169,6 +182,10 @@ En una sola ejecución el comando:
5. registra mTLS, instala SGU/RustDesk y une el equipo al dominio; 5. registra mTLS, instala SGU/RustDesk y une el equipo al dominio;
6. reinicia Windows. 6. reinicia Windows.
El quinto argumento es opcional. Cuando se proporciona, instala el binario
oficial de FSLogix después de validar su firma de Microsoft; puede omitirse si
la imagen ya lo contiene. Las GPO activan el contenedor únicamente para AD/DO.
Para el modo directo, cada Windows 10/11 usa el lanzador normal y la IP pública; Para el modo directo, cada Windows 10/11 usa el lanzador normal y la IP pública;
no necesita perfil ni certificado VPN: no necesita perfil ni certificado VPN:
+4
View File
@@ -36,6 +36,10 @@ contrario, el contenedor de equipos configurado en AD. Los parámetros
explícitamente. Para otra cuenta, editar el usuario sugerido como explícitamente. Para otra cuenta, editar el usuario sugerido como
`DOMINIO\usuario` o `usuario@dominio`. No se guardan contraseñas. `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 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 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 un certificado DoH, publica DNS cifrado en TCP 443 y devuelve únicamente su
+33
View File
@@ -0,0 +1,33 @@
# Protección del fondo de escritorio en Laboratorio
El bootstrap de servidor publica `SGU - Laboratorio wallpaper protection` en
`OU=Laboratorio` y sus OU hijas. Para actualizar un dominio existente sin volver
a ejecutar el bootstrap, ejecute en el DC con permisos para administrar GPO:
```powershell
.\scripts\Set-SguLaboratorioWallpaperPolicy.ps1 `
-TargetOuDn 'OU=Laboratorio,DC=lci,DC=lasalle,DC=mx'
```
El script reutiliza la GPO, respalda una existente antes de modificarla y verifica
los valores escritos. Admite `-WhatIf`. El vínculo está habilitado y forzado, con
prioridad 1, para que también alcance las OU hijas con herencia bloqueada.
- Loopback **Merge** aplica las opciones de usuario según la OU de la computadora
y conserva las otras políticas del usuario.
- `NoChangingWallPaper=1` bloquea el cambio desde Personalización.
- `Wallpaper` fija el archivo generado por SGU:
`%LOCALAPPDATA%\SGU\Wallpapers\welcome-%COMPUTERNAME%.jpg`, con estilo Fill.
Los clientes deben tener el generador, el fondo base y las fuentes que ya entrega
el enrolamiento SGU. La política no reemplaza el generador ni modifica el fondo
de inicio de sesión. No aplica a Linux. Es una restricción de la interfaz de
Windows, no una barrera contra administradores ni contra la modificación directa
del JPG, que está dentro del perfil del usuario.
Tras actualizar las políticas del equipo, cierre e inicie sesión para procesar
loopback y comprobar la política de usuario con `gpresult /h resultado.html`.
Compruebe también visualmente el fondo y Personalización en una sesión de dominio.
La validación por WinRM no sustituye esa prueba de escritorio interactivo.
Referencia: [configuración de fondos mediante GPO de Microsoft](https://learn.microsoft.com/windows/configuration/background/).
+42
View File
@@ -0,0 +1,42 @@
# SGU Credential Provider 0.6.0
Continúa la serie publicada después de `0.5.11`; no sustituye ni renumera versiones anteriores.
## Cambios
- Se incorpora al bootstrap la GPO `SGU - Laboratorio wallpaper protection`:
restringe el cambio de fondo en equipos Windows de Laboratorio y sus OU hijas
mediante loopback Merge, manteniendo el fondo personalizado de SGU.
- Se publican los cambios pendientes de roaming: Azure Files privado, recursos
separados para perfiles y carpetas, configuración de AD DS/ACL, perfiles
FSLogix por grupo AD/DO y rutas de Escritorio/Documentos para AL.
- El enrolamiento Windows admite un instalador FSLogix opcional y verifica su
firma Microsoft. Se incluyen scripts y documentación en los paquetes.
- Se incluyen las mejoras ya presentes en main desde el último lanzamiento:
ubicación del fondo por equipo/OU y pantalla de bloqueo mediante PersonalizationCSP.
## Alcance y precauciones
- Roaming requiere un piloto de inicio/cierre de sesión, permisos entre usuarios,
desconexión de VPN y recuperación de datos. Esta publicación no lo despliega ni
lo activa en el dominio existente. Los perfiles locales no se eliminan por
omisión; no habilite su eliminación sin respaldo y migración.
- La plantilla Azure agrega almacenamiento privado por omisión para P2S. El modo
de enrolamiento público directo requiere `-DeployUserRoaming $false`.
- La GPO de fondo necesita el generador SGU instalado. Las comprobaciones de GPO
no sustituyen la verificación visual tras un inicio de sesión de dominio.
- El bootstrap de servidor crea un bosque nuevo: no restaura SID, contraseñas ni
relaciones de confianza de un bosque anterior.
- Windows Home no admite unión al dominio; use una edición compatible. FSLogix
requiere verificar la licencia correspondiente.
## Descargas
- `sgu-windows-client-bootstrap-0.6.0.zip`
- `sgu-server-bootstrap-0.6.0.zip`
- `sgu-linux-client-bootstrap-0.6.0.zip`
- `sgu-azure-infrastructure-0.6.0.zip`
- `SHA256SUMS-0.6.0.txt`
Verifique los hashes SHA-256 antes de ejecutar los paquetes. No se incluyen
credenciales, certificados privados ni configuración local del entorno desplegado.
+39
View File
@@ -0,0 +1,39 @@
# SGU Credential Provider 0.6.1
Continúa la serie publicada después de `0.6.0`.
## Cambios
- El configurador de roaming admite explícitamente Azure Files mediante endpoint
público con `-EndpointAccess Public`; el modo privado sigue siendo el valor
predeterminado.
- Se corrige la generación del nombre de la identidad de almacenamiento para
respetar el límite de 15 caracteres de una cuenta de equipo de Active
Directory.
- Las ACL de `profiles` y `redirected` aplican mínimo privilegio en la raíz: los
grupos autorizados pueden crear su directorio, pero no modificar ni eliminar
directorios ajenos; `CREATOR OWNER` conserva el control dentro del directorio
propio.
- Se documenta la secuencia de despliegue con firewall de Azure Storage en modo
`Deny`, autorizando finalmente sólo la red institucional `200.13.89.0/24`.
## Validación del despliegue
- Azure Files usa autenticación AD DS con Kerberos AES-256.
- `AF-02` y `AF-03` validaron ticket Kerberos, TCP 445 y lectura/escritura en los
shares `profiles` y `redirected`.
- FSLogix 26.08 (`3.26.826.17182`) quedó instalado y activo en ambos clientes.
- La GPO de FSLogix mantiene una base deshabilitada y se habilita sólo para los
SID de `SGU-Docentes` y `SGU-Administrativos`.
- La GPO de Alumnos redirige únicamente Escritorio y Documentos.
## Descargas
- `sgu-windows-client-bootstrap-0.6.1.zip`
- `sgu-server-bootstrap-0.6.1.zip`
- `sgu-linux-client-bootstrap-0.6.1.zip`
- `sgu-azure-infrastructure-0.6.1.zip`
- `SHA256SUMS-0.6.1.txt`
Verifique los hashes SHA-256 antes de ejecutar los paquetes. No se incluyen
credenciales, claves de almacenamiento ni artefactos temporales del despliegue.
+36
View File
@@ -0,0 +1,36 @@
# SGU Credential Provider 0.6.2
Continúa la serie publicada después de `0.6.1`.
## Cambios
- Se agrega despliegue automático e idempotente de FSLogix 26.08 mediante una
GPO de inicio de equipo ligada a `OU=Laboratorio`.
- El publicador verifica la firma Authenticode de Microsoft y el SHA-256 del
instalador antes de copiarlo al SYSVOL.
- Los equipos que estaban apagados reciben e instalan FSLogix en su siguiente
arranque; no se emplean tareas programadas.
- El script queda incluido en el paquete de servidor y la guía de roaming
documenta su publicación y comprobación.
## Validación del despliegue
- La GPO `SGU - FSLogix client deployment` está enlazada y sus versiones de AD
y SYSVOL coinciden.
- El instalador publicado conserva firma válida de Microsoft y SHA-256
`0BAF7FE8195571060F0361B351E58FE6CD422AE521A96BAB3E66D900544E8A30`.
- `AF-02` y `AF-03` aplicaron la GPO y ejecutan `frxsvc` con FSLogix
`3.26.826.17182`.
- Azure Storage permanece en `defaultAction=Deny` y sólo autoriza
`200.13.89.0/24`.
## Descargas
- `sgu-windows-client-bootstrap-0.6.2.zip`
- `sgu-server-bootstrap-0.6.2.zip`
- `sgu-linux-client-bootstrap-0.6.2.zip`
- `sgu-azure-infrastructure-0.6.2.zip`
- `SHA256SUMS-0.6.2.txt`
Verifique los hashes SHA-256 antes de ejecutar los paquetes. No se incluyen
credenciales, claves de almacenamiento ni artefactos temporales del despliegue.
+44
View File
@@ -0,0 +1,44 @@
# SGU Credential Provider 0.6.3
Corrige el primer inicio de sesión de perfiles completos `AD`/`DO` cuando Azure
Files se consume mediante su endpoint público.
## Cambios
- `Enable-SguAzureUserRoaming.ps1` selecciona automáticamente FSLogix Cloud
Cache para endpoints públicos y conserva el montaje directo para endpoints
privados.
- Cloud Cache realiza el I/O del perfil sobre el VHDX local y sincroniza con
Azure Files, evitando que la latencia WAN de SMB bloquee cada operación.
- La GPO elimina de forma efectiva `VHDLocations` al activar `CCDLocations`,
incluyendo equipos que habían aplicado la política directa anterior.
- Se impide el retorno silencioso a un perfil local o temporal cuando FSLogix
no puede registrar el contenedor.
- El cierre de sesión deja de esperar indefinidamente por un proveedor remoto:
se limita a 60 segundos y conserva la caché local si vence el plazo.
- La documentación agrega capacidad de disco, validación del registro efectivo
y rutas de diagnóstico para ambos modos.
## Validación del despliegue
- Se eliminó únicamente el perfil piloto fallido de `AD017045` en `AF-03`; la
cuenta de Active Directory se conservó.
- La GPO `SGU - AD-DO FSLogix profiles` quedó en Cloud Cache para los SID de
`SGU-Administrativos` y `SGU-Docentes`; `VHDLocations` ya no aparece en el
registro efectivo de `AF-03`.
- El nuevo inicio de `AD017045` creó la caché y el proxy locales, agregó la
redirección del perfil al VHDX y completó `LoadProfile` en 4.649 segundos, sin
error 258, perfil temporal ni retorno local.
- Pasaron 70 pruebas Pester, 41 pruebas de `SGU.AuthBroker.Core`, 16 de
`SGU.AuthBroker` y 6 del Credential Provider. La solución compiló sin errores.
## Descargas
- `sgu-windows-client-bootstrap-0.6.3.zip`
- `sgu-server-bootstrap-0.6.3.zip`
- `sgu-linux-client-bootstrap-0.6.3.zip`
- `sgu-azure-infrastructure-0.6.3.zip`
- `SHA256SUMS-0.6.3.txt`
Verifique los hashes SHA-256 antes de ejecutar los paquetes. No se incluyen
credenciales, claves de almacenamiento ni artefactos temporales del despliegue.
+27
View File
@@ -0,0 +1,27 @@
# SGU Credential Provider 0.6.4
Esta versión revierte el despliegue de perfiles móviles y deja los perfiles
locales como comportamiento predeterminado para todo el dominio SGU.
## Cambio operativo
- `AL`, `AD`, `DO` y cualquier otra cuenta usan un perfil local bajo `C:\Users`.
- Escritorio y Documentos vuelven a las rutas locales del usuario.
- No se configura `profilePath`, `homeDirectory` ni `homeDrive` en Active Directory.
- Las políticas deshabilitan FSLogix Profiles, ODFC, `frxsvc` y `frxccds`.
- El enlace de instalación automática de FSLogix queda deshabilitado.
- Los recursos Azure Files existentes no se destruyen y pueden conservarse para
recuperación o para un piloto futuro.
## Herramientas
`Disable-SguAzureUserRoaming.ps1` aplica el rollback idempotente en el dominio.
`Deploy-SguAzureInfrastructure.ps1` y Bicep usan
`DeployUserRoaming=false` por omisión. Tanto el aprovisionamiento como la
publicación de políticas exigen `AcknowledgeUserRoamingRisks` para volver a
activar la función opcional.
La documentación registra los tiempos prolongados de inicio/cierre, estados
pendientes de VHDX, caché obsoleta y recuperación mediante reinicio observados
durante el piloto. User roaming se considera experimental y nunca se habilita de
forma implícita.
+180
View File
@@ -0,0 +1,180 @@
# User roaming por rol en Azure (opcional)
> **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 sigue local. |
| `AD` / `DO` | Perfil completo | FSLogix VHDX: montaje directo con endpoint privado o Cloud Cache con endpoint público. |
Linux conserva su directorio personal nativo y no monta un perfil de Windows.
`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.
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.
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.
## Riesgos que deben aceptarse
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:
- 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 `
-SubscriptionId '00000000-0000-0000-0000-000000000000' `
-ResourceGroupName 'rg-sgu-lab' `
-Location 'centralus' `
-AdministratorUsername 'azureadmin' `
-P2sRootCertificatePath $p2s.RootCertificatePath `
-DeployUserRoaming $true `
-AcknowledgeUserRoamingRisks
```
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, instale Azure PowerShell y el módulo oficial
[AzFilesHybrid](https://github.com/Azure-Samples/azure-files-samples/releases),
y ejecute:
```powershell
$parameters = @{
SubscriptionId = '00000000-0000-0000-0000-000000000000'
ResourceGroupName = 'rg-sgu-lab'
StorageAccountName = '<cuenta de perfiles>'
FsLogixProfilesShareName = 'profiles'
RedirectedFoldersShareName = 'redirected'
AzFilesHybridModulePath = 'C:\Admin\AzFilesHybrid'
UseDeviceAuthentication = $true
AcknowledgeUserRoamingRisks = $true
}
.\payload\scripts\Enable-SguAzureUserRoaming.ps1 @parameters
```
Para una cuenta autorizada con endpoint público:
```powershell
$parameters.EndpointAccess = 'Public'
$parameters.StaffProfileStorageMode = 'Auto'
.\payload\scripts\Enable-SguAzureUserRoaming.ps1 @parameters
```
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`.
## 3. Instalar FSLogix sólo para un piloto
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\ObjectSpecific\<SID-del-grupo>'
& 'C:\Program Files\FSLogix\Apps\frx.exe' list-redirects
```
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:
- [Autenticación AD DS para Azure Files](https://learn.microsoft.com/azure/storage/files/storage-files-identity-ad-ds-enable)
- [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)
+9 -1
View File
@@ -21,12 +21,17 @@ guardar credenciales. Obtiene:
- `location` del objeto de equipo. - `location` del objeto de equipo.
- La OU padre inmediata a partir de `distinguishedName`. - La OU padre inmediata a partir de `distinguishedName`.
Los valores de equipo `SGU_WELCOME_LOCATION` y
`SGU_WELCOME_ORGANIZATIONAL_UNIT` pueden fijar ambos datos en una máquina
concreta. Tienen prioridad sobre el directorio y sobreviven cuando la GPO
restaura el comando genérico de inicio.
El saludo usa `Bienvenido/ubicado` para `Male` y `Bienvenida/ubicada` para El saludo usa `Bienvenido/ubicado` para `Male` y `Bienvenida/ubicada` para
`Female`. Cuando el enriquecimiento no produjo este dato, utiliza la redacción `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 neutral `Te damos la bienvenida` y `Ubicación:`. El texto secundario sigue estas
reglas: reglas:
1. Con `location` y OU: `Estás ubicado en la Sala de Inmersión del Centro de Experiencia Digital.` 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. 2. Con sólo uno de los datos: muestra únicamente el dato disponible.
3. Sin ambos: el texto adaptado `Bienvenido/Bienvenida al Laboratorio...`; sin 3. Sin ambos: el texto adaptado `Bienvenido/Bienvenida al Laboratorio...`; sin
sexo disponible, la forma neutral `Acceso al Laboratorio de Cómputo de Ingeniería.` sexo disponible, la forma neutral `Acceso al Laboratorio de Cómputo de Ingeniería.`
@@ -46,6 +51,9 @@ en `%LOCALAPPDATA%\SGU\Wallpapers` y se aplica con la API nativa de Windows.
La antigua directiva estática de escritorio se elimina para que no sobrescriba La antigua directiva estática de escritorio se elimina para que no sobrescriba
el archivo individual. La personalización sigue estando gobernada por dominio: el archivo individual. La personalización sigue estando gobernada por dominio:
el comando de inicio y la pantalla de bloqueo pertenecen a la GPO de equipos. el comando de inicio y la pantalla de bloqueo pertenecen a la GPO de equipos.
La GPO configura tanto la directiva clásica como `PersonalizationCSP` y desactiva
Windows Spotlight en la pantalla de bloqueo, porque Windows 11 Pro puede ignorar
la ruta clásica aun cuando el valor de registro está presente.
## Linux ## Linux
+170
View File
@@ -55,6 +55,32 @@ param publicEnrollmentSourceAddressPrefixes array = []
@description('Optional public CIDR allowed to RDP to the VM public IP, for example 203.0.113.10/32. Leave empty to expose no management port.') @description('Optional public CIDR allowed to RDP to the VM public IP, for example 203.0.113.10/32. Leave empty to expose no management port.')
param administratorSourceAddressPrefix string = '' 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 = 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 = ''
@description('Azure Files share used for AD/DO FSLogix profile containers.')
@minLength(3)
@maxLength(63)
param fsLogixProfilesShareName string = 'profiles'
@description('Azure Files share used for AL Documents and Desktop folder redirection.')
@minLength(3)
@maxLength(63)
param redirectedFoldersShareName string = 'redirected'
@description('Quota in GiB for the FSLogix profile-container share.')
@minValue(100)
@maxValue(102400)
param fsLogixProfilesQuotaGiB int = 1024
@description('Quota in GiB for the redirected-folders share.')
@minValue(100)
@maxValue(102400)
param redirectedFoldersQuotaGiB int = 1024
var virtualNetworkName = '${deploymentPrefix}-vnet' var virtualNetworkName = '${deploymentPrefix}-vnet'
var domainControllerSubnetName = 'DomainControllers' var domainControllerSubnetName = 'DomainControllers'
var gatewaySubnetName = 'GatewaySubnet' var gatewaySubnetName = 'GatewaySubnet'
@@ -64,6 +90,12 @@ var gatewayPublicIpName = '${deploymentPrefix}-vpngw-pip'
var networkInterfaceName = '${deploymentPrefix}-dc-nic' var networkInterfaceName = '${deploymentPrefix}-dc-nic'
var virtualMachineName = '${deploymentPrefix}-dc' var virtualMachineName = '${deploymentPrefix}-dc'
var virtualNetworkGatewayName = '${deploymentPrefix}-vpngw' var virtualNetworkGatewayName = '${deploymentPrefix}-vpngw'
var effectiveUserRoamingStorageAccountName = empty(userRoamingStorageAccountName)
? 'sguroam${uniqueString(resourceGroup().id)}'
: toLower(userRoamingStorageAccountName)
var userRoamingPrivateEndpointName = '${deploymentPrefix}-profiles-pe'
var storageEndpointSuffix = environment().suffixes.storage
var azureFilesPrivateDnsZoneName = 'privatelink.file.${storageEndpointSuffix}'
resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2024-05-01' = { resource networkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2024-05-01' = {
name: networkSecurityGroupName name: networkSecurityGroupName
@@ -178,11 +210,17 @@ resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' = {
virtualNetworkAddressPrefix virtualNetworkAddressPrefix
] ]
} }
dhcpOptions: {
dnsServers: [
domainControllerPrivateIp
]
}
subnets: concat([ subnets: concat([
{ {
name: domainControllerSubnetName name: domainControllerSubnetName
properties: { properties: {
addressPrefix: domainControllerSubnetPrefix addressPrefix: domainControllerSubnetPrefix
privateEndpointNetworkPolicies: deployUserRoaming ? 'Disabled' : null
networkSecurityGroup: { networkSecurityGroup: {
id: networkSecurityGroup.id id: networkSecurityGroup.id
} }
@@ -199,6 +237,122 @@ resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' = {
} }
} }
resource userRoamingStorageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = if (deployUserRoaming) {
name: effectiveUserRoamingStorageAccountName
location: location
tags: {
purpose: 'SGU-user-roaming'
}
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
allowBlobPublicAccess: false
allowCrossTenantReplication: false
allowSharedKeyAccess: true
largeFileSharesState: 'Enabled'
minimumTlsVersion: 'TLS1_2'
publicNetworkAccess: 'Disabled'
supportsHttpsTrafficOnly: true
networkAcls: {
bypass: 'AzureServices'
defaultAction: 'Deny'
}
}
}
resource userRoamingFileService 'Microsoft.Storage/storageAccounts/fileServices@2023-05-01' = if (deployUserRoaming) {
parent: userRoamingStorageAccount
name: 'default'
properties: {
shareDeleteRetentionPolicy: {
enabled: true
days: 14
}
}
}
resource fsLogixProfilesShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2023-05-01' = if (deployUserRoaming) {
parent: userRoamingFileService
name: fsLogixProfilesShareName
properties: {
accessTier: 'TransactionOptimized'
enabledProtocols: 'SMB'
shareQuota: fsLogixProfilesQuotaGiB
}
}
resource redirectedFoldersShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2023-05-01' = if (deployUserRoaming) {
parent: userRoamingFileService
name: redirectedFoldersShareName
properties: {
accessTier: 'TransactionOptimized'
enabledProtocols: 'SMB'
shareQuota: redirectedFoldersQuotaGiB
}
}
resource azureFilesPrivateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (deployUserRoaming) {
name: azureFilesPrivateDnsZoneName
location: 'global'
}
resource azureFilesPrivateDnsVnetLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (deployUserRoaming) {
parent: azureFilesPrivateDnsZone
name: '${deploymentPrefix}-vnet-link'
location: 'global'
properties: {
registrationEnabled: false
virtualNetwork: {
id: virtualNetwork.id
}
}
}
resource userRoamingPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = if (deployUserRoaming) {
name: userRoamingPrivateEndpointName
location: location
properties: {
subnet: {
id: resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetworkName, domainControllerSubnetName)
}
privateLinkServiceConnections: [
{
name: 'azure-files'
properties: {
groupIds: [
'file'
]
privateLinkServiceId: userRoamingStorageAccount.id
}
}
]
}
dependsOn: [
virtualNetwork
]
}
resource userRoamingPrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = if (deployUserRoaming) {
parent: userRoamingPrivateEndpoint
name: 'default'
properties: {
privateDnsZoneConfigs: [
{
name: 'azure-files'
properties: {
privateDnsZoneId: azureFilesPrivateDnsZone.id
}
}
]
}
dependsOn: [
azureFilesPrivateDnsVnetLink
]
}
resource domainControllerPublicIp 'Microsoft.Network/publicIPAddresses@2024-05-01' = { resource domainControllerPublicIp 'Microsoft.Network/publicIPAddresses@2024-05-01' = {
name: domainControllerPublicIpName name: domainControllerPublicIpName
location: location location: location
@@ -389,6 +543,22 @@ output virtualNetworkName string = virtualNetwork.name
output virtualNetworkAddressPrefix string = virtualNetworkAddressPrefix output virtualNetworkAddressPrefix string = virtualNetworkAddressPrefix
output vpnGatewayName string = deployVpnGateway ? virtualNetworkGateway.name : '' output vpnGatewayName string = deployVpnGateway ? virtualNetworkGateway.name : ''
output vpnClientAddressPoolPrefix string = vpnClientAddressPoolPrefix output vpnClientAddressPoolPrefix string = vpnClientAddressPoolPrefix
output userRoamingEnabled bool = deployUserRoaming
output userRoamingStorageAccountName string = deployUserRoaming ? userRoamingStorageAccount.name : ''
output fsLogixProfilesSharePath string = deployUserRoaming ? '\\\\${userRoamingStorageAccount.name}.file.${storageEndpointSuffix}\\${fsLogixProfilesShare.name}' : ''
output redirectedFoldersSharePath string = deployUserRoaming ? '\\\\${userRoamingStorageAccount.name}.file.${storageEndpointSuffix}\\${redirectedFoldersShare.name}' : ''
output userRoamingSetupArguments array = deployUserRoaming ? [
'-SubscriptionId'
subscription().subscriptionId
'-ResourceGroupName'
resourceGroup().name
'-StorageAccountName'
userRoamingStorageAccount.name
'-FsLogixProfilesShareName'
fsLogixProfilesShare.name
'-RedirectedFoldersShareName'
redirectedFoldersShare.name
] : []
output serverBootstrapArguments array = concat([ output serverBootstrapArguments array = concat([
'-ServerIPv4Address' '-ServerIPv4Address'
domainControllerPrivateIp domainControllerPrivateIp
+41 -1
View File
@@ -18,6 +18,18 @@ param(
[string]$VpnClientAddressPoolPrefix = '172.30.0.0/24', [string]$VpnClientAddressPoolPrefix = '172.30.0.0/24',
[string[]]$PublicEnrollmentSourceAddressPrefixes = @(), [string[]]$PublicEnrollmentSourceAddressPrefixes = @(),
[string]$AdministratorSourceAddressPrefix = '', [string]$AdministratorSourceAddressPrefix = '',
[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])$')]
[string]$FsLogixProfilesShareName = 'profiles',
[ValidatePattern('^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$')]
[string]$RedirectedFoldersShareName = 'redirected',
[ValidateRange(100, 102400)]
[int]$FsLogixProfilesQuotaGiB = 1024,
[ValidateRange(100, 102400)]
[int]$RedirectedFoldersQuotaGiB = 1024,
[string]$TemplateFile = (Join-Path $PSScriptRoot '..\infra\azure\main.bicep') [string]$TemplateFile = (Join-Path $PSScriptRoot '..\infra\azure\main.bicep')
) )
@@ -33,6 +45,20 @@ if (-not (Test-Path -LiteralPath $TemplateFile -PathType Leaf)) {
if (-not $AdministratorPassword) { if (-not $AdministratorPassword) {
$AdministratorPassword = Read-Host 'Password for the local Azure VM administrator' -AsSecureString $AdministratorPassword = Read-Host 'Password for the local Azure VM administrator' -AsSecureString
} }
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"
}
}
if ($FsLogixProfilesShareName -eq $RedirectedFoldersShareName) {
throw 'FsLogixProfilesShareName and RedirectedFoldersShareName must be different.'
}
$rootCertificateData = '' $rootCertificateData = ''
if ($DeployVpnGateway) { if ($DeployVpnGateway) {
@@ -58,7 +84,10 @@ if ($LASTEXITCODE -ne 0) {
throw "Could not select Azure subscription $SubscriptionId." throw "Could not select Azure subscription $SubscriptionId."
} }
$deploymentDescription = if ($DeployVpnGateway) { $deploymentDescription = if ($DeployVpnGateway -and $DeployUserRoaming) {
'Create Azure VNet, Windows Server 2025 VM, public IP, P2S VPN Gateway, and private user-roaming storage'
}
elseif ($DeployVpnGateway) {
'Create Azure VNet, Windows Server 2025 VM, public IP, and P2S VPN Gateway' 'Create Azure VNet, Windows Server 2025 VM, public IP, and P2S VPN Gateway'
} }
else { else {
@@ -105,6 +134,12 @@ if ($PSCmdlet.ShouldProcess("$ResourceGroupName in $Location", $deploymentDescri
p2sRootCertificateData = @{ value = $rootCertificateData } p2sRootCertificateData = @{ value = $rootCertificateData }
publicEnrollmentSourceAddressPrefixes = @{ value = @($PublicEnrollmentSourceAddressPrefixes) } publicEnrollmentSourceAddressPrefixes = @{ value = @($PublicEnrollmentSourceAddressPrefixes) }
administratorSourceAddressPrefix = @{ value = $AdministratorSourceAddressPrefix } administratorSourceAddressPrefix = @{ value = $AdministratorSourceAddressPrefix }
deployUserRoaming = @{ value = $DeployUserRoaming }
userRoamingStorageAccountName = @{ value = $UserRoamingStorageAccountName }
fsLogixProfilesShareName = @{ value = $FsLogixProfilesShareName }
redirectedFoldersShareName = @{ value = $RedirectedFoldersShareName }
fsLogixProfilesQuotaGiB = @{ value = $FsLogixProfilesQuotaGiB }
redirectedFoldersQuotaGiB = @{ value = $RedirectedFoldersQuotaGiB }
} }
} }
[IO.File]::WriteAllText( [IO.File]::WriteAllText(
@@ -152,5 +187,10 @@ if ($PSCmdlet.ShouldProcess("$ResourceGroupName in $Location", $deploymentDescri
DeployVpnGateway = $DeployVpnGateway DeployVpnGateway = $DeployVpnGateway
PublicEnrollmentSourceAddressPrefixes = @($PublicEnrollmentSourceAddressPrefixes) PublicEnrollmentSourceAddressPrefixes = @($PublicEnrollmentSourceAddressPrefixes)
ServerBootstrapArguments = $values.serverBootstrapArguments ServerBootstrapArguments = $values.serverBootstrapArguments
UserRoamingEnabled = [bool]$values.userRoamingEnabled
UserRoamingStorageAccountName = $values.userRoamingStorageAccountName
FsLogixProfilesSharePath = $values.fsLogixProfilesSharePath
RedirectedFoldersSharePath = $values.redirectedFoldersSharePath
UserRoamingSetupArguments = @($values.userRoamingSetupArguments)
} }
} }
+210
View File
@@ -0,0 +1,210 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]
param(
[string]$DomainController = $env:COMPUTERNAME,
[string]$StudentOuName = 'Alumnos',
[string]$ProfessorOuName = 'Docentes',
[string]$AdministrativeOuName = 'Administrativos',
[string]$LaboratoryOuName = 'Laboratorio',
[string]$StudentGroupName = 'SGU-Alumnos',
[string]$ProfessorGroupName = 'SGU-Docentes',
[string]$AdministrativeGroupName = 'SGU-Administrativos',
[string]$StudentGpoName = 'SGU - AL redirected folders',
[string]$StaffGpoName = 'SGU - AD-DO FSLogix profiles',
[string]$FsLogixDeploymentGpoName = 'SGU - FSLogix client deployment',
[switch]$PreserveLegacyUserMappings
)
$ErrorActionPreference = 'Stop'
function Assert-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this script from an elevated Windows PowerShell 5.1 session on the SGU domain controller.'
}
}
function Set-SguPolicyValue {
param(
[Parameter(Mandatory)][string]$GpoName,
[Parameter(Mandatory)][string]$DomainName,
[Parameter(Mandatory)][string]$Server,
[Parameter(Mandatory)][string]$Key,
[Parameter(Mandatory)][string]$ValueName,
[Parameter(Mandatory)][ValidateSet('DWord','String','ExpandString')][string]$Type,
[Parameter(Mandatory)]$Value
)
if ($PSCmdlet.ShouldProcess("$GpoName :: $Key\\$ValueName", "Set rollback value $Value")) {
Remove-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
-Key $Key -ValueName "**del.$ValueName" -ErrorAction SilentlyContinue | Out-Null
Set-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
-Key $Key -ValueName $ValueName -Type $Type -Value $Value | Out-Null
}
}
function Remove-SguPolicyValue {
param(
[Parameter(Mandatory)][string]$GpoName,
[Parameter(Mandatory)][string]$DomainName,
[Parameter(Mandatory)][string]$Server,
[Parameter(Mandatory)][string]$Key,
[Parameter(Mandatory)][string]$ValueName
)
if ($PSCmdlet.ShouldProcess("$GpoName :: $Key\\$ValueName", 'Remove roaming value')) {
Remove-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
-Key $Key -ValueName $ValueName -ErrorAction SilentlyContinue | Out-Null
Set-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
-Key $Key -ValueName "**del.$ValueName" -Type String -Value '' | Out-Null
}
}
Assert-Administrator
Import-Module ActiveDirectory -ErrorAction Stop
Import-Module GroupPolicy -ErrorAction Stop
$domain = Get-ADDomain -Server $DomainController
$domainName = $domain.DNSRoot
$domainDn = $domain.DistinguishedName
$laboratoryOuDn = "OU=$LaboratoryOuName,$domainDn"
$usersOuDn = "OU=Usuarios-SGU,$domainDn"
$studentOuDn = "OU=$StudentOuName,$usersOuDn"
$professorOuDn = "OU=$ProfessorOuName,$usersOuDn"
$administrativeOuDn = "OU=$AdministrativeOuName,$usersOuDn"
foreach ($ou in $laboratoryOuDn,$studentOuDn,$professorOuDn,$administrativeOuDn) {
Get-ADOrganizationalUnit -Identity $ou -Server $DomainController -ErrorAction Stop | Out-Null
}
$staffGpo = Get-GPO -Name $StaffGpoName -Domain $domainName -Server $DomainController `
-ErrorAction SilentlyContinue
if (-not $staffGpo -and $PSCmdlet.ShouldProcess($StaffGpoName, 'Create local-profile enforcement GPO')) {
$staffGpo = New-GPO -Name $StaffGpoName -Domain $domainName -Server $DomainController
}
if (-not $staffGpo) {
throw "The GPO '$StaffGpoName' does not exist and was not created."
}
$staffLink = @(Get-GPInheritance -Target $laboratoryOuDn -Domain $domainName `
-Server $DomainController).GpoLinks |
Where-Object DisplayName -eq $StaffGpoName | Select-Object -First 1
if (-not $staffLink -and $PSCmdlet.ShouldProcess($laboratoryOuDn, "Link '$StaffGpoName'")) {
New-GPLink -Name $StaffGpoName -Target $laboratoryOuDn -Domain $domainName `
-Server $DomainController -LinkEnabled Yes | Out-Null
}
elseif ($staffLink -and -not [bool]$staffLink.Enabled -and
$PSCmdlet.ShouldProcess($laboratoryOuDn, "Enable '$StaffGpoName' rollback link")) {
Set-GPLink -Name $StaffGpoName -Target $laboratoryOuDn -Domain $domainName `
-Server $DomainController -LinkEnabled Yes | Out-Null
}
$profilesRoot = 'HKLM\SOFTWARE\FSLogix\Profiles'
Set-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName -Server $DomainController `
-Key $profilesRoot -ValueName Enabled -Type DWord -Value 0
Set-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName -Server $DomainController `
-Key 'HKLM\SOFTWARE\FSLogix\ODFC' -ValueName Enabled -Type DWord -Value 0
foreach ($serviceName in 'frxsvc','frxccds') {
Set-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName -Server $DomainController `
-Key "HKLM\SYSTEM\CurrentControlSet\Services\$serviceName" `
-ValueName Start -Type DWord -Value 4
}
Remove-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName -Server $DomainController `
-Key 'HKLM\SOFTWARE\FSLogix\Apps' -ValueName VHDCompactDisk
$profileValueNames = @(
'CCDLocations','VHDLocations','CcdUnregisterTimeout','ClearCacheOnForcedUnregister',
'ClearCacheOnLogoff','HealthyProvidersRequiredForRegister',
'HealthyProvidersRequiredForUnregister','PreventLoginWithFailure',
'PreventLoginWithTempProfile','DeleteLocalProfileWhenVHDShouldApply',
'FlipFlopProfileDirectoryName','IsDynamic','LockedRetryCount','LockedRetryInterval',
'ProfileType','ReAttachIntervalSeconds','ReAttachRetryCount','SizeInMBs','VolumeType',
'VHDCompactDisk'
)
$roleGroups = @(
Get-ADGroup -Identity "CN=$ProfessorGroupName,$professorOuDn" -Server $DomainController
Get-ADGroup -Identity "CN=$AdministrativeGroupName,$administrativeOuDn" -Server $DomainController
)
foreach ($group in $roleGroups) {
$key = "$profilesRoot\ObjectSpecific\$($group.SID.Value)"
Set-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName -Server $DomainController `
-Key $key -ValueName Enabled -Type DWord -Value 0
foreach ($valueName in $profileValueNames) {
Remove-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName `
-Server $DomainController -Key $key -ValueName $valueName
}
}
Set-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName -Server $DomainController `
-Key 'HKLM\SOFTWARE\SGU\UserRoaming' -ValueName StaffProfileStorageMode `
-Type String -Value Disabled
$studentGpo = Get-GPO -Name $StudentGpoName -Domain $domainName -Server $DomainController `
-ErrorAction SilentlyContinue
if ($studentGpo) {
$studentLink = @(Get-GPInheritance -Target $studentOuDn -Domain $domainName `
-Server $DomainController).GpoLinks |
Where-Object DisplayName -eq $StudentGpoName | Select-Object -First 1
if ($studentLink -and -not [bool]$studentLink.Enabled -and
$PSCmdlet.ShouldProcess($studentOuDn, "Enable '$StudentGpoName' local-path rollback")) {
Set-GPLink -Name $StudentGpoName -Target $studentOuDn -Domain $domainName `
-Server $DomainController -LinkEnabled Yes | Out-Null
}
$shellFoldersKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders'
Set-SguPolicyValue -GpoName $StudentGpoName -DomainName $domainName -Server $DomainController `
-Key $shellFoldersKey -ValueName Desktop -Type ExpandString `
-Value '%USERPROFILE%\Desktop'
Set-SguPolicyValue -GpoName $StudentGpoName -DomainName $domainName -Server $DomainController `
-Key $shellFoldersKey -ValueName Personal -Type ExpandString `
-Value '%USERPROFILE%\Documents'
Remove-SguPolicyValue -GpoName $StudentGpoName -DomainName $domainName `
-Server $DomainController -Key 'HKCU\Software\Policies\Microsoft\Windows\NetCache' `
-ValueName DisableFRAdminPin
}
$deploymentGpo = Get-GPO -Name $FsLogixDeploymentGpoName -Domain $domainName `
-Server $DomainController -ErrorAction SilentlyContinue
if ($deploymentGpo) {
$deploymentLink = @(Get-GPInheritance -Target $laboratoryOuDn -Domain $domainName `
-Server $DomainController).GpoLinks |
Where-Object DisplayName -eq $FsLogixDeploymentGpoName | Select-Object -First 1
if ($deploymentLink -and [bool]$deploymentLink.Enabled -and
$PSCmdlet.ShouldProcess($laboratoryOuDn, "Disable '$FsLogixDeploymentGpoName'")) {
Set-GPLink -Name $FsLogixDeploymentGpoName -Target $laboratoryOuDn `
-Domain $domainName -Server $DomainController -LinkEnabled No | Out-Null
}
}
$clearedUsers = [Collections.Generic.List[string]]::new()
if (-not $PreserveLegacyUserMappings) {
foreach ($roleOu in $studentOuDn,$professorOuDn,$administrativeOuDn) {
foreach ($user in Get-ADUser -SearchBase $roleOu -SearchScope Subtree -Filter * `
-Server $DomainController -Properties profilePath,homeDirectory,homeDrive) {
if ($user.profilePath -or $user.homeDirectory -or $user.homeDrive) {
if ($PSCmdlet.ShouldProcess($user.SamAccountName, 'Clear AD profile and home-drive mappings')) {
Set-ADUser -Identity $user -Server $DomainController `
-Clear profilePath,homeDirectory,homeDrive
$clearedUsers.Add($user.SamAccountName)
}
}
}
}
}
$staffGpo = Get-GPO -Name $StaffGpoName -Domain $domainName -Server $DomainController
$studentGpo = Get-GPO -Name $StudentGpoName -Domain $domainName -Server $DomainController `
-ErrorAction SilentlyContinue
$deploymentLink = @(Get-GPInheritance -Target $laboratoryOuDn -Domain $domainName `
-Server $DomainController).GpoLinks |
Where-Object DisplayName -eq $FsLogixDeploymentGpoName | Select-Object -First 1
[pscustomobject]@{
Domain = $domainName
UserRoamingEnabled = $false
StaffGpoVersion = "$($staffGpo.Computer.DSVersion)/$($staffGpo.Computer.SysVolVersion)"
StudentGpoVersion = if ($studentGpo) {
"$($studentGpo.User.DSVersion)/$($studentGpo.User.SysVolVersion)"
} else { $null }
FsLogixDeploymentLinkEnabled = [bool]$deploymentLink.Enabled
StaffProfilesEnabled = 0
StudentDesktop = '%USERPROFILE%\Desktop'
StudentDocuments = '%USERPROFILE%\Documents'
ClearedAdUsers = @($clearedUsers)
RebootManagedWindowsClients = $true
}
+550
View File
@@ -0,0 +1,550 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)][guid]$SubscriptionId,
[Parameter(Mandatory)][string]$ResourceGroupName,
[Parameter(Mandatory)]
[ValidatePattern('^[a-z0-9]{3,24}$')]
[string]$StorageAccountName,
[ValidatePattern('^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$')]
[string]$FsLogixProfilesShareName = 'profiles',
[ValidatePattern('^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$')]
[string]$RedirectedFoldersShareName = 'redirected',
[string]$DomainController = $env:COMPUTERNAME,
[string]$AzureFilesOuName = 'AzureFilesConfig',
[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',
[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]$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()
$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 Import-SguAzFilesHybrid {
param([string]$ModulePath)
if ($ModulePath) {
if (-not (Test-Path -LiteralPath $ModulePath)) {
throw "AzFilesHybridModulePath does not exist: $ModulePath"
}
$resolvedModule = if (Test-Path -LiteralPath $ModulePath -PathType Container) {
Get-ChildItem -LiteralPath $ModulePath -Recurse -File |
Where-Object Name -in @('AzFilesHybrid.psd1', 'AzFilesHybrid.psm1') |
Sort-Object @{ Expression = { $_.Extension -eq '.psd1' }; Descending = $true }, FullName |
Select-Object -First 1
}
else {
Get-Item -LiteralPath $ModulePath
}
if (-not $resolvedModule) {
throw "AzFilesHybrid.psd1 or AzFilesHybrid.psm1 was not found beneath $ModulePath."
}
Import-Module -Name $resolvedModule.FullName -Force -ErrorAction Stop
}
else {
Import-Module -Name AzFilesHybrid -Force -ErrorAction Stop
}
$joinCommand = Get-Command Join-AzStorageAccount -ErrorAction SilentlyContinue
if (-not $joinCommand) {
$joinCommand = Get-Command Join-AzStorageAccountForAuth -ErrorAction SilentlyContinue
}
if (-not $joinCommand) {
throw 'AzFilesHybrid did not expose Join-AzStorageAccount. Install the current Microsoft AzFilesHybrid module and retry.'
}
return $joinCommand
}
function Get-SguStorageSamAccountName {
param([Parameter(Mandatory)][string]$StorageName)
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, 10).ToLowerInvariant()
return "sgufs$suffix"
}
finally {
$sha256.Dispose()
}
}
function Ensure-SguOrganizationalUnit {
param(
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string]$Path,
[Parameter(Mandatory)][string]$Server
)
$escapedName = $Name.Replace('\', '\5c').Replace('*', '\2a').Replace('(', '\28').Replace(')', '\29')
$ou = Get-ADOrganizationalUnit -LDAPFilter "(ou=$escapedName)" -SearchBase $Path `
-SearchScope OneLevel -Server $Server -ErrorAction Stop | Select-Object -First 1
if (-not $ou -and $PSCmdlet.ShouldProcess("OU=$Name,$Path", 'Create Azure Files identity OU')) {
New-ADOrganizationalUnit -Name $Name -Path $Path -ProtectedFromAccidentalDeletion $true `
-Server $Server | Out-Null
$ou = Get-ADOrganizationalUnit -Identity "OU=$Name,$Path" -Server $Server
}
if (-not $ou) {
throw "The organizational unit OU=$Name,$Path does not exist."
}
return $ou
}
function Ensure-SguGpoLink {
param(
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string]$TargetOuDn,
[Parameter(Mandatory)][string]$DomainName,
[Parameter(Mandatory)][string]$Server
)
$gpo = Get-GPO -Name $Name -Domain $DomainName -Server $Server -ErrorAction SilentlyContinue
if (-not $gpo -and $PSCmdlet.ShouldProcess($Name, 'Create user-roaming GPO')) {
$gpo = New-GPO -Name $Name -Domain $DomainName -Server $Server
}
if (-not $gpo) {
throw "The GPO '$Name' does not exist and was not created."
}
$link = @(Get-GPInheritance -Target $TargetOuDn -Domain $DomainName -Server $Server).GpoLinks |
Where-Object DisplayName -eq $Name | Select-Object -First 1
$linkEnabled = $link -and ($link.Enabled -eq $true -or [string]$link.Enabled -eq 'Yes')
if (-not $link -and $PSCmdlet.ShouldProcess($TargetOuDn, "Link and enable '$Name'")) {
New-GPLink -Name $Name -Target $TargetOuDn -Domain $DomainName -Server $Server `
-LinkEnabled Yes | Out-Null
}
elseif ($link -and -not $linkEnabled -and
$PSCmdlet.ShouldProcess($TargetOuDn, "Enable the '$Name' link")) {
Set-GPLink -Name $Name -Target $TargetOuDn -Domain $DomainName -Server $Server `
-LinkEnabled Yes | Out-Null
}
return $gpo
}
function Set-SguGpoRegistryValue {
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 $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')) {
if ($used -notcontains $name) {
return $name
}
}
throw 'No temporary drive letter is available for configuring Azure Files ACLs.'
}
function Set-SguAzureFileRootAcl {
param(
[Parameter(Mandatory)][string]$UncPath,
[Parameter(Mandatory)][PSCredential]$Credential,
[Parameter(Mandatory)][Security.Principal.SecurityIdentifier]$DomainAdminsSid,
[Parameter(Mandatory)][Security.Principal.SecurityIdentifier[]]$ContributorSids,
[Security.AccessControl.FileSystemRights]$ContributorRights =
[Security.AccessControl.FileSystemRights]::Modify
)
$driveName = Get-SguUnusedDriveName
try {
New-PSDrive -Name $driveName -PSProvider FileSystem -Root $UncPath `
-Credential $Credential -Scope Script -ErrorAction Stop | Out-Null
$rootPath = "${driveName}:\"
$acl = [Security.AccessControl.DirectorySecurity]::new()
$acl.SetAccessRuleProtection($true, $false)
$acl.SetOwner($DomainAdminsSid)
$allow = [Security.AccessControl.AccessControlType]::Allow
$containerAndObject = [Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit'
$noneInheritance = [Security.AccessControl.InheritanceFlags]::None
$nonePropagation = [Security.AccessControl.PropagationFlags]::None
$inheritOnly = [Security.AccessControl.PropagationFlags]::InheritOnly
$systemSid = [Security.Principal.SecurityIdentifier]::new('S-1-5-18')
$creatorOwnerSid = [Security.Principal.SecurityIdentifier]::new('S-1-3-0')
foreach ($administratorSid in @($systemSid, $DomainAdminsSid)) {
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
$administratorSid,
[Security.AccessControl.FileSystemRights]::FullControl,
$containerAndObject,
$nonePropagation,
$allow))
}
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
$creatorOwnerSid,
[Security.AccessControl.FileSystemRights]::Modify,
$containerAndObject,
$inheritOnly,
$allow))
foreach ($contributorSid in $ContributorSids) {
$acl.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new(
$contributorSid,
$ContributorRights,
$noneInheritance,
$nonePropagation,
$allow))
}
Set-Acl -LiteralPath $rootPath -AclObject $acl -ErrorAction Stop
}
finally {
Remove-PSDrive -Name $driveName -Scope Script -Force -ErrorAction SilentlyContinue
}
}
Assert-Administrator
if ($FsLogixProfilesShareName.Contains('--') -or $RedirectedFoldersShareName.Contains('--')) {
throw 'Azure Files share names cannot contain consecutive hyphens.'
}
if ($FsLogixProfilesShareName -eq $RedirectedFoldersShareName) {
throw 'The profile-container and redirected-folder shares must have different names.'
}
Import-Module ActiveDirectory -ErrorAction Stop
Import-Module GroupPolicy -ErrorAction Stop
foreach ($azureModule in @('Az.Accounts', 'Az.Storage')) {
try {
Import-Module $azureModule -ErrorAction Stop
}
catch {
throw "The current $azureModule module is required on the domain controller. Install Azure PowerShell and retry. $($_.Exception.Message)"
}
}
$joinStorageCommand = Import-SguAzFilesHybrid -ModulePath $AzFilesHybridModulePath
$domain = Get-ADDomain -Server $DomainController
$baseDn = $domain.DistinguishedName
$domainName = $domain.DNSRoot
$usersOuDn = "OU=Usuarios-SGU,$baseDn"
$studentOuDn = "OU=$StudentOuName,$usersOuDn"
$professorOuDn = "OU=$ProfessorOuName,$usersOuDn"
$administrativeOuDn = "OU=$AdministrativeOuName,$usersOuDn"
$laboratoryOuDn = "OU=$LaboratoryOuName,$baseDn"
foreach ($requiredOu in @($studentOuDn, $professorOuDn, $administrativeOuDn, $laboratoryOuDn)) {
Get-ADOrganizationalUnit -Identity $requiredOu -Server $DomainController -ErrorAction Stop | Out-Null
}
$studentGroup = Get-ADGroup -Identity "CN=$StudentGroupName,$studentOuDn" -Server $DomainController
$professorGroup = Get-ADGroup -Identity "CN=$ProfessorGroupName,$professorOuDn" -Server $DomainController
$administrativeGroup = Get-ADGroup -Identity "CN=$AdministrativeGroupName,$administrativeOuDn" `
-Server $DomainController
$domainAdminsSid = [Security.Principal.SecurityIdentifier]::new("$($domain.DomainSID.Value)-512")
$azureFilesOu = Ensure-SguOrganizationalUnit -Name $AzureFilesOuName -Path $baseDn -Server $DomainController
$azureContext = Get-AzContext -ErrorAction SilentlyContinue
if (-not $azureContext -or $azureContext.Subscription.Id -ne $SubscriptionId.Guid) {
$connectParameters = @{}
if ($UseDeviceAuthentication) {
$connectParameters.UseDeviceAuthentication = $true
}
Connect-AzAccount @connectParameters | Out-Null
}
Set-AzContext -SubscriptionId $SubscriptionId.Guid | Out-Null
$storageAccount = Get-AzStorageAccount -ResourceGroupName $ResourceGroupName `
-Name $StorageAccountName -ErrorAction Stop
$fileEndpointHost = ([uri]$storageAccount.PrimaryEndpoints.File).Host
if (-not $fileEndpointHost) {
throw "Azure did not return a file endpoint for $StorageAccountName."
}
$directoryService = [string]$storageAccount.AzureFilesIdentityBasedAuth.DirectoryServiceOptions
if ($directoryService -and $directoryService -ne 'None' -and $directoryService -ne 'AD') {
throw "Storage account $StorageAccountName already uses the incompatible Azure Files identity source '$directoryService'."
}
if ($directoryService -ne 'AD') {
if ($PSCmdlet.ShouldProcess($StorageAccountName, "Join Azure Files to $domainName with AES-256 Kerberos")) {
$requestedSamAccountName = Get-SguStorageSamAccountName -StorageName $StorageAccountName
$joinParameters = @{
ResourceGroupName = $ResourceGroupName
StorageAccountName = $StorageAccountName
SamAccountName = $requestedSamAccountName
DomainAccountType = 'ComputerAccount'
OrganizationalUnitDistinguishedName = $azureFilesOu.DistinguishedName
}
& $joinStorageCommand @joinParameters
$storageAccount = Get-AzStorageAccount -ResourceGroupName $ResourceGroupName `
-Name $StorageAccountName -ErrorAction Stop
}
}
$directoryService = [string]$storageAccount.AzureFilesIdentityBasedAuth.DirectoryServiceOptions
if ($directoryService -ne 'AD') {
throw "Azure Files identity authentication is '$directoryService', not AD. The domain join did not complete."
}
$activeDirectoryProperties = $storageAccount.AzureFilesIdentityBasedAuth.ActiveDirectoryProperties
if ([string]$activeDirectoryProperties.DomainName -ne $domainName) {
throw "Storage account $StorageAccountName is joined to $($activeDirectoryProperties.DomainName), not $domainName."
}
$storageSamAccountName = [string]$activeDirectoryProperties.SamAccountName
if (-not $storageSamAccountName) {
$storageSamAccountName = $StorageAccountName
}
$storageComputer = Get-ADComputer -Identity "${storageSamAccountName}$" `
-Server $DomainController -ErrorAction Stop
if ($PSCmdlet.ShouldProcess($storageComputer.DistinguishedName, 'Require AES-256 Kerberos and prevent an unattended storage identity password expiry')) {
Set-ADComputer -Identity $storageComputer -Server $DomainController `
-KerberosEncryptionType AES256 -PasswordNeverExpires $true
}
if ($PSCmdlet.ShouldProcess($StorageAccountName, 'Grant authenticated AD identities the Azure Files SMB contributor default share permission')) {
$storageAccount = Set-AzStorageAccount -ResourceGroupName $ResourceGroupName `
-Name $StorageAccountName `
-DefaultSharePermission StorageFileDataSmbShareContributor
}
$endpointAddresses = @(Resolve-DnsName -Name $fileEndpointHost -Type A -ErrorAction Stop |
Where-Object IPAddress | Select-Object -ExpandProperty IPAddress)
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 using the selected $EndpointAccess endpoint."
}
$storageKey = @(Get-AzStorageAccountKey -ResourceGroupName $ResourceGroupName `
-Name $StorageAccountName -ErrorAction Stop | Where-Object KeyName -eq 'key1' |
Select-Object -First 1).Value
if (-not $storageKey) {
throw "Azure did not return key1 for $StorageAccountName; it is required only to set the initial root ACLs."
}
$storageCredential = [PSCredential]::new(
"Azure\$StorageAccountName",
(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) `
-ContributorRights $perUserRootRights
}
if ($PSCmdlet.ShouldProcess($redirectedFoldersSharePath, 'Apply isolated student-folder root ACLs')) {
Set-SguAzureFileRootAcl -UncPath $redirectedFoldersSharePath -Credential $storageCredential `
-DomainAdminsSid $domainAdminsSid -ContributorSids @($studentGroup.SID) `
-ContributorRights $perUserRootRights
}
}
finally {
$storageKey = $null
$storageCredential = $null
}
$studentGpo = Ensure-SguGpoLink -Name $StudentGpoName -TargetOuDn $studentOuDn `
-DomainName $domainName -Server $DomainController
$userShellFoldersKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders'
Set-SguGpoRegistryValue -GpoName $studentGpo.DisplayName -DomainName $domainName `
-Server $DomainController -Key $userShellFoldersKey -ValueName 'Desktop' `
-Type ExpandString -Value "$redirectedFoldersSharePath\%USERNAME%\Desktop"
Set-SguGpoRegistryValue -GpoName $studentGpo.DisplayName -DomainName $domainName `
-Server $DomainController -Key $userShellFoldersKey -ValueName 'Personal' `
-Type ExpandString -Value "$redirectedFoldersSharePath\%USERNAME%\Documents"
Set-SguGpoRegistryValue -GpoName $studentGpo.DisplayName -DomainName $domainName `
-Server $DomainController -Key 'HKCU\Software\Policies\Microsoft\Windows\NetCache' `
-ValueName 'DisableFRAdminPin' -Type DWord -Value 1
$staffGpo = Ensure-SguGpoLink -Name $StaffGpoName -TargetOuDn $laboratoryOuDn `
-DomainName $domainName -Server $DomainController
$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 = @{
Type = 'DWord'
Value = if ($DeleteExistingStaffLocalProfiles) { 1 } else { 0 }
}
FlipFlopProfileDirectoryName = @{ Type = 'DWord'; Value = 1 }
IsDynamic = @{ Type = 'DWord'; Value = 1 }
LockedRetryCount = @{ Type = 'DWord'; Value = 3 }
LockedRetryInterval = @{ Type = 'DWord'; Value = 15 }
ProfileType = @{ Type = 'DWord'; Value = 0 }
ReAttachIntervalSeconds = @{ Type = 'DWord'; Value = 15 }
ReAttachRetryCount = @{ Type = 'DWord'; Value = 3 }
SizeInMBs = @{ Type = 'DWord'; Value = $FsLogixProfileSizeMiB }
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
}
}
[pscustomobject]@{
StorageAccountName = $StorageAccountName
FileEndpoint = $fileEndpointHost
EndpointAccess = $EndpointAccess
StaffProfileStorageMode = $resolvedStaffProfileStorageMode
EndpointAddresses = $endpointAddresses
PrivateEndpointAddresses = $privateAddresses
DirectoryService = $directoryService
KerberosEncryption = 'AES256'
StorageIdentity = $storageComputer.DistinguishedName
StorageIdentityPasswordNeverExpires = $true
ProfilesSharePath = $profilesSharePath
RedirectedFoldersSharePath = $redirectedFoldersSharePath
StudentPolicy = $studentGpo.DisplayName
StaffPolicy = $staffGpo.DisplayName
StudentBehavior = 'Local non-authoritative profile; Documents and Desktop redirected without Offline Files pinning.'
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
}
@@ -520,6 +520,7 @@ foreach ($requiredPath in @(
(Join-Path $scriptsRoot 'Set-LabBrokerDns.ps1'), (Join-Path $scriptsRoot 'Set-LabBrokerDns.ps1'),
(Join-Path $scriptsRoot 'Set-SguDomainComputerPolicies.ps1'), (Join-Path $scriptsRoot 'Set-SguDomainComputerPolicies.ps1'),
(Join-Path $scriptsRoot 'Set-SguDomainUserPolicies.ps1'), (Join-Path $scriptsRoot 'Set-SguDomainUserPolicies.ps1'),
(Join-Path $scriptsRoot 'Set-SguLaboratorioWallpaperPolicy.ps1'),
(Join-Path $scriptsRoot 'Enable-SguServerRemoteManagement.ps1'), (Join-Path $scriptsRoot 'Enable-SguServerRemoteManagement.ps1'),
(Join-Path $scriptsRoot 'Install-SguDomainMonitoring.ps1'), (Join-Path $scriptsRoot 'Install-SguDomainMonitoring.ps1'),
(Join-Path $scriptsRoot 'Install-SguRustDeskClient.ps1'), (Join-Path $scriptsRoot 'Install-SguRustDeskClient.ps1'),
@@ -829,6 +830,8 @@ $userPolicyParameters = @{
ClearManagedWallpaper = $true ClearManagedWallpaper = $true
} }
& (Join-Path $scriptsRoot 'Set-SguDomainUserPolicies.ps1') @userPolicyParameters | Out-Null & (Join-Path $scriptsRoot 'Set-SguDomainUserPolicies.ps1') @userPolicyParameters | Out-Null
& (Join-Path $scriptsRoot 'Set-SguLaboratorioWallpaperPolicy.ps1') `
-TargetOuDn $laboratoryOuDn -DomainController $env:COMPUTERNAME | Out-Null
$rustDeskServer = & (Join-Path $scriptsRoot 'Install-SguRustDeskServer.ps1') ` $rustDeskServer = & (Join-Path $scriptsRoot 'Install-SguRustDeskServer.ps1') `
-ServerAddress $rustDeskDnsName ` -ServerAddress $rustDeskDnsName `
+8
View File
@@ -314,6 +314,11 @@ if ($PSCmdlet.ShouldProcess($installPath, 'Install and register the SGU Credenti
-Value 1 ` -Value 1 `
-PropertyType DWord ` -PropertyType DWord `
-Force | Out-Null -Force | Out-Null
New-ItemProperty -Path $interactiveLogonPolicyPath `
-Name HideFastUserSwitching `
-Value 1 `
-PropertyType DWord `
-Force | Out-Null
if (-not (Test-Path -LiteralPath $defaultProviderPolicyPath)) { if (-not (Test-Path -LiteralPath $defaultProviderPolicyPath)) {
New-Item -Path $defaultProviderPolicyPath -Force | Out-Null New-Item -Path $defaultProviderPolicyPath -Force | Out-Null
} }
@@ -344,6 +349,9 @@ catch {
LastSignedInUserHidden = (Get-ItemPropertyValue ` LastSignedInUserHidden = (Get-ItemPropertyValue `
-LiteralPath $interactiveLogonPolicyPath ` -LiteralPath $interactiveLogonPolicyPath `
-Name DontDisplayLastUserName) -eq 1 -Name DontDisplayLastUserName) -eq 1
FastUserSwitchingHidden = (Get-ItemPropertyValue `
-LiteralPath $interactiveLogonPolicyPath `
-Name HideFastUserSwitching) -eq 1
LocalUserEnumerationDisabled = (Get-ItemPropertyValue ` LocalUserEnumerationDisabled = (Get-ItemPropertyValue `
-LiteralPath $defaultProviderPolicyPath ` -LiteralPath $defaultProviderPolicyPath `
-Name EnumerateLocalUsers) -eq 0 -Name EnumerateLocalUsers) -eq 0
+67
View File
@@ -0,0 +1,67 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)][string]$InstallerPath
)
$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 installer from an elevated Windows PowerShell session.'
}
$appsRoot = Join-Path $env:ProgramFiles 'FSLogix\Apps'
$frxPath = Join-Path $appsRoot 'frx.exe'
$service = Get-Service -Name frxsvc -ErrorAction SilentlyContinue
if ($service -and (Test-Path -LiteralPath $frxPath -PathType Leaf)) {
$versionOutput = @(& $frxPath version 2>&1) -join [Environment]::NewLine
return [pscustomobject]@{
Installed = $true
Changed = $false
RestartRequired = $false
Service = $service.Status.ToString()
Version = $versionOutput.Trim()
}
}
if (-not (Test-Path -LiteralPath $InstallerPath -PathType Leaf)) {
throw "FSLogixAppsSetup.exe was not found: $InstallerPath"
}
$resolvedInstaller = (Resolve-Path -LiteralPath $InstallerPath).Path
if ([IO.Path]::GetFileName($resolvedInstaller) -ne 'FSLogixAppsSetup.exe') {
throw 'InstallerPath must identify the Microsoft FSLogix core installer named FSLogixAppsSetup.exe.'
}
$signature = Get-AuthenticodeSignature -LiteralPath $resolvedInstaller
if ($signature.Status -ne [Management.Automation.SignatureStatus]::Valid -or
-not $signature.SignerCertificate -or
$signature.SignerCertificate.Subject -notmatch '(^|,\s*)CN=Microsoft Corporation(,|$)') {
throw 'FSLogixAppsSetup.exe must have a valid Microsoft Corporation Authenticode signature.'
}
$logRoot = Join-Path $env:ProgramData 'SGU\FSLogix'
$logPath = Join-Path $logRoot 'install.log'
if (-not $PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Install Microsoft FSLogix Apps without restarting')) {
return
}
New-Item -ItemType Directory -Path $logRoot -Force | Out-Null
$process = Start-Process -FilePath $resolvedInstaller `
-ArgumentList @('/install', '/quiet', '/norestart', '/log', "`"$logPath`"") `
-Wait -PassThru -WindowStyle Hidden
if ($process.ExitCode -notin @(0, 1641, 3010)) {
throw "FSLogix installation failed with exit code $($process.ExitCode). Review $logPath."
}
$service = Get-Service -Name frxsvc -ErrorAction SilentlyContinue
if (-not $service -or -not (Test-Path -LiteralPath $frxPath -PathType Leaf)) {
throw "FSLogix installation did not create the frxsvc service and frx.exe. Review $logPath."
}
$versionOutput = @(& $frxPath version 2>&1) -join [Environment]::NewLine
[pscustomobject]@{
Installed = $true
Changed = $true
RestartRequired = $true
Service = $service.Status.ToString()
Version = $versionOutput.Trim()
LogPath = $logPath
}
+16
View File
@@ -21,6 +21,7 @@ param(
[securestring]$VpnClientCertificatePfxPassword, [securestring]$VpnClientCertificatePfxPassword,
[string]$VpnClientRootCertificatePath, [string]$VpnClientRootCertificatePath,
[string[]]$AzureNetworkPrefixes = @('10.77.0.0/16'), [string[]]$AzureNetworkPrefixes = @('10.77.0.0/16'),
[string]$FsLogixInstallerPath,
[switch]$PauseOnError, [switch]$PauseOnError,
[switch]$SkipRestart [switch]$SkipRestart
) )
@@ -551,6 +552,7 @@ $runtimeInstaller = Get-ChildItem (Join-Path $packageRoot 'payload\prerequisites
Select-Object -First 1 Select-Object -First 1
foreach ($requiredPath in @( foreach ($requiredPath in @(
(Join-Path $scriptsRoot 'Enroll-SguDomainClient.ps1'), (Join-Path $scriptsRoot 'Enroll-SguDomainClient.ps1'),
(Join-Path $scriptsRoot 'Install-SguFsLogix.ps1'),
(Join-Path $scriptsRoot 'Install-SguRustDeskClient.ps1'), (Join-Path $scriptsRoot 'Install-SguRustDeskClient.ps1'),
(Join-Path $scriptsRoot 'Register-SguClientCertificate.ps1'), (Join-Path $scriptsRoot 'Register-SguClientCertificate.ps1'),
(Join-Path $providerPublishPath 'SGU.CredentialProvider.comhost.dll'))) { (Join-Path $providerPublishPath 'SGU.CredentialProvider.comhost.dll'))) {
@@ -562,6 +564,18 @@ if (-not $runtimeInstaller) {
throw 'The offline Microsoft .NET 10 x64 runtime installer is missing from the client package.' throw 'The offline Microsoft .NET 10 x64 runtime installer is missing from the client package.'
} }
if (-not $PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Enroll with SGU server $DomainControllerIPv4Address")) { return } if (-not $PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Enroll with SGU server $DomainControllerIPv4Address")) { return }
$fsLogixResult = $null
if ($FsLogixInstallerPath) {
$fsLogixResult = & (Join-Path $scriptsRoot 'Install-SguFsLogix.ps1') `
-InstallerPath $FsLogixInstallerPath
}
elseif (Get-Service -Name frxsvc -ErrorAction SilentlyContinue) {
$fsLogixResult = [pscustomobject]@{
Installed = $true
Changed = $false
RestartRequired = $false
}
}
if ($ClientIPv4Address) { if ($ClientIPv4Address) {
if (-not $NetworkInterfaceAlias -or $ConnectivityMode -eq 'AzureP2S') { if (-not $NetworkInterfaceAlias -or $ConnectivityMode -eq 'AzureP2S') {
throw 'Explicit static IP setup requires -NetworkInterfaceAlias with Direct connectivity. Omit -ClientIPv4Address to preserve the current LAN/VPN configuration.' throw 'Explicit static IP setup requires -NetworkInterfaceAlias with Direct connectivity. Omit -ClientIPv4Address to preserve the current LAN/VPN configuration.'
@@ -1004,6 +1018,8 @@ if ($SkipRestart) {
ConnectivityMode = $ConnectivityMode ConnectivityMode = $ConnectivityMode
CompatibilityProfile = $CompatibilityProfile CompatibilityProfile = $CompatibilityProfile
VpnConnectionName = if ($ConnectivityMode -eq 'AzureP2S') { $VpnConnectionName } else { $null } VpnConnectionName = if ($ConnectivityMode -eq 'AzureP2S') { $VpnConnectionName } else { $null }
FsLogixInstalled = [bool]($fsLogixResult -and $fsLogixResult.Installed)
FsLogixChanged = [bool]($fsLogixResult -and $fsLogixResult.Changed)
RestartRequired = $true RestartRequired = $true
RustDesk = if ($result) { $result.RustDesk } else { $null } RustDesk = if ($result) { $result.RustDesk } else { $null }
EnrollmentResult = $result EnrollmentResult = $result
+11
View File
@@ -123,6 +123,7 @@ $clientScripts = @(
'Enable-SguClientMonitoring.ps1', 'Enable-SguClientMonitoring.ps1',
'Enroll-SguDomainClient.ps1', 'Enroll-SguDomainClient.ps1',
'Install-CredentialProvider.ps1', 'Install-CredentialProvider.ps1',
'Install-SguFsLogix.ps1',
'Install-SguEnrollmentGuard.ps1', 'Install-SguEnrollmentGuard.ps1',
'Install-SguRustDeskClient.ps1', 'Install-SguRustDeskClient.ps1',
'Register-SguClientCertificate.ps1', 'Register-SguClientCertificate.ps1',
@@ -195,10 +196,13 @@ Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguAzureServerBootstra
-Destination (Join-Path $serverRoot 'Start-SguAzureServerBootstrap.cmd') -Destination (Join-Path $serverRoot 'Start-SguAzureServerBootstrap.cmd')
$serverScripts = @( $serverScripts = @(
'Deploy-AuthBroker.ps1', 'Deploy-AuthBroker.ps1',
'Disable-SguAzureUserRoaming.ps1',
'Enable-SguAzureUserRoaming.ps1',
'Enable-SguServerRemoteManagement.ps1', 'Enable-SguServerRemoteManagement.ps1',
'Get-SguUsageReport.ps1', 'Get-SguUsageReport.ps1',
'Get-SguBrokerLog.ps1', 'Get-SguBrokerLog.ps1',
'Install-SguDomainMonitoring.ps1', 'Install-SguDomainMonitoring.ps1',
'Publish-SguFsLogixClientDeployment.ps1',
'Install-SguRustDeskClient.ps1', 'Install-SguRustDeskClient.ps1',
'Install-SguRustDeskLinuxEnrollment.ps1', 'Install-SguRustDeskLinuxEnrollment.ps1',
'Install-SguRustDeskServer.ps1', 'Install-SguRustDeskServer.ps1',
@@ -210,6 +214,7 @@ $serverScripts = @(
'Register-SguRustDeskDevice.ps1', 'Register-SguRustDeskDevice.ps1',
'Set-LabBrokerDns.ps1', 'Set-LabBrokerDns.ps1',
'Set-SguDomainComputerPolicies.ps1', 'Set-SguDomainComputerPolicies.ps1',
'Set-SguLaboratorioWallpaperPolicy.ps1',
'Set-SguDomainUserPolicies.ps1' 'Set-SguDomainUserPolicies.ps1'
) )
foreach ($scriptName in $serverScripts) { foreach ($scriptName in $serverScripts) {
@@ -237,6 +242,10 @@ foreach ($fontName in $welcomeFontNames) {
Copy-RequiredFile -Source (Join-Path $repositoryRoot "assets\branding\fonts\$fontName") ` Copy-RequiredFile -Source (Join-Path $repositoryRoot "assets\branding\fonts\$fontName") `
-Destination (Join-Path $serverContentTarget "welcome-wallpaper\fonts\$fontName") -Destination (Join-Path $serverContentTarget "welcome-wallpaper\fonts\$fontName")
} }
foreach ($documentation in @('user-roaming.md', 'laboratorio-wallpaper-policy.md')) {
Copy-RequiredFile -Source (Join-Path $repositoryRoot "docs\$documentation") `
-Destination (Join-Path $serverRoot "docs\$documentation")
}
Write-PackageManifest -PackageRoot $serverRoot -PackageVersion $Version -PackageKind Server Write-PackageManifest -PackageRoot $serverRoot -PackageVersion $Version -PackageKind Server
Compress-Archive -Path (Join-Path $serverRoot '*') -DestinationPath $serverZip ` Compress-Archive -Path (Join-Path $serverRoot '*') -DestinationPath $serverZip `
-CompressionLevel Optimal -CompressionLevel Optimal
@@ -258,6 +267,8 @@ foreach ($scriptName in @(
} }
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'docs\azure-vpn-deployment.md') ` Copy-RequiredFile -Source (Join-Path $repositoryRoot 'docs\azure-vpn-deployment.md') `
-Destination (Join-Path $azureRoot 'README.md') -Destination (Join-Path $azureRoot 'README.md')
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'docs\user-roaming.md') `
-Destination (Join-Path $azureRoot 'user-roaming.md')
Write-PackageManifest -PackageRoot $azureRoot -PackageVersion $Version -PackageKind AzureInfrastructure Write-PackageManifest -PackageRoot $azureRoot -PackageVersion $Version -PackageKind AzureInfrastructure
Compress-Archive -Path (Join-Path $azureRoot '*') -DestinationPath $azureZip ` Compress-Archive -Path (Join-Path $azureRoot '*') -DestinationPath $azureZip `
-CompressionLevel Optimal -CompressionLevel Optimal
+7
View File
@@ -9,10 +9,14 @@ param(
[string]$Owner = 'alexrg', [string]$Owner = 'alexrg',
[string]$Repository = 'SGU-CredentialProvider', [string]$Repository = 'SGU-CredentialProvider',
[string]$TargetCommitish = 'main', [string]$TargetCommitish = 'main',
[string]$ReleaseNotesPath,
[switch]$Draft [switch]$Draft
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
if ($ReleaseNotesPath -and -not (Test-Path -LiteralPath $ReleaseNotesPath -PathType Leaf)) {
throw "Release notes file is missing: $ReleaseNotesPath"
}
$tagName = "v$Version" $tagName = "v$Version"
$assetPaths = @( $assetPaths = @(
(Join-Path $ReleaseDirectory "sgu-windows-client-bootstrap-$Version.zip"), (Join-Path $ReleaseDirectory "sgu-windows-client-bootstrap-$Version.zip"),
@@ -131,6 +135,9 @@ Bootstrap reproducible para el laboratorio SGU.
Las contraseñas se solicitan de forma interactiva y no se escriben en archivos ni en la línea de comandos. Verifique los ZIP con `SHA256SUMS-$Version.txt`. Las contraseñas se solicitan de forma interactiva y no se escriben en archivos ni en la línea de comandos. Verifique los ZIP con `SHA256SUMS-$Version.txt`.
"@ "@
if ($ReleaseNotesPath) {
$releaseNotes = Get-Content -LiteralPath $ReleaseNotesPath -Raw -Encoding UTF8
}
$releaseBody = [ordered]@{ $releaseBody = [ordered]@{
tag_name = $tagName tag_name = $tagName
target_commitish = $TargetCommitish target_commitish = $TargetCommitish
@@ -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'
}
+13 -1
View File
@@ -79,6 +79,8 @@ $eventForwardingPolicyKey = 'HKLM\Software\Policies\Microsoft\Windows\EventLog\E
$auditPolicyKey = 'HKLM\System\CurrentControlSet\Control\Lsa' $auditPolicyKey = 'HKLM\System\CurrentControlSet\Control\Lsa'
$runPolicyKey = 'HKLM\Software\Microsoft\Windows\CurrentVersion\Run' $runPolicyKey = 'HKLM\Software\Microsoft\Windows\CurrentVersion\Run'
$personalizationPolicyKey = 'HKLM\Software\Policies\Microsoft\Windows\Personalization' $personalizationPolicyKey = 'HKLM\Software\Policies\Microsoft\Windows\Personalization'
$personalizationCspKey = 'HKLM\Software\Microsoft\Windows\CurrentVersion\PersonalizationCSP'
$cloudContentPolicyKey = 'HKLM\Software\Policies\Microsoft\Windows\CloudContent'
$providerClassId = '{D789CFD8-5AD4-489F-9B83-7EB5D9D09335}' $providerClassId = '{D789CFD8-5AD4-489F-9B83-7EB5D9D09335}'
$welcomeWallpaperCommand = 'powershell.exe -NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "{0}" -BaseImagePath "{1}"' -f ` $welcomeWallpaperCommand = 'powershell.exe -NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "{0}" -BaseImagePath "{1}"' -f `
$WelcomeWallpaperScriptPath,$WelcomeWallpaperBasePath $WelcomeWallpaperScriptPath,$WelcomeWallpaperBasePath
@@ -97,6 +99,7 @@ $policies = @(
@{ Key = $credentialProviderPolicyKey; Name = 'DefaultCredentialProvider'; Type = 'String'; Value = $providerClassId }, @{ Key = $credentialProviderPolicyKey; Name = 'DefaultCredentialProvider'; Type = 'String'; Value = $providerClassId },
@{ Key = $credentialProviderPolicyKey; Name = 'EnumerateLocalUsers'; Type = 'DWord'; Value = 0 }, @{ Key = $credentialProviderPolicyKey; Name = 'EnumerateLocalUsers'; Type = 'DWord'; Value = 0 },
@{ Key = $interactiveLogonPolicyKey; Name = 'DontDisplayLastUserName'; Type = 'DWord'; Value = 1 }, @{ 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 # Use Windows' native default account image for named user tiles. LogonUI
# retains ownership of the anonymous Other user tile and its circular mask. # retains ownership of the anonymous Other user tile and its circular mask.
@@ -111,7 +114,16 @@ $policies = @(
# local payload lets the first desktop render without depending on SMB. # local payload lets the first desktop render without depending on SMB.
@{ Key = $runPolicyKey; Name = 'SGUWelcomeWallpaper'; Type = 'String'; Value = $welcomeWallpaperCommand }, @{ Key = $runPolicyKey; Name = 'SGUWelcomeWallpaper'; Type = 'String'; Value = $welcomeWallpaperCommand },
@{ Key = $personalizationPolicyKey; Name = 'LockScreenImage'; Type = 'String'; Value = $WelcomeWallpaperBasePath }, @{ Key = $personalizationPolicyKey; Name = 'LockScreenImage'; Type = 'String'; Value = $WelcomeWallpaperBasePath },
@{ Key = $personalizationPolicyKey; Name = 'NoChangingLockScreen'; Type = 'DWord'; Value = 1 } @{ 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 = 'LockScreenImageStatus'; Type = 'DWord'; Value = 1 },
@{ Key = $cloudContentPolicyKey; Name = 'DisableWindowsSpotlightFeatures'; Type = 'DWord'; Value = 1 },
@{ Key = $cloudContentPolicyKey; Name = 'DisableWindowsSpotlightOnLockScreen'; Type = 'DWord'; Value = 1 }
) )
$powerSettingIds = @( $powerSettingIds = @(
@@ -0,0 +1,75 @@
#Requires -Version 5.1
[CmdletBinding(SupportsShouldProcess)]
param(
[string]$TargetOuDn = 'OU=Laboratorio,DC=lci,DC=lasalle,DC=mx',
[string]$GpoName = 'SGU - Laboratorio wallpaper protection',
[string]$DomainController = $env:COMPUTERNAME,
[string]$WallpaperPath = '%LOCALAPPDATA%\SGU\Wallpapers\welcome-%COMPUTERNAME%.jpg'
)
$ErrorActionPreference = 'Stop'
Import-Module ActiveDirectory -ErrorAction Stop
Import-Module GroupPolicy -ErrorAction Stop
$domainName = (Get-ADDomain -Server $DomainController).DNSRoot
Get-ADOrganizationalUnit -Identity $TargetOuDn -Server $DomainController -ErrorAction Stop | Out-Null
if (-not $PSCmdlet.ShouldProcess($TargetOuDn, "Apply '$GpoName' with enforced loopback Merge")) {
return
}
$backupPath = $null
$gpo = Get-GPO -Name $GpoName -Domain $domainName -Server $DomainController -ErrorAction SilentlyContinue
if ($gpo) {
$backupPath = Join-Path $env:ProgramData ('SGU\PolicyBackups\Wallpaper-' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $backupPath -Force | Out-Null
Backup-GPO -Guid $gpo.Id -Path $backupPath -Domain $domainName -Server $DomainController | Out-Null
}
else {
$gpo = New-GPO -Name $GpoName -Domain $domainName -Server $DomainController `
-Comment 'Protects the SGU desktop wallpaper on Laboratorio computers and child OUs; loopback Merge preserves existing user policies.'
}
# These are user policies scoped by the computer OU, not by the user OU.
# Match the per-user/per-computer output of Set-SguWelcomeWallpaper.ps1.
$settings = @(
@{ Key = 'HKLM\Software\Policies\Microsoft\Windows\System'; Name = 'UserPolicyMode'; Type = 'DWord'; Value = 1 },
@{ Key = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\ActiveDesktop'; Name = 'NoChangingWallPaper'; Type = 'DWord'; Value = 1 },
@{ Key = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System'; Name = 'Wallpaper'; Type = 'ExpandString'; Value = $WallpaperPath },
@{ Key = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System'; Name = 'WallpaperStyle'; Type = 'String'; Value = '10' }
)
foreach ($setting in $settings) {
Set-GPRegistryValue -Guid $gpo.Id -Domain $domainName -Server $DomainController `
-Key $setting.Key -ValueName $setting.Name -Type $setting.Type -Value $setting.Value | Out-Null
}
$linkParameters = @{
Guid = $gpo.Id
Target = $TargetOuDn
Domain = $domainName
Server = $DomainController
LinkEnabled = 'Yes'
Enforced = 'Yes'
Order = 1
}
$existingLink = (Get-GPInheritance -Target $TargetOuDn -Domain $domainName -Server $DomainController).GpoLinks |
Where-Object GpoId -eq $gpo.Id
if ($existingLink) {
Set-GPLink @linkParameters | Out-Null
}
else {
New-GPLink @linkParameters | Out-Null
}
foreach ($setting in $settings) {
$actual = Get-GPRegistryValue -Guid $gpo.Id -Domain $domainName -Server $DomainController `
-Key $setting.Key -ValueName $setting.Name
if ([string]$actual.Value -ne [string]$setting.Value -or [string]$actual.Type -ne $setting.Type) {
throw "Wallpaper policy verification failed for $($setting.Name)."
}
}
[pscustomobject]@{
Name = $gpo.DisplayName
Id = $gpo.Id
TargetOuDn = $TargetOuDn
WallpaperPath = $WallpaperPath
Loopback = 'Merge'
Enforced = $true
BackupPath = $backupPath
}
+32 -8
View File
@@ -170,10 +170,10 @@ function Get-DirectoryWelcomeMetadata {
function Get-SpanishArticle { function Get-SpanishArticle {
param([Parameter(Mandatory)][string]$Value) param([Parameter(Mandatory)][string]$Value)
if ($Value -match '^(Sala|Aula|Facultad|Unidad|Biblioteca|Oficina|Coordinaci.n)\b') { if ($Value -match '^(Sala|Facultad|Unidad|Biblioteca|Oficina|Coordinaci.n)\b') {
return 'la' return 'la'
} }
if ($Value -match '^(Laboratorio|Centro|Edificio|Campus|Taller|Auditorio)\b') { if ($Value -match '^(Aula|Laboratorio|Centro|Edificio|Campus|Taller|Auditorio)\b') {
return 'el' return 'el'
} }
return $null return $null
@@ -202,9 +202,17 @@ function Get-WelcomeLocationText {
if ($Room -and $OuName) { if ($Room -and $OuName) {
$roomArticle = Get-SpanishArticle -Value $Room $roomArticle = Get-SpanishArticle -Value $Room
$ouArticle = Get-SpanishArticle -Value $OuName $ouArticle = Get-SpanishArticle -Value $OuName
$roomPhrase = if ($roomArticle) { "$roomArticle $Room" } else { $Room } $roomPhrase = if ($roomArticle -eq 'el') {
"al $Room"
}
elseif ($roomArticle) {
"a $roomArticle $Room"
}
else {
"a $Room"
}
$ouPhrase = if ($ouArticle -eq 'el') { "del $OuName" } elseif ($ouArticle) { "de $ouArticle $OuName" } else { "de $OuName" } $ouPhrase = if ($ouArticle -eq 'el') { "del $OuName" } elseif ($ouArticle) { "de $ouArticle $OuName" } else { "de $OuName" }
return "$located $roomPhrase $ouPhrase." return "Acceso $roomPhrase $ouPhrase."
} }
if ($Room) { if ($Room) {
$article = Get-SpanishArticle -Value $Room $article = Get-SpanishArticle -Value $Room
@@ -297,6 +305,14 @@ if (-not (Test-Path -LiteralPath $BaseImagePath -PathType Leaf)) {
$userName = [Environment]::UserName $userName = [Environment]::UserName
$metadata = $null $metadata = $null
$machineLocation = [Environment]::GetEnvironmentVariable('SGU_WELCOME_LOCATION', 'Machine')
$machineOrganizationalUnit = [Environment]::GetEnvironmentVariable(
'SGU_WELCOME_ORGANIZATIONAL_UNIT',
'Machine')
if (-not $machineLocation) { $machineLocation = $env:SGU_WELCOME_LOCATION }
if (-not $machineOrganizationalUnit) {
$machineOrganizationalUnit = $env:SGU_WELCOME_ORGANIZATIONAL_UNIT
}
if (-not $SkipDirectoryLookup) { if (-not $SkipDirectoryLookup) {
try { try {
$metadata = Get-DirectoryWelcomeMetadata -UserName $userName -MachineName $ComputerName $metadata = Get-DirectoryWelcomeMetadata -UserName $userName -MachineName $ComputerName
@@ -312,8 +328,8 @@ if (-not $PSBoundParameters.ContainsKey('DisplayName')) {
if (-not $DisplayName) { if (-not $DisplayName) {
$DisplayName = $userName $DisplayName = $userName
} }
if (-not $PSBoundParameters.ContainsKey('Location') -and $metadata) { if (-not $PSBoundParameters.ContainsKey('Location')) {
$Location = $metadata.Location $Location = if ($machineLocation) { $machineLocation } elseif ($metadata) { $metadata.Location } else { $null }
} }
$genderWasProvided = $PSBoundParameters.ContainsKey('Gender') $genderWasProvided = $PSBoundParameters.ContainsKey('Gender')
if (-not $genderWasProvided -and $metadata -and $metadata.Gender -in @('Male', 'Female')) { if (-not $genderWasProvided -and $metadata -and $metadata.Gender -in @('Male', 'Female')) {
@@ -322,8 +338,16 @@ if (-not $genderWasProvided -and $metadata -and $metadata.Gender -in @('Male', '
$Gender = $metadata.Gender $Gender = $metadata.Gender
} }
$welcomeHeading = Get-WelcomeHeading -Gender $Gender $welcomeHeading = Get-WelcomeHeading -Gender $Gender
if (-not $PSBoundParameters.ContainsKey('OrganizationalUnit') -and $metadata) { if (-not $PSBoundParameters.ContainsKey('OrganizationalUnit')) {
$OrganizationalUnit = $metadata.OrganizationalUnit $OrganizationalUnit = if ($machineOrganizationalUnit) {
$machineOrganizationalUnit
}
elseif ($metadata) {
$metadata.OrganizationalUnit
}
else {
$null
}
} }
$locationText = Get-WelcomeLocationText -Room $Location -OuName $OrganizationalUnit -Gender $Gender $locationText = Get-WelcomeLocationText -Room $Location -OuName $OrganizationalUnit -Gender $Gender
+2 -1
View File
@@ -4,7 +4,8 @@ set "SGU_BOOTSTRAP_IP=%~1"
set "SGU_VPN_PACKAGE=%~2" set "SGU_VPN_PACKAGE=%~2"
set "SGU_VPN_PFX=%~3" set "SGU_VPN_PFX=%~3"
set "SGU_VPN_ROOT=%~4" set "SGU_VPN_ROOT=%~4"
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "$script = Join-Path '%~dp0' 'Invoke-SguClientBootstrap.ps1'; $arguments = @('-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',('"' + $script + '"'),'-PauseOnError','-ConnectivityMode','AzureP2S'); if ($env:SGU_BOOTSTRAP_IP) { $arguments += @('-DomainControllerIPv4Address',('"' + $env:SGU_BOOTSTRAP_IP + '"')) }; if ($env:SGU_VPN_PACKAGE) { $arguments += @('-VpnProfilePackagePath',('"' + [IO.Path]::GetFullPath($env:SGU_VPN_PACKAGE) + '"')) }; if ($env:SGU_VPN_PFX) { $arguments += @('-VpnClientCertificatePfxPath',('"' + [IO.Path]::GetFullPath($env:SGU_VPN_PFX) + '"')) }; if ($env:SGU_VPN_ROOT) { $arguments += @('-VpnClientRootCertificatePath',('"' + [IO.Path]::GetFullPath($env:SGU_VPN_ROOT) + '"')) }; $process = Start-Process -FilePath powershell.exe -Verb RunAs -ArgumentList $arguments -Wait -PassThru; exit $process.ExitCode" set "SGU_FSLOGIX_INSTALLER=%~5"
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "$script = Join-Path '%~dp0' 'Invoke-SguClientBootstrap.ps1'; $arguments = @('-NoLogo','-NoProfile','-ExecutionPolicy','Bypass','-File',('"' + $script + '"'),'-PauseOnError','-ConnectivityMode','AzureP2S'); if ($env:SGU_BOOTSTRAP_IP) { $arguments += @('-DomainControllerIPv4Address',('"' + $env:SGU_BOOTSTRAP_IP + '"')) }; if ($env:SGU_VPN_PACKAGE) { $arguments += @('-VpnProfilePackagePath',('"' + [IO.Path]::GetFullPath($env:SGU_VPN_PACKAGE) + '"')) }; if ($env:SGU_VPN_PFX) { $arguments += @('-VpnClientCertificatePfxPath',('"' + [IO.Path]::GetFullPath($env:SGU_VPN_PFX) + '"')) }; if ($env:SGU_VPN_ROOT) { $arguments += @('-VpnClientRootCertificatePath',('"' + [IO.Path]::GetFullPath($env:SGU_VPN_ROOT) + '"')) }; if ($env:SGU_FSLOGIX_INSTALLER) { $arguments += @('-FsLogixInstallerPath',('"' + [IO.Path]::GetFullPath($env:SGU_FSLOGIX_INSTALLER) + '"')) }; $process = Start-Process -FilePath powershell.exe -Verb RunAs -ArgumentList $arguments -Wait -PassThru; exit $process.ExitCode"
set "SGU_EXIT_CODE=%errorlevel%" set "SGU_EXIT_CODE=%errorlevel%"
if not "%SGU_EXIT_CODE%"=="0" ( if not "%SGU_EXIT_CODE%"=="0" (
echo. echo.
+15
View File
@@ -92,6 +92,20 @@ if (-not $lastSignedInUserHidden) {
$issues.Add('The last signed-in user is not hidden from LogonUI.') $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 $localUserEnumerationDisabled = $false
try { try {
$localUserEnumerationDisabled = (Get-ItemPropertyValue ` $localUserEnumerationDisabled = (Get-ItemPropertyValue `
@@ -279,6 +293,7 @@ $result = [pscustomobject]@{
ProviderBinaryPresent = [bool]$providerBinaryPresent ProviderBinaryPresent = [bool]$providerBinaryPresent
DefaultProviderConfigured = $defaultProviderConfigured DefaultProviderConfigured = $defaultProviderConfigured
LastSignedInUserHidden = $lastSignedInUserHidden LastSignedInUserHidden = $lastSignedInUserHidden
FastUserSwitchingHidden = $fastUserSwitchingHidden
LocalUserEnumerationDisabled = $localUserEnumerationDisabled LocalUserEnumerationDisabled = $localUserEnumerationDisabled
PasswordProviderPreserved = $passwordProviderPreserved PasswordProviderPreserved = $passwordProviderPreserved
StandardLocalUserPresent = $standardLocalUserPresent StandardLocalUserPresent = $standardLocalUserPresent
+11
View File
@@ -4,6 +4,8 @@ $enrollmentTestScriptPath = Join-Path $repositoryRoot 'scripts\Test-SguClientEnr
$packageScriptPath = Join-Path $repositoryRoot 'scripts\New-SguBootstrapPackages.ps1' $packageScriptPath = Join-Path $repositoryRoot 'scripts\New-SguBootstrapPackages.ps1'
$releaseScriptPath = Join-Path $repositoryRoot 'scripts\Publish-GiteaRelease.ps1' $releaseScriptPath = Join-Path $repositoryRoot 'scripts\Publish-GiteaRelease.ps1'
$azureLauncherPath = Join-Path $repositoryRoot 'scripts\Start-SguAzureClientEnrollment.cmd' $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 $tokens = $null
$parseErrors = $null $parseErrors = $null
@@ -72,4 +74,13 @@ Describe 'SGU Windows client enrollment scripts' {
Should Be $true Should Be $true
$azureLauncher | Should Match '-PauseOnError' $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"
}
} }
@@ -0,0 +1,37 @@
$repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
$policyPath = Join-Path $repositoryRoot 'scripts\Set-SguLaboratorioWallpaperPolicy.ps1'
$policySource = Get-Content -LiteralPath $policyPath -Raw
Describe 'Laboratorio wallpaper policy packaging and scope' {
It 'parses in Windows PowerShell and supports a read-only preview' {
$tokens = $null
$parseErrors = $null
[Management.Automation.Language.Parser]::ParseFile($policyPath, [ref]$tokens, [ref]$parseErrors) | Out-Null
$parseErrors.Count | Should Be 0
$policySource | Should Match 'SupportsShouldProcess'
$policySource | Should Match 'if \(-not \$PSCmdlet.ShouldProcess'
}
It 'scopes user settings through computer loopback Merge' {
$policySource | Should Match "OU=Laboratorio,DC=lci,DC=lasalle,DC=mx"
$policySource | Should Match "Name = 'UserPolicyMode'; Type = 'DWord'; Value = 1"
$policySource | Should Match "Enforced = 'Yes'"
$policySource | Should Match "LinkEnabled = 'Yes'"
}
It 'locks the generated per-user and per-computer wallpaper path' {
$policySource | Should Match "Name = 'NoChangingWallPaper'; Type = 'DWord'; Value = 1"
$policySource | Should Match ([regex]::Escape('%LOCALAPPDATA%\SGU\Wallpapers\welcome-%COMPUTERNAME%.jpg'))
$policySource | Should Match "Name = 'Wallpaper'; Type = 'ExpandString'"
}
It 'reuses and backs up an existing GPO and verifies the result' {
foreach ($command in @('Get-GPO', 'Backup-GPO', 'Set-GPLink', 'New-GPLink', 'Get-GPRegistryValue')) {
$policySource | Should Match ([regex]::Escape($command))
}
$policySource | Should Match 'Wallpaper policy verification failed'
}
It 'ships in the server package and runs in server initialization' {
foreach ($path in @('scripts\New-SguBootstrapPackages.ps1', 'scripts\Initialize-SguDomainController.ps1')) {
(Get-Content (Join-Path $repositoryRoot $path) -Raw) |
Should Match 'Set-SguLaboratorioWallpaperPolicy.ps1'
}
}
}
+229
View File
@@ -0,0 +1,229 @@
$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'
foreach ($scriptPath in @(
$deploymentPath,
$configurationPath,
$rollbackPath,
$installerPath,
$gpoDeploymentPath,
$clientBootstrapPath,
$packagePath)) {
$tokens = $null
$parseErrors = $null
[Management.Automation.Language.Parser]::ParseFile(
$scriptPath,
[ref]$tokens,
[ref]$parseErrors) | Out-Null
if ($parseErrors.Count -gt 0) {
throw "$scriptPath contains PowerShell parser errors: $($parseErrors -join '; ')"
}
}
$configurationTokens = $null
$configurationParseErrors = $null
$configurationAst = [Management.Automation.Language.Parser]::ParseFile(
$configurationPath,
[ref]$configurationTokens,
[ref]$configurationParseErrors)
$samFunction = $configurationAst.Find({
param($node)
$node -is [Management.Automation.Language.FunctionDefinitionAst] -and
$node.Name -eq 'Get-SguStorageSamAccountName'
}, $true)
Invoke-Expression $samFunction.Extent.Text
Describe 'SGU Azure user-roaming infrastructure' {
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 = false'
$source | Should Match "purpose: 'SGU-user-roaming'"
$source | Should Match "publicNetworkAccess: 'Disabled'"
$source | Should Match "privateLinkServiceId: userRoamingStorageAccount.id"
$source | Should Match "'file'"
$source | Should Match "privatelink\.file\.\$\{storageEndpointSuffix\}"
$source | Should Match 'dhcpOptions:'
$source | Should Match 'dnsServers:'
$source | Should Match 'domainControllerPrivateIp'
$source | Should Match 'fsLogixProfilesShare'
$source | Should Match 'redirectedFoldersShare'
}
It 'does not repurpose the temporary bootstrap staging account' {
(Get-Content -LiteralPath $bicepPath -Raw) | Should Not Match 'sgustage|SGU-bootstrap-staging'
}
It 'requires a private P2S route when roaming is enabled' {
$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' {
$source = Get-Content -LiteralPath $deploymentPath -Raw
foreach ($name in @(
'UserRoamingEnabled',
'UserRoamingStorageAccountName',
'FsLogixProfilesSharePath',
'RedirectedFoldersSharePath',
'UserRoamingSetupArguments')) {
$source | Should Match $name
}
}
}
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'"
$source | Should Match 'KerberosEncryptionType AES256'
$source | Should Match 'PasswordNeverExpires \$true'
$source | Should Match 'StorageFileDataSmbShareContributor'
}
It 'isolates per-user directories with CREATOR OWNER ACLs' {
$source = Get-Content -LiteralPath $configurationPath -Raw
$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' {
$source = Get-Content -LiteralPath $configurationPath -Raw
$source | Should Match "ValueName 'Desktop'"
$source | Should Match "ValueName 'Personal'"
$source | Should Match '%USERNAME%\\Desktop'
$source | Should Match '%USERNAME%\\Documents'
$source | Should Match "ValueName 'DisableFRAdminPin'"
}
It 'enables FSLogix only through the AD and DO group SIDs' {
$source = Get-Content -LiteralPath $configurationPath -Raw
$source | Should Match "\$fsLogixRoot = 'HKLM\\SOFTWARE\\FSLogix\\Profiles'"
$source | Should Match '\\ObjectSpecific\\\$\(\$staffGroup\.SID\.Value\)'
$source | Should Match '\$professorGroup, \$administrativeGroup'
$source | Should Match "ValueName 'Enabled' -Type DWord -Value 0"
$source | Should Match 'VHDLocations = @\{ Type = ''String''; Value = \$profilesSharePath \}'
}
It 'does not delete existing staff profiles unless explicitly requested' {
$source = Get-Content -LiteralPath $configurationPath -Raw
$source | Should Match '\[switch\]\$DeleteExistingStaffLocalProfiles'
$source | Should Match 'if \(\$DeleteExistingStaffLocalProfiles\) \{ 1 \} else \{ 0 \}'
}
It 'derives valid deterministic AD names for long storage account names' {
$name = Get-SguStorageSamAccountName -StorageName 'abcdefghijklmnopqrstuvwx'
$name.Length | Should Be 15
$name | Should Match '^sgufs[0-9a-f]{10}$'
(Get-SguStorageSamAccountName -StorageName 'abcdefghijklmnopqrstuvwx') | Should Be $name
(Get-SguStorageSamAccountName -StorageName 'sguroam1234567') |
Should Be 'sguroam1234567'
}
}
Describe 'SGU FSLogix image enrollment' {
It 'verifies the Microsoft signature and installs unattended' {
$source = Get-Content -LiteralPath $installerPath -Raw
$source | Should Match 'Get-AuthenticodeSignature'
$source | Should Match 'CN=Microsoft Corporation'
foreach ($argument in @('/install', '/quiet', '/norestart')) {
$source | Should Match ([regex]::Escape($argument))
}
$source | Should Match "Get-Service -Name frxsvc"
}
It 'exposes the optional installer through the unified Azure launcher' {
((Get-Command $clientBootstrapPath).Parameters.Keys -contains
'FsLogixInstallerPath') | Should Be $true
$launcher = Get-Content -LiteralPath $azureLauncherPath -Raw
$launcher | Should Match 'SGU_FSLOGIX_INSTALLER=%~5'
$launcher | Should Match '-FsLogixInstallerPath'
}
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'
}
}
+35 -4
View File
@@ -12,15 +12,22 @@ $lookup = $ast.Find({
}, $true) }, $true)
function Invoke-WelcomeFixture { function Invoke-WelcomeFixture {
param($DirectoryGender, [string]$ExplicitGender) param(
$DirectoryGender,
[string]$ExplicitGender,
[string]$DirectoryLocation = 'Sala de pruebas',
[string]$DirectoryOu = 'Laboratorio',
[string]$MachineLocation,
[string]$MachineOu
)
# Replace only the external directory lookup. Execute the actual script, # Replace only the external directory lookup. Execute the actual script,
# including its validated parameters, metadata assignment and JPEG renderer. # including its validated parameters, metadata assignment and JPEG renderer.
$fixtureJson = [pscustomobject]@{ $fixtureJson = [pscustomobject]@{
DisplayName = 'Usuario de prueba' DisplayName = 'Usuario de prueba'
Gender = $DirectoryGender Gender = $DirectoryGender
Location = 'Sala de pruebas' Location = $DirectoryLocation
OrganizationalUnit = 'Laboratorio' OrganizationalUnit = $DirectoryOu
} | ConvertTo-Json -Compress } | ConvertTo-Json -Compress
$fixtureFunction = 'function Get-DirectoryWelcomeMetadata { param($UserName, $MachineName); ConvertFrom-Json ''' + $fixtureFunction = 'function Get-DirectoryWelcomeMetadata { param($UserName, $MachineName); ConvertFrom-Json ''' +
$fixtureJson.Replace("'", "''") + ''' }' $fixtureJson.Replace("'", "''") + ''' }'
@@ -38,11 +45,19 @@ function Invoke-WelcomeFixture {
} }
if ($ExplicitGender) { $parameters.Gender = $ExplicitGender } if ($ExplicitGender) { $parameters.Gender = $ExplicitGender }
$previousLocalAppData = $env:LOCALAPPDATA $previousLocalAppData = $env:LOCALAPPDATA
$previousMachineLocation = $env:SGU_WELCOME_LOCATION
$previousMachineOu = $env:SGU_WELCOME_ORGANIZATIONAL_UNIT
try { try {
$env:LOCALAPPDATA = $TestDrive $env:LOCALAPPDATA = $TestDrive
$env:SGU_WELCOME_LOCATION = $MachineLocation
$env:SGU_WELCOME_ORGANIZATIONAL_UNIT = $MachineOu
& $testScript @parameters & $testScript @parameters
} }
finally { $env:LOCALAPPDATA = $previousLocalAppData } finally {
$env:LOCALAPPDATA = $previousLocalAppData
$env:SGU_WELCOME_LOCATION = $previousMachineLocation
$env:SGU_WELCOME_ORGANIZATIONAL_UNIT = $previousMachineOu
}
} }
Describe 'Welcome wallpaper with AD metadata' { Describe 'Welcome wallpaper with AD metadata' {
@@ -69,4 +84,20 @@ Describe 'Welcome wallpaper with AD metadata' {
It 'honors an explicit gender over directory metadata' { It 'honors an explicit gender over directory metadata' {
(Invoke-WelcomeFixture -DirectoryGender 'Female' -ExplicitGender 'Male').WelcomeHeading | Should Be 'Bienvenido,' (Invoke-WelcomeFixture -DirectoryGender 'Female' -ExplicitGender 'Male').WelcomeHeading | Should Be 'Bienvenido,'
} }
It 'renders the flexible classroom and immediate OU as an access label' {
$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.'
}
It 'keeps machine location metadata when the GPO command has no location arguments' {
$result = Invoke-WelcomeFixture -DirectoryGender $null `
-DirectoryLocation 'Sala de pruebas' `
-DirectoryOu 'Laboratorio' `
-MachineLocation 'Aula Flexible' `
-MachineOu 'Centro de Experiencia Digital'
$result.LocationText | Should Be 'Acceso al Aula Flexible del Centro de Experiencia Digital.'
}
} }