Files
Panama/config/dot/quickshell/services/Polkit.qml
T
Gabriel Brown 536958430f Fix the polkit unit's ignored rate limit
systemd logged "Unknown key 'StartLimitIntervalSec'" on every start:
rate limiting belongs in [Unit], not [Service], so both keys were
ignored and the restart limit they were meant to impose did not exist.
An agent that failed repeatedly would have flapped rather than stopping.

Found while investigating reported prompt failures, which turned out not
to be a defect: the trace showed both authentications succeeding on the
first attempt. The failures came from testing -- prompts raised by
background pkexec runs and then cancelled seconds later, while someone
was trying to type into them.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-19 19:54:16 -04:00

200 lines
7.0 KiB
QML

pragma Singleton
// Panama's authentication prompt -- the half that handles the password.
//
// The split with scripts/panama-polkit-agent is the security design, not an
// accident of implementation:
//
// * The agent talks to polkitd and hands this a request FILE containing the
// action, the message, who may answer, and a one-time cookie. No password
// ever reaches it.
// * This draws the prompt, and on submit spawns the setuid
// polkit-agent-helper-1 and writes the password to that helper's stdin. The
// helper performs the PAM conversation and reports to polkitd itself.
//
// So the password lives in this process and the helper's stdin, and nowhere
// else. It is never an argument -- argv is world-readable through /proc -- and
// it is cleared the moment it has been handed over.
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
readonly property string helperBinary: "/usr/lib/polkit-1/polkit-agent-helper-1"
// The request currently on screen, or null.
property var request: null
property string requestPath: ""
property string message: ""
property string actionId: ""
property var users: []
property string chosenUser: ""
property bool authenticating: false
property string failureText: ""
// How many times a wrong password has been offered for this request.
property int attempts: 0
readonly property bool active: root.request !== null
// Held only between pressing Enter and the helper accepting it on stdin.
property string pendingSecret: ""
function begin(path: string): void {
// A second request while one is open would leave the first
// unanswerable; polkit serializes these in practice, and refusing is
// safer than stacking prompts.
if (root.active) {
console.warn("Polkit: a prompt is already open; ignoring", path);
return;
}
root.requestPath = path;
requestFile.path = path;
requestFile.reload();
}
function adopt(text: string): void {
try {
const parsed = JSON.parse(text);
root.request = parsed;
root.actionId = String(parsed.actionId ?? "");
root.message = String(parsed.message ?? "Authentication is required");
root.users = Array.isArray(parsed.users) ? parsed.users : [];
root.chosenUser = root.users.includes(String(parsed.preferred ?? ""))
? String(parsed.preferred)
: (root.users.length > 0 ? String(root.users[0]) : "");
root.attempts = 0;
root.failureText = "";
} catch (error) {
console.warn("Polkit: could not read the request:", error);
root.dismiss("failed");
}
}
function submit(password: string): void {
if (!root.active || root.authenticating || root.chosenUser === "")
return;
root.authenticating = true;
root.failureText = "";
root.pendingSecret = password;
helper.command = [root.helperBinary, root.chosenUser];
helper.running = true;
}
// Cancelled by the person at the keyboard, or withdrawn by whatever asked.
// Both close the prompt; only the first is something they did.
function cancel(): void {
root.dismiss("cancelled");
}
// Writes the outcome where the agent is watching for it, then forgets
// everything about the request.
function dismiss(result: string): void {
if (root.requestPath !== "") {
// umask first: everything in this directory concerns one
// authentication attempt, and a file written with the default mask
// would be world-readable in a directory whose whole point is that
// it is not.
answer.command = ["sh", "-c",
"umask 077; printf '%s' " + JSON.stringify(JSON.stringify({ result: result }))
+ " > " + JSON.stringify(root.responsePathFor(root.requestPath))];
answer.running = true;
}
root.request = null;
root.requestPath = "";
root.message = "";
root.actionId = "";
root.users = [];
root.chosenUser = "";
root.attempts = 0;
root.failureText = "";
root.pendingSecret = "";
root.authenticating = false;
}
function responsePathFor(path: string): string {
return String(path).replace(/\.json$/, "") + ".response";
}
FileView {
id: requestFile
onLoaded: root.adopt(this.text())
onLoadFailed: {
console.warn("Polkit: the request file could not be read");
root.dismiss("failed");
}
}
// Watches for polkit withdrawing the request while the prompt is open --
// the caller gave up, or another agent answered it.
Timer {
running: root.active
interval: 500
repeat: true
onTriggered: cancelledCheck.running = true
}
Process {
id: cancelledCheck
command: ["test", "-e", root.requestPath.replace(/\.json$/, "") + ".cancelled"]
onExited: (code) => { if (code === 0 && root.active) root.dismiss("cancelled"); }
}
Process {
id: helper
stdinEnabled: true
onStarted: {
// The cookie first, then the password when the helper asks for it.
// Both go to stdin; neither is ever an argument.
helper.write(String(root.request?.cookie ?? "") + "\n");
}
stdout: SplitParser {
splitMarker: "\n"
onRead: line => {
const text = String(line);
if (text.startsWith("PAM_PROMPT_ECHO_OFF")
|| text.startsWith("PAM_PROMPT_ECHO_ON")) {
helper.write(root.pendingSecret + "\n");
// Gone from this process the instant it has been handed
// over; the helper owns it from here.
root.pendingSecret = "";
return;
}
if (text.startsWith("PAM_ERROR_MSG")) {
const detail = text.slice("PAM_ERROR_MSG".length).trim();
if (detail !== "")
root.failureText = detail;
return;
}
if (text.startsWith("SUCCESS")) {
root.dismiss("ok");
return;
}
if (text.startsWith("FAILURE")) {
root.attempts += 1;
if (root.failureText === "")
root.failureText = "That password was not accepted.";
// Three tries, then the request is failed rather than left
// open forever -- polkit's own agents behave the same way.
if (root.attempts >= 3)
root.dismiss("failed");
}
}
}
onExited: {
root.authenticating = false;
root.pendingSecret = "";
helper.stdinEnabled = true;
}
}
Process { id: answer }
}