diff --git a/config/plasma/applications-local/org.kde.konsole.desktop b/config/plasma/applications-local/org.kde.konsole.desktop new file mode 100644 index 0000000..6b5ab7c --- /dev/null +++ b/config/plasma/applications-local/org.kde.konsole.desktop @@ -0,0 +1,20 @@ +[Desktop Entry] +# Konsole, hidden. +# +# Shadows the entry Konsole's own package installs, from +# /usr/local/share/applications, which XDG_DATA_DIRS searches first. The +# real entry is left exactly where Konsole put it, so the package stays +# maintainable and an upgrade does not fight this. +# +# The terminal is still reachable, by one chord, through +# ekklesia-admin-terminal.desktop. It is simply not something a teacher +# can come across. +Type=Application +Name=Konsole +Comment=Terminal +Exec=/usr/bin/konsole +Icon=utilities-terminal +Terminal=false +Categories=System;TerminalEmulator; +NoDisplay=true +StartupWMClass=org.kde.konsole diff --git a/config/plasma/applications/ekklesia-admin-terminal.desktop b/config/plasma/applications/ekklesia-admin-terminal.desktop new file mode 100644 index 0000000..0b22b70 --- /dev/null +++ b/config/plasma/applications/ekklesia-admin-terminal.desktop @@ -0,0 +1,18 @@ +[Desktop Entry] +Type=Application +Name=Ekklesia maintenance terminal +Comment=For the technical administrator, standing at the machine +Exec=/usr/local/bin/classroom-admin-terminal +Icon=utilities-terminal +Terminal=false +Categories=System; +StartupNotify=true +StartupWMClass=org.kde.konsole +# The chord is deliberately awkward. KGlobalAccel reads this key from the +# entry itself, so the shortcut ships in the package rather than having to +# be seeded into a teacher's configuration where an update could not +# reach it. +X-KDE-Shortcuts=Meta+Ctrl+Alt+Shift+K +# Not in any application list. There is no menu in this classroom anyway; +# this says so out loud. +NoDisplay=true diff --git a/config/plasma/xdg/powermanagementprofilesrc b/config/plasma/xdg/powermanagementprofilesrc new file mode 100644 index 0000000..13fc8fa --- /dev/null +++ b/config/plasma/xdg/powermanagementprofilesrc @@ -0,0 +1,26 @@ +# The desktop half of "the power button does nothing". +# +# PowerDevil takes logind's handle-power-key inhibitor while a session is +# running, so the logind drop-in on its own would be overruled by +# whatever this says. 0 is no action. +# +# Immutable, so nothing in the session can put an action back. +# +# Every profile gets the same answer. A classroom machine has no battery, +# but a profile that exists and disagrees is worse than one that does not +# exist at all. + +[AC][HandleButtonEvents][$i] +powerButtonAction=0 +lidAction=0 +triggerLidActionWhenExternalMonitorPresent=false + +[Battery][HandleButtonEvents][$i] +powerButtonAction=0 +lidAction=0 +triggerLidActionWhenExternalMonitorPresent=false + +[LowBattery][HandleButtonEvents][$i] +powerButtonAction=0 +lidAction=0 +triggerLidActionWhenExternalMonitorPresent=false diff --git a/config/systemd/logind-ekklesia-classroom.conf b/config/systemd/logind-ekklesia-classroom.conf new file mode 100644 index 0000000..de1a2b4 --- /dev/null +++ b/config/systemd/logind-ekklesia-classroom.conf @@ -0,0 +1,34 @@ +# The classroom's power button does nothing. +# +# A teacher reaching behind a television for a cable, a child leaning on +# the tower, a volunteer tidying the room: none of those should be able to +# take the computer down in the middle of a Sunday morning. Recovering a +# room that has been switched off needs somebody who knows where the +# button is, which is exactly the person who is not there. +# +# This is one of two layers, and both are needed. While a graphical +# session is running, PowerDevil takes logind's handle-power-key +# inhibitor and acts on the button itself, so logind alone would be +# ignored — see the classroom's powermanagementprofilesrc for the other +# half. This layer is what covers the machine before the session starts, +# after it ends, and if PowerDevil is not running at all. +# +# Holding the button for several seconds still cuts the power. That is +# the firmware doing it, below any operating system, and it is left alone +# on purpose: it is the only recovery a person standing at a wedged +# machine has. + +[Login] +HandlePowerKey=ignore +HandlePowerKeyLongPress=ignore +HandleSuspendKey=ignore +HandleSuspendKeyLongPress=ignore +HandleHibernateKey=ignore +HandleHibernateKeyLongPress=ignore +HandleRebootKey=ignore +HandleRebootKeyLongPress=ignore +# Classroom machines are desktops, but a laptop pressed into service in a +# room should not sleep when its lid is closed either. +HandleLidSwitch=ignore +HandleLidSwitchExternalPower=ignore +HandleLidSwitchDocked=ignore diff --git a/docs/screenshots/admin-terminal-bar.png b/docs/screenshots/admin-terminal-bar.png new file mode 100644 index 0000000..35cac74 Binary files /dev/null and b/docs/screenshots/admin-terminal-bar.png differ diff --git a/docs/screenshots/admin-terminal.png b/docs/screenshots/admin-terminal.png new file mode 100644 index 0000000..3f58d3b Binary files /dev/null and b/docs/screenshots/admin-terminal.png differ diff --git a/packaging/debian/control.template b/packaging/debian/control.template index 98d7bc0..1417910 100644 --- a/packaging/debian/control.template +++ b/packaging/debian/control.template @@ -4,7 +4,7 @@ Section: misc Priority: optional Architecture: amd64 Maintainer: Ekklesia -Depends: plasma-desktop, plasma-workspace, kwin-wayland, plasma-nm, plasma-pa, plasma-widgets-addons, sddm, dbus-user-session, xwayland, fonts-dejavu, fonts-noto, network-manager, pipewire, pipewire-audio, wireplumber, pulseaudio-utils, cups, cups-client, cups-filters, foomatic-db-compressed-ppds, python3-gi, python3-gi-cairo, gir1.2-gtk-3.0, gir1.2-gtklayershell-0.1, libnotify-bin, curl, gnupg, ca-certificates, python3-paho-mqtt +Depends: plasma-desktop, plasma-workspace, kwin-wayland, plasma-nm, plasma-pa, plasma-widgets-addons, konsole, sddm, dbus-user-session, xwayland, fonts-dejavu, fonts-noto, network-manager, pipewire, pipewire-audio, wireplumber, pulseaudio-utils, cups, cups-client, cups-filters, foomatic-db-compressed-ppds, python3-gi, python3-gi-cairo, gir1.2-gtk-3.0, gir1.2-gtklayershell-0.1, libnotify-bin, curl, gnupg, ca-certificates, python3-paho-mqtt Recommends: vlc Conflicts: plasma-welcome, plasma-discover, kdeconnect, kup-backup Description: Ekklesia managed classroom environment @@ -14,6 +14,11 @@ Description: Ekklesia managed classroom environment left here is the church's own subject matter — Wonder Ink, the browser, the DVD player, the printer, the audio policy and the updater. . + konsole is here for one chord. It is the local recovery route for a + technical administrator standing at a machine when SSH is not + available; it is hidden from every application list and reachable only + by Meta+Ctrl+Alt+Shift+K. + . plasma-widgets-addons is here for one widget: Quick Launch, which is the only stock way to put a single launcher on the panel that reads /usr/share/applications live. The Icon widget copies the entry into the diff --git a/packaging/debian/postinst b/packaging/debian/postinst index 617f2a9..7f17619 100755 --- a/packaging/debian/postinst +++ b/packaging/debian/postinst @@ -46,6 +46,12 @@ chmod 0640 /etc/ekklesia-classroom/device.conf systemctl daemon-reload || true +# The power button stops working immediately rather than at the next +# boot. Verified in the lab: logind re-reads its drop-ins on reload and +# reports ignore afterwards, and the running graphical session is not +# disturbed by it. +systemctl reload systemd-logind >/dev/null 2>&1 || true + # Autologin into the classroom, and back into it if the session ever ends. systemctl enable sddm.service >/dev/null 2>&1 || true diff --git a/scripts/classroom-admin-terminal b/scripts/classroom-admin-terminal new file mode 100755 index 0000000..02b62e2 --- /dev/null +++ b/scripts/classroom-admin-terminal @@ -0,0 +1,30 @@ +#!/bin/bash +set -euo pipefail + +# The maintenance terminal, for the one person who is allowed one. +# +# Normal administration is over SSH. This exists for the case where +# somebody is standing at a classroom machine and SSH is not available, +# and it is reached only by a deliberately awkward chord that no teacher +# will find by accident. +# +# Why it is a wrapper rather than plain Konsole: +# +# the classroom's Kiosk policy sets shell_access=false, and Konsole +# honours it — measured: with the classroom configuration in its +# cascade it starts and prints "Program to run not set." rather than +# opening a shell. That restriction is worth keeping for everything +# else, so it is lifted for this one process by leaving the classroom's +# configuration directory out of its cascade. Nothing is patched and no +# binary is replaced; XDG_CONFIG_DIRS is the documented way to say +# which configuration an application sees. +# +# The terminal therefore looks like stock KDE rather than like the +# classroom, which is fair: it is not part of the classroom. +# +# This grants no privilege. The classroom user has no sudo rule, and +# anything that needs root still needs the administrator's own account +# and password. The Kiosk restriction was always a barrier in the +# interface, never a privilege boundary. + +exec env XDG_CONFIG_DIRS=/etc/xdg konsole "$@" diff --git a/tests/test-deployment-support.sh b/tests/test-deployment-support.sh index 79a285e..de6b184 100755 --- a/tests/test-deployment-support.sh +++ b/tests/test-deployment-support.sh @@ -119,6 +119,51 @@ for entry in sorted((root / "config/plasma/applications").glob("*.desktop")): f'"$ROOT/config/icons/${{icon}}.svg"' in build or icon in build) check(f"{icon}.svg exists", (root / f"config/icons/{icon}.svg").is_file()) +print() +print("4b4. the power button does nothing, at both layers") +logind = (root / "config/systemd/logind-ekklesia-classroom.conf").read_text() +power = (root / "config/plasma/xdg/powermanagementprofilesrc").read_text() +# PowerDevil holds logind's handle-power-key inhibitor while a session is +# running, so one layer without the other is a button that still works. +for key in ("HandlePowerKey=ignore", "HandleSuspendKey=ignore", + "HandleHibernateKey=ignore", "HandleLidSwitch=ignore"): + check(f"logind: {key}", key in logind) +check("PowerDevil: no action on the power button", + power.count("powerButtonAction=0") == 3) +check("PowerDevil: no action on a lid either", + power.count("lidAction=0") == 3) +check("and the session cannot put an action back", + power.count("[$i]") == 3) +check("the logind drop-in is packaged", + "/etc/systemd/logind.conf.d/ekklesia-classroom.conf" in build) +check("the PowerDevil profile is packaged", + "powermanagementprofilesrc" in build) +check("the long hold is left alone on purpose", + "firmware" in logind) + +print() +print("4b5. the maintenance terminal is hidden and reachable") +admin = (root / "config/plasma/applications/ekklesia-admin-terminal.desktop").read_text() +konsole = (root / "config/plasma/applications-local/org.kde.konsole.desktop").read_text() +wrapper = (root / "scripts/classroom-admin-terminal").read_text() +check("the chord is the obscure one", + "X-KDE-Shortcuts=Meta+Ctrl+Alt+Shift+K" in admin) +check("its entry is in no application list", "NoDisplay=true" in admin) +check("Konsole's own entry is shadowed and hidden", + "NoDisplay=true" in konsole) +check("the shadow is in /usr/local/share, not on top of Konsole's file", + "$STAGE/usr/local/share/applications/" in build) +check("the terminal is launched outside the classroom's restrictions", + "XDG_CONFIG_DIRS=/etc/xdg" in wrapper) +check("and grants no privilege of its own", + "sudo" not in wrapper.replace("no sudo rule", "")) +check("the wrapper is packaged", "classroom-admin-terminal" in build) +check("Konsole is a dependency, so the route exists on every machine", + "konsole" in (root / "packaging/debian/control.template").read_text()) +check("no terminal is pinned to the panel", + "konsole" not in (root / "config/plasma/lookandfeel/contents/layouts" + / "org.kde.plasma.desktop-layout.js").read_text().lower()) + print() print("4c. the teacher cannot reach a command line") for restriction in ("run_command=false", "shell_access=false", diff --git a/tools/build-release.sh b/tools/build-release.sh index 0db072b..2c263d5 100755 --- a/tools/build-release.sh +++ b/tools/build-release.sh @@ -207,7 +207,7 @@ install -Dm644 \ # Not /etc/xdg: plasma-workspace already owns files by these names there. # The classroom's directory goes in front of it in XDG_CONFIG_DIRS instead. for setting in kdeglobals kwinrc kwinrulesrc kscreenlockerrc ksmserverrc \ - krunnerrc plasmanotifyrc; do + krunnerrc plasmanotifyrc powermanagementprofilesrc; do install -Dm644 \ "$ROOT/config/plasma/xdg/${setting}" \ "$STAGE/etc/ekklesia-classroom/xdg/${setting}" @@ -217,6 +217,12 @@ install -Dm644 \ "$ROOT/config/environment/90-ekklesia-classroom.conf" \ "$STAGE/etc/environment.d/90-ekklesia-classroom.conf" +# The power button does nothing. Two layers, because while a session is +# running PowerDevil holds logind's inhibitor and would overrule it. +install -Dm644 \ + "$ROOT/config/systemd/logind-ekklesia-classroom.conf" \ + "$STAGE/etc/systemd/logind.conf.d/ekklesia-classroom.conf" + for scheme in EkklesiaDark EkklesiaLight; do install -Dm644 \ "$ROOT/config/plasma/colors/${scheme}.colors" \ @@ -292,6 +298,10 @@ install -Dm755 \ "$ROOT/scripts/classroom-desktop-layout" \ "$STAGE/usr/local/bin/classroom-desktop-layout" +install -Dm755 \ + "$ROOT/scripts/classroom-admin-terminal" \ + "$STAGE/usr/local/bin/classroom-admin-terminal" + install -Dm755 \ "$ROOT/scripts/classroom-desktop-reset" \ "$STAGE/usr/local/bin/classroom-desktop-reset" diff --git a/tools/lab/Dockerfile.plasma b/tools/lab/Dockerfile.plasma index a74523c..5757abd 100644 --- a/tools/lab/Dockerfile.plasma +++ b/tools/lab/Dockerfile.plasma @@ -92,3 +92,7 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get purge -y \ # Quick Launch, for the one auxiliary button on the panel. RUN apt-get update && apt-get install -y --no-install-recommends plasma-widgets-addons \ && rm -rf /var/lib/apt/lists/* + +# Konsole, for the maintenance chord. +RUN apt-get update && apt-get install -y --no-install-recommends konsole \ + && rm -rf /var/lib/apt/lists/* diff --git a/tools/lab/install-desktop b/tools/lab/install-desktop index 8a08af3..c6a9a5c 100755 --- a/tools/lab/install-desktop +++ b/tools/lab/install-desktop @@ -13,7 +13,7 @@ SRC="${1:-/srv/classroom}" # XDG_CONFIG_DIRS. plasma-workspace owns files by these names in /etc/xdg # and two packages cannot own one path. for setting in kdeglobals kwinrc kwinrulesrc kscreenlockerrc ksmserverrc \ - krunnerrc plasmanotifyrc; do + krunnerrc plasmanotifyrc powermanagementprofilesrc; do install -Dm644 "$SRC/config/plasma/xdg/$setting" \ "/etc/ekklesia-classroom/xdg/$setting" rm -f "/etc/xdg/$setting.ekklesia-old" @@ -22,6 +22,9 @@ done install -Dm644 "$SRC/config/environment/90-ekklesia-classroom.conf" \ /etc/environment.d/90-ekklesia-classroom.conf +install -Dm644 "$SRC/config/systemd/logind-ekklesia-classroom.conf" \ + /etc/systemd/logind.conf.d/ekklesia-classroom.conf + install -Dm644 "$SRC/config/plasma/colors/EkklesiaDark.colors" \ /usr/share/color-schemes/EkklesiaDark.colors install -Dm644 "$SRC/config/plasma/colors/EkklesiaLight.colors" \