Web UI: install/reconfigure addons, default-on install, visual redesign (v2.17.0)
Web UI now installs by default during first-time provisioning (fixed port 8090, no prompt) instead of being opt-in, and can install/ reconfigure CUPS Printing, LMS Server, Squeezelite Player, and Asterisk Intercom, and check for updates - the addons and Update action asked for by name. Privilege model: the web service itself still runs as $KIOSK_USER with zero ambient sudo. A new narrow, allow-listed root helper (menus/addon_webui.sh's webui_write_helper_script) is the only way it ever gains privilege, reachable only via a single-path passwordless sudo rule generated and validated with `visudo -c -f` before being installed, and it re-checks its own fixed action allow-list before dispatching anything. Each allow-listed action is the exact same interactive action_* function the terminal menu already uses, driven by piping the right answers on stdin - the same technique this project's own bash tests already use, so no prompt/mutation refactor of any addon file was needed. webui/lib/actions.js's stdin sequences were cross-validated against the real bash functions (not just read), which caught two real bugs (Squeezelite and Asterisk Intercom both silently lost their "decline reconfigure" path). Long-running installs stream live output via Server-Sent Events (webui/lib/jobs.js), one action at a time. Full visual redesign: a sidebar shell (Sites/Display/Lockout/Addons/ Update) replacing the single scrolling page, light+dark themes via prefers-color-scheme, no external font/CDN dependency. Actually driving the redesigned UI in a headless browser (not just reading the code) caught a real bug: refreshing an addon's pill/button after a successful install used to rebuild the whole card, racing (and usually losing to) the success status/log that job had just written. Fixed to update pill/buttons in place. Uninstall-via-web is deliberately still not offered, for any addon. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VfsFSoRqfbRG7XAg5RoE7e
This commit is contained in:
+158
-14
@@ -3,24 +3,48 @@
|
||||
# menus/addon_webui.sh - "Web UI" addon: installs webui/ (a small Node/
|
||||
# Express app) as a systemd service running as $KIOSK_USER, giving a
|
||||
# browser-based editor for Sites & Page Timing, Display & Interaction,
|
||||
# and Password Protection & Lockout - the three Core Settings menus that
|
||||
# are pure config.json read/write with no privileged system mutation
|
||||
# involved. The remaining Core Settings/Addons/Advanced menus (WiFi,
|
||||
# Timezone, Power/Display/Quiet Hours, Complete Uninstall, every other
|
||||
# addon, everything in Advanced) stay terminal-only for now - a
|
||||
# network-facing process shouldn't be handed sudo-level system mutation
|
||||
# without a lot more thought than this first pass gives it.
|
||||
# and Password Protection & Lockout - config.json read/write only, no
|
||||
# sudo needed, since that file is already owned by $KIOSK_USER.
|
||||
#
|
||||
# Installed by default during first-time provisioning now
|
||||
# (lib/provision.sh's provision_configure_webui, which calls the same
|
||||
# install helpers this file defines) - this menu remains here for
|
||||
# reconfiguring the port, restarting the service, or reinstalling it on
|
||||
# a kiosk provisioned before this change.
|
||||
#
|
||||
# Also lets the web UI trigger a fixed, vetted set of privileged actions
|
||||
# - install/reconfigure CUPS/LMS/Squeezelite/Asterisk Intercom, and
|
||||
# Update - through a narrow, allow-listed root helper
|
||||
# (webui_write_helper_script below), rather than by giving the service
|
||||
# itself any elevated privilege. The helper is reachable only via a
|
||||
# single-path passwordless sudo rule (webui_write_sudoers_file) and
|
||||
# re-checks its own fixed action allow-list before dispatching anything,
|
||||
# even though the sudoers rule alone already restricts which script can
|
||||
# run - defense in depth. Each allow-listed action is the exact same
|
||||
# interactive `action_*` function the terminal menu already uses,
|
||||
# driven by piping the right answers on stdin, the same technique this
|
||||
# project's own bash tests already use (see webui/lib/actions.js and
|
||||
# webui/test/*.test.js). No prompt/mutation refactor of any addon file
|
||||
# was needed for this to work. Everything else (WiFi, Timezone, Power/
|
||||
# Display/Quiet Hours, Complete Uninstall, Remote Access, Authelia,
|
||||
# Factory Reset, Virtual Consoles, Emergency Hotspot, Clone Settings)
|
||||
# stays terminal-only for now.
|
||||
#
|
||||
# No login of its own, by design: this addon assumes it'll be put behind
|
||||
# a reverse proxy (e.g. Caddy) with Authelia forward-auth in front, the
|
||||
# same way other self-hosted apps get protected - Authelia integration
|
||||
# is explicitly out of scope for this repo (Authelia runs elsewhere).
|
||||
# Direct LAN access with no proxy in front has no authentication at all -
|
||||
# treat it the same as SSH access to this kiosk.
|
||||
# treat it the same as SSH access to this kiosk, which is also the
|
||||
# access level the privileged helper effectively grants if reached
|
||||
# without a proxy in front: bounded to its fixed action list, not a
|
||||
# root shell, but real system mutation all the same.
|
||||
#
|
||||
# webui/'s own app-level logic (config.json schema/merge, API validation)
|
||||
# lives and is tested entirely under webui/ - this file only wires it up
|
||||
# as a system service and never touches config.json itself.
|
||||
# webui/'s own app-level logic (config.json schema/merge, API
|
||||
# validation, the action allow-list/stdin synthesis, the job/SSE
|
||||
# system) lives and is tested entirely under webui/ - this file only
|
||||
# wires it up as a system service plus the privileged helper, and never
|
||||
# touches config.json itself.
|
||||
#
|
||||
# Depends on: lib/menu.sh, lib/config.sh being sourced first.
|
||||
################################################################################
|
||||
@@ -102,9 +126,122 @@ webui_write_env_file() {
|
||||
PORT=${port}
|
||||
BIND_ADDR=0.0.0.0
|
||||
CONFIG_PATH=${CONFIG_PATH}
|
||||
HELPER_PATH=${WEBUI_HELPER_PATH}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Writes the one root-owned script the Web UI is ever allowed to reach
|
||||
# via sudo (see the sudoers rule webui_write_sudoers_file writes right
|
||||
# after this). Sources the exact same files install.sh does - full path
|
||||
# list baked in at generation time from $SCRIPT_DIR, so the generated
|
||||
# script has no runtime dependency on where it happens to be invoked
|
||||
# from. The fixed ALLOWED_ACTIONS allow-list inside the script itself is
|
||||
# the real gate (checked in addition to, not instead of, the sudoers
|
||||
# rule only permitting this one script path) - a request that reaches
|
||||
# this script can only ever trigger one of these exact, already-tested
|
||||
# interactive `action_*` functions, driven the same way this project's
|
||||
# own bash tests already drive them: real answers piped in on stdin, in
|
||||
# the exact order the function's own prompts expect them. No prompt/
|
||||
# mutation refactor of any addon file needed for this to work.
|
||||
webui_write_helper_script() {
|
||||
sudo mkdir -p "$(dirname "$WEBUI_HELPER_PATH")"
|
||||
sudo tee "$WEBUI_HELPER_PATH" > /dev/null <<EOF
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# lib/provision.sh reads \$SCRIPT_DIR directly at source time (no
|
||||
# fallback default, unlike the lib/config.sh path vars) - must be a real
|
||||
# exported variable here, not just used to interpolate the source paths
|
||||
# below, or it's unbound under set -u.
|
||||
export SCRIPT_DIR="$SCRIPT_DIR"
|
||||
|
||||
source "$SCRIPT_DIR/lib/menu.sh"
|
||||
source "$SCRIPT_DIR/lib/config.sh"
|
||||
source "$SCRIPT_DIR/lib/electron.sh"
|
||||
source "$SCRIPT_DIR/menus/sites.sh"
|
||||
source "$SCRIPT_DIR/menus/display.sh"
|
||||
source "$SCRIPT_DIR/menus/timezone.sh"
|
||||
source "$SCRIPT_DIR/menus/hidden_pin.sh"
|
||||
source "$SCRIPT_DIR/menus/lockout.sh"
|
||||
source "$SCRIPT_DIR/menus/wifi.sh"
|
||||
source "$SCRIPT_DIR/menus/power_schedule.sh"
|
||||
source "$SCRIPT_DIR/menus/diagnostics.sh"
|
||||
source "$SCRIPT_DIR/menus/addon_cups.sh"
|
||||
source "$SCRIPT_DIR/menus/addon_authelia.sh"
|
||||
source "$SCRIPT_DIR/menus/addon_remote_access.sh"
|
||||
source "$SCRIPT_DIR/menus/addon_lms_squeezelite.sh"
|
||||
source "$SCRIPT_DIR/menus/addon_asterisk_intercom.sh"
|
||||
source "$SCRIPT_DIR/menus/addon_webui.sh"
|
||||
source "$SCRIPT_DIR/menus/advanced_electron.sh"
|
||||
source "$SCRIPT_DIR/menus/advanced_upgrade.sh"
|
||||
source "$SCRIPT_DIR/menus/advanced_factory_reset.sh"
|
||||
source "$SCRIPT_DIR/menus/advanced_virtual_consoles.sh"
|
||||
source "$SCRIPT_DIR/menus/advanced_emergency_hotspot.sh"
|
||||
source "$SCRIPT_DIR/menus/complete_uninstall.sh"
|
||||
source "$SCRIPT_DIR/menus/clone_settings.sh"
|
||||
source "$SCRIPT_DIR/lib/provision.sh"
|
||||
|
||||
# Read-only status wrappers, not addon logic of their own - just a
|
||||
# single fixed-shape JSON line so the web UI can show "Install" vs.
|
||||
# "Reconfigure" per addon in one round trip. None of the underlying
|
||||
# checks (dpkg query, systemctl is-active/is-enabled, a file-existence
|
||||
# test under \$KIOSK_HOME) need root, but this script is still only
|
||||
# reachable via the same sudo-gated path as everything else here - one
|
||||
# access path is simpler to reason about than two, and the extra sudo
|
||||
# call for a read-only check is negligible.
|
||||
status_all() {
|
||||
echo "{\"cups\":\$(cups_is_installed && echo true || echo false),\"lms\":\$(lms_is_installed && echo true || echo false),\"squeezelite\":\$(squeezelite_is_installed && echo true || echo false),\"asterisk_intercom\":\$(baresip_is_installed && echo true || echo false)}"
|
||||
}
|
||||
|
||||
ALLOWED_ACTIONS=(
|
||||
action_install_cups
|
||||
action_reconfigure_cups
|
||||
action_install_lms
|
||||
action_install_squeezelite
|
||||
action_configure_asterisk_intercom
|
||||
action_upgrade
|
||||
status_all
|
||||
)
|
||||
|
||||
action="\${1:-}"
|
||||
allowed=false
|
||||
for a in "\${ALLOWED_ACTIONS[@]}"; do
|
||||
[[ "\$a" == "\$action" ]] && allowed=true && break
|
||||
done
|
||||
|
||||
if ! \$allowed; then
|
||||
echo "kiosk-webui-helper: action not permitted: \$action" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"\$action"
|
||||
EOF
|
||||
sudo chown root:root "$WEBUI_HELPER_PATH"
|
||||
sudo chmod 750 "$WEBUI_HELPER_PATH"
|
||||
}
|
||||
|
||||
# Grants $KIOSK_USER passwordless sudo on exactly this one script path -
|
||||
# no argument wildcarding at the sudoers level, since the script's own
|
||||
# ALLOWED_ACTIONS check above is the real gate. Validated with
|
||||
# `visudo -c -f` on a temp file before it's moved into place: a
|
||||
# malformed sudoers snippet can break sudo system-wide, so this step is
|
||||
# never skipped.
|
||||
webui_write_sudoers_file() {
|
||||
local tmp
|
||||
tmp=$(mktemp)
|
||||
echo "${KIOSK_USER} ALL=(root) NOPASSWD: ${WEBUI_HELPER_PATH}" > "$tmp"
|
||||
|
||||
if ! sudo visudo -c -f "$tmp" &>/dev/null; then
|
||||
log_error "Generated sudoers rule failed validation - not installed"
|
||||
rm -f "$tmp"
|
||||
return 1
|
||||
fi
|
||||
|
||||
sudo mkdir -p "$SUDOERS_D_DIR"
|
||||
sudo install -m 0440 -o root -g root "$tmp" "$SUDOERS_D_DIR/kiosk-webui"
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
webui_write_unit_file() {
|
||||
sudo tee "$SYSTEMD_DIR/kiosk-webui.service" > /dev/null <<EOF
|
||||
[Unit]
|
||||
@@ -133,9 +270,10 @@ action_webui_install() {
|
||||
echo
|
||||
echo "This installs a small web app for editing Sites & Page Timing,"
|
||||
echo "Display & Interaction, and Password Protection & Lockout from a"
|
||||
echo "browser. It has no login of its own - put it behind your own"
|
||||
echo "reverse proxy (e.g. Caddy + Authelia) if it needs to be reachable"
|
||||
echo "beyond a trusted LAN."
|
||||
echo "browser - plus installing/reconfiguring CUPS, LMS/Squeezelite,"
|
||||
echo "and Asterisk Intercom, and checking for updates. It has no login"
|
||||
echo "of its own - put it behind your own reverse proxy (e.g. Caddy +"
|
||||
echo "Authelia) if it needs to be reachable beyond a trusted LAN."
|
||||
echo
|
||||
ask_yes_no "Install the Web UI?" "n" || { echo "Cancelled"; return; }
|
||||
|
||||
@@ -150,6 +288,11 @@ action_webui_install() {
|
||||
|
||||
webui_write_env_file "$port"
|
||||
webui_write_unit_file
|
||||
webui_write_helper_script
|
||||
if ! webui_write_sudoers_file; then
|
||||
log_error "Web UI install failed - could not grant the addon-install/Update helper permission"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if enable_and_start_units kiosk-webui; then
|
||||
sudo ufw allow "${port}/tcp" comment 'Kiosk Web UI' 2>/dev/null || true
|
||||
@@ -204,6 +347,7 @@ webui_do_uninstall() {
|
||||
sudo systemctl disable kiosk-webui 2>/dev/null || true
|
||||
sudo rm -f "$SYSTEMD_DIR/kiosk-webui.service"
|
||||
sudo systemctl daemon-reload 2>/dev/null || true
|
||||
sudo rm -f "$SUDOERS_D_DIR/kiosk-webui" "$WEBUI_HELPER_PATH"
|
||||
sudo rm -rf "$WEBUI_DIR" "$WEBUI_ENV_DIR"
|
||||
log_success "Web UI removed"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user