mirror of
https://github.com/omacom/omarchy.git
synced 2026-09-28 06:13:13 +08:00
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:
co-authored by
Claude Opus 5.5
parent
7b336b1b0d
commit
e1614f2bdb
+58
-21
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" \
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user