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]>
227 lines
8.7 KiB
Bash
Executable File
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
|