diff --git a/README.md b/README.md index 39b3a95..6ea6eb5 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/assets/branding/darkblue.jpg b/assets/branding/darkblue.jpg new file mode 100644 index 0000000..eaceb19 Binary files /dev/null and b/assets/branding/darkblue.jpg differ diff --git a/assets/branding/fonts/IndivisaTextSans-Bold.otf b/assets/branding/fonts/IndivisaTextSans-Bold.otf new file mode 100644 index 0000000..d458060 Binary files /dev/null and b/assets/branding/fonts/IndivisaTextSans-Bold.otf differ diff --git a/assets/branding/fonts/IndivisaTextSans-BoldItalic.otf b/assets/branding/fonts/IndivisaTextSans-BoldItalic.otf new file mode 100644 index 0000000..476b435 Binary files /dev/null and b/assets/branding/fonts/IndivisaTextSans-BoldItalic.otf differ diff --git a/assets/branding/fonts/IndivisaTextSans-Regular.otf b/assets/branding/fonts/IndivisaTextSans-Regular.otf new file mode 100644 index 0000000..ad3f2a2 Binary files /dev/null and b/assets/branding/fonts/IndivisaTextSans-Regular.otf differ diff --git a/assets/branding/fonts/IndivisaTextSerif-BoldItalic.otf b/assets/branding/fonts/IndivisaTextSerif-BoldItalic.otf new file mode 100644 index 0000000..0862360 Binary files /dev/null and b/assets/branding/fonts/IndivisaTextSerif-BoldItalic.otf differ diff --git a/assets/branding/fonts/IndivisaTextSerif-Regular.otf b/assets/branding/fonts/IndivisaTextSerif-Regular.otf new file mode 100644 index 0000000..ce42b8c Binary files /dev/null and b/assets/branding/fonts/IndivisaTextSerif-Regular.otf differ diff --git a/docs/architecture.md b/docs/architecture.md index 78d50ef..bdf9174 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 diff --git a/docs/bootstrap-recovery.md b/docs/bootstrap-recovery.md index 145f0aa..d330e9c 100644 --- a/docs/bootstrap-recovery.md +++ b/docs/bootstrap-recovery.md @@ -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: diff --git a/docs/broker-operations.md b/docs/broker-operations.md index b86106d..8597bd2 100644 --- a/docs/broker-operations.md +++ b/docs/broker-operations.md @@ -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 diff --git a/docs/client-enrollment.md b/docs/client-enrollment.md index c782b88..05a707b 100644 --- a/docs/client-enrollment.md +++ b/docs/client-enrollment.md @@ -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 diff --git a/docs/linux-client-enrollment.md b/docs/linux-client-enrollment.md index b9f6ac7..857486f 100644 --- a/docs/linux-client-enrollment.md +++ b/docs/linux-client-enrollment.md @@ -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). diff --git a/docs/security.md b/docs/security.md index 32778c0..ee6153d 100644 --- a/docs/security.md +++ b/docs/security.md @@ -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 diff --git a/docs/welcome-wallpaper.md b/docs/welcome-wallpaper.md new file mode 100644 index 0000000..83893e0 --- /dev/null +++ b/docs/welcome-wallpaper.md @@ -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. diff --git a/docs/windows-client-onboarding.md b/docs/windows-client-onboarding.md index e781e02..b432b05 100644 --- a/docs/windows-client-onboarding.md +++ b/docs/windows-client-onboarding.md @@ -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: y PowerShell Remoting en: diff --git a/scripts/Deploy-AuthBroker.ps1 b/scripts/Deploy-AuthBroker.ps1 index 9cb99b1..b462352 100644 --- a/scripts/Deploy-AuthBroker.ps1 +++ b/scripts/Deploy-AuthBroker.ps1 @@ -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 diff --git a/scripts/Enroll-SguLinuxDomainClient.sh b/scripts/Enroll-SguLinuxDomainClient.sh index b8a169d..53c9ce9 100755 --- a/scripts/Enroll-SguLinuxDomainClient.sh +++ b/scripts/Enroll-SguLinuxDomainClient.sh @@ -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 diff --git a/scripts/Initialize-SguDomainController.ps1 b/scripts/Initialize-SguDomainController.ps1 index bdb3ed0..4a3c296 100644 --- a/scripts/Initialize-SguDomainController.ps1 +++ b/scripts/Initialize-SguDomainController.ps1 @@ -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 diff --git a/scripts/Install-CredentialProvider.ps1 b/scripts/Install-CredentialProvider.ps1 index 39ab8b2..c483e72 100644 --- a/scripts/Install-CredentialProvider.ps1 +++ b/scripts/Install-CredentialProvider.ps1 @@ -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) } diff --git a/scripts/New-SguBootstrapPackages.ps1 b/scripts/New-SguBootstrapPackages.ps1 index b27b994..cea3db4 100644 --- a/scripts/New-SguBootstrapPackages.ps1 +++ b/scripts/New-SguBootstrapPackages.ps1 @@ -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 diff --git a/scripts/Set-SguDomainComputerPolicies.ps1 b/scripts/Set-SguDomainComputerPolicies.ps1 index 0887cf4..ecddc53 100644 --- a/scripts/Set-SguDomainComputerPolicies.ps1 +++ b/scripts/Set-SguDomainComputerPolicies.ps1 @@ -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 } diff --git a/scripts/Set-SguDomainUserPolicies.ps1 b/scripts/Set-SguDomainUserPolicies.ps1 index 9b3dbc0..0285879 100644 --- a/scripts/Set-SguDomainUserPolicies.ps1 +++ b/scripts/Set-SguDomainUserPolicies.ps1 @@ -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 } diff --git a/scripts/Set-SguWelcomeWallpaper.ps1 b/scripts/Set-SguWelcomeWallpaper.ps1 new file mode 100644 index 0000000..700774b --- /dev/null +++ b/scripts/Set-SguWelcomeWallpaper.ps1 @@ -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, '(?/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 diff --git a/src/SGU.AuthBroker.Core/Profiles/SguProfileParser.cs b/src/SGU.AuthBroker.Core/Profiles/SguProfileParser.cs index fe2c22a..fcab71f 100644 --- a/src/SGU.AuthBroker.Core/Profiles/SguProfileParser.cs +++ b/src/SGU.AuthBroker.Core/Profiles/SguProfileParser.cs @@ -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); diff --git a/src/SGU.AuthBroker/Options/BrokerOptions.cs b/src/SGU.AuthBroker/Options/BrokerOptions.cs index 241c599..fa54e6b 100644 --- a/src/SGU.AuthBroker/Options/BrokerOptions.cs +++ b/src/SGU.AuthBroker/Options/BrokerOptions.cs @@ -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; diff --git a/src/SGU.AuthBroker/Services/NtlmCredentialValidator.cs b/src/SGU.AuthBroker/Services/NtlmCredentialValidator.cs index 7191ce5..a8d79bb 100644 --- a/src/SGU.AuthBroker/Services/NtlmCredentialValidator.cs +++ b/src/SGU.AuthBroker/Services/NtlmCredentialValidator.cs @@ -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 TryEnrichStaffProfileAsync( HttpClient client, InstitutionalProfile baseProfile, - InstitutionalRole role, + UserIdentity identity, HashSet allowedHosts, CancellationToken timeoutToken, CancellationToken requestCancellationToken, Stopwatch elapsed) { InstitutionalProfile profile = baseProfile; - (string Path, Func Parser)[] pages = - [ - (options.AdministrativePersonalProfilePath, SguProfileParser.ParseAdministrativePersonal), - (options.AdministrativeLocationProfilePath, SguProfileParser.ParseAdministrativeLocation) - ]; + List<(string Path, Func 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 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); } } diff --git a/src/SGU.AuthBroker/appsettings.json b/src/SGU.AuthBroker/appsettings.json index 6f9d857..a1c52c3 100644 --- a/src/SGU.AuthBroker/appsettings.json +++ b/src/SGU.AuthBroker/appsettings.json @@ -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": [ diff --git a/tests/SGU.AuthBroker.Core.Tests/SguProfileParserTests.cs b/tests/SGU.AuthBroker.Core.Tests/SguProfileParserTests.cs index 2c61217..cd2225d 100644 --- a/tests/SGU.AuthBroker.Core.Tests/SguProfileParserTests.cs +++ b/tests/SGU.AuthBroker.Core.Tests/SguProfileParserTests.cs @@ -56,6 +56,58 @@ public sealed class SguProfileParserTests Assert.Null(SguProfileParser.ParseAdministrative(html, "999999")); } + [Fact] + public void ParsesOnlyTheSupportedProfessorPayrollHeaderFields() + { + const string html = """ +
+ + 013473 - ALEJANDRO LARA VILLARREAL + + + SINDICALIZADO QUINCENAL (ACTIVO) + + + alejandro.lara@lasallistas.org.mx + + + DOCENTE + + NO EXTRAER + NO EXTRAER +
+ """; + + 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 = """ + + 013473 - PERSONA INCORRECTA + + + incorrecta@lasallistas.org.mx + + """; + + Assert.Null(SguProfileParser.ParseProfessorPayroll(html, "123456")); + } + [Fact] public void ParsesStructuredAdministrativeNameWithoutReadingOtherPersonalData() { diff --git a/tests/SGU.AuthBroker.Tests/NtlmCredentialValidatorTests.cs b/tests/SGU.AuthBroker.Tests/NtlmCredentialValidatorTests.cs index a36caee..b7021a0 100644 --- a/tests/SGU.AuthBroker.Tests/NtlmCredentialValidatorTests.cs +++ b/tests/SGU.AuthBroker.Tests/NtlmCredentialValidatorTests.cs @@ -209,6 +209,15 @@ public sealed class NtlmCredentialValidatorTests """ MARÍA DEL CARMEN """), + Response( + HttpStatusCode.OK, + """ + 123456 - MARÍA DEL CARMEN + docente@lasallistas.org.mx + SINDICALIZADO QUINCENAL (ACTIVO) + DOCENTE + + """), 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 """ MIGUEL DE CERVANTES """), + Response(HttpStatusCode.NotFound), Response(HttpStatusCode.OK, "Unrecognized layout"), 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