From 23dff0b7b40029c0b475057b916de50ac7864317 Mon Sep 17 00:00:00 2001 From: ekklesia Date: Sat, 19 Sep 2026 20:50:36 -0500 Subject: [PATCH] feat: close the chord that let a teacher hibernate the room MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ctrl+Alt+Delete opened Plasma's session screen and a teacher hibernated a classroom computer from it. A hibernated room cannot be recovered by anybody standing in front of it, which on a Sunday morning is everybody. logout=false was already set and did not stop it. That restriction governs Plasma's own actions — what plasmashell draws in a menu or an applet. The chord is a KGlobalAccel shortcut registered by ksmserver and it asks D-Bus for the greeter without consulting a Plasma action at all. A restriction on a menu cannot see a key. Three layers, because each one alone has a way past it. The key. kglobalshortcutsrc unbinds every ksmserver session action and PowerDevil's Sleep, Hibernate, PowerDown and PowerOff, each entry marked immutable. It is deliberately not in the xdg directory with the other settings: kglobalacceld opens that file as a SimpleConfig, so it reads one file in the teacher's home and no system directory at all. Measured — with it in the cascade, kreadconfig6 reported the chord unbound while the shortcut server went on answering Ctrl+Alt+Del. classroom-desktop-reset copies the package's copy into place before the shortcut server starts, the same way the panel is copied out of the package at every login. The policy. A polkit rule refuses login1.suspend* and .hibernate* for the classroom user and for nobody else. This is the layer that matters: a key can be unbound and anything reaching logind can still ask. Refusing here rather than masking sleep.target keeps a technician's own account working, and is also what tells Plasma to stop drawing the button — CanSuspend and CanHibernate come back no. Hybrid sleep and suspend-then-hibernate have no action of their own in systemd 257; both are authorised through hibernate, so denying it denies all three. The greeter. Its D-Bus activation is shadowed with /bin/false, the same mechanism already used for the old notification daemon. Layer one removes the caller, this removes the callee. Measured in the lab, from the package, after a reboot: the escape reproduced first with the shadow removed — the greeter really does start — and then refused, exited 1, with it back. The chord reports nothing bound. systemctl suspend as the classroom user is denied. The maintenance chord and the display chord both still answer, by key code, after the shortcut file is replaced wholesale. A root process may still suspend, hibernate, reboot and power off. 182 checks pass across a logout and back. Screen locking stays disabled, the power button still does nothing, and reboot and shut down are not newly denied to anybody. Co-Authored-By: Claude Opus 5 --- config/dbus/org.kde.LogoutPrompt.service | 28 +++++ config/plasma/kglobalshortcutsrc | 86 +++++++++++++ .../polkit/org.ekklesia.classroom.sleep.rules | 59 +++++++++ docs/kiosk.md | 118 ++++++++++++++++++ scripts/classroom-desktop-reset | 31 +++++ .../user/ekklesia-classroom-desktop.service | 6 + tools/build-release.sh | 27 ++++ tools/lab/converged-check | 112 +++++++++++++++++ 8 files changed, 467 insertions(+) create mode 100644 config/dbus/org.kde.LogoutPrompt.service create mode 100644 config/plasma/kglobalshortcutsrc create mode 100644 config/polkit/org.ekklesia.classroom.sleep.rules create mode 100644 docs/kiosk.md diff --git a/config/dbus/org.kde.LogoutPrompt.service b/config/dbus/org.kde.LogoutPrompt.service new file mode 100644 index 0000000..ea511fb --- /dev/null +++ b/config/dbus/org.kde.LogoutPrompt.service @@ -0,0 +1,28 @@ +[D-BUS Service] +# Plasma's session screen, made unstartable. +# +# The third and last layer over Ctrl+Alt+Delete, and the one that does not +# depend on knowing which key is bound to what. +# +# Ctrl+Alt+Delete does not draw anything itself. It asks D-Bus for +# org.kde.LogoutPrompt, and D-Bus starts ksmserver-logout-greeter, which +# is the screen with Log Out, Restart, Shut Down, Sleep and Hibernate on +# it. Unbinding the chord in kglobalshortcutsrc takes away the caller; +# this takes away the callee, so any other caller — a Plasma release that +# binds the prompt to something new, an applet that ends up on the panel +# by accident, a script — finds nothing to start either. +# +# The same shadowing mechanism the classroom already uses for the old +# notification daemon: the session bus searches XDG_DATA_DIRS in order and +# /usr/local/share comes before /usr/share, so no file belonging to +# plasma-workspace is touched and Plasma can be upgraded underneath this +# without anything here changing. +# +# What this does NOT do: end a session. Logging out, restarting and +# shutting down are all still there for anyone with an account on the +# machine, through systemctl and loginctl, which is how a technician does +# it already. The greeter is a teacher-facing screen in a room that has no +# other teacher-facing way to do any of it, and that is the only thing +# taken away. +Name=org.kde.LogoutPrompt +Exec=/bin/false diff --git a/config/plasma/kglobalshortcutsrc b/config/plasma/kglobalshortcutsrc new file mode 100644 index 0000000..fa6578a --- /dev/null +++ b/config/plasma/kglobalshortcutsrc @@ -0,0 +1,86 @@ +# The keys that end a classroom session, taken away. +# +# A teacher pressed Ctrl+Alt+Delete, got Plasma's session screen with +# Hibernate on it, and hibernated the room. This file is what closes that, +# and it is worth being precise about why the lockdown already in +# kdeglobals did not. +# +# [KDE Action Restrictions] logout=false stops *Plasma's own* logout +# actions — the ones plasmashell draws in a menu or an applet. The +# Ctrl+Alt+Delete chord is not one of those. It is a KGlobalAccel +# shortcut registered by ksmserver, and pressing it calls the logout +# greeter over D-Bus without ever asking a Plasma action whether it is +# allowed. A restriction on a menu cannot see a key. +# +# WHY THIS FILE IS NOT IN THE XDG DIRECTORY WITH THE OTHERS. +# +# Every other classroom setting is installed into +# /etc/ekklesia-classroom/xdg and reaches Plasma through KConfig's +# cascade. That was tried here first and it does not work, which was +# measured rather than reasoned about: with this file in the cascade, +# kreadconfig6 reported the chord as "none" while kglobalacceld went on +# answering Ctrl+Alt+Del. kglobalacceld opens kglobalshortcutsrc as a +# SimpleConfig — no cascade, no system directories, no immutability from +# anywhere but the file itself — so it reads exactly one file, +# ~/.config/kglobalshortcutsrc, and nothing else. +# +# So this is installed to /etc/ekklesia-classroom/kglobalshortcutsrc and +# classroom-desktop-reset copies it into the teacher's home before the +# session's shortcut server starts. That is the same thing the classroom +# already does with the panel: the file in the home directory is not +# state, it is a copy of the package, replaced at every login so that an +# update can always reach a room. +# +# KGlobalAccel stores every shortcut as +# +# Name=active,default,friendly name +# +# and repopulates whatever is missing from each component's registered +# defaults when that component starts — which is why replacing the whole +# file costs nothing: everything not named below comes straight back. +# Measured in the lab, including the maintenance chord, which is +# registered from its desktop entry and is bound again after the replace. +# +# Each key is marked [$i] — immutable — and that is what makes the +# replacement stick: KGlobalAccel writes this file constantly, and +# without the marker it would put the chord back the moment ksmserver +# registered its actions again. +# +# Per key rather than per group, deliberately. Marking +# [org_kde_powerdevil] immutable as a whole would also freeze the +# brightness and keyboard backlight keys, which a classroom has no reason +# to take away. +# +# The default column is set to none as well as the active one, so that +# "reset to default" is not a way back to the chord. +# +# This is one of three layers. The other two are the polkit rule that +# makes the sleep states unavailable to the classroom user at all, and the +# D-Bus service file that leaves the greeter itself with nothing to start. + +[ksmserver] +# The chord itself. +Log Out[$i]=none,none,Show Logout Screen +# And every other way ksmserver offers to end the session. All of these +# ship unbound; they are written out so that a future Plasma release +# cannot introduce a default binding into a room without this file being +# updated too. +Log Out Without Confirmation[$i]=none,none,Log Out Without Confirmation +LogOut[$i]=none,none,Log Out +Halt Without Confirmation[$i]=none,none,Shut Down Without Confirmation +Reboot[$i]=none,none,Reboot +Reboot Without Confirmation[$i]=none,none,Reboot Without Confirmation +Shut Down[$i]=none,none,Shut Down + +[org_kde_powerdevil] +# Sleep and Hibernate are bound by default to the XF86Sleep and +# XF86Hibernate keys, which a classroom keyboard may well have. PowerDown +# and PowerOff are the same story for XF86PowerDown and XF86PowerOff. +# +# The power *button* is already ignored at two layers; these are the +# keyboard keys of the same name, which are a different path and were +# never covered. +Sleep[$i]=none,none,Suspend +Hibernate[$i]=none,none,Hibernate +PowerDown[$i]=none,none,Power Down +PowerOff[$i]=none,none,Power Off diff --git a/config/polkit/org.ekklesia.classroom.sleep.rules b/config/polkit/org.ekklesia.classroom.sleep.rules new file mode 100644 index 0000000..3c6fd31 --- /dev/null +++ b/config/polkit/org.ekklesia.classroom.sleep.rules @@ -0,0 +1,59 @@ +// The classroom user cannot put the room to sleep. +// +// The second layer under the shortcut that was removed from +// kglobalshortcutsrc, and the one that matters: a key can be unbound, but +// anything that can reach logind can still ask for a suspend. PowerDevil +// does, the logout greeter does, `systemctl suspend` from the maintenance +// terminal does, and a future Plasma applet would too. They all go through +// the same polkit actions, so this is the one place that answers all of +// them at once. +// +// Answering here rather than masking sleep.target is deliberate. Masking +// is machine-wide and would take suspend away from a technician's own +// account as well; worse, it would be invisible to Plasma, which would go +// on offering a Hibernate button that silently failed. logind is asked +// whether this *user* may sleep, so a NO here is both enforcement and the +// supported way to tell the desktop not to offer it: CanSuspend() and +// CanHibernate() come back "no" and Plasma stops drawing the buttons. +// +// Scoped to the classroom user by name. Every other account — a +// technician's, root's — is left entirely alone, and nothing here touches +// power-off or reboot for anybody. A tech standing at the machine, or on +// SSH, can still shut it down and start it up. +// +// To lift this for maintenance, drop a file of the same name into +// /etc/polkit-1/rules.d/, which polkit reads ahead of this one. + +polkit.addRule(function (action, subject) { + if (subject.user !== "classroom") { + return polkit.Result.NOT_HANDLED; + } + + // Every sleep state logind exposes, present and future. systemd maps + // HybridSleep() and SuspendThenHibernate() onto the hibernate + // actions today rather than giving them action ids of their own; the + // two spellings are matched anyway so that a systemd release which + // splits them out does not quietly reopen this. + // + // The -multiple-sessions and -ignore-inhibit variants are the same + // operation asked for in different circumstances, so the prefixes are + // matched rather than the exact ids. + var sleeping = [ + "org.freedesktop.login1.suspend", + "org.freedesktop.login1.hibernate", + "org.freedesktop.login1.hybrid-sleep", + "org.freedesktop.login1.suspend-then-hibernate", + ]; + + for (var i = 0; i < sleeping.length; i++) { + if (action.id.indexOf(sleeping[i]) === 0) { + polkit.log( + "ekklesia-classroom: refusing " + action.id + + " for the classroom user" + ); + return polkit.Result.NO; + } + } + + return polkit.Result.NOT_HANDLED; +}); diff --git a/docs/kiosk.md b/docs/kiosk.md new file mode 100644 index 0000000..196cd80 --- /dev/null +++ b/docs/kiosk.md @@ -0,0 +1,118 @@ +# Lockdown + +What a teacher can and cannot do to the machine, and where each of those +answers lives. + +Most of it is KDE's own Kiosk mechanism, in +`/etc/ekklesia-classroom/xdg/kdeglobals`: no shell, no command line, no +lock screen, no editable panel. That file is the first place to look, and +lifting a restriction for maintenance is a one-line edit there followed by +a session restart. + +This page is about the part that is *not* in that file: ending or +suspending the session. + +## The escape this closed + +A teacher pressed **Ctrl+Alt+Delete**, got Plasma's session screen, and +hibernated the classroom computer from it. A hibernated room cannot be +recovered by anybody standing in front of it who does not know where the +power button is, which on a Sunday morning is everybody. + +`[KDE Action Restrictions] logout=false` was already set and did not stop +it, and the reason is worth keeping written down: that restriction governs +**Plasma's own actions** — the entries plasmashell draws in a menu or an +applet. Ctrl+Alt+Delete is not one of them. It is a KGlobalAccel shortcut +registered by ksmserver, and pressing it asks D-Bus for the logout greeter +without ever consulting a Plasma action. A restriction on a menu cannot +see a key. + +## Three layers + +### 1. The key + +`/etc/ekklesia-classroom/kglobalshortcutsrc` unbinds every ksmserver +session action and PowerDevil's Sleep, Hibernate, PowerDown and PowerOff +keys, with each entry marked `[$i]` so nothing writes them back. + +It is **not** in `/etc/ekklesia-classroom/xdg` with the other settings, and +that is deliberate. `kglobalacceld` opens `kglobalshortcutsrc` as a +KConfig `SimpleConfig`: no cascade, no system directories. Measured — with +the file in the cascade, `kreadconfig6` reported the chord unbound while +the shortcut server went on answering Ctrl+Alt+Del. + +So `classroom-desktop-reset` copies the package's copy into +`~/.config/kglobalshortcutsrc` before the session's shortcut server +starts, exactly as it does with the panel. Replacing the whole file costs +nothing: KGlobalAccel puts every shortcut the file does not name back from +each component's own defaults, and the entries the package does name are +immutable, which is what stops it putting those back too. + +The room's own chords are registered from their desktop entries and are +unaffected — `converged-check` asserts both of them by key code after the +replace. + +### 2. The policy + +`/usr/share/polkit-1/rules.d/49-ekklesia-classroom-sleep.rules` refuses +`org.freedesktop.login1.suspend*` and `.hibernate*` **for the `classroom` +user only**. + +This is the layer that matters, because a key can be unbound and anything +that reaches logind can still ask. PowerDevil, the greeter, `systemctl +suspend` from the maintenance terminal and any future applet all go +through these actions. + +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. + +Refusing here rather than masking `sleep.target` is the point. Masking is +machine-wide and would take suspend away from a technician's account too; +worse, it would be invisible to Plasma, which would go on drawing a +Hibernate button that silently failed. logind asks polkit whether *this +user* may sleep, so a NO is both the enforcement and the supported way to +tell the desktop not to offer it — `CanSuspend()` and `CanHibernate()` +come back `no`. + +Nothing here touches power-off or reboot, and nothing here applies to any +other account. + +### 3. The greeter + +`/usr/local/share/dbus-1/services/org.kde.LogoutPrompt.service` shadows +plasma-workspace's own activation file with `Exec=/bin/false`, the same +mechanism the classroom already uses for the old notification daemon. + +Ctrl+Alt+Delete does not draw anything itself; it asks D-Bus to start +`ksmserver-logout-greeter`. Layer 1 removes the caller, this removes the +callee, so any other caller finds nothing to start either. + +## What is deliberately still there + +- **Screen locking stays disabled.** None of this touches + `kscreenlockerrc`, and `action/lock_screen=false` is unchanged. A + classroom must never present a password prompt. +- **The power button still does nothing** on a short press, at both the + logind and PowerDevil layers, and a long firmware hold still cuts the + power. That is the only recovery a person standing at a wedged machine + has. +- **Meta+Ctrl+Alt+Shift+K still opens the maintenance terminal**, and + Meta+Ctrl+Alt+Shift+D still opens the display dialog. +- **Reboot and shut down are not newly denied to anybody.** A teacher + asking for either already lands on an administrator's password prompt, + which they do not have; that behaviour is unchanged. A technician on SSH + or at the maintenance terminal, with their own account, can still do all + of it. + +## Lifting it + +For a technician who needs a classroom machine to suspend — a laptop on a +bench, say — drop a file of the same name into `/etc/polkit-1/rules.d/`, +which polkit reads ahead of the package's copy. Nothing needs rebuilding +and nothing needs reinstalling. + +To put the session screen back, remove the D-Bus shadow and the +`kglobalshortcutsrc` entries; both are package files, so an update will +restore them, which is the intended direction. diff --git a/scripts/classroom-desktop-reset b/scripts/classroom-desktop-reset index 96bac07..5e9a2ec 100755 --- a/scripts/classroom-desktop-reset +++ b/scripts/classroom-desktop-reset @@ -123,6 +123,37 @@ if [[ -f "$CONFIG_HOME/kdeglobals" ]]; then esac fi +# THE KEYS THAT END A SESSION. +# +# Ctrl+Alt+Delete opened Plasma's session screen in a real classroom and a +# teacher hibernated the machine from it. The chord is a KGlobalAccel +# shortcut, and KGlobalAccel is the one part of Plasma the classroom +# cannot reach through KConfig's cascade: kglobalacceld opens +# kglobalshortcutsrc as a SimpleConfig, so it reads this one file in the +# teacher's home and no system directory at all. Measured — with the +# settings in /etc/ekklesia-classroom/xdg, kreadconfig6 reported the chord +# unbound while the shortcut server went on answering Ctrl+Alt+Del. +# +# So the package's copy is put there directly, before the shortcut server +# starts, exactly as the panel is. Nothing of a teacher's is lost: there +# is no settings application in this room, and KGlobalAccel puts every +# shortcut that is not named in the file back from its own defaults when +# each component registers. The entries the package does name are marked +# immutable, which is what stops it putting those back too. +SHORTCUTS=/etc/ekklesia-classroom/kglobalshortcutsrc + +if [[ -r "$SHORTCUTS" ]]; then + if cmp -s "$SHORTCUTS" "$CONFIG_HOME/kglobalshortcutsrc"; then + say "session shortcuts already the package's" + else + install -m 600 "$SHORTCUTS" "$CONFIG_HOME/kglobalshortcutsrc" + say "session shortcuts replaced with the package's" + fi +else + say "WARNING: $SHORTCUTS is missing — Ctrl+Alt+Delete may open the" + say " session screen this room is not supposed to have" +fi + # The room's power behaviour is policy, not preference: never sleep, never # dim, never lock. PowerDevil loads an idle action when it finds a group # for it, and the merged view of this file is where such a group could diff --git a/systemd/user/ekklesia-classroom-desktop.service b/systemd/user/ekklesia-classroom-desktop.service index 0bebb9a..0449b99 100644 --- a/systemd/user/ekklesia-classroom-desktop.service +++ b/systemd/user/ekklesia-classroom-desktop.service @@ -4,6 +4,12 @@ Documentation=file:///usr/local/bin/classroom-desktop-reset # Before the shell, because the shell owns the file this removes. Editing # it underneath a running plasmashell was measured to destroy the panel. Before=plasma-plasmashell.service +# And before the shortcut server, for the same reason: kglobalacceld reads +# ~/.config/kglobalshortcutsrc once, at start, and this writes it. Both +# units are wanted by plasma-workspace.target, so without this line they +# would start together and a room could come up with Ctrl+Alt+Delete still +# bound to the session screen. +Before=plasma-kglobalaccel.service PartOf=graphical-session.target [Service] diff --git a/tools/build-release.sh b/tools/build-release.sh index 85ec0f7..8295886 100755 --- a/tools/build-release.sh +++ b/tools/build-release.sh @@ -225,6 +225,18 @@ install -Dm644 \ "$ROOT/config/environment/90-ekklesia-classroom.conf" \ "$STAGE/etc/environment.d/90-ekklesia-classroom.conf" +# The session shortcuts, and deliberately NOT in the xdg directory above. +# +# kglobalacceld reads ~/.config/kglobalshortcutsrc as a SimpleConfig: no +# cascade, no system directories. Measured — with this file in the +# cascade, kreadconfig6 reported the chord unbound while the shortcut +# server went on answering Ctrl+Alt+Del. classroom-desktop-reset copies +# this into the teacher's home before the shortcut server starts, the +# same way the panel is copied out of the package at every login. +install -Dm644 \ + "$ROOT/config/plasma/kglobalshortcutsrc" \ + "$STAGE/etc/ekklesia-classroom/kglobalshortcutsrc" + # The one that actually reaches a session started by a display manager. # startplasma sources this directory; PAM does not read environment.d, so # without this file none of the settings above are in the cascade at all @@ -310,6 +322,14 @@ install -Dm644 \ "$ROOT/config/dbus/fr.emersion.mako.service" \ "$STAGE/usr/local/share/dbus-1/services/fr.emersion.mako.service" +# Shadows Plasma's session screen. Ctrl+Alt+Delete asks D-Bus to start it; +# with this in front of plasma-workspace's own file there is nothing to +# start. See the file itself for why the unbound chord is not enough on +# its own. +install -Dm644 \ + "$ROOT/config/dbus/org.kde.LogoutPrompt.service" \ + "$STAGE/usr/local/share/dbus-1/services/org.kde.LogoutPrompt.service" + # Two marks the classroom draws itself, because nothing in Breeze says # either thing: a lesson, and fetching a child's grown-up. for icon in ekklesia-wonderink ekklesia-parent-call; do @@ -384,6 +404,13 @@ install -Dm644 \ "$ROOT/config/polkit/org.ekklesia.classroom.display.policy" \ "$STAGE/usr/share/polkit-1/actions/org.ekklesia.classroom.display.policy" +# The classroom user cannot suspend or hibernate the room. A vendor rule +# in /usr/share, not /etc, so a technician can override it by dropping a +# file of the same name into /etc/polkit-1/rules.d. +install -Dm644 \ + "$ROOT/config/polkit/org.ekklesia.classroom.sleep.rules" \ + "$STAGE/usr/share/polkit-1/rules.d/49-ekklesia-classroom-sleep.rules" + install -Dm644 \ "$ROOT/config/plasma/autostart/ekklesia-classroom-scale.desktop" \ "$STAGE/etc/xdg/autostart/ekklesia-classroom-scale.desktop" diff --git a/tools/lab/converged-check b/tools/lab/converged-check index e02544f..f99eef9 100755 --- a/tools/lab/converged-check +++ b/tools/lab/converged-check @@ -48,6 +48,60 @@ notification_owner() { [[ -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 \ @@ -145,6 +199,64 @@ check "logind ignores a long press" ignore "$(logind_property HandlePowerKeyLong 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")"