Improve domain enrollment and desktop personalization

This commit is contained in:
2026-09-07 15:07:04 -06:00
parent c737bd3192
commit 9f32ed2cb1
30 changed files with 1259 additions and 51 deletions
+3 -1
View File
@@ -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.
+20 -10
View File
@@ -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
+4 -3
View File
@@ -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:
+3 -2
View File
@@ -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
+5
View File
@@ -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
+29 -3
View File
@@ -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
View File
@@ -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
+62
View File
@@ -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.
+6
View File
@@ -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:
+3
View File
@@ -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
+226 -3
View File
@@ -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
+1 -7
View File
@@ -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
+27
View File
@@ -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)
}
+31
View File
@@ -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
+16 -2
View File
@@ -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
}
+14 -1
View File
@@ -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
}
+445
View File
@@ -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
}
+249
View File
@@ -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);
}
}
+1
View File
@@ -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&amp;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