Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7de6e2d867 | ||
|
|
7460f9316d | ||
|
|
351adb0bdc | ||
|
|
2de1105ecd | ||
|
|
6c52a9546c |
@@ -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)
|
||||||
|
- [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,9 @@ 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. P2S deployments also provision private Azure Files storage for
|
||||||
|
role-based roaming: `AL` redirects only Documents/Desktop, while `AD` and `DO`
|
||||||
|
can mount an FSLogix VHDX profile after the post-domain setup step.
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
@@ -78,6 +78,17 @@ 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.
|
||||||
|
|
||||||
|
## Azure user roaming
|
||||||
|
|
||||||
|
Azure P2S deployments can add a dedicated, private Azure Files account. Student
|
||||||
|
accounts (`AL`) keep a non-authoritative local profile and redirect only
|
||||||
|
Documents/Desktop. Administrative and professor accounts (`AD`/`DO`) receive an
|
||||||
|
FSLogix VHDX profile container through object-specific settings keyed to their
|
||||||
|
AD security-group SIDs. The storage account uses AD DS Kerberos with AES-256;
|
||||||
|
share-root ACLs use `CREATOR OWNER` so users cannot browse one another's data.
|
||||||
|
The bootstrap staging account is deliberately separate. See
|
||||||
|
[user-roaming.md](user-roaming.md) for rollout and recovery constraints.
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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`;
|
||||||
|
- Azure Files privado para user roaming: `AL` conserva sólo Escritorio/Documentos
|
||||||
|
y `AD`/`DO` usa contenedores de perfil FSLogix.
|
||||||
|
|
||||||
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,9 @@ 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 se crea por omisión y requiere la ruta
|
||||||
|
privada del P2S; para un despliegue de enrolamiento público directo indique
|
||||||
|
`-DeployUserRoaming $false`.
|
||||||
|
|
||||||
## 3. Conectarse al servidor
|
## 3. Conectarse al servidor
|
||||||
|
|
||||||
@@ -137,6 +141,12 @@ 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, 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
|
||||||
|
[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 +165,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 +179,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:
|
||||||
|
|
||||||
|
|||||||
@@ -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/).
|
||||||
@@ -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.
|
||||||
@@ -137,7 +137,7 @@ Windows Firewall al CIDR permitido. Después de un reinicio real se comprobó:
|
|||||||
- `IsValid=True`, sin incidencias;
|
- `IsValid=True`, sin incidencias;
|
||||||
- `BrokerHealth=ok`, `RemoteAccessReady=True` y `RustDeskReady=True`;
|
- `BrokerHealth=ok`, `RemoteAccessReady=True` y `RustDeskReady=True`;
|
||||||
- ningún perfil VPN instalado;
|
- ningún perfil VPN instalado;
|
||||||
- paquete final `0.5.9`, SHA-256 del ZIP de Windows:
|
- paquete candidato usado en la prueba `0.5.9`, SHA-256 del ZIP de Windows:
|
||||||
`E7AF77252E444FB7EBACF3C90005D322EE19F76DD9387AC5782C57EA9EE617B7`.
|
`E7AF77252E444FB7EBACF3C90005D322EE19F76DD9387AC5782C57EA9EE617B7`.
|
||||||
|
|
||||||
## Prueba real con Azure VPN
|
## Prueba real con Azure VPN
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
# User roaming por rol en Azure
|
||||||
|
|
||||||
|
El despliegue Azure puede crear almacenamiento privado y políticas distintas para
|
||||||
|
cada tipo de cuenta SGU:
|
||||||
|
|
||||||
|
| Rol | Datos conservados | Implementación |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `AL` | `Desktop` y `Documents` | Redirección de carpetas a Azure Files; el resto del perfil local no es autoritativo y no se copia al iniciar o cerrar sesión. |
|
||||||
|
| `AD` / `DO` | Perfil completo | Un contenedor VHDX único de FSLogix montado desde Azure Files. |
|
||||||
|
|
||||||
|
Linux conserva su solución nativa de directorios personales; no intenta montar
|
||||||
|
un perfil de Windows.
|
||||||
|
|
||||||
|
## Arquitectura y seguridad
|
||||||
|
|
||||||
|
`infra/azure/main.bicep` crea, por omisión, una cuenta `StorageV2` dedicada con
|
||||||
|
dos recursos SMB (`profiles` y `redirected`), soft delete de 14 días, un private
|
||||||
|
endpoint y la zona `privatelink.file.<sufijo-de-Azure>` ligada a la VNet. El
|
||||||
|
endpoint público queda deshabilitado. La VNet entrega el DNS del controlador de
|
||||||
|
dominio a los clientes P2S para que el nombre de Azure Files resuelva a la IP
|
||||||
|
privada. La cuenta de staging usada para transferir paquetes no se reutiliza ni
|
||||||
|
se convierte en almacenamiento de perfiles.
|
||||||
|
|
||||||
|
Los clientes 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 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.
|
||||||
|
|
||||||
|
## 1. Desplegar la infraestructura
|
||||||
|
|
||||||
|
El soporte está activado por omisión para despliegues P2S:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$azure = .\scripts\Deploy-SguAzureInfrastructure.ps1 `
|
||||||
|
-SubscriptionId '00000000-0000-0000-0000-000000000000' `
|
||||||
|
-ResourceGroupName 'rg-sgu-lab' `
|
||||||
|
-Location 'centralus' `
|
||||||
|
-AdministratorUsername 'azureadmin' `
|
||||||
|
-P2sRootCertificatePath $p2s.RootCertificatePath
|
||||||
|
|
||||||
|
$azure.UserRoamingStorageAccountName
|
||||||
|
$azure.FsLogixProfilesSharePath
|
||||||
|
$azure.RedirectedFoldersSharePath
|
||||||
|
$azure.UserRoamingSetupArguments
|
||||||
|
```
|
||||||
|
|
||||||
|
Puede definirse un nombre globalmente único con
|
||||||
|
`-UserRoamingStorageAccountName`. Para omitir completamente el almacenamiento:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
-DeployUserRoaming $false
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
$storageAccountName = '<valor UserRoamingStorageAccountName del despliegue>'
|
||||||
|
$parameters = @{
|
||||||
|
SubscriptionId = '00000000-0000-0000-0000-000000000000'
|
||||||
|
ResourceGroupName = 'rg-sgu-lab'
|
||||||
|
StorageAccountName = $storageAccountName
|
||||||
|
FsLogixProfilesShareName = 'profiles'
|
||||||
|
RedirectedFoldersShareName = 'redirected'
|
||||||
|
AzFilesHybridModulePath = 'C:\Admin\AzFilesHybrid'
|
||||||
|
UseDeviceAuthentication = $true
|
||||||
|
}
|
||||||
|
.\payload\scripts\Enable-SguAzureUserRoaming.ps1 @parameters
|
||||||
|
```
|
||||||
|
|
||||||
|
La operación es idempotente. Crea `OU=AzureFilesConfig`, une la cuenta de
|
||||||
|
almacenamiento al bosque y publica dos GPO:
|
||||||
|
|
||||||
|
- `SGU - AL redirected folders`, ligada a `OU=Alumnos`;
|
||||||
|
- `SGU - AD-DO FSLogix profiles`, ligada a `OU=Laboratorio` y habilitada sólo
|
||||||
|
mediante configuración `ObjectSpecific` para los SID de `SGU-Docentes` y
|
||||||
|
`SGU-Administrativos`.
|
||||||
|
|
||||||
|
Por seguridad, el script no borra perfiles locales existentes de AD/DO. Antes
|
||||||
|
de un piloto real, migre o respalde su contenido y elimínelos manualmente. El
|
||||||
|
switch `-DeleteExistingStaffLocalProfiles` habilita la opción de FSLogix que los
|
||||||
|
elimina al aplicar el contenedor; úselo sólo después de respaldar y validar.
|
||||||
|
|
||||||
|
La identidad de Azure Files queda con contraseña sin expiración en la OU
|
||||||
|
dedicada para evitar que una cuenta de equipo sin agente pierda Kerberos a los
|
||||||
|
30 días. Si la política exige rotación, use periódicamente el procedimiento
|
||||||
|
oficial `Update-AzStorageAccountADObjectPassword` y no cambie sólo un lado.
|
||||||
|
|
||||||
|
## 3. Instalar FSLogix en las imágenes Windows
|
||||||
|
|
||||||
|
FSLogix no forma parte de Windows y su licencia debe validarse para las ediciones
|
||||||
|
y suscripciones usadas. Descargue `FSLogixAppsSetup.exe` directamente de
|
||||||
|
Microsoft y agréguelo a la imagen base, o páselo como quinto argumento al
|
||||||
|
enrolamiento Azure:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
Start-SguAzureClientEnrollment.cmd 10.77.0.4 C:\SGU\vpn.zip C:\SGU\client.pfx C:\SGU\root.cer C:\SGU\FSLogixAppsSetup.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
## 4. Validar un piloto
|
||||||
|
|
||||||
|
Después de `gpupdate /force` y reiniciar un cliente:
|
||||||
|
|
||||||
|
```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'
|
||||||
|
& 'C:\Program Files\FSLogix\Apps\frx.exe' list-redirects
|
||||||
|
```
|
||||||
|
|
||||||
|
La resolución debe devolver la IP privada del endpoint. Inicie primero con un
|
||||||
|
`AL` nuevo y confirme las ubicaciones de Escritorio/Documentos; luego pruebe un
|
||||||
|
`AD` o `DO` sin perfil local previo y confirme la creación de un VHDX en
|
||||||
|
`profiles`. No active el borrado de perfiles locales ni despliegue a todos los
|
||||||
|
usuarios hasta probar inicio, cierre, desconexión de VPN, cuota y restauración.
|
||||||
|
|
||||||
|
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)
|
||||||
|
- [Redirección de carpetas](https://learn.microsoft.com/windows-server/storage/folder-redirection/deploy-folder-redirection)
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 = true
|
||||||
|
|
||||||
|
@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
|
||||||
|
|||||||
@@ -18,6 +18,17 @@ 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 = $true,
|
||||||
|
[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 +44,17 @@ 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.'
|
||||||
|
}
|
||||||
|
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 +80,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 +130,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 +183,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,430 @@
|
|||||||
|
#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',
|
||||||
|
[ValidateRange(1024, 1048576)]
|
||||||
|
[int]$FsLogixProfileSizeMiB = 30000,
|
||||||
|
[string]$AzFilesHybridModulePath,
|
||||||
|
[switch]$UseDeviceAuthentication,
|
||||||
|
[switch]$DeleteExistingStaffLocalProfiles
|
||||||
|
)
|
||||||
|
|
||||||
|
$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 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 20) {
|
||||||
|
return $StorageName
|
||||||
|
}
|
||||||
|
$sha256 = [Security.Cryptography.SHA256]::Create()
|
||||||
|
try {
|
||||||
|
$hash = $sha256.ComputeHash([Text.Encoding]::UTF8.GetBytes($StorageName))
|
||||||
|
$suffix = ([BitConverter]::ToString($hash) -replace '-', '').Substring(0, 15).ToLowerInvariant()
|
||||||
|
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")) {
|
||||||
|
Set-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
|
||||||
|
-Key $Key -ValueName $ValueName -Type $Type -Value $Value | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
$privateAddresses = @(Resolve-DnsName -Name $fileEndpointHost -Type A -ErrorAction Stop |
|
||||||
|
Where-Object IPAddress | Select-Object -ExpandProperty IPAddress)
|
||||||
|
if ($privateAddresses.Count -eq 0 -or @($privateAddresses | Where-Object {
|
||||||
|
$_ -match '^(10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)'
|
||||||
|
}).Count -eq 0) {
|
||||||
|
throw "$fileEndpointHost did not resolve to a private endpoint. Verify the privatelink.file.core.windows.net VNet link and the DC Azure DNS forwarder."
|
||||||
|
}
|
||||||
|
if (-not (Test-NetConnection -ComputerName $fileEndpointHost -Port 445 -InformationLevel Quiet)) {
|
||||||
|
throw "The domain controller cannot reach $fileEndpointHost on TCP 445 through the private endpoint."
|
||||||
|
}
|
||||||
|
|
||||||
|
$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"
|
||||||
|
try {
|
||||||
|
if ($PSCmdlet.ShouldProcess($profilesSharePath, 'Apply isolated FSLogix root ACLs')) {
|
||||||
|
Set-SguAzureFileRootAcl -UncPath $profilesSharePath -Credential $storageCredential `
|
||||||
|
-DomainAdminsSid $domainAdminsSid `
|
||||||
|
-ContributorSids @($professorGroup.SID, $administrativeGroup.SID)
|
||||||
|
}
|
||||||
|
if ($PSCmdlet.ShouldProcess($redirectedFoldersSharePath, 'Apply isolated student-folder root ACLs')) {
|
||||||
|
$studentRootRights = [Security.AccessControl.FileSystemRights]::CreateDirectories -bor
|
||||||
|
[Security.AccessControl.FileSystemRights]::ListDirectory -bor
|
||||||
|
[Security.AccessControl.FileSystemRights]::ReadAttributes -bor
|
||||||
|
[Security.AccessControl.FileSystemRights]::ReadExtendedAttributes -bor
|
||||||
|
[Security.AccessControl.FileSystemRights]::ReadPermissions -bor
|
||||||
|
[Security.AccessControl.FileSystemRights]::Traverse -bor
|
||||||
|
[Security.AccessControl.FileSystemRights]::Synchronize
|
||||||
|
Set-SguAzureFileRootAcl -UncPath $redirectedFoldersSharePath -Credential $storageCredential `
|
||||||
|
-DomainAdminsSid $domainAdminsSid -ContributorSids @($studentGroup.SID) `
|
||||||
|
-ContributorRights $studentRootRights
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
$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 }
|
||||||
|
VHDLocations = @{ Type = 'String'; Value = $profilesSharePath }
|
||||||
|
VolumeType = @{ Type = 'String'; Value = 'VHDX' }
|
||||||
|
}
|
||||||
|
foreach ($staffGroup in @($professorGroup, $administrativeGroup)) {
|
||||||
|
$objectSpecificKey = "$fsLogixRoot\ObjectSpecific\$($staffGroup.SID.Value)"
|
||||||
|
foreach ($setting in $fsLogixValues.GetEnumerator()) {
|
||||||
|
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
|
||||||
|
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 = '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 `
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,6 +196,7 @@ 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',
|
||||||
|
'Enable-SguAzureUserRoaming.ps1',
|
||||||
'Enable-SguServerRemoteManagement.ps1',
|
'Enable-SguServerRemoteManagement.ps1',
|
||||||
'Get-SguUsageReport.ps1',
|
'Get-SguUsageReport.ps1',
|
||||||
'Get-SguBrokerLog.ps1',
|
'Get-SguBrokerLog.ps1',
|
||||||
@@ -210,6 +212,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 +240,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 +265,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
|
||||||
|
|||||||
@@ -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"),
|
||||||
@@ -58,6 +62,7 @@ if (-not $authorizationParameter) {
|
|||||||
Add-Type -AssemblyName System.Net.Http
|
Add-Type -AssemblyName System.Net.Http
|
||||||
$handler = [Net.Http.HttpClientHandler]::new()
|
$handler = [Net.Http.HttpClientHandler]::new()
|
||||||
$client = [Net.Http.HttpClient]::new($handler)
|
$client = [Net.Http.HttpClient]::new($handler)
|
||||||
|
$client.Timeout = [TimeSpan]::FromMinutes(10)
|
||||||
$client.BaseAddress = [uri]($GiteaBaseUri.AbsoluteUri.TrimEnd('/') + '/')
|
$client.BaseAddress = [uri]($GiteaBaseUri.AbsoluteUri.TrimEnd('/') + '/')
|
||||||
$client.DefaultRequestHeaders.Authorization = [Net.Http.Headers.AuthenticationHeaderValue]::new(
|
$client.DefaultRequestHeaders.Authorization = [Net.Http.Headers.AuthenticationHeaderValue]::new(
|
||||||
$authorizationScheme,
|
$authorizationScheme,
|
||||||
@@ -130,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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -111,7 +113,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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
$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'
|
||||||
|
$installerPath = Join-Path $repositoryRoot 'scripts\Install-SguFsLogix.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,
|
||||||
|
$installerPath,
|
||||||
|
$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 'deploys dedicated private Azure Files resources by default' {
|
||||||
|
$source = Get-Content -LiteralPath $bicepPath -Raw
|
||||||
|
$source | Should Match 'param deployUserRoaming bool = true'
|
||||||
|
$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'
|
||||||
|
}
|
||||||
|
|
||||||
|
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 '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\)'
|
||||||
|
}
|
||||||
|
|
||||||
|
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 20
|
||||||
|
$name | Should Match '^sgufs[0-9a-f]{15}$'
|
||||||
|
(Get-SguStorageSamAccountName -StorageName 'abcdefghijklmnopqrstuvwx') | Should Be $name
|
||||||
|
(Get-SguStorageSamAccountName -StorageName 'sguroam1234567890123') |
|
||||||
|
Should Be 'sguroam1234567890123'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "'Enable-SguAzureUserRoaming\.ps1'"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user