WIP
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import qs.HyprChrome.Theme
|
||||
import qs.HyprChrome.Widgets.Polkit
|
||||
|
||||
// GUI password prompt for `sudo -A`, reusing the polkit dialog.
|
||||
//
|
||||
// sudo does NOT speak polkit — it is setuid + PAM reading your tty, and no
|
||||
// sudoers option bridges the two. What it does support is an ASKPASS helper: a
|
||||
// program it runs to obtain the password, which prints it on stdout. So this is
|
||||
// not the polkit agent serving sudo; it is a second, separate path that happens
|
||||
// to render the same dialog.
|
||||
//
|
||||
// Flow, driven by the helper in home.nix (`qs-askpass`):
|
||||
//
|
||||
// sudo -A
|
||||
// -> qs-askpass makes a 0600 fifo under XDG_RUNTIME_DIR
|
||||
// -> qs ipc call askpass prompt "<prompt>" "<fifo>" (returns at once)
|
||||
// -> this dialog opens, user types
|
||||
// -> a one-line writer is started here, secret written to its STDIN
|
||||
// -> qs-askpass reads the fifo and prints the secret on stdout
|
||||
// -> sudo reads it
|
||||
//
|
||||
// The secret travels on a pipe the whole way. It is never an argument and never
|
||||
// an environment variable, so it does not appear in /proc for any process — the
|
||||
// fifo PATH is in argv, which is not secret. It does cross more process
|
||||
// boundaries than the polkit path, where the password stays inside the PAM
|
||||
// conversation; that is the inherent cost of askpass, not of this design.
|
||||
//
|
||||
// Cancelling answers with an empty line, so the helper reads nothing, exits
|
||||
// non-zero, and sudo aborts rather than burning a retry on a blank password.
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
// Which output to appear on; the shell puts it on the focused monitor.
|
||||
property var screen: null
|
||||
|
||||
// The fifo the helper is blocked reading. Non-empty means a request is in
|
||||
// flight, which is exactly what "a prompt is open" means here.
|
||||
property string fifoPath: ""
|
||||
property string promptText: ""
|
||||
property bool failed: false
|
||||
|
||||
readonly property bool active: root.fifoPath !== ""
|
||||
|
||||
// Held only between submit and the writer process actually starting: a
|
||||
// Process cannot be written to before it is running.
|
||||
property string pendingSecret: ""
|
||||
|
||||
IpcHandler {
|
||||
target: "askpass"
|
||||
|
||||
// Called by qs-askpass. Returns immediately — the helper blocks on the
|
||||
// fifo, not on this call, because an IpcHandler function runs on the
|
||||
// QML thread and blocking here would freeze the whole shell.
|
||||
function prompt(message: string, fifo: string): string {
|
||||
if (root.active)
|
||||
return "busy";
|
||||
|
||||
root.promptText = message === "" ? "Password:" : message;
|
||||
root.fifoPath = fifo;
|
||||
root.failed = false;
|
||||
return "ok";
|
||||
}
|
||||
|
||||
// So a helper that times out can take the dialog down with it rather
|
||||
// than leaving it on screen with nothing listening.
|
||||
function cancel(): string {
|
||||
root.dismiss();
|
||||
return "ok";
|
||||
}
|
||||
}
|
||||
|
||||
// Cancelling answers with an EMPTY line rather than by closing silently:
|
||||
// the helper then reads zero bytes and exits non-zero, so sudo aborts
|
||||
// instead of spending a retry on a blank password.
|
||||
function dismiss() {
|
||||
root.respond("");
|
||||
}
|
||||
|
||||
function submit(secret) {
|
||||
root.respond(secret);
|
||||
}
|
||||
|
||||
// The writer reads ONE LINE and exits; it does not wait for EOF.
|
||||
//
|
||||
// The obvious version — `cat > fifo`, write the secret, then close stdin by
|
||||
// setting stdinEnabled false — does not terminate. Measured: the secret
|
||||
// arrives intact but `cat` never sees EOF, so the fifo is never closed and
|
||||
// the helper blocks until its timeout. sudo would hang after you typed.
|
||||
//
|
||||
// A single `read` needs no EOF at all: the trailing newline ends it, the
|
||||
// shell writes what it got and exits, and THAT close is what gives the
|
||||
// helper its EOF. `IFS=` keeps leading and trailing whitespace, `-r` keeps
|
||||
// backslashes, and the secret still travels on stdin rather than in argv.
|
||||
function respond(secret) {
|
||||
if (!root.active)
|
||||
return;
|
||||
|
||||
root.pendingSecret = secret + "\n";
|
||||
writer.command = ["sh", "-c", "IFS= read -r line; printf %s \"$line\" > \"$1\"", "sh", root.fifoPath];
|
||||
writer.running = true;
|
||||
root.fifoPath = "";
|
||||
}
|
||||
|
||||
// Opening a fifo for writing BLOCKS until a reader attaches, which is why
|
||||
// this is a subprocess rather than a FileView: the helper's `cat` is that
|
||||
// reader, and blocking the QML thread on it would freeze the shell.
|
||||
Process {
|
||||
id: writer
|
||||
|
||||
stdinEnabled: true
|
||||
|
||||
// Written on `started`, not at respond() time: a Process has no stdin
|
||||
// to write to until it is actually running.
|
||||
onStarted: {
|
||||
writer.write(root.pendingSecret);
|
||||
root.pendingSecret = "";
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: win
|
||||
|
||||
screen: root.screen
|
||||
visible: root.active
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
color: Theme.textAlpha(0)
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
// No click-off dismissal, for the same reason the polkit dialog has
|
||||
// none: something is blocked waiting on the answer, and losing it to a
|
||||
// stray click would leave sudo hanging with no visible cause.
|
||||
|
||||
PolkitPromptContent {
|
||||
id: content
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: Math.max(32, Math.round(parent.height / 3 - height / 2))
|
||||
width: 520
|
||||
|
||||
// Deliberately the polkit dialog's own content component: this is a
|
||||
// password prompt with the same shape, and keeping one means a
|
||||
// restyle of PolkitPanel covers both. `identities` stays empty —
|
||||
// sudo offers no choice of who authenticates — which hides the
|
||||
// picker and the "AS" line on its own.
|
||||
message: "Authentication is required to run a command as another user"
|
||||
actionId: "sudo"
|
||||
iconName: ""
|
||||
showIcon: false
|
||||
identities: []
|
||||
|
||||
responseRequired: true
|
||||
inputPrompt: root.promptText
|
||||
responseVisible: false
|
||||
failed: root.failed
|
||||
|
||||
onSubmitted: value => root.submit(value)
|
||||
onCancelled: root.dismiss()
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (win.visible) {
|
||||
content.clearResponse();
|
||||
content.focusInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user