Files
Classroom_Desktop/tools/validate-trixie-release.sh
T
ekklesiaandClaude Opus 5 bf7b959893 feat: the room may use the church Wi-Fi and may not read its password
These are shared computers in rooms full of people, and in a stock Plasma
desktop "use the network" and "read the network's password" are much
closer together than they look.

Measured on Debian 13 with NetworkManager 1.52 and Plasma 6.3:

  * org.freedesktop.NetworkManager.settings.modify.own ships as "yes" for
    an active session — no prompt, no administrator. It covers any
    connection whose connection.permissions names that user, and a Wi-Fi
    network added from the desktop is created exactly that way. So a
    password a teacher types in is readable by the next person to sit
    down.
  * the applet offers "Show Network's QR Code" on every saved wireless
    connection, and the QR code contains the pre-shared key.
  * that action, and the "Configure…" beside it, are contextual actions in
    ConnectionItem.qml and do NOT consult
    authorizeControlModule("kcm_networkmanagement"). The Kiosk key gates
    the toolbar button only. Hiding buttons was never going to be the
    answer.

Every one of those paths ends at NetworkManager's GetSecrets, so that is
where this is answered. A polkit rule refuses settings.modify.own,
settings.modify.system, both wifi.share actions and the machine-wide
settings, for the classroom user by name.

Deliberately NOT refused: network-control, wifi.scan and the
enable/disable actions. None returns a secret, and they are what
connecting and seeing a network are made of. Automatic reconnection does
not go through polkit at all — NetworkManager does it as root — so nothing
here can stop the room coming back on the network after a reboot, which is
the property that matters most on a Sunday.

An administrator's account and root are untouched, and the override route
is the same one the sleep rule has.

classroom-wifi is the other half, and it is a requirement rather than a
convenience: it provisions the church Wi-Fi as a SYSTEM connection, with
no connection.permissions, psk-flags=0 and a 0600 root:root file. A
connection the machine owns is one nobody in the room can read. Verified
against real NetworkManager in the lab — the classroom account gets
"Permission denied" on the file, "--" from nmcli and "Insufficient
privileges" from GetSecrets, while autoconnect stays on.

There is deliberately no teacher-facing join dialog. Anything that lets
the room add a connection lets the room own it, and an owned connection is
a readable one; docs/networking.md says so rather than leaving it to be
rediscovered.

The rule is plain JavaScript, so the test evaluates it with polkit's
objects stubbed and asserts both halves: which actions come back NO, and
which are left alone. What it cannot test is the last mile — polkit
resolves a subject through logind and a container has no graphical seat
session, so every check there is denied for reasons unrelated to this
rule. That one is a physical check, and it is written down as one.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-20 21:04:07 -05:00

227 lines
8.7 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
if [[ $# -lt 1 || $# -gt 2 ]]; then
echo "Usage: $0 CANDIDATE.deb [PREVIOUS.deb]" >&2
exit 2
fi
candidate="$(realpath "$1")"
previous=""
[[ -f "$candidate" ]] || { echo "Package not found: $candidate" >&2; exit 1; }
if [[ $# -eq 2 ]]; then
previous="$(realpath "$2")"
[[ -f "$previous" ]] || { echo "Package not found: $previous" >&2; exit 1; }
fi
echo "Validating $(basename "$candidate") on Debian Trixie"
docker_args=(
run --rm -i
-v "$candidate:/packages/candidate.deb:ro"
)
if [[ -n "$previous" ]]; then
docker_args+=( -v "$previous:/packages/previous.deb:ro" )
fi
timeout --signal=TERM --kill-after=15s 720s \
docker "${docker_args[@]}" debian:trixie-slim \
bash -s -- "${previous:+/packages/previous.deb}" <<'CONTAINER'
set -Eeuo pipefail
validation_failure() {
status=$?
echo "Validation failed at line ${BASH_LINENO[0]}: ${BASH_COMMAND}" >&2
if command -v lpstat >/dev/null 2>&1; then
lpstat -t >&2 || true
lpoptions -p Elmer -l >&2 || true
fi
[[ ! -f /tmp/provision-first.log ]] || cat /tmp/provision-first.log >&2
[[ ! -f /tmp/provision-second.log ]] || cat /tmp/provision-second.log >&2
exit "$status"
}
trap validation_failure ERR
export DEBIAN_FRONTEND=noninteractive
# Package validation runs in a container with no init system. Do not let
# maintainer scripts try to start CUPS, Avahi or other irrelevant daemons;
# services are inspected and CUPS is started explicitly when the test needs it.
cat > /usr/sbin/policy-rc.d <<'POLICY'
#!/bin/sh
exit 101
POLICY
chmod 0755 /usr/sbin/policy-rc.d
run_logged() {
label="$1"
seconds="$2"
shift 2
if ! timeout --signal=TERM --kill-after=10s "${seconds}s" "$@" \
>"/tmp/${label}.log" 2>&1; then
cat "/tmp/${label}.log" >&2
echo "${label} exceeded ${seconds}s or failed" >&2
exit 1
fi
}
run_logged apt-update 120 apt-get update -qq
previous="${1:-}"
if [[ -n "$previous" ]]; then
run_logged previous-install 360 apt-get install -y -qq "$previous"
cat > /etc/ekklesia-classroom/device.conf <<'CONFIG'
# Configuration carried forward from 0.1.36
UPDATE_URL=https://updates.riverofglorymn.org
CHANNEL=canary
DVD_ENABLED=false
UI_SCALE=1.25
MQTT_ENABLED=true
MQTT_HOST=broker.example.test
MQTT_PORT=1884
MQTT_USERNAME=classroom
MQTT_PASSWORD=preserve-me
CUSTOM_SETTING=preserve-me-too
CONFIG
fi
run_logged candidate-install 360 apt-get install -y -qq /packages/candidate.deb
command -v nmtui >/dev/null
command -v lpadmin >/dev/null
command -v lpinfo >/dev/null
systemctl is-enabled NetworkManager.service >/dev/null
systemctl is-enabled cups.service >/dev/null
! systemctl is-enabled NetworkManager-wait-online.service >/dev/null 2>&1
# The audio policy reaches a session, and is ordered after the server it
# actually talks to. Ordering it only after pipewire and wireplumber is
# what let it race PipeWire's PulseAudio server at boot, fail its one
# startup attempt and then sit idle — the rooms were silent on 0.2.2 for
# exactly this reason, so the ordering is asserted rather than assumed.
audio_unit=/etc/systemd/user/ekklesia-classroom-audio.service
[[ -f "$audio_unit" ]]
grep -q 'pipewire-pulse' "$audio_unit"
[[ -L /etc/systemd/user/plasma-workspace.target.wants/ekklesia-classroom-audio.service ]]
command -v classroom-audio >/dev/null
command -v pactl >/dev/null
# The classroom account cannot be shown a saved Wi-Fi password. The rule
# has to be installed, owned by root — polkit silently ignores a rules
# file that is not — and it has to refuse the action Debian ships as
# "yes", which is the one a teacher would actually reach.
network_rules=/usr/share/polkit-1/rules.d/49-ekklesia-classroom-network.rules
[[ -f "$network_rules" ]]
[[ "$(stat -c %U:%G "$network_rules")" == "root:root" ]]
grep -q "settings.modify.own" "$network_rules"
grep -q "wifi.share.protected" "$network_rules"
# And it must NOT refuse the actions that keep the room on the network.
! grep -qE '"org\.freedesktop\.NetworkManager\.network-control"' "$network_rules"
grep -q "kcm_networkmanagement=false" /etc/ekklesia-classroom/xdg/kdeglobals
command -v classroom-wifi >/dev/null
# The policy module and the daemon agree on what they import, on a machine
# rather than in a checkout.
PYTHONPATH=/usr/local/lib/ekklesia-classroom python3 -c \
'import classroom_audio; classroom_audio.settled([], "auto", None)'
grep -q '^managed=false$' /etc/NetworkManager/NetworkManager.conf
# A normal Docker container has Ethernet and no Wi-Fi adapter. Reaching this
# point proves package configuration did not require Wi-Fi hardware.
! find /sys/class/net -mindepth 2 -maxdepth 2 -type d -name wireless | grep -q .
/usr/sbin/cupsd
sleep 1
driver='foomatic-db-compressed-ppds:0/ppd/foomatic-ppd/Generic-PCL_6_PCL_XL_Printer-pxlcolor.ppd'
lpinfo -m | grep -q "^${driver} "
# Simulate the queue an older release may have left behind, then run the real
# boot provisioning path. Chrome is stubbed as already installed and DVD is
# disabled so the test remains entirely local and deterministic.
lpadmin -p Classroom_Color -v socket://192.0.2.10:9100 -m "$driver"
install -m755 /bin/true /usr/local/bin/google-chrome
timeout --signal=TERM --kill-after=5s 45s classroom-provision \
>/tmp/provision-first.log
! lpstat -p Classroom_Color >/dev/null 2>&1
lpstat -p Elmer -d -v | grep -q 'socket://10.10.0.155:9100'
lpstat -l -p Elmer | grep -q 'Description: Elmer'
lpoptions -p Elmer -l | grep -Eq '^PrintoutMode/.*:.*\*Normal([[:space:]]|$)'
lpoptions -p Elmer -l | grep -Eq '^ColorModel/.*:.*\*Color([[:space:]]|$)'
[[ "$(lpstat -e)" == "Elmer" ]]
[[ "$(systemctl is-enabled avahi-daemon.service 2>/dev/null || true)" == "masked" ]]
[[ "$(systemctl is-enabled avahi-daemon.socket 2>/dev/null || true)" == "masked" ]]
timeout --signal=TERM --kill-after=5s 45s classroom-provision \
>/tmp/provision-second.log
grep -q 'Elmer is already correctly configured' /tmp/provision-second.log
if [[ -n "$previous" ]]; then
# The room's own values, untouched by the upgrade.
grep -q '^DVD_ENABLED=false$' /etc/ekklesia-classroom/device.conf
grep -q '^UI_SCALE=1.25$' /etc/ekklesia-classroom/device.conf
grep -q '^MQTT_PASSWORD=preserve-me$' /etc/ekklesia-classroom/device.conf
grep -q '^CUSTOM_SETTING=preserve-me-too$' /etc/ekklesia-classroom/device.conf
# The channel a room was put on deliberately survives the upgrade.
# canary is where the next development release goes, and the Plasma
# classroom is the stable line now, so nothing here has to move.
grep -q '^CHANNEL=canary$' /etc/ekklesia-classroom/device.conf
# And a room that was carried through the experiment on its own
# channel comes home, because that channel is the obsolete one now.
sed -i 's/^CHANNEL=.*/CHANNEL=classroom-v2/' /etc/ekklesia-classroom/device.conf
classroom-migrate >/tmp/migrate-from-v2.log 2>&1
grep -q '^CHANNEL=stable$' /etc/ekklesia-classroom/device.conf
grep -q '^MQTT_PASSWORD=preserve-me$' /etc/ekklesia-classroom/device.conf
grep -q '^CUSTOM_SETTING=preserve-me-too$' /etc/ekklesia-classroom/device.conf
sed -i 's/^CHANNEL=.*/CHANNEL=canary/' /etc/ekklesia-classroom/device.conf
# Every setting this release has that the room predates, appended
# exactly once. classroom-migrate hands device.conf to the shared
# helper after its own work for this, so that a machine converges
# whether it got here through this postinst or through somebody
# running the command by hand.
for setting in LIVE_ENABLED AUDIO_OUTPUT UI_SCALE; do
[[ "$(grep -c "^${setting}=" /etc/ekklesia-classroom/device.conf)" -eq 1 ]]
done
run_logged candidate-reinstall 360 \
apt-get install --reinstall -y -qq /packages/candidate.deb
# An install that changes nothing must change nothing.
for setting in LIVE_ENABLED AUDIO_OUTPUT UI_SCALE; do
[[ "$(grep -c "^${setting}=" /etc/ekklesia-classroom/device.conf)" -eq 1 ]]
done
grep -q '^MQTT_PASSWORD=preserve-me$' /etc/ekklesia-classroom/device.conf
grep -q '^CUSTOM_SETTING=preserve-me-too$' /etc/ekklesia-classroom/device.conf
grep -q '^CHANNEL=canary$' /etc/ekklesia-classroom/device.conf
# And by hand, on a machine that is already converged: the command a
# technician runs must be safe to run twice.
classroom-migrate >/tmp/migrate-by-hand.log 2>&1
for setting in LIVE_ENABLED AUDIO_OUTPUT UI_SCALE; do
[[ "$(grep -c "^${setting}=" /etc/ekklesia-classroom/device.conf)" -eq 1 ]]
done
grep -q '^MQTT_PASSWORD=preserve-me$' /etc/ekklesia-classroom/device.conf
! grep -q 'preserve-me' /tmp/migrate-by-hand.log
fi
echo "Trixie package, upgrade, bounded install, networking, Elmer printing and reinstall checks passed."
CONTAINER