Improve domain enrollment and desktop personalization
This commit is contained in:
@@ -111,7 +111,9 @@ Linux clients are enrolled through their native PAM/SSSD stack instead of the
|
||||
Windows Credential Provider:
|
||||
|
||||
```bash
|
||||
sudo bash ./Enroll-SguLinuxDomainClient.sh --domain-controller 192.168.50.10
|
||||
sudo bash ./Enroll-SguLinuxDomainClient.sh \
|
||||
--domain-controller 192.168.50.10 \
|
||||
--enable-hyperv-enhanced-session
|
||||
```
|
||||
|
||||
The server command creates a new forest and resumes by itself after its required
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+20
-10
@@ -23,13 +23,15 @@ is forced because NTLM authentication is connection-bound.
|
||||
The authoritative logical GET is sent to `/psulsa/`, a lightweight route that
|
||||
returns the NTLM challenge without waiting for the slow application pages. A
|
||||
`401` or `403` rejects the credential; an allowed `2xx` or `3xx` proves that IIS
|
||||
accepted it. The broker then makes a separately bounded, best-effort GET to the
|
||||
administrative incident overview for `AD`, the student information page for
|
||||
`AL`, or the portal menu for `DO`. After the incident page confirms an `AD`
|
||||
employee number, two additional GETs in the same in-memory session read the
|
||||
structured name from `datos/personales.aspx` and the address from
|
||||
`datos/ubicacion.aspx`. Docentes keep the menu name as a base and attempt those
|
||||
same two shared staff modules without requiring them to exist. A supplemental
|
||||
accepted it. The broker then makes separately bounded, best-effort profile GETs.
|
||||
It uses the administrative incident overview for `AD`, the student information
|
||||
page for `AL`, and the portal menu as a conservative base for `DO`. After the
|
||||
incident page confirms an `AD` employee number, two additional GETs in the same
|
||||
in-memory session read the structured name from `datos/personales.aspx` and the
|
||||
address from `datos/ubicacion.aspx`. Docentes request
|
||||
`nomina/consultanomina.aspx` for a matching employee number, email, employee
|
||||
type and job title, then attempt the same two shared staff modules without
|
||||
requiring any optional route to exist. A supplemental
|
||||
404, changed/missing element ID, other failure, or timeout preserves fields
|
||||
already collected, and a profile timeout does not invalidate an already
|
||||
authenticated credential. NTLM may still require its normal
|
||||
@@ -62,9 +64,10 @@ updates the applicable `displayName`, `givenName`, `sn`, `mail`, `title`,
|
||||
`postalCode` attributes. Administrative and student numbers must match the six
|
||||
numeric digits of the requested identity before any role-specific metadata is
|
||||
trusted. Administrative personal and location pages are accepted only after
|
||||
that incident-page match. A docente's supplemental fields remain tied to the
|
||||
fresh NTLM-authenticated portal session and are optional; the menu display name
|
||||
remains usable if neither shared page is available. Student faculty/department
|
||||
that incident-page match. Docente payroll metadata must match the requested
|
||||
six-digit number; all docente supplemental fields remain tied to the fresh
|
||||
NTLM-authenticated portal session and are optional. The menu display name
|
||||
remains usable if the payroll or shared staff pages are unavailable. Student faculty/department
|
||||
is deliberately left unset because the verified page does not expose it.
|
||||
Missing metadata does not clear existing AD values and never changes the
|
||||
password outcome.
|
||||
@@ -105,6 +108,13 @@ to `OU=Laboratorio`; it suppresses first-logon/privacy/diagnostic prompts,
|
||||
disables location, and enforces always-on display, sleep, and hibernation
|
||||
settings for managed clients.
|
||||
|
||||
That computer GPO also owns the base lock-screen image and a per-logon command
|
||||
for the personalized desktop wallpaper. The client-side renderer reads the
|
||||
authenticated user's `displayName` plus the computer object's `location` and
|
||||
immediate parent OU, then composes those values over the bundled dark-blue
|
||||
background with the bundled Indivisa fonts. Missing directory attributes degrade
|
||||
to deterministic text and never block the interactive session.
|
||||
|
||||
The domain controller is also the source-initiated Windows Event Collector for
|
||||
managed laboratory computers. Kerberos-authenticated WEF sends only selected
|
||||
logon/logoff, failed-logon, reconnect/disconnect, and operating-system power
|
||||
|
||||
@@ -66,9 +66,10 @@ continúa por la NIC que tenga el gateway predeterminado.
|
||||
El broker arranca con una lista de clientes vacía. Eso no abre el servicio: mTLS
|
||||
rechaza todos los certificados hasta que el primer cliente registra el suyo.
|
||||
Los archivos opcionales colocados en `payload\server-content\Packages` al crear
|
||||
el release se copian al recurso compartido. Si allí existe `wallpaper.jpg`,
|
||||
`wallpaper.jpeg`, `wallpaper.png` o `wallpaper.bmp`, la GPO de usuarios lo aplica
|
||||
automáticamente como fondo con ajuste Fill.
|
||||
el release se copian al recurso compartido. El paquete siempre incluye
|
||||
`welcome-wallpaper`: fondo azul, fuentes Indivisa y generador de respaldo para
|
||||
reparación o actualización de clientes. La GPO de equipos inicia la copia local
|
||||
del generador en cada sesión; ya no se impone un único fondo estático por usuario.
|
||||
|
||||
Estado y diagnóstico:
|
||||
|
||||
|
||||
@@ -51,8 +51,9 @@ Eso es comportamiento esperado, no una caída del servicio.
|
||||
`/psulsa/`. El enriquecimiento usa el límite total independiente
|
||||
`ProfileTimeoutSeconds` —**90 segundos** en la configuración del laboratorio—
|
||||
y conserva los campos que alcance a obtener si una página de personal se
|
||||
retrasa, no existe o cambia sus IDs. Esto incluye los módulos opcionales de
|
||||
nombre y ubicación para docentes. El Credential Provider mantiene su propio límite de **90
|
||||
retrasa, no existe o cambia sus IDs. Para docentes esto incluye consulta de
|
||||
nómina —clave, nombre, correo, tipo y puesto— más los módulos opcionales de
|
||||
nombre y ubicación. El Credential Provider mantiene su propio límite de **90
|
||||
segundos**: si SGU excede ese presupuesto, Windows continúa por el fallback
|
||||
normal de AD o credenciales de dominio en caché.
|
||||
- El instalador configura recuperación del servicio con reinicios a los 5, 15
|
||||
|
||||
@@ -112,6 +112,11 @@ pantalla, suspensión, hibernación y suspensión híbrida, conectado a corrient
|
||||
batería. El guard de enrolamiento vuelve a aplicar `powercfg /hibernate off` y
|
||||
los tiempos en cero al inicio y diariamente.
|
||||
|
||||
Esa GPO también ejecuta el generador local del fondo de bienvenida y aplica el
|
||||
fondo azul base a la pantalla de bloqueo. El fondo individual se crea al abrir la
|
||||
sesión con el nombre del usuario y `location`/OU del equipo; véase
|
||||
[welcome-wallpaper.md](welcome-wallpaper.md).
|
||||
|
||||
`HideEULAPage` no forma parte de esta GPO: es una opción de archivo Unattend para
|
||||
la fase OOBE y Microsoft la reserva para pruebas de OEM/System Builder. La GPO
|
||||
usa las alternativas soportadas `DisablePrivacyExperience=1` y
|
||||
|
||||
@@ -21,33 +21,53 @@ sudo bash ./Enroll-SguLinuxDomainClient.sh \
|
||||
--domain-controller 192.168.50.10 \
|
||||
--domain-interface eth0 \
|
||||
--domain-address 192.168.50.12/24 \
|
||||
--enable-ssh
|
||||
--enable-ssh \
|
||||
--enable-hyperv-enhanced-session
|
||||
```
|
||||
|
||||
`--domain-interface` y `--domain-address` son opcionales como pareja. Si ya se aprovisionó la red privada mediante cloud-init, DHCP o gestión de configuración, omítelos y conserva únicamente `--domain-controller`.
|
||||
|
||||
El script se niega a reconfigurar una interfaz que posea la ruta predeterminada; así no deja a la máquina sin salida a Internet al agregar AD.
|
||||
|
||||
En Ubuntu con GNOME, cierra la sesión gráfica de **Sesión básica** antes de
|
||||
entrar con el mismo usuario mediante **Sesión mejorada**. GNOME no admite dos
|
||||
escritorios simultáneos del mismo usuario; intentar conservar ambos produce una
|
||||
pantalla negra aunque XRDP haya autenticado correctamente. El inicio automático
|
||||
de sesión de GDM también debe permanecer deshabilitado.
|
||||
|
||||
## Qué instala y configura
|
||||
|
||||
1. Instala `realmd`, `adcli`, SSSD, Kerberos y los módulos NSS/PAM adecuados para la familia de distribución.
|
||||
2. Comprueba el registro DNS SRV de Active Directory y sincronización de hora ya existente.
|
||||
3. Establece el nombre de host `NOMBRE.lci.lasalle.mx` antes de crear la cuenta de equipo.
|
||||
4. Une el equipo con `adcli` en `OU=Laboratorio`.
|
||||
5. Activa SSSD, creación de directorio personal mediante PAM y valida la contraseña de la cuenta de equipo con `adcli testjoin`.
|
||||
5. Activa SSSD, creación de directorio personal mediante PAM y valida la contraseña de la cuenta de equipo con `adcli testjoin`. En distribuciones que habilitan los respondedores NSS/PAM de SSSD tanto en `sssd.conf` como mediante sockets de systemd, desactiva los sockets duplicados para evitar una colisión al arrancar.
|
||||
6. Cuando se proporcionó la NIC privada, activa actualizaciones DNS dinámicas de SSSD en esa interfaz.
|
||||
7. Con `--enable-ssh`, instala y habilita OpenSSH y abre únicamente el servicio SSH cuando el firewall local ya está activo.
|
||||
8. Registra `lightdm` y `cinnamon-screensaver` como inicios interactivos ante las políticas GPO de SSSD. En equipos con LightDM también muestra el ingreso manual, necesario para el primer acceso de un usuario del dominio.
|
||||
9. Con `--enable-hyperv-enhanced-session`, configura XRDP sobre Hyper-V sockets para que VMConnect pueda usar **Sesión mejorada**, repara certificados incompletos, registra `xrdp-sesman` en el mismo mapa interactivo y valida ambos servicios XRDP.
|
||||
10. Instala el fondo azul, las fuentes Indivisa y un autoinicio compatible con Cinnamon, GNOME y XFCE. En cada sesión gráfica genera el saludo con el nombre del usuario y la ubicación/OU del equipo obtenidas de AD.
|
||||
|
||||
El objeto de equipo aparece como `NOMBRE` en `OU=Laboratorio`. SSSD registra su registro A cuando la actualización DNS dinámica está activada.
|
||||
|
||||
## Inicio de sesión de dominio
|
||||
|
||||
Después de la unión, el formato explícito es:
|
||||
Después de la unión se acepta directamente la clave institucional corta:
|
||||
|
||||
```text
|
||||
al201428
|
||||
```
|
||||
|
||||
El formato UPN explícito también permanece disponible:
|
||||
|
||||
```text
|
||||
usuario@lci.lasalle.mx
|
||||
```
|
||||
|
||||
En Linux Mint, selecciona **Iniciar sesión** o el campo manual de usuario en
|
||||
Slick Greeter y escribe la clave corta. El mosaico con el nombre de una cuenta
|
||||
local no cambia de identidad al escribir únicamente otra contraseña.
|
||||
|
||||
La primera sesión crea `/home/usuario@lci.lasalle.mx`. El valor predeterminado de SSSD conserva credenciales para desconexiones breves de la red; las contraseñas no son administradas ni almacenadas por el Auth Broker.
|
||||
|
||||
Para limitar quién puede iniciar sesión, incluye un grupo de AD:
|
||||
@@ -67,6 +87,8 @@ realm list
|
||||
sudo adcli testjoin --domain=lci.lasalle.mx
|
||||
getent passwd 'usuario@lci.lasalle.mx'
|
||||
sudo sssctl domain-status lci.lasalle.mx
|
||||
sudo sssctl user-checks usuario -a acct -s lightdm
|
||||
sudo sssctl user-checks usuario -a acct -s cinnamon-screensaver
|
||||
```
|
||||
|
||||
Para sacar un equipo del dominio de forma explícita:
|
||||
@@ -76,3 +98,7 @@ sudo realm leave lci.lasalle.mx
|
||||
```
|
||||
|
||||
Esta última acción elimina la relación de confianza local; debe ejecutarse sólo durante baja o reconstrucción del equipo.
|
||||
|
||||
La personalización gráfica es deliberadamente opcional: si ImageMagick, LDAP o
|
||||
la API del escritorio fallan, no revierte la unión ni impide iniciar sesión. Consulta
|
||||
los detalles y las reglas de degradación en [welcome-wallpaper.md](welcome-wallpaper.md).
|
||||
|
||||
+11
-7
@@ -43,16 +43,20 @@
|
||||
city/municipality, state, and postal code from known element IDs.
|
||||
- Student CURP, birth date, sex, blood type, marital status, telephone, mobile,
|
||||
guardian, medical, financial, and academic-history values are ignored.
|
||||
- Professor enrichment keeps the menu display name as its base and optionally
|
||||
reads only the same name and postal-address element IDs used by staff pages.
|
||||
A missing professor route or element never makes authentication fail.
|
||||
- Incident details, calendars, photographs, manager names, and manager positions
|
||||
are deliberately ignored.
|
||||
- Professor enrichment keeps the menu display name as its base. From the payroll
|
||||
consultation header it reads only a matching employee number, name, email,
|
||||
employee type/status, job title, and the optional department field. It then
|
||||
optionally reads the same structured-name and postal-address element IDs used
|
||||
by staff pages. A missing professor route or element never makes authentication
|
||||
fail.
|
||||
- Payroll/receipt contents, incident details, calendars, photographs, manager
|
||||
names, and manager positions are deliberately ignored.
|
||||
- The employee or student number must match the authenticated `AD` or `AL` key
|
||||
before role-specific metadata is synchronized. The two supplemental
|
||||
administrative pages are never requested unless the incident page supplied
|
||||
the matching employee number. Professor supplemental data comes from the
|
||||
same fresh, request-scoped NTLM session as its menu fallback.
|
||||
the matching employee number. Professor payroll metadata independently
|
||||
requires the matching six-digit number, and every supplemental request uses
|
||||
the same fresh, request-scoped NTLM session as its menu fallback.
|
||||
- If SGU changes its HTML, authentication and exact-password synchronization
|
||||
continue without enrichment; existing AD metadata is not erased.
|
||||
- Slow profile pages cannot change an accepted credential into a rejection. The
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# Fondo de bienvenida personalizado
|
||||
|
||||
El enrolamiento instala un fondo base azul, las familias `Indivisa Text Sans` y
|
||||
`Indivisa Text Serif`, y un generador local. La GPO de equipos
|
||||
`SGU - Windows client experience` ejecuta el generador al abrir cada sesión y
|
||||
mantiene el fondo base en la pantalla de bloqueo.
|
||||
|
||||
Windows no conoce todavía la identidad que se autenticará mientras muestra la
|
||||
pantalla previa al inicio de sesión. Por ello, esa pantalla utiliza el fondo base
|
||||
sin datos personales y la composición individual se genera inmediatamente
|
||||
después de autenticar, antes de que el usuario empiece a trabajar en el escritorio.
|
||||
|
||||
## Datos y degradación controlada
|
||||
|
||||
El generador consulta Active Directory con la identidad ya autenticada y sin
|
||||
guardar credenciales. Obtiene:
|
||||
|
||||
- `displayName` del usuario; si falta, utiliza `sAMAccountName`.
|
||||
- `location` del objeto de equipo.
|
||||
- La OU padre inmediata a partir de `distinguishedName`.
|
||||
|
||||
El texto secundario sigue estas reglas:
|
||||
|
||||
1. Con `location` y OU: `Estás ubicado en la Sala de Inmersión del Centro de Experiencia Digital.`
|
||||
2. Con sólo uno de los datos: muestra únicamente el dato disponible.
|
||||
3. Sin ambos: `Bienvenido 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.
|
||||
Los errores de generación se registran en
|
||||
`%LOCALAPPDATA%\SGU\Logs\welcome-wallpaper.log`.
|
||||
|
||||
## Windows
|
||||
|
||||
El paquete de cliente copia los recursos a `C:\ProgramData\SGU\Branding`. La
|
||||
GPO crea el valor de equipo `SGUWelcomeWallpaper` bajo
|
||||
`HKLM\Software\Microsoft\Windows\CurrentVersion\Run`; por tanto, se ejecuta en
|
||||
el contexto de cada usuario y puede leer sus datos de AD. El resultado se guarda
|
||||
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
|
||||
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.
|
||||
|
||||
## Linux
|
||||
|
||||
El paquete Linux instala el generador en
|
||||
`/usr/local/lib/sgu-welcome-wallpaper` y registra
|
||||
`/etc/xdg/autostart/sgu-welcome-wallpaper.desktop`. Utiliza el ticket Kerberos
|
||||
creado por SSSD para consultar el objeto de equipo mediante LDAP; nunca contiene
|
||||
una contraseña de enlace.
|
||||
|
||||
Se admiten Cinnamon, GNOME y XFCE. La composición requiere ImageMagick; si la
|
||||
dependencia o la API del escritorio no está disponible, el enrolamiento y el
|
||||
inicio de sesión continúan normalmente y se escribe un diagnóstico en
|
||||
`~/.local/state/sgu/welcome-wallpaper.log`.
|
||||
|
||||
## Tipografía
|
||||
|
||||
Los archivos OTF necesarios viajan dentro de cada paquete y se cargan en memoria
|
||||
para renderizar el fondo; no se sustituyen fuentes del sistema. Se usa Sans en el
|
||||
saludo y la ubicación, y Serif Bold Italic en el nombre. Si los archivos no
|
||||
pueden cargarse, Windows usa Segoe UI/Georgia y Linux usa DejaVu Sans/Serif.
|
||||
@@ -129,6 +129,12 @@ configuración siempre activa y evita las experiencias iniciales de privacidad,
|
||||
telemetría, ubicación y **Hi / Preparing Windows** antes de que un usuario SGU
|
||||
entre por primera vez.
|
||||
|
||||
La misma GPO configura el fondo azul de bloqueo y ejecuta el generador local al
|
||||
abrir cada sesión. El generador usa el `displayName` del usuario, la propiedad
|
||||
`location` del equipo y su OU padre inmediata para crear el fondo individual.
|
||||
Consulta [welcome-wallpaper.md](welcome-wallpaper.md) para conocer los fallbacks
|
||||
y la ubicación de los diagnósticos.
|
||||
|
||||
Microsoft documenta este derecho en:
|
||||
<https://learn.microsoft.com/en-us/windows/client-management/mdm/policy-csp-userrights#allowlogonthroughremotedesktop>
|
||||
y PowerShell Remoting en:
|
||||
|
||||
@@ -22,6 +22,8 @@ param(
|
||||
[ValidatePattern('^/')]
|
||||
[string]$StudentProfilePath = '/psulsa/alumnos/consultainformacionalumnos/consultainformacion.aspx',
|
||||
[ValidatePattern('^/')]
|
||||
[string]$ProfessorPayrollProfilePath = '/psulsa/gadmon/nomina/consultanomina.aspx',
|
||||
[ValidatePattern('^/')]
|
||||
[string]$MenuProfilePath = '/psulsa/menu.aspx',
|
||||
[ValidateRange(32768, 2097152)]
|
||||
[int]$MaxProfileBytes = 524288,
|
||||
@@ -171,6 +173,7 @@ $productionSettings = @{
|
||||
AdministrativePersonalProfilePath = $AdministrativePersonalProfilePath
|
||||
AdministrativeLocationProfilePath = $AdministrativeLocationProfilePath
|
||||
StudentProfilePath = $StudentProfilePath
|
||||
ProfessorPayrollProfilePath = $ProfessorPayrollProfilePath
|
||||
MenuProfilePath = $MenuProfilePath
|
||||
MaxProfileBytes = $MaxProfileBytes
|
||||
AllowedRedirectHosts = $AllowedNtlmRedirectHosts
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
set -Eeuo pipefail
|
||||
IFS=$'\n\t'
|
||||
SCRIPT_DIRECTORY=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
|
||||
DOMAIN_NAME='lci.lasalle.mx'
|
||||
DOMAIN_CONTROLLER=''
|
||||
@@ -18,6 +19,7 @@ DOMAIN_ADDRESS=''
|
||||
COMPUTER_NAME=''
|
||||
ALLOW_GROUP=''
|
||||
ENABLE_SSH=false
|
||||
ENABLE_HYPERV_ENHANCED_SESSION=false
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
@@ -37,6 +39,8 @@ Options:
|
||||
--domain-address CIDR Static IPv4 address for --domain-interface, e.g. 192.168.50.12/24.
|
||||
--allow-group GROUP Restrict Linux sign-in to this AD group after joining.
|
||||
--enable-ssh Install, enable, and (when active) permit OpenSSH in the local firewall.
|
||||
--enable-hyperv-enhanced-session
|
||||
Install and configure XRDP over Hyper-V sockets for VMConnect.
|
||||
--help Show this help.
|
||||
|
||||
Network safety:
|
||||
@@ -70,6 +74,7 @@ while (($#)); do
|
||||
--domain-address) DOMAIN_ADDRESS=${2:?Missing value for --domain-address}; shift 2 ;;
|
||||
--allow-group) ALLOW_GROUP=${2:?Missing value for --allow-group}; shift 2 ;;
|
||||
--enable-ssh) ENABLE_SSH=true; shift ;;
|
||||
--enable-hyperv-enhanced-session) ENABLE_HYPERV_ENHANCED_SESSION=true; shift ;;
|
||||
--help|-h) usage; exit 0 ;;
|
||||
*) fail "Unknown argument: $1. Use --help for usage." ;;
|
||||
esac
|
||||
@@ -100,6 +105,19 @@ install_prerequisites() {
|
||||
if [[ $ENABLE_SSH == true ]]; then
|
||||
packages+=(openssh-server)
|
||||
fi
|
||||
if [[ $ENABLE_HYPERV_ENHANCED_SESSION == true ]]; then
|
||||
packages+=(xrdp xorgxrdp ssl-cert)
|
||||
|
||||
# XRDP's Debian post-install script cannot replace a dangling
|
||||
# certificate symlink left by an interrupted/older installation.
|
||||
# Remove only dangling links so dpkg can recreate them safely.
|
||||
local xrdp_link
|
||||
for xrdp_link in /etc/xrdp/cert.pem /etc/xrdp/key.pem; do
|
||||
if [[ -L $xrdp_link && ! -e $xrdp_link ]]; then
|
||||
rm -f -- "$xrdp_link"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y "${packages[@]}"
|
||||
@@ -121,7 +139,7 @@ install_prerequisites() {
|
||||
}
|
||||
|
||||
configure_private_ad_interface() {
|
||||
[[ -n $DOMAIN_INTERFACE ]] || return
|
||||
[[ -n $DOMAIN_INTERFACE ]] || return 0
|
||||
need_command nmcli
|
||||
ip link show "$DOMAIN_INTERFACE" >/dev/null 2>&1 || \
|
||||
fail "Network interface does not exist: $DOMAIN_INTERFACE"
|
||||
@@ -148,7 +166,7 @@ configure_private_ad_interface() {
|
||||
}
|
||||
|
||||
enable_sssd_dyndns() {
|
||||
[[ -n $DOMAIN_INTERFACE ]] || return
|
||||
[[ -n $DOMAIN_INTERFACE ]] || return 0
|
||||
local configuration_directory='/etc/sssd/conf.d'
|
||||
local configuration_path="${configuration_directory}/90-sgu-dyndns.conf"
|
||||
local temporary_path
|
||||
@@ -165,8 +183,76 @@ enable_sssd_dyndns() {
|
||||
rm -f "$temporary_path"
|
||||
}
|
||||
|
||||
enable_short_domain_login_names() {
|
||||
local configuration_path='/etc/sssd/sssd.conf'
|
||||
[[ -f $configuration_path ]] || return 0
|
||||
|
||||
# Institutional account names (AL/AD/DO) are unique in this lab and are
|
||||
# the identifiers users already know. Keep UPN logins valid while also
|
||||
# allowing the short form in PAM applications such as XRDP/VMConnect.
|
||||
if grep -Eq '^[[:space:]]*use_fully_qualified_names[[:space:]]*=' "$configuration_path"; then
|
||||
sed -Ei 's/^[[:space:]]*use_fully_qualified_names[[:space:]]*=.*/use_fully_qualified_names = False/' \
|
||||
"$configuration_path"
|
||||
else
|
||||
sed -Ei "/^\[domain\/${DOMAIN_NAME//./\\.}\]$/a use_fully_qualified_names = False" \
|
||||
"$configuration_path"
|
||||
fi
|
||||
chmod 600 "$configuration_path"
|
||||
}
|
||||
|
||||
configure_sssd_responder_mode() {
|
||||
local configuration_path='/etc/sssd/sssd.conf'
|
||||
[[ -f $configuration_path ]] || return 0
|
||||
|
||||
# realmd writes a persistent responder list, while recent Debian-family
|
||||
# packages can enable the same NSS/PAM responders through systemd sockets.
|
||||
# Running both modes makes the sockets fail at boot and can leave graphical
|
||||
# PAM clients unable to contact SSSD reliably. Keep realmd's persistent
|
||||
# responders and disable only the duplicate socket units when they exist.
|
||||
local unit
|
||||
for unit in sssd-nss.socket sssd-pam.socket sssd-pam-priv.socket; do
|
||||
if systemctl list-unit-files "$unit" --no-legend 2>/dev/null | grep -q "^${unit}"; then
|
||||
systemctl disable --now "$unit" >/dev/null 2>&1 || true
|
||||
systemctl reset-failed "$unit" >/dev/null 2>&1 || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
configure_graphical_domain_login() {
|
||||
local sssd_configuration_directory='/etc/sssd/conf.d'
|
||||
local temporary_sssd_configuration
|
||||
local interactive_services='+lightdm,+cinnamon-screensaver'
|
||||
if [[ $ENABLE_HYPERV_ENHANCED_SESSION == true ]]; then
|
||||
interactive_services+=',+xrdp-sesman'
|
||||
fi
|
||||
|
||||
temporary_sssd_configuration=$(mktemp)
|
||||
printf '%s\n' \
|
||||
"[domain/${DOMAIN_NAME,,}]" \
|
||||
"ad_gpo_map_interactive = ${interactive_services}" >"$temporary_sssd_configuration"
|
||||
install -d -o root -g root -m 700 "$sssd_configuration_directory"
|
||||
install -o root -g root -m 600 "$temporary_sssd_configuration" \
|
||||
"${sssd_configuration_directory}/91-sgu-graphical-login.conf"
|
||||
rm -f "$temporary_sssd_configuration"
|
||||
rm -f "${sssd_configuration_directory}/91-sgu-xrdp.conf"
|
||||
|
||||
# Slick Greeter normally shows only the last/local account tile. Expose a
|
||||
# manual user-name prompt so a first-time AD user can enter AL/AD/DO IDs.
|
||||
if [[ -d /etc/lightdm/lightdm.conf.d ]]; then
|
||||
local temporary_lightdm_configuration
|
||||
temporary_lightdm_configuration=$(mktemp)
|
||||
printf '%s\n' \
|
||||
'[Seat:*]' \
|
||||
'greeter-show-manual-login=true' \
|
||||
'greeter-hide-users=false' >"$temporary_lightdm_configuration"
|
||||
install -o root -g root -m 644 "$temporary_lightdm_configuration" \
|
||||
'/etc/lightdm/lightdm.conf.d/91-sgu-domain-login.conf'
|
||||
rm -f "$temporary_lightdm_configuration"
|
||||
fi
|
||||
}
|
||||
|
||||
enable_ssh() {
|
||||
[[ $ENABLE_SSH == true ]] || return
|
||||
[[ $ENABLE_SSH == true ]] || return 0
|
||||
local service_name='sshd'
|
||||
if systemctl list-unit-files ssh.service >/dev/null 2>&1; then
|
||||
service_name='ssh'
|
||||
@@ -180,6 +266,137 @@ enable_ssh() {
|
||||
fi
|
||||
}
|
||||
|
||||
configure_hyperv_enhanced_session() {
|
||||
[[ $ENABLE_HYPERV_ENHANCED_SESSION == true ]] || return 0
|
||||
|
||||
command -v xrdp >/dev/null 2>&1 || {
|
||||
printf 'WARNING: XRDP is unavailable; Hyper-V Enhanced Session was not enabled.\n' >&2
|
||||
return 0
|
||||
}
|
||||
|
||||
local xrdp_configuration='/etc/xrdp/xrdp.ini'
|
||||
[[ -f $xrdp_configuration ]] || {
|
||||
printf 'WARNING: %s is missing; Hyper-V Enhanced Session was not enabled.\n' "$xrdp_configuration" >&2
|
||||
return 0
|
||||
}
|
||||
|
||||
# VMConnect uses AF_VSOCK rather than TCP. Only change the first occurrence,
|
||||
# which belongs to [Globals]; later port entries describe XRDP backends.
|
||||
sed -Ei '0,/^port=.*/s|^port=.*|port=vsock://-1:3389|' "$xrdp_configuration"
|
||||
if grep -q '^use_vsock=' "$xrdp_configuration"; then
|
||||
sed -Ei '0,/^use_vsock=.*/s|^use_vsock=.*|use_vsock=true|' "$xrdp_configuration"
|
||||
else
|
||||
sed -Ei '/^port=vsock:\/\/-1:3389/a use_vsock=true' "$xrdp_configuration"
|
||||
fi
|
||||
sed -Ei '0,/^security_layer=.*/s|^security_layer=.*|security_layer=rdp|' "$xrdp_configuration"
|
||||
sed -Ei '0,/^crypt_level=.*/s|^crypt_level=.*|crypt_level=none|' "$xrdp_configuration"
|
||||
|
||||
# A clean Ubuntu installation can contain XRDP symlinks before the
|
||||
# snake-oil certificate has actually been generated.
|
||||
if [[ ! -s /etc/ssl/certs/ssl-cert-snakeoil.pem || \
|
||||
! -s /etc/ssl/private/ssl-cert-snakeoil.key ]]; then
|
||||
if command -v make-ssl-cert >/dev/null 2>&1; then
|
||||
make-ssl-cert generate-default-snakeoil --force-overwrite
|
||||
else
|
||||
printf 'WARNING: make-ssl-cert is unavailable; XRDP certificate generation was skipped.\n' >&2
|
||||
fi
|
||||
fi
|
||||
usermod -aG ssl-cert xrdp
|
||||
|
||||
# xrdp-sesman (root) and xrdp (the xrdp account) share /run/xrdp. Give the
|
||||
# directory the shared group/mode so the second service can create its PID
|
||||
# file instead of timing out while VMConnect remains at "Connecting".
|
||||
local override_directory='/etc/systemd/system/xrdp-sesman.service.d'
|
||||
local temporary_override
|
||||
temporary_override=$(mktemp)
|
||||
printf '%s\n' \
|
||||
'[Service]' \
|
||||
'Group=xrdp' \
|
||||
'RuntimeDirectory=xrdp' \
|
||||
'RuntimeDirectoryMode=0775' >"$temporary_override"
|
||||
install -d -o root -g root -m 755 "$override_directory"
|
||||
install -o root -g root -m 644 "$temporary_override" \
|
||||
"${override_directory}/sgu-runtime.conf"
|
||||
rm -f "$temporary_override"
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable xrdp xrdp-sesman
|
||||
systemctl restart xrdp
|
||||
systemctl is-active --quiet xrdp
|
||||
systemctl is-active --quiet xrdp-sesman
|
||||
}
|
||||
|
||||
install_welcome_wallpaper() {
|
||||
local source_directory="${SCRIPT_DIRECTORY}/welcome-wallpaper"
|
||||
local source_script="${source_directory}/Set-SguWelcomeWallpaper.sh"
|
||||
local source_image="${source_directory}/darkblue.jpg"
|
||||
local install_directory='/usr/local/lib/sgu-welcome-wallpaper'
|
||||
local configuration_directory='/etc/sgu'
|
||||
local autostart_directory='/etc/xdg/autostart'
|
||||
|
||||
if [[ ! -r $source_script || ! -r $source_image ]]; then
|
||||
printf 'WARNING: Welcome wallpaper assets are absent; domain enrollment will continue without desktop branding.\n' >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Desktop branding is optional and must never invalidate an otherwise valid
|
||||
# domain join. Install its distribution-specific dependencies best-effort.
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
if ! apt-get install -y imagemagick ldap-utils fontconfig; then
|
||||
printf 'WARNING: Could not install welcome wallpaper dependencies; enrollment remains valid.\n' >&2
|
||||
return 0
|
||||
fi
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
if ! dnf install -y ImageMagick openldap-clients fontconfig; then
|
||||
printf 'WARNING: Could not install welcome wallpaper dependencies; enrollment remains valid.\n' >&2
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
install -d -o root -g root -m 755 "$install_directory" "$configuration_directory" "$autostart_directory"
|
||||
install -o root -g root -m 755 "$source_script" "${install_directory}/Set-SguWelcomeWallpaper.sh"
|
||||
install -o root -g root -m 644 "$source_image" "${install_directory}/darkblue.jpg"
|
||||
if compgen -G "${source_directory}/fonts/*.[ot]tf" >/dev/null; then
|
||||
install -d -o root -g root -m 755 "${install_directory}/fonts"
|
||||
install -o root -g root -m 644 "${source_directory}"/fonts/*.[ot]tf "${install_directory}/fonts/"
|
||||
fi
|
||||
|
||||
local base_dn=''
|
||||
local component
|
||||
IFS='.' read -ra domain_components <<<"$DOMAIN_NAME"
|
||||
for component in "${domain_components[@]}"; do
|
||||
if [[ -n $base_dn ]]; then
|
||||
base_dn+=','
|
||||
fi
|
||||
base_dn+="DC=${component}"
|
||||
done
|
||||
|
||||
local temporary_configuration
|
||||
temporary_configuration=$(mktemp)
|
||||
printf 'DOMAIN_CONTROLLER=%q\nDOMAIN_NAME=%q\nBASE_DN=%q\n' \
|
||||
"$DOMAIN_CONTROLLER" "$DOMAIN_NAME" "$base_dn" >"$temporary_configuration"
|
||||
install -o root -g root -m 644 "$temporary_configuration" \
|
||||
"${configuration_directory}/welcome-wallpaper.conf"
|
||||
rm -f "$temporary_configuration"
|
||||
|
||||
local temporary_autostart
|
||||
temporary_autostart=$(mktemp)
|
||||
cat >"$temporary_autostart" <<'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=SGU welcome wallpaper
|
||||
Comment=Generate a personalized La Salle laboratory welcome wallpaper
|
||||
Exec=/usr/local/lib/sgu-welcome-wallpaper/Set-SguWelcomeWallpaper.sh
|
||||
Terminal=false
|
||||
NoDisplay=true
|
||||
X-GNOME-Autostart-enabled=true
|
||||
X-Cinnamon-Autostart-enabled=true
|
||||
EOF
|
||||
install -o root -g root -m 644 "$temporary_autostart" \
|
||||
"${autostart_directory}/sgu-welcome-wallpaper.desktop"
|
||||
rm -f "$temporary_autostart"
|
||||
}
|
||||
|
||||
verify_domain_connectivity() {
|
||||
need_command getent
|
||||
getent ahostsv4 "$DOMAIN_CONTROLLER" >/dev/null || \
|
||||
@@ -212,6 +429,9 @@ else
|
||||
fi
|
||||
|
||||
enable_sssd_dyndns
|
||||
enable_short_domain_login_names
|
||||
configure_sssd_responder_mode
|
||||
configure_graphical_domain_login
|
||||
systemctl enable --now sssd
|
||||
sssctl config-check
|
||||
systemctl restart sssd
|
||||
@@ -224,10 +444,13 @@ if [[ -n $ALLOW_GROUP ]]; then
|
||||
fi
|
||||
|
||||
enable_ssh
|
||||
configure_hyperv_enhanced_session
|
||||
install_welcome_wallpaper
|
||||
|
||||
printf '\nLinux enrollment completed.\n'
|
||||
printf ' Host: %s\n' "$HOST_FQDN"
|
||||
printf ' Domain: %s\n' "$DOMAIN_NAME"
|
||||
printf ' OU: %s\n' "$COMPUTER_OU"
|
||||
printf ' Login format: %%U@%s\n' "$DOMAIN_NAME"
|
||||
printf ' Welcome wallpaper: generated at each graphical sign-in when the desktop is supported.\n'
|
||||
realm list
|
||||
|
||||
@@ -622,13 +622,7 @@ $collectorFqdn = "$env:COMPUTERNAME.$DomainName"
|
||||
$userPolicyParameters = @{
|
||||
TargetOuDn = $usersOuDn
|
||||
DomainController = $env:COMPUTERNAME
|
||||
}
|
||||
$wallpaper = Get-ChildItem -LiteralPath $PackageSharePath -File -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.BaseName -eq 'wallpaper' -and $_.Extension -in @('.jpg','.jpeg','.png','.bmp') } |
|
||||
Sort-Object Name |
|
||||
Select-Object -First 1
|
||||
if ($wallpaper) {
|
||||
$userPolicyParameters.WallpaperPath = "\\$env:COMPUTERNAME\Packages\$($wallpaper.Name)"
|
||||
ClearManagedWallpaper = $true
|
||||
}
|
||||
& (Join-Path $scriptsRoot 'Set-SguDomainUserPolicies.ps1') @userPolicyParameters | Out-Null
|
||||
|
||||
|
||||
@@ -37,6 +37,10 @@ $defaultProviderPolicyPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System'
|
||||
$interactiveLogonPolicyPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
|
||||
$accountPictureSourcePath = Join-Path $PublishPath 'branding\user.png'
|
||||
$accountPictureDirectory = Join-Path $env:ProgramData 'Microsoft\User Account Pictures'
|
||||
$welcomeWallpaperSourcePath = Join-Path $PublishPath 'branding\darkblue.jpg'
|
||||
$welcomeWallpaperScriptSourcePath = Join-Path $PublishPath 'branding\Set-SguWelcomeWallpaper.ps1'
|
||||
$welcomeFontsSourcePath = Join-Path $PublishPath 'branding\fonts'
|
||||
$welcomeWallpaperDirectory = Join-Path $env:ProgramData 'SGU\Branding'
|
||||
|
||||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
|
||||
@@ -122,6 +126,25 @@ function Install-DefaultAccountPicture {
|
||||
return $true
|
||||
}
|
||||
|
||||
function Install-WelcomeWallpaperAssets {
|
||||
if (-not (Test-Path -LiteralPath $welcomeWallpaperSourcePath -PathType Leaf) -or
|
||||
-not (Test-Path -LiteralPath $welcomeWallpaperScriptSourcePath -PathType Leaf)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Path $welcomeWallpaperDirectory -Force | Out-Null
|
||||
Copy-Item -LiteralPath $welcomeWallpaperSourcePath `
|
||||
-Destination (Join-Path $welcomeWallpaperDirectory 'darkblue.jpg') -Force
|
||||
Copy-Item -LiteralPath $welcomeWallpaperScriptSourcePath `
|
||||
-Destination (Join-Path $welcomeWallpaperDirectory 'Set-SguWelcomeWallpaper.ps1') -Force
|
||||
if (Test-Path -LiteralPath $welcomeFontsSourcePath -PathType Container) {
|
||||
$fontDestination = Join-Path $welcomeWallpaperDirectory 'fonts'
|
||||
New-Item -ItemType Directory -Path $fontDestination -Force | Out-Null
|
||||
Copy-Item -Path (Join-Path $welcomeFontsSourcePath '*') -Destination $fontDestination -Force
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
if (-not (Test-DotNet10Runtime)) {
|
||||
if (-not $InstallDotNetRuntime) {
|
||||
throw 'Microsoft .NET 10 x64 runtime is required. Re-run with -InstallDotNetRuntime or install it first.'
|
||||
@@ -235,6 +258,7 @@ if ($PSCmdlet.ShouldProcess($installPath, 'Install and register the SGU Credenti
|
||||
# The domain GPO selects the Windows default account picture. Install its
|
||||
# branded bitmap during enrollment so no per-machine manual setup is needed.
|
||||
Install-DefaultAccountPicture -SourcePath $accountPictureSourcePath | Out-Null
|
||||
Install-WelcomeWallpaperAssets | Out-Null
|
||||
|
||||
New-Item -ItemType Directory -Path (Split-Path $settingsPath -Parent) -Force | Out-Null
|
||||
$settingsJson = @{
|
||||
@@ -324,4 +348,7 @@ catch {
|
||||
-LiteralPath $defaultProviderPolicyPath `
|
||||
-Name EnumerateLocalUsers) -eq 0
|
||||
SystemPasswordProviderPreserved = $true
|
||||
WelcomeWallpaperAssetsInstalled =
|
||||
(Test-Path -LiteralPath (Join-Path $welcomeWallpaperDirectory 'darkblue.jpg') -PathType Leaf) -and
|
||||
(Test-Path -LiteralPath (Join-Path $welcomeWallpaperDirectory 'Set-SguWelcomeWallpaper.ps1') -PathType Leaf)
|
||||
}
|
||||
|
||||
@@ -91,6 +91,13 @@ foreach ($target in @($clientRoot,$serverRoot,$linuxClientRoot,$clientZip,$serve
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Path $clientRoot,$serverRoot,$linuxClientRoot -Force | Out-Null
|
||||
$welcomeFontNames = @(
|
||||
'IndivisaTextSans-Regular.otf',
|
||||
'IndivisaTextSans-Bold.otf',
|
||||
'IndivisaTextSans-BoldItalic.otf',
|
||||
'IndivisaTextSerif-Regular.otf',
|
||||
'IndivisaTextSerif-BoldItalic.otf'
|
||||
)
|
||||
|
||||
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Invoke-SguClientBootstrap.ps1') `
|
||||
-Destination (Join-Path $clientRoot 'Invoke-SguClientBootstrap.ps1')
|
||||
@@ -116,6 +123,14 @@ Copy-Item -Path (Join-Path $providerOutput '*') `
|
||||
-Recurse -Force
|
||||
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'assets\branding\lasalle-mascot-account.png') `
|
||||
-Destination (Join-Path $clientRoot 'payload\credential-provider\branding\user.png')
|
||||
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'assets\branding\darkblue.jpg') `
|
||||
-Destination (Join-Path $clientRoot 'payload\credential-provider\branding\darkblue.jpg')
|
||||
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Set-SguWelcomeWallpaper.ps1') `
|
||||
-Destination (Join-Path $clientRoot 'payload\credential-provider\branding\Set-SguWelcomeWallpaper.ps1')
|
||||
foreach ($fontName in $welcomeFontNames) {
|
||||
Copy-RequiredFile -Source (Join-Path $repositoryRoot "assets\branding\fonts\$fontName") `
|
||||
-Destination (Join-Path $clientRoot "payload\credential-provider\branding\fonts\$fontName")
|
||||
}
|
||||
Copy-RequiredFile -Source $runtimeInstaller.FullName `
|
||||
-Destination (Join-Path $clientRoot "payload\prerequisites\$($runtimeInstaller.Name)")
|
||||
Write-PackageManifest -PackageRoot $clientRoot -PackageVersion $Version -PackageKind Client
|
||||
@@ -129,6 +144,14 @@ Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Enroll-SguLinuxDomainClient.
|
||||
-Destination (Join-Path $linuxClientRoot 'Enroll-SguLinuxDomainClient.sh')
|
||||
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'docs\linux-client-enrollment.md') `
|
||||
-Destination (Join-Path $linuxClientRoot 'README.md')
|
||||
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Set-SguWelcomeWallpaper.sh') `
|
||||
-Destination (Join-Path $linuxClientRoot 'welcome-wallpaper\Set-SguWelcomeWallpaper.sh')
|
||||
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'assets\branding\darkblue.jpg') `
|
||||
-Destination (Join-Path $linuxClientRoot 'welcome-wallpaper\darkblue.jpg')
|
||||
foreach ($fontName in $welcomeFontNames) {
|
||||
Copy-RequiredFile -Source (Join-Path $repositoryRoot "assets\branding\fonts\$fontName") `
|
||||
-Destination (Join-Path $linuxClientRoot "welcome-wallpaper\fonts\$fontName")
|
||||
}
|
||||
Write-PackageManifest -PackageRoot $linuxClientRoot -PackageVersion $Version -PackageKind LinuxClient
|
||||
Compress-Archive -Path (Join-Path $linuxClientRoot '*') -DestinationPath $linuxClientZip `
|
||||
-CompressionLevel Optimal
|
||||
@@ -167,6 +190,14 @@ if ($ServerContentPath) {
|
||||
Copy-Item -Path (Join-Path $ServerContentPath '*') `
|
||||
-Destination $serverContentTarget -Recurse -Force
|
||||
}
|
||||
Copy-RequiredFile -Source (Join-Path $PSScriptRoot 'Set-SguWelcomeWallpaper.ps1') `
|
||||
-Destination (Join-Path $serverContentTarget 'welcome-wallpaper\Set-SguWelcomeWallpaper.ps1')
|
||||
Copy-RequiredFile -Source (Join-Path $repositoryRoot 'assets\branding\darkblue.jpg') `
|
||||
-Destination (Join-Path $serverContentTarget 'welcome-wallpaper\darkblue.jpg')
|
||||
foreach ($fontName in $welcomeFontNames) {
|
||||
Copy-RequiredFile -Source (Join-Path $repositoryRoot "assets\branding\fonts\$fontName") `
|
||||
-Destination (Join-Path $serverContentTarget "welcome-wallpaper\fonts\$fontName")
|
||||
}
|
||||
Write-PackageManifest -PackageRoot $serverRoot -PackageVersion $Version -PackageKind Server
|
||||
Compress-Archive -Path (Join-Path $serverRoot '*') -DestinationPath $serverZip `
|
||||
-CompressionLevel Optimal
|
||||
|
||||
@@ -3,7 +3,9 @@ param(
|
||||
[string]$TargetOuDn = 'OU=Laboratorio,DC=lci,DC=lasalle,DC=mx',
|
||||
[string]$GpoName = 'SGU - Windows client experience',
|
||||
[string]$DomainController = $env:COMPUTERNAME,
|
||||
[string]$EventCollectorFqdn
|
||||
[string]$EventCollectorFqdn,
|
||||
[string]$WelcomeWallpaperScriptPath = 'C:\ProgramData\SGU\Branding\Set-SguWelcomeWallpaper.ps1',
|
||||
[string]$WelcomeWallpaperBasePath = 'C:\ProgramData\SGU\Branding\darkblue.jpg'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
@@ -75,7 +77,11 @@ $interactiveLogonPolicyKey = 'HKLM\Software\Microsoft\Windows\CurrentVersion\Pol
|
||||
$accountPicturePolicyKey = 'HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer'
|
||||
$eventForwardingPolicyKey = 'HKLM\Software\Policies\Microsoft\Windows\EventLog\EventForwarding\SubscriptionManager'
|
||||
$auditPolicyKey = 'HKLM\System\CurrentControlSet\Control\Lsa'
|
||||
$runPolicyKey = 'HKLM\Software\Microsoft\Windows\CurrentVersion\Run'
|
||||
$personalizationPolicyKey = 'HKLM\Software\Policies\Microsoft\Windows\Personalization'
|
||||
$providerClassId = '{D789CFD8-5AD4-489F-9B83-7EB5D9D09335}'
|
||||
$welcomeWallpaperCommand = 'powershell.exe -NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "{0}" -BaseImagePath "{1}"' -f `
|
||||
$WelcomeWallpaperScriptPath,$WelcomeWallpaperBasePath
|
||||
$policies = @(
|
||||
@{ Key = $dataCollectionKey; Name = 'AllowTelemetry'; Type = 'DWord'; Value = 0 },
|
||||
@{ Key = $dataCollectionKey; Name = 'DisableTelemetryOptInSettingsUx'; Type = 'DWord'; Value = 1 },
|
||||
@@ -99,7 +105,13 @@ $policies = @(
|
||||
# Source-initiated Windows Event Forwarding. Kerberos authenticates domain
|
||||
# computers to the collector; no SGU password or reusable secret is logged.
|
||||
@{ Key = $eventForwardingPolicyKey; Name = '1'; Type = 'String'; Value = "Server=http://${EventCollectorFqdn}:5985/wsman/SubscriptionManager/WEC,Refresh=300" },
|
||||
@{ Key = $auditPolicyKey; Name = 'SCENoApplyLegacyAuditPolicy'; Type = 'DWord'; Value = 1 }
|
||||
@{ Key = $auditPolicyKey; Name = 'SCENoApplyLegacyAuditPolicy'; Type = 'DWord'; Value = 1 },
|
||||
|
||||
# The machine GPO remains the authority for every interactive session. The
|
||||
# local payload lets the first desktop render without depending on SMB.
|
||||
@{ Key = $runPolicyKey; Name = 'SGUWelcomeWallpaper'; Type = 'String'; Value = $welcomeWallpaperCommand },
|
||||
@{ Key = $personalizationPolicyKey; Name = 'LockScreenImage'; Type = 'String'; Value = $WelcomeWallpaperBasePath },
|
||||
@{ Key = $personalizationPolicyKey; Name = 'NoChangingLockScreen'; Type = 'DWord'; Value = 1 }
|
||||
)
|
||||
|
||||
$powerSettingIds = @(
|
||||
@@ -151,5 +163,7 @@ $linkEnabled = $link -and (
|
||||
LinkEnabled = [bool]$linkEnabled
|
||||
PolicyCount = $configuredPolicies.Count
|
||||
EventCollector = $EventCollectorFqdn
|
||||
WelcomeWallpaperCommand = $welcomeWallpaperCommand
|
||||
LockScreenImage = $WelcomeWallpaperBasePath
|
||||
Policies = [pscustomobject]$configuredPolicies
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ param(
|
||||
[string]$TargetOuDn = 'OU=Usuarios-SGU,DC=lci,DC=lasalle,DC=mx',
|
||||
[string]$GpoName = 'SGU - User session restrictions',
|
||||
[string]$DomainController = $env:COMPUTERNAME,
|
||||
[string]$WallpaperPath
|
||||
[string]$WallpaperPath,
|
||||
[switch]$ClearManagedWallpaper
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
@@ -115,6 +116,17 @@ if ($PSCmdlet.ShouldProcess($GpoName, 'Prevent SGU users from manually locking w
|
||||
-Type String `
|
||||
-Value '10' | Out-Null
|
||||
}
|
||||
elseif ($ClearManagedWallpaper) {
|
||||
foreach ($wallpaperValueName in 'Wallpaper','WallpaperStyle') {
|
||||
Remove-GPRegistryValue `
|
||||
-Name $GpoName `
|
||||
-Domain $domainName `
|
||||
-Server $DomainController `
|
||||
-Key $policyKey `
|
||||
-ValueName $wallpaperValueName `
|
||||
-ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$configuredValue = Get-GPRegistryValue `
|
||||
@@ -166,4 +178,5 @@ if ($WallpaperPath) {
|
||||
ScreenSaverDisabled = [string]$screenSaverValue.Value -eq '0'
|
||||
DarkMode = ([int]$appsThemeValue.Value -eq 0) -and ([int]$systemThemeValue.Value -eq 0)
|
||||
Wallpaper = $configuredWallpaper
|
||||
DynamicWallpaperAllowed = -not [bool]$configuredWallpaper
|
||||
}
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
#Requires -Version 5.1
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$BaseImagePath = (Join-Path $env:ProgramData 'SGU\Branding\darkblue.jpg'),
|
||||
[string]$FontsPath = (Join-Path $env:ProgramData 'SGU\Branding\fonts'),
|
||||
[string]$OutputPath,
|
||||
[string]$DisplayName,
|
||||
[string]$ComputerName = $env:COMPUTERNAME,
|
||||
[string]$Location,
|
||||
[string]$OrganizationalUnit,
|
||||
[ValidateRange(640, 16384)]
|
||||
[int]$CanvasWidth,
|
||||
[ValidateRange(480, 16384)]
|
||||
[int]$CanvasHeight,
|
||||
[switch]$SkipDirectoryLookup,
|
||||
[switch]$SkipApply
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$script:LogPath = Join-Path $env:LOCALAPPDATA 'SGU\Logs\welcome-wallpaper.log'
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
function Write-WelcomeLog {
|
||||
param([Parameter(Mandatory)][string]$Message)
|
||||
|
||||
try {
|
||||
$logDirectory = Split-Path $script:LogPath -Parent
|
||||
New-Item -ItemType Directory -Path $logDirectory -Force | Out-Null
|
||||
Add-Content -LiteralPath $script:LogPath `
|
||||
-Value ('{0:o} {1}' -f (Get-Date), $Message) `
|
||||
-Encoding UTF8
|
||||
}
|
||||
catch {
|
||||
# The wallpaper must still be generated when logging is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
function ConvertTo-LdapFilterValue {
|
||||
param([Parameter(Mandatory)][string]$Value)
|
||||
|
||||
return $Value.Replace('\', '\5c').Replace('*', '\2a').Replace('(', '\28').Replace(')', '\29').Replace(([string][char]0), '\00')
|
||||
}
|
||||
|
||||
function ConvertFrom-LdapRdnValue {
|
||||
param([Parameter(Mandatory)][string]$Value)
|
||||
|
||||
$decoded = [Text.RegularExpressions.Regex]::Replace(
|
||||
$Value,
|
||||
'\\([0-9A-Fa-f]{2})',
|
||||
{ param($match) [char][Convert]::ToByte($match.Groups[1].Value, 16) })
|
||||
return $decoded.Replace('\,', ',').Replace('\+', '+').Replace('\=', '=').Replace('\\', '\')
|
||||
}
|
||||
|
||||
function Get-ImmediateOrganizationalUnit {
|
||||
param([string]$DistinguishedName)
|
||||
|
||||
if (-not $DistinguishedName) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$parts = [Text.RegularExpressions.Regex]::Split($DistinguishedName, '(?<!\\),')
|
||||
foreach ($part in $parts) {
|
||||
if ($part.StartsWith('OU=', [StringComparison]::OrdinalIgnoreCase)) {
|
||||
return ConvertFrom-LdapRdnValue -Value $part.Substring(3)
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-DirectoryWelcomeMetadata {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$UserName,
|
||||
[Parameter(Mandatory)][string]$MachineName
|
||||
)
|
||||
|
||||
Add-Type -AssemblyName System.DirectoryServices
|
||||
$rootDse = [DirectoryServices.DirectoryEntry]::new('LDAP://RootDSE')
|
||||
try {
|
||||
$namingContext = [string]$rootDse.Properties['defaultNamingContext'][0]
|
||||
}
|
||||
finally {
|
||||
$rootDse.Dispose()
|
||||
}
|
||||
if (-not $namingContext) {
|
||||
throw 'Active Directory did not return a default naming context.'
|
||||
}
|
||||
|
||||
$searchRoot = [DirectoryServices.DirectoryEntry]::new("LDAP://$namingContext")
|
||||
try {
|
||||
$userSearcher = [DirectoryServices.DirectorySearcher]::new($searchRoot)
|
||||
try {
|
||||
$userSearcher.PageSize = 1
|
||||
$userSearcher.Filter = '(&(objectCategory=person)(objectClass=user)(sAMAccountName={0}))' -f `
|
||||
(ConvertTo-LdapFilterValue -Value $UserName)
|
||||
[void]$userSearcher.PropertiesToLoad.Add('displayName')
|
||||
$userResult = $userSearcher.FindOne()
|
||||
$directoryDisplayName = if ($userResult -and $userResult.Properties['displayname'].Count) {
|
||||
[string]$userResult.Properties['displayname'][0]
|
||||
}
|
||||
else {
|
||||
$null
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$userSearcher.Dispose()
|
||||
}
|
||||
|
||||
$computerSearcher = [DirectoryServices.DirectorySearcher]::new($searchRoot)
|
||||
try {
|
||||
$computerSearcher.PageSize = 1
|
||||
$computerSearcher.Filter = '(&(objectCategory=computer)(sAMAccountName={0}))' -f `
|
||||
(ConvertTo-LdapFilterValue -Value ($MachineName + '$'))
|
||||
[void]$computerSearcher.PropertiesToLoad.Add('location')
|
||||
[void]$computerSearcher.PropertiesToLoad.Add('distinguishedName')
|
||||
$computerResult = $computerSearcher.FindOne()
|
||||
$directoryLocation = if ($computerResult -and $computerResult.Properties['location'].Count) {
|
||||
[string]$computerResult.Properties['location'][0]
|
||||
}
|
||||
else {
|
||||
$null
|
||||
}
|
||||
$computerDn = if ($computerResult -and $computerResult.Properties['distinguishedname'].Count) {
|
||||
[string]$computerResult.Properties['distinguishedname'][0]
|
||||
}
|
||||
else {
|
||||
$null
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$computerSearcher.Dispose()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$searchRoot.Dispose()
|
||||
}
|
||||
|
||||
[pscustomobject]@{
|
||||
DisplayName = $directoryDisplayName
|
||||
Location = $directoryLocation
|
||||
OrganizationalUnit = Get-ImmediateOrganizationalUnit -DistinguishedName $computerDn
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SpanishArticle {
|
||||
param([Parameter(Mandatory)][string]$Value)
|
||||
|
||||
if ($Value -match '^(Sala|Aula|Facultad|Unidad|Biblioteca|Oficina|Coordinaci.n)\b') {
|
||||
return 'la'
|
||||
}
|
||||
if ($Value -match '^(Laboratorio|Centro|Edificio|Campus|Taller|Auditorio)\b') {
|
||||
return 'el'
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-WelcomeLocationText {
|
||||
param(
|
||||
[string]$Room,
|
||||
[string]$OuName
|
||||
)
|
||||
|
||||
$located = 'Est{0}s ubicado en' -f [char]0x00E1
|
||||
$engineeringLab = 'Bienvenido al Laboratorio de C{0}mputo de Ingenier{1}a.' -f [char]0x00F3,[char]0x00ED
|
||||
$Room = if ($Room) { $Room.Trim() } else { $null }
|
||||
$OuName = if ($OuName) { $OuName.Trim() } else { $null }
|
||||
|
||||
if ($Room -and $OuName) {
|
||||
$roomArticle = Get-SpanishArticle -Value $Room
|
||||
$ouArticle = Get-SpanishArticle -Value $OuName
|
||||
$roomPhrase = if ($roomArticle) { "$roomArticle $Room" } else { $Room }
|
||||
$ouPhrase = if ($ouArticle -eq 'el') { "del $OuName" } elseif ($ouArticle) { "de $ouArticle $OuName" } else { "de $OuName" }
|
||||
return "$located $roomPhrase $ouPhrase."
|
||||
}
|
||||
if ($Room) {
|
||||
$article = Get-SpanishArticle -Value $Room
|
||||
$phrase = if ($article) { "$article $Room" } else { $Room }
|
||||
return "$located $phrase."
|
||||
}
|
||||
if ($OuName) {
|
||||
$article = Get-SpanishArticle -Value $OuName
|
||||
$phrase = if ($article) { "$article $OuName" } else { $OuName }
|
||||
return "$located $phrase."
|
||||
}
|
||||
return $engineeringLab
|
||||
}
|
||||
|
||||
function Get-AvailableFontFamily {
|
||||
param(
|
||||
[Parameter(Mandatory)][string[]]$Candidates,
|
||||
[Drawing.FontFamily[]]$PrivateFamilies = @()
|
||||
)
|
||||
|
||||
foreach ($candidate in $Candidates) {
|
||||
$privateMatch = @($PrivateFamilies | Where-Object Name -eq $candidate | Select-Object -First 1)
|
||||
if ($privateMatch.Count) {
|
||||
return $privateMatch[0]
|
||||
}
|
||||
if (@([Drawing.FontFamily]::Families | ForEach-Object Name) -contains $candidate) {
|
||||
return [Drawing.FontFamily]::new($candidate)
|
||||
}
|
||||
}
|
||||
return [Drawing.FontFamily]::GenericSansSerif
|
||||
}
|
||||
|
||||
function New-WelcomeFont {
|
||||
param(
|
||||
[Parameter(Mandatory)][Drawing.FontFamily]$Family,
|
||||
[Parameter(Mandatory)][single]$Size,
|
||||
[Parameter(Mandatory)][Drawing.FontStyle]$PreferredStyle
|
||||
)
|
||||
|
||||
$style = if ($Family.IsStyleAvailable($PreferredStyle)) { $PreferredStyle } `
|
||||
elseif ($Family.IsStyleAvailable([Drawing.FontStyle]::Bold)) { [Drawing.FontStyle]::Bold } `
|
||||
else { [Drawing.FontStyle]::Regular }
|
||||
return [Drawing.Font]::new($Family, $Size, $style, [Drawing.GraphicsUnit]::Pixel)
|
||||
}
|
||||
|
||||
function Draw-CenteredText {
|
||||
param(
|
||||
[Parameter(Mandatory)][Drawing.Graphics]$Graphics,
|
||||
[Parameter(Mandatory)][string]$Text,
|
||||
[Parameter(Mandatory)][Drawing.Font]$Font,
|
||||
[Parameter(Mandatory)][Drawing.Brush]$Brush,
|
||||
[Parameter(Mandatory)][Drawing.RectangleF]$Bounds,
|
||||
[Parameter(Mandatory)][Drawing.StringFormat]$Format,
|
||||
[single]$ShadowOffset = 2
|
||||
)
|
||||
|
||||
$shadowBounds = [Drawing.RectangleF]::new(
|
||||
$Bounds.X + $ShadowOffset,
|
||||
$Bounds.Y + $ShadowOffset,
|
||||
$Bounds.Width,
|
||||
$Bounds.Height)
|
||||
$shadow = [Drawing.SolidBrush]::new([Drawing.Color]::FromArgb(135, 0, 0, 0))
|
||||
try {
|
||||
$Graphics.DrawString($Text, $Font, $shadow, $shadowBounds, $Format)
|
||||
$Graphics.DrawString($Text, $Font, $Brush, $Bounds, $Format)
|
||||
}
|
||||
finally {
|
||||
$shadow.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
trap {
|
||||
Write-WelcomeLog -Message ('ERROR ' + $_.Exception.Message)
|
||||
throw
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $BaseImagePath -PathType Leaf)) {
|
||||
throw "The welcome wallpaper base image does not exist: $BaseImagePath"
|
||||
}
|
||||
|
||||
$userName = [Environment]::UserName
|
||||
$metadata = $null
|
||||
if (-not $SkipDirectoryLookup) {
|
||||
try {
|
||||
$metadata = Get-DirectoryWelcomeMetadata -UserName $userName -MachineName $ComputerName
|
||||
}
|
||||
catch {
|
||||
Write-WelcomeLog -Message ('WARN Active Directory metadata was unavailable: ' + $_.Exception.Message)
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $PSBoundParameters.ContainsKey('DisplayName')) {
|
||||
$DisplayName = if ($metadata -and $metadata.DisplayName) { $metadata.DisplayName } else { $userName }
|
||||
}
|
||||
if (-not $DisplayName) {
|
||||
$DisplayName = $userName
|
||||
}
|
||||
if (-not $PSBoundParameters.ContainsKey('Location') -and $metadata) {
|
||||
$Location = $metadata.Location
|
||||
}
|
||||
if (-not $PSBoundParameters.ContainsKey('OrganizationalUnit') -and $metadata) {
|
||||
$OrganizationalUnit = $metadata.OrganizationalUnit
|
||||
}
|
||||
$locationText = Get-WelcomeLocationText -Room $Location -OuName $OrganizationalUnit
|
||||
|
||||
if (-not $CanvasWidth -or -not $CanvasHeight) {
|
||||
try {
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
$screenBounds = [Windows.Forms.Screen]::PrimaryScreen.Bounds
|
||||
if (-not $CanvasWidth) { $CanvasWidth = $screenBounds.Width }
|
||||
if (-not $CanvasHeight) { $CanvasHeight = $screenBounds.Height }
|
||||
}
|
||||
catch {
|
||||
if (-not $CanvasWidth) { $CanvasWidth = 1600 }
|
||||
if (-not $CanvasHeight) { $CanvasHeight = 1000 }
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $OutputPath) {
|
||||
$wallpaperDirectory = Join-Path $env:LOCALAPPDATA 'SGU\Wallpapers'
|
||||
$safeComputerName = $ComputerName -replace '[^A-Za-z0-9_.-]', '_'
|
||||
$OutputPath = Join-Path $wallpaperDirectory "welcome-$safeComputerName.jpg"
|
||||
}
|
||||
New-Item -ItemType Directory -Path (Split-Path $OutputPath -Parent) -Force | Out-Null
|
||||
|
||||
$source = [Drawing.Image]::FromFile($BaseImagePath)
|
||||
$canvas = [Drawing.Bitmap]::new($CanvasWidth, $CanvasHeight, [Drawing.Imaging.PixelFormat]::Format24bppRgb)
|
||||
try {
|
||||
$graphics = [Drawing.Graphics]::FromImage($canvas)
|
||||
try {
|
||||
$graphics.SmoothingMode = [Drawing.Drawing2D.SmoothingMode]::HighQuality
|
||||
$graphics.InterpolationMode = [Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
||||
$graphics.PixelOffsetMode = [Drawing.Drawing2D.PixelOffsetMode]::HighQuality
|
||||
$graphics.TextRenderingHint = [Drawing.Text.TextRenderingHint]::AntiAliasGridFit
|
||||
|
||||
$sourceRatio = $source.Width / $source.Height
|
||||
$targetRatio = $CanvasWidth / $CanvasHeight
|
||||
if ($sourceRatio -gt $targetRatio) {
|
||||
$sourceHeight = $source.Height
|
||||
$sourceWidth = [int]($sourceHeight * $targetRatio)
|
||||
$sourceX = [int](($source.Width - $sourceWidth) / 2)
|
||||
$sourceY = 0
|
||||
}
|
||||
else {
|
||||
$sourceWidth = $source.Width
|
||||
$sourceHeight = [int]($sourceWidth / $targetRatio)
|
||||
$sourceX = 0
|
||||
$sourceY = [int](($source.Height - $sourceHeight) / 2)
|
||||
}
|
||||
$graphics.DrawImage(
|
||||
$source,
|
||||
[Drawing.Rectangle]::new(0, 0, $CanvasWidth, $CanvasHeight),
|
||||
$sourceX,
|
||||
$sourceY,
|
||||
$sourceWidth,
|
||||
$sourceHeight,
|
||||
[Drawing.GraphicsUnit]::Pixel)
|
||||
|
||||
$scale = [Math]::Min($CanvasWidth / 1600.0, $CanvasHeight / 1000.0)
|
||||
$panelWidth = [single]($CanvasWidth * 0.76)
|
||||
$panelHeight = [single](310 * $scale)
|
||||
$panelX = [single](($CanvasWidth - $panelWidth) / 2)
|
||||
$panelY = [single]($CanvasHeight * 0.50 - ($panelHeight / 2))
|
||||
$panelBrush = [Drawing.SolidBrush]::new([Drawing.Color]::FromArgb(72, 0, 13, 58))
|
||||
$whiteBrush = [Drawing.SolidBrush]::new([Drawing.Color]::White)
|
||||
$accentBrush = [Drawing.SolidBrush]::new([Drawing.Color]::FromArgb(255, 211, 226, 255))
|
||||
$linePen = [Drawing.Pen]::new([Drawing.Color]::FromArgb(155, 211, 226, 255), [single](2 * $scale))
|
||||
$privateFonts = [Drawing.Text.PrivateFontCollection]::new()
|
||||
if (Test-Path -LiteralPath $FontsPath -PathType Container) {
|
||||
foreach ($fontFile in Get-ChildItem -LiteralPath $FontsPath -File |
|
||||
Where-Object Extension -in '.otf','.ttf') {
|
||||
try {
|
||||
$privateFonts.AddFontFile($fontFile.FullName)
|
||||
}
|
||||
catch {
|
||||
Write-WelcomeLog -Message ("WARN Font could not be loaded: {0}" -f $fontFile.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
$sansFamily = Get-AvailableFontFamily `
|
||||
-Candidates @('Indivisa Text Sans', 'Indivisa Text', 'Segoe UI') `
|
||||
-PrivateFamilies $privateFonts.Families
|
||||
$serifFamily = Get-AvailableFontFamily `
|
||||
-Candidates @('Indivisa Text Serif', 'Indivisa Serif', 'Georgia') `
|
||||
-PrivateFamilies $privateFonts.Families
|
||||
$welcomeFont = New-WelcomeFont -Family $sansFamily -Size ([single](34 * $scale)) -PreferredStyle ([Drawing.FontStyle]::Bold)
|
||||
$nameFont = New-WelcomeFont -Family $serifFamily -Size ([single](70 * $scale)) `
|
||||
-PreferredStyle ([Drawing.FontStyle]::Bold -bor [Drawing.FontStyle]::Italic)
|
||||
$locationFont = New-WelcomeFont -Family $sansFamily -Size ([single](27 * $scale)) `
|
||||
-PreferredStyle ([Drawing.FontStyle]::Regular)
|
||||
$format = [Drawing.StringFormat]::new()
|
||||
$format.Alignment = [Drawing.StringAlignment]::Center
|
||||
$format.LineAlignment = [Drawing.StringAlignment]::Center
|
||||
$format.Trimming = [Drawing.StringTrimming]::EllipsisWord
|
||||
try {
|
||||
$graphics.FillRectangle($panelBrush, $panelX, $panelY, $panelWidth, $panelHeight)
|
||||
Draw-CenteredText -Graphics $graphics -Text 'Bienvenido,' -Font $welcomeFont `
|
||||
-Brush $accentBrush -Bounds ([Drawing.RectangleF]::new($panelX, $panelY + 24*$scale, $panelWidth, 50*$scale)) -Format $format
|
||||
Draw-CenteredText -Graphics $graphics -Text $DisplayName -Font $nameFont `
|
||||
-Brush $whiteBrush -Bounds ([Drawing.RectangleF]::new($panelX + 30*$scale, $panelY + 64*$scale, $panelWidth - 60*$scale, 105*$scale)) -Format $format
|
||||
$graphics.DrawLine($linePen, $panelX + 150*$scale, $panelY + 180*$scale, $panelX + $panelWidth - 150*$scale, $panelY + 180*$scale)
|
||||
Draw-CenteredText -Graphics $graphics -Text $locationText -Font $locationFont `
|
||||
-Brush $accentBrush -Bounds ([Drawing.RectangleF]::new($panelX + 60*$scale, $panelY + 190*$scale, $panelWidth - 120*$scale, 94*$scale)) -Format $format
|
||||
}
|
||||
finally {
|
||||
$format.Dispose()
|
||||
$locationFont.Dispose()
|
||||
$nameFont.Dispose()
|
||||
$welcomeFont.Dispose()
|
||||
$serifFamily.Dispose()
|
||||
$sansFamily.Dispose()
|
||||
$privateFonts.Dispose()
|
||||
$linePen.Dispose()
|
||||
$accentBrush.Dispose()
|
||||
$whiteBrush.Dispose()
|
||||
$panelBrush.Dispose()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$graphics.Dispose()
|
||||
}
|
||||
|
||||
$jpegCodec = [Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() |
|
||||
Where-Object MimeType -eq 'image/jpeg' |
|
||||
Select-Object -First 1
|
||||
$encoderParameters = [Drawing.Imaging.EncoderParameters]::new(1)
|
||||
$encoderParameters.Param[0] = [Drawing.Imaging.EncoderParameter]::new(
|
||||
[Drawing.Imaging.Encoder]::Quality,
|
||||
[long]94)
|
||||
try {
|
||||
$canvas.Save($OutputPath, $jpegCodec, $encoderParameters)
|
||||
}
|
||||
finally {
|
||||
$encoderParameters.Dispose()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
$canvas.Dispose()
|
||||
$source.Dispose()
|
||||
}
|
||||
|
||||
if (-not $SkipApply) {
|
||||
$desktopKey = 'HKCU:\Control Panel\Desktop'
|
||||
Set-ItemProperty -LiteralPath $desktopKey -Name Wallpaper -Value $OutputPath
|
||||
Set-ItemProperty -LiteralPath $desktopKey -Name WallpaperStyle -Value '10'
|
||||
Set-ItemProperty -LiteralPath $desktopKey -Name TileWallpaper -Value '0'
|
||||
if (-not ('Sgu.NativeMethods' -as [type])) {
|
||||
Add-Type @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
namespace Sgu {
|
||||
public static class NativeMethods {
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
public static extern bool SystemParametersInfo(int action, int parameter, string value, int flags);
|
||||
}
|
||||
}
|
||||
'@
|
||||
}
|
||||
if (-not [Sgu.NativeMethods]::SystemParametersInfo(20, 0, $OutputPath, 3)) {
|
||||
throw "Windows could not apply the generated wallpaper. Win32 error: $([Runtime.InteropServices.Marshal]::GetLastWin32Error())"
|
||||
}
|
||||
}
|
||||
|
||||
Write-WelcomeLog -Message ("OK computer={0}; location={1}; ou={2}; output={3}" -f $ComputerName,[bool]$Location,[bool]$OrganizationalUnit,$OutputPath)
|
||||
[pscustomobject]@{
|
||||
DisplayName = $DisplayName
|
||||
ComputerName = $ComputerName
|
||||
Location = $Location
|
||||
OrganizationalUnit = $OrganizationalUnit
|
||||
LocationText = $locationText
|
||||
OutputPath = $OutputPath
|
||||
Applied = -not $SkipApply
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generates and applies the SGU welcome wallpaper inside a Linux desktop session.
|
||||
# It is intentionally best-effort: unavailable AD metadata or desktop APIs must
|
||||
# never delay or prevent the user's session from opening.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
CONFIG_PATH=${SGU_WELCOME_CONFIG:-/etc/sgu/welcome-wallpaper.conf}
|
||||
INSTALL_ROOT=${SGU_WELCOME_ROOT:-/usr/local/lib/sgu-welcome-wallpaper}
|
||||
BASE_IMAGE=${SGU_WELCOME_BASE_IMAGE:-${INSTALL_ROOT}/darkblue.jpg}
|
||||
|
||||
if [[ -r $CONFIG_PATH ]]; then
|
||||
# The root-owned file contains only deployment metadata, never credentials.
|
||||
# shellcheck source=/dev/null
|
||||
source "$CONFIG_PATH"
|
||||
fi
|
||||
|
||||
DOMAIN_CONTROLLER=${DOMAIN_CONTROLLER:-}
|
||||
DOMAIN_NAME=${DOMAIN_NAME:-}
|
||||
BASE_DN=${BASE_DN:-}
|
||||
state_root=${XDG_STATE_HOME:-${HOME}/.local/state}
|
||||
wallpaper_root=${XDG_CACHE_HOME:-${HOME}/.cache}/sgu/wallpapers
|
||||
log_path="${state_root}/sgu/welcome-wallpaper.log"
|
||||
|
||||
log_message() {
|
||||
mkdir -p "$(dirname "$log_path")" 2>/dev/null || true
|
||||
printf '%s %s\n' "$(date --iso-8601=seconds 2>/dev/null || date)" "$*" >>"$log_path" 2>/dev/null || true
|
||||
}
|
||||
|
||||
fail_softly() {
|
||||
log_message "ERROR $*"
|
||||
exit 0
|
||||
}
|
||||
|
||||
[[ -r $BASE_IMAGE ]] || fail_softly "Missing base image: $BASE_IMAGE"
|
||||
|
||||
if command -v magick >/dev/null 2>&1; then
|
||||
image_command=(magick)
|
||||
elif command -v convert >/dev/null 2>&1; then
|
||||
image_command=(convert)
|
||||
else
|
||||
fail_softly 'ImageMagick is unavailable.'
|
||||
fi
|
||||
|
||||
raw_user=${USER:-$(id -un 2>/dev/null || printf user)}
|
||||
account_name=${raw_user%@*}
|
||||
account_name=${account_name##*\\}
|
||||
display_name=$(getent passwd "$raw_user" 2>/dev/null | awk -F: 'NR == 1 { split($5,a,","); print a[1] }')
|
||||
[[ -n $display_name ]] || display_name=$account_name
|
||||
|
||||
computer_name=$(hostname -s 2>/dev/null || true)
|
||||
computer_name=${computer_name^^}
|
||||
location=''
|
||||
distinguished_name=''
|
||||
organizational_unit=''
|
||||
|
||||
read_ldif_value() {
|
||||
local attribute=$1
|
||||
local content=$2
|
||||
local line value
|
||||
line=$(printf '%s\n' "$content" | awk -v name="$attribute" '
|
||||
BEGIN { IGNORECASE=1 }
|
||||
index(tolower($0), tolower(name) ":") == 1 { print; exit }
|
||||
')
|
||||
[[ -n $line ]] || return 0
|
||||
if [[ $line == "${attribute}:: "* || ${line,,} == "${attribute,,}:: "* ]]; then
|
||||
value=${line#*:: }
|
||||
printf '%s' "$value" | base64 --decode 2>/dev/null || true
|
||||
else
|
||||
printf '%s' "${line#*: }"
|
||||
fi
|
||||
}
|
||||
|
||||
# SSSD normally obtains a Kerberos ticket during PAM authentication. Use that
|
||||
# ticket for a read-only AD query; never embed a bind password in this helper.
|
||||
if [[ -n $DOMAIN_CONTROLLER && -n $BASE_DN ]] &&
|
||||
command -v ldapsearch >/dev/null 2>&1 &&
|
||||
command -v klist >/dev/null 2>&1 && klist -s; then
|
||||
ldap_server=$DOMAIN_CONTROLLER
|
||||
if [[ -n $DOMAIN_NAME ]] && command -v resolvectl >/dev/null 2>&1; then
|
||||
discovered_server=$(resolvectl query --type=SRV \
|
||||
"_ldap._tcp.dc._msdcs.${DOMAIN_NAME}" 2>/dev/null |
|
||||
awk '/ IN SRV / { for (i=1; i<=NF; i++) if ($i == "SRV") { print $(i+4); exit } }' |
|
||||
sed 's/\.$//' || true)
|
||||
[[ -n $discovered_server ]] && ldap_server=$discovered_server
|
||||
elif [[ -n $DOMAIN_NAME ]] && command -v dig >/dev/null 2>&1; then
|
||||
discovered_server=$(dig +short SRV "_ldap._tcp.dc._msdcs.${DOMAIN_NAME}" 2>/dev/null |
|
||||
awk 'NR == 1 { print $4 }' | sed 's/\.$//' || true)
|
||||
[[ -n $discovered_server ]] && ldap_server=$discovered_server
|
||||
fi
|
||||
ldap_result=$(ldapsearch -LLL -N -o ldif-wrap=no -Y GSSAPI \
|
||||
-H "ldap://${ldap_server}" -b "$BASE_DN" \
|
||||
"(&(objectCategory=computer)(sAMAccountName=${computer_name}\\24))" \
|
||||
location distinguishedName 2>/dev/null || true)
|
||||
location=$(read_ldif_value location "$ldap_result")
|
||||
distinguished_name=$(read_ldif_value distinguishedName "$ldap_result")
|
||||
if [[ $distinguished_name =~ ,OU=([^,]+) ]]; then
|
||||
organizational_unit=${BASH_REMATCH[1]}
|
||||
organizational_unit=${organizational_unit//\\,/,}
|
||||
organizational_unit=${organizational_unit//\\=/=}
|
||||
organizational_unit=${organizational_unit//\\+/+}
|
||||
fi
|
||||
|
||||
# SSSD's GECOS field is not guaranteed to expose AD displayName. Query it
|
||||
# through the same authenticated LDAP session and retain the account-name
|
||||
# fallback when the institutional identifier contains unexpected symbols.
|
||||
if [[ $account_name =~ ^[A-Za-z0-9._-]+$ ]]; then
|
||||
user_result=$(ldapsearch -LLL -N -o ldif-wrap=no -Y GSSAPI \
|
||||
-H "ldap://${ldap_server}" -b "$BASE_DN" \
|
||||
"(&(objectCategory=person)(objectClass=user)(sAMAccountName=${account_name}))" \
|
||||
displayName 2>/dev/null || true)
|
||||
directory_display_name=$(read_ldif_value displayName "$user_result")
|
||||
[[ -n $directory_display_name ]] && display_name=$directory_display_name
|
||||
fi
|
||||
else
|
||||
log_message 'WARN AD metadata query skipped because Kerberos or LDAP session data was unavailable.'
|
||||
fi
|
||||
|
||||
article_for() {
|
||||
local value=${1,,}
|
||||
case "$value" in
|
||||
sala*|aula*|facultad*|unidad*|biblioteca*|oficina*|coordinación*) printf la ;;
|
||||
laboratorio*|centro*|edificio*|campus*|taller*|auditorio*) printf el ;;
|
||||
*) printf '' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
with_article() {
|
||||
local value=$1
|
||||
local article
|
||||
article=$(article_for "$value")
|
||||
if [[ -n $article ]]; then
|
||||
printf '%s %s' "$article" "$value"
|
||||
else
|
||||
printf '%s' "$value"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ -n $location && -n $organizational_unit ]]; then
|
||||
room_phrase=$(with_article "$location")
|
||||
ou_article=$(article_for "$organizational_unit")
|
||||
if [[ $ou_article == el ]]; then
|
||||
ou_phrase="del ${organizational_unit}"
|
||||
elif [[ -n $ou_article ]]; then
|
||||
ou_phrase="de ${ou_article} ${organizational_unit}"
|
||||
else
|
||||
ou_phrase="de ${organizational_unit}"
|
||||
fi
|
||||
location_text="Estás ubicado en ${room_phrase} ${ou_phrase}."
|
||||
elif [[ -n $location ]]; then
|
||||
location_text="Estás ubicado en $(with_article "$location")."
|
||||
elif [[ -n $organizational_unit ]]; then
|
||||
location_text="Estás ubicado en $(with_article "$organizational_unit")."
|
||||
else
|
||||
location_text='Bienvenido al Laboratorio de Cómputo de Ingeniería.'
|
||||
fi
|
||||
|
||||
width=1600
|
||||
height=1000
|
||||
if command -v xrandr >/dev/null 2>&1; then
|
||||
geometry=$(xrandr --current 2>/dev/null | awk '/\*/ { print $1; exit }')
|
||||
if [[ $geometry =~ ^([0-9]+)x([0-9]+)$ ]]; then
|
||||
width=${BASH_REMATCH[1]}
|
||||
height=${BASH_REMATCH[2]}
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$wallpaper_root" "$(dirname "$log_path")" ||
|
||||
fail_softly "Cannot create welcome wallpaper state directories."
|
||||
safe_computer=${computer_name//[^A-Za-z0-9_.-]/_}
|
||||
output_path="${wallpaper_root}/welcome-${safe_computer}.jpg"
|
||||
scale=$(( height * 100 / 1000 ))
|
||||
(( scale > 45 )) || scale=45
|
||||
welcome_size=$(( 34 * scale / 100 ))
|
||||
name_size=$(( 70 * scale / 100 ))
|
||||
location_size=$(( 27 * scale / 100 ))
|
||||
panel_width=$(( width * 76 / 100 ))
|
||||
panel_height=$(( 310 * scale / 100 ))
|
||||
panel_x1=$(( (width - panel_width) / 2 ))
|
||||
panel_y1=$(( height / 2 - panel_height / 2 ))
|
||||
panel_x2=$(( panel_x1 + panel_width ))
|
||||
panel_y2=$(( panel_y1 + panel_height ))
|
||||
|
||||
sans_font='DejaVu-Sans'
|
||||
serif_font='DejaVu-Serif'
|
||||
if [[ -r ${INSTALL_ROOT}/fonts/IndivisaTextSans-Bold.otf ]]; then
|
||||
sans_font="${INSTALL_ROOT}/fonts/IndivisaTextSans-Bold.otf"
|
||||
fi
|
||||
if [[ -r ${INSTALL_ROOT}/fonts/IndivisaTextSerif-BoldItalic.otf ]]; then
|
||||
serif_font="${INSTALL_ROOT}/fonts/IndivisaTextSerif-BoldItalic.otf"
|
||||
fi
|
||||
if [[ $sans_font == DejaVu-Sans ]] && command -v fc-list >/dev/null 2>&1; then
|
||||
if fc-list : family | grep -Fqi 'Indivisa Text Sans'; then
|
||||
sans_font='Indivisa Text Sans'
|
||||
elif fc-list : family | grep -Fqi 'Indivisa Text'; then
|
||||
sans_font='Indivisa Text'
|
||||
fi
|
||||
fi
|
||||
if [[ $serif_font == DejaVu-Serif ]] && command -v fc-list >/dev/null 2>&1; then
|
||||
if fc-list : family | grep -Fqi 'Indivisa Text Serif'; then
|
||||
serif_font='Indivisa Text Serif'
|
||||
elif fc-list : family | grep -Fqi 'Indivisa Serif'; then
|
||||
serif_font='Indivisa Serif'
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! "${image_command[@]}" "$BASE_IMAGE" \
|
||||
-resize "${width}x${height}^" -gravity center -extent "${width}x${height}" \
|
||||
-fill 'rgba(0,13,58,0.30)' -draw "rectangle ${panel_x1},${panel_y1} ${panel_x2},${panel_y2}" \
|
||||
-gravity center \
|
||||
-font "$sans_font" -weight 700 -style Normal -pointsize "$welcome_size" \
|
||||
-fill '#D3E2FF' -stroke 'rgba(0,0,0,0.48)' -strokewidth 1 \
|
||||
-annotate "+0-$(( 92 * scale / 100 ))" 'Bienvenido,' \
|
||||
-font "$serif_font" -weight 700 -style Italic -pointsize "$name_size" \
|
||||
-fill white -annotate "+0-$(( 22 * scale / 100 ))" "$display_name" \
|
||||
-font "$sans_font" -weight 400 -style Normal -pointsize "$location_size" \
|
||||
-fill '#D3E2FF' -annotate "+0+$(( 88 * scale / 100 ))" "$location_text" \
|
||||
-quality 94 "$output_path" 2>>"$log_path"; then
|
||||
fail_softly 'ImageMagick could not render the welcome wallpaper.'
|
||||
fi
|
||||
|
||||
applied=false
|
||||
if command -v gsettings >/dev/null 2>&1; then
|
||||
if gsettings list-schemas 2>/dev/null | grep -Fxq 'org.cinnamon.desktop.background'; then
|
||||
gsettings set org.cinnamon.desktop.background picture-uri "file://${output_path}" >/dev/null 2>&1 || true
|
||||
gsettings set org.cinnamon.desktop.background picture-options zoom >/dev/null 2>&1 || true
|
||||
applied=true
|
||||
fi
|
||||
if gsettings list-schemas 2>/dev/null | grep -Fxq 'org.gnome.desktop.background'; then
|
||||
gsettings set org.gnome.desktop.background picture-uri "file://${output_path}" >/dev/null 2>&1 || true
|
||||
gsettings set org.gnome.desktop.background picture-uri-dark "file://${output_path}" >/dev/null 2>&1 || true
|
||||
gsettings set org.gnome.desktop.background picture-options zoom >/dev/null 2>&1 || true
|
||||
applied=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $applied == false ]] && command -v xfconf-query >/dev/null 2>&1; then
|
||||
while IFS= read -r property; do
|
||||
xfconf-query -c xfce4-desktop -p "$property" -s "$output_path" >/dev/null 2>&1 || true
|
||||
applied=true
|
||||
done < <(xfconf-query -c xfce4-desktop -l 2>/dev/null | grep '/last-image$' || true)
|
||||
fi
|
||||
|
||||
if [[ $applied == true ]]; then
|
||||
log_message "OK computer=${computer_name}; location=$([[ -n $location ]] && printf true || printf false); ou=$([[ -n $organizational_unit ]] && printf true || printf false); output=${output_path}"
|
||||
else
|
||||
log_message 'WARN Wallpaper rendered, but no supported desktop background API was found.'
|
||||
fi
|
||||
exit 0
|
||||
@@ -36,6 +36,12 @@ public static class SguProfileParser
|
||||
private const string StudentPostalCodeId = "ctl00_contenedor_HistorialAlumno1_lblCPAlumnoHP";
|
||||
|
||||
public static InstitutionalProfile? ParseAdministrative(string html, string expectedEmployeeNumber)
|
||||
=> ParseStaffHeader(html, expectedEmployeeNumber);
|
||||
|
||||
public static InstitutionalProfile? ParseProfessorPayroll(string html, string expectedEmployeeNumber)
|
||||
=> ParseStaffHeader(html, expectedEmployeeNumber);
|
||||
|
||||
private static InstitutionalProfile? ParseStaffHeader(string html, string expectedEmployeeNumber)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(html);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(expectedEmployeeNumber);
|
||||
|
||||
@@ -49,6 +49,7 @@ public sealed class BrokerOptions
|
||||
Ntlm.AdministrativePersonalProfilePath,
|
||||
Ntlm.AdministrativeLocationProfilePath,
|
||||
Ntlm.StudentProfilePath,
|
||||
Ntlm.ProfessorPayrollProfilePath,
|
||||
Ntlm.MenuProfilePath
|
||||
})
|
||||
{
|
||||
@@ -141,6 +142,9 @@ public sealed class NtlmOptions
|
||||
public string StudentProfilePath { get; init; } =
|
||||
"/psulsa/alumnos/consultainformacionalumnos/consultainformacion.aspx";
|
||||
|
||||
public string ProfessorPayrollProfilePath { get; init; } =
|
||||
"/psulsa/gadmon/nomina/consultanomina.aspx";
|
||||
|
||||
public string MenuProfilePath { get; init; } = "/psulsa/menu.aspx";
|
||||
|
||||
public int MaxProfileBytes { get; init; } = 512 * 1024;
|
||||
|
||||
@@ -344,7 +344,7 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
|
||||
profile = await TryEnrichStaffProfileAsync(
|
||||
client,
|
||||
profile!,
|
||||
identity.Role,
|
||||
identity,
|
||||
allowedHosts,
|
||||
timeout.Token,
|
||||
cancellationToken,
|
||||
@@ -413,18 +413,27 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
|
||||
private async Task<InstitutionalProfile> TryEnrichStaffProfileAsync(
|
||||
HttpClient client,
|
||||
InstitutionalProfile baseProfile,
|
||||
InstitutionalRole role,
|
||||
UserIdentity identity,
|
||||
HashSet<string> allowedHosts,
|
||||
CancellationToken timeoutToken,
|
||||
CancellationToken requestCancellationToken,
|
||||
Stopwatch elapsed)
|
||||
{
|
||||
InstitutionalProfile profile = baseProfile;
|
||||
(string Path, Func<string, InstitutionalProfile?> Parser)[] pages =
|
||||
[
|
||||
(options.AdministrativePersonalProfilePath, SguProfileParser.ParseAdministrativePersonal),
|
||||
(options.AdministrativeLocationProfilePath, SguProfileParser.ParseAdministrativeLocation)
|
||||
];
|
||||
List<(string Path, Func<string, InstitutionalProfile?> Parser)> pages = [];
|
||||
if (identity.Role == InstitutionalRole.Professor)
|
||||
{
|
||||
pages.Add((
|
||||
options.ProfessorPayrollProfilePath,
|
||||
html => SguProfileParser.ParseProfessorPayroll(html, identity.NumericId)));
|
||||
}
|
||||
|
||||
pages.Add((
|
||||
options.AdministrativePersonalProfilePath,
|
||||
SguProfileParser.ParseAdministrativePersonal));
|
||||
pages.Add((
|
||||
options.AdministrativeLocationProfilePath,
|
||||
SguProfileParser.ParseAdministrativeLocation));
|
||||
|
||||
foreach ((string path, Func<string, InstitutionalProfile?> parser) in pages)
|
||||
{
|
||||
@@ -441,7 +450,7 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
|
||||
BrokerEventIds.ProfilePageUnavailable,
|
||||
"Optional SGU profile page {Path} did not return usable HTML for role {Role}; preserving fields already collected.",
|
||||
path,
|
||||
role);
|
||||
identity.Role);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -452,7 +461,7 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
|
||||
BrokerEventIds.ProfileHtmlUnexpected,
|
||||
"Optional SGU profile page {Path} returned HTML without its supported field IDs for role {Role}; preserving fields already collected.",
|
||||
path,
|
||||
role);
|
||||
identity.Role);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -463,7 +472,7 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
|
||||
logger.LogWarning(
|
||||
BrokerEventIds.ProfileEnrichmentTimeout,
|
||||
"SGU optional staff profile enrichment for role {Role} reached its total timeout after {ElapsedMilliseconds} ms; preserving fields already collected.",
|
||||
role,
|
||||
identity.Role,
|
||||
elapsed.ElapsedMilliseconds);
|
||||
break;
|
||||
}
|
||||
@@ -473,7 +482,7 @@ public sealed class NtlmCredentialValidator : INtlmCredentialValidator
|
||||
BrokerEventIds.ProfileEnrichmentFailure,
|
||||
exception,
|
||||
"An optional SGU staff profile page for role {Role} failed after {ElapsedMilliseconds} ms; preserving fields already collected.",
|
||||
role,
|
||||
identity.Role,
|
||||
elapsed.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"AdministrativePersonalProfilePath": "/psulsa/gadmon/capitalhumano/datos/personales.aspx",
|
||||
"AdministrativeLocationProfilePath": "/psulsa/gadmon/capitalhumano/datos/ubicacion.aspx",
|
||||
"StudentProfilePath": "/psulsa/alumnos/consultainformacionalumnos/consultainformacion.aspx",
|
||||
"ProfessorPayrollProfilePath": "/psulsa/gadmon/nomina/consultanomina.aspx",
|
||||
"MenuProfilePath": "/psulsa/menu.aspx",
|
||||
"MaxProfileBytes": 524288,
|
||||
"AllowedRedirectHosts": [
|
||||
|
||||
@@ -56,6 +56,58 @@ public sealed class SguProfileParserTests
|
||||
Assert.Null(SguProfileParser.ParseAdministrative(html, "999999"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesOnlyTheSupportedProfessorPayrollHeaderFields()
|
||||
{
|
||||
const string html = """
|
||||
<div id="ctl00_contenedor_decEncabezado_pnlSinPoP">
|
||||
<span id="ctl00_contenedor_decEncabezado_lblNombre">
|
||||
013473 - ALEJANDRO LARA VILLARREAL
|
||||
</span>
|
||||
<span id="ctl00_contenedor_decEncabezado_lblIndicadorValue">
|
||||
SINDICALIZADO QUINCENAL (ACTIVO)
|
||||
</span>
|
||||
<span id="ctl00_contenedor_decEncabezado_lblCorreo">
|
||||
alejandro.lara@lasallistas.org.mx
|
||||
</span>
|
||||
<img id="ctl00_contenedor_decEncabezado_imgFoto"
|
||||
src="../admonPersonal/ashx/Fotografia.ashx?id=013473&tp=2" />
|
||||
<span id="ctl00_contenedor_decEncabezado_lblPuesto">DOCENTE</span>
|
||||
<span id="ctl00_contenedor_decEncabezado_lblDependencia"></span>
|
||||
<span id="ctl00_contenedor_decEncabezado_lblJefeNombre">NO EXTRAER</span>
|
||||
<span id="ctl00_contenedor_decEncabezado_lblJefePuesto">NO EXTRAER</span>
|
||||
</div>
|
||||
""";
|
||||
|
||||
InstitutionalProfile? profile = SguProfileParser.ParseProfessorPayroll(html, "013473");
|
||||
|
||||
Assert.NotNull(profile);
|
||||
Assert.Equal("013473", profile.EmployeeNumber);
|
||||
Assert.Equal("Alejandro Lara Villarreal", profile.DisplayName);
|
||||
Assert.Equal("alejandro.lara@lasallistas.org.mx", profile.Email);
|
||||
Assert.Equal("Sindicalizado quincenal (activo)", profile.EmployeeType);
|
||||
Assert.Equal("Docente", profile.JobTitle);
|
||||
Assert.Null(profile.Department);
|
||||
Assert.Null(profile.GivenName);
|
||||
Assert.Null(profile.Surname);
|
||||
Assert.Null(profile.StreetAddress);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RejectsProfessorPayrollMetadataForADifferentEmployeeNumber()
|
||||
{
|
||||
const string html = """
|
||||
<span id="ctl00_contenedor_decEncabezado_lblNombre">
|
||||
013473 - PERSONA INCORRECTA
|
||||
</span>
|
||||
<span id="ctl00_contenedor_decEncabezado_lblCorreo">
|
||||
incorrecta@lasallistas.org.mx
|
||||
</span>
|
||||
""";
|
||||
|
||||
Assert.Null(SguProfileParser.ParseProfessorPayroll(html, "123456"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesStructuredAdministrativeNameWithoutReadingOtherPersonalData()
|
||||
{
|
||||
|
||||
@@ -209,6 +209,15 @@ public sealed class NtlmCredentialValidatorTests
|
||||
"""
|
||||
<span id="ctl00_lblNombreUsuario">MARÍA DEL CARMEN</span>
|
||||
"""),
|
||||
Response(
|
||||
HttpStatusCode.OK,
|
||||
"""
|
||||
<span id="ctl00_contenedor_decEncabezado_lblNombre">123456 - MARÍA DEL CARMEN</span>
|
||||
<span id="ctl00_contenedor_decEncabezado_lblCorreo">docente@lasallistas.org.mx</span>
|
||||
<span id="ctl00_contenedor_decEncabezado_lblIndicadorValue">SINDICALIZADO QUINCENAL (ACTIVO)</span>
|
||||
<span id="ctl00_contenedor_decEncabezado_lblPuesto">DOCENTE</span>
|
||||
<span id="ctl00_contenedor_decEncabezado_lblDependencia"></span>
|
||||
"""),
|
||||
Response(
|
||||
HttpStatusCode.OK,
|
||||
"""
|
||||
@@ -244,6 +253,11 @@ public sealed class NtlmCredentialValidatorTests
|
||||
Assert.Equal("María del Carmen de la Fuente O'Connor", result.Profile.DisplayName);
|
||||
Assert.Equal("María del Carmen", result.Profile.GivenName);
|
||||
Assert.Equal("de la Fuente O'Connor", result.Profile.Surname);
|
||||
Assert.Equal("123456", result.Profile.EmployeeNumber);
|
||||
Assert.Equal("docente@lasallistas.org.mx", result.Profile.Email);
|
||||
Assert.Equal("Sindicalizado quincenal (activo)", result.Profile.EmployeeType);
|
||||
Assert.Equal("Docente", result.Profile.JobTitle);
|
||||
Assert.Null(result.Profile.Department);
|
||||
Assert.Equal("Calle del Sol 15\r\nFlorida", result.Profile.StreetAddress);
|
||||
Assert.Equal("Álvaro Obregón", result.Profile.City);
|
||||
Assert.Equal("Ciudad de México", result.Profile.State);
|
||||
@@ -253,6 +267,7 @@ public sealed class NtlmCredentialValidatorTests
|
||||
"/psulsa/",
|
||||
"/psulsa/",
|
||||
"/psulsa/menu.aspx",
|
||||
"/psulsa/gadmon/nomina/consultanomina.aspx",
|
||||
"/psulsa/gadmon/capitalhumano/datos/personales.aspx",
|
||||
"/psulsa/gadmon/capitalhumano/datos/ubicacion.aspx"
|
||||
],
|
||||
@@ -270,6 +285,7 @@ public sealed class NtlmCredentialValidatorTests
|
||||
"""
|
||||
<span id="ctl00_lblNombreUsuario">MIGUEL DE CERVANTES</span>
|
||||
"""),
|
||||
Response(HttpStatusCode.NotFound),
|
||||
Response(HttpStatusCode.OK, "<html><body>Unrecognized layout</body></html>"),
|
||||
Response(HttpStatusCode.NotFound));
|
||||
NtlmCredentialValidator validator = CreateValidator(handler);
|
||||
@@ -283,7 +299,10 @@ public sealed class NtlmCredentialValidatorTests
|
||||
Assert.NotNull(result.Profile);
|
||||
Assert.Equal("Miguel de Cervantes", result.Profile.DisplayName);
|
||||
Assert.Null(result.Profile.StreetAddress);
|
||||
Assert.Equal(5, handler.RequestPaths.Count);
|
||||
Assert.Equal(6, handler.RequestPaths.Count);
|
||||
Assert.Equal(
|
||||
"/psulsa/gadmon/nomina/consultanomina.aspx",
|
||||
handler.RequestPaths[3]);
|
||||
}
|
||||
|
||||
private static NtlmCredentialValidator CreateValidator(SequenceHandler handler)
|
||||
@@ -298,6 +317,7 @@ public sealed class NtlmCredentialValidatorTests
|
||||
AdministrativePersonalProfilePath = "/psulsa/gadmon/capitalhumano/datos/personales.aspx",
|
||||
AdministrativeLocationProfilePath = "/psulsa/gadmon/capitalhumano/datos/ubicacion.aspx",
|
||||
StudentProfilePath = "/psulsa/alumnos/consultainformacionalumnos/consultainformacion.aspx",
|
||||
ProfessorPayrollProfilePath = "/psulsa/gadmon/nomina/consultanomina.aspx",
|
||||
AllowedRedirectHosts = ["sgu.example"],
|
||||
TimeoutSeconds = 5,
|
||||
ProfileTimeoutSeconds = 5
|
||||
|
||||
Reference in New Issue
Block a user