opencode-polkit 0.3.1 → 0.4.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 +84 -0
- package/README.md +108 -16
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -1
- package/dist/shims.d.ts +3 -1
- package/dist/shims.js +440 -92
- package/dist/shims.js.map +1 -1
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,90 @@ 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.4.1] - 2026-11-07
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `test/shims.test.ts`: behaviour tests for the generated shims. They stub
|
|
13
|
+
`pkexec`, `pkcheck` and `pkaction` on `PATH`, so they need neither polkit, a
|
|
14
|
+
desktop agent nor root; `npm test` runs them with Node's test runner.
|
|
15
|
+
- README: account-lockout section. Where the `polkit-1` PAM service includes
|
|
16
|
+
`pam_faillock`, a wrong or empty password submitted in the dialog adds a
|
|
17
|
+
record shared with terminal sudo and login; dismissing the dialog adds none,
|
|
18
|
+
an agent may allow several attempts in one dialog (each retry starts a new PAM
|
|
19
|
+
helper), and a locked account rejects even the correct password for
|
|
20
|
+
`unlock_time`. Documents how to inspect and reset the tally, and why retrying
|
|
21
|
+
does not help.
|
|
22
|
+
- README limitation: `-n`, `-v` and `-l` check
|
|
23
|
+
`org.freedesktop.policykit.exec` itself, while pkexec may select another
|
|
24
|
+
action installed with `org.freedesktop.policykit.exec.path`/`argv1`
|
|
25
|
+
annotations.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- The shell tool's description now asks the model to consult the user before
|
|
30
|
+
retrying a dismissed or failed dialog, because failed or empty password
|
|
31
|
+
submissions count toward a system-wide `pam_faillock` lockout while a dismissal
|
|
32
|
+
does not.
|
|
33
|
+
- README: `sudoers` and the `sudo` PAM stack are not consulted; polkit policy and
|
|
34
|
+
the `polkit-1` PAM service decide, so sudo-only modules such as a fingerprint
|
|
35
|
+
reader or a second factor do not run in OpenCode.
|
|
36
|
+
- README now describes agent caching and retry behaviour generically instead of
|
|
37
|
+
attributing it to one desktop agent.
|
|
38
|
+
- `sudo -v` reports an interactive failure as "polkit did not authorize" instead
|
|
39
|
+
of "polkit denies", since the check can fail after the password is rejected.
|
|
40
|
+
- The shim's action comment now records that pkexec may select a custom action by
|
|
41
|
+
annotation, which the checks do not see.
|
|
42
|
+
- `npm run check` now also type-checks `test/`, and CI runs check, test and
|
|
43
|
+
build.
|
|
44
|
+
|
|
45
|
+
## [0.4.0] - 2026-11-05
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- `sudo -n`, `doas -n`: a non-interactive `pkcheck` on
|
|
50
|
+
`org.freedesktop.policykit.exec` decides whether the command runs. polkit grants
|
|
51
|
+
it without asking, or the shim exits `1` with `sudo: a password is required` and
|
|
52
|
+
the reason that dropping `-n` lets the agent answer. Measured: a successful
|
|
53
|
+
`pkexec` leaves no temporary authorization behind (`pkcheck --list-temp` stays
|
|
54
|
+
empty), so the silent second run is the agent's own password cache and no
|
|
55
|
+
non-interactive query can predict it; `-n` therefore reports policy, not a
|
|
56
|
+
guess.
|
|
57
|
+
- `sudo -v` performs the authentication (`pkcheck --allow-user-interaction`) and
|
|
58
|
+
exits `0`, `1`, `126` when the dialog is dismissed, or `127`; `sudo -nv` uses
|
|
59
|
+
the non-interactive check.
|
|
60
|
+
- `sudo -l` prints `pkaction --verbose` for the action and this session's
|
|
61
|
+
temporary authorizations.
|
|
62
|
+
- `sudo -k`/`-K` revoke polkit's temporary authorizations and say that an agent's
|
|
63
|
+
own password cache cannot be cleared from here.
|
|
64
|
+
- `sudo -H`, `-N`, `-P` are accepted as no-ops: pkexec already sets `HOME`, keeps
|
|
65
|
+
no timestamp and leaves groups alone.
|
|
66
|
+
- `sudo -s COMMAND` and `--shell` run the command through a shell, `sudo -b`
|
|
67
|
+
starts it in the background and returns, and `sudo -E`/
|
|
68
|
+
`--preserve-env[=LIST]` re-export the environment inside the command.
|
|
69
|
+
- `sudo --help` and `sudo -V` are answered by the real binary, so scripts that
|
|
70
|
+
probe sudo keep working.
|
|
71
|
+
- Short options bundle (`sudo -nu bob`), and unknown long options report their own
|
|
72
|
+
name instead of `--`.
|
|
73
|
+
- `opencode-polkit-env`: helper that turns `NAME=VALUE` arguments into export
|
|
74
|
+
statements. `-E` passes entries through argv (`env -0` into `xargs -0`), so
|
|
75
|
+
spaces, quotes and newlines survive, and it drops every shim directory from
|
|
76
|
+
`PATH` so an elevated command cannot resolve sudo back to a shim.
|
|
77
|
+
- `src/index.ts`: the shell tool's description gains one sentence naming the
|
|
78
|
+
redirect and what `-n`, `-v` and `-l` answer, so the model plans privilege with
|
|
79
|
+
the real contract instead of learning it from a failed call.
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
|
|
83
|
+
- The shims share one prologue: `fail`, the polkit subject (pinned with
|
|
84
|
+
`pid,start-time,uid` as `pkcheck(1)` requires), the two checks and the `pkexec`
|
|
85
|
+
wrapper.
|
|
86
|
+
- `-n` failures exit `1` like sudo, not `64`: `64` now means only that the
|
|
87
|
+
invocation cannot be expressed. README documents the four-way handling
|
|
88
|
+
(translated, no-op, checked, refused) and polkit's missing temp authorization.
|
|
89
|
+
- `sudo -i`, `-S`, `-A`, `sudoedit` and `visudo` still refuse, now from the shared
|
|
90
|
+
prelude.
|
|
91
|
+
|
|
8
92
|
## [0.3.1] - 2026-11-04
|
|
9
93
|
|
|
10
94
|
### Added
|
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# opencode-polkit
|
|
2
2
|
|
|
3
3
|
Redirects `sudo`, `doas` and `su -c` to `pkexec` inside OpenCode, so
|
|
4
|
-
authorisation goes through the desktop's polkit agent
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
authorisation goes through the desktop's polkit agent instead of a terminal the
|
|
5
|
+
shell tool does not have. Applies to commands run by the shell tool; `sudo` in
|
|
6
|
+
other terminals is unchanged.
|
|
7
7
|
|
|
8
8
|
## Scope
|
|
9
9
|
|
|
@@ -48,8 +48,16 @@ For a local checkout, run `npm run build` once, then reference the directory:
|
|
|
48
48
|
| `sudo apt install x` | polkit dialog, then runs as root |
|
|
49
49
|
| `sudo -u bob …` | as above, as `bob` |
|
|
50
50
|
| `cat x \| sudo tee y` | dialog for `tee` |
|
|
51
|
-
| `sudo -n …` |
|
|
52
|
-
| `sudo -
|
|
51
|
+
| `sudo -n …` | runs only if polkit would authorise without asking; otherwise exit 1 with `sudo: a password is required` |
|
|
52
|
+
| `sudo -v` | authenticates through polkit; creates no timestamp |
|
|
53
|
+
| `sudo -l` | prints the polkit policy and this session's temporary authorizations |
|
|
54
|
+
| `sudo -k`, `-K` | revokes polkit's temporary authorizations |
|
|
55
|
+
| `sudo -H`, `-N`, `-P` | accepted: pkexec already does what they ask |
|
|
56
|
+
| `sudo -s CMD` | runs `CMD` through a shell, as root |
|
|
57
|
+
| `sudo -E …` | runs with the caller's environment |
|
|
58
|
+
| `sudo -b …` | starts the command in the background |
|
|
59
|
+
| `sudo --help`, `-V` | answered by the real sudo; nothing elevates |
|
|
60
|
+
| `sudo -i`, `-S`, `-A` | refused, with the equivalent command |
|
|
53
61
|
| `sudoedit`, `visudo` | refused, with the alternative |
|
|
54
62
|
| `su -c 'id'` | dialog, then runs as root |
|
|
55
63
|
| `su -c 'id' bob` | as above, as `bob` (administrator password) |
|
|
@@ -57,7 +65,9 @@ For a local checkout, run `npm run build` once, then reference the directory:
|
|
|
57
65
|
| `su -m`, `-s`, `-g`, `-w`, `--pty` | refused, with the alternative |
|
|
58
66
|
| `pkexec …` | passed through |
|
|
59
67
|
|
|
60
|
-
|
|
68
|
+
An option is refused only when polkit cannot express it: it needs a terminal, a
|
|
69
|
+
password channel polkit does not have, or an editor. Refusals exit with status 64
|
|
70
|
+
and one line naming the alternative.
|
|
61
71
|
|
|
62
72
|
## How it works
|
|
63
73
|
|
|
@@ -66,18 +76,82 @@ resolves those instead of the system binaries, and they forward the command to
|
|
|
66
76
|
`pkexec`. Command text is never rewritten, so quoting, pipes and subshells need
|
|
67
77
|
no special handling.
|
|
68
78
|
|
|
79
|
+
The `create.before` shell hook prepends the stand-in directory to the spawned
|
|
80
|
+
`PATH`. The directory lives in `$XDG_RUNTIME_DIR` (falling back to `/tmp`) as
|
|
81
|
+
`0700`, is removed when the plugin is unloaded, and also holds
|
|
82
|
+
`opencode-polkit-env`, the helper `sudo -E` uses to re-export an environment
|
|
83
|
+
pkexec stripped.
|
|
84
|
+
|
|
85
|
+
The shell tool's description gains a note about the redirect, so the model
|
|
86
|
+
plans privilege with the real contract instead of learning it from a failed
|
|
87
|
+
call.
|
|
88
|
+
|
|
69
89
|
## Authorisation
|
|
70
90
|
|
|
71
91
|
`polkitd` decides whether a password is required. These commands always require
|
|
72
92
|
one; other actions may not, according to policy. The plugin takes no part in
|
|
73
93
|
that decision.
|
|
74
94
|
|
|
95
|
+
`sudoers` is not consulted: polkit policy decides who may run what, so per-user
|
|
96
|
+
and per-command rules, `NOPASSWD`, `secure_path` and `env_keep` do not apply.
|
|
97
|
+
Authentication runs through the `polkit-1` PAM service, not `/etc/pam.d/sudo`,
|
|
98
|
+
so a module configured only for sudo — a fingerprint reader, a second factor —
|
|
99
|
+
is not used here; the service's own stack (normally `system-auth`) decides how
|
|
100
|
+
the password is verified.
|
|
101
|
+
|
|
75
102
|
`org.freedesktop.policykit.exec` asks for administrator authentication on every
|
|
76
|
-
call, but
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
agent
|
|
80
|
-
|
|
103
|
+
call, but an agent may keep the password it just received and answer a later
|
|
104
|
+
challenge itself. In that case a second command can run without a dialog. That
|
|
105
|
+
cache belongs to the agent: a different agent, a restarted agent or a change in
|
|
106
|
+
the agent's state can mean the next command prompts again, so do not rely on it.
|
|
107
|
+
|
|
108
|
+
With the default policy, polkit records no temporary authorization for this
|
|
109
|
+
action: after a successful `pkexec`, `pkcheck --list-temp` stays empty and a
|
|
110
|
+
non-interactive `pkcheck` still reports that authentication is required. Whether
|
|
111
|
+
a later command prompts is therefore the agent's decision, and no user-space
|
|
112
|
+
query can predict it. That is why `sudo -n` reports what the policy says rather
|
|
113
|
+
than guessing whether a dialog would appear, and why `sudo -k` can only clear
|
|
114
|
+
polkit's own record.
|
|
115
|
+
|
|
116
|
+
The plugin keeps no authorization state of its own: `sudo -v` verifies once and
|
|
117
|
+
writes nothing, `sudo -n` assumes nothing, and `sudo -k` only clears polkit's
|
|
118
|
+
record. Authorising one command therefore leaves the next one to be decided
|
|
119
|
+
again, which is the point — the person who allowed an upgrade can still refuse
|
|
120
|
+
the deletion that follows. What widens that window is the agent's password
|
|
121
|
+
cache, not this plugin, and nothing here can revoke it.
|
|
122
|
+
|
|
123
|
+
## Account lockout
|
|
124
|
+
|
|
125
|
+
When the `polkit-1` PAM service includes `pam_faillock` (Arch's default stack
|
|
126
|
+
does), the dialog shares one failure tally per user with terminal sudo, `su` and
|
|
127
|
+
the login screen:
|
|
128
|
+
|
|
129
|
+
- A wrong or empty password submitted in the dialog adds a record. Dismissing
|
|
130
|
+
the dialog, closing it or pressing Esc cancels the agent's helper before PAM
|
|
131
|
+
returns, and adds none. (An agent process that is killed instead of
|
|
132
|
+
cancelling leaves the helper at EOF, and that attempt can count.)
|
|
133
|
+
- An agent may allow several attempts in one dialog, and each retry starts a
|
|
134
|
+
new PAM helper; a dialog can therefore add several records and use up the
|
|
135
|
+
default `deny=3`.
|
|
136
|
+
- Once a user has `deny` failures within `fail_interval` (900 s by default),
|
|
137
|
+
`pam_faillock` refuses that user in every service using the stack for
|
|
138
|
+
`unlock_time` (600 s by default, measured from the newest record). A locked
|
|
139
|
+
account rejects even the correct password, because `preauth` fails and
|
|
140
|
+
`authsucc` cannot reset the tally; further attempts neither extend nor shorten
|
|
141
|
+
the lock. A successful password authentication outside a lock resets the
|
|
142
|
+
tally.
|
|
143
|
+
- `sudo -k` clears polkit's authorizations, not the tally. The locked user's own
|
|
144
|
+
authentication cannot succeed, so OpenCode cannot elevate with that user's
|
|
145
|
+
credentials to reset it: wait for the lock to expire, use a root console or
|
|
146
|
+
another administrator, or reboot (`/run` is cleared). Where the tally file is
|
|
147
|
+
readable by the user, `faillock --user "$USER"` shows the records and
|
|
148
|
+
`faillock --user "$USER" --reset` clears them without root; otherwise both
|
|
149
|
+
need root. Check readability with `ls -l /run/faillock/$USER`.
|
|
150
|
+
|
|
151
|
+
Retrying a failed dialog in a loop only gives the user more chances to submit a
|
|
152
|
+
wrong or empty password, and the lock applies to the whole account. The shell
|
|
153
|
+
tool's description therefore tells the model to ask before retrying, and
|
|
154
|
+
`sudo -n` remains the probe that never invokes PAM.
|
|
81
155
|
|
|
82
156
|
## Limitations
|
|
83
157
|
|
|
@@ -87,6 +161,15 @@ again, so do not rely on it.
|
|
|
87
161
|
- `su -c … USER` authenticates an administrator (polkit), not `USER`, so an
|
|
88
162
|
account that real `su` would allow can be refused here.
|
|
89
163
|
- `su -c` runs the command with `sh -c`, not with the target user's login shell.
|
|
164
|
+
- `-n`, `-v` and `-l` check `org.freedesktop.policykit.exec` itself. pkexec may
|
|
165
|
+
select a different action installed with an
|
|
166
|
+
`org.freedesktop.policykit.exec.path` (and `argv1`) annotation, so that
|
|
167
|
+
action's rules are not reflected by these checks.
|
|
168
|
+
- `sudo -E` re-exports the environment through `pkexec`'s argument list, so the
|
|
169
|
+
values are visible in `ps` while the command starts. Exported shell functions
|
|
170
|
+
are skipped, and `PATH` loses the shim directories.
|
|
171
|
+
- `sudo -k` cannot clear the password a desktop agent holds; only polkit's
|
|
172
|
+
temporary authorizations are revoked.
|
|
90
173
|
|
|
91
174
|
These bypass the redirect, not polkit, which still decides whether the command
|
|
92
175
|
runs.
|
|
@@ -98,14 +181,23 @@ without one, `pkexec` fails instead of prompting.
|
|
|
98
181
|
|
|
99
182
|
## Exit status
|
|
100
183
|
|
|
101
|
-
| Status | Meaning
|
|
102
|
-
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
|
|
|
184
|
+
| Status | Meaning |
|
|
185
|
+
|--------|--------------------------------------------------------|
|
|
186
|
+
| 1 | `-n` or `-v` could not be satisfied: polkit requires authentication, denies it, or authentication failed |
|
|
187
|
+
| 126 | the polkit dialog was dismissed |
|
|
188
|
+
| 127 | not authorised (including after failed authentication), or the program could not run |
|
|
189
|
+
| 64 | the plugin refused the invocation |
|
|
106
190
|
|
|
107
191
|
Messages are in English.
|
|
108
192
|
|
|
193
|
+
## Development
|
|
194
|
+
|
|
195
|
+
`npm install`, then `npm run check` type-checks the plugin and its tests, and
|
|
196
|
+
`npm test` runs the shim behaviour tests. The tests stub `pkexec`, `pkcheck` and
|
|
197
|
+
`pkaction` on `PATH`, so they need neither polkit, a desktop agent nor root;
|
|
198
|
+
they need Node 22.19 or later for `--experimental-strip-types`. `npm run build`
|
|
199
|
+
regenerates `dist`.
|
|
200
|
+
|
|
109
201
|
## License
|
|
110
202
|
|
|
111
203
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,16 @@ import { installPolkitShims, removePolkitShims } from "./shims.js";
|
|
|
4
4
|
* Only the prepend matters; the rest is a last-resort fallback.
|
|
5
5
|
*/
|
|
6
6
|
const FALLBACK_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
7
|
+
/** Tool names whose description should name how elevation works here. */
|
|
8
|
+
const SHELL_TOOLS = ["bash", "shell"];
|
|
9
|
+
/** Opening words of the appended description; also the guard against appending twice. */
|
|
10
|
+
const CONTRACT_MARKER = "polkit redirect:";
|
|
11
|
+
/**
|
|
12
|
+
* Appended to the shell tool's description. The model plans privilege with what
|
|
13
|
+
* it knows about sudo, so the difference is stated where it reads the tool,
|
|
14
|
+
* instead of being learned from a failed call.
|
|
15
|
+
*/
|
|
16
|
+
const SHELL_TOOL_CONTRACT = `${CONTRACT_MARKER} \`sudo\`, \`doas\` and \`su -c\` run through pkexec, so a desktop polkit dialog authorises them instead of a terminal password prompt. \`sudo -n\` runs the command only if polkit would authorise it without asking, \`sudo -v\` performs a real authentication, and \`sudo -l\` prints the polkit policy that decides. Ask the user before retrying a dismissed or failed dialog instead of looping: on systems with pam_faillock, failed or empty password submissions count toward a system-wide lockout shared with login, while dismissing the dialog does not.`;
|
|
7
17
|
/**
|
|
8
18
|
* Redirects sudo/doas/su -c to pkexec without touching the command text.
|
|
9
19
|
*
|
|
@@ -26,6 +36,20 @@ const setup = async (ctx) => {
|
|
|
26
36
|
if (!path.startsWith(prefix))
|
|
27
37
|
event.env.PATH = prefix + path;
|
|
28
38
|
});
|
|
39
|
+
// The shell tool is the host's, so its description is extended rather than
|
|
40
|
+
// replaced. Every reload runs this transform again, so a description that
|
|
41
|
+
// already carries the line is left alone.
|
|
42
|
+
await ctx.tool.transform((tools) => {
|
|
43
|
+
for (const tool of tools.list()) {
|
|
44
|
+
if (!SHELL_TOOLS.includes(tool.id) && !SHELL_TOOLS.includes(tool.name))
|
|
45
|
+
continue;
|
|
46
|
+
tools.update(tool.id, (entry) => {
|
|
47
|
+
if (entry.description.includes(CONTRACT_MARKER))
|
|
48
|
+
return;
|
|
49
|
+
entry.description = `${entry.description}\n\n${SHELL_TOOL_CONTRACT}`;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
29
53
|
return () => removePolkitShims(shimDir);
|
|
30
54
|
};
|
|
31
55
|
/**
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAElE;;;GAGG;AACH,MAAM,aAAa,GAAG,8DAA8D,CAAA;AAEpF;;;;;;;;;;;;GAYG;AACH,MAAM,KAAK,GAAG,KAAK,EAAE,GAAmB,EAA2B,EAAE;IACnE,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAA;IACpC,MAAM,MAAM,GAAG,GAAG,OAAO,GAAG,CAAA;IAE5B,8EAA8E;IAC9E,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,KAAK,EAAE,EAAE;QAC9C,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,IAAI,aAAa,CAAA;QAC5C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IAC9D,CAAC,CAAC,CAAA;IAEF,OAAO,GAAG,EAAE,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAA;AACzC,CAAC,CAAA;AAED;;;GAGG;AACH,eAAe,EAAE,EAAE,EAAE,iBAAiB,EAAE,KAAK,EAA0B,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAElE;;;GAGG;AACH,MAAM,aAAa,GAAG,8DAA8D,CAAA;AAEpF,yEAAyE;AACzE,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAErC,yFAAyF;AACzF,MAAM,eAAe,GAAG,kBAAkB,CAAA;AAE1C;;;;GAIG;AACH,MAAM,mBAAmB,GAAG,GAAG,eAAe,wiBAAwiB,CAAA;AAEtlB;;;;;;;;;;;;GAYG;AACH,MAAM,KAAK,GAAG,KAAK,EAAE,GAAmB,EAA2B,EAAE;IACnE,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAA;IACpC,MAAM,MAAM,GAAG,GAAG,OAAO,GAAG,CAAA;IAE5B,8EAA8E;IAC9E,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC,KAAK,EAAE,EAAE;QAC9C,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,IAAI,aAAa,CAAA;QAC5C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,MAAM,GAAG,IAAI,CAAA;IAC9D,CAAC,CAAC,CAAA;IAEF,2EAA2E;IAC3E,0EAA0E;IAC1E,0CAA0C;IAC1C,MAAM,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;QACjC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;YAChC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,SAAQ;YAChF,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE;gBAC9B,IAAI,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,eAAe,CAAC;oBAAE,OAAM;gBACvD,KAAK,CAAC,WAAW,GAAG,GAAG,KAAK,CAAC,WAAW,OAAO,mBAAmB,EAAE,CAAA;YACtE,CAAC,CAAC,CAAA;QACJ,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,OAAO,GAAG,EAAE,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAA;AACzC,CAAC,CAAA;AAED;;;GAGG;AACH,eAAe,EAAE,EAAE,EAAE,iBAAiB,EAAE,KAAK,EAA0B,CAAA"}
|
package/dist/shims.d.ts
CHANGED
|
@@ -9,7 +9,9 @@ export declare const SHIM_USAGE_EXIT = 64;
|
|
|
9
9
|
* Writes the shim scripts into a fresh private directory and returns it.
|
|
10
10
|
* The caller prepends this directory to PATH; the shell then resolves
|
|
11
11
|
* `sudo`/`doas`/`su` to these files instead of the system ones. The directory
|
|
12
|
-
* is 0700 so no other local user can rewrite the scripts
|
|
12
|
+
* is 0700 so no other local user can rewrite the scripts, and it holds
|
|
13
|
+
* `opencode-polkit-env`, the helper `sudo -E` uses to re-export an environment
|
|
14
|
+
* pkexec stripped.
|
|
13
15
|
*/
|
|
14
16
|
export declare function installPolkitShims(): string;
|
|
15
17
|
/** Deletes a directory created by installPolkitShims. */
|
package/dist/shims.js
CHANGED
|
@@ -8,82 +8,261 @@ import { isAbsolute, join } from "node:path";
|
|
|
8
8
|
* any output.
|
|
9
9
|
*/
|
|
10
10
|
export const SHIM_USAGE_EXIT = 64;
|
|
11
|
+
/** The action pkexec checks; the checks below ask polkit about the same one. */
|
|
12
|
+
const POLKIT_ACTION = "org.freedesktop.policykit.exec";
|
|
11
13
|
/**
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
14
|
-
* and never parses what pkexec prints: the exit status is the whole result.
|
|
15
|
-
*
|
|
16
|
-
* Only options with an exact pkexec equivalent are translated. Everything
|
|
17
|
-
* else is refused with an actionable message rather than forwarded, because
|
|
18
|
-
* pkexec would reject it with a message that does not say what to do instead.
|
|
14
|
+
* The part every shim starts with: one refusal helper, and a check that the two
|
|
15
|
+
* polkit tools exist at all.
|
|
19
16
|
*/
|
|
20
|
-
const
|
|
21
|
-
# opencode-polkit: sudo shim. Routes sudo to pkexec so polkit can authorize it.
|
|
22
|
-
# Generated at plugin startup; do not edit.
|
|
23
|
-
|
|
24
|
-
fail() {
|
|
17
|
+
const SHIM_PRELUDE = `fail() {
|
|
25
18
|
printf 'opencode-polkit: %s\\n' "$1" >&2
|
|
26
19
|
exit ${SHIM_USAGE_EXIT}
|
|
27
20
|
}
|
|
28
21
|
|
|
29
22
|
command -v pkexec >/dev/null 2>&1 || fail 'pkexec is not installed; install polkit'
|
|
23
|
+
command -v pkcheck >/dev/null 2>&1 || fail 'pkcheck is not installed; install polkit'
|
|
24
|
+
`;
|
|
25
|
+
/**
|
|
26
|
+
* The checks that answer what sudo's timestamp file used to answer, plus the
|
|
27
|
+
* pkexec wrapper. Only exit statuses are read: pkcheck's messages and its
|
|
28
|
+
* `polkit.result` detail are localised and addressed to humans, so parsing them
|
|
29
|
+
* would break on the next locale or polkit release.
|
|
30
|
+
*/
|
|
31
|
+
const SHIM_CHECKS = `# The action checked below. pkexec uses it by default; a site can install
|
|
32
|
+
# another action selected by org.freedesktop.policykit.exec.path/argv1, whose
|
|
33
|
+
# rules these checks do not see.
|
|
34
|
+
ACTION=${POLKIT_ACTION}
|
|
30
35
|
|
|
36
|
+
# pkcheck(1) warns that a bare pid races with pid reuse, so pin the start time.
|
|
37
|
+
subject() {
|
|
38
|
+
started=$(awk '{ print $22 }' /proc/$$/stat 2>/dev/null)
|
|
39
|
+
if [ -n "$started" ]; then
|
|
40
|
+
printf '%s,%s,%s' "$$" "$started" "$(id -u)"
|
|
41
|
+
else
|
|
42
|
+
printf '%s' "$$"
|
|
43
|
+
fi
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# 0 grants the action without asking anyone, 2 requires authentication,
|
|
47
|
+
# 1 refuses it. pkcheck's own 126 (usage) and 127 (error) pass through.
|
|
48
|
+
check_without_prompt() {
|
|
49
|
+
pkcheck --action-id "$ACTION" --process "$(subject)" >/dev/null 2>&1
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# The same check with the desktop agent allowed to answer. An agent that still
|
|
53
|
+
# holds the password answers without a dialog, which is why a 0 here also means
|
|
54
|
+
# the next pkexec runs as root without asking.
|
|
55
|
+
check_with_prompt() {
|
|
56
|
+
pkcheck --action-id "$ACTION" --process "$(subject)" --allow-user-interaction >/dev/null 2>&1
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# -n means "run only if no one has to be asked". polkit cannot say whether the
|
|
60
|
+
# agent would answer from its cache, so a 2 is reported as needing a password
|
|
61
|
+
# and the command is not run: that is what -n promises, in both directions.
|
|
62
|
+
require_without_prompt() {
|
|
63
|
+
if [ -n "$noninteractive" ]; then
|
|
64
|
+
check_without_prompt
|
|
65
|
+
status=$?
|
|
66
|
+
case $status in
|
|
67
|
+
0)
|
|
68
|
+
return 0
|
|
69
|
+
;;
|
|
70
|
+
1)
|
|
71
|
+
printf 'opencode-polkit: %s: polkit denies %s to this process\\n' "$PROG" "$ACTION" >&2
|
|
72
|
+
exit 1
|
|
73
|
+
;;
|
|
74
|
+
2)
|
|
75
|
+
printf '%s: a password is required (polkit requires authentication for %s; drop -n to let the desktop agent answer, which it may do from its cache)\\n' "$PROG" "$ACTION" >&2
|
|
76
|
+
exit 1
|
|
77
|
+
;;
|
|
78
|
+
*)
|
|
79
|
+
printf 'opencode-polkit: %s: polkit check failed (status %s)\\n' "$PROG" "$status" >&2
|
|
80
|
+
exit 127
|
|
81
|
+
;;
|
|
82
|
+
esac
|
|
83
|
+
fi
|
|
84
|
+
return 0
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# pkexec is silent when the dialog is dismissed (status 126), so say what the
|
|
88
|
+
# status means. Every other failure path already prints its own reason.
|
|
89
|
+
run_pkexec() {
|
|
90
|
+
pkexec "$@"
|
|
91
|
+
status=$?
|
|
92
|
+
if [ $status -eq 126 ]; then
|
|
93
|
+
printf 'opencode-polkit: %s\\n' 'the polkit authentication dialog was dismissed' >&2
|
|
94
|
+
fi
|
|
95
|
+
exit $status
|
|
96
|
+
}
|
|
97
|
+
`;
|
|
98
|
+
/**
|
|
99
|
+
* The sudo shim. Lives on PATH ahead of the real sudo, so the shell — not this
|
|
100
|
+
* plugin — decides that `sudo` means this file. It forwards to pkexec and
|
|
101
|
+
* never parses what pkexec prints: the exit status is the whole result.
|
|
102
|
+
*
|
|
103
|
+
* Options are handled in four ways, and only the last one refuses. Those whose
|
|
104
|
+
* effect pkexec already has (`-H`, `-N`, `-P`) are accepted as no-ops; those
|
|
105
|
+
* polkit answers better than a run (`-n`, `-v`, `-l`, `-k`) become checks; those
|
|
106
|
+
* a wrapper can express (`-s COMMAND`, `-E`, `-b`) are translated; the rest,
|
|
107
|
+
* which need a terminal or a password channel polkit does not have, are refused
|
|
108
|
+
* with one line naming the alternative.
|
|
109
|
+
*/
|
|
110
|
+
const SUDO_SHIM = `#!/bin/sh
|
|
111
|
+
# opencode-polkit: sudo shim. Routes sudo to pkexec so polkit can authorize it.
|
|
112
|
+
# Generated at plugin startup; do not edit.
|
|
113
|
+
|
|
114
|
+
${SHIM_PRELUDE}
|
|
115
|
+
${SHIM_CHECKS}
|
|
116
|
+
PROG=sudo
|
|
31
117
|
user=
|
|
118
|
+
noninteractive=
|
|
119
|
+
preserve=
|
|
120
|
+
preserve_names=
|
|
121
|
+
background=
|
|
122
|
+
mode=
|
|
123
|
+
|
|
32
124
|
while [ $# -gt 0 ]; do
|
|
33
125
|
case $1 in
|
|
34
126
|
--)
|
|
35
127
|
shift
|
|
36
128
|
break
|
|
37
129
|
;;
|
|
38
|
-
-u)
|
|
39
|
-
[ $# -ge 2 ] || fail 'sudo -u needs a username: -u USER PROGRAM'
|
|
40
|
-
user=$2
|
|
41
|
-
shift 2
|
|
42
|
-
;;
|
|
43
130
|
--user)
|
|
44
131
|
[ $# -ge 2 ] || fail 'sudo --user needs a username: --user USER PROGRAM'
|
|
45
132
|
user=$2
|
|
46
133
|
shift 2
|
|
47
134
|
;;
|
|
48
|
-
-u?*)
|
|
49
|
-
user=\${1#-u}
|
|
50
|
-
[ -n "$user" ] || fail 'sudo -u needs a username: -u USER PROGRAM'
|
|
51
|
-
shift
|
|
52
|
-
;;
|
|
53
135
|
--user=*)
|
|
54
136
|
user=\${1#--user=}
|
|
55
|
-
[ -n "$user" ] || fail 'sudo --user needs a username: --user USER PROGRAM'
|
|
56
137
|
shift
|
|
57
138
|
;;
|
|
58
|
-
|
|
59
|
-
|
|
139
|
+
--non-interactive)
|
|
140
|
+
noninteractive=1
|
|
141
|
+
shift
|
|
142
|
+
;;
|
|
143
|
+
--preserve-env)
|
|
144
|
+
preserve=1
|
|
145
|
+
shift
|
|
146
|
+
;;
|
|
147
|
+
--preserve-env=*)
|
|
148
|
+
preserve=1
|
|
149
|
+
preserve_names=\${1#--preserve-env=}
|
|
150
|
+
shift
|
|
151
|
+
;;
|
|
152
|
+
--set-home | --no-update | --preserve-groups)
|
|
153
|
+
shift
|
|
60
154
|
;;
|
|
61
|
-
|
|
62
|
-
|
|
155
|
+
--list)
|
|
156
|
+
mode=list
|
|
157
|
+
shift
|
|
63
158
|
;;
|
|
64
|
-
|
|
65
|
-
|
|
159
|
+
--validate)
|
|
160
|
+
mode=validate
|
|
161
|
+
shift
|
|
66
162
|
;;
|
|
67
|
-
-
|
|
68
|
-
|
|
163
|
+
--reset-timestamp | --remove-timestamp)
|
|
164
|
+
mode=revoke
|
|
165
|
+
shift
|
|
69
166
|
;;
|
|
70
|
-
|
|
71
|
-
|
|
167
|
+
--background)
|
|
168
|
+
background=1
|
|
169
|
+
shift
|
|
72
170
|
;;
|
|
73
|
-
|
|
74
|
-
|
|
171
|
+
--help)
|
|
172
|
+
mode=help
|
|
173
|
+
shift
|
|
75
174
|
;;
|
|
76
|
-
|
|
77
|
-
|
|
175
|
+
--version)
|
|
176
|
+
mode=version
|
|
177
|
+
shift
|
|
78
178
|
;;
|
|
79
|
-
|
|
80
|
-
|
|
179
|
+
--stdin | --askpass | --edit | --login)
|
|
180
|
+
case $1 in
|
|
181
|
+
--edit)
|
|
182
|
+
fail 'sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.'
|
|
183
|
+
;;
|
|
184
|
+
--stdin)
|
|
185
|
+
fail 'sudo -S reads the password from stdin; pkexec authenticates through the polkit agent. Drop -S.'
|
|
186
|
+
;;
|
|
187
|
+
--askpass)
|
|
188
|
+
fail 'SUDO_ASKPASS does not apply to pkexec; drop -A.'
|
|
189
|
+
;;
|
|
190
|
+
*)
|
|
191
|
+
fail 'sudo -i opens a login shell; run pkexec bash instead.'
|
|
192
|
+
;;
|
|
193
|
+
esac
|
|
194
|
+
;;
|
|
195
|
+
--shell)
|
|
196
|
+
mode=shell
|
|
197
|
+
shift
|
|
81
198
|
;;
|
|
82
|
-
|
|
83
|
-
fail
|
|
199
|
+
--*)
|
|
200
|
+
fail "option has no pkexec equivalent: $1 (pkexec accepts --user, --keep-cwd, --disable-internal-agent)"
|
|
84
201
|
;;
|
|
85
202
|
-?*)
|
|
86
|
-
|
|
203
|
+
rest=\${1#-}
|
|
204
|
+
shift
|
|
205
|
+
while [ -n "$rest" ]; do
|
|
206
|
+
letter=\${rest%"\${rest#?}"}
|
|
207
|
+
rest=\${rest#?}
|
|
208
|
+
case $letter in
|
|
209
|
+
u)
|
|
210
|
+
if [ -n "$rest" ]; then
|
|
211
|
+
user=$rest
|
|
212
|
+
rest=
|
|
213
|
+
else
|
|
214
|
+
[ $# -ge 1 ] || fail 'sudo -u needs a username: -u USER PROGRAM'
|
|
215
|
+
user=$1
|
|
216
|
+
shift
|
|
217
|
+
fi
|
|
218
|
+
;;
|
|
219
|
+
n)
|
|
220
|
+
noninteractive=1
|
|
221
|
+
;;
|
|
222
|
+
E)
|
|
223
|
+
preserve=1
|
|
224
|
+
;;
|
|
225
|
+
H | N | P)
|
|
226
|
+
# pkexec already sets HOME, keeps no timestamp and leaves groups alone.
|
|
227
|
+
;;
|
|
228
|
+
b)
|
|
229
|
+
background=1
|
|
230
|
+
;;
|
|
231
|
+
l)
|
|
232
|
+
mode=list
|
|
233
|
+
;;
|
|
234
|
+
v)
|
|
235
|
+
mode=validate
|
|
236
|
+
;;
|
|
237
|
+
k | K)
|
|
238
|
+
mode=revoke
|
|
239
|
+
;;
|
|
240
|
+
s)
|
|
241
|
+
mode=shell
|
|
242
|
+
;;
|
|
243
|
+
h)
|
|
244
|
+
mode=help
|
|
245
|
+
;;
|
|
246
|
+
V)
|
|
247
|
+
mode=version
|
|
248
|
+
;;
|
|
249
|
+
i)
|
|
250
|
+
fail 'sudo -i opens a login shell; run pkexec bash instead.'
|
|
251
|
+
;;
|
|
252
|
+
e)
|
|
253
|
+
fail 'sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.'
|
|
254
|
+
;;
|
|
255
|
+
S)
|
|
256
|
+
fail 'sudo -S reads the password from stdin; pkexec authenticates through the polkit agent. Drop -S.'
|
|
257
|
+
;;
|
|
258
|
+
A)
|
|
259
|
+
fail 'SUDO_ASKPASS does not apply to pkexec; drop -A.'
|
|
260
|
+
;;
|
|
261
|
+
*)
|
|
262
|
+
fail "option has no pkexec equivalent: -$letter (pkexec accepts --user, --keep-cwd, --disable-internal-agent)"
|
|
263
|
+
;;
|
|
264
|
+
esac
|
|
265
|
+
done
|
|
87
266
|
;;
|
|
88
267
|
*)
|
|
89
268
|
break
|
|
@@ -91,20 +270,148 @@ while [ $# -gt 0 ]; do
|
|
|
91
270
|
esac
|
|
92
271
|
done
|
|
93
272
|
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
|
|
273
|
+
# --help and --version describe the binary, not the authorisation, so nothing is
|
|
274
|
+
# elevated: answering them exactly keeps scripts that probe sudo working.
|
|
275
|
+
if [ "$mode" = help ] || [ "$mode" = version ]; then
|
|
276
|
+
for candidate in /usr/bin/sudo /bin/sudo /usr/local/bin/sudo; do
|
|
277
|
+
[ -x "$candidate" ] || continue
|
|
278
|
+
if [ "$mode" = help ]; then
|
|
279
|
+
exec "$candidate" --help
|
|
280
|
+
fi
|
|
281
|
+
exec "$candidate" --version
|
|
282
|
+
done
|
|
283
|
+
fail 'the real sudo is not installed, so --help and --version cannot be answered'
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
# -l reports the policy polkit will apply and the authorizations this session
|
|
287
|
+
# already holds. Both halves are local and never prompt.
|
|
288
|
+
if [ "$mode" = list ]; then
|
|
289
|
+
pkaction --action-id "$ACTION" --verbose || exit 127
|
|
290
|
+
printf '\\nopencode-polkit: temporary authorizations in this session\\n'
|
|
291
|
+
pkcheck --list-temp || exit 127
|
|
292
|
+
exit 0
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
# -v asks the same question sudo -v asks: can authentication succeed right now?
|
|
296
|
+
if [ "$mode" = validate ]; then
|
|
297
|
+
if [ -n "$noninteractive" ]; then
|
|
298
|
+
check_without_prompt
|
|
299
|
+
else
|
|
300
|
+
check_with_prompt
|
|
301
|
+
fi
|
|
98
302
|
status=$?
|
|
99
|
-
|
|
100
|
-
|
|
303
|
+
case $status in
|
|
304
|
+
0)
|
|
305
|
+
exit 0
|
|
306
|
+
;;
|
|
307
|
+
1)
|
|
308
|
+
printf 'opencode-polkit: sudo: polkit did not authorize %s (the policy denies it, or authentication failed)\\n' "$ACTION" >&2
|
|
309
|
+
exit 1
|
|
310
|
+
;;
|
|
311
|
+
2)
|
|
312
|
+
if [ -n "$noninteractive" ]; then
|
|
313
|
+
printf 'sudo: a password is required (polkit requires authentication for %s)\\n' "$ACTION" >&2
|
|
314
|
+
else
|
|
315
|
+
printf 'opencode-polkit: sudo: no polkit authentication agent answered\\n' >&2
|
|
316
|
+
fi
|
|
317
|
+
exit 1
|
|
318
|
+
;;
|
|
319
|
+
3)
|
|
320
|
+
printf 'opencode-polkit: %s\\n' 'the polkit authentication dialog was dismissed' >&2
|
|
321
|
+
exit 126
|
|
322
|
+
;;
|
|
323
|
+
*)
|
|
324
|
+
printf 'opencode-polkit: sudo: polkit check failed (status %s)\\n' "$status" >&2
|
|
325
|
+
exit 127
|
|
326
|
+
;;
|
|
327
|
+
esac
|
|
328
|
+
fi
|
|
329
|
+
|
|
330
|
+
# -k drops what polkit itself recorded. The password a desktop agent keeps is
|
|
331
|
+
# the agent's, not polkit's, so say that it stays.
|
|
332
|
+
if [ "$mode" = revoke ]; then
|
|
333
|
+
pkcheck --revoke-temp || exit 127
|
|
334
|
+
if [ $# -eq 0 ]; then
|
|
335
|
+
printf 'opencode-polkit: %s\\n' 'revoked the temporary polkit authorizations; a desktop agent that caches the password keeps its own copy, so the next command may still not ask' >&2
|
|
336
|
+
exit 0
|
|
101
337
|
fi
|
|
102
|
-
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
# -E: pkexec strips the environment, so name the variables again inside the shell
|
|
341
|
+
# that runs the command. Entries reach the helper as arguments, so spaces, quotes
|
|
342
|
+
# and newlines survive exactly; the helper also drops every shim directory from
|
|
343
|
+
# PATH, so an elevated command cannot resolve sudo back to a shim.
|
|
344
|
+
shim_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd 2>/dev/null)
|
|
345
|
+
env_helper=$shim_dir/opencode-polkit-env
|
|
346
|
+
[ -x "$env_helper" ] || fail "the opencode-polkit environment helper is missing: $env_helper"
|
|
347
|
+
|
|
348
|
+
# Runs in the caller's shell, never in a command substitution: an exit there
|
|
349
|
+
# would only end the subshell, and a name reaches eval below, so it must be a
|
|
350
|
+
# name before that happens.
|
|
351
|
+
check_preserve() {
|
|
352
|
+
if [ -z "$preserve_names" ]; then
|
|
353
|
+
command -v xargs >/dev/null 2>&1 || fail '-E needs xargs to forward the environment; name the variables instead: --preserve-env=NAME[,NAME...]'
|
|
354
|
+
return 0
|
|
355
|
+
fi
|
|
356
|
+
old_ifs=$IFS
|
|
357
|
+
IFS=,
|
|
358
|
+
for name in $preserve_names; do
|
|
359
|
+
case $name in
|
|
360
|
+
'' | [0-9]* | *[!A-Za-z0-9_]*)
|
|
361
|
+
IFS=$old_ifs
|
|
362
|
+
fail "not a variable name: $name"
|
|
363
|
+
;;
|
|
364
|
+
esac
|
|
365
|
+
done
|
|
366
|
+
IFS=$old_ifs
|
|
103
367
|
}
|
|
104
368
|
|
|
369
|
+
export_prefix() {
|
|
370
|
+
if [ -n "$preserve_names" ]; then
|
|
371
|
+
set --
|
|
372
|
+
old_ifs=$IFS
|
|
373
|
+
IFS=,
|
|
374
|
+
for name in $preserve_names; do
|
|
375
|
+
is_set=
|
|
376
|
+
eval "is_set=\\\${$name+yes}"
|
|
377
|
+
[ -n "$is_set" ] || continue
|
|
378
|
+
eval "value=\\\${$name}"
|
|
379
|
+
set -- "$@" "$name=$value"
|
|
380
|
+
done
|
|
381
|
+
IFS=$old_ifs
|
|
382
|
+
"$env_helper" "$@"
|
|
383
|
+
return
|
|
384
|
+
fi
|
|
385
|
+
env -0 | xargs -0 -n 64 "$env_helper"
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
require_without_prompt
|
|
389
|
+
|
|
390
|
+
[ $# -gt 0 ] || fail 'sudo needs a command: sudo [OPTIONS] COMMAND (an interactive shell needs a terminal)'
|
|
391
|
+
|
|
392
|
+
# -s runs the command through a shell, which pkexec expresses exactly.
|
|
393
|
+
if [ "$mode" = shell ]; then
|
|
394
|
+
set -- /bin/sh -c "$*"
|
|
395
|
+
fi
|
|
396
|
+
|
|
397
|
+
# The preserved environment is exported by a shell prologue, so the command keeps
|
|
398
|
+
# its own argv rather than being flattened into one string.
|
|
399
|
+
if [ -n "$preserve" ]; then
|
|
400
|
+
check_preserve
|
|
401
|
+
prefix=$(export_prefix)
|
|
402
|
+
set -- /bin/sh -c "\${prefix}exec \\"\\$@\\"" sh "$@"
|
|
403
|
+
fi
|
|
404
|
+
|
|
105
405
|
if [ -n "$user" ]; then
|
|
106
|
-
|
|
406
|
+
set -- --user "$user" "$@"
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
# -b returns as soon as the command is started, as sudo -b does.
|
|
410
|
+
if [ -n "$background" ]; then
|
|
411
|
+
pkexec "$@" &
|
|
412
|
+
exit 0
|
|
107
413
|
fi
|
|
414
|
+
|
|
108
415
|
run_pkexec "$@"
|
|
109
416
|
`;
|
|
110
417
|
/** The doas shim: same contract, doas option grammar. */
|
|
@@ -112,14 +419,13 @@ const DOAS_SHIM = `#!/bin/sh
|
|
|
112
419
|
# opencode-polkit: doas shim. Routes doas to pkexec so polkit can authorize it.
|
|
113
420
|
# Generated at plugin startup; do not edit.
|
|
114
421
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
command -v pkexec >/dev/null 2>&1 || fail 'pkexec is not installed; install polkit'
|
|
121
|
-
|
|
422
|
+
${SHIM_PRELUDE}
|
|
423
|
+
${SHIM_CHECKS}
|
|
424
|
+
PROG=doas
|
|
122
425
|
user=
|
|
426
|
+
noninteractive=
|
|
427
|
+
mode=
|
|
428
|
+
|
|
123
429
|
while [ $# -gt 0 ]; do
|
|
124
430
|
case $1 in
|
|
125
431
|
--)
|
|
@@ -133,14 +439,15 @@ while [ $# -gt 0 ]; do
|
|
|
133
439
|
;;
|
|
134
440
|
-u?*)
|
|
135
441
|
user=\${1#-u}
|
|
136
|
-
[ -n "$user" ] || fail 'doas -u needs a username: -u USER PROGRAM'
|
|
137
442
|
shift
|
|
138
443
|
;;
|
|
139
444
|
-n)
|
|
140
|
-
|
|
445
|
+
noninteractive=1
|
|
446
|
+
shift
|
|
141
447
|
;;
|
|
142
448
|
-s)
|
|
143
|
-
|
|
449
|
+
mode=shell
|
|
450
|
+
shift
|
|
144
451
|
;;
|
|
145
452
|
-C | -a | -L)
|
|
146
453
|
fail "option has no pkexec equivalent: $1"
|
|
@@ -154,20 +461,18 @@ while [ $# -gt 0 ]; do
|
|
|
154
461
|
esac
|
|
155
462
|
done
|
|
156
463
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
fi
|
|
165
|
-
exit $status
|
|
166
|
-
}
|
|
464
|
+
require_without_prompt
|
|
465
|
+
|
|
466
|
+
[ $# -gt 0 ] || fail 'doas needs a command: doas [OPTIONS] COMMAND (an interactive shell needs a terminal)'
|
|
467
|
+
|
|
468
|
+
if [ "$mode" = shell ]; then
|
|
469
|
+
set -- /bin/sh -c "$*"
|
|
470
|
+
fi
|
|
167
471
|
|
|
168
472
|
if [ -n "$user" ]; then
|
|
169
|
-
|
|
473
|
+
set -- --user "$user" "$@"
|
|
170
474
|
fi
|
|
475
|
+
|
|
171
476
|
run_pkexec "$@"
|
|
172
477
|
`;
|
|
173
478
|
/**
|
|
@@ -180,13 +485,9 @@ const SU_SHIM = `#!/bin/sh
|
|
|
180
485
|
# opencode-polkit: su shim. Routes su -c to pkexec so polkit can authorize it.
|
|
181
486
|
# Generated at plugin startup; do not edit.
|
|
182
487
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
command -v pkexec >/dev/null 2>&1 || fail 'pkexec is not installed; install polkit'
|
|
189
|
-
|
|
488
|
+
${SHIM_PRELUDE}
|
|
489
|
+
${SHIM_CHECKS}
|
|
490
|
+
PROG=su
|
|
190
491
|
user=
|
|
191
492
|
cmd=
|
|
192
493
|
has_cmd=
|
|
@@ -234,7 +535,7 @@ while [ $# -gt 0 ]; do
|
|
|
234
535
|
fail 'su -w whitelists environment variables; pkexec sanitizes the environment.'
|
|
235
536
|
;;
|
|
236
537
|
-P | --pty)
|
|
237
|
-
fail 'su --pty asks for a terminal; the
|
|
538
|
+
fail 'su --pty asks for a terminal; the bash tool has none.'
|
|
238
539
|
;;
|
|
239
540
|
-f | --fast)
|
|
240
541
|
fail 'su -f is a shell flag; pass it to the shell in the command instead.'
|
|
@@ -267,24 +568,68 @@ done
|
|
|
267
568
|
[ -n "$has_cmd" ] || fail 'su without -c opens an interactive shell; run pkexec bash instead.'
|
|
268
569
|
|
|
269
570
|
# su keeps the working directory unless it is a login shell, and --keep-cwd is
|
|
270
|
-
# how pkexec does the same.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
571
|
+
# how pkexec does the same.
|
|
572
|
+
run_pkexec --keep-cwd --user "$user" sh -c "$cmd"
|
|
573
|
+
`;
|
|
574
|
+
/**
|
|
575
|
+
* The environment helper: turns `NAME=VALUE` arguments into an export statement
|
|
576
|
+
* each, so `-E` can rebuild the environment pkexec stripped. Values arrive as
|
|
577
|
+
* arguments rather than text, so spaces, quotes and newlines need no escaping on
|
|
578
|
+
* the way in. PATH keeps every entry except the shim directories.
|
|
579
|
+
*/
|
|
580
|
+
const POLKIT_ENV_HELPER = `#!/bin/sh
|
|
581
|
+
# opencode-polkit: rebuilds a stripped environment. Generated at plugin startup;
|
|
582
|
+
# do not edit.
|
|
583
|
+
|
|
584
|
+
for entry in "$@"; do
|
|
585
|
+
name=\${entry%%=*}
|
|
586
|
+
value=\${entry#*=}
|
|
587
|
+
case $name in
|
|
588
|
+
'' | [0-9]* | *[!A-Za-z0-9_]*)
|
|
589
|
+
continue
|
|
590
|
+
;;
|
|
591
|
+
PATH)
|
|
592
|
+
filtered=
|
|
593
|
+
old_ifs=$IFS
|
|
594
|
+
IFS=:
|
|
595
|
+
for dir in $value; do
|
|
596
|
+
case \${dir##*/} in
|
|
597
|
+
opencode-polkit-*)
|
|
598
|
+
continue
|
|
599
|
+
;;
|
|
600
|
+
esac
|
|
601
|
+
filtered=$filtered\${filtered:+:}$dir
|
|
602
|
+
done
|
|
603
|
+
IFS=$old_ifs
|
|
604
|
+
value=$filtered
|
|
605
|
+
;;
|
|
606
|
+
esac
|
|
607
|
+
quote="'"
|
|
608
|
+
quoted=
|
|
609
|
+
while :; do
|
|
610
|
+
case $value in
|
|
611
|
+
*"$quote"*)
|
|
612
|
+
quoted=$quoted\${value%%"$quote"*}"'\\\\''"
|
|
613
|
+
value=\${value#*"$quote"}
|
|
614
|
+
;;
|
|
615
|
+
*)
|
|
616
|
+
quoted=$quoted$value
|
|
617
|
+
break
|
|
618
|
+
;;
|
|
619
|
+
esac
|
|
620
|
+
done
|
|
621
|
+
printf "export '%s=%s'; " "$name" "$quoted"
|
|
622
|
+
done
|
|
278
623
|
`;
|
|
279
624
|
/** sudoedit edits through sudo's own temporary-file flow; polkit has none. */
|
|
280
625
|
const SUDOEDIT_SHIM = `#!/bin/sh
|
|
281
|
-
|
|
282
|
-
|
|
626
|
+
${SHIM_PRELUDE}
|
|
627
|
+
fail 'sudoedit is not supported; write a temp file and run pkexec install or pkexec cp on it.'
|
|
283
628
|
`;
|
|
284
629
|
/** visudo needs an interactive editor on a terminal the shell tool does not have. */
|
|
285
630
|
const VISUDO_SHIM = `#!/bin/sh
|
|
286
|
-
|
|
287
|
-
|
|
631
|
+
${SHIM_PRELUDE}
|
|
632
|
+
fail 'visudo needs an interactive terminal; validate a candidate file with pkexec visudo -c -f FILE instead.'
|
|
288
633
|
`;
|
|
289
634
|
/** File name → script body. Names must match the commands being redirected. */
|
|
290
635
|
const POLKIT_SHIM_SCRIPTS = {
|
|
@@ -293,12 +638,15 @@ const POLKIT_SHIM_SCRIPTS = {
|
|
|
293
638
|
su: SU_SHIM,
|
|
294
639
|
sudoedit: SUDOEDIT_SHIM,
|
|
295
640
|
visudo: VISUDO_SHIM,
|
|
641
|
+
"opencode-polkit-env": POLKIT_ENV_HELPER,
|
|
296
642
|
};
|
|
297
643
|
/**
|
|
298
644
|
* Writes the shim scripts into a fresh private directory and returns it.
|
|
299
645
|
* The caller prepends this directory to PATH; the shell then resolves
|
|
300
646
|
* `sudo`/`doas`/`su` to these files instead of the system ones. The directory
|
|
301
|
-
* is 0700 so no other local user can rewrite the scripts
|
|
647
|
+
* is 0700 so no other local user can rewrite the scripts, and it holds
|
|
648
|
+
* `opencode-polkit-env`, the helper `sudo -E` uses to re-export an environment
|
|
649
|
+
* pkexec stripped.
|
|
302
650
|
*/
|
|
303
651
|
export function installPolkitShims() {
|
|
304
652
|
const directory = mkdtempSync(join(shimBaseDirectory(), "opencode-polkit-"));
|
package/dist/shims.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shims.js","sourceRoot":"","sources":["../src/shims.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACnF,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAE5C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,EAAE,CAAA;AAEjC
|
|
1
|
+
{"version":3,"file":"shims.js","sourceRoot":"","sources":["../src/shims.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-polkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Redirect sudo, doas and su to pkexec in OpenCode via PATH shims, so the system polkit agent handles authentication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
23
|
-
"check": "tsc -p tsconfig.json --noEmit"
|
|
23
|
+
"check": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
|
|
24
|
+
"test": "node --experimental-strip-types --test test/*.test.ts",
|
|
25
|
+
"prepublishOnly": "npm run check && npm test && npm run build"
|
|
24
26
|
},
|
|
25
27
|
"engines": {
|
|
26
28
|
"opencode": ">=2.0.29"
|