pi-polkit 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.1] - 2026-11-04
9
+
10
+ ### Added
11
+
12
+ - `polkit-shims.ts`: `su` stand-in. `su -c COMMAND [USER]` (also `-cCOMMAND`,
13
+ `--command COMMAND`, `--command=COMMAND`) becomes
14
+ `pkexec --keep-cwd --user USER sh -c COMMAND`, because a non-login `su`
15
+ keeps the working directory and runs its command through a shell. Every other
16
+ form is refused with status `64` and one line naming the alternative: `su`,
17
+ `su -`, `su -l` (login shell → `pkexec bash`), `-m`/`-p`/`-w` (pkexec
18
+ sanitizes the environment), `-s` (name the shell in the command), `-g`/`-G`
19
+ (pkexec cannot switch groups), `--pty` (no terminal), `-f` and
20
+ `--session-command`.
21
+ - README scope statement: the extension covers elevation → polkit only;
22
+ ssh, gpg/pinentry, keyrings, database clients, `kinit` and disk encryption
23
+ are explicitly out of scope and need an askpass helper or a PTY.
24
+
25
+ ### Changed
26
+
27
+ - README authorisation note: a desktop agent may reuse the password it already
28
+ holds, so a second command can run without a dialog; that cache belongs to
29
+ the agent and must not be relied on. `su -c … USER` authenticates an
30
+ administrator rather than `USER`, and runs the command with `sh -c`.
31
+
8
32
  ## [0.1.0] - 2026-11-01
9
33
 
10
34
  ### Added
package/README.md CHANGED
@@ -1,10 +1,24 @@
1
1
  # pi-polkit
2
2
 
3
- Redirects `sudo` and `doas` to `pkexec` inside Pi, so authorisation
3
+ Redirects `sudo`, `doas` and `su -c` to `pkexec` inside Pi, so authorisation
4
4
  goes through the desktop's polkit agent (KDE, GNOME, …) instead of a terminal
5
5
  the shell tools do not have. Applies to the `bash` tool and to `!` user
6
6
  commands; `sudo` in other terminals is unchanged.
7
7
 
8
+ ## Scope
9
+
10
+ This extension covers one category: **elevation → polkit**. It exists because
11
+ the `bash` tool has no terminal, so any program that reads a password from
12
+ `/dev/tty` fails there. `sudo`, `doas` and `su -c` are that category, and they
13
+ are routed to `pkexec`, which asks the desktop's polkit agent instead.
14
+
15
+ Password prompts that are not polkit's business are out of scope: `ssh`,
16
+ `ssh-add`, `gpg`/pinentry, keyrings (`secret-tool`, KWallet), database clients
17
+ (`psql`, `mysql -p`, `redis-cli`), `kinit`, and disk encryption (`cryptsetup`,
18
+ `fscrypt`, `gocryptfs`). They read the process's own terminal, so they need an
19
+ askpass helper or a pseudo-terminal, not polkit; mixing them in would make this
20
+ extension's boundary meaningless.
21
+
8
22
  ## Installation
9
23
 
10
24
  Install as a Pi package (published on npm):
@@ -35,14 +49,18 @@ pi -e ./pi-polkit
35
49
  | `sudo -n …` | refused: pkexec has no non-interactive mode |
36
50
  | `sudo -i`, `-s`, `-b` | refused, with the equivalent command |
37
51
  | `sudoedit`, `visudo` | refused, with the alternative |
52
+ | `su -c 'id'` | dialog, then runs as root |
53
+ | `su -c 'id' bob` | as above, as `bob` (administrator password) |
54
+ | `su`, `su -`, `su -l` | refused: no interactive shell, no terminal |
55
+ | `su -m`, `-s`, `-g`, `-w`, `--pty` | refused, with the alternative |
38
56
  | `pkexec …` | passed through |
39
57
 
40
58
  Refused invocations exit with status 64 and one line naming the alternative.
41
59
 
42
60
  ## How it works
43
61
 
44
- `sudo` and `doas` stand-ins are placed first on `PATH`, so the shell resolves
45
- those instead of the system binaries, and they forward the command to
62
+ `sudo`, `doas` and `su` stand-ins are placed first on `PATH`, so the shell
63
+ resolves those instead of the system binaries, and they forward the command to
46
64
  `pkexec`. Command text is never rewritten, so quoting, pipes and subshells need
47
65
  no special handling.
48
66
 
@@ -63,11 +81,21 @@ The shim directory is created lazily in `$XDG_RUNTIME_DIR` (falling back to
63
81
  one; other actions may not, according to policy. The extension takes no part in
64
82
  that decision.
65
83
 
84
+ `org.freedesktop.policykit.exec` asks for administrator authentication on every
85
+ call, but the desktop agent may keep the password it just received and answer
86
+ the next challenge itself (KDE's agent does). A second command in the same
87
+ session then runs without a dialog. That cache belongs to the agent: another
88
+ agent, a restarted agent or a dismissed dialog means the next command prompts
89
+ again, so do not rely on it.
90
+
66
91
  ## Limitations
67
92
 
68
93
  - Absolute paths (`/usr/bin/sudo`) and relative ones (`./sudo`) bypass `PATH`.
69
94
  - Commands that reset `PATH` (`env -i`, `PATH=/usr/bin …`) bypass it as well.
70
95
  - Programs that invoke `sudo` with a scrubbed environment bypass it.
96
+ - `su -c … USER` authenticates an administrator (polkit), not `USER`, so an
97
+ account that real `su` would allow can be refused here.
98
+ - `su -c` runs the command with `sh -c`, not with the target user's login shell.
71
99
  - PowerShell is not handled; the extension targets Linux only.
72
100
 
73
101
  These bypass the redirect, not polkit, which still decides whether the command
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-polkit",
3
- "version": "0.1.0",
4
- "description": "Redirect sudo/doas to pkexec in Pi via PATH shims, so the desktop polkit agent handles authentication",
3
+ "version": "0.1.1",
4
+ "description": "Redirect sudo, doas and su to pkexec in Pi via PATH shims, so the desktop polkit agent handles authentication",
5
5
  "type": "module",
6
6
  "pi": {
7
7
  "extensions": ["./index.ts"]
@@ -27,7 +27,7 @@
27
27
  "engines": {
28
28
  "node": ">=22.19.0"
29
29
  },
30
- "keywords": ["pi-package", "pi-extension", "sudo", "doas", "pkexec", "polkit", "security"],
30
+ "keywords": ["pi-package", "pi-extension", "sudo", "doas", "su", "pkexec", "polkit", "security"],
31
31
  "license": "MIT",
32
32
  "repository": {
33
33
  "type": "git",
@@ -3,13 +3,13 @@ import { createBashToolDefinition, createLocalBashOperations } from "@earendil-w
3
3
  import { polkitShimsPathExportLine, removePolkitShims, withPolkitShimsOnPath } from "./polkit-shims"
4
4
 
5
5
  /**
6
- * Redirects sudo and doas to pkexec inside Pi, so authorisation goes
6
+ * Redirects sudo, doas and su -c to pkexec inside Pi, so authorisation goes
7
7
  * through the desktop's polkit agent (KDE, GNOME, …) instead of a terminal the
8
8
  * shell tools do not have. Applies to the `bash` tool and to `!` user
9
9
  * commands; `sudo` in other terminals is unchanged.
10
10
  *
11
11
  * The redirect is a PATH prepend, not a command rewrite: the shims in
12
- * polkit-shims.ts stand in for the sudo/doas binaries, and the shell's own
12
+ * polkit-shims.ts stand in for the sudo/doas/su binaries, and the shell's own
13
13
  * name resolution picks them up for `sudo`, `$(sudo …)`, `xargs sudo`,
14
14
  * `FOO=1 sudo` and nested scripts alike. Nothing parses the command string,
15
15
  * and nothing parses pkexec's output: the shims hand pkexec's exit status
@@ -32,7 +32,7 @@ export default function polkitRedirect(pi: ExtensionAPI): void {
32
32
  }
33
33
 
34
34
  /**
35
- * Local shell operations whose commands resolve sudo through the shims. The
35
+ * Local shell operations whose commands resolve sudo and su through the shims. The
36
36
  * `!` user commands run through pi's own bash backend with no prepared
37
37
  * environment, so the PATH prepend happens as the first command line; the
38
38
  * exit status and output of the command itself are unchanged.
package/polkit-shims.ts CHANGED
@@ -174,6 +174,113 @@ fi
174
174
  run_pkexec "$@"
175
175
  `
176
176
 
177
+ /**
178
+ * The su shim. `su -c` is the only form pkexec can express exactly: a command
179
+ * run as the target user through a shell, in the current directory. Every
180
+ * other form asks for a login shell, an environment, a shell binary, a group
181
+ * or a terminal, none of which pkexec can honour, so it is refused instead.
182
+ */
183
+ const SU_SHIM = `#!/bin/sh
184
+ # pi-polkit: su shim. Routes su -c to pkexec so polkit can authorize it.
185
+ # Generated at extension startup; do not edit.
186
+
187
+ fail() {
188
+ printf 'pi-polkit: %s\\n' "$1" >&2
189
+ exit ${SHIM_USAGE_EXIT}
190
+ }
191
+
192
+ command -v pkexec >/dev/null 2>&1 || fail 'pkexec is not installed; install polkit'
193
+
194
+ user=
195
+ cmd=
196
+ has_cmd=
197
+ while [ $# -gt 0 ]; do
198
+ case $1 in
199
+ --)
200
+ shift
201
+ break
202
+ ;;
203
+ -c)
204
+ [ $# -ge 2 ] || fail 'su -c needs a command: su -c COMMAND [USER]'
205
+ cmd=$2
206
+ has_cmd=1
207
+ shift 2
208
+ ;;
209
+ -c?*)
210
+ cmd=\${1#-c}
211
+ has_cmd=1
212
+ shift
213
+ ;;
214
+ --command)
215
+ [ $# -ge 2 ] || fail 'su --command needs a command: su --command COMMAND [USER]'
216
+ cmd=$2
217
+ has_cmd=1
218
+ shift 2
219
+ ;;
220
+ --command=*)
221
+ cmd=\${1#--command=}
222
+ has_cmd=1
223
+ shift
224
+ ;;
225
+ - | -l | --login)
226
+ fail 'su starts a login shell; run pkexec bash instead.'
227
+ ;;
228
+ -s | --shell | -s?* | --shell=*)
229
+ fail 'su -s picks the shell; name the shell in the command: pkexec sh -c ...'
230
+ ;;
231
+ -m | -p | --preserve-environment)
232
+ fail 'pkexec deliberately sanitizes the environment; drop -m/-p.'
233
+ ;;
234
+ -g | -G | --group | --supp-group | -g?* | -G?* | --group=* | --supp-group=*)
235
+ fail 'su -g/-G switches groups; pkexec can only switch users.'
236
+ ;;
237
+ -w | --whitelist-environment | -w?* | --whitelist-environment=*)
238
+ fail 'su -w whitelists environment variables; pkexec sanitizes the environment.'
239
+ ;;
240
+ -P | --pty)
241
+ fail 'su --pty asks for a terminal; the bash tool has none.'
242
+ ;;
243
+ -f | --fast)
244
+ fail 'su -f is a shell flag; pass it to the shell in the command instead.'
245
+ ;;
246
+ --session-command | --session-command=*)
247
+ fail 'su --session-command keeps the session; pkexec always starts a new process.'
248
+ ;;
249
+ -h | --help | -V | --version)
250
+ fail "option has no pkexec equivalent: $1"
251
+ ;;
252
+ -?*)
253
+ fail "unknown su option: $1"
254
+ ;;
255
+ *)
256
+ [ -z "$user" ] || fail 'su passes arguments after the user name to the shell; use su -c COMMAND USER.'
257
+ user=$1
258
+ shift
259
+ ;;
260
+ esac
261
+ done
262
+
263
+ # Operands after -- are the same user-then-nothing sequence.
264
+ while [ $# -gt 0 ]; do
265
+ [ -z "$user" ] || fail 'su passes arguments after the user name to the shell; use su -c COMMAND USER.'
266
+ user=$1
267
+ shift
268
+ done
269
+
270
+ [ -n "$user" ] || user=root
271
+ [ -n "$has_cmd" ] || fail 'su without -c opens an interactive shell; run pkexec bash instead.'
272
+
273
+ # su keeps the working directory unless it is a login shell, and --keep-cwd is
274
+ # how pkexec does the same. pkexec is silent when the dialog is dismissed
275
+ # (status 126), so say what the status means.
276
+ pkexec --keep-cwd --user "$user" sh -c "$cmd"
277
+ status=$?
278
+ if [ $status -eq 126 ]; then
279
+ printf 'pi-polkit: %s\\n' 'the polkit authentication dialog was dismissed' >&2
280
+ fi
281
+ exit $status
282
+ `
283
+
177
284
  /** sudoedit edits through sudo's own temporary-file flow; polkit has none. */
178
285
  const SUDOEDIT_SHIM = `#!/bin/sh
179
286
  printf 'pi-polkit: sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.\\n' >&2
@@ -190,6 +297,7 @@ exit ${SHIM_USAGE_EXIT}
190
297
  const POLKIT_SHIM_SCRIPTS: Record<string, string> = {
191
298
  sudo: SUDO_SHIM,
192
299
  doas: DOAS_SHIM,
300
+ su: SU_SHIM,
193
301
  sudoedit: SUDOEDIT_SHIM,
194
302
  visudo: VISUDO_SHIM,
195
303
  }
@@ -198,8 +306,8 @@ const POLKIT_SHIM_SCRIPTS: Record<string, string> = {
198
306
  let shimDirectory: string | undefined
199
307
 
200
308
  /**
201
- * Returns the shim directory, writing the sudo/doas stand-in scripts on first
202
- * use. Creation is lazy so factory-only invocations (runs that never start a
309
+ * Returns the shim directory, writing the stand-in scripts on first use.
310
+ * Creation is lazy so factory-only invocations (runs that never start a
203
311
  * session) leave nothing behind.
204
312
  */
205
313
  export function ensurePolkitShims(): string {
@@ -219,7 +327,7 @@ export function removePolkitShims(): void {
219
327
 
220
328
  /**
221
329
  * Returns the environment with the shim directory first on PATH, so the shell
222
- * resolves sudo and doas to the shims instead of the system binaries. The
330
+ * resolves sudo, doas and su to the shims instead of the system binaries. The
223
331
  * prepend is idempotent: an environment that already carries the prefix is
224
332
  * returned unchanged.
225
333
  */