Files
omarchy/test/shell.d/passwordless-grant-lifecycle-test.sh
T
Spencer BullandCodex XHigh 2ffae36a18 Resolve the passwordless sudo helper by name in its migration
Pinning /usr/bin ran the packaged helper even where the migration came from somewhere else. Under a dev link the migration is read from the checkout while /usr/bin still holds the last installed package, and a package predating __migrate prints its usage and fails the update. By name, the unprivileged call goes through PATH and the sudo call through secure_path, which is how other migrations reach their helpers and which lands on /usr/bin on an install.

Co-Authored-By: Codex XHigh <noreply@openai.com>
2026-09-26 14:43:00 -05:00

219 lines
10 KiB
Bash

#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
source "$SHELL_TEST_DIR/fixtures/passwordless-sudo-test.sh"
quarantine="$test_tmp/var/lib/omarchy/sudoers-quarantine"
quarantined_policy() {
local entry
for entry in "$quarantine"/*/; do
if [[ $(cat "$entry/name") == "$1" ]]; then
cat "$entry/policy"
return 0
fi
done
return 1
}
# The old writer accepted any $USER, so a basename can sit just under NAME_MAX.
long_suffix=$(printf 'l%.0s' {1..223})
(
source "$library"
printf 'deleteduser ALL=(ALL) NOPASSWD: ALL\n' >"$(rule_file 1000)"
printf 'buildbot$ ALL=(ALL) NOPASSWD: ALL\n' >"$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-buildbot$"
# The legacy command never validated the account name, so a manual or NSS
# account outside the current policy still has its exact old grant removed.
printf 'Alice ALL=(ALL) NOPASSWD: ALL\n' >"$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-Alice"
# The legacy writer produced the body with echo. Under BASH_ENV with
# xpg_echo, USER='ali\0143e' yields this filename with an 'alice' rule, so
# a suffix/body mismatch does not prove administrator authorship.
printf 'alice ALL=(ALL) NOPASSWD: ALL\n' >"$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-ali\\0143e"
printf 'alice ALL=(ALL) NOPASSWD: ALL\n' >"$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-$long_suffix"
printf 'admin ALL=(ALL) NOPASSWD: /usr/bin/true\n' >"$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-custom"
TEST_DELETE_FAIL=1 assert_status 1 cleanup_all_locked
[[ -e $(rule_file 1000) ]]
cleanup_all_locked
! compgen -G "$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-*"
[[ $(stat -c '%a' "$quarantine") == 700 ]]
[[ $(quarantined_policy '99-omarchy-nopasswd-ali\0143e') == 'alice ALL=(ALL) NOPASSWD: ALL' ]]
[[ $(quarantined_policy "99-omarchy-nopasswd-$long_suffix") == 'alice ALL=(ALL) NOPASSWD: ALL' ]]
[[ $(quarantined_policy 99-omarchy-nopasswd-custom) == 'admin ALL=(ALL) NOPASSWD: /usr/bin/true' ]]
(( $(ls -A "$quarantine" | wc -l) == 3 ))
)
pass "legacy cleanup removes generated rules for any account and quarantines everything else in the prefix"
# Run the actual migration queue for separate temporary homes. Sudo only calls
# the mapped helper and can be refused without requesting host authorization.
# The migration names the helper rather than a path, so both of its calls reach
# the mapped copy through PATH, as they reach a dev checkout's through the link.
mkdir -p "$test_tmp/source/migrations"
cp "$ROOT/migrations/1788163635.sh" "$test_tmp/source/migrations/"
ln -s ../omarchy-sudo-passwordless "$test_tmp/bin/omarchy-sudo-passwordless"
printf 'echo "later migration ran"\n' >"$test_tmp/source/migrations/1788163636.sh"
run_migrations() {
TEST_MIGRATION=1 OMARCHY_PATH="$test_tmp/source" OMARCHY_MIGRATION_STATE="$test_tmp/$1" \
PATH="$test_tmp/bin:$PATH" /usr/bin/bash "$ROOT/bin/omarchy-migrate" >"$test_tmp/migrations.log" 2>&1
}
marker="$test_tmp/var/lib/omarchy/migrations/1788163635"
(
source "$library"
# A quarantine that cannot be trusted keeps the migration pending.
printf 'alice ALL=(ALL) NOPASSWD: ALL\n' >"$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-mismatch"
TEST_BAD_PATH="$test_tmp/var/lib/omarchy" assert_status 1 run_migrations first
[[ ! -e $marker && -e $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-mismatch ]]
printf 'audituser ALL=(ALL) NOPASSWD: ALL\n' >"$(rule_file 1000)"
printf 'Alice ALL=(ALL) NOPASSWD: ALL\n' >"$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-Alice"
TEST_DELETE_FAIL=1 assert_status 1 run_migrations first
[[ ! -e $marker && ! -e $test_tmp/first/1788163636.sh ]]
run_migrations first
[[ -f $marker && -f $test_tmp/first/1788163636.sh ]]
! compgen -G "$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-*"
[[ $(quarantined_policy 99-omarchy-nopasswd-mismatch) == 'alice ALL=(ALL) NOPASSWD: ALL' ]]
enable_locked 1000 15
cp "$(rule_file 1000)" "$test_tmp/renewed"
: >"$test_tmp/commands"
TEST_NO_SUDO=1 run_migrations second
[[ -f $test_tmp/second/1788163636.sh ]]
! grep -q '^sudo ' "$test_tmp/commands"
cmp "$(rule_file 1000)" "$test_tmp/renewed"
)
pass "migration completion is machine-wide, retryable, and needs no sudo for later users"
(
source "$library"
TEST_BAD_PATH="$marker" assert_status 1 migration_complete
rm "$marker"
ln -s "$test_tmp/renewed" "$marker"
assert_status 1 migration_complete
assert_status 1 migrate_locked
[[ -L $marker ]]
rm "$marker"
)
pass "migration checks marker ownership and rejects symlinks"
# Keep real package scripts in the contract: source and packaging share the
# same lock and blocker, including the legacy scriptlet fallback.
pkgs_path=${OMARCHY_PKGS_PATH:-$ROOT/../omarchy-pkgs}
[[ ! -d $pkgs_path/pkgbuilds ]] || pkgs_path=$pkgs_path/pkgbuilds
for name in omarchy-settings omarchy-settings-dev; do
script="$pkgs_path/$name/$name.install"
[[ -f $script ]] || fail "set OMARCHY_PKGS_PATH to the companion package checkout"
sed -e "s|/etc/|$test_tmp/etc/|g" -e "s|/run|$test_tmp/run|g" \
-e "s|/usr/bin/stat|$test_tmp/bin/stat|g" -e "s|/usr/bin/rm|$test_tmp/bin/rm|g" \
"$script" >"$test_tmp/$name.install"
reset_grant
(
source "$library"
source "$test_tmp/$name.install"
_etc_overrides_apply() { :; }
enable_locked 1000 15
TEST_DELETE_FAIL=1 assert_status 1 pre_remove
[[ -e $REMOVAL_BLOCKER && -e $(rule_file 1000) ]]
assert_status 1 enable_locked 1000 15
pre_remove && post_remove
[[ ! -e $(rule_file 1000) ]]
post_install
[[ ! -e $REMOVAL_BLOCKER ]]
enable_locked 1000 15
pre_upgrade && post_upgrade
[[ ! -e $(rule_file 1000) && ! -e $REMOVAL_BLOCKER ]]
# pacman does not stop a transaction on a failed scriptlet, so a failed
# pre_upgrade can be followed directly by post_upgrade. Completion must
# not clear the blocker while a rule remains, and a clean retry recovers.
enable_locked 1000 15
TEST_DELETE_FAIL=1 assert_status 1 pre_upgrade
TEST_DELETE_FAIL=1 assert_status 1 post_upgrade
[[ -e $REMOVAL_BLOCKER && -e $(rule_file 1000) ]]
assert_status 1 enable_locked 1000 15
post_upgrade
[[ ! -e $(rule_file 1000) && ! -e $REMOVAL_BLOCKER ]]
# A stranded blocker plus a live rule from an interrupted removal is
# cleaned by the next completed installation, not merely unblocked.
enable_locked 1000 15
: >"$REMOVAL_BLOCKER"
post_install
[[ ! -e $(rule_file 1000) && ! -e $REMOVAL_BLOCKER ]]
# A fresh install has no earlier step, so completion must hold the blocker
# itself while it sweeps: a leftover rule it cannot remove leaves
# publication refused rather than merely reporting an error.
enable_locked 1000 15
rm -f "$REMOVAL_BLOCKER"
TEST_DELETE_FAIL=1 assert_status 1 post_install
[[ -e $REMOVAL_BLOCKER && -e $(rule_file 1000) ]]
assert_status 1 enable_locked 1000 15
post_install
[[ ! -e $(rule_file 1000) && ! -e $REMOVAL_BLOCKER ]]
# The sweep must not depend on the glob state pacman's shell inherits.
enable_locked 1000 15
( set -f; GLOBIGNORE='*' post_upgrade )
[[ ! -e $(rule_file 1000) && ! -e $REMOVAL_BLOCKER ]]
# A failure before the lock is even taken, such as an untrusted lock
# directory, must still leave publication refused.
enable_locked 1000 15
rm -f "$REMOVAL_BLOCKER"
TEST_BAD_PATH="$test_tmp/run/lock" assert_status 1 post_install
[[ -e $REMOVAL_BLOCKER && -e $(rule_file 1000) ]]
TEST_BAD_PATH="$test_tmp/run/lock" assert_status 1 pre_upgrade
[[ -e $REMOVAL_BLOCKER ]]
post_install
[[ ! -e $(rule_file 1000) && ! -e $REMOVAL_BLOCKER ]]
)
done
pass "both settings packages revoke grants, block publication, and recover on installation only with the namespace empty"
reset_grant
# Hold the source lock, then start package removal. A native flock on the
# mapped file must serialize both implementations.
cat >"$test_tmp/worker" <<'WORKER'
#!/bin/bash
set -euo pipefail
source "$TEST_LIBRARY"
critical() {
touch "$TEST_GRANT_ROOT/entered"
for (( attempt=0; attempt<500; attempt++ )); do
[[ ! -e $TEST_GRANT_ROOT/release ]] || break
sleep 0.01
done
[[ -e $TEST_GRANT_ROOT/release ]] || return 1
enable_locked 1000 15
}
with_root_lock critical
WORKER
TEST_LIBRARY="$library" /usr/bin/bash "$test_tmp/worker" >"$test_tmp/publisher.log" 2>&1 &
publisher=$!
children+=("$publisher")
for (( attempt=0; attempt<200; attempt++ )); do
[[ ! -e $test_tmp/entered ]] || break
sleep 0.01
done
[[ -f $test_tmp/entered ]] || fail "publisher failed to acquire the lock"
/usr/bin/bash -euo pipefail -c 'source "$1"; pre_remove; post_remove' bash "$test_tmp/omarchy-settings.install" >"$test_tmp/removal.log" 2>&1 &
removal=$!
children+=("$removal")
# The removal announces itself before waiting for the lock, so a publisher
# still holding it is refused rather than allowed to publish a rule that the
# removal would delete a moment later.
for (( attempt=0; attempt<200; attempt++ )); do
[[ ! -f $test_tmp/run/omarchy-sudo-passwordless-package-removing ]] || break
sleep 0.01
done
[[ -f $test_tmp/run/omarchy-sudo-passwordless-package-removing ]] || fail "removal did not announce itself before waiting for the lock"
touch "$test_tmp/release"
if wait "$publisher"; then fail "publisher was allowed to publish after removal announced itself" "$(cat "$test_tmp/publisher.log")"; fi
wait "$removal" || fail "removal failed" "$(cat "$test_tmp/removal.log")"
children=()
[[ ! -e $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000 ]]
[[ -f $test_tmp/run/omarchy-sudo-passwordless-package-removing ]]
pass "an announced package removal refuses a waiting publisher and clears the namespace"
# systemd-tmpfiles operates on an explicit disposable root, never the host.
reset_grant
: >"$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000"
: >"$test_tmp/etc/sudoers.d/unrelated"
rule='r! /etc/sudoers.d/99-omarchy-nopasswd-*'
/usr/bin/systemd-tmpfiles --root="$test_tmp" --remove --inline "$rule"
[[ -f $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000 ]] || fail "routine tmpfiles shortened a live grant"
/usr/bin/systemd-tmpfiles --root="$test_tmp" --remove --boot --inline "$rule"
[[ ! -e $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000 && -f $test_tmp/etc/sudoers.d/unrelated ]] || fail "boot cleanup boundary"
pass "native boot cleanup removes grants while routine tmpfiles preserves them"