Files
Classroom_Desktop/config/polkit/org.ekklesia.classroom.sleep.rules
ekklesiaandClaude Opus 5 23dff0b7b4 feat: close the chord that let a teacher hibernate the room
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 <[email protected]>
2026-09-19 20:50:36 -05:00

60 lines
2.6 KiB
Plaintext

// 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;
});