3b increment 4a: polkit authorization decision - #5
Merged
Merged
Conversation
Add the §4.3-step-2 decision layer (R/polkit.R): given a verb and the
mode, decide whether a commit may proceed. This is the decision only; the
next increment wires it into .commit_session (authorized -> effect intent;
a machine-mode refusal opens a plain intent + terminal outcome and stops).
.authorize(verb_spec, interactive)
interactive -> "authorized" WITHOUT pkcheck; the pkexec prompt
authenticates at the entrypoint spawn later
machine -> a native, non-interactive pkcheck for the verb's action
(ai.cornball.runix.apt.<verb>) against this process's
race-safe pid,start-time,uid subject, mapped to a
decision
pkcheck exit code -> decision, pinned to the tested canary matrix
(deploy/canary-apt/polkit-matrix.sh): 0 authorized, 1 unauthorized, 2/3
approval_required (a challenge unobtainable non-interactively), else
check_failed (fail closed, never silently authorized).
The pkcheck call is behind an injectable seam (set_pkcheck), so the
decision is hermetic (36 tests, no real polkit). It is NOT a privilege
boundary: polkit still enforces at the pkexec spawn inside runix's C, so a
substituted check can only make pkgops proceed to a commit pkexec then
denies, or refuse one it would have allowed -- both degrade safely.
Autonomous update/hold need no special-casing: the runix-apt-autonomous
rule grants members rc 0 through the same check.
Boundary for review: the exact rc 1-vs-2 split (unauthorized vs
approval_required) is pinned to the canary contract and confirmed against
real polkit in the VM-gated increment.
.pkcheck_decision() guarded only finiteness, so as.integer() would truncate a fractional 1.5 to 1 and classify it as `unauthorized` rather than `check_failed`. Require a finite, scalar, integer-valued numeric (rc == floor(rc)) before coercion, so a malformed/non-integral result fails closed. Regression tests for 1.5, 2.5, 0.9, NA_real_; a integer-valued double (2.0) still maps normally.
Contributor
Author
|
Fixed in the latest commit. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First reviewed slice of the polkit authorization branch (contract §4.4, lifecycle
step 2), held before it is wired into
.commit_session. Pure decision + aninjectable
pkcheckseam — no broker, no intent, no dpkg.Same cadence as increment 2 (the classifier) → increment 3 (the orchestration):
build the decision first, wire it next.
What this adds
.authorize(verb_spec, interactive)→ one of a closed decision vocabulary:"authorized"without runningpkcheck— thepkexecprompt authenticates at the entrypoint spawn later (a cancelled prompt becomes a known-falseunauthorizedoutcome on the effect intent)pkcheckfor the verb's polkit action against this process's race-safepid,start-time,uidsubject, and maps the exit codeAction id:
ai.cornball.runix.apt.<verb>(pinned topkgexec/polkit/ai.cornball.runix.apt.policy).pkcheckexit code → decision, pinned to the tested canary matrix(
deploy/canary-apt/polkit-matrix.sh:17-18):authorizedunauthorized(flat deny)approval_required(a challenge that can't be obtained non-interactively)check_failed— fail closed, never silently authorizedAutonomous
update/holdneed no special-casing: therunix-apt-autonomousrule grants members
rc 0through the same check; a non-member falls through to arefusal like any other verb.
Not a privilege boundary
The
pkcheckcall is behind an injectable seam (set_pkcheck()) so the decisionis hermetic (36 tests, no real polkit; 401 total). polkit still enforces at the
pkexecspawn inside runix's C, so a test-substituted check can only make pkgopsproceed to a commit
pkexecthen denies, or refuse one it would have allowed —both degrade safely.
pkcheckcarries no secret (unlike the pkexec commit), so asystem2spawn without a shell is appropriate.Deferred to later increments (marked in source)
.commit_session: authorized proceeds to theeffect intent; a machine-mode refusal opens a plain intent via
runix::broker_audit_sink()/audit_two_phase()and writes the terminalunauthorized/approval_requiredoutcome (effect_issued=FALSE) under onecorrelation_id, then stops (contract §4.4, "no unused effect receipt for a
refusal"). Mode is a caller-supplied parameter — runix exposes no TTY probe, so
detection stays at the rctl/CLI layer.
pkgstateverification, then the exported per-verbapt_<verb>()API.Point for review
The exact
rc 1-vs-2split (unauthorizedvsapproval_required) is pinned tothe canary contract; it's confirmed against real polkit behaviour in the VM-gated
increment where the terminal outcomes meet a live broker.
CI hermetic;
R CMD checkclean but for the expected "New submission" WARNING.