From 106be26e7cba419269e2ecdf83c2b3f7ff08aa81 Mon Sep 17 00:00:00 2001 From: Alejandro Rosales Date: Fri, 18 Sep 2026 12:06:15 -0600 Subject: [PATCH] Default all users to local Windows profiles --- README.md | 13 +- docs/architecture.md | 24 ++- docs/azure-vpn-deployment.md | 21 +- docs/release-0.6.4.md | 27 +++ docs/user-roaming.md | 243 ++++++++++------------ infra/azure/main.bicep | 2 +- scripts/Deploy-SguAzureInfrastructure.ps1 | 6 +- scripts/Disable-SguAzureUserRoaming.ps1 | 210 +++++++++++++++++++ scripts/Enable-SguAzureUserRoaming.ps1 | 27 ++- scripts/New-SguBootstrapPackages.ps1 | 1 + tests/UserRoaming.Tests.ps1 | 35 +++- 11 files changed, 442 insertions(+), 167 deletions(-) create mode 100644 docs/release-0.6.4.md create mode 100644 scripts/Disable-SguAzureUserRoaming.ps1 diff --git a/README.md b/README.md index 5415285..fa2da51 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Operational documentation: - [Self-hosted RustDesk server and managed Windows remote access](docs/rustdesk-operations.md) - [Domain monitoring, usage reports, and six-month retention](docs/monitoring.md) - [Laboratorio desktop wallpaper protection](docs/laboratorio-wallpaper-policy.md) -- [Azure user roaming by role with Azure Files and FSLogix](docs/user-roaming.md) +- [Optional Azure user roaming by role with Azure Files and FSLogix](docs/user-roaming.md) - [Decision: do not persist password verifiers in Redis](docs/decisions/0001-no-password-cache.md) | Prefix | Role | Default OU | Security group in the same OU | @@ -109,11 +109,12 @@ Follow [docs/lab-runbook.md](docs/lab-runbook.md). Review For a public Azure VM, use [docs/azure-vpn-deployment.md](docs/azure-vpn-deployment.md). It supports an optional Azure P2S gateway or direct enrollment restricted to explicit public -source CIDRs. P2S deployments also provision private Azure Files storage for -role-based roaming: `AL` redirects only Documents/Desktop, while `AD` and `DO` -use an FSLogix VHDX profile after the post-domain setup step. Existing public -Azure Files deployments automatically use FSLogix Cloud Cache locally instead -of performing all profile I/O across WAN SMB. +source CIDRs. Every SGU role uses an ordinary local Windows profile by default: +Desktop and Documents stay under `C:\Users` and no profile share is mapped. +Azure Files/FSLogix roaming remains available only as an experimental, +explicit opt-in that requires acknowledgement of its login, sign-out, storage, +cache and recovery risks. See [docs/user-roaming.md](docs/user-roaming.md) for +the opt-in and rollback procedures. Never disable the built-in Microsoft password Credential Provider. It is the supported recovery path if a third-party provider fails to load. diff --git a/docs/architecture.md b/docs/architecture.md index 56be2d7..64c43b0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -78,18 +78,20 @@ is deliberately left unset because the verified page does not expose it. Missing metadata does not clear existing AD values and never changes the password outcome. -## Azure user roaming +## Optional Azure user roaming -Azure P2S deployments can add a dedicated, private Azure Files account. Student -accounts (`AL`) keep a non-authoritative local profile and redirect only -Documents/Desktop. Administrative and professor accounts (`AD`/`DO`) receive an -FSLogix VHDX profile container through object-specific settings keyed to their -AD security-group SIDs. Private endpoints mount the VHDX directly; public WAN -endpoints use a local FSLogix Cloud Cache synchronized to Azure Files so profile -I/O does not block on every SMB round trip. The storage account uses AD DS Kerberos with AES-256; -share-root ACLs use `CREATOR OWNER` so users cannot browse one another's data. -The bootstrap staging account is deliberately separate. See -[user-roaming.md](user-roaming.md) for rollout and recovery constraints. +The default architecture uses local Windows profiles for every role. Desktop, +Documents and the remainder of the profile live under `C:\Users`; enrollment +does not map a profile share and the domain GPO keeps FSLogix disabled. + +An experimental opt-in can add a dedicated Azure Files account. Student +accounts (`AL`) redirect only Documents/Desktop, while administrative and +professor accounts (`AD`/`DO`) receive an FSLogix VHDX profile container through +object-specific settings. Enabling it is never implicit: both infrastructure +deployment and policy configuration require an explicit risk acknowledgement. +The storage account uses AD DS Kerberos with AES-256 and share-root ACLs use +`CREATOR OWNER`. See [user-roaming.md](user-roaming.md) for the observed +sign-out/cache failure modes, staged rollout requirements and rollback. Every synchronization also enforces one idempotent security-group membership from the classified institutional prefix: `AL` to `SGU-Alumnos`, `AD` to diff --git a/docs/azure-vpn-deployment.md b/docs/azure-vpn-deployment.md index 01e4b8d..a3e27d9 100644 --- a/docs/azure-vpn-deployment.md +++ b/docs/azure-vpn-deployment.md @@ -14,8 +14,8 @@ La plantilla crea: - VPN Gateway opcional `VpnGw1AZ` con IKEv2/OpenVPN y autenticación por certificados; - pool P2S `172.30.0.0/24`, autorizado en los firewalls SGU; - DNS de la NIC del servidor apuntando a `10.77.0.4`; -- Azure Files privado para user roaming: `AL` conserva sólo Escritorio/Documentos - y `AD`/`DO` usa contenedores de perfil FSLogix. +- perfiles locales para todos los usuarios por omisión; Azure Files/FSLogix es + una característica opcional que debe habilitarse y aceptarse explícitamente. Los prefijos privados deben ser RFC1918 y no deben solaparse con las redes usadas por Hyper-V, el `Default Switch`, Wi-Fi o Ethernet locales. Los prefijos de @@ -84,9 +84,10 @@ pública actual: No utilice `0.0.0.0/0`. El despliegue de un VPN Gateway suele tardar bastante más que la VM; omitirlo reduce tiempo y costo. El comando espera hasta que Azure -entregue un resultado final. User roaming se crea por omisión y requiere la ruta -privada del P2S; para un despliegue de enrolamiento público directo indique -`-DeployUserRoaming $false`. +entregue un resultado final. User roaming está desactivado por omisión. Sólo se +provisiona al indicar juntos `-DeployUserRoaming $true` y +`-AcknowledgeUserRoamingRisks`, después de revisar +[sus riesgos y recuperación](user-roaming.md). ## 3. Conectarse al servidor @@ -141,10 +142,12 @@ El JSON debe indicar `NetworkConfigurationMode = PlatformManaged`, el pool P2S en `TrustedClientNetworks` cuando exista VPN y los CIDR directos en `PublicEnrollmentNetworks` cuando se hayan habilitado. -Después de terminar la promoción, una Azure Files al nuevo bosque y publique -las políticas por rol con `Enable-SguAzureUserRoaming.ps1`. Esta segunda fase -requiere Azure PowerShell y el módulo oficial AzFilesHybrid porque el SID y GUID -del bosque todavía no existen durante el despliegue Bicep. Consulte +Después de terminar la promoción no se necesita ninguna acción de perfiles: las +cuentas usarán archivos locales. Si se decide realizar un piloto explícito de +Azure Files, publique las políticas ejecutando +`Enable-SguAzureUserRoaming.ps1` con `-AcknowledgeUserRoamingRisks`. Esa fase opcional requiere Azure PowerShell y el +módulo oficial AzFilesHybrid porque el SID y GUID del bosque todavía no existen +durante el despliegue Bicep. Consulte [user roaming por rol en Azure](user-roaming.md). ## 5. Enrolar cada VM Hyper-V diff --git a/docs/release-0.6.4.md b/docs/release-0.6.4.md new file mode 100644 index 0000000..655a8c0 --- /dev/null +++ b/docs/release-0.6.4.md @@ -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. diff --git a/docs/user-roaming.md b/docs/user-roaming.md index ad9d0dc..860e46b 100644 --- a/docs/user-roaming.md +++ b/docs/user-roaming.md @@ -1,74 +1,70 @@ -# User roaming por rol en Azure +# User roaming por rol en Azure (opcional) -El despliegue Azure puede crear almacenamiento privado y políticas distintas para -cada tipo de cuenta SGU: +> **Estado predeterminado:** desactivado. Todas las cuentas `AL`, `AD`, `DO` y +> cualquier otra cuenta de dominio usan un perfil local normal bajo `C:\Users`. +> Escritorio y Documentos permanecen en el disco del equipo; no se mapean shares +> de perfiles ni se monta un contenedor FSLogix. + +La implementación se conserva para un posible piloto futuro, pero no forma parte +del enrolamiento normal. Activarla requiere dos decisiones explícitas: desplegar +la infraestructura con `-DeployUserRoaming $true +-AcknowledgeUserRoamingRisks` y después ejecutar el configurador con +`-AcknowledgeUserRoamingRisks`. + +## Diseño opcional + +Cuando se habilita expresamente, el comportamiento es: | Rol | Datos conservados | Implementación | | --- | --- | --- | -| `AL` | `Desktop` y `Documents` | Redirección de carpetas a Azure Files; el resto del perfil local no es autoritativo y no se copia al iniciar o cerrar sesión. | -| `AD` / `DO` | Perfil completo | FSLogix: montaje directo del VHDX con endpoint privado o Cloud Cache local sincronizada con Azure Files cuando el endpoint es público. | +| `AL` | `Desktop` y `Documents` | Redirección de carpetas a Azure Files; el resto del perfil sigue local. | +| `AD` / `DO` | Perfil completo | FSLogix VHDX: montaje directo con endpoint privado o Cloud Cache con endpoint público. | -Linux conserva su solución nativa de directorios personales; no intenta montar -un perfil de Windows. +Linux conserva su directorio personal nativo y no monta un perfil de Windows. -## Arquitectura y seguridad +`infra/azure/main.bicep` puede crear una cuenta `StorageV2` dedicada con shares +`profiles` y `redirected`, soft delete de 14 días, private endpoint y zona +`privatelink.file.`. Su valor predeterminado +`deployUserRoaming=false` evita crear estos recursos. La cuenta de staging usada +para paquetes no se reutiliza automáticamente como almacenamiento de perfiles. -`infra/azure/main.bicep` crea, por omisión, una cuenta `StorageV2` dedicada con -dos recursos SMB (`profiles` y `redirected`), soft delete de 14 días, un private -endpoint y la zona `privatelink.file.` ligada a la VNet. El -endpoint público queda deshabilitado. La VNet entrega el DNS del controlador de -dominio a los clientes P2S para que el nombre de Azure Files resuelva a la IP -privada. La cuenta de staging usada para transferir paquetes no se reutiliza ni -se convierte en almacenamiento de perfiles. +Los clientes de un endpoint privado requieren P2S u otra ruta privada a la VNet. +Un endpoint público exige resolución DNS, salida TCP 445 y reglas de red de +Storage limitadas a redes institucionales. El modo `Auto` selecciona `Direct` +para endpoint privado y `CloudCache` para uno público. `CCDLocations` y +`VHDLocations` son mutuamente excluyentes. -Los clientes deben usar el P2S de Azure o disponer de otra ruta privada a la -VNet. El modo de enrolamiento público directo no puede usar estos recursos y el -script de despliegue exige `-DeployUserRoaming $false` cuando no se despliega el -gateway P2S. +Azure Files se integra con AD DS mediante Kerberos AES-256. Las ACL de la raíz +usan `CREATOR OWNER` para aislar los directorios de usuarios. La clave de la +cuenta se usa sólo en memoria durante la creación de ACL y no se guarda en GPO. -Para una cuenta SMB ya existente cuyo endpoint público se limite mediante el -firewall de Storage a las redes institucionales, el configurador admite -`-EndpointAccess Public`. En ese modo no exige una dirección RFC1918, pero sigue -comprobando resolución DNS y TCP 445. La creación de los shares y las reglas de -red de la cuenta existente se realiza antes de ejecutar el configurador. El host -que aplica la integración AD/ACL debe conservar acceso temporal a SMB durante -esa ejecución; después se establece `defaultAction=Deny` y se deja únicamente la -red institucional autorizada. En el despliegue LCI actual esa regla final es -`200.13.89.0/24`. +## Riesgos que deben aceptarse -El modo `-StaffProfileStorageMode Auto` es el predeterminado. Selecciona -`Direct` con un endpoint privado y `CloudCache` con uno público. Cloud Cache -mantiene el VHDX de trabajo en `C:\ProgramData\FSLogix\Cache` y sincroniza sus -cambios con Azure Files, evitando que cada operación del perfil dependa de la -latencia WAN de SMB. `CCDLocations` y `VHDLocations` son mutuamente excluyentes: -el configurador elimina de forma efectiva el valor obsoleto al cambiar de modo, -incluso en clientes que ya habían aplicado la GPO anterior. Puede forzarse un -modo con `-StaffProfileStorageMode Direct` o `CloudCache`, pero `Direct` sobre -un endpoint público sólo debe usarse para diagnóstico. +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: -Cloud Cache necesita espacio local suficiente para el perfil (30 GB de máximo -por omisión), conserva la caché tras cerrar sesión y exige al menos un proveedor -remoto sano al registrar o desmontar el perfil. Si el proveedor no completa la -sincronización, el cierre espera como máximo 60 segundos y después conserva la -caché local para reintentar; no la descarta. La GPO también impide el inicio -con perfil temporal o local cuando FSLogix falla, para que una incidencia no -produzca dos perfiles divergentes silenciosamente. +- 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. -Azure Files se integra con el AD DS SGU, no con cuentas o claves guardadas en -cada cliente. El script configura AES-256 para Kerberos, una ACL de mínimo -privilegio con `CREATOR OWNER`, administradores y los grupos del rol, y permiso -SMB predeterminado para identidades autenticadas. La ACL NTFS mantiene aislado -el contenido de cada usuario. La clave de la cuenta se usa en memoria sólo para -crear las ACL iniciales y no se escribe en el dominio, en GPO ni en disco. +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. -Los grupos de rol sólo pueden enumerar la raíz y crear su propio directorio; no -reciben `Modify` en la raíz compartida. El directorio creado hereda `CREATOR -OWNER`, de modo que un usuario no puede modificar ni eliminar el contenedor de -otro usuario. +## 1. Desplegar la infraestructura opcional -## 1. Desplegar la infraestructura - -El soporte está activado por omisión para despliegues P2S: +El despliegue normal no incluye user roaming. Para crear deliberadamente sus +recursos: ```powershell $azure = .\scripts\Deploy-SguAzureInfrastructure.ps1 ` @@ -76,128 +72,103 @@ $azure = .\scripts\Deploy-SguAzureInfrastructure.ps1 ` -ResourceGroupName 'rg-sgu-lab' ` -Location 'centralus' ` -AdministratorUsername 'azureadmin' ` - -P2sRootCertificatePath $p2s.RootCertificatePath - -$azure.UserRoamingStorageAccountName -$azure.FsLogixProfilesSharePath -$azure.RedirectedFoldersSharePath -$azure.UserRoamingSetupArguments + -P2sRootCertificatePath $p2s.RootCertificatePath ` + -DeployUserRoaming $true ` + -AcknowledgeUserRoamingRisks ``` -Puede definirse un nombre globalmente único con -`-UserRoamingStorageAccountName`. Para omitir completamente el almacenamiento: - -```powershell --DeployUserRoaming $false -``` +Puede definirse un nombre con `-UserRoamingStorageAccountName`. Si no se pasan +los dos parámetros de aceptación, la infraestructura de perfiles no se crea. ## 2. Unir Azure Files al dominio y publicar las GPO Primero termine `Initialize-SguDomainController.ps1`. Después, en Windows -PowerShell 5.1 elevado dentro del DC: - -1. Instale Azure PowerShell actual (`Az.Accounts` y `Az.Storage` 8.1 o posterior). -2. Descargue y extraiga el módulo oficial - [AzFilesHybrid](https://github.com/Azure-Samples/azure-files-samples/releases). -3. Ejecute el configurador incluido en el paquete de servidor: +PowerShell 5.1 elevado dentro del DC, instale Azure PowerShell y el módulo oficial +[AzFilesHybrid](https://github.com/Azure-Samples/azure-files-samples/releases), +y ejecute: ```powershell -$storageAccountName = '' $parameters = @{ SubscriptionId = '00000000-0000-0000-0000-000000000000' ResourceGroupName = 'rg-sgu-lab' - StorageAccountName = $storageAccountName + StorageAccountName = '' FsLogixProfilesShareName = 'profiles' RedirectedFoldersShareName = 'redirected' AzFilesHybridModulePath = 'C:\Admin\AzFilesHybrid' UseDeviceAuthentication = $true + AcknowledgeUserRoamingRisks = $true } .\payload\scripts\Enable-SguAzureUserRoaming.ps1 @parameters ``` -Para reutilizar una cuenta con endpoint público: +Para una cuenta autorizada con endpoint público: ```powershell $parameters.EndpointAccess = 'Public' -$parameters.StaffProfileStorageMode = 'Auto' # resuelve a CloudCache +$parameters.StaffProfileStorageMode = 'Auto' .\payload\scripts\Enable-SguAzureUserRoaming.ps1 @parameters ``` -La operación es idempotente. Crea `OU=AzureFilesConfig`, une la cuenta de -almacenamiento al bosque y publica dos GPO: +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`. -- `SGU - AL redirected folders`, ligada a `OU=Alumnos`; -- `SGU - AD-DO FSLogix profiles`, ligada a `OU=Laboratorio` y habilitada sólo - mediante configuración `ObjectSpecific` para los SID de `SGU-Docentes` y - `SGU-Administrativos`. +## 3. Instalar FSLogix sólo para un piloto -Por seguridad, el script no borra perfiles locales existentes de AD/DO. Antes -de un piloto real, migre o respalde su contenido y elimínelos manualmente. El -switch `-DeleteExistingStaffLocalProfiles` habilita la opción de FSLogix que los -elimina al aplicar el contenedor; úselo sólo después de respaldar y validar. - -La identidad de Azure Files queda con contraseña sin expiración en la OU -dedicada para evitar que una cuenta de equipo sin agente pierda Kerberos a los -30 días. Si la política exige rotación, use periódicamente el procedimiento -oficial `Update-AzStorageAccountADObjectPassword` y no cambie sólo un lado. - -## 3. Instalar FSLogix en los clientes Windows - -FSLogix no forma parte de Windows y su licencia debe validarse para las ediciones -y suscripciones usadas. Descargue `FSLogixAppsSetup.exe` directamente de -Microsoft y agréguelo a la imagen base, o páselo como quinto argumento al -enrolamiento Azure: - -```bat -Start-SguAzureClientEnrollment.cmd 10.77.0.4 C:\SGU\vpn.zip C:\SGU\client.pfx C:\SGU\root.cer C:\SGU\FSLogixAppsSetup.exe -``` - -`Install-SguFsLogix.ps1` exige una firma Authenticode válida de Microsoft, -instala silenciosamente sin reiniciar y comprueba `frxsvc` y `frx.exe`. Si la -imagen ya contiene FSLogix, el instalador puede omitirse. Microsoft recomienda -mantener la versión más reciente; FSLogix no se actualiza mediante Windows -Update. - -Para cubrir también equipos ya unidos al dominio —incluidos los que estaban -apagados durante el despliegue— publique el instalador como script de inicio de -equipo: +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 script descarga el paquete oficial FSLogix 26.08, valida la firma -Authenticode de Microsoft y el SHA-256 esperado, y crea la GPO -`SGU - FSLogix client deployment` ligada a `OU=Laboratorio`. El ejecutable queda -en el SYSVOL de esa GPO y se instala de forma idempotente durante el arranque; -no usa tareas programadas ni requiere que el equipo estuviera encendido al -publicarla. Si `frxsvc` y `frx.exe` ya existen, el script de inicio termina sin -reinstalar. +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. Validar un piloto +## 4. Desactivar y volver a perfiles locales -Después de `gpupdate /force` y reiniciar un cliente: +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\' -Get-ItemProperty 'HKLM:\SOFTWARE\SGU\FSLogixDeployment' & 'C:\Program Files\FSLogix\Apps\frx.exe' list-redirects ``` -En modo privado, la resolución debe devolver la IP privada del endpoint; en -modo público debe devolver una dirección pública y TCP 445 debe estar permitido -desde la red institucional. En Cloud Cache compruebe además que existe -`C:\ProgramData\FSLogix\Cache` y que el registro efectivo contiene -`CCDLocations`, pero no `VHDLocations`. Inicie primero con un -`AL` nuevo y confirme las ubicaciones de Escritorio/Documentos; luego pruebe un -`AD` o `DO` sin perfil local previo y confirme la creación de un VHDX en -`profiles`. Revise `%ProgramData%\FSLogix\Logs\Profile` para confirmar que el -perfil se registró sin `ERROR: 258`, perfil temporal ni retorno a `C:\Users`. -No active el borrado de perfiles locales ni despliegue a todos los -usuarios hasta probar inicio, cierre, desconexión de VPN, cuota y restauración. +Revise `%ProgramData%\FSLogix\Logs\Profile`, pruebe inicio y cierre varias veces, +simule indisponibilidad y valide restauración. No active el borrado de perfiles +locales ni amplíe el alcance antes de superar esas pruebas. Referencias de Microsoft: diff --git a/infra/azure/main.bicep b/infra/azure/main.bicep index 1c7c148..371c34d 100644 --- a/infra/azure/main.bicep +++ b/infra/azure/main.bicep @@ -56,7 +56,7 @@ param publicEnrollmentSourceAddressPrefixes array = [] param administratorSourceAddressPrefix string = '' @description('Deploy private Azure Files shares for SGU user roaming. Clients require P2S or another private route into the VNet.') -param deployUserRoaming bool = true +param deployUserRoaming bool = false @description('Optional globally unique storage account name for roaming data. Leave empty to derive a stable name from the resource group.') param userRoamingStorageAccountName string = '' diff --git a/scripts/Deploy-SguAzureInfrastructure.ps1 b/scripts/Deploy-SguAzureInfrastructure.ps1 index 56e7a0d..5838826 100644 --- a/scripts/Deploy-SguAzureInfrastructure.ps1 +++ b/scripts/Deploy-SguAzureInfrastructure.ps1 @@ -18,7 +18,8 @@ param( [string]$VpnClientAddressPoolPrefix = '172.30.0.0/24', [string[]]$PublicEnrollmentSourceAddressPrefixes = @(), [string]$AdministratorSourceAddressPrefix = '', - [bool]$DeployUserRoaming = $true, + [bool]$DeployUserRoaming = $false, + [switch]$AcknowledgeUserRoamingRisks, [ValidatePattern('^$|^[a-z0-9]{3,24}$')] [string]$UserRoamingStorageAccountName = '', [ValidatePattern('^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$')] @@ -47,6 +48,9 @@ if (-not $AdministratorPassword) { if ($DeployUserRoaming -and -not $DeployVpnGateway) { throw 'Azure user roaming requires the P2S gateway deployed by this template so clients can reach the private Azure Files endpoint. Use -DeployUserRoaming $false with direct public enrollment.' } +if ($DeployUserRoaming -and -not $AcknowledgeUserRoamingRisks) { + throw 'User roaming is disabled by default. To provision it, also pass -AcknowledgeUserRoamingRisks after reviewing the sign-in, sign-out, storage-availability, stale-cache, and recovery risks in docs/user-roaming.md.' +} foreach ($shareName in @($FsLogixProfilesShareName, $RedirectedFoldersShareName)) { if ($shareName.Contains('--')) { throw "Azure Files share names cannot contain consecutive hyphens: $shareName" diff --git a/scripts/Disable-SguAzureUserRoaming.ps1 b/scripts/Disable-SguAzureUserRoaming.ps1 new file mode 100644 index 0000000..ac81878 --- /dev/null +++ b/scripts/Disable-SguAzureUserRoaming.ps1 @@ -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 +} diff --git a/scripts/Enable-SguAzureUserRoaming.ps1 b/scripts/Enable-SguAzureUserRoaming.ps1 index 3c3e635..ce0adbc 100644 --- a/scripts/Enable-SguAzureUserRoaming.ps1 +++ b/scripts/Enable-SguAzureUserRoaming.ps1 @@ -29,10 +29,15 @@ param( [int]$FsLogixProfileSizeMiB = 30000, [string]$AzFilesHybridModulePath, [switch]$UseDeviceAuthentication, - [switch]$DeleteExistingStaffLocalProfiles + [switch]$DeleteExistingStaffLocalProfiles, + [switch]$AcknowledgeUserRoamingRisks, + [string]$FsLogixDeploymentGpoName = 'SGU - FSLogix client deployment' ) $ErrorActionPreference = 'Stop' +if (-not $AcknowledgeUserRoamingRisks -and -not $WhatIfPreference) { + throw 'User roaming is an optional high-risk feature. Pass -AcknowledgeUserRoamingRisks only after reviewing docs/user-roaming.md, testing sign-in and sign-out, and accepting the documented storage and recovery failure modes.' +} function Assert-Administrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() @@ -158,6 +163,8 @@ function Set-SguGpoRegistryValue { ) if ($PSCmdlet.ShouldProcess("$GpoName :: $Key\\$ValueName", "Set $Type policy value")) { + Remove-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server ` + -Key $Key -ValueName "**del.$ValueName" -ErrorAction SilentlyContinue | Out-Null Set-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server ` -Key $Key -ValueName $ValueName -Type $Type -Value $Value | Out-Null } @@ -431,6 +438,24 @@ $staffGpo = Ensure-SguGpoLink -Name $StaffGpoName -TargetOuDn $laboratoryOuDn ` $fsLogixRoot = 'HKLM\SOFTWARE\FSLogix\Profiles' Set-SguGpoRegistryValue -GpoName $staffGpo.DisplayName -DomainName $domainName ` -Server $DomainController -Key $fsLogixRoot -ValueName 'Enabled' -Type DWord -Value 0 +Set-SguGpoRegistryValue -GpoName $staffGpo.DisplayName -DomainName $domainName ` + -Server $DomainController -Key 'HKLM\SYSTEM\CurrentControlSet\Services\frxsvc' ` + -ValueName 'Start' -Type DWord -Value 2 +Set-SguGpoRegistryValue -GpoName $staffGpo.DisplayName -DomainName $domainName ` + -Server $DomainController -Key 'HKLM\SYSTEM\CurrentControlSet\Services\frxccds' ` + -ValueName 'Start' -Type DWord -Value 2 +$deploymentGpo = Get-GPO -Name $FsLogixDeploymentGpoName -Domain $domainName ` + -Server $DomainController -ErrorAction SilentlyContinue +if ($deploymentGpo) { + $deploymentLink = @(Get-GPInheritance -Target $laboratoryOuDn -Domain $domainName ` + -Server $DomainController).GpoLinks | + Where-Object DisplayName -eq $FsLogixDeploymentGpoName | Select-Object -First 1 + if ($deploymentLink -and -not [bool]$deploymentLink.Enabled -and + $PSCmdlet.ShouldProcess($laboratoryOuDn, "Enable '$FsLogixDeploymentGpoName'")) { + Set-GPLink -Name $FsLogixDeploymentGpoName -Target $laboratoryOuDn ` + -Domain $domainName -Server $DomainController -LinkEnabled Yes | Out-Null + } +} $fsLogixValues = [ordered]@{ Enabled = @{ Type = 'DWord'; Value = 1 } DeleteLocalProfileWhenVHDShouldApply = @{ diff --git a/scripts/New-SguBootstrapPackages.ps1 b/scripts/New-SguBootstrapPackages.ps1 index dde2468..b8a479b 100644 --- a/scripts/New-SguBootstrapPackages.ps1 +++ b/scripts/New-SguBootstrapPackages.ps1 @@ -196,6 +196,7 @@ Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Start-SguAzureServerBootstra -Destination (Join-Path $serverRoot 'Start-SguAzureServerBootstrap.cmd') $serverScripts = @( 'Deploy-AuthBroker.ps1', + 'Disable-SguAzureUserRoaming.ps1', 'Enable-SguAzureUserRoaming.ps1', 'Enable-SguServerRemoteManagement.ps1', 'Get-SguUsageReport.ps1', diff --git a/tests/UserRoaming.Tests.ps1 b/tests/UserRoaming.Tests.ps1 index 9a4afec..73b7504 100644 --- a/tests/UserRoaming.Tests.ps1 +++ b/tests/UserRoaming.Tests.ps1 @@ -2,6 +2,7 @@ $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' @@ -11,6 +12,7 @@ $packagePath = Join-Path $repositoryRoot 'scripts\New-SguBootstrapPackages.ps1' foreach ($scriptPath in @( $deploymentPath, $configurationPath, + $rollbackPath, $installerPath, $gpoDeploymentPath, $clientBootstrapPath, @@ -40,9 +42,9 @@ $samFunction = $configurationAst.Find({ Invoke-Expression $samFunction.Extent.Text Describe 'SGU Azure user-roaming infrastructure' { - It 'deploys dedicated private Azure Files resources by default' { + It 'keeps roaming disabled by default while retaining optional private Azure Files resources' { $source = Get-Content -LiteralPath $bicepPath -Raw - $source | Should Match 'param deployUserRoaming bool = true' + $source | Should Match 'param deployUserRoaming bool = false' $source | Should Match "purpose: 'SGU-user-roaming'" $source | Should Match "publicNetworkAccess: 'Disabled'" $source | Should Match "privateLinkServiceId: userRoamingStorageAccount.id" @@ -63,6 +65,8 @@ Describe 'SGU Azure user-roaming infrastructure' { $source = Get-Content -LiteralPath $deploymentPath -Raw $source | Should Match '\$DeployUserRoaming -and -not \$DeployVpnGateway' $source | Should Match 'private Azure Files endpoint' + $source | Should Match '\$DeployUserRoaming -and -not \$AcknowledgeUserRoamingRisks' + $source | Should Match '\[bool\]\$DeployUserRoaming = \$false' } It 'returns every post-domain setup value from the Azure deployment' { @@ -79,6 +83,12 @@ Describe 'SGU Azure user-roaming infrastructure' { } Describe 'SGU role-specific roaming policies' { + It 'requires an explicit risk acknowledgement before enabling roaming' { + $source = Get-Content -LiteralPath $configurationPath -Raw + $source | Should Match '\[switch\]\$AcknowledgeUserRoamingRisks' + $source | Should Match 'optional high-risk feature' + } + It 'supports an explicitly selected public SMB endpoint while keeping private as the default' { (((Get-Command $configurationPath).Parameters['EndpointAccess'].Attributes | Where-Object { $_ -is [Management.Automation.ValidateSetAttribute] }).ValidValues -join ',') | @@ -181,6 +191,7 @@ Describe 'SGU FSLogix image enrollment' { It 'packages both roaming setup scripts' { $source = Get-Content -LiteralPath $packagePath -Raw $source | Should Match "'Install-SguFsLogix\.ps1'" + $source | Should Match "'Disable-SguAzureUserRoaming\.ps1'" $source | Should Match "'Enable-SguAzureUserRoaming\.ps1'" $source | Should Match "'Publish-SguFsLogixClientDeployment\.ps1'" } @@ -196,3 +207,23 @@ Describe 'SGU FSLogix image enrollment' { $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' + } +}