pi-polkit 0.0.0-stage → 0.1.0
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 +36 -0
- package/LICENSE +21 -0
- package/README.md +98 -2
- package/index.ts +1 -0
- package/package.json +37 -4
- package/polkit-redirect.ts +45 -0
- package/polkit-shims.ts +263 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-11-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `polkit-shims.ts`: `sudo`, `doas`, `sudoedit` and `visudo` scripts written to
|
|
13
|
+
a private `0700` directory under `$XDG_RUNTIME_DIR` (falling back to `/tmp`)
|
|
14
|
+
on first use and removed at session shutdown. `sudo` and `doas` forward to
|
|
15
|
+
`pkexec`; `sudoedit` and `visudo` refuse, because polkit has no equivalent
|
|
16
|
+
flow.
|
|
17
|
+
- Deterministic option contract in the shims: `-u USER`, `-uUSER`, `--user USER`
|
|
18
|
+
and `--user=USER` become `pkexec --user USER`, `--` ends option parsing, and
|
|
19
|
+
every other option is refused with exit status `64` and a message naming the
|
|
20
|
+
alternative (`-n` → polkit decides, `-i`/`-s` → `pkexec bash`, `-E`/`-H`/`-P`
|
|
21
|
+
→ pkexec sanitizes the environment, `-b` → append `&`). `pkexec` would
|
|
22
|
+
otherwise reject them with `Cannot run program`, which says nothing about what
|
|
23
|
+
to do next.
|
|
24
|
+
- `polkit-redirect.ts`: the Pi extension factory. The `bash` tool is
|
|
25
|
+
replaced by the built-in's own definition with a
|
|
26
|
+
`spawnHook` that prepends the shim directory to `PATH`, and `!` user commands
|
|
27
|
+
run through `createLocalBashOperations` with the same prepend as their first
|
|
28
|
+
command line. Command text is never rewritten.
|
|
29
|
+
- Authentication outcomes are reported from exit status alone (`126` dismissed,
|
|
30
|
+
`127` not authorized, `64` refused invocation); the shims print one line
|
|
31
|
+
explaining status `126`, the only pkexec failure path that is silent.
|
|
32
|
+
- `SHIM_USAGE_EXIT` (64) documents that the refusal status is the shim's own
|
|
33
|
+
and cannot be confused with pkexec's `126`/`127`.
|
|
34
|
+
- Engineering scaffolding: `npm run check` type-checks against the pinned
|
|
35
|
+
`@earendil-works/pi-coding-agent` 1.4.0, and GitHub Actions CI runs it on
|
|
36
|
+
every push and pull request.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pi-polkit contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,99 @@
|
|
|
1
|
-
#
|
|
1
|
+
# pi-polkit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Redirects `sudo` and `doas` to `pkexec` inside Pi, so authorisation
|
|
4
|
+
goes through the desktop's polkit agent (KDE, GNOME, …) instead of a terminal
|
|
5
|
+
the shell tools do not have. Applies to the `bash` tool and to `!` user
|
|
6
|
+
commands; `sudo` in other terminals is unchanged.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
Install as a Pi package (published on npm):
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pi install npm:pi-polkit
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or copy or symlink this directory into Pi's extension location:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
cp -r pi-polkit ~/.pi/agent/extensions/pi-polkit
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
For a quick test without installing, pass the directory to Pi:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pi -e ./pi-polkit
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Behavior
|
|
29
|
+
|
|
30
|
+
| Command | Result |
|
|
31
|
+
|------------------------|---------------------------------------------|
|
|
32
|
+
| `sudo apt install x` | polkit dialog, then runs as root |
|
|
33
|
+
| `sudo -u bob …` | as above, as `bob` |
|
|
34
|
+
| `cat x \| sudo tee y` | dialog for `tee` |
|
|
35
|
+
| `sudo -n …` | refused: pkexec has no non-interactive mode |
|
|
36
|
+
| `sudo -i`, `-s`, `-b` | refused, with the equivalent command |
|
|
37
|
+
| `sudoedit`, `visudo` | refused, with the alternative |
|
|
38
|
+
| `pkexec …` | passed through |
|
|
39
|
+
|
|
40
|
+
Refused invocations exit with status 64 and one line naming the alternative.
|
|
41
|
+
|
|
42
|
+
## How it works
|
|
43
|
+
|
|
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
|
|
46
|
+
`pkexec`. Command text is never rewritten, so quoting, pipes and subshells need
|
|
47
|
+
no special handling.
|
|
48
|
+
|
|
49
|
+
Two wiring paths put the stand-ins on `PATH`, one per execution form:
|
|
50
|
+
|
|
51
|
+
- The `bash` tool is replaced by the built-in's own definition with a
|
|
52
|
+
`spawnHook` that prepends the shim directory to the spawned environment.
|
|
53
|
+
- `!` user commands run through Pi's local bash backend with no prepared
|
|
54
|
+
environment, so their wrapper prepends `export PATH=…` as the first command
|
|
55
|
+
line instead.
|
|
56
|
+
|
|
57
|
+
The shim directory is created lazily in `$XDG_RUNTIME_DIR` (falling back to
|
|
58
|
+
`/tmp`) as `0700` and removed at session shutdown.
|
|
59
|
+
|
|
60
|
+
## Authorisation
|
|
61
|
+
|
|
62
|
+
`polkitd` decides whether a password is required. These commands always require
|
|
63
|
+
one; other actions may not, according to policy. The extension takes no part in
|
|
64
|
+
that decision.
|
|
65
|
+
|
|
66
|
+
## Limitations
|
|
67
|
+
|
|
68
|
+
- Absolute paths (`/usr/bin/sudo`) and relative ones (`./sudo`) bypass `PATH`.
|
|
69
|
+
- Commands that reset `PATH` (`env -i`, `PATH=/usr/bin …`) bypass it as well.
|
|
70
|
+
- Programs that invoke `sudo` with a scrubbed environment bypass it.
|
|
71
|
+
- PowerShell is not handled; the extension targets Linux only.
|
|
72
|
+
|
|
73
|
+
These bypass the redirect, not polkit, which still decides whether the command
|
|
74
|
+
runs.
|
|
75
|
+
|
|
76
|
+
## Requirements
|
|
77
|
+
|
|
78
|
+
`polkit` and a registered authentication agent. Desktop environments start one;
|
|
79
|
+
without one, `pkexec` fails instead of prompting.
|
|
80
|
+
|
|
81
|
+
## Exit status
|
|
82
|
+
|
|
83
|
+
| Status | Meaning |
|
|
84
|
+
|--------|----------------------------------------------|
|
|
85
|
+
| 126 | the polkit dialog was dismissed |
|
|
86
|
+
| 127 | not authorized, or the program could not run |
|
|
87
|
+
| 64 | the extension refused the invocation |
|
|
88
|
+
|
|
89
|
+
Messages are in English.
|
|
90
|
+
|
|
91
|
+
## Development
|
|
92
|
+
|
|
93
|
+
`npm install && npm run check` type-checks against the pinned
|
|
94
|
+
`@earendil-works/pi-coding-agent` 1.4.0, the API Pi serves to Node
|
|
95
|
+
extensions.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./polkit-redirect"
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-polkit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Redirect sudo/doas to pkexec in Pi via PATH shims, so the desktop polkit agent handles authentication",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"pi": {
|
|
7
|
+
"extensions": ["./index.ts"]
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.ts",
|
|
11
|
+
"polkit-redirect.ts",
|
|
12
|
+
"polkit-shims.ts",
|
|
13
|
+
"CHANGELOG.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
17
|
+
"prepublishOnly": "npm run check"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@earendil-works/pi-coding-agent": "1.4.0",
|
|
24
|
+
"@types/node": "^24.0.0",
|
|
25
|
+
"typescript": "^5.5.0"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22.19.0"
|
|
29
|
+
},
|
|
30
|
+
"keywords": ["pi-package", "pi-extension", "sudo", "doas", "pkexec", "polkit", "security"],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/lenitain/pi-polkit.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/lenitain/pi-polkit/issues"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { BashOperations, ExtensionAPI } from "@earendil-works/pi-coding-agent"
|
|
2
|
+
import { createBashToolDefinition, createLocalBashOperations } from "@earendil-works/pi-coding-agent"
|
|
3
|
+
import { polkitShimsPathExportLine, removePolkitShims, withPolkitShimsOnPath } from "./polkit-shims"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Redirects sudo and doas to pkexec inside Pi, so authorisation goes
|
|
7
|
+
* through the desktop's polkit agent (KDE, GNOME, …) instead of a terminal the
|
|
8
|
+
* shell tools do not have. Applies to the `bash` tool and to `!` user
|
|
9
|
+
* commands; `sudo` in other terminals is unchanged.
|
|
10
|
+
*
|
|
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
|
|
13
|
+
* name resolution picks them up for `sudo`, `$(sudo …)`, `xargs sudo`,
|
|
14
|
+
* `FOO=1 sudo` and nested scripts alike. Nothing parses the command string,
|
|
15
|
+
* and nothing parses pkexec's output: the shims hand pkexec's exit status
|
|
16
|
+
* (126 dismissed, 127 not authorized) back to the caller unchanged.
|
|
17
|
+
*/
|
|
18
|
+
export default function polkitRedirect(pi: ExtensionAPI): void {
|
|
19
|
+
// The bash tool spawns with a prepared environment, so the shims reach it as
|
|
20
|
+
// a PATH prepend. Registering the built-in's own definition under its own
|
|
21
|
+
// name replaces it and keeps its prompt metadata and renderers.
|
|
22
|
+
pi.registerTool(
|
|
23
|
+
createBashToolDefinition(process.cwd(), {
|
|
24
|
+
spawnHook: ({ command, cwd, env }) => ({ command, cwd, env: withPolkitShimsOnPath(env) }),
|
|
25
|
+
}),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
pi.on("user_bash", () => ({ operations: polkitShimsUserBashOperations() }))
|
|
29
|
+
|
|
30
|
+
// The shims are session-scoped; the next session recreates them lazily.
|
|
31
|
+
pi.on("session_shutdown", () => removePolkitShims())
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Local shell operations whose commands resolve sudo through the shims. The
|
|
36
|
+
* `!` user commands run through pi's own bash backend with no prepared
|
|
37
|
+
* environment, so the PATH prepend happens as the first command line; the
|
|
38
|
+
* exit status and output of the command itself are unchanged.
|
|
39
|
+
*/
|
|
40
|
+
function polkitShimsUserBashOperations(): BashOperations {
|
|
41
|
+
const local = createLocalBashOperations()
|
|
42
|
+
return {
|
|
43
|
+
exec: (command, cwd, options) => local.exec(`${polkitShimsPathExportLine()}\n${command}`, cwd, options),
|
|
44
|
+
}
|
|
45
|
+
}
|
package/polkit-shims.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { tmpdir } from "node:os"
|
|
3
|
+
import { isAbsolute, join } from "node:path"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Exit status every shim uses for an invocation pkexec cannot express
|
|
7
|
+
* (EX_USAGE). Distinct from pkexec's own 126 (dismissed) and 127 (not
|
|
8
|
+
* authorized or failed) so callers can tell the two apart without reading
|
|
9
|
+
* any output.
|
|
10
|
+
*/
|
|
11
|
+
export const SHIM_USAGE_EXIT = 64
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The sudo shim. Lives on PATH ahead of the real sudo, so the shell — not
|
|
15
|
+
* this extension — decides that `sudo` means this file. It forwards to pkexec
|
|
16
|
+
* and never parses what pkexec prints: the exit status is the whole result.
|
|
17
|
+
*
|
|
18
|
+
* Only options with an exact pkexec equivalent are translated. Everything
|
|
19
|
+
* else is refused with an actionable message rather than forwarded, because
|
|
20
|
+
* pkexec would reject it with a message that does not say what to do instead.
|
|
21
|
+
*/
|
|
22
|
+
const SUDO_SHIM = `#!/bin/sh
|
|
23
|
+
# pi-polkit: sudo shim. Routes sudo to pkexec so polkit can authorize it.
|
|
24
|
+
# Generated at extension startup; do not edit.
|
|
25
|
+
|
|
26
|
+
fail() {
|
|
27
|
+
printf 'pi-polkit: %s\\n' "$1" >&2
|
|
28
|
+
exit ${SHIM_USAGE_EXIT}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
command -v pkexec >/dev/null 2>&1 || fail 'pkexec is not installed; install polkit'
|
|
32
|
+
|
|
33
|
+
user=
|
|
34
|
+
while [ $# -gt 0 ]; do
|
|
35
|
+
case $1 in
|
|
36
|
+
--)
|
|
37
|
+
shift
|
|
38
|
+
break
|
|
39
|
+
;;
|
|
40
|
+
-u)
|
|
41
|
+
[ $# -ge 2 ] || fail 'sudo -u needs a username: -u USER PROGRAM'
|
|
42
|
+
user=$2
|
|
43
|
+
shift 2
|
|
44
|
+
;;
|
|
45
|
+
--user)
|
|
46
|
+
[ $# -ge 2 ] || fail 'sudo --user needs a username: --user USER PROGRAM'
|
|
47
|
+
user=$2
|
|
48
|
+
shift 2
|
|
49
|
+
;;
|
|
50
|
+
-u?*)
|
|
51
|
+
user=\${1#-u}
|
|
52
|
+
[ -n "$user" ] || fail 'sudo -u needs a username: -u USER PROGRAM'
|
|
53
|
+
shift
|
|
54
|
+
;;
|
|
55
|
+
--user=*)
|
|
56
|
+
user=\${1#--user=}
|
|
57
|
+
[ -n "$user" ] || fail 'sudo --user needs a username: --user USER PROGRAM'
|
|
58
|
+
shift
|
|
59
|
+
;;
|
|
60
|
+
-n | --non-interactive)
|
|
61
|
+
fail 'sudo -n is not supported: pkexec has no non-interactive mode, polkit decides whether authentication is needed. Drop -n.'
|
|
62
|
+
;;
|
|
63
|
+
-v | --validate | -k | --reset-timestamp | -K | --remove-timestamp)
|
|
64
|
+
fail 'sudo credential caching does not exist under polkit; drop -v/-k.'
|
|
65
|
+
;;
|
|
66
|
+
-l | --list)
|
|
67
|
+
fail 'sudo -l is not available under polkit; inspect policy with pkaction --verbose.'
|
|
68
|
+
;;
|
|
69
|
+
-e | --edit)
|
|
70
|
+
fail 'sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.'
|
|
71
|
+
;;
|
|
72
|
+
-i | --login | -s | --shell)
|
|
73
|
+
fail 'sudo -i/-s opens a login shell; run pkexec bash instead.'
|
|
74
|
+
;;
|
|
75
|
+
-S | --stdin)
|
|
76
|
+
fail 'sudo -S reads the password from stdin; pkexec authenticates through the polkit agent. Drop -S.'
|
|
77
|
+
;;
|
|
78
|
+
-A | --askpass)
|
|
79
|
+
fail 'SUDO_ASKPASS does not apply to pkexec; drop -A.'
|
|
80
|
+
;;
|
|
81
|
+
-b | --background)
|
|
82
|
+
fail 'sudo -b backgrounds the command; append & instead.'
|
|
83
|
+
;;
|
|
84
|
+
-E | --preserve-env | --preserve-env=* | -H | --set-home | -P | --preserve-groups | -N | --no-update)
|
|
85
|
+
fail 'pkexec deliberately sanitizes the environment; drop -E/-H/-P.'
|
|
86
|
+
;;
|
|
87
|
+
-?*)
|
|
88
|
+
fail "option has no pkexec equivalent: $1 (pkexec accepts --user, --keep-cwd, --disable-internal-agent)"
|
|
89
|
+
;;
|
|
90
|
+
*)
|
|
91
|
+
break
|
|
92
|
+
;;
|
|
93
|
+
esac
|
|
94
|
+
done
|
|
95
|
+
|
|
96
|
+
# pkexec is silent when the dialog is dismissed (status 126), so say what the
|
|
97
|
+
# status means. Every other failure path already prints its own reason.
|
|
98
|
+
run_pkexec() {
|
|
99
|
+
pkexec "$@"
|
|
100
|
+
status=$?
|
|
101
|
+
if [ $status -eq 126 ]; then
|
|
102
|
+
printf 'pi-polkit: %s\n' 'the polkit authentication dialog was dismissed' >&2
|
|
103
|
+
fi
|
|
104
|
+
exit $status
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if [ -n "$user" ]; then
|
|
108
|
+
run_pkexec --user "$user" "$@"
|
|
109
|
+
fi
|
|
110
|
+
run_pkexec "$@"
|
|
111
|
+
`
|
|
112
|
+
|
|
113
|
+
/** The doas shim: same contract, doas option grammar. */
|
|
114
|
+
const DOAS_SHIM = `#!/bin/sh
|
|
115
|
+
# pi-polkit: doas shim. Routes doas to pkexec so polkit can authorize it.
|
|
116
|
+
# Generated at extension startup; do not edit.
|
|
117
|
+
|
|
118
|
+
fail() {
|
|
119
|
+
printf 'pi-polkit: %s\\n' "$1" >&2
|
|
120
|
+
exit ${SHIM_USAGE_EXIT}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
command -v pkexec >/dev/null 2>&1 || fail 'pkexec is not installed; install polkit'
|
|
124
|
+
|
|
125
|
+
user=
|
|
126
|
+
while [ $# -gt 0 ]; do
|
|
127
|
+
case $1 in
|
|
128
|
+
--)
|
|
129
|
+
shift
|
|
130
|
+
break
|
|
131
|
+
;;
|
|
132
|
+
-u)
|
|
133
|
+
[ $# -ge 2 ] || fail 'doas -u needs a username: -u USER PROGRAM'
|
|
134
|
+
user=$2
|
|
135
|
+
shift 2
|
|
136
|
+
;;
|
|
137
|
+
-u?*)
|
|
138
|
+
user=\${1#-u}
|
|
139
|
+
[ -n "$user" ] || fail 'doas -u needs a username: -u USER PROGRAM'
|
|
140
|
+
shift
|
|
141
|
+
;;
|
|
142
|
+
-n)
|
|
143
|
+
fail 'doas -n is not supported: pkexec has no non-interactive mode. Drop -n.'
|
|
144
|
+
;;
|
|
145
|
+
-s)
|
|
146
|
+
fail 'doas -s opens a shell; run pkexec bash instead.'
|
|
147
|
+
;;
|
|
148
|
+
-C | -a | -L)
|
|
149
|
+
fail "option has no pkexec equivalent: $1"
|
|
150
|
+
;;
|
|
151
|
+
-?*)
|
|
152
|
+
fail "unknown doas option: $1"
|
|
153
|
+
;;
|
|
154
|
+
*)
|
|
155
|
+
break
|
|
156
|
+
;;
|
|
157
|
+
esac
|
|
158
|
+
done
|
|
159
|
+
|
|
160
|
+
# pkexec is silent when the dialog is dismissed (status 126), so say what the
|
|
161
|
+
# status means. Every other failure path already prints its own reason.
|
|
162
|
+
run_pkexec() {
|
|
163
|
+
pkexec "$@"
|
|
164
|
+
status=$?
|
|
165
|
+
if [ $status -eq 126 ]; then
|
|
166
|
+
printf 'pi-polkit: %s\n' 'the polkit authentication dialog was dismissed' >&2
|
|
167
|
+
fi
|
|
168
|
+
exit $status
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if [ -n "$user" ]; then
|
|
172
|
+
run_pkexec --user "$user" "$@"
|
|
173
|
+
fi
|
|
174
|
+
run_pkexec "$@"
|
|
175
|
+
`
|
|
176
|
+
|
|
177
|
+
/** sudoedit edits through sudo's own temporary-file flow; polkit has none. */
|
|
178
|
+
const SUDOEDIT_SHIM = `#!/bin/sh
|
|
179
|
+
printf 'pi-polkit: sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.\\n' >&2
|
|
180
|
+
exit ${SHIM_USAGE_EXIT}
|
|
181
|
+
`
|
|
182
|
+
|
|
183
|
+
/** visudo needs an interactive editor on a terminal the shell tool does not have. */
|
|
184
|
+
const VISUDO_SHIM = `#!/bin/sh
|
|
185
|
+
printf 'pi-polkit: visudo needs an interactive terminal; validate a candidate file with pkexec visudo -c -f FILE instead.\\n' >&2
|
|
186
|
+
exit ${SHIM_USAGE_EXIT}
|
|
187
|
+
`
|
|
188
|
+
|
|
189
|
+
/** File name → script body. Names must match the commands being redirected. */
|
|
190
|
+
const POLKIT_SHIM_SCRIPTS: Record<string, string> = {
|
|
191
|
+
sudo: SUDO_SHIM,
|
|
192
|
+
doas: DOAS_SHIM,
|
|
193
|
+
sudoedit: SUDOEDIT_SHIM,
|
|
194
|
+
visudo: VISUDO_SHIM,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Shim directory of the running extension; created on first use, cleared by removePolkitShims. */
|
|
198
|
+
let shimDirectory: string | undefined
|
|
199
|
+
|
|
200
|
+
/**
|
|
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
|
|
203
|
+
* session) leave nothing behind.
|
|
204
|
+
*/
|
|
205
|
+
export function ensurePolkitShims(): string {
|
|
206
|
+
shimDirectory ??= installPolkitShims()
|
|
207
|
+
return shimDirectory
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Deletes the shim directory created by ensurePolkitShims. Idempotent, and the
|
|
212
|
+
* next use recreates the directory, so it can run at every session shutdown.
|
|
213
|
+
*/
|
|
214
|
+
export function removePolkitShims(): void {
|
|
215
|
+
if (!shimDirectory) return
|
|
216
|
+
rmSync(shimDirectory, { recursive: true, force: true })
|
|
217
|
+
shimDirectory = undefined
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 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
|
|
223
|
+
* prepend is idempotent: an environment that already carries the prefix is
|
|
224
|
+
* returned unchanged.
|
|
225
|
+
*/
|
|
226
|
+
export function withPolkitShimsOnPath(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
227
|
+
const prefix = `${ensurePolkitShims()}:`
|
|
228
|
+
const path = env.PATH ?? ""
|
|
229
|
+
return path.startsWith(prefix) ? env : { ...env, PATH: prefix + path }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* A shell line that puts the shim directory first on PATH, for execution paths
|
|
234
|
+
* that receive no prepared environment (the `!` user commands run through a
|
|
235
|
+
* shell, so the prepend happens as their first command line).
|
|
236
|
+
*/
|
|
237
|
+
export function polkitShimsPathExportLine(): string {
|
|
238
|
+
return `export PATH="${ensurePolkitShims()}:$PATH"`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Writes the shim scripts into a fresh private directory and returns it. The
|
|
243
|
+
* directory is 0700 so no other local user can rewrite the scripts.
|
|
244
|
+
*/
|
|
245
|
+
function installPolkitShims(): string {
|
|
246
|
+
const directory = mkdtempSync(join(shimBaseDirectory(), "pi-polkit-"))
|
|
247
|
+
for (const [name, script] of Object.entries(POLKIT_SHIM_SCRIPTS)) {
|
|
248
|
+
const file = join(directory, name)
|
|
249
|
+
writeFileSync(file, script)
|
|
250
|
+
// writeFileSync's mode is filtered by umask, so set it explicitly.
|
|
251
|
+
chmodSync(file, 0o755)
|
|
252
|
+
}
|
|
253
|
+
return directory
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Where shim directories live. Prefers the login session's runtime directory,
|
|
258
|
+
* which the system clears on logout, over /tmp which keeps leftovers.
|
|
259
|
+
*/
|
|
260
|
+
function shimBaseDirectory(): string {
|
|
261
|
+
const runtime = process.env.XDG_RUNTIME_DIR
|
|
262
|
+
return runtime && isAbsolute(runtime) && existsSync(runtime) ? runtime : tmpdir()
|
|
263
|
+
}
|