The kernel memory killer's "Memory Shortage Avoided" notice carried a Configure button that ran kcmshell6 and opened KDE's whole Notifications settings under the teacher's account. Reproduced in the lab. Plasma draws that button without consulting the Kiosk policy, but kcmshell6 and System Settings do: every settings module Debian 13's Plasma 6.3 installs (90) is now refused in the classroom's immutable kdeglobals, so the same click shows only "disabled by the system administrator". The clock's Configure Digital Clock goes with plasma/allow_configure_when_locked=false; Configure Audio Devices disappears on its own; the disabled window's Help button opened KDE Help Center, which the package now conflicts with (plasma-desktop only recommends it). The maintenance terminal runs outside the classroom's configuration and still opens every module, verified. converged-check fails if any module on a machine is ever left unrestricted: 126 passed in the lab. Co-Authored-By: Claude Opus 5.5 <[email protected]>
428 lines
25 KiB
Bash
Executable File
428 lines
25 KiB
Bash
Executable File
#!/bin/bash
|
|
# Is this machine the classroom?
|
|
#
|
|
# One list of properties, run against whatever the lab is currently
|
|
# holding. A fresh install and a machine upgraded from the old sway or
|
|
# Hyprland shell have to answer it identically — that is the whole point
|
|
# of the migration work, and the reason this is a file rather than a
|
|
# paragraph in a report.
|
|
#
|
|
# tools/lab/converged-check the properties
|
|
# tools/lab/converged-check --restart and again after a session restart
|
|
#
|
|
# Every check reads the running machine. Nothing here consults the
|
|
# repository, so a check cannot pass by agreeing with the source it came
|
|
# from.
|
|
set -uo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
LAB="$ROOT/tools/lab/lab"
|
|
NAME="${LAB_NAME:-classroom-v2-lab}"
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
|
|
in_lab() { docker exec "$NAME" bash -c "$1" 2>/dev/null; }
|
|
|
|
# As the session itself reads configuration: the cascade of the running
|
|
# shell, not one this script made up.
|
|
session_config() {
|
|
local dirs
|
|
dirs=$(in_lab 'p=$(pgrep -x plasmashell | head -1); tr "\0" "\n" < /proc/$p/environ | grep "^XDG_CONFIG_DIRS=" | cut -d= -f2-')
|
|
docker exec -u classroom -e XDG_RUNTIME_DIR=/run/user/1000 -e HOME=/home/classroom \
|
|
-e LANG=C.UTF-8 -e "XDG_CONFIG_DIRS=$dirs" "$NAME" \
|
|
kreadconfig6 "$@" 2>/dev/null | tr -d '\r'
|
|
}
|
|
|
|
running() { in_lab "pgrep -x $1 | wc -l"; }
|
|
|
|
# Which process is actually answering org.freedesktop.Notifications. On a
|
|
# machine that still has mako installed this is the question that matters:
|
|
# the name is taken by whoever gets there first.
|
|
notification_owner() {
|
|
local pid
|
|
pid=$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- \
|
|
busctl --user --no-pager call org.freedesktop.DBus /org/freedesktop/DBus \
|
|
org.freedesktop.DBus GetConnectionUnixProcessID s org.freedesktop.Notifications' \
|
|
| awk '{print $2}')
|
|
[[ -n "$pid" ]] && in_lab "cat /proc/$pid/comm"
|
|
}
|
|
|
|
# How many keys KGlobalAccel has bound to an action, asked of the running
|
|
# shortcut server rather than read out of a file. This is the question
|
|
# Ctrl+Alt+Delete actually turns on: a file can say anything, and
|
|
# kglobalacceld is what answers a key press.
|
|
shortcut_of() {
|
|
docker exec -u classroom -e XDG_RUNTIME_DIR=/run/user/1000 -e HOME=/home/classroom \
|
|
-e DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus "$NAME" \
|
|
busctl --user --no-pager call org.kde.kglobalaccel /kglobalaccel \
|
|
org.kde.KGlobalAccel shortcut as 4 "$1" "$2" "" "" 2>/dev/null
|
|
}
|
|
|
|
# How many keys are bound. "ai 0" is an empty array: nothing answers.
|
|
shortcut_keys() { shortcut_of "$@" | awk '{print $2}'; }
|
|
|
|
# And which key, as Qt's combined key code, for the chords that have to
|
|
# keep working.
|
|
shortcut_key() { shortcut_of "$@" | awk '{print $3}'; }
|
|
|
|
# polkit's own verdict on a running process, which is the exact question
|
|
# a teacher's session asks when something offers to suspend the machine.
|
|
#
|
|
# allowed it would go ahead
|
|
# challenge it would ask for an administrator's password, which no
|
|
# teacher has — this is what reboot and shut down already do
|
|
# denied polkit refuses outright, which is the classroom rule
|
|
#
|
|
# Asked about a pid rather than by running pkcheck as somebody, because
|
|
# the subject polkit judges is the calling process and its session.
|
|
polkit_verdict() {
|
|
in_lab "/usr/bin/pkcheck --action-id $2 --process $1 >/dev/null 2>&1;
|
|
case \$? in 0) echo allowed ;; 1) echo denied ;; *) echo challenge ;; esac"
|
|
}
|
|
|
|
teacher_session() { in_lab 'pgrep -x plasmashell | head -1'; }
|
|
|
|
# What logind tells the classroom user it may do, which is what Plasma
|
|
# reads before deciding whether to draw a button.
|
|
#
|
|
# yes / challenge the operation is on offer
|
|
# no policy refuses it
|
|
# na this machine cannot do it at all
|
|
#
|
|
# A teacher must never see the first two. The lab container cannot
|
|
# hibernate whatever the policy says, so it answers "na" there and "no" on
|
|
# a real machine; both mean refused, and the polkit checks above are what
|
|
# actually prove the rule.
|
|
logind_offers() {
|
|
case "$(in_lab "runuser -u classroom -- busctl --no-pager call org.freedesktop.login1 \
|
|
/org/freedesktop/login1 org.freedesktop.login1.Manager $1" | cut -d'"' -f2)" in
|
|
yes|challenge) echo offered ;;
|
|
*) echo refused ;;
|
|
esac
|
|
}
|
|
|
|
# logind answers for the running configuration, not for a file on disk.
|
|
logind_property() {
|
|
in_lab "busctl get-property org.freedesktop.login1 /org/freedesktop/login1 \
|
|
org.freedesktop.login1.Manager $1" | cut -d'"' -f2
|
|
}
|
|
|
|
APPLETS=/home/classroom/.config/plasma-org.kde.plasma.desktop-appletsrc
|
|
|
|
check() {
|
|
# check DESCRIPTION EXPECTED ACTUAL
|
|
if [[ "$2" == "$3" ]]; then
|
|
printf ' \033[32mPASS\033[0m %-52s %s\n' "$1" "$3"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
printf ' \033[31mFAIL\033[0m %-52s got [%s] want [%s]\n' "$1" "$3" "$2"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
}
|
|
|
|
section() { printf '\n%s\n' "$1"; }
|
|
|
|
run_checks() {
|
|
section "the session"
|
|
check "kwin_wayland running" 1 "$(running kwin_wayland)"
|
|
check "plasmashell running" 1 "$(running plasmashell)"
|
|
|
|
section "the old shell is gone"
|
|
check "waybar not running" 0 "$(running waybar)"
|
|
check "mako not running" 0 "$(running mako)"
|
|
# The bracket keeps the pattern from matching the command that carries it.
|
|
check "classroom dock not running" 0 "$(in_lab 'pgrep -f "[c]lassroom-dock" | wc -l')"
|
|
# The lab's own outer compositor is sway, so the question is whether any
|
|
# sway is running that is not the harness's.
|
|
check "no sway but the lab harness's" 0 \
|
|
"$(in_lab 'pgrep -x sway | while read -r p; do tr "\0" " " < /proc/$p/cmdline | grep -q outer-sway.conf || echo x; done | grep -c x || true')"
|
|
check "Hyprland not running" 0 "$(running Hyprland)"
|
|
check "waybar activation removed" 0 \
|
|
"$(in_lab 'find /etc/systemd/user /etc/xdg/systemd/user /home/classroom/.config/systemd/user -name "waybar.service" -path "*.wants/*" 2>/dev/null | wc -l')"
|
|
check "mako activation removed" 0 \
|
|
"$(in_lab 'find /etc/systemd/user /etc/xdg/systemd/user /home/classroom/.config/systemd/user -name "mako.service" -path "*.wants/*" 2>/dev/null | wc -l')"
|
|
check "waybar masked" masked "$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- systemctl --user is-enabled waybar.service || true')"
|
|
check "mako masked" masked "$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- systemctl --user is-enabled mako.service || true')"
|
|
check "no login shell starting a compositor" 0 \
|
|
"$(in_lab 'grep -lE "exec (sway|Hyprland|start-hyprland|classroom-session)" /home/classroom/.bash_profile 2>/dev/null | wc -l')"
|
|
check "no tty1 autologin drop-in" 0 \
|
|
"$(in_lab 'ls /etc/systemd/system/[email protected]/autologin.conf 2>/dev/null | wc -l')"
|
|
check "no autostart entry for the old shell" 0 \
|
|
"$(in_lab 'grep -lE "^Exec=.*(waybar|mako|classroom-dock)" /home/classroom/.config/autostart/*.desktop 2>/dev/null | wc -l')"
|
|
|
|
section "device.conf"
|
|
check "COMPOSITOR removed" 0 "$(in_lab 'grep -c "^COMPOSITOR=" /etc/ekklesia-classroom/device.conf || true')"
|
|
check "not on the hyprland channel" 0 "$(in_lab 'grep -c "^CHANNEL=hyprland" /etc/ekklesia-classroom/device.conf || true')"
|
|
|
|
section "the configuration cascade"
|
|
check "classroom settings in the session's cascade" 1 \
|
|
"$(in_lab 'p=$(pgrep -x plasmashell | head -1); tr "\0" "\n" < /proc/$p/environ | grep -c "^XDG_CONFIG_DIRS=.*ekklesia-classroom/xdg"')"
|
|
check "look and feel authoritative" org.ekklesia.classroom.desktop \
|
|
"$(session_config --file kdeglobals --group KDE --key LookAndFeelPackage)"
|
|
check "and immutable" 1 \
|
|
"$(in_lab 'grep -c "^LookAndFeelPackage\[\$i\]=" /etc/ekklesia-classroom/xdg/kdeglobals')"
|
|
check "the palette is the packaged one" 20,28,32 \
|
|
"$(session_config --file kdeglobals --group 'Colors:Window' --key BackgroundNormal)"
|
|
|
|
section "the panel"
|
|
check "applet count" 10 "$(in_lab "grep -c '^plugin=' $APPLETS")"
|
|
check "Parent Call launcher" 1 \
|
|
"$(in_lab "grep -c 'ekklesia-parent-call.desktop' $APPLETS")"
|
|
check "quick launch, one widget" 1 \
|
|
"$(in_lab "grep -c '^plugin=org.kde.plasma.quicklaunch' $APPLETS")"
|
|
check "the applications are the room's" 1 \
|
|
"$(in_lab "grep -c 'launchers=applications:ekklesia-wonderink.desktop,applications:google-chrome.desktop' $APPLETS")"
|
|
check "network, volume, notifications, clock" 4 \
|
|
"$(in_lab "grep -cE '^plugin=org.kde.plasma.(networkmanagement|volume|notifications|digitalclock)$' $APPLETS")"
|
|
check "no kickoff" 0 "$(in_lab "grep -c 'plugin=org.kde.plasma.kickoff' $APPLETS")"
|
|
check "no pager" 0 "$(in_lab "grep -c 'plugin=org.kde.plasma.pager' $APPLETS")"
|
|
check "no system tray" 0 "$(in_lab "grep -c 'systemtray' $APPLETS")"
|
|
check "no discover or system settings" 0 \
|
|
"$(in_lab "grep -cE 'discover.desktop|systemsettings.desktop' $APPLETS")"
|
|
check "the packaged wallpaper" 1 \
|
|
"$(in_lab "grep -c 'Image=file:///usr/share/backgrounds/ekklesia/classroom-calm.png' $APPLETS")"
|
|
|
|
section "it will not strand a teacher"
|
|
check "no automatic lock" false "$(session_config --file kscreenlockerrc --group Daemon --key Autolock)"
|
|
check "no lock on resume" false "$(session_config --file kscreenlockerrc --group Daemon --key LockOnResume)"
|
|
check "the lock action is forbidden" false \
|
|
"$(session_config --file kdeglobals --group 'KDE Action Restrictions' --key action/lock_screen)"
|
|
check "power button does nothing" 0 \
|
|
"$(session_config --file powermanagementprofilesrc --group AC --group HandleButtonEvents --key powerButtonAction)"
|
|
check "no idle suspend, dim or blank configured" 0 \
|
|
"$(in_lab 'grep -cE "^\[(AC|Battery|LowBattery)\]\[(SuspendSession|DimDisplay|DPMSControl)\]" /etc/ekklesia-classroom/xdg/powermanagementprofilesrc || true')"
|
|
check "and the profiles are immutable" 3 \
|
|
"$(in_lab 'grep -cE "^\[(AC|Battery|LowBattery)\]\[\\\$i\]" /etc/ekklesia-classroom/xdg/powermanagementprofilesrc || true')"
|
|
check "logind ignores the power key" ignore "$(logind_property HandlePowerKey)"
|
|
check "logind ignores a long press" ignore "$(logind_property HandlePowerKeyLongPress)"
|
|
check "logind has no idle action" ignore "$(logind_property IdleAction)"
|
|
check "logind ignores the lid" ignore "$(logind_property HandleLidSwitch)"
|
|
|
|
section "Ctrl+Alt+Delete, and the sleep states"
|
|
# The chord a teacher used to hibernate a room.
|
|
check "Ctrl+Alt+Delete is bound to nothing" 0 "$(shortcut_keys ksmserver 'Log Out')"
|
|
for action in 'Log Out Without Confirmation' LogOut 'Shut Down' Reboot \
|
|
'Reboot Without Confirmation' 'Halt Without Confirmation'; do
|
|
check "ksmserver/$action unbound" 0 "$(shortcut_keys ksmserver "$action")"
|
|
done
|
|
for action in Sleep Hibernate PowerDown PowerOff; do
|
|
check "powerdevil/$action unbound" 0 "$(shortcut_keys org_kde_powerdevil "$action")"
|
|
done
|
|
# And the room's own chords are not collateral damage. The shortcut file
|
|
# is replaced wholesale at login, so this is the check that says the
|
|
# maintenance route survived it. 503316555 is Meta+Ctrl+Alt+Shift+K.
|
|
check "the maintenance chord still answers" 503316555 \
|
|
"$(shortcut_key ekklesia-admin-terminal.desktop _launch)"
|
|
check "the display chord still answers" 503316548 \
|
|
"$(shortcut_key ekklesia-display-settings.desktop _launch)"
|
|
# The greeter itself, with nothing to start it.
|
|
check "the session screen cannot be activated" 1 \
|
|
"$(in_lab 'grep -c "^Exec=/bin/false" /usr/local/share/dbus-1/services/org.kde.LogoutPrompt.service 2>/dev/null || true')"
|
|
# The policy layer, asked of the teacher's own running session.
|
|
#
|
|
# Hybrid sleep and suspend-then-hibernate have no polkit action of their
|
|
# own in systemd 257: logind authorises both through the hibernate action,
|
|
# so denying hibernate denies all three. The rule names the other two
|
|
# spellings anyway, in case a future systemd splits them out.
|
|
TEACHER="$(teacher_session)"
|
|
for action in suspend suspend-multiple-sessions suspend-ignore-inhibit \
|
|
hibernate hibernate-multiple-sessions hibernate-ignore-inhibit; do
|
|
check "the teacher's session may not $action" denied \
|
|
"$(polkit_verdict "$TEACHER" "org.freedesktop.login1.$action")"
|
|
done
|
|
# What the desktop is told, which is why no button is drawn for it.
|
|
check "logind offers the classroom no suspend" refused "$(logind_offers CanSuspend)"
|
|
check "logind offers the classroom no hibernate" refused "$(logind_offers CanHibernate)"
|
|
check "logind offers the classroom no hybrid sleep" refused \
|
|
"$(logind_offers CanHybridSleep)"
|
|
check "logind offers the classroom no suspend-then-hibernate" refused \
|
|
"$(logind_offers CanSuspendThenHibernate)"
|
|
|
|
# AND THE MACHINE IS NOT CRIPPLED FOR THE PERSON WHO MAINTAINS IT.
|
|
#
|
|
# The rule is scoped to the classroom user by name, so every other account
|
|
# has to be able to do all of it. pid 1 stands in for a root process.
|
|
for action in suspend hibernate reboot power-off; do
|
|
check "a root process may still $action" allowed \
|
|
"$(polkit_verdict 1 "org.freedesktop.login1.$action")"
|
|
done
|
|
# And the two the classroom user was deliberately NOT denied: ending up at
|
|
# an administrator's password prompt is the existing behaviour, and it is
|
|
# not this release's business to change it.
|
|
for action in reboot power-off; do
|
|
check "the classroom is not newly denied $action" challenge \
|
|
"$(polkit_verdict "$TEACHER" "org.freedesktop.login1.$action")"
|
|
done
|
|
check "sleep is not masked machine-wide" 0 \
|
|
"$(in_lab 'systemctl is-enabled sleep.target suspend.target hibernate.target 2>/dev/null | grep -c masked || true')"
|
|
|
|
section "lockdown still in force"
|
|
for key in run_command shell_access logout; do
|
|
check "$key" false "$(session_config --file kdeglobals --group 'KDE Action Restrictions' --key "$key")"
|
|
done
|
|
check "the panel cannot be edited" false \
|
|
"$(session_config --file kdeglobals --group 'KDE Action Restrictions' --key plasma/plasmashell/unlockedDesktop)"
|
|
|
|
section "autologin"
|
|
check "sddm enabled" enabled "$(in_lab 'systemctl is-enabled sddm.service || true')"
|
|
# Not a reading of the configuration: what SDDM actually did at the last
|
|
# boot of this machine. It authenticates the classroom through PAM and
|
|
# launches the session; the lab has no display hardware for the session to
|
|
# land on, so everything past this point is a hardware test.
|
|
check "sddm autologged the classroom in at boot" yes \
|
|
"$(in_lab 'journalctl -u sddm --no-pager 2>/dev/null | grep -q "Authentication for user .classroom. successful" && echo yes || echo no')"
|
|
check "and chose the Plasma session" yes \
|
|
"$(in_lab 'journalctl -u sddm --no-pager 2>/dev/null | grep -q "wayland-sessions/plasma.desktop. selected" && echo yes || echo no')"
|
|
check "autologin user" classroom \
|
|
"$(in_lab 'grep -A3 "^\[Autologin\]" /etc/sddm.conf.d/ekklesia-classroom.conf | sed -n "s/^User=//p"')"
|
|
check "autologin session" plasma \
|
|
"$(in_lab 'grep -A3 "^\[Autologin\]" /etc/sddm.conf.d/ekklesia-classroom.conf | sed -n "s/^Session=//p"')"
|
|
check "the session it names exists" 1 \
|
|
"$(in_lab 'ls /usr/share/wayland-sessions/plasma.desktop 2>/dev/null | wc -l')"
|
|
check "the classroom uid is inside the greeter's range" 1 \
|
|
"$(in_lab 'uid=$(id -u classroom); min=$(sed -n "s/^MinimumUid=//p" /etc/sddm.conf.d/ekklesia-classroom.conf); max=$(sed -n "s/^MaximumUid=//p" /etc/sddm.conf.d/ekklesia-classroom.conf); [ "$uid" -ge "$min" ] && [ "$uid" -le "$max" ] && echo 1 || echo 0')"
|
|
|
|
section "notifications belong to Plasma"
|
|
check "the old daemon cannot be activated" 1 \
|
|
"$(in_lab 'grep -c "^Exec=/bin/false" /usr/local/share/dbus-1/services/fr.emersion.mako.service 2>/dev/null || true')"
|
|
check "and Plasma owns the notification service" plasmashell "$(notification_owner)"
|
|
|
|
section "Wonder Ink is an application with tabs"
|
|
check "the shell is the classroom's, not a Chrome app window" 0 \
|
|
"$(in_lab 'grep -c "^exec google-chrome" /usr/local/bin/classroom-wonderink || true')"
|
|
check "and it is the Qt WebEngine one" 1 \
|
|
"$(in_lab 'grep -c "^from PyQt6.QtWebEngineWidgets import" /usr/local/bin/classroom-wonderink || true')"
|
|
check "the engine is installed" ok \
|
|
"$(in_lab 'QT_QPA_PLATFORM=offscreen python3 -c "import PyQt6.QtWebEngineWidgets; print(\"ok\")" 2>/dev/null')"
|
|
# The panel matches a window to its launcher by this, and a mismatch is
|
|
# not an error anywhere — it is silently two buttons instead of one.
|
|
check "the launcher names the shell's app id" ekklesia-wonderink \
|
|
"$(in_lab 'sed -n "s/^StartupWMClass=//p" /usr/share/applications/ekklesia-wonderink.desktop')"
|
|
# Comments stripped first. The shell has a comment saying Ctrl+T is
|
|
# deliberately absent, and a check that cannot tell that from a binding
|
|
# is a check that fails for being right.
|
|
# One text box exists, in the print preview, for the page range. The
|
|
# check reads the code outside that class; anywhere else a text box in
|
|
# this window would be an address bar.
|
|
check "no code offers a new tab button or an address bar" 0 \
|
|
"$(in_lab "sed '/^class PrintPreview/,/^def notify/d; /setCornerWidget(self._controls/d' /usr/local/bin/classroom-wonderink | grep -vE '^[[:space:]]*#' | grep -cE 'QLineEdit\\(|setCornerWidget|addressBar|bind\\(\"Ctrl\\+T' || true")"
|
|
# Separate from the ordinary browser, which is the whole reason the split
|
|
# exists: a teacher's Wonder Ink sign-in and the room's shared YouTube
|
|
# account must never be in one profile.
|
|
check "its profile is its own" own \
|
|
"$(in_lab 'grep -q "wonderink-shell" /usr/local/bin/classroom-wonderink && echo own || echo shared')"
|
|
# And the Chrome profile is left where it is, so a rollback to 0.2.0
|
|
# finds a teacher's Wonder Ink directory intact.
|
|
check "and Chrome's old one is not reused" 0 \
|
|
"$(in_lab 'grep -c "ekklesia-classroom/wonderink\"" /usr/local/bin/classroom-wonderink || true')"
|
|
# The Present-view fix: a lesson may start its own video, and only this
|
|
# profile may. The browser's policy is not touched, which the next two
|
|
# checks are about.
|
|
check "cookies are still session-only" 1 \
|
|
"$(in_lab 'grep -c "NoPersistentCookies" /usr/local/bin/classroom-wonderink || true')"
|
|
check "a lesson may start its own video" 1 \
|
|
"$(in_lab 'grep -c "PlaybackRequiresUserGesture, False" /usr/local/bin/classroom-wonderink || true')"
|
|
check "and the browser's policy still says nothing about autoplay" 0 \
|
|
"$(in_lab 'grep -ci "autoplay" /etc/opt/chrome/policies/managed/ekklesia-classroom.json || true')"
|
|
check "the browser is still Chrome" 1 \
|
|
"$(in_lab 'grep -c "^exec google-chrome" /usr/local/bin/classroom-browser || true')"
|
|
check "and the browser is untouched by any of this" 0 \
|
|
"$(in_lab 'grep -ci "pyqt\|webengine" /usr/local/bin/classroom-browser || true')"
|
|
|
|
section "a lesson's worksheets come back as tabs"
|
|
check "worksheets have a directory of Wonder Ink's own" 1 \
|
|
"$(in_lab 'grep -c ") / \"Wonder Ink\"$" /usr/local/bin/classroom-wonderink || true')"
|
|
check "a finished PDF becomes a tab" 1 \
|
|
"$(in_lab 'grep -c "def open_worksheet" /usr/local/bin/classroom-wonderink || true')"
|
|
check "checked on its contents, not its name" 1 \
|
|
"$(in_lab 'grep -c "head != PDF_MAGIC" /usr/local/bin/classroom-wonderink || true')"
|
|
# Measured: isFinishedChanged is never emitted for an interrupted
|
|
# download, so a worksheet that did not arrive said nothing at all.
|
|
check "terminal states come from stateChanged" 0 \
|
|
"$(in_lab 'grep -c "isFinishedChanged.connect" /usr/local/bin/classroom-wonderink || true')"
|
|
check "which local files may be opened is a list, not a directory" 1 \
|
|
"$(in_lab 'grep -c "def may_open" /usr/local/bin/classroom-wonderink || true')"
|
|
check "and a guard consults it on every navigation" 1 \
|
|
"$(in_lab 'grep -c "def acceptNavigationRequest" /usr/local/bin/classroom-wonderink || true')"
|
|
check "a local document cannot read other local documents" 1 \
|
|
"$(in_lab 'grep -c "LocalContentCanAccessFileUrls, False" /usr/local/bin/classroom-wonderink || true')"
|
|
# Qt's print dialog, and its Print to File chooser, are gone altogether:
|
|
# the preview offers CUPS printers and nothing else.
|
|
check "no file chooser behind printing" 0 \
|
|
"$(in_lab 'grep -cE "QFileDialog|QPrintDialog" /usr/local/bin/classroom-wonderink || true')"
|
|
check "nothing downloaded is handed to the desktop to run" 0 \
|
|
"$(in_lab 'grep -cE "QDesktopServices|xdg-open" /usr/local/bin/classroom-wonderink || true')"
|
|
check "and no file browser was added" 0 \
|
|
"$(in_lab 'grep -c "QFileDialog" /usr/local/bin/classroom-wonderink || true')"
|
|
# The separation the whole split exists for: Wonder Ink handles its own
|
|
# downloads because they arrive through its own profile, not because
|
|
# anything inspects a filename or watches a directory.
|
|
check "downloads arrive through Wonder Ink's own profile" 1 \
|
|
"$(in_lab 'grep -c "profile.downloadRequested.connect(window.on_download)" /usr/local/bin/classroom-wonderink || true')"
|
|
check "nothing watches a directory" 0 \
|
|
"$(in_lab 'grep -cE "QFileSystemWatcher|inotify" /usr/local/bin/classroom-wonderink || true')"
|
|
check "and the browser still knows nothing about any of it" 0 \
|
|
"$(in_lab 'grep -cE "pdf|download" /usr/local/bin/classroom-browser || true')"
|
|
|
|
section "Wonder Ink can be turned off per device"
|
|
check "device.conf carries the setting" 1 \
|
|
"$(in_lab 'grep -c "^WONDERINK_ENABLED=" /etc/ekklesia-classroom/device.conf || true')"
|
|
check "and it defaults to on" true \
|
|
"$(in_lab 'sed -n "s/^WONDERINK_ENABLED=//p" /etc/ekklesia-classroom/device.conf')"
|
|
# THE TEACHER CANNOT OPEN SYSTEM SETTINGS, AND THE MAINTENANCE TERMINAL CAN.
|
|
# Every settings module this machine has must be refused in the classroom's
|
|
# cascade — a notification's Configure button opens whichever one it names —
|
|
# and none may be refused in the cascade the maintenance terminal uses.
|
|
teacher_cascade() { in_lab "XDG_CONFIG_DIRS=/etc/ekklesia-classroom/xdg:/etc/xdg kreadconfig6 --file kdeglobals --group 'KDE Control Module Restrictions' --key $1 --default true 2>/dev/null"; }
|
|
modules=$(in_lab 'find /usr/lib/x86_64-linux-gnu/qt6/plugins -path "*kcms*" -name "*.so" | sed "s#.*/##; s#\.so\$##" | sort')
|
|
open_modules=$(for m in $modules; do [[ "$(teacher_cascade "$m")" == false ]] || echo "$m"; done | tr '\n' ' ')
|
|
check "every settings module is refused to the teacher (unrestricted: ${open_modules:-none})" 0 \
|
|
"$(wc -w <<< "$open_modules")"
|
|
check "the notification settings the Configure button opens are among them" false \
|
|
"$(teacher_cascade kcm_notifications)"
|
|
check "but not to the maintenance terminal" true \
|
|
"$(in_lab "XDG_CONFIG_DIRS=/etc/xdg kreadconfig6 --file kdeglobals --group 'KDE Control Module Restrictions' --key kcm_notifications --default true 2>/dev/null")"
|
|
check "which still starts outside the classroom's configuration" 1 \
|
|
"$(in_lab 'grep -c "XDG_CONFIG_DIRS=/etc/xdg konsole" /usr/local/bin/classroom-admin-terminal || true')"
|
|
check "a locked panel offers no widget settings" false \
|
|
"$(in_lab "XDG_CONFIG_DIRS=/etc/ekklesia-classroom/xdg:/etc/xdg kreadconfig6 --file kdeglobals --group 'KDE Action Restrictions' --key plasma/allow_configure_when_locked --default true 2>/dev/null")"
|
|
check "and no help center is installed to open from a Help button" 0 \
|
|
"$(in_lab 'dpkg -l khelpcenter 2>/dev/null | grep -c "^ii" || true')"
|
|
|
|
# One line the setting switches, since 0.2.3's one-setting-per-application
|
|
# layout: var WONDERINK = true|false. This asked for the older marker
|
|
# until 0.2.6.1 and so failed on every lab run in between.
|
|
check "the layout carries a marker the setting can switch" 1 \
|
|
"$(in_lab 'grep -cE "^var WONDERINK = (true|false);" /usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/layouts/org.kde.plasma.desktop-layout.js || true')"
|
|
check "and it is switched on for this machine" 1 \
|
|
"$(in_lab 'grep -c "^var WONDERINK = true;" /usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/layouts/org.kde.plasma.desktop-layout.js || true')"
|
|
|
|
section "the room's own parts survived"
|
|
check "parent call installed" 1 "$(in_lab 'ls /usr/local/bin/classroom-parent-call 2>/dev/null | wc -l')"
|
|
check "parent call icon installed" 1 \
|
|
"$(in_lab 'ls /usr/share/icons/hicolor/scalable/apps/ekklesia-parent-call.svg 2>/dev/null | wc -l')"
|
|
check "wonderink installed" 1 "$(in_lab 'ls /usr/local/bin/classroom-wonderink 2>/dev/null | wc -l')"
|
|
check "the maintenance chord is registered" 1 \
|
|
"$(in_lab 'grep -c "^X-KDE-Shortcuts=Meta+Ctrl+Alt+Shift+K" /usr/share/applications/ekklesia-admin-terminal.desktop || true')"
|
|
check "no sudo rule for the classroom" 0 \
|
|
"$(in_lab 'grep -rl classroom /etc/sudoers /etc/sudoers.d 2>/dev/null | wc -l')"
|
|
check "the panel is supervised" always \
|
|
"$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- systemctl --user show plasma-plasmashell.service -p Restart --value')"
|
|
check "audio policy active" active \
|
|
"$(in_lab 'XDG_RUNTIME_DIR=/run/user/1000 runuser -u classroom -- systemctl --user is-active ekklesia-classroom-audio.service || true')"
|
|
}
|
|
|
|
run_checks
|
|
|
|
if [[ "${1:-}" == "--restart" ]]; then
|
|
section "and again after logging out and back in"
|
|
"$LAB" plasma-down >/dev/null
|
|
"$LAB" plasma-up >/dev/null
|
|
sleep 3
|
|
run_checks
|
|
fi
|
|
|
|
printf '\n%s passed, %s failed\n' "$PASS" "$FAIL"
|
|
[[ "$FAIL" -eq 0 ]]
|