Ask for the sudo password once per omarchy update (#13323)

* Ask for the sudo password once per omarchy update

Every sudo call in omarchy update prompted, because the no-update wrapper
covered the whole run on top of per-phase revokes, and stay-awake revoked
the timestamp on its own entry and exit. A single update could ask four
times before the snapshot finished (#13319).

Authorize once, right after confirmation, starting from a revoked
timestamp so the prompt always belongs to this update. A background
keepalive refreshes it until the update is done. Prune, snapshot,
stay-awake, keyring, system packages, migrations, orphan removal, service
restarts, the post-update hook, and mise all share that authorization.

AUR builds run third-party PKGBUILD code, so they move to the end and run
cold: the keepalive stops, the timestamp is revoked, and yay and any bare
sudo use the no-update wrapper. The timestamp is revoked again after AUR
and on every exit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the single authorization for passwordless sudo and ttyless inhibition

Authorize by running a command instead of sudo -v. Under the default
verifypw=all, -v prompts even when passwordless sudo is enabled, which
would have added a prompt those users never had.

Inside an update without a terminal, stay-awake now reuses the update's
authorization with a non-interactive sudo instead of asking again through
polkit. It falls back to polkit only if that authorization is gone.

The test sudo refuses a cold non-interactive call, as the real one does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
David Heinemeier Hansson
2026-09-26 15:31:40 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 7b336b1b0d
commit e1614f2bdb
9 changed files with 163 additions and 51 deletions
+58 -21
View File
@@ -18,20 +18,62 @@ set -e
omarchy_security_sanitize_bash_environment "$0" "$@"
omarchy_security_require_source_root "$0"
# Logging and lock acquisition re-exec this command with a sanitized PATH.
# Preserve the caller's path only for the later unprivileged hook/mise phases.
# Preserve the caller's path only for the later hook/mise phases.
user_path=${OMARCHY_UPDATE_USER_PATH:-$PATH}
unset OMARCHY_UPDATE_USER_PATH
# Traps first, so a signal or failure during the entry revocation still
# exits through the cleanup path.
# exits through the cleanup path. Starting cold means the single password
# prompt below always belongs to this update, never to an earlier session.
omarchy_security_install_sudo_cleanup_traps
omarchy_security_revoke_sudo_timestamp || exit 1
# Validate the no-update wrapper up front: the AUR phase depends on it. Every
# other phase shares the one authorization, so it stays off PATH until then.
omarchy_security_enable_no_update_sudo
PATH="$OMARCHY_PATH/bin:/usr/bin:/usr/sbin:/bin:/sbin"
unset OMARCHY_SUDO_NO_UPDATE
# Helpers that manage their own sudo boundary leave this authorization to us.
export PATH OMARCHY_UPDATE_SUDO_SESSION=1
update_stay_awake_stopped=0
sudo_keepalive_pid=""
# Ask for the password once, then keep sudo's timestamp fresh so long package
# downloads, migrations, hooks, and mise never outlast it and prompt again.
# Authorize by running a command rather than sudo -v: under the default
# verifypw=all, -v prompts even when passwordless sudo is enabled.
authorize_update() {
/usr/bin/sudo /usr/bin/true || return 1
local update_pid=$$
(
[[ -z ${OMARCHY_UPDATE_LOCK_FD:-} ]] || exec {OMARCHY_UPDATE_LOCK_FD}>&-
sleep_pid=""
trap - EXIT HUP INT
trap '[[ -z $sleep_pid ]] || kill "$sleep_pid" 2>/dev/null; exit 0' TERM
while :; do
sleep 60 &
sleep_pid=$!
wait "$sleep_pid"
kill -0 "$update_pid" 2>/dev/null || exit 0
/usr/bin/sudo -n /usr/bin/true 2>/dev/null || exit 0
done
) &
sudo_keepalive_pid=$!
}
end_update_authorization() {
if [[ -n $sudo_keepalive_pid ]]; then
kill "$sudo_keepalive_pid" 2>/dev/null || true
wait "$sudo_keepalive_pid" 2>/dev/null || true
sudo_keepalive_pid=""
fi
omarchy_security_revoke_sudo_timestamp
}
cleanup_update() {
local status=$?
trap - EXIT HUP INT TERM
if ! omarchy_security_revoke_sudo_timestamp; then
if ! end_update_authorization; then
echo "Could not invalidate sudo before update cleanup." >&2
omarchy_security_exit_with_revoked_sudo 1
fi
@@ -61,6 +103,8 @@ omarchy-update-requires-free-space
[[ ${1:-} != "-y" ]] || export OMARCHY_UPDATE_UNATTENDED=1
if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
authorize_update
# Before the snapshot: the cache is on the snapshotted subvolume, so pruning
# after it frees nothing until that snapshot ages out.
omarchy-update-pkg-prune
@@ -82,33 +126,26 @@ if [[ ${1:-} == "-y" ]] || omarchy-update-confirm; then
# takes the update with it rather than migrating against what is still on disk.
omarchy-update-system-pkgs
# Historical migrations are strictly ordered and mix user hooks/downloaded
# tooling with privileged repairs. The no-update sudo wrapper has covered the
# whole update, so neither the package transaction nor a later repair can
# publish a timestamp to a detached migration child.
omarchy_security_revoke_sudo_timestamp
omarchy-migrate
omarchy-update-orphan-pkgs
omarchy-update-analyze-logs
omarchy-update-status
# Service restart helpers can need sudo. Run them before any user-controlled
# update tooling; the reboot-only phase below performs no privileged work.
# Service restart helpers can need sudo. The reboot-only phase below
# performs no privileged work.
omarchy-update-restart --services-only
# AUR package installation must also use the no-update wrapper. Finish
# update-owned system work before build code, hooks, or mise can run.
omarchy_security_revoke_sudo_timestamp
omarchy-update-aur-pkgs
omarchy_security_revoke_sudo_timestamp
# Hooks and mise run with the caller's PATH so user-installed tools resolve.
PATH="$user_path" "$OMARCHY_PATH/bin/omarchy-hook" post-update
PATH="$user_path" "$OMARCHY_PATH/bin/omarchy-update-mise"
# Hooks and mise execute user-controlled code. Give each a cold credential
# boundary and run mise last so it cannot wait for a legitimate hook sudo.
# Only the unprivileged reboot prompt follows them.
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$user_path" "$OMARCHY_PATH/bin/omarchy-hook" post-update
omarchy_security_revoke_sudo_timestamp
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$user_path" "$OMARCHY_PATH/bin/omarchy-update-mise"
# AUR builds run third-party PKGBUILD code, so they go last and never see the
# update's authorization: revoke it, then authenticate each sudo call with
# the no-update wrapper, for yay and any bare sudo a PKGBUILD runs, so an AUR
# install prompts without caching anything.
end_update_authorization
PATH="$OMARCHY_PATH/default/omarchy/sudo-no-update:$PATH" OMARCHY_SUDO_NO_UPDATE=1 omarchy-update-aur-pkgs
omarchy_security_revoke_sudo_timestamp
# The sleep inhibitor covers AUR builds, hooks and mise as well; releasing it
+14 -3
View File
@@ -15,6 +15,13 @@ omarchy_security_require_privileged_bash_startup || exit 126
set -e
omarchy_security_sanitize_bash_environment "$0" "$@"
omarchy_security_require_source_root "$0"
# omarchy update authorizes once and revokes when it is done. Revoking here
# would throw that away and prompt again for the rest of the update.
update_sudo_session=0
if [[ ${OMARCHY_UPDATE_SUDO_SESSION:-} == "1" ]]; then
update_sudo_session=1
omarchy_security_revoke_sudo_timestamp() { :; }
fi
# Traps first, so a signal or failure during the entry revocation still
# exits through the cleanup path.
omarchy_security_install_sudo_cleanup_traps
@@ -504,11 +511,15 @@ start_locked() {
exec "${hold_command[@]}"
) &
launcher_pid=$!
elif [[ -t 0 ]]; then
elif [[ -t 0 ]] || { (( update_sudo_session )) && /usr/bin/sudo -n -N /usr/bin/true 2>/dev/null; }; then
# Inside an update without a terminal, reuse its authorization
# non-interactively rather than asking again through polkit.
sudo_options=(-N -b)
[[ -t 0 ]] || sudo_options=(-n -N -b)
if ! (
[[ -z ${OMARCHY_UPDATE_LOCK_FD:-} ]] || exec {OMARCHY_UPDATE_LOCK_FD}>&-
exec {state_lock_fd}>&-
exec /usr/bin/sudo -N -b -- "${hold_command[@]}"
exec /usr/bin/sudo "${sudo_options[@]}" -- "${hold_command[@]}"
); then
return 1
fi
@@ -535,7 +546,7 @@ start_locked() {
return 1
fi
(( readiness_attempts += 1 ))
if [[ -t 0 ]] && (( EUID != 0 && readiness_attempts >= 100 )); then
if [[ -z $launcher_pid ]] && (( EUID != 0 && readiness_attempts >= 100 )); then
echo "The update sleep inhibitor did not become ready." >&2
return 1
fi
+10 -9
View File
@@ -57,7 +57,7 @@ privileged work should invoke the appropriate helper or privilege prompt.
Migrations must be idempotent; if one user already applied a machine-wide repair,
the migration should no-op for other users.
When invoked by the update, migrations inherit its cold credential state and no-update sudo wrapper. The standalone migration runner has its own security changes in the migration-boundary PR; this update change does not establish that standalone boundary. Historical migrations remain strictly ordered.
When invoked by the update, migrations share its single sudo authorization. The standalone migration runner has its own security changes in the migration-boundary PR; this update change does not establish that standalone boundary. Historical migrations remain strictly ordered.
For watchers and diagnostics, `omarchy-migrate --pending` prints pending
migration names and exits `0` when any are pending. When no migrations are
@@ -126,6 +126,7 @@ omarchy-update
├─ omarchy-update-requires-free-space
│ └─ abort below the configured free-space threshold on /
├─ confirm unless -y
├─ authorize sudo once, then keep the timestamp fresh in the background
├─ omarchy-update-pkg-prune
│ └─ trim the pacman cache to two versions per package, deliberately
│ before the snapshot since the cache lives on the snapshotted subvolume
@@ -134,14 +135,14 @@ omarchy-update
│ install/config/snapper.sh, and the update continues without one)
├─ omarchy-update-stay-awake start
├─ run system-package updates
├─ invalidate sudo, then run migrations and all later privileged work with
│ no-update authentication
├─ run migrations
├─ run orphan review and log analysis
├─ omarchy-update-status
│ └─ refresh or clear the shell update indicator
├─ restart marked services and the shell
├─ invalidate sudo credentials, then update AUR packages
├─ invalidate again, run the post-update hook, invalidate again, then update mise tools
├─ run the post-update hook, then update mise tools
├─ stop the keepalive and invalidate sudo, then update AUR packages with
│ no-update authentication, and invalidate again
├─ omarchy-update-stay-awake stop
│ └─ release the sleep inhibitor and restore shell idle state, if changed
└─ offer the unprivileged reboot prompt
@@ -149,15 +150,15 @@ omarchy-update
Important behavior:
- Protected update entrypoints require the session's canonical `OMARCHY_PATH` to match their own checkout or the packaged `/usr/bin` entrypoint before selecting commands or the sudo wrapper. This preserves intentionally trusted development checkouts while rejecting a command paired with a different source root. System phases use a fixed command search path; user PATH is restored behind the sudo wrapper for hooks and mise.
- Protected update entrypoints require the session's canonical `OMARCHY_PATH` to match their own checkout or the packaged `/usr/bin` entrypoint before selecting commands or the sudo wrapper. This preserves intentionally trusted development checkouts while rejecting a command paired with a different source root. System phases use a fixed command search path; user PATH is restored for hooks and mise.
- Mixed-trust update entrypoints start Bash in privileged mode, discard `BASH_ENV`, `ENV`, and exported-function records before launching helpers, and reject an ordinary `bash path/to/command` invocation. Run them as executables (normally through the `omarchy` CLI); `/usr/bin/bash -p path/to/command` is the explicit interpreter form. This keeps shell startup injection from replacing the no-update sudo boundary.
- In dev-link mode, `omarchy update` fast-forwards the active checkout from its configured upstream before changing system packages or running migrations.
- Migrations remain in chronological order even though historical entries mix user-controlled code with later privileged repairs. Before entering that mixed-trust tail, Omarchy invalidates its timestamp and forces every later sudo call—including AUR's configurable sudo command—to use `--no-update`; prompts authorize one command without publishing a reusable timestamp. Yay's credential loop is disabled for the update.
- User-controlled post-update hooks and mise tools run only after every sudo-capable update stage. Omarchy invalidates its sudo timestamp before each boundary and on every exit; detached children therefore have no later reusable update authorization to wait for.
- The update asks for the sudo password once, right after confirmation. It first invalidates any existing timestamp so that prompt always belongs to this update, then a background keepalive refreshes the timestamp every minute so long downloads, migrations, hooks, and mise never outlast it. Everything except AUR shares that one authorization: package prune, snapshot, stay-awake, keyring, system packages, migrations, orphan removal, service restarts, the post-update hook, and mise. Stay-awake sees `OMARCHY_UPDATE_SUDO_SESSION=1`, uses that authorization non-interactively whatever its stdin is, and leaves it to the update instead of revoking it. The authorization runs a command rather than `sudo -v`, so passwordless sudo still needs no prompt. Standalone commands that keep their own cold boundary, such as `omarchy-refresh-pacman`, still revoke if a post-update hook calls them.
- AUR builds run third-party PKGBUILD code, so they run last and never see the update's authorization. The update stops the keepalive, invalidates the timestamp, and runs yay with the no-update wrapper as its sudo command and its credential loop disabled; an AUR install prompts per command without publishing a reusable timestamp. The timestamp is invalidated again afterwards and on every exit.
- This lifecycle controls authorization created by the protected workflow. `sudo -N` prevents cache updates but can use an existing valid credential, and `sudo -k` revokes the current session's timestamp. It does not isolate the account from unrelated concurrent authentication in another workflow.
- Sleep inhibition authenticates before detaching, drops the held command back to the caller, and closes both update lock descriptors before the persistent process starts. Cleanup accepts only caller-owned, mode-0600, single-link state and revalidates the recorded PID, process start time, owner, and random token immediately before every signal.
- Channel switching establishes the same boundary before dev link/unlink, refresh and package operations. It keeps the wrapper first when changing source roots, carries the original user PATH into update hooks and mise, and checks after each package transaction that the wrapper still exists before any further privileged step, since a transaction can replace the running tree with a release that predates it; when it is gone, or the destination otherwise lacks it, the switch stops after the package switch with instructions to run that release's update from a fresh session rather than letting a bare `sudo` or an updater that authenticates without `--no-update` publish a timestamp. Failed and interrupted channel switches revoke on exit.
- `-y` exports `OMARCHY_UPDATE_UNATTENDED=1` and suppresses Omarchy confirmation prompts. Interactive review steps (orphan removal, conflict handoff) report and skip instead of blocking. Privileged commands still require sudo authorization, and command-scoped authentication can prompt separately for each command.
- `-y` exports `OMARCHY_UPDATE_UNATTENDED=1` and suppresses Omarchy confirmation prompts. Interactive review steps (orphan removal, conflict handoff) report and skip instead of blocking. Privileged commands still require the one sudo authorization, and AUR installs can prompt separately.
- The free-space requirement uses a 10 GiB threshold and stops the update before
confirmation when it is not met. If free space cannot be determined, the
check is silently skipped. Set `OMARCHY_UPDATE_FORCE=1` to bypass the check.
+18 -8
View File
@@ -43,7 +43,10 @@ assert_scoped_channel() {
import sys
events = open(sys.argv[1]).read().splitlines()
assert events[0] == 'sudo -k', events
sudo = [event for event in events if event.startswith('sudo ')]
# The switch itself authorizes command by command. The update it hands off to
# starts cold and authorizes once for its own phases.
auth = events.index('sudo /usr/bin/true')
sudo = [event for event in events[:auth] if event.startswith('sudo ')]
assert all(event in ('sudo -h', 'sudo -k') or event.startswith('sudo -N ') for event in sudo), events
hooks = [i for i, event in enumerate(events) if event.startswith('step:omarchy-hook ')]
assert len(hooks) == 2, events
@@ -51,8 +54,8 @@ assert events[hooks[0]] == 'step:omarchy-hook pre-refresh-pacman', events
assert events[hooks[1]] == 'step:omarchy-hook post-update', events
assert events[hooks[0] - 1] == 'sudo -k' and events[hooks[0] + 1] == 'sudo -k', events
transaction = next(i for i, event in enumerate(events) if event.startswith('step:pacman '))
assert hooks[0] < transaction, events
assert not any(event.startswith('sudo -N ') for event in events[hooks[1]:]), events
assert hooks[0] < transaction < auth < hooks[1], events
assert 'sudo -k' in events[transaction:auth], events
PY
}
@@ -63,7 +66,7 @@ for channel in stable rc edge dev; do
reset_boundary
run_channel "$channel" || fail "$channel failed" "$(<"$boundary_tmp/output")"
assert_scoped_channel "$channel"
pass "$channel starts cold, authorizes only individual commands, runs the refresh hook cold before its transaction and exits cold"
pass "$channel starts cold, authorizes the switch per command, runs the refresh hook cold, hands off to one update authorization and exits cold"
done
reset_boundary
@@ -81,7 +84,7 @@ pass "a stale dev checkout is rejected before linking or privileged work"
reset_boundary
OMARCHY_PATH="$SUDO_TEST_HOME/omarchy" run_channel stable || fail "leaving dev failed" "$(<"$boundary_tmp/output")"
assert_scoped_channel "dev to stable"
pass "leaving dev preserves no-update sudo through unlink and the packaged update"
pass "leaving dev preserves no-update sudo through unlink and hands off to the packaged update"
# A packaged destination that predates the wrapper cannot be checked before its
# package is installed. Its updater authenticates without --no-update, so the
@@ -107,6 +110,7 @@ pass "an older packaged destination stops the switch cold with instructions inst
# without the wrapper. From then on a bare sudo would be the real one, so no
# privileged step may follow either transaction without checking first. A decoy
# sudo in the package bin catches any such call instead of reaching the host.
rm "$SUDO_TEST_ROOT/bin/sudo"
cat >"$SUDO_TEST_ROOT/bin/sudo" <<'STUB'
#!/bin/bash
printf 'unwrapped-sudo %s\n' "$*" >>"$SUDO_TEST_LOG"
@@ -135,6 +139,7 @@ PY
pass "a transaction that removes the wrapper stops the switch before any further sudo ($pattern)"
done
rm "$SUDO_TEST_ROOT/bin/sudo"
ln -s ../mock/sudo "$SUDO_TEST_ROOT/bin/sudo"
mkdir "$boundary_tmp/user tools"
cat >"$boundary_tmp/user tools/channel-user-tool" <<'STUB'
@@ -146,8 +151,13 @@ for command in omarchy-hook omarchy-update-mise; do
rm "$SUDO_TEST_ROOT/bin/$command"
cat >"$SUDO_TEST_ROOT/bin/$command" <<'STUB'
#!/bin/bash
[[ ! -e $SUDO_TEST_CACHE ]] || exit 91
[[ $(command -v sudo) == "$OMARCHY_PATH/default/omarchy/sudo-no-update/sudo" ]] || exit 92
if [[ ${1:-} == "pre-refresh-pacman" ]]; then
[[ ! -e $SUDO_TEST_CACHE ]] || exit 91
[[ $(command -v sudo) == "$OMARCHY_PATH/default/omarchy/sudo-no-update/sudo" ]] || exit 92
else
[[ -e $SUDO_TEST_CACHE ]] || exit 94
[[ $(command -v sudo) != "$OMARCHY_PATH/default/omarchy/sudo-no-update/sudo" ]] || exit 95
fi
channel-user-tool "${0##*/}" "$@"
STUB
chmod +x "$SUDO_TEST_ROOT/bin/$command"
@@ -161,7 +171,7 @@ assert_boundary_cold "channel user PATH"
for command in omarchy-hook omarchy-update-mise; do
ln -sfn test-step "$SUDO_TEST_ROOT/bin/$command"
done
pass "channel switching preserves user tools behind the wrapper for both hooks and mise"
pass "channel switching preserves user tools for both hooks and mise"
for step in pacman omarchy-update-system-pkgs omarchy-hook; do
reset_boundary
+13 -1
View File
@@ -52,6 +52,10 @@ if [[ ${1:-} == "-k" || ${1:-} == "-K" ]]; then
/usr/bin/rm -f "$SUDO_TEST_CACHE"
exit 0
fi
if [[ ${1:-} == "-n" && ! -e $SUDO_TEST_CACHE ]]; then
# Non-interactive sudo cannot authenticate without a cached credential.
exit 1
fi
if [[ ${1:-} == "-N" ]]; then
shift
else
@@ -77,6 +81,9 @@ else
fi
STUB
chmod +x "$SUDO_TEST_ROOT/mock/sudo"
# The updater's own phases call a bare sudo from its fixed PATH. Resolve it to
# the stand-in so no test can ever reach the host's sudo.
ln -s ../mock/sudo "$SUDO_TEST_ROOT/bin/sudo"
cat >"$SUDO_TEST_ROOT/bin/test-step" <<'STUB'
#!/bin/bash
@@ -89,7 +96,11 @@ if [[ $step == "systemd-run" ]]; then
while (( $# )) && [[ $1 == -* ]]; do shift; done
exec "$@"
fi
if [[ $step == "omarchy-hook" || $step == "omarchy-update-mise" ]]; then
# omarchy update shares one authorization with its post-update hook and mise.
# Standalone hooks, such as the pre-refresh one, and AUR builds run cold.
if [[ $step == "omarchy-hook" && ${1:-} == "post-update" ]] || [[ $step == "omarchy-update-mise" ]]; then
[[ -e $SUDO_TEST_CACHE ]] || exit 94
elif [[ $step == "omarchy-hook" || $step == "yay" ]]; then
[[ ! -e $SUDO_TEST_CACHE ]] || exit 91
fi
if [[ -n ${SUDO_TEST_REMOVE_WRAPPER_STEP:-} && "$step $*" == $SUDO_TEST_REMOVE_WRAPPER_STEP ]]; then
@@ -116,6 +127,7 @@ case "$step" in
yay)
[[ $* == *"--sudo $OMARCHY_PATH/default/omarchy/sudo-no-update/sudo"* ]] || exit 92
[[ $* == *"--sudoloop=false"* ]] || exit 93
[[ $(command -v sudo) == "$OMARCHY_PATH/default/omarchy/sudo-no-update/sudo" ]] || exit 95
;;
esac
STUB
+18 -4
View File
@@ -20,15 +20,29 @@ for args in '-y' ''; do
touch "$SUDO_TEST_CACHE"
run_update $args || fail "update failed" "$(<"$boundary_tmp/output")"
assert_boundary_cold "successful update"
grep -q '^sudo -N /usr/bin/true$' "$SUDO_TEST_LOG" || fail "update package helpers must use no-update sudo"
python3 - "$SUDO_TEST_LOG" <<'PY'
import sys
s=open(sys.argv[1]).read().splitlines()
positions=[next(i for i,line in enumerate(s) if line.startswith(prefix)) for prefix in ['step:omarchy-update-restart --services-only','step:yay','step:omarchy-hook post-update','step:omarchy-update-mise','step:omarchy-update-stay-awake stop','step:omarchy-update-restart --reboot-only']]
# A cached credential from before the update is revoked, then the update
# authorizes exactly once before any step that could need sudo.
assert s[0]=='sudo -k', s
# Authorization runs a command (sudo -v prompts even with passwordless sudo).
prune=s.index('step:omarchy-update-pkg-prune ')
auth=s.index('sudo /usr/bin/true')
assert auth < prune, s
assert [l for l in s[:prune] if l.startswith('sudo ') and l not in ('sudo -k','sudo -h')]==['sudo /usr/bin/true'], s
assert 'sudo -v' not in s, s
positions=[next(i for i,line in enumerate(s) if line.startswith(prefix)) for prefix in ['step:omarchy-update-system-pkgs','step:omarchy-migrate','step:omarchy-update-restart --services-only','step:omarchy-hook post-update','step:omarchy-update-mise','step:yay','step:omarchy-update-stay-awake stop','step:omarchy-update-restart --reboot-only']]
assert positions==sorted(positions), s
assert not any(line.startswith('sudo -N ') for line in s[positions[2]:]), s
yay=positions[5]
# Everything before AUR shares the one authorization: plain sudo, no revokes.
assert not any(line=='sudo -k' or line.startswith('sudo -N ') for line in s[auth:positions[4]]), s
assert 'sudo /usr/bin/true' in s[auth:positions[0]+1], s
# AUR builds start from a revoked credential and cannot refresh one.
assert 'sudo -k' in s[positions[4]:yay], s
assert not any(line.startswith('sudo ') and line!='sudo -k' and not line.startswith('sudo -N ') for line in s[yay:]), s
PY
pass "update $args runs privileged phases before hooks and exits cold"
pass "update $args authorizes once for everything but AUR, which runs cold last, and exits cold"
done
for step in omarchy-update-system-pkgs yay omarchy-hook omarchy-update-mise; do
+1 -1
View File
@@ -73,9 +73,9 @@ expected_steps() {
omarchy-update-analyze-logs \
omarchy-update-status \
omarchy-update-restart \
omarchy-update-aur-pkgs \
omarchy-hook \
omarchy-update-mise \
omarchy-update-aur-pkgs \
omarchy-update-stay-awake \
omarchy-update-restart
}
@@ -37,6 +37,7 @@ mkdir -p "$stub_bin" "$test_home" "$mapped_root/bin" "$mapped_root/default/omarc
cat >"$stub_bin/pkexec" <<'SH'
#!/bin/bash
[[ -z ${SUDO_EVENT_LOG:-} ]] || printf 'pkexec\n' >>"$SUDO_EVENT_LOG"
exec "$@"
SH
@@ -44,8 +45,12 @@ cat >"$stub_bin/sudo" <<'SH'
#!/bin/bash
case ${1:-} in
-h) echo 'usage: sudo [-bHkNnPS] command'; exit 0 ;;
-k|-K|-v) exit 0 ;;
-k|-K|-v)
[[ -z ${SUDO_EVENT_LOG:-} ]] || printf 'sudo %s\n' "$1" >>"$SUDO_EVENT_LOG"
exit 0
;;
esac
[[ -z ${SUDO_EVENT_LOG:-} ]] || printf 'sudo %s\n' "$*" >>"$SUDO_EVENT_LOG"
background=0
while (( $# )); do
case "$1" in
@@ -173,9 +178,31 @@ read -r version valid_pid valid_start valid_owner valid_token <"$state_dir/inhib
fail "inhibitor state is private, caller-owned, and singly linked"
run_helper stop
wait_dead "$valid_pid" || fail "valid inhibitor identity is stopped"
[[ ! -e $state_dir ]] || fail "valid state is cleaned after stop"
pass "valid XDG runtime uses private atomic inhibitor state"
# omarchy update owns its one authorization; the helper must not revoke it.
# Run on its own, the helper still starts and ends cold.
sudo_events="$test_tmp/sudo-events"
: >"$sudo_events"
OMARCHY_UPDATE_SUDO_SESSION=1 SUDO_EVENT_LOG="$sudo_events" run_helper start
OMARCHY_UPDATE_SUDO_SESSION=1 SUDO_EVENT_LOG="$sudo_events" run_helper stop
! grep -qx 'sudo -k' "$sudo_events" || fail "helper revoked the update's authorization" "$(<"$sudo_events")"
SUDO_EVENT_LOG="$sudo_events" run_helper start
SUDO_EVENT_LOG="$sudo_events" run_helper stop
grep -qx 'sudo -k' "$sudo_events" || fail "standalone helper no longer revokes sudo"
pass "helper leaves the update's authorization alone and revokes when standalone"
# Without a terminal, an update's inhibitor reuses the update's authorization
# non-interactively instead of asking again through polkit.
: >"$sudo_events"
OMARCHY_UPDATE_SUDO_SESSION=1 SUDO_EVENT_LOG="$sudo_events" run_helper start </dev/null
OMARCHY_UPDATE_SUDO_SESSION=1 SUDO_EVENT_LOG="$sudo_events" run_helper stop </dev/null
grep -q -- '^sudo -n -N -b -- ' "$sudo_events" || fail "update inhibitor without a terminal did not reuse sudo" "$(<"$sudo_events")"
! grep -qx pkexec "$sudo_events" || fail "update inhibitor without a terminal asked polkit" "$(<"$sudo_events")"
pass "update inhibitor without a terminal reuses the update's authorization instead of polkit"
permissive_runtime="$test_tmp/permissive-runtime"
mkdir -m 755 "$permissive_runtime"
if HOME="$test_home" XDG_RUNTIME_DIR="$permissive_runtime" PATH="$stub_bin:$ROOT/bin:/usr/bin:/bin" \
+3 -3
View File
@@ -38,8 +38,8 @@ for step in omarchy-hook omarchy-update-mise; do
rm "$SUDO_TEST_ROOT/bin/$step"
cat >"$SUDO_TEST_ROOT/bin/$step" <<'STUB'
#!/bin/bash
[[ ! -e $SUDO_TEST_CACHE ]] || exit 91
[[ $(command -v sudo) == "$OMARCHY_PATH/default/omarchy/sudo-no-update/sudo" ]] || exit 92
[[ -e $SUDO_TEST_CACHE ]] || exit 91
[[ $(command -v sudo) != "$OMARCHY_PATH/default/omarchy/sudo-no-update/sudo" ]] || exit 92
update-user-tool "${0##*/}"
STUB
chmod +x "$SUDO_TEST_ROOT/bin/$step"
@@ -63,5 +63,5 @@ for entry in fresh logged locked; do
grep -q '^locked-reexec$' "$SUDO_TEST_LOG" || fail "$entry update did not exercise the lock exec"
fi
assert_boundary_cold "$entry update"
pass "$entry update preserves the original user PATH through logging and locking with no-update sudo first"
pass "$entry update preserves the original user PATH through logging and locking and shares its authorization"
done