Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80ab99cc8a | ||
|
|
106be26e7c |
@@ -61,7 +61,7 @@ Operational documentation:
|
|||||||
- [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)
|
- [Laboratorio desktop wallpaper protection](docs/laboratorio-wallpaper-policy.md)
|
||||||
- [Azure user roaming by role with Azure Files and FSLogix](docs/user-roaming.md)
|
- [Optional Azure user roaming by role with Azure Files and FSLogix](docs/user-roaming.md)
|
||||||
- [Decision: do not persist password verifiers in Redis](docs/decisions/0001-no-password-cache.md)
|
- [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 |
|
||||||
@@ -109,11 +109,12 @@ Follow [docs/lab-runbook.md](docs/lab-runbook.md). Review
|
|||||||
For a public Azure VM, use
|
For a public Azure VM, use
|
||||||
[docs/azure-vpn-deployment.md](docs/azure-vpn-deployment.md). It supports an
|
[docs/azure-vpn-deployment.md](docs/azure-vpn-deployment.md). It supports an
|
||||||
optional Azure P2S gateway or direct enrollment restricted to explicit public
|
optional Azure P2S gateway or direct enrollment restricted to explicit public
|
||||||
source CIDRs. P2S deployments also provision private Azure Files storage for
|
source CIDRs. Every SGU role uses an ordinary local Windows profile by default:
|
||||||
role-based roaming: `AL` redirects only Documents/Desktop, while `AD` and `DO`
|
Desktop and Documents stay under `C:\Users` and no profile share is mapped.
|
||||||
use an FSLogix VHDX profile after the post-domain setup step. Existing public
|
Azure Files/FSLogix roaming remains available only as an experimental,
|
||||||
Azure Files deployments automatically use FSLogix Cloud Cache locally instead
|
explicit opt-in that requires acknowledgement of its login, sign-out, storage,
|
||||||
of performing all profile I/O across WAN SMB.
|
cache and recovery risks. See [docs/user-roaming.md](docs/user-roaming.md) for
|
||||||
|
the opt-in and rollback procedures.
|
||||||
|
|
||||||
Never disable the built-in Microsoft password Credential Provider. It is the
|
Never disable the built-in Microsoft password Credential Provider. It is the
|
||||||
supported recovery path if a third-party provider fails to load.
|
supported recovery path if a third-party provider fails to load.
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 88 KiB |
+13
-11
@@ -78,18 +78,20 @@ is deliberately left unset because the verified page does not expose it.
|
|||||||
Missing metadata does not clear existing AD values and never changes the
|
Missing metadata does not clear existing AD values and never changes the
|
||||||
password outcome.
|
password outcome.
|
||||||
|
|
||||||
## Azure user roaming
|
## Optional Azure user roaming
|
||||||
|
|
||||||
Azure P2S deployments can add a dedicated, private Azure Files account. Student
|
The default architecture uses local Windows profiles for every role. Desktop,
|
||||||
accounts (`AL`) keep a non-authoritative local profile and redirect only
|
Documents and the remainder of the profile live under `C:\Users`; enrollment
|
||||||
Documents/Desktop. Administrative and professor accounts (`AD`/`DO`) receive an
|
does not map a profile share and the domain GPO keeps FSLogix disabled.
|
||||||
FSLogix VHDX profile container through object-specific settings keyed to their
|
|
||||||
AD security-group SIDs. Private endpoints mount the VHDX directly; public WAN
|
An experimental opt-in can add a dedicated Azure Files account. Student
|
||||||
endpoints use a local FSLogix Cloud Cache synchronized to Azure Files so profile
|
accounts (`AL`) redirect only Documents/Desktop, while administrative and
|
||||||
I/O does not block on every SMB round trip. The storage account uses AD DS Kerberos with AES-256;
|
professor accounts (`AD`/`DO`) receive an FSLogix VHDX profile container through
|
||||||
share-root ACLs use `CREATOR OWNER` so users cannot browse one another's data.
|
object-specific settings. Enabling it is never implicit: both infrastructure
|
||||||
The bootstrap staging account is deliberately separate. See
|
deployment and policy configuration require an explicit risk acknowledgement.
|
||||||
[user-roaming.md](user-roaming.md) for rollout and recovery constraints.
|
The storage account uses AD DS Kerberos with AES-256 and share-root ACLs use
|
||||||
|
`CREATOR OWNER`. See [user-roaming.md](user-roaming.md) for the observed
|
||||||
|
sign-out/cache failure modes, staged rollout requirements and rollback.
|
||||||
|
|
||||||
Every synchronization also enforces one idempotent security-group membership
|
Every synchronization also enforces one idempotent security-group membership
|
||||||
from the classified institutional prefix: `AL` to `SGU-Alumnos`, `AD` to
|
from the classified institutional prefix: `AL` to `SGU-Alumnos`, `AD` to
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ La plantilla crea:
|
|||||||
- 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
|
- perfiles locales para todos los usuarios por omisión; Azure Files/FSLogix es
|
||||||
y `AD`/`DO` usa contenedores de perfil FSLogix.
|
una característica opcional que debe habilitarse y aceptarse explícitamente.
|
||||||
|
|
||||||
Los prefijos privados deben ser RFC1918 y no deben solaparse con las redes usadas
|
Los prefijos privados deben ser RFC1918 y no deben solaparse con las redes usadas
|
||||||
por Hyper-V, el `Default Switch`, Wi-Fi o Ethernet locales. Los prefijos de
|
por Hyper-V, el `Default Switch`, Wi-Fi o Ethernet locales. Los prefijos de
|
||||||
@@ -84,9 +84,10 @@ pública actual:
|
|||||||
|
|
||||||
No utilice `0.0.0.0/0`. El despliegue de un VPN Gateway suele tardar bastante
|
No utilice `0.0.0.0/0`. El despliegue de un VPN Gateway suele tardar bastante
|
||||||
más que la VM; omitirlo reduce tiempo y costo. El comando espera hasta que Azure
|
más que la VM; omitirlo reduce tiempo y costo. El comando espera hasta que Azure
|
||||||
entregue un resultado final. User roaming se crea por omisión y requiere la ruta
|
entregue un resultado final. User roaming está desactivado por omisión. Sólo se
|
||||||
privada del P2S; para un despliegue de enrolamiento público directo indique
|
provisiona al indicar juntos `-DeployUserRoaming $true` y
|
||||||
`-DeployUserRoaming $false`.
|
`-AcknowledgeUserRoamingRisks`, después de revisar
|
||||||
|
[sus riesgos y recuperación](user-roaming.md).
|
||||||
|
|
||||||
## 3. Conectarse al servidor
|
## 3. Conectarse al servidor
|
||||||
|
|
||||||
@@ -141,10 +142,12 @@ El JSON debe indicar `NetworkConfigurationMode = PlatformManaged`, el pool P2S
|
|||||||
en `TrustedClientNetworks` cuando exista VPN y los CIDR directos en
|
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
|
Después de terminar la promoción no se necesita ninguna acción de perfiles: las
|
||||||
las políticas por rol con `Enable-SguAzureUserRoaming.ps1`. Esta segunda fase
|
cuentas usarán archivos locales. Si se decide realizar un piloto explícito de
|
||||||
requiere Azure PowerShell y el módulo oficial AzFilesHybrid porque el SID y GUID
|
Azure Files, publique las políticas ejecutando
|
||||||
del bosque todavía no existen durante el despliegue Bicep. Consulte
|
`Enable-SguAzureUserRoaming.ps1` con `-AcknowledgeUserRoamingRisks`. Esa fase opcional requiere Azure PowerShell y el
|
||||||
|
módulo oficial AzFilesHybrid porque el SID y GUID del bosque todavía no existen
|
||||||
|
durante el despliegue Bicep. Consulte
|
||||||
[user roaming por rol en Azure](user-roaming.md).
|
[user roaming por rol en Azure](user-roaming.md).
|
||||||
|
|
||||||
## 5. Enrolar cada VM Hyper-V
|
## 5. Enrolar cada VM Hyper-V
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# SGU Credential Provider 0.6.4
|
||||||
|
|
||||||
|
Esta versión revierte el despliegue de perfiles móviles y deja los perfiles
|
||||||
|
locales como comportamiento predeterminado para todo el dominio SGU.
|
||||||
|
|
||||||
|
## Cambio operativo
|
||||||
|
|
||||||
|
- `AL`, `AD`, `DO` y cualquier otra cuenta usan un perfil local bajo `C:\Users`.
|
||||||
|
- Escritorio y Documentos vuelven a las rutas locales del usuario.
|
||||||
|
- No se configura `profilePath`, `homeDirectory` ni `homeDrive` en Active Directory.
|
||||||
|
- Las políticas deshabilitan FSLogix Profiles, ODFC, `frxsvc` y `frxccds`.
|
||||||
|
- El enlace de instalación automática de FSLogix queda deshabilitado.
|
||||||
|
- Los recursos Azure Files existentes no se destruyen y pueden conservarse para
|
||||||
|
recuperación o para un piloto futuro.
|
||||||
|
|
||||||
|
## Herramientas
|
||||||
|
|
||||||
|
`Disable-SguAzureUserRoaming.ps1` aplica el rollback idempotente en el dominio.
|
||||||
|
`Deploy-SguAzureInfrastructure.ps1` y Bicep usan
|
||||||
|
`DeployUserRoaming=false` por omisión. Tanto el aprovisionamiento como la
|
||||||
|
publicación de políticas exigen `AcknowledgeUserRoamingRisks` para volver a
|
||||||
|
activar la función opcional.
|
||||||
|
|
||||||
|
La documentación registra los tiempos prolongados de inicio/cierre, estados
|
||||||
|
pendientes de VHDX, caché obsoleta y recuperación mediante reinicio observados
|
||||||
|
durante el piloto. User roaming se considera experimental y nunca se habilita de
|
||||||
|
forma implícita.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# SGU Credential Provider 0.6.5
|
||||||
|
|
||||||
|
Esta versión actualiza los fondos de bienvenida de Windows y Linux sin agregar
|
||||||
|
dependencias ni trabajo de composición durante el inicio de sesión.
|
||||||
|
|
||||||
|
## Identidad visual
|
||||||
|
|
||||||
|
- El fondo base azul incorpora de forma permanente el logotipo blanco oficial
|
||||||
|
de Universidad La Salle México.
|
||||||
|
- El generador utiliza directamente ese JPEG: no abre, redimensiona ni compone
|
||||||
|
un segundo archivo de logotipo cada vez que inicia una sesión.
|
||||||
|
- Se conservan las dimensiones institucionales de `1600x1000` y el área central
|
||||||
|
disponible para el saludo personalizado.
|
||||||
|
|
||||||
|
## Texto de ubicación
|
||||||
|
|
||||||
|
- La sala aparece en una primera línea de mayor tamaño.
|
||||||
|
- La dependencia aparece debajo con una tipografía menor.
|
||||||
|
- La redacción neutral usa `Acceso a...`; cuando Active Directory contiene el
|
||||||
|
sexo enriquecido, usa `Estás ubicado...` o `Estás ubicada...`.
|
||||||
|
- La misma lógica está incluida en los enrolamientos de Windows y Linux.
|
||||||
|
- Si faltan el sexo o los metadatos de ubicación, permanecen los fallbacks
|
||||||
|
neutrales documentados y el inicio de sesión no se bloquea.
|
||||||
|
|
||||||
|
Los perfiles de usuario locales y el rollback de roaming introducido en 0.6.4
|
||||||
|
no cambian en esta versión.
|
||||||
+107
-136
@@ -1,74 +1,70 @@
|
|||||||
# User roaming por rol en Azure
|
# User roaming por rol en Azure (opcional)
|
||||||
|
|
||||||
El despliegue Azure puede crear almacenamiento privado y políticas distintas para
|
> **Estado predeterminado:** desactivado. Todas las cuentas `AL`, `AD`, `DO` y
|
||||||
cada tipo de cuenta SGU:
|
> cualquier otra cuenta de dominio usan un perfil local normal bajo `C:\Users`.
|
||||||
|
> Escritorio y Documentos permanecen en el disco del equipo; no se mapean shares
|
||||||
|
> de perfiles ni se monta un contenedor FSLogix.
|
||||||
|
|
||||||
|
La implementación se conserva para un posible piloto futuro, pero no forma parte
|
||||||
|
del enrolamiento normal. Activarla requiere dos decisiones explícitas: desplegar
|
||||||
|
la infraestructura con `-DeployUserRoaming $true
|
||||||
|
-AcknowledgeUserRoamingRisks` y después ejecutar el configurador con
|
||||||
|
`-AcknowledgeUserRoamingRisks`.
|
||||||
|
|
||||||
|
## Diseño opcional
|
||||||
|
|
||||||
|
Cuando se habilita expresamente, el comportamiento es:
|
||||||
|
|
||||||
| Rol | Datos conservados | Implementación |
|
| 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. |
|
| `AL` | `Desktop` y `Documents` | Redirección de carpetas a Azure Files; el resto del perfil sigue local. |
|
||||||
| `AD` / `DO` | Perfil completo | FSLogix: montaje directo del VHDX con endpoint privado o Cloud Cache local sincronizada con Azure Files cuando el endpoint es público. |
|
| `AD` / `DO` | Perfil completo | FSLogix VHDX: montaje directo con endpoint privado o Cloud Cache con endpoint público. |
|
||||||
|
|
||||||
Linux conserva su solución nativa de directorios personales; no intenta montar
|
Linux conserva su directorio personal nativo y no monta un perfil de Windows.
|
||||||
un perfil de Windows.
|
|
||||||
|
|
||||||
## Arquitectura y seguridad
|
`infra/azure/main.bicep` puede crear una cuenta `StorageV2` dedicada con shares
|
||||||
|
`profiles` y `redirected`, soft delete de 14 días, private endpoint y zona
|
||||||
|
`privatelink.file.<sufijo-de-Azure>`. Su valor predeterminado
|
||||||
|
`deployUserRoaming=false` evita crear estos recursos. La cuenta de staging usada
|
||||||
|
para paquetes no se reutiliza automáticamente como almacenamiento de perfiles.
|
||||||
|
|
||||||
`infra/azure/main.bicep` crea, por omisión, una cuenta `StorageV2` dedicada con
|
Los clientes de un endpoint privado requieren P2S u otra ruta privada a la VNet.
|
||||||
dos recursos SMB (`profiles` y `redirected`), soft delete de 14 días, un private
|
Un endpoint público exige resolución DNS, salida TCP 445 y reglas de red de
|
||||||
endpoint y la zona `privatelink.file.<sufijo-de-Azure>` ligada a la VNet. El
|
Storage limitadas a redes institucionales. El modo `Auto` selecciona `Direct`
|
||||||
endpoint público queda deshabilitado. La VNet entrega el DNS del controlador de
|
para endpoint privado y `CloudCache` para uno público. `CCDLocations` y
|
||||||
dominio a los clientes P2S para que el nombre de Azure Files resuelva a la IP
|
`VHDLocations` son mutuamente excluyentes.
|
||||||
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
|
Azure Files se integra con AD DS mediante Kerberos AES-256. Las ACL de la raíz
|
||||||
VNet. El modo de enrolamiento público directo no puede usar estos recursos y el
|
usan `CREATOR OWNER` para aislar los directorios de usuarios. La clave de la
|
||||||
script de despliegue exige `-DeployUserRoaming $false` cuando no se despliega el
|
cuenta se usa sólo en memoria durante la creación de ACL y no se guarda en GPO.
|
||||||
gateway P2S.
|
|
||||||
|
|
||||||
Para una cuenta SMB ya existente cuyo endpoint público se limite mediante el
|
## Riesgos que deben aceptarse
|
||||||
firewall de Storage a las redes institucionales, el configurador admite
|
|
||||||
`-EndpointAccess Public`. En ese modo no exige una dirección RFC1918, pero sigue
|
|
||||||
comprobando resolución DNS y TCP 445. La creación de los shares y las reglas de
|
|
||||||
red de la cuenta existente se realiza antes de ejecutar el configurador. El host
|
|
||||||
que aplica la integración AD/ACL debe conservar acceso temporal a SMB durante
|
|
||||||
esa ejecución; después se establece `defaultAction=Deny` y se deja únicamente la
|
|
||||||
red institucional autorizada. En el despliegue LCI actual esa regla final es
|
|
||||||
`200.13.89.0/24`.
|
|
||||||
|
|
||||||
El modo `-StaffProfileStorageMode Auto` es el predeterminado. Selecciona
|
El piloto de septiembre de 2026 confirmó que esta función puede aumentar de
|
||||||
`Direct` con un endpoint privado y `CloudCache` con uno público. Cloud Cache
|
forma considerable el tiempo de inicio y cierre de sesión. Antes de activarla se
|
||||||
mantiene el VHDX de trabajo en `C:\ProgramData\FSLogix\Cache` y sincroniza sus
|
deben aceptar y probar, como mínimo, estos escenarios:
|
||||||
cambios con Azure Files, evitando que cada operación del perfil dependa de la
|
|
||||||
latencia WAN de SMB. `CCDLocations` y `VHDLocations` son mutuamente excluyentes:
|
|
||||||
el configurador elimina de forma efectiva el valor obsoleto al cambiar de modo,
|
|
||||||
incluso en clientes que ya habían aplicado la GPO anterior. Puede forzarse un
|
|
||||||
modo con `-StaffProfileStorageMode Direct` o `CloudCache`, pero `Direct` sobre
|
|
||||||
un endpoint público sólo debe usarse para diagnóstico.
|
|
||||||
|
|
||||||
Cloud Cache necesita espacio local suficiente para el perfil (30 GB de máximo
|
- latencia o indisponibilidad de Azure Files que mantiene `Welcome` o `Sign out`
|
||||||
por omisión), conserva la caché tras cerrar sesión y exige al menos un proveedor
|
durante varios minutos;
|
||||||
remoto sano al registrar o desmontar el perfil. Si el proveedor no completa la
|
- VHDX remoto en estado abierto, pendiente de eliminación o de creación, con
|
||||||
sincronización, el cierre espera como máximo 60 segundos y después conserva la
|
fallos como `0x80070050`;
|
||||||
caché local para reintentar; no la descarta. La GPO también impide el inicio
|
- caché o proxy local obsoleto de Cloud Cache y fallos de registro como
|
||||||
con perfil temporal o local cuando FSLogix falla, para que una incidencia no
|
`0xE000000B`;
|
||||||
produzca dos perfiles divergentes silenciosamente.
|
- recuperación que puede exigir detener por fuerza los servicios FSLogix y
|
||||||
|
reiniciar el equipo;
|
||||||
|
- datos todavía no sincronizados, caché local insuficiente o divergencia si se
|
||||||
|
fuerza una recuperación;
|
||||||
|
- dependencia de DNS, Kerberos, TCP 445, la red y el servicio de Storage tanto
|
||||||
|
para registrar como para desmontar el perfil.
|
||||||
|
|
||||||
Azure Files se integra con el AD DS SGU, no con cuentas o claves guardadas en
|
No se debe desplegar a todos los usuarios sin un piloto de inicio, cierre,
|
||||||
cada cliente. El script configura AES-256 para Kerberos, una ACL de mínimo
|
desconexión, cuota, restauración y pérdida temporal del proveedor. Cloud Cache
|
||||||
privilegio con `CREATOR OWNER`, administradores y los grupos del rol, y permiso
|
reduce I/O WAN síncrono, pero no elimina la dependencia al cerrar sesión.
|
||||||
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.
|
|
||||||
|
|
||||||
Los grupos de rol sólo pueden enumerar la raíz y crear su propio directorio; no
|
## 1. Desplegar la infraestructura opcional
|
||||||
reciben `Modify` en la raíz compartida. El directorio creado hereda `CREATOR
|
|
||||||
OWNER`, de modo que un usuario no puede modificar ni eliminar el contenedor de
|
|
||||||
otro usuario.
|
|
||||||
|
|
||||||
## 1. Desplegar la infraestructura
|
El despliegue normal no incluye user roaming. Para crear deliberadamente sus
|
||||||
|
recursos:
|
||||||
El soporte está activado por omisión para despliegues P2S:
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$azure = .\scripts\Deploy-SguAzureInfrastructure.ps1 `
|
$azure = .\scripts\Deploy-SguAzureInfrastructure.ps1 `
|
||||||
@@ -76,128 +72,103 @@ $azure = .\scripts\Deploy-SguAzureInfrastructure.ps1 `
|
|||||||
-ResourceGroupName 'rg-sgu-lab' `
|
-ResourceGroupName 'rg-sgu-lab' `
|
||||||
-Location 'centralus' `
|
-Location 'centralus' `
|
||||||
-AdministratorUsername 'azureadmin' `
|
-AdministratorUsername 'azureadmin' `
|
||||||
-P2sRootCertificatePath $p2s.RootCertificatePath
|
-P2sRootCertificatePath $p2s.RootCertificatePath `
|
||||||
|
-DeployUserRoaming $true `
|
||||||
$azure.UserRoamingStorageAccountName
|
-AcknowledgeUserRoamingRisks
|
||||||
$azure.FsLogixProfilesSharePath
|
|
||||||
$azure.RedirectedFoldersSharePath
|
|
||||||
$azure.UserRoamingSetupArguments
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Puede definirse un nombre globalmente único con
|
Puede definirse un nombre con `-UserRoamingStorageAccountName`. Si no se pasan
|
||||||
`-UserRoamingStorageAccountName`. Para omitir completamente el almacenamiento:
|
los dos parámetros de aceptación, la infraestructura de perfiles no se crea.
|
||||||
|
|
||||||
```powershell
|
|
||||||
-DeployUserRoaming $false
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Unir Azure Files al dominio y publicar las GPO
|
## 2. Unir Azure Files al dominio y publicar las GPO
|
||||||
|
|
||||||
Primero termine `Initialize-SguDomainController.ps1`. Después, en Windows
|
Primero termine `Initialize-SguDomainController.ps1`. Después, en Windows
|
||||||
PowerShell 5.1 elevado dentro del DC:
|
PowerShell 5.1 elevado dentro del DC, instale Azure PowerShell y el módulo oficial
|
||||||
|
[AzFilesHybrid](https://github.com/Azure-Samples/azure-files-samples/releases),
|
||||||
1. Instale Azure PowerShell actual (`Az.Accounts` y `Az.Storage` 8.1 o posterior).
|
y ejecute:
|
||||||
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
|
```powershell
|
||||||
$storageAccountName = '<valor UserRoamingStorageAccountName del despliegue>'
|
|
||||||
$parameters = @{
|
$parameters = @{
|
||||||
SubscriptionId = '00000000-0000-0000-0000-000000000000'
|
SubscriptionId = '00000000-0000-0000-0000-000000000000'
|
||||||
ResourceGroupName = 'rg-sgu-lab'
|
ResourceGroupName = 'rg-sgu-lab'
|
||||||
StorageAccountName = $storageAccountName
|
StorageAccountName = '<cuenta de perfiles>'
|
||||||
FsLogixProfilesShareName = 'profiles'
|
FsLogixProfilesShareName = 'profiles'
|
||||||
RedirectedFoldersShareName = 'redirected'
|
RedirectedFoldersShareName = 'redirected'
|
||||||
AzFilesHybridModulePath = 'C:\Admin\AzFilesHybrid'
|
AzFilesHybridModulePath = 'C:\Admin\AzFilesHybrid'
|
||||||
UseDeviceAuthentication = $true
|
UseDeviceAuthentication = $true
|
||||||
|
AcknowledgeUserRoamingRisks = $true
|
||||||
}
|
}
|
||||||
.\payload\scripts\Enable-SguAzureUserRoaming.ps1 @parameters
|
.\payload\scripts\Enable-SguAzureUserRoaming.ps1 @parameters
|
||||||
```
|
```
|
||||||
|
|
||||||
Para reutilizar una cuenta con endpoint público:
|
Para una cuenta autorizada con endpoint público:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$parameters.EndpointAccess = 'Public'
|
$parameters.EndpointAccess = 'Public'
|
||||||
$parameters.StaffProfileStorageMode = 'Auto' # resuelve a CloudCache
|
$parameters.StaffProfileStorageMode = 'Auto'
|
||||||
.\payload\scripts\Enable-SguAzureUserRoaming.ps1 @parameters
|
.\payload\scripts\Enable-SguAzureUserRoaming.ps1 @parameters
|
||||||
```
|
```
|
||||||
|
|
||||||
La operación es idempotente. Crea `OU=AzureFilesConfig`, une la cuenta de
|
El script crea las GPO `SGU - AL redirected folders` y `SGU - AD-DO FSLogix
|
||||||
almacenamiento al bosque y publica dos GPO:
|
profiles`, y vuelve a habilitar el enlace de instalación de FSLogix si ya existe.
|
||||||
|
No borra perfiles locales existentes salvo que se proporcione deliberadamente
|
||||||
|
`-DeleteExistingStaffLocalProfiles`.
|
||||||
|
|
||||||
- `SGU - AL redirected folders`, ligada a `OU=Alumnos`;
|
## 3. Instalar FSLogix sólo para un piloto
|
||||||
- `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
|
FSLogix no forma parte de Windows. Valide su licencia y descargue
|
||||||
de un piloto real, migre o respalde su contenido y elimínelos manualmente. El
|
`FSLogixAppsSetup.exe` directamente de Microsoft. Puede pasarse como quinto
|
||||||
switch `-DeleteExistingStaffLocalProfiles` habilita la opción de FSLogix que los
|
argumento al enrolamiento Azure o publicarse con:
|
||||||
elimina al aplicar el contenedor; úselo sólo después de respaldar y validar.
|
|
||||||
|
|
||||||
La identidad de Azure Files queda con contraseña sin expiración en la OU
|
|
||||||
dedicada para evitar que una cuenta de equipo sin agente pierda Kerberos a los
|
|
||||||
30 días. Si la política exige rotación, use periódicamente el procedimiento
|
|
||||||
oficial `Update-AzStorageAccountADObjectPassword` y no cambie sólo un lado.
|
|
||||||
|
|
||||||
## 3. Instalar FSLogix en los clientes Windows
|
|
||||||
|
|
||||||
FSLogix no forma parte de Windows y su licencia debe validarse para las ediciones
|
|
||||||
y suscripciones usadas. Descargue `FSLogixAppsSetup.exe` directamente de
|
|
||||||
Microsoft y agréguelo a la imagen base, o páselo como quinto argumento al
|
|
||||||
enrolamiento Azure:
|
|
||||||
|
|
||||||
```bat
|
|
||||||
Start-SguAzureClientEnrollment.cmd 10.77.0.4 C:\SGU\vpn.zip C:\SGU\client.pfx C:\SGU\root.cer C:\SGU\FSLogixAppsSetup.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
`Install-SguFsLogix.ps1` exige una firma Authenticode válida de Microsoft,
|
|
||||||
instala silenciosamente sin reiniciar y comprueba `frxsvc` y `frx.exe`. Si la
|
|
||||||
imagen ya contiene FSLogix, el instalador puede omitirse. Microsoft recomienda
|
|
||||||
mantener la versión más reciente; FSLogix no se actualiza mediante Windows
|
|
||||||
Update.
|
|
||||||
|
|
||||||
Para cubrir también equipos ya unidos al dominio —incluidos los que estaban
|
|
||||||
apagados durante el despliegue— publique el instalador como script de inicio de
|
|
||||||
equipo:
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\payload\scripts\Publish-SguFsLogixClientDeployment.ps1
|
.\payload\scripts\Publish-SguFsLogixClientDeployment.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
El script descarga el paquete oficial FSLogix 26.08, valida la firma
|
El publicador valida firma y SHA-256 y crea `SGU - FSLogix client deployment`.
|
||||||
Authenticode de Microsoft y el SHA-256 esperado, y crea la GPO
|
El rollback descrito abajo deshabilita su enlace para que no se instale en más
|
||||||
`SGU - FSLogix client deployment` ligada a `OU=Laboratorio`. El ejecutable queda
|
equipos.
|
||||||
en el SYSVOL de esa GPO y se instala de forma idempotente durante el arranque;
|
|
||||||
no usa tareas programadas ni requiere que el equipo estuviera encendido al
|
|
||||||
publicarla. Si `frxsvc` y `frx.exe` ya existen, el script de inicio termina sin
|
|
||||||
reinstalar.
|
|
||||||
|
|
||||||
## 4. Validar un piloto
|
## 4. Desactivar y volver a perfiles locales
|
||||||
|
|
||||||
Después de `gpupdate /force` y reiniciar un cliente:
|
Ejecute desde Windows PowerShell 5.1 elevado en el controlador de dominio:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\payload\scripts\Disable-SguAzureUserRoaming.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Es idempotente y realiza lo siguiente:
|
||||||
|
|
||||||
|
- fija `Profiles\Enabled=0` y `ODFC\Enabled=0`;
|
||||||
|
- deshabilita `frxsvc` y `frxccds` mediante GPO;
|
||||||
|
- elimina `CCDLocations`, `VHDLocations` y valores de contenedor específicos de
|
||||||
|
los grupos `SGU-Administrativos` y `SGU-Docentes`;
|
||||||
|
- restaura Escritorio y Documentos de alumnos a
|
||||||
|
`%USERPROFILE%\Desktop` y `%USERPROFILE%\Documents`;
|
||||||
|
- deshabilita el enlace de despliegue del cliente FSLogix;
|
||||||
|
- limpia `profilePath`, `homeDirectory` y `homeDrive` de los usuarios de las OU
|
||||||
|
SGU, salvo que se use `-PreserveLegacyUserMappings`.
|
||||||
|
|
||||||
|
Después aplique `gpupdate /force` y reinicie cada Windows administrado antes de
|
||||||
|
probar un nuevo inicio de sesión. El primer ingreso vuelve a crear o reutilizar
|
||||||
|
un perfil local. El script no borra automáticamente VHDX, shares de Azure ni
|
||||||
|
cachés existentes: conservarlos permite recuperación y evita destrucción de
|
||||||
|
datos. Su eliminación debe ser una decisión separada y posterior a la validación.
|
||||||
|
|
||||||
|
## 5. Validar un piloto futuro
|
||||||
|
|
||||||
|
Después de `gpupdate /force` y reiniciar un cliente de prueba:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Resolve-DnsName "$($azure.UserRoamingStorageAccountName).file.core.windows.net"
|
Resolve-DnsName "$($azure.UserRoamingStorageAccountName).file.core.windows.net"
|
||||||
Test-NetConnection "$($azure.UserRoamingStorageAccountName).file.core.windows.net" -Port 445
|
Test-NetConnection "$($azure.UserRoamingStorageAccountName).file.core.windows.net" -Port 445
|
||||||
Get-Service frxsvc
|
Get-Service frxsvc
|
||||||
Get-ItemProperty 'HKLM:\SOFTWARE\FSLogix\Profiles\ObjectSpecific\<SID-del-grupo>'
|
Get-ItemProperty 'HKLM:\SOFTWARE\FSLogix\Profiles\ObjectSpecific\<SID-del-grupo>'
|
||||||
Get-ItemProperty 'HKLM:\SOFTWARE\SGU\FSLogixDeployment'
|
|
||||||
& 'C:\Program Files\FSLogix\Apps\frx.exe' list-redirects
|
& 'C:\Program Files\FSLogix\Apps\frx.exe' list-redirects
|
||||||
```
|
```
|
||||||
|
|
||||||
En modo privado, la resolución debe devolver la IP privada del endpoint; en
|
Revise `%ProgramData%\FSLogix\Logs\Profile`, pruebe inicio y cierre varias veces,
|
||||||
modo público debe devolver una dirección pública y TCP 445 debe estar permitido
|
simule indisponibilidad y valide restauración. No active el borrado de perfiles
|
||||||
desde la red institucional. En Cloud Cache compruebe además que existe
|
locales ni amplíe el alcance antes de superar esas pruebas.
|
||||||
`C:\ProgramData\FSLogix\Cache` y que el registro efectivo contiene
|
|
||||||
`CCDLocations`, pero no `VHDLocations`. Inicie primero con un
|
|
||||||
`AL` nuevo y confirme las ubicaciones de Escritorio/Documentos; luego pruebe un
|
|
||||||
`AD` o `DO` sin perfil local previo y confirme la creación de un VHDX en
|
|
||||||
`profiles`. Revise `%ProgramData%\FSLogix\Logs\Profile` para confirmar que el
|
|
||||||
perfil se registró sin `ERROR: 258`, perfil temporal ni retorno a `C:\Users`.
|
|
||||||
No active el borrado de perfiles locales ni despliegue a todos los
|
|
||||||
usuarios hasta probar inicio, cierre, desconexión de VPN, cuota y restauración.
|
|
||||||
|
|
||||||
Referencias de Microsoft:
|
Referencias de Microsoft:
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
# Fondo de bienvenida personalizado
|
# Fondo de bienvenida personalizado
|
||||||
|
|
||||||
El enrolamiento instala un fondo base azul, las familias `Indivisa Text Sans` y
|
El enrolamiento instala un fondo base azul que ya contiene el logotipo blanco
|
||||||
`Indivisa Text Serif`, y un generador local. La GPO de equipos
|
oficial de Universidad La Salle México, las familias `Indivisa Text Sans` y
|
||||||
|
`Indivisa Text Serif`, y un generador local. El logotipo está horneado en
|
||||||
|
`assets/branding/darkblue.jpg`: el generador no carga, redimensiona ni compone
|
||||||
|
otro logo durante el inicio de sesión. La GPO de equipos
|
||||||
`SGU - Windows client experience` ejecuta el generador al abrir cada sesión y
|
`SGU - Windows client experience` ejecuta el generador al abrir cada sesión y
|
||||||
mantiene el fondo base en la pantalla de bloqueo.
|
mantiene el fondo base en la pantalla de bloqueo.
|
||||||
|
|
||||||
@@ -28,12 +31,18 @@ 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 `Acceso`. La ubicación se divide en dos
|
||||||
reglas:
|
líneas: la sala ocupa la primera y la dependencia aparece debajo con un tamaño
|
||||||
|
menor. Sigue estas reglas:
|
||||||
|
|
||||||
1. Con `location` y OU: `Acceso al Aula Flexible del Centro de Experiencia Digital.`
|
1. Sin sexo: `Acceso a la Sala de Aplicaciones`, seguido por
|
||||||
2. Con sólo uno de los datos: muestra únicamente el dato disponible.
|
`del Laboratorio de Cómputo de Ingeniería.`
|
||||||
3. Sin ambos: el texto adaptado `Bienvenido/Bienvenida al Laboratorio...`; sin
|
2. Masculino: `Estás ubicado en el Aula Flexible`, seguido por
|
||||||
|
`del Centro de Experiencia Digital.`
|
||||||
|
3. Femenino: `Estás ubicada en la Sala de Redes`, seguido por
|
||||||
|
`del Laboratorio de Cómputo de Ingeniería.`
|
||||||
|
4. Con sólo uno de los datos: muestra únicamente el dato disponible.
|
||||||
|
5. Sin ambos: el texto adaptado `Bienvenido/Bienvenida al Laboratorio...`; sin
|
||||||
sexo disponible, la forma neutral `Acceso al Laboratorio de Cómputo de Ingeniería.`
|
sexo disponible, la forma neutral `Acceso al Laboratorio de Cómputo de Ingeniería.`
|
||||||
|
|
||||||
La ausencia de AD, de un atributo o de una tipografía nunca bloquea la sesión.
|
La ausencia de AD, de un atributo o de una tipografía nunca bloquea la sesión.
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ param publicEnrollmentSourceAddressPrefixes array = []
|
|||||||
param administratorSourceAddressPrefix string = ''
|
param administratorSourceAddressPrefix string = ''
|
||||||
|
|
||||||
@description('Deploy private Azure Files shares for SGU user roaming. Clients require P2S or another private route into the VNet.')
|
@description('Deploy private Azure Files shares for SGU user roaming. Clients require P2S or another private route into the VNet.')
|
||||||
param deployUserRoaming bool = true
|
param deployUserRoaming bool = false
|
||||||
|
|
||||||
@description('Optional globally unique storage account name for roaming data. Leave empty to derive a stable name from the resource group.')
|
@description('Optional globally unique storage account name for roaming data. Leave empty to derive a stable name from the resource group.')
|
||||||
param userRoamingStorageAccountName string = ''
|
param userRoamingStorageAccountName string = ''
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ 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,
|
[bool]$DeployUserRoaming = $false,
|
||||||
|
[switch]$AcknowledgeUserRoamingRisks,
|
||||||
[ValidatePattern('^$|^[a-z0-9]{3,24}$')]
|
[ValidatePattern('^$|^[a-z0-9]{3,24}$')]
|
||||||
[string]$UserRoamingStorageAccountName = '',
|
[string]$UserRoamingStorageAccountName = '',
|
||||||
[ValidatePattern('^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$')]
|
[ValidatePattern('^[a-z0-9](?:[a-z0-9-]{1,61}[a-z0-9])$')]
|
||||||
@@ -47,6 +48,9 @@ if (-not $AdministratorPassword) {
|
|||||||
if ($DeployUserRoaming -and -not $DeployVpnGateway) {
|
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.'
|
throw 'Azure user roaming requires the P2S gateway deployed by this template so clients can reach the private Azure Files endpoint. Use -DeployUserRoaming $false with direct public enrollment.'
|
||||||
}
|
}
|
||||||
|
if ($DeployUserRoaming -and -not $AcknowledgeUserRoamingRisks) {
|
||||||
|
throw 'User roaming is disabled by default. To provision it, also pass -AcknowledgeUserRoamingRisks after reviewing the sign-in, sign-out, storage-availability, stale-cache, and recovery risks in docs/user-roaming.md.'
|
||||||
|
}
|
||||||
foreach ($shareName in @($FsLogixProfilesShareName, $RedirectedFoldersShareName)) {
|
foreach ($shareName in @($FsLogixProfilesShareName, $RedirectedFoldersShareName)) {
|
||||||
if ($shareName.Contains('--')) {
|
if ($shareName.Contains('--')) {
|
||||||
throw "Azure Files share names cannot contain consecutive hyphens: $shareName"
|
throw "Azure Files share names cannot contain consecutive hyphens: $shareName"
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
#Requires -Version 5.1
|
||||||
|
[CmdletBinding(SupportsShouldProcess)]
|
||||||
|
param(
|
||||||
|
[string]$DomainController = $env:COMPUTERNAME,
|
||||||
|
[string]$StudentOuName = 'Alumnos',
|
||||||
|
[string]$ProfessorOuName = 'Docentes',
|
||||||
|
[string]$AdministrativeOuName = 'Administrativos',
|
||||||
|
[string]$LaboratoryOuName = 'Laboratorio',
|
||||||
|
[string]$StudentGroupName = 'SGU-Alumnos',
|
||||||
|
[string]$ProfessorGroupName = 'SGU-Docentes',
|
||||||
|
[string]$AdministrativeGroupName = 'SGU-Administrativos',
|
||||||
|
[string]$StudentGpoName = 'SGU - AL redirected folders',
|
||||||
|
[string]$StaffGpoName = 'SGU - AD-DO FSLogix profiles',
|
||||||
|
[string]$FsLogixDeploymentGpoName = 'SGU - FSLogix client deployment',
|
||||||
|
[switch]$PreserveLegacyUserMappings
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Assert-Administrator {
|
||||||
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
|
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
||||||
|
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||||
|
throw 'Run this script from an elevated Windows PowerShell 5.1 session on the SGU domain controller.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-SguPolicyValue {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][string]$GpoName,
|
||||||
|
[Parameter(Mandatory)][string]$DomainName,
|
||||||
|
[Parameter(Mandatory)][string]$Server,
|
||||||
|
[Parameter(Mandatory)][string]$Key,
|
||||||
|
[Parameter(Mandatory)][string]$ValueName,
|
||||||
|
[Parameter(Mandatory)][ValidateSet('DWord','String','ExpandString')][string]$Type,
|
||||||
|
[Parameter(Mandatory)]$Value
|
||||||
|
)
|
||||||
|
if ($PSCmdlet.ShouldProcess("$GpoName :: $Key\\$ValueName", "Set rollback value $Value")) {
|
||||||
|
Remove-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
|
||||||
|
-Key $Key -ValueName "**del.$ValueName" -ErrorAction SilentlyContinue | Out-Null
|
||||||
|
Set-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
|
||||||
|
-Key $Key -ValueName $ValueName -Type $Type -Value $Value | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Remove-SguPolicyValue {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)][string]$GpoName,
|
||||||
|
[Parameter(Mandatory)][string]$DomainName,
|
||||||
|
[Parameter(Mandatory)][string]$Server,
|
||||||
|
[Parameter(Mandatory)][string]$Key,
|
||||||
|
[Parameter(Mandatory)][string]$ValueName
|
||||||
|
)
|
||||||
|
if ($PSCmdlet.ShouldProcess("$GpoName :: $Key\\$ValueName", 'Remove roaming value')) {
|
||||||
|
Remove-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
|
||||||
|
-Key $Key -ValueName $ValueName -ErrorAction SilentlyContinue | Out-Null
|
||||||
|
Set-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
|
||||||
|
-Key $Key -ValueName "**del.$ValueName" -Type String -Value '' | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-Administrator
|
||||||
|
Import-Module ActiveDirectory -ErrorAction Stop
|
||||||
|
Import-Module GroupPolicy -ErrorAction Stop
|
||||||
|
|
||||||
|
$domain = Get-ADDomain -Server $DomainController
|
||||||
|
$domainName = $domain.DNSRoot
|
||||||
|
$domainDn = $domain.DistinguishedName
|
||||||
|
$laboratoryOuDn = "OU=$LaboratoryOuName,$domainDn"
|
||||||
|
$usersOuDn = "OU=Usuarios-SGU,$domainDn"
|
||||||
|
$studentOuDn = "OU=$StudentOuName,$usersOuDn"
|
||||||
|
$professorOuDn = "OU=$ProfessorOuName,$usersOuDn"
|
||||||
|
$administrativeOuDn = "OU=$AdministrativeOuName,$usersOuDn"
|
||||||
|
|
||||||
|
foreach ($ou in $laboratoryOuDn,$studentOuDn,$professorOuDn,$administrativeOuDn) {
|
||||||
|
Get-ADOrganizationalUnit -Identity $ou -Server $DomainController -ErrorAction Stop | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$staffGpo = Get-GPO -Name $StaffGpoName -Domain $domainName -Server $DomainController `
|
||||||
|
-ErrorAction SilentlyContinue
|
||||||
|
if (-not $staffGpo -and $PSCmdlet.ShouldProcess($StaffGpoName, 'Create local-profile enforcement GPO')) {
|
||||||
|
$staffGpo = New-GPO -Name $StaffGpoName -Domain $domainName -Server $DomainController
|
||||||
|
}
|
||||||
|
if (-not $staffGpo) {
|
||||||
|
throw "The GPO '$StaffGpoName' does not exist and was not created."
|
||||||
|
}
|
||||||
|
$staffLink = @(Get-GPInheritance -Target $laboratoryOuDn -Domain $domainName `
|
||||||
|
-Server $DomainController).GpoLinks |
|
||||||
|
Where-Object DisplayName -eq $StaffGpoName | Select-Object -First 1
|
||||||
|
if (-not $staffLink -and $PSCmdlet.ShouldProcess($laboratoryOuDn, "Link '$StaffGpoName'")) {
|
||||||
|
New-GPLink -Name $StaffGpoName -Target $laboratoryOuDn -Domain $domainName `
|
||||||
|
-Server $DomainController -LinkEnabled Yes | Out-Null
|
||||||
|
}
|
||||||
|
elseif ($staffLink -and -not [bool]$staffLink.Enabled -and
|
||||||
|
$PSCmdlet.ShouldProcess($laboratoryOuDn, "Enable '$StaffGpoName' rollback link")) {
|
||||||
|
Set-GPLink -Name $StaffGpoName -Target $laboratoryOuDn -Domain $domainName `
|
||||||
|
-Server $DomainController -LinkEnabled Yes | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$profilesRoot = 'HKLM\SOFTWARE\FSLogix\Profiles'
|
||||||
|
Set-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName -Server $DomainController `
|
||||||
|
-Key $profilesRoot -ValueName Enabled -Type DWord -Value 0
|
||||||
|
Set-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName -Server $DomainController `
|
||||||
|
-Key 'HKLM\SOFTWARE\FSLogix\ODFC' -ValueName Enabled -Type DWord -Value 0
|
||||||
|
foreach ($serviceName in 'frxsvc','frxccds') {
|
||||||
|
Set-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName -Server $DomainController `
|
||||||
|
-Key "HKLM\SYSTEM\CurrentControlSet\Services\$serviceName" `
|
||||||
|
-ValueName Start -Type DWord -Value 4
|
||||||
|
}
|
||||||
|
Remove-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName -Server $DomainController `
|
||||||
|
-Key 'HKLM\SOFTWARE\FSLogix\Apps' -ValueName VHDCompactDisk
|
||||||
|
|
||||||
|
$profileValueNames = @(
|
||||||
|
'CCDLocations','VHDLocations','CcdUnregisterTimeout','ClearCacheOnForcedUnregister',
|
||||||
|
'ClearCacheOnLogoff','HealthyProvidersRequiredForRegister',
|
||||||
|
'HealthyProvidersRequiredForUnregister','PreventLoginWithFailure',
|
||||||
|
'PreventLoginWithTempProfile','DeleteLocalProfileWhenVHDShouldApply',
|
||||||
|
'FlipFlopProfileDirectoryName','IsDynamic','LockedRetryCount','LockedRetryInterval',
|
||||||
|
'ProfileType','ReAttachIntervalSeconds','ReAttachRetryCount','SizeInMBs','VolumeType',
|
||||||
|
'VHDCompactDisk'
|
||||||
|
)
|
||||||
|
$roleGroups = @(
|
||||||
|
Get-ADGroup -Identity "CN=$ProfessorGroupName,$professorOuDn" -Server $DomainController
|
||||||
|
Get-ADGroup -Identity "CN=$AdministrativeGroupName,$administrativeOuDn" -Server $DomainController
|
||||||
|
)
|
||||||
|
foreach ($group in $roleGroups) {
|
||||||
|
$key = "$profilesRoot\ObjectSpecific\$($group.SID.Value)"
|
||||||
|
Set-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName -Server $DomainController `
|
||||||
|
-Key $key -ValueName Enabled -Type DWord -Value 0
|
||||||
|
foreach ($valueName in $profileValueNames) {
|
||||||
|
Remove-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName `
|
||||||
|
-Server $DomainController -Key $key -ValueName $valueName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Set-SguPolicyValue -GpoName $StaffGpoName -DomainName $domainName -Server $DomainController `
|
||||||
|
-Key 'HKLM\SOFTWARE\SGU\UserRoaming' -ValueName StaffProfileStorageMode `
|
||||||
|
-Type String -Value Disabled
|
||||||
|
|
||||||
|
$studentGpo = Get-GPO -Name $StudentGpoName -Domain $domainName -Server $DomainController `
|
||||||
|
-ErrorAction SilentlyContinue
|
||||||
|
if ($studentGpo) {
|
||||||
|
$studentLink = @(Get-GPInheritance -Target $studentOuDn -Domain $domainName `
|
||||||
|
-Server $DomainController).GpoLinks |
|
||||||
|
Where-Object DisplayName -eq $StudentGpoName | Select-Object -First 1
|
||||||
|
if ($studentLink -and -not [bool]$studentLink.Enabled -and
|
||||||
|
$PSCmdlet.ShouldProcess($studentOuDn, "Enable '$StudentGpoName' local-path rollback")) {
|
||||||
|
Set-GPLink -Name $StudentGpoName -Target $studentOuDn -Domain $domainName `
|
||||||
|
-Server $DomainController -LinkEnabled Yes | Out-Null
|
||||||
|
}
|
||||||
|
$shellFoldersKey = 'HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders'
|
||||||
|
Set-SguPolicyValue -GpoName $StudentGpoName -DomainName $domainName -Server $DomainController `
|
||||||
|
-Key $shellFoldersKey -ValueName Desktop -Type ExpandString `
|
||||||
|
-Value '%USERPROFILE%\Desktop'
|
||||||
|
Set-SguPolicyValue -GpoName $StudentGpoName -DomainName $domainName -Server $DomainController `
|
||||||
|
-Key $shellFoldersKey -ValueName Personal -Type ExpandString `
|
||||||
|
-Value '%USERPROFILE%\Documents'
|
||||||
|
Remove-SguPolicyValue -GpoName $StudentGpoName -DomainName $domainName `
|
||||||
|
-Server $DomainController -Key 'HKCU\Software\Policies\Microsoft\Windows\NetCache' `
|
||||||
|
-ValueName DisableFRAdminPin
|
||||||
|
}
|
||||||
|
|
||||||
|
$deploymentGpo = Get-GPO -Name $FsLogixDeploymentGpoName -Domain $domainName `
|
||||||
|
-Server $DomainController -ErrorAction SilentlyContinue
|
||||||
|
if ($deploymentGpo) {
|
||||||
|
$deploymentLink = @(Get-GPInheritance -Target $laboratoryOuDn -Domain $domainName `
|
||||||
|
-Server $DomainController).GpoLinks |
|
||||||
|
Where-Object DisplayName -eq $FsLogixDeploymentGpoName | Select-Object -First 1
|
||||||
|
if ($deploymentLink -and [bool]$deploymentLink.Enabled -and
|
||||||
|
$PSCmdlet.ShouldProcess($laboratoryOuDn, "Disable '$FsLogixDeploymentGpoName'")) {
|
||||||
|
Set-GPLink -Name $FsLogixDeploymentGpoName -Target $laboratoryOuDn `
|
||||||
|
-Domain $domainName -Server $DomainController -LinkEnabled No | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$clearedUsers = [Collections.Generic.List[string]]::new()
|
||||||
|
if (-not $PreserveLegacyUserMappings) {
|
||||||
|
foreach ($roleOu in $studentOuDn,$professorOuDn,$administrativeOuDn) {
|
||||||
|
foreach ($user in Get-ADUser -SearchBase $roleOu -SearchScope Subtree -Filter * `
|
||||||
|
-Server $DomainController -Properties profilePath,homeDirectory,homeDrive) {
|
||||||
|
if ($user.profilePath -or $user.homeDirectory -or $user.homeDrive) {
|
||||||
|
if ($PSCmdlet.ShouldProcess($user.SamAccountName, 'Clear AD profile and home-drive mappings')) {
|
||||||
|
Set-ADUser -Identity $user -Server $DomainController `
|
||||||
|
-Clear profilePath,homeDirectory,homeDrive
|
||||||
|
$clearedUsers.Add($user.SamAccountName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$staffGpo = Get-GPO -Name $StaffGpoName -Domain $domainName -Server $DomainController
|
||||||
|
$studentGpo = Get-GPO -Name $StudentGpoName -Domain $domainName -Server $DomainController `
|
||||||
|
-ErrorAction SilentlyContinue
|
||||||
|
$deploymentLink = @(Get-GPInheritance -Target $laboratoryOuDn -Domain $domainName `
|
||||||
|
-Server $DomainController).GpoLinks |
|
||||||
|
Where-Object DisplayName -eq $FsLogixDeploymentGpoName | Select-Object -First 1
|
||||||
|
[pscustomobject]@{
|
||||||
|
Domain = $domainName
|
||||||
|
UserRoamingEnabled = $false
|
||||||
|
StaffGpoVersion = "$($staffGpo.Computer.DSVersion)/$($staffGpo.Computer.SysVolVersion)"
|
||||||
|
StudentGpoVersion = if ($studentGpo) {
|
||||||
|
"$($studentGpo.User.DSVersion)/$($studentGpo.User.SysVolVersion)"
|
||||||
|
} else { $null }
|
||||||
|
FsLogixDeploymentLinkEnabled = [bool]$deploymentLink.Enabled
|
||||||
|
StaffProfilesEnabled = 0
|
||||||
|
StudentDesktop = '%USERPROFILE%\Desktop'
|
||||||
|
StudentDocuments = '%USERPROFILE%\Documents'
|
||||||
|
ClearedAdUsers = @($clearedUsers)
|
||||||
|
RebootManagedWindowsClients = $true
|
||||||
|
}
|
||||||
@@ -29,10 +29,15 @@ param(
|
|||||||
[int]$FsLogixProfileSizeMiB = 30000,
|
[int]$FsLogixProfileSizeMiB = 30000,
|
||||||
[string]$AzFilesHybridModulePath,
|
[string]$AzFilesHybridModulePath,
|
||||||
[switch]$UseDeviceAuthentication,
|
[switch]$UseDeviceAuthentication,
|
||||||
[switch]$DeleteExistingStaffLocalProfiles
|
[switch]$DeleteExistingStaffLocalProfiles,
|
||||||
|
[switch]$AcknowledgeUserRoamingRisks,
|
||||||
|
[string]$FsLogixDeploymentGpoName = 'SGU - FSLogix client deployment'
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
if (-not $AcknowledgeUserRoamingRisks -and -not $WhatIfPreference) {
|
||||||
|
throw 'User roaming is an optional high-risk feature. Pass -AcknowledgeUserRoamingRisks only after reviewing docs/user-roaming.md, testing sign-in and sign-out, and accepting the documented storage and recovery failure modes.'
|
||||||
|
}
|
||||||
|
|
||||||
function Assert-Administrator {
|
function Assert-Administrator {
|
||||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
@@ -158,6 +163,8 @@ function Set-SguGpoRegistryValue {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if ($PSCmdlet.ShouldProcess("$GpoName :: $Key\\$ValueName", "Set $Type policy value")) {
|
if ($PSCmdlet.ShouldProcess("$GpoName :: $Key\\$ValueName", "Set $Type policy value")) {
|
||||||
|
Remove-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
|
||||||
|
-Key $Key -ValueName "**del.$ValueName" -ErrorAction SilentlyContinue | Out-Null
|
||||||
Set-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
|
Set-GPRegistryValue -Name $GpoName -Domain $DomainName -Server $Server `
|
||||||
-Key $Key -ValueName $ValueName -Type $Type -Value $Value | Out-Null
|
-Key $Key -ValueName $ValueName -Type $Type -Value $Value | Out-Null
|
||||||
}
|
}
|
||||||
@@ -431,6 +438,24 @@ $staffGpo = Ensure-SguGpoLink -Name $StaffGpoName -TargetOuDn $laboratoryOuDn `
|
|||||||
$fsLogixRoot = 'HKLM\SOFTWARE\FSLogix\Profiles'
|
$fsLogixRoot = 'HKLM\SOFTWARE\FSLogix\Profiles'
|
||||||
Set-SguGpoRegistryValue -GpoName $staffGpo.DisplayName -DomainName $domainName `
|
Set-SguGpoRegistryValue -GpoName $staffGpo.DisplayName -DomainName $domainName `
|
||||||
-Server $DomainController -Key $fsLogixRoot -ValueName 'Enabled' -Type DWord -Value 0
|
-Server $DomainController -Key $fsLogixRoot -ValueName 'Enabled' -Type DWord -Value 0
|
||||||
|
Set-SguGpoRegistryValue -GpoName $staffGpo.DisplayName -DomainName $domainName `
|
||||||
|
-Server $DomainController -Key 'HKLM\SYSTEM\CurrentControlSet\Services\frxsvc' `
|
||||||
|
-ValueName 'Start' -Type DWord -Value 2
|
||||||
|
Set-SguGpoRegistryValue -GpoName $staffGpo.DisplayName -DomainName $domainName `
|
||||||
|
-Server $DomainController -Key 'HKLM\SYSTEM\CurrentControlSet\Services\frxccds' `
|
||||||
|
-ValueName 'Start' -Type DWord -Value 2
|
||||||
|
$deploymentGpo = Get-GPO -Name $FsLogixDeploymentGpoName -Domain $domainName `
|
||||||
|
-Server $DomainController -ErrorAction SilentlyContinue
|
||||||
|
if ($deploymentGpo) {
|
||||||
|
$deploymentLink = @(Get-GPInheritance -Target $laboratoryOuDn -Domain $domainName `
|
||||||
|
-Server $DomainController).GpoLinks |
|
||||||
|
Where-Object DisplayName -eq $FsLogixDeploymentGpoName | Select-Object -First 1
|
||||||
|
if ($deploymentLink -and -not [bool]$deploymentLink.Enabled -and
|
||||||
|
$PSCmdlet.ShouldProcess($laboratoryOuDn, "Enable '$FsLogixDeploymentGpoName'")) {
|
||||||
|
Set-GPLink -Name $FsLogixDeploymentGpoName -Target $laboratoryOuDn `
|
||||||
|
-Domain $domainName -Server $DomainController -LinkEnabled Yes | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
$fsLogixValues = [ordered]@{
|
$fsLogixValues = [ordered]@{
|
||||||
Enabled = @{ Type = 'DWord'; Value = 1 }
|
Enabled = @{ Type = 'DWord'; Value = 1 }
|
||||||
DeleteLocalProfileWhenVHDShouldApply = @{
|
DeleteLocalProfileWhenVHDShouldApply = @{
|
||||||
|
|||||||
@@ -196,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',
|
||||||
|
'Disable-SguAzureUserRoaming.ps1',
|
||||||
'Enable-SguAzureUserRoaming.ps1',
|
'Enable-SguAzureUserRoaming.ps1',
|
||||||
'Enable-SguServerRemoteManagement.ps1',
|
'Enable-SguServerRemoteManagement.ps1',
|
||||||
'Get-SguUsageReport.ps1',
|
'Get-SguUsageReport.ps1',
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ function Get-WelcomeLocationText {
|
|||||||
$located = switch ($Gender) {
|
$located = switch ($Gender) {
|
||||||
'Male' { 'Est{0}s ubicado en' -f [char]0x00E1 }
|
'Male' { 'Est{0}s ubicado en' -f [char]0x00E1 }
|
||||||
'Female' { 'Est{0}s ubicada en' -f [char]0x00E1 }
|
'Female' { 'Est{0}s ubicada en' -f [char]0x00E1 }
|
||||||
default { 'Ubicaci{0}n:' -f [char]0x00F3 }
|
default { $null }
|
||||||
}
|
}
|
||||||
$engineeringLab = switch ($Gender) {
|
$engineeringLab = switch ($Gender) {
|
||||||
'Male' { 'Bienvenido al Laboratorio de C{0}mputo de Ingenier{1}a.' -f [char]0x00F3,[char]0x00ED }
|
'Male' { 'Bienvenido al Laboratorio de C{0}mputo de Ingenier{1}a.' -f [char]0x00F3,[char]0x00ED }
|
||||||
@@ -202,7 +202,10 @@ function Get-WelcomeLocationText {
|
|||||||
if ($Room -and $OuName) {
|
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 -eq 'el') {
|
$roomPhrase = if ($Gender) {
|
||||||
|
if ($roomArticle) { "$roomArticle $Room" } else { $Room }
|
||||||
|
}
|
||||||
|
elseif ($roomArticle -eq 'el') {
|
||||||
"al $Room"
|
"al $Room"
|
||||||
}
|
}
|
||||||
elseif ($roomArticle) {
|
elseif ($roomArticle) {
|
||||||
@@ -212,18 +215,27 @@ function Get-WelcomeLocationText {
|
|||||||
"a $Room"
|
"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 "Acceso $roomPhrase $ouPhrase."
|
$primary = if ($Gender) { "$located $roomPhrase" } else { "Acceso $roomPhrase" }
|
||||||
|
return "$primary`n$ouPhrase."
|
||||||
}
|
}
|
||||||
if ($Room) {
|
if ($Room) {
|
||||||
$article = Get-SpanishArticle -Value $Room
|
$article = Get-SpanishArticle -Value $Room
|
||||||
|
if ($Gender) {
|
||||||
$phrase = if ($article) { "$article $Room" } else { $Room }
|
$phrase = if ($article) { "$article $Room" } else { $Room }
|
||||||
return "$located $phrase."
|
return "$located $phrase."
|
||||||
}
|
}
|
||||||
|
$destination = if ($article -eq 'el') { "al $Room" } elseif ($article) { "a $article $Room" } else { "a $Room" }
|
||||||
|
return "Acceso $destination."
|
||||||
|
}
|
||||||
if ($OuName) {
|
if ($OuName) {
|
||||||
$article = Get-SpanishArticle -Value $OuName
|
$article = Get-SpanishArticle -Value $OuName
|
||||||
|
if ($Gender) {
|
||||||
$phrase = if ($article) { "$article $OuName" } else { $OuName }
|
$phrase = if ($article) { "$article $OuName" } else { $OuName }
|
||||||
return "$located $phrase."
|
return "$located $phrase."
|
||||||
}
|
}
|
||||||
|
$destination = if ($article -eq 'el') { "al $OuName" } elseif ($article) { "a $article $OuName" } else { "a $OuName" }
|
||||||
|
return "Acceso $destination."
|
||||||
|
}
|
||||||
return $engineeringLab
|
return $engineeringLab
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,6 +362,9 @@ if (-not $PSBoundParameters.ContainsKey('OrganizationalUnit')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$locationText = Get-WelcomeLocationText -Room $Location -OuName $OrganizationalUnit -Gender $Gender
|
$locationText = Get-WelcomeLocationText -Room $Location -OuName $OrganizationalUnit -Gender $Gender
|
||||||
|
$locationLines = @($locationText -split '\r?\n', 2)
|
||||||
|
$locationPrimaryText = $locationLines[0]
|
||||||
|
$locationSecondaryText = if ($locationLines.Count -gt 1) { $locationLines[1] } else { $null }
|
||||||
|
|
||||||
if (-not $CanvasWidth -or -not $CanvasHeight) {
|
if (-not $CanvasWidth -or -not $CanvasHeight) {
|
||||||
try {
|
try {
|
||||||
@@ -436,6 +451,8 @@ try {
|
|||||||
-PreferredStyle ([Drawing.FontStyle]::Bold -bor [Drawing.FontStyle]::Italic)
|
-PreferredStyle ([Drawing.FontStyle]::Bold -bor [Drawing.FontStyle]::Italic)
|
||||||
$locationFont = New-WelcomeFont -Family $sansFamily -Size ([single](27 * $scale)) `
|
$locationFont = New-WelcomeFont -Family $sansFamily -Size ([single](27 * $scale)) `
|
||||||
-PreferredStyle ([Drawing.FontStyle]::Regular)
|
-PreferredStyle ([Drawing.FontStyle]::Regular)
|
||||||
|
$locationSecondaryFont = New-WelcomeFont -Family $sansFamily -Size ([single](20 * $scale)) `
|
||||||
|
-PreferredStyle ([Drawing.FontStyle]::Regular)
|
||||||
$format = [Drawing.StringFormat]::new()
|
$format = [Drawing.StringFormat]::new()
|
||||||
$format.Alignment = [Drawing.StringAlignment]::Center
|
$format.Alignment = [Drawing.StringAlignment]::Center
|
||||||
$format.LineAlignment = [Drawing.StringAlignment]::Center
|
$format.LineAlignment = [Drawing.StringAlignment]::Center
|
||||||
@@ -447,11 +464,20 @@ try {
|
|||||||
Draw-CenteredText -Graphics $graphics -Text $DisplayName -Font $nameFont `
|
Draw-CenteredText -Graphics $graphics -Text $DisplayName -Font $nameFont `
|
||||||
-Brush $whiteBrush -Bounds ([Drawing.RectangleF]::new($panelX + 30*$scale, $panelY + 64*$scale, $panelWidth - 60*$scale, 105*$scale)) -Format $format
|
-Brush $whiteBrush -Bounds ([Drawing.RectangleF]::new($panelX + 30*$scale, $panelY + 64*$scale, $panelWidth - 60*$scale, 105*$scale)) -Format $format
|
||||||
$graphics.DrawLine($linePen, $panelX + 150*$scale, $panelY + 180*$scale, $panelX + $panelWidth - 150*$scale, $panelY + 180*$scale)
|
$graphics.DrawLine($linePen, $panelX + 150*$scale, $panelY + 180*$scale, $panelX + $panelWidth - 150*$scale, $panelY + 180*$scale)
|
||||||
Draw-CenteredText -Graphics $graphics -Text $locationText -Font $locationFont `
|
if ($locationSecondaryText) {
|
||||||
-Brush $accentBrush -Bounds ([Drawing.RectangleF]::new($panelX + 60*$scale, $panelY + 190*$scale, $panelWidth - 120*$scale, 94*$scale)) -Format $format
|
Draw-CenteredText -Graphics $graphics -Text $locationPrimaryText -Font $locationFont `
|
||||||
|
-Brush $accentBrush -Bounds ([Drawing.RectangleF]::new($panelX + 60*$scale, $panelY + 190*$scale, $panelWidth - 120*$scale, 46*$scale)) -Format $format
|
||||||
|
Draw-CenteredText -Graphics $graphics -Text $locationSecondaryText -Font $locationSecondaryFont `
|
||||||
|
-Brush $accentBrush -Bounds ([Drawing.RectangleF]::new($panelX + 60*$scale, $panelY + 235*$scale, $panelWidth - 120*$scale, 38*$scale)) -Format $format
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Draw-CenteredText -Graphics $graphics -Text $locationPrimaryText -Font $locationFont `
|
||||||
|
-Brush $accentBrush -Bounds ([Drawing.RectangleF]::new($panelX + 60*$scale, $panelY + 195*$scale, $panelWidth - 120*$scale, 78*$scale)) -Format $format
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
$format.Dispose()
|
$format.Dispose()
|
||||||
|
$locationSecondaryFont.Dispose()
|
||||||
$locationFont.Dispose()
|
$locationFont.Dispose()
|
||||||
$nameFont.Dispose()
|
$nameFont.Dispose()
|
||||||
$welcomeFont.Dispose()
|
$welcomeFont.Dispose()
|
||||||
@@ -519,6 +545,8 @@ Write-WelcomeLog -Message ("OK computer={0}; gender={1}; location={2}; ou={3}; o
|
|||||||
Gender = $Gender
|
Gender = $Gender
|
||||||
WelcomeHeading = $welcomeHeading
|
WelcomeHeading = $welcomeHeading
|
||||||
LocationText = $locationText
|
LocationText = $locationText
|
||||||
|
LocationPrimaryText = $locationPrimaryText
|
||||||
|
LocationSecondaryText = $locationSecondaryText
|
||||||
OutputPath = $OutputPath
|
OutputPath = $OutputPath
|
||||||
Applied = -not $SkipApply
|
Applied = -not $SkipApply
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ fi
|
|||||||
article_for() {
|
article_for() {
|
||||||
local value=${1,,}
|
local value=${1,,}
|
||||||
case "$value" in
|
case "$value" in
|
||||||
sala*|aula*|facultad*|unidad*|biblioteca*|oficina*|coordinación*) printf la ;;
|
sala*|facultad*|unidad*|biblioteca*|oficina*|coordinación*) printf la ;;
|
||||||
laboratorio*|centro*|edificio*|campus*|taller*|auditorio*) printf el ;;
|
aula*|laboratorio*|centro*|edificio*|campus*|taller*|auditorio*) printf el ;;
|
||||||
*) printf '' ;;
|
*) printf '' ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -159,29 +159,60 @@ case "$gender" in
|
|||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
welcome_text='Te damos la bienvenida,'
|
welcome_text='Te damos la bienvenida,'
|
||||||
located_text='Ubicación:'
|
located_text=''
|
||||||
engineering_lab_text='Acceso al Laboratorio de Cómputo de Ingeniería.'
|
engineering_lab_text='Acceso al Laboratorio de Cómputo de Ingeniería.'
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
location_primary=''
|
||||||
|
location_secondary=''
|
||||||
if [[ -n $location && -n $organizational_unit ]]; then
|
if [[ -n $location && -n $organizational_unit ]]; then
|
||||||
|
room_article=$(article_for "$location")
|
||||||
|
if [[ -n $gender ]]; then
|
||||||
room_phrase=$(with_article "$location")
|
room_phrase=$(with_article "$location")
|
||||||
|
location_primary="${located_text} ${room_phrase}"
|
||||||
|
elif [[ $room_article == el ]]; then
|
||||||
|
location_primary="Acceso al ${location}"
|
||||||
|
elif [[ -n $room_article ]]; then
|
||||||
|
location_primary="Acceso a ${room_article} ${location}"
|
||||||
|
else
|
||||||
|
location_primary="Acceso a ${location}"
|
||||||
|
fi
|
||||||
ou_article=$(article_for "$organizational_unit")
|
ou_article=$(article_for "$organizational_unit")
|
||||||
if [[ $ou_article == el ]]; then
|
if [[ $ou_article == el ]]; then
|
||||||
ou_phrase="del ${organizational_unit}"
|
location_secondary="del ${organizational_unit}."
|
||||||
elif [[ -n $ou_article ]]; then
|
elif [[ -n $ou_article ]]; then
|
||||||
ou_phrase="de ${ou_article} ${organizational_unit}"
|
location_secondary="de ${ou_article} ${organizational_unit}."
|
||||||
else
|
else
|
||||||
ou_phrase="de ${organizational_unit}"
|
location_secondary="de ${organizational_unit}."
|
||||||
fi
|
fi
|
||||||
location_text="${located_text} ${room_phrase} ${ou_phrase}."
|
|
||||||
elif [[ -n $location ]]; then
|
elif [[ -n $location ]]; then
|
||||||
location_text="${located_text} $(with_article "$location")."
|
room_article=$(article_for "$location")
|
||||||
|
if [[ -n $gender ]]; then
|
||||||
|
location_primary="${located_text} $(with_article "$location")."
|
||||||
|
elif [[ $room_article == el ]]; then
|
||||||
|
location_primary="Acceso al ${location}."
|
||||||
|
elif [[ -n $room_article ]]; then
|
||||||
|
location_primary="Acceso a ${room_article} ${location}."
|
||||||
|
else
|
||||||
|
location_primary="Acceso a ${location}."
|
||||||
|
fi
|
||||||
elif [[ -n $organizational_unit ]]; then
|
elif [[ -n $organizational_unit ]]; then
|
||||||
location_text="${located_text} $(with_article "$organizational_unit")."
|
ou_article=$(article_for "$organizational_unit")
|
||||||
|
if [[ -n $gender ]]; then
|
||||||
|
location_primary="${located_text} $(with_article "$organizational_unit")."
|
||||||
|
elif [[ $ou_article == el ]]; then
|
||||||
|
location_primary="Acceso al ${organizational_unit}."
|
||||||
|
elif [[ -n $ou_article ]]; then
|
||||||
|
location_primary="Acceso a ${ou_article} ${organizational_unit}."
|
||||||
|
else
|
||||||
|
location_primary="Acceso a ${organizational_unit}."
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
location_text=$engineering_lab_text
|
location_primary=$engineering_lab_text
|
||||||
fi
|
fi
|
||||||
|
location_text=$location_primary
|
||||||
|
[[ -n $location_secondary ]] && location_text+=$'\n'"$location_secondary"
|
||||||
|
|
||||||
width=1600
|
width=1600
|
||||||
height=1000
|
height=1000
|
||||||
@@ -202,6 +233,7 @@ scale=$(( height * 100 / 1000 ))
|
|||||||
welcome_size=$(( 34 * scale / 100 ))
|
welcome_size=$(( 34 * scale / 100 ))
|
||||||
name_size=$(( 70 * scale / 100 ))
|
name_size=$(( 70 * scale / 100 ))
|
||||||
location_size=$(( 27 * scale / 100 ))
|
location_size=$(( 27 * scale / 100 ))
|
||||||
|
location_secondary_size=$(( 20 * scale / 100 ))
|
||||||
panel_width=$(( width * 76 / 100 ))
|
panel_width=$(( width * 76 / 100 ))
|
||||||
panel_height=$(( 310 * scale / 100 ))
|
panel_height=$(( 310 * scale / 100 ))
|
||||||
panel_x1=$(( (width - panel_width) / 2 ))
|
panel_x1=$(( (width - panel_width) / 2 ))
|
||||||
@@ -232,6 +264,17 @@ if [[ $serif_font == DejaVu-Serif ]] && command -v fc-list >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
location_annotations=(
|
||||||
|
-font "$sans_font" -weight 400 -style Normal -pointsize "$location_size"
|
||||||
|
-fill '#D3E2FF' -stroke none -annotate "+0+$(( 76 * scale / 100 ))" "$location_primary"
|
||||||
|
)
|
||||||
|
if [[ -n $location_secondary ]]; then
|
||||||
|
location_annotations+=(
|
||||||
|
-font "$sans_font" -weight 400 -style Normal -pointsize "$location_secondary_size"
|
||||||
|
-fill '#D3E2FF' -stroke none -annotate "+0+$(( 116 * scale / 100 ))" "$location_secondary"
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
if ! "${image_command[@]}" "$BASE_IMAGE" \
|
if ! "${image_command[@]}" "$BASE_IMAGE" \
|
||||||
-resize "${width}x${height}^" -gravity center -extent "${width}x${height}" \
|
-resize "${width}x${height}^" -gravity center -extent "${width}x${height}" \
|
||||||
-fill 'rgba(0,13,58,0.30)' -draw "rectangle ${panel_x1},${panel_y1} ${panel_x2},${panel_y2}" \
|
-fill 'rgba(0,13,58,0.30)' -draw "rectangle ${panel_x1},${panel_y1} ${panel_x2},${panel_y2}" \
|
||||||
@@ -241,8 +284,7 @@ if ! "${image_command[@]}" "$BASE_IMAGE" \
|
|||||||
-annotate "+0-$(( 92 * scale / 100 ))" "$welcome_text" \
|
-annotate "+0-$(( 92 * scale / 100 ))" "$welcome_text" \
|
||||||
-font "$serif_font" -weight 700 -style Italic -pointsize "$name_size" \
|
-font "$serif_font" -weight 700 -style Italic -pointsize "$name_size" \
|
||||||
-fill white -annotate "+0-$(( 22 * scale / 100 ))" "$display_name" \
|
-fill white -annotate "+0-$(( 22 * scale / 100 ))" "$display_name" \
|
||||||
-font "$sans_font" -weight 400 -style Normal -pointsize "$location_size" \
|
"${location_annotations[@]}" \
|
||||||
-fill '#D3E2FF' -annotate "+0+$(( 88 * scale / 100 ))" "$location_text" \
|
|
||||||
-quality 94 "$output_path" 2>>"$log_path"; then
|
-quality 94 "$output_path" 2>>"$log_path"; then
|
||||||
fail_softly 'ImageMagick could not render the welcome wallpaper.'
|
fail_softly 'ImageMagick could not render the welcome wallpaper.'
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ $repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
|||||||
$bicepPath = Join-Path $repositoryRoot 'infra\azure\main.bicep'
|
$bicepPath = Join-Path $repositoryRoot 'infra\azure\main.bicep'
|
||||||
$deploymentPath = Join-Path $repositoryRoot 'scripts\Deploy-SguAzureInfrastructure.ps1'
|
$deploymentPath = Join-Path $repositoryRoot 'scripts\Deploy-SguAzureInfrastructure.ps1'
|
||||||
$configurationPath = Join-Path $repositoryRoot 'scripts\Enable-SguAzureUserRoaming.ps1'
|
$configurationPath = Join-Path $repositoryRoot 'scripts\Enable-SguAzureUserRoaming.ps1'
|
||||||
|
$rollbackPath = Join-Path $repositoryRoot 'scripts\Disable-SguAzureUserRoaming.ps1'
|
||||||
$installerPath = Join-Path $repositoryRoot 'scripts\Install-SguFsLogix.ps1'
|
$installerPath = Join-Path $repositoryRoot 'scripts\Install-SguFsLogix.ps1'
|
||||||
$gpoDeploymentPath = Join-Path $repositoryRoot 'scripts\Publish-SguFsLogixClientDeployment.ps1'
|
$gpoDeploymentPath = Join-Path $repositoryRoot 'scripts\Publish-SguFsLogixClientDeployment.ps1'
|
||||||
$clientBootstrapPath = Join-Path $repositoryRoot 'scripts\Invoke-SguClientBootstrap.ps1'
|
$clientBootstrapPath = Join-Path $repositoryRoot 'scripts\Invoke-SguClientBootstrap.ps1'
|
||||||
@@ -11,6 +12,7 @@ $packagePath = Join-Path $repositoryRoot 'scripts\New-SguBootstrapPackages.ps1'
|
|||||||
foreach ($scriptPath in @(
|
foreach ($scriptPath in @(
|
||||||
$deploymentPath,
|
$deploymentPath,
|
||||||
$configurationPath,
|
$configurationPath,
|
||||||
|
$rollbackPath,
|
||||||
$installerPath,
|
$installerPath,
|
||||||
$gpoDeploymentPath,
|
$gpoDeploymentPath,
|
||||||
$clientBootstrapPath,
|
$clientBootstrapPath,
|
||||||
@@ -40,9 +42,9 @@ $samFunction = $configurationAst.Find({
|
|||||||
Invoke-Expression $samFunction.Extent.Text
|
Invoke-Expression $samFunction.Extent.Text
|
||||||
|
|
||||||
Describe 'SGU Azure user-roaming infrastructure' {
|
Describe 'SGU Azure user-roaming infrastructure' {
|
||||||
It 'deploys dedicated private Azure Files resources by default' {
|
It 'keeps roaming disabled by default while retaining optional private Azure Files resources' {
|
||||||
$source = Get-Content -LiteralPath $bicepPath -Raw
|
$source = Get-Content -LiteralPath $bicepPath -Raw
|
||||||
$source | Should Match 'param deployUserRoaming bool = true'
|
$source | Should Match 'param deployUserRoaming bool = false'
|
||||||
$source | Should Match "purpose: 'SGU-user-roaming'"
|
$source | Should Match "purpose: 'SGU-user-roaming'"
|
||||||
$source | Should Match "publicNetworkAccess: 'Disabled'"
|
$source | Should Match "publicNetworkAccess: 'Disabled'"
|
||||||
$source | Should Match "privateLinkServiceId: userRoamingStorageAccount.id"
|
$source | Should Match "privateLinkServiceId: userRoamingStorageAccount.id"
|
||||||
@@ -63,6 +65,8 @@ Describe 'SGU Azure user-roaming infrastructure' {
|
|||||||
$source = Get-Content -LiteralPath $deploymentPath -Raw
|
$source = Get-Content -LiteralPath $deploymentPath -Raw
|
||||||
$source | Should Match '\$DeployUserRoaming -and -not \$DeployVpnGateway'
|
$source | Should Match '\$DeployUserRoaming -and -not \$DeployVpnGateway'
|
||||||
$source | Should Match 'private Azure Files endpoint'
|
$source | Should Match 'private Azure Files endpoint'
|
||||||
|
$source | Should Match '\$DeployUserRoaming -and -not \$AcknowledgeUserRoamingRisks'
|
||||||
|
$source | Should Match '\[bool\]\$DeployUserRoaming = \$false'
|
||||||
}
|
}
|
||||||
|
|
||||||
It 'returns every post-domain setup value from the Azure deployment' {
|
It 'returns every post-domain setup value from the Azure deployment' {
|
||||||
@@ -79,6 +83,12 @@ Describe 'SGU Azure user-roaming infrastructure' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Describe 'SGU role-specific roaming policies' {
|
Describe 'SGU role-specific roaming policies' {
|
||||||
|
It 'requires an explicit risk acknowledgement before enabling roaming' {
|
||||||
|
$source = Get-Content -LiteralPath $configurationPath -Raw
|
||||||
|
$source | Should Match '\[switch\]\$AcknowledgeUserRoamingRisks'
|
||||||
|
$source | Should Match 'optional high-risk feature'
|
||||||
|
}
|
||||||
|
|
||||||
It 'supports an explicitly selected public SMB endpoint while keeping private as the default' {
|
It 'supports an explicitly selected public SMB endpoint while keeping private as the default' {
|
||||||
(((Get-Command $configurationPath).Parameters['EndpointAccess'].Attributes |
|
(((Get-Command $configurationPath).Parameters['EndpointAccess'].Attributes |
|
||||||
Where-Object { $_ -is [Management.Automation.ValidateSetAttribute] }).ValidValues -join ',') |
|
Where-Object { $_ -is [Management.Automation.ValidateSetAttribute] }).ValidValues -join ',') |
|
||||||
@@ -181,6 +191,7 @@ Describe 'SGU FSLogix image enrollment' {
|
|||||||
It 'packages both roaming setup scripts' {
|
It 'packages both roaming setup scripts' {
|
||||||
$source = Get-Content -LiteralPath $packagePath -Raw
|
$source = Get-Content -LiteralPath $packagePath -Raw
|
||||||
$source | Should Match "'Install-SguFsLogix\.ps1'"
|
$source | Should Match "'Install-SguFsLogix\.ps1'"
|
||||||
|
$source | Should Match "'Disable-SguAzureUserRoaming\.ps1'"
|
||||||
$source | Should Match "'Enable-SguAzureUserRoaming\.ps1'"
|
$source | Should Match "'Enable-SguAzureUserRoaming\.ps1'"
|
||||||
$source | Should Match "'Publish-SguFsLogixClientDeployment\.ps1'"
|
$source | Should Match "'Publish-SguFsLogixClientDeployment\.ps1'"
|
||||||
}
|
}
|
||||||
@@ -196,3 +207,23 @@ Describe 'SGU FSLogix image enrollment' {
|
|||||||
$source | Should Match 'Install-SguFsLogix-Startup\.ps1'
|
$source | Should Match 'Install-SguFsLogix-Startup\.ps1'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Describe 'SGU local-profile rollback' {
|
||||||
|
It 'disables FSLogix containers and their services through computer policy' {
|
||||||
|
$source = Get-Content -LiteralPath $rollbackPath -Raw
|
||||||
|
$source | Should Match "'HKLM\\SOFTWARE\\FSLogix\\Profiles'"
|
||||||
|
$source | Should Match "'HKLM\\SOFTWARE\\FSLogix\\ODFC'"
|
||||||
|
$source | Should Match '-ValueName Enabled -Type DWord -Value 0'
|
||||||
|
$source | Should Match "'frxsvc','frxccds'"
|
||||||
|
$source | Should Match '-ValueName Start -Type DWord -Value 4'
|
||||||
|
$source | Should Match 'StaffProfileStorageMode[\s\S]*-Value Disabled'
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'restores student shell folders to the local profile and disables deployment' {
|
||||||
|
$source = Get-Content -LiteralPath $rollbackPath -Raw
|
||||||
|
$source | Should Match '%USERPROFILE%\\Desktop'
|
||||||
|
$source | Should Match '%USERPROFILE%\\Documents'
|
||||||
|
$source | Should Match "-LinkEnabled No"
|
||||||
|
$source | Should Match 'profilePath,homeDirectory,homeDrive'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -89,7 +89,29 @@ Describe 'Welcome wallpaper with AD metadata' {
|
|||||||
$result = Invoke-WelcomeFixture -DirectoryGender $null `
|
$result = Invoke-WelcomeFixture -DirectoryGender $null `
|
||||||
-DirectoryLocation 'Aula Flexible' `
|
-DirectoryLocation 'Aula Flexible' `
|
||||||
-DirectoryOu 'Centro de Experiencia Digital'
|
-DirectoryOu 'Centro de Experiencia Digital'
|
||||||
$result.LocationText | Should Be 'Acceso al Aula Flexible del Centro de Experiencia Digital.'
|
$result.LocationText | Should Be "Acceso al Aula Flexible`ndel Centro de Experiencia Digital."
|
||||||
|
$result.LocationPrimaryText | Should Be 'Acceso al Aula Flexible'
|
||||||
|
$result.LocationSecondaryText | Should Be 'del Centro de Experiencia Digital.'
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'uses the requested neutral, masculine and feminine location wording' {
|
||||||
|
$neutral = Invoke-WelcomeFixture -DirectoryGender $null `
|
||||||
|
-DirectoryLocation 'Sala de Aplicaciones' `
|
||||||
|
-DirectoryOu 'Laboratorio de Cómputo de Ingeniería'
|
||||||
|
$neutral.LocationPrimaryText | Should Be 'Acceso a la Sala de Aplicaciones'
|
||||||
|
$neutral.LocationSecondaryText | Should Be 'del Laboratorio de Cómputo de Ingeniería.'
|
||||||
|
|
||||||
|
$male = Invoke-WelcomeFixture -DirectoryGender 'Male' `
|
||||||
|
-DirectoryLocation 'Aula Flexible' `
|
||||||
|
-DirectoryOu 'Centro de Experiencia Digital'
|
||||||
|
$male.LocationPrimaryText | Should Be 'Estás ubicado en el Aula Flexible'
|
||||||
|
$male.LocationSecondaryText | Should Be 'del Centro de Experiencia Digital.'
|
||||||
|
|
||||||
|
$female = Invoke-WelcomeFixture -DirectoryGender 'Female' `
|
||||||
|
-DirectoryLocation 'Sala de Redes' `
|
||||||
|
-DirectoryOu 'Laboratorio de Cómputo de Ingeniería'
|
||||||
|
$female.LocationPrimaryText | Should Be 'Estás ubicada en la Sala de Redes'
|
||||||
|
$female.LocationSecondaryText | Should Be 'del Laboratorio de Cómputo de Ingeniería.'
|
||||||
}
|
}
|
||||||
|
|
||||||
It 'keeps machine location metadata when the GPO command has no location arguments' {
|
It 'keeps machine location metadata when the GPO command has no location arguments' {
|
||||||
@@ -98,6 +120,26 @@ Describe 'Welcome wallpaper with AD metadata' {
|
|||||||
-DirectoryOu 'Laboratorio' `
|
-DirectoryOu 'Laboratorio' `
|
||||||
-MachineLocation 'Aula Flexible' `
|
-MachineLocation 'Aula Flexible' `
|
||||||
-MachineOu 'Centro de Experiencia Digital'
|
-MachineOu 'Centro de Experiencia Digital'
|
||||||
$result.LocationText | Should Be 'Acceso al Aula Flexible del Centro de Experiencia Digital.'
|
$result.LocationText | Should Be "Acceso al Aula Flexible`ndel Centro de Experiencia Digital."
|
||||||
|
}
|
||||||
|
|
||||||
|
It 'ships the logo inside the base image instead of compositing it at runtime' {
|
||||||
|
$source | Should Not Match 'lasalle-logo-blanco|DrawImage.*logo|composite.*logo'
|
||||||
|
$bitmap = [Drawing.Bitmap]::FromFile((Join-Path $repositoryRoot 'assets\branding\darkblue.jpg'))
|
||||||
|
try {
|
||||||
|
$whitePixels = 0
|
||||||
|
for ($x = 60; $x -lt 390; $x += 2) {
|
||||||
|
for ($y = 45; $y -lt 175; $y += 2) {
|
||||||
|
$pixel = $bitmap.GetPixel($x, $y)
|
||||||
|
if ($pixel.R -gt 220 -and $pixel.G -gt 220 -and $pixel.B -gt 220) {
|
||||||
|
$whitePixels++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
($whitePixels -gt 250) | Should Be $true
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$bitmap.Dispose()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user