Simplify passwordless sudo grant lifecycle

This commit is contained in:
Afonso Oliveira committed 2026-09-10 22:01:09 +01:00
1 parent 5bfc8b5cc3
commit c46f321680
6 files changed
+525 -944

No files matched your search

@@ -0,0 +1,125 @@
#!/bin/bash
# Exercise complete production functions with private paths and harmless
# command stand-ins. Never install sudo policy or start a host timer.
test_tmp=$(mktemp -d)
children=()
cleanup_grant_fixture() {
local status=$?
trap - EXIT
if (( ${#children[@]} )); then
kill "${children[@]}" 2>/dev/null || true
wait "${children[@]}" 2>/dev/null || true
fi
rm -rf "$test_tmp"
exit "$status"
}
trap cleanup_grant_fixture EXIT
export TEST_GRANT_ROOT=$test_tmp
mkdir -p "$test_tmp/bin" "$test_tmp/etc/sudoers.d" "$test_tmp/etc/tmpfiles.d" "$test_tmp/run/lock" "$test_tmp/var/lib" "$test_tmp/hooks"
cat >"$test_tmp/bin/mock" <<'STUB'
#!/bin/bash
set -euo pipefail
name=${0##*/}
printf '%s %s\n' "$name" "$*" >>"$TEST_GRANT_ROOT/commands"
case "$name" in
stat)
path=${@: -1}
owner=0
mode=$(/usr/bin/stat -Lc '%a' -- "$path")
[[ $path != /tmp ]] || mode=755
[[ $path != "${TEST_BAD_PATH:-}" ]] || owner=1000
case $2 in
'%u') echo "$owner" ;;
'%a') echo "$mode" ;;
'%u %a') echo "$owner $mode" ;;
*) exec /usr/bin/stat "$@" ;;
esac
;;
chown) exit 0 ;;
install)
args=()
while (($#)); do
case $1 in -o|-g) shift 2 ;; *) args+=("$1"); shift ;; esac
done
exec /usr/bin/install "${args[@]}"
;;
rm)
for path in "$@"; do
if [[ ${TEST_DELETE_FAIL:-0} == 1 && $path == "$TEST_GRANT_ROOT/etc/sudoers.d/99-omarchy-nopasswd-1000" ]]; then exit 1; fi
done
exec /usr/bin/rm "$@"
;;
mv)
[[ ${TEST_PUBLISH_FAIL:-0} != 1 ]] || exit 1
/usr/bin/mv "$@"
[[ ${TEST_POST_PUBLISH_FAIL:-0} != 1 ]] || : >"$TEST_GRANT_ROOT/run/omarchy-sudo-passwordless-package-removing"
;;
systemd-run)
[[ ${TEST_TIMER_FAIL:-0} != 1 ]] || exit 1
if [[ ${TEST_CANCEL_ENABLE:-0} == 1 ]]; then kill -TERM "$PPID"; fi
;;
systemctl)
[[ $1 != "is-active" || ${TEST_INACTIVE_TIMER:-0} != 1 ]]
;;
date)
if [[ ${TEST_EXPIRED:-0} == 1 && $* == '-u +%Y%m%d%H%M%SZ' ]]; then echo 99991231235959Z; else /usr/bin/date "$@"; fi
;;
getent) printf '%s:x:1000:1000:Test:/nonexistent:/bin/bash\n' "${TEST_ACCOUNT:-audituser}" ;;
sudo)
if [[ ${1:-} == -h ]]; then echo 'usage: sudo [-N] command'; exit 0; fi
if [[ ${1:-} == -k ]]; then exit 0; fi
if [[ ${1:-} == -N ]]; then shift; fi
if [[ ${1:-} == -- ]]; then shift; fi
if [[ ${TEST_MIGRATION:-0} == 1 ]]; then
[[ ${TEST_NO_SUDO:-0} != 1 ]] || exit 1
TEST_EUID=0 /usr/bin/bash -p "$@"
else
[[ ${2:-} != __status ]] || exit "${TEST_STATUS:-3}"
fi
;;
gum) exit 1 ;;
*) exit 99 ;;
esac
STUB
chmod +x "$test_tmp/bin/mock"
for name in stat chown install rm mv systemd-run systemctl date getent sudo gum; do
ln -s mock "$test_tmp/bin/$name"
done
python3 - "$ROOT" "$test_tmp" <<'PY'
from pathlib import Path
import sys
root, temp = map(Path, sys.argv[1:])
for name in ('omarchy-sudo-passwordless', 'omarchy-security-functions'):
text = (root/'bin'/name).read_text()
for path in ('/etc/', '/var/lib', '/run/', '/usr/share/libalpm/hooks'):
target = str(temp/'hooks') if path == '/usr/share/libalpm/hooks' else str(temp) + path
text = text.replace(path, target)
text = text.replace('((EUID == 0))', '((${TEST_EUID:-1} == 0))')
for command in ('stat', 'chown', 'install', 'rm', 'mv', 'systemd-run', 'systemctl', 'date', 'getent', 'sudo', 'gum'):
text = text.replace('/usr/bin/' + command, str(temp/'bin'/command))
(temp/name).write_text(text)
(temp/name).chmod(0o755)
PY
library="$test_tmp/functions.sh"
{
printf 'source %q\n' "$test_tmp/omarchy-security-functions"
awk '/^set -euo pipefail$/ { functions=1 } /^case "\$\{1:-\}" in$/ { exit } functions { print }' "$test_tmp/omarchy-sudo-passwordless"
} >"$library"
cp "$ROOT/default/libalpm/hooks/05-omarchy-passwordless-revoke.hook" "$test_tmp/hooks/"
sed "s|/etc/|$test_tmp/etc/|g" "$ROOT/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf" >"$test_tmp/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
: >"$test_tmp/commands"
# New subshell per case prevents one test's overrides and readonly constants
# from affecting the next. External commands log enough to verify ordering.
assert_status() {
local expected=$1 actual=0
shift
"$@" || actual=$?
(( actual == expected )) || fail "expected status $expected, got $actual from $*"
}
reset_grant() {
rm -f "$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000" "$test_tmp/run/omarchy-sudo-passwordless-package-removing"
: >"$test_tmp/commands"
}
+142 -437
View File
@@ -1,458 +1,163 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
source "$SHELL_TEST_DIR/fixtures/passwordless-sudo-test.sh"
command_path="$ROOT/bin/omarchy-sudo-passwordless"
security_library_path="$ROOT/bin/omarchy-security-functions"
tmpfiles_path="$ROOT/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
migration_path="$ROOT/migrations/1788163635.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
function_prefix() {
printf 'source %q\n' "$security_library_path"
awk '/^set -euo pipefail$/ { functions=1 } /^case "\$\{1:-\}" in$/ { exit } functions { print }' "$command_path"
}
# Exercise the validation code itself. Leading zeroes remain numeric, but zero,
# negatives, oversized grants, and shell syntax are rejected.
(
source <(function_prefix)
for minutes in 1 15 1440 00015; do
valid_minutes "$minutes" || fail "passwordless sudo accepts bounded duration $minutes"
done
for minutes in 0 1441 -1 1m '1;id' '' 18446744073709551617; do
! valid_minutes "$minutes" || fail "passwordless sudo rejects invalid duration '$minutes'"
done
source "$library"
for minutes in 1 15 1440 00015; do valid_minutes "$minutes" || exit 1; done
for minutes in 0 1441 -1 1m '' 18446744073709551617; do ! valid_minutes "$minutes" || exit 1; done
for name in audituser 'buildbot$'; do valid_account_name "$name" || exit 1; done
for name in 'a$b' '$' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; do ! valid_account_name "$name" || exit 1; done
! valid_uid 18446744073709551617
)
pass "passwordless sudo validates a bounded positive duration"
pass "duration and account validation retains bounded inputs and trailing-dollar usernames"
# The public entry point uses the kernel-backed numeric identity; $USER is
# never interpolated into a privileged filename or sudoers rule.
grep -F 'uid=$(/usr/bin/id -u)' "$command_path" >/dev/null ||
fail "passwordless sudo derives the caller from id -u"
! grep -Eq '\$\{?USER\}?' "$command_path" ||
fail "passwordless sudo does not trust USER for privileged policy"
grep -F '[[ ${SUDO_UID:-} =~ ^[0-9]+$ ]]' "$command_path" >/dev/null ||
fail "passwordless sudo validates sudo provenance"
pass "passwordless sudo derives and validates trusted account identity"
(
source "$library"
assert_status 2 root_dispatch __status 1000
assert_status 2 env TEST_EUID=0 SUDO_UID=1001 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" __status 1000
assert_status 3 env TEST_EUID=0 SUDO_UID=1000 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" __status 1000
)
pass "internal actions reject missing root and mismatched sudo identity"
# Status inspection and the confirmation UI are mixed-trust: a normal sudo
# status call would publish a timestamp that a hostile prompt helper could use
# even when the user declines the grant. Exercise the public flow with a sudo
# model that publishes a token only when -N is missing.
grep -Fxq '#!/bin/bash -p' "$command_path" ||
fail "passwordless sudo no longer suppresses Bash startup injection"
public_sudo_stub="$test_tmp/public-sudo"
public_gum_stub="$test_tmp/public-gum"
public_token="$test_tmp/public-token"
public_exploit="$test_tmp/public-exploit"
cat >"$public_sudo_stub" <<'STUB'
#!/bin/bash
if [[ ${1:-} == -h ]]; then
echo 'usage: sudo [-ABbEHkNnPS] command'
exit 0
fi
if [[ ${1:-} == -k ]]; then
rm -f -- "$TEST_PUBLIC_TOKEN"
exit 0
fi
no_update=0
if [[ ${1:-} == -N ]]; then no_update=1; shift; fi
[[ ${1:-} != -- ]] || shift
((no_update)) || : >"$TEST_PUBLIC_TOKEN"
case "${2:-}" in
__status) exit "${TEST_PUBLIC_STATUS:-3}" ;;
__enable|__disable) exit 0 ;;
*) exit 2 ;;
esac
STUB
cat >"$public_gum_stub" <<'STUB'
#!/bin/bash
[[ -z ${TEST_PUBLIC_GUM_LOG:-} ]] || : >"$TEST_PUBLIC_GUM_LOG"
[[ ! -e $TEST_PUBLIC_TOKEN ]] || : >"$TEST_PUBLIC_EXPLOIT"
exit 1
STUB
chmod 0755 "$public_sudo_stub" "$public_gum_stub"
public_flow="$test_tmp/passwordless-public-flow"
/usr/bin/sed "s#/usr/bin/sudo#$public_sudo_stub#g" "$security_library_path" >"$test_tmp/omarchy-security-functions"
/usr/bin/sed \
-e "s#/usr/bin/sudo#$public_sudo_stub#g" \
-e "s#/usr/bin/gum#$public_gum_stub#g" \
"$command_path" >"$public_flow"
chmod 0755 "$public_flow"
TEST_PUBLIC_TOKEN="$public_token" TEST_PUBLIC_EXPLOIT="$public_exploit" \
/usr/bin/bash -p "$public_flow" 15 >/dev/null
[[ ! -e $public_token && ! -e $public_exploit ]] ||
fail "passwordless confirmation inherited a reusable status credential"
for status in 1 2; do
if TEST_PUBLIC_TOKEN="$public_token" TEST_PUBLIC_EXPLOIT="$public_exploit" \
TEST_PUBLIC_STATUS="$status" TEST_PUBLIC_GUM_LOG="$test_tmp/unsafe-status-confirmation" \
/usr/bin/bash -p "$public_flow" 15 >"$test_tmp/status-error.output" 2>&1; then
fail "passwordless sudo treats status/authorization failure $status as inactive"
for status in 1 2 3; do
: >"$test_tmp/commands"
result=0
TEST_STATUS=$status /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" 15 >"$test_tmp/public.log" 2>&1 || result=$?
if (( status == 3 )); then
(( result == 0 )) && grep -q '^gum confirm ' "$test_tmp/commands" || fail "inactive status must allow confirmation"
else
(( result != 0 )) && ! grep -q '^gum ' "$test_tmp/commands" || fail "inspection errors must not offer enablement"
fi
[[ ! -e $test_tmp/unsafe-status-confirmation ]] || fail "failed status inspection opens the enable prompt"
grep -q 'Could not safely inspect passwordless sudo' "$test_tmp/status-error.output" ||
fail "failed status inspection lacks recovery guidance"
grep -q '^sudo -N -- .* __status ' "$test_tmp/commands" || fail "status must not publish reusable authorization"
[[ $(tail -1 "$test_tmp/commands") == 'sudo -k' ]] || fail "public exit must revoke its authorization"
done
pass "public status distinguishes inactive from errors and revokes authorization on exit"
startup_env="$test_tmp/passwordless-bash-env"
startup_marker="$test_tmp/passwordless-bash-env-ran"
cat >"$startup_env" <<'STUB'
: >"$TEST_STARTUP_MARKER"
set -o privileged
unset BASH_ENV
STUB
if BASH_ENV="$startup_env" TEST_STARTUP_MARKER="$startup_marker" \
/usr/bin/bash "$public_flow" -p >/dev/null 2>&1; then
fail "passwordless sudo accepted an unsafe interpreter with a decoy -p"
printf ': >"$TEST_STARTUP_MARKER"\nset -o privileged\nunset BASH_ENV\n' >"$test_tmp/startup"
: >"$test_tmp/commands"
if TEST_STARTUP_MARKER="$test_tmp/startup-ran" BASH_ENV="$test_tmp/startup" bash "$test_tmp/omarchy-sudo-passwordless" -p >/dev/null 2>&1; then
fail "ordinary Bash with a decoy -p was accepted"
fi
[[ -e $startup_marker && ! -e $public_token && ! -e $public_exploit ]] ||
fail "unsafe passwordless startup reached its sudo workflow"
pass "passwordless confirmation uses a cold command-scoped credential boundary"
[[ -f $test_tmp/startup-ran && ! -s $test_tmp/commands ]] || fail "startup rejection must precede sudo"
pass "startup validation rejects ordinary Bash before authorization"
# Source a path-rewritten copy so the real cleanup implementation can be
# exercised without touching /etc. Exact generated numeric rules are removed
# even after account deletion or a crash before state publication. Anything an
# administrator changed, and every symlink, is preserved.
fake_sudoers="$test_tmp/sudoers.d"
mkdir "$fake_sudoers"
rewritten="$test_tmp/passwordless-lib.sh"
function_prefix | sed "s#/etc/sudoers.d#$fake_sudoers#g" >"$rewritten"
(
source "$rewritten"
printf 'deleteduser ALL=(ALL) NOPASSWD: ALL\n' >"$fake_sudoers/99-omarchy-nopasswd-424242"
printf 'admin ALL=(ALL) NOPASSWD: /usr/bin/pacman\n' >"$fake_sudoers/99-omarchy-nopasswd-424243"
ln -s "$fake_sudoers/99-omarchy-nopasswd-424243" "$fake_sudoers/99-omarchy-nopasswd-424244"
remove_known_legacy_rules
)
[[ ! -e $fake_sudoers/99-omarchy-nopasswd-424242 ]] ||
fail "boot cleanup removes a state-less numeric orphan"
[[ -f $fake_sudoers/99-omarchy-nopasswd-424243 ]] ||
fail "boot cleanup preserves administrator-authored policy"
[[ -L $fake_sudoers/99-omarchy-nopasswd-424244 ]] ||
fail "boot cleanup refuses sudoers symlinks"
pass "boot cleanup removes crash/deleted-account orphans conservatively"
# A boot gate must not report success when deletion itself fails. Exercise the
# real cleanup and post-cleanup verification with a deterministic failing rm.
rm_failure_dir="$test_tmp/rm-failure-sudoers"
mkdir "$rm_failure_dir"
printf 'deleteduser ALL=(ALL) NOPASSWD: ALL\n' >"$rm_failure_dir/99-omarchy-nopasswd-424245"
failing_rm="$test_tmp/failing-rm"
cat >"$failing_rm" <<'FAILING_RM'
#!/bin/bash
exit 1
FAILING_RM
chmod +x "$failing_rm"
rm_failure_lib="$test_tmp/rm-failure-lib.sh"
function_prefix |
sed -e "s#/etc/sudoers.d#$rm_failure_dir#g" \
-e "s#/var/lib/omarchy/sudo-passwordless#$test_tmp/empty-state#g" \
-e "s#/usr/bin/rm#$failing_rm#g" >"$rm_failure_lib"
mkdir "$test_tmp/empty-state"
(
source "$rm_failure_lib"
! cleanup_all_locked
) || fail "boot cleanup fails when an Omarchy rule cannot be removed"
[[ -f $rm_failure_dir/99-omarchy-nopasswd-424245 ]] ||
fail "rm-failure fixture remains available for verification"
pass "boot cleanup fails closed when policy deletion fails"
# Reproduce the migration's real sudo provenance: sudo sets SUDO_UID. Rewrite
# only the read-only EUID probe so this unprivileged test can exercise the root
# dispatcher, then assert that cleanup (which can only revoke privilege) runs.
dispatch_lib="$test_tmp/dispatch-lib.sh"
function_prefix | sed 's/((EUID == 0))/((TEST_EUID == 0))/g' >"$dispatch_lib"
(
source "$dispatch_lib"
called=""
cleanup_all_locked() { called=cleanup; }
with_root_lock() { "$@"; }
TEST_EUID=0 SUDO_UID=1000 root_dispatch __cleanup-all
[[ $called == cleanup ]]
) || fail "migration cleanup dispatch accepts authenticated sudo provenance"
pass "migration can invoke fail-closed cleanup through sudo"
# A grant cannot be published until the static unit is verified/enabled, and a
# timer setup failure removes its pending state without calling publish_rule.
transaction_dir="$test_tmp/transaction"
mkdir "$transaction_dir"
transaction_lib="$test_tmp/transaction-lib.sh"
function_prefix | sed "s#/var/lib/omarchy/sudo-passwordless#$transaction_dir#g" >"$transaction_lib"
(
source "$transaction_lib"
ACCOUNT_NAME=audituser
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
prepare_root_state() { :; }
verify_boot_cleanup() { return 1; }
publish_rule() { return 99; }
! enable_locked 1000 15
)
(
source "$transaction_lib"
ACCOUNT_NAME=audituser
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
prepare_root_state() { :; }
verify_boot_cleanup() { return 0; }
read_state_timer() { return 1; }
prepare_state_file() { local pending="$transaction_dir/pending"; : >"$pending"; printf %s "$pending"; }
start_expiry_timer() { return 1; }
publish_rule() { printf published >"$transaction_dir/published"; }
cleanup_uid_locked() { : >"$transaction_dir/failed-timer-cleanup"; }
! enable_locked 1000 15
[[ ! -e $transaction_dir/pending && ! -e $transaction_dir/published &&
-e $transaction_dir/failed-timer-cleanup ]]
) || fail "passwordless sudo fails closed on prerequisite/timer failure"
pass "passwordless sudo publishes no rule after partial setup failure"
# Erik's predecessor fix revoked an already-active grant when an extension
# could not arm its replacement timer. Keep that fail-closed property while
# the new transaction deliberately leaves the old timer armed until the new
# one is verified.
replacement_state="$transaction_dir/1000.state"
replacement_rule="$transaction_dir/1000.rule"
replacement_stopped="$transaction_dir/old-timer-stopped"
old_timer=omarchy-nopasswd-expire-1000-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
printf 'UID=1000\nUSER=audituser\nEXPIRES=2000000000\nTIMER=%s\n' "$old_timer" >"$replacement_state"
printf 'audituser ALL=(ALL) NOPASSWD: ALL\n' >"$replacement_rule"
(
source "$transaction_lib"
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
prepare_root_state() { :; }
verify_boot_cleanup() { return 0; }
state_file() { printf '%s' "$replacement_state"; }
rule_file() { printf '%s' "$replacement_rule"; }
prepare_state_file() { local pending="$transaction_dir/replacement-pending"; : >"$pending"; printf %s "$pending"; }
start_expiry_timer() { return 1; }
stop_timer() { [[ $1 == "$old_timer" ]] && : >"$replacement_stopped"; }
! enable_locked 1000 30
[[ ! -e $replacement_state && ! -e $replacement_rule && -e $replacement_stopped ]]
) || fail "passwordless sudo leaves an existing grant live after replacement timer failure"
pass "replacement timer failure revokes the existing grant"
# Expiry is a wall-clock promise, so the transient timer must carry the exact
# absolute epoch recorded in root state. A monotonic-only --on-active timer
# pauses during suspend and can otherwise extend a short grant by hours.
timer_args="$test_tmp/timer-args"
calendar_systemd_run="$test_tmp/calendar-systemd-run"
calendar_systemctl="$test_tmp/calendar-systemctl"
cat >"$calendar_systemd_run" <<'STUB'
#!/bin/bash
printf '%s\n' "$@" >"$TEST_TIMER_ARGS"
STUB
cat >"$calendar_systemctl" <<'STUB'
#!/bin/bash
exit 0
STUB
chmod 0755 "$calendar_systemd_run" "$calendar_systemctl"
calendar_lib="$test_tmp/calendar-lib.sh"
function_prefix |
sed -e "s#/usr/bin/systemd-run#$calendar_systemd_run#g" \
-e "s#/usr/bin/systemctl#$calendar_systemctl#g" >"$calendar_lib"
(
source "$calendar_lib"
TEST_TIMER_ARGS="$timer_args" start_expiry_timer 1000 2000000000 \
omarchy-nopasswd-expire-1000-0123456789abcdef0123456789abcdef
) || fail "passwordless sudo cannot arm its absolute expiry timer"
grep -Fx -- '--on-calendar=@2000000000' "$timer_args" >/dev/null ||
fail "passwordless sudo timer does not advance across suspend"
pass "passwordless sudo arms the recorded absolute wall-clock expiry"
# A resumed machine can briefly observe the timer as active before systemd
# dispatches its overdue service. Status must independently enforce EXPIRES and
# synchronously remove policy instead of trusting timer activity alone.
expired_state="$test_tmp/expired-state"
expired_sudoers="$test_tmp/expired-sudoers"
mkdir "$expired_state" "$expired_sudoers"
expired_timer=omarchy-nopasswd-expire-1000-0123456789abcdef0123456789abcdef
printf 'UID=1000\nUSER=audituser\nEXPIRES=1\nTIMER=%s\n' "$expired_timer" >"$expired_state/1000.state"
printf 'audituser ALL=(ALL) NOPASSWD: ALL\n' >"$expired_sudoers/99-omarchy-nopasswd-1000"
expired_lib="$test_tmp/expired-lib.sh"
function_prefix |
sed -e "s#/var/lib/omarchy/sudo-passwordless#$expired_state#g" \
-e "s#/etc/sudoers.d#$expired_sudoers#g" \
-e "s#/usr/bin/systemctl#$calendar_systemctl#g" >"$expired_lib"
(
source "$expired_lib"
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
! status_locked 1000
) || fail "passwordless sudo accepts expired root state while its timer is active"
[[ ! -e $expired_state/1000.state && ! -e $expired_sudoers/99-omarchy-nopasswd-1000 ]] ||
fail "passwordless sudo does not synchronously revoke expired state"
pass "passwordless sudo enforces wall-clock expiry independently of timer dispatch"
# If the transient timer fires between its first active check and publication,
# the just-created rule must be synchronously revoked instead of surviving to
# reboot. Model that narrow transition with the real enable transaction.
inactive_systemctl="$test_tmp/inactive-systemctl"
cat >"$inactive_systemctl" <<'STUB'
#!/bin/bash
exit 1
STUB
chmod 0755 "$inactive_systemctl"
post_publish_lib="$test_tmp/post-publish-lib.sh"
sed "s#/usr/bin/systemctl#$inactive_systemctl#g" "$transaction_lib" >"$post_publish_lib"
(
source "$post_publish_lib"
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
prepare_root_state() { :; }
verify_boot_cleanup() { return 0; }
read_state_timer() { return 1; }
prepare_state_file() { local pending="$transaction_dir/pending-after-arm"; : >"$pending"; printf %s "$pending"; }
start_expiry_timer() { return 0; }
publish_rule() { : >"$transaction_dir/published-after-arm"; }
cleanup_uid_locked() { rm -f "$transaction_dir/published-after-arm"; : >"$transaction_dir/revoked-after-arm"; }
! enable_locked 1000 15
[[ ! -e $transaction_dir/published-after-arm && -e $transaction_dir/revoked-after-arm ]]
) || fail "passwordless sudo leaves a grant when its armed timer expires before publication completes"
pass "timer expiry during publication revokes the grant synchronously"
# Follow the maintainer's package-owned tmpfiles design: one boot-only rule
# owns this filename namespace. A routine --remove leaves live grants alone;
# early boot removes them before a user can log in. The migration only revokes
# legacy runtime state and never writes static policy into /usr.
mapfile -t tmpfiles_rules < <(/usr/bin/grep -vE '^[[:space:]]*(#|$)' "$tmpfiles_path")
(( ${#tmpfiles_rules[@]} == 1 )) || fail "passwordless sudo ships one boot cleanup rule"
[[ ${tmpfiles_rules[0]} == 'r! /etc/sudoers.d/99-omarchy-nopasswd-*' ]] ||
fail "passwordless sudo boot cleanup does not own the exact generated namespace"
fake_root="$test_tmp/tmpfiles-root"
sudoers_dir="$fake_root/etc/sudoers.d"
mkdir -p "$sudoers_dir"
for name in alice buildbot-2 424242; do
: >"$sudoers_dir/99-omarchy-nopasswd-$name"
done
: >"$sudoers_dir/omarchy-dns"
/usr/bin/systemd-tmpfiles --root="$fake_root" --remove --inline "${tmpfiles_rules[0]}"
[[ -e $sudoers_dir/99-omarchy-nopasswd-alice ]] || fail "non-boot tmpfiles run shortened a live grant"
/usr/bin/systemd-tmpfiles --root="$fake_root" --remove --boot --inline "${tmpfiles_rules[0]}"
! find "$sudoers_dir" -name '99-omarchy-nopasswd-*' -print -quit | /usr/bin/grep -q . ||
fail "boot cleanup left a generated passwordless grant"
[[ -e $sudoers_dir/omarchy-dns ]] || fail "boot cleanup removed an unrelated sudoers rule"
/usr/bin/grep -Fx 'sudo /usr/bin/omarchy-sudo-passwordless __cleanup-all' "$migration_path" >/dev/null
! /usr/bin/grep -q 'omarchy-sudo-passwordless-cleanup.service' "$migration_path" ||
fail "migration retained a custom boot service instead of package-owned tmpfiles"
pass "package-owned boot cleanup is narrow, boot-only, and migration-safe"
# Removing the settings package also removes the tmpfiles rule. Its package
# lifecycle must therefore revoke the same owned namespace synchronously, while
# preserving every unrelated sudoers file.
pkgs_candidates=(
"${OMARCHY_PKGS_PATH:-}"
"$ROOT/../omarchy-pkgs"
"$ROOT/../../omarchy-pkgs"
"$HOME/Work/omarchy/omarchy-pkgs"
"$HOME/Work/omacom/omarchy-pkgs"
)
pkgs_root=""
for candidate in "${pkgs_candidates[@]}"; do
if [[ -n $candidate && -d $candidate/pkgbuilds/omarchy-settings ]]; then
pkgs_root=$candidate/pkgbuilds
break
elif [[ -n $candidate && -d $candidate/omarchy-settings ]]; then
pkgs_root=$candidate
break
source "$library"
enable_locked 1000 15
read_grant 1000
[[ $GRANT_NAME == audituser && $(stat -c '%a' "$(rule_file 1000)") == 440 ]]
/usr/sbin/visudo -cf "$(rule_file 1000)" >/dev/null
expiry=$(sed -n 's/^systemd-run .*--on-calendar=@\([0-9]*\).*$/\1/p' "$test_tmp/commands" | tail -1)
[[ $GRANT_DEADLINE == "$(/usr/bin/date -u -d "@$expiry" +%Y%m%d%H%M%SZ)" ]]
if [[ -n ${OMARCHY_TEST_SUDOERS:-} ]]; then
[[ -x $OMARCHY_TEST_SUDOERS ]] || fail "OMARCHY_TEST_SUDOERS must name an executable"
printf 'root:x:0:0:root:/root:/bin/bash\naudituser:x:1000:1000:Test:/nonexistent:/bin/bash\n' >"$test_tmp/passwd"
printf 'root:x:0:\naudituser:x:1000:\n' >"$test_tmp/group"
{ printf 'audituser ALL=(ALL) ALL\n'; cat "$(rule_file 1000)"; } >"$test_tmp/policy"
for offset in -1 1; do
when=$(/usr/bin/date -u -d "@$((expiry + offset))" +%Y%m%d%H%M%SZ)
"$OMARCHY_TEST_SUDOERS" -p "$test_tmp/passwd" -P "$test_tmp/group" -T "$when" audituser /usr/bin/true <"$test_tmp/policy" >"$test_tmp/policy-result"
if (( offset < 0 )); then
! grep -q 'Password required' "$test_tmp/policy-result" || fail "native policy requires a password before expiry"
else
grep -q 'Password required' "$test_tmp/policy-result" || fail "native policy remains passwordless after expiry"
fi
done
pass "native sudoers evaluation requires authentication after the generated deadline"
fi
done
[[ -n $pkgs_root ]] || fail "omarchy-pkgs checkout found for passwordless package-removal coverage"
assert_status 0 status_locked 1000
TEST_INACTIVE_TIMER=1 assert_status 0 status_locked 1000
[[ ! -e $test_tmp/var/lib/omarchy/sudo-passwordless ]]
! compgen -G "$test_tmp/etc/sudoers.d/.omarchy-nopasswd.*"
)
pass "one complete mode-0440 sudoers rule holds the deadline with no separate grant state"
for package_name in omarchy-settings omarchy-settings-dev; do
install_script="$pkgs_root/$package_name/$package_name.install"
transformed_install="$test_tmp/$package_name.install"
removal_root="$test_tmp/$package_name-remove"
removal_sudoers="$removal_root/etc/sudoers.d"
mkdir -p "$removal_sudoers" "$removal_root/run/lock" "$removal_root/etc/tmpfiles.d"
: >"$removal_sudoers/99-omarchy-nopasswd-1000"
: >"$removal_sudoers/99-omarchy-nopasswd-legacy-user"
: >"$removal_sudoers/omarchy-dns"
ln -s ../administrator/os-release "$removal_root/etc/os-release"
package_stat="$test_tmp/package-stat"
cat >"$package_stat" <<'STUB'
#!/bin/bash
if [[ $2 == '%u' ]]; then printf '0\n'; else /usr/bin/stat "$@"; fi
STUB
chmod +x "$package_stat"
sed -e "s#/etc/#$removal_root/etc/#g" \
-e "s#/run#$removal_root/run#g" \
-e "s#/usr/bin/stat#$package_stat#g" "$install_script" >"$transformed_install"
(
source "$library"
before=$(cat "$(rule_file 1000)")
expire_locked 1000 omarchy-nopasswd-expire-1000-ffffffffffffffffffffffffffffffff
[[ $(cat "$(rule_file 1000)") == "$before" ]]
enable_locked 1000 30
renewed=$(cat "$(rule_file 1000)")
[[ $renewed != "$before" ]]
expire_locked 1000
[[ $(cat "$(rule_file 1000)") == "$renewed" ]]
TEST_EXPIRED=1 expire_locked 1000
[[ ! -e $(rule_file 1000) ]]
)
pass "legacy and current callbacks preserve renewed grants and remove expired ones"
(
source "$library"
enable_locked 1000 1
assert_status 2 env TEST_EXPIRED=1 TEST_DELETE_FAIL=1 TEST_EUID=0 SUDO_UID=1000 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" __status 1000
[[ -e $(rule_file 1000) ]]
TEST_EXPIRED=1 assert_status 3 status_locked 1000
[[ ! -e $(rule_file 1000) ]]
)
pass "expired status reports cleanup failure separately from confirmed inactivity"
for failure in TEST_TIMER_FAIL TEST_INACTIVE_TIMER TEST_PUBLISH_FAIL TEST_POST_PUBLISH_FAIL TEST_CANCEL_ENABLE; do
reset_grant
expected=1
if [[ $failure == "TEST_CANCEL_ENABLE" ]]; then expected=143; fi
(
source "$transformed_install"
pre_remove
[[ -f $removal_root/run/omarchy-sudo-passwordless-package-removing ]]
post_remove
) || fail "$package_name removal revokes active passwordless grants"
! find "$removal_sudoers" -name '99-omarchy-nopasswd-*' -print -quit | grep -q . ||
fail "$package_name removal leaves a passwordless grant behind"
[[ -e $removal_sudoers/omarchy-dns ]] ||
fail "$package_name removal deletes an unrelated sudoers policy"
[[ $(readlink "$removal_root/etc/os-release") == ../administrator/os-release ]] ||
fail "$package_name removal changes unrelated OS metadata"
: >"$removal_sudoers/99-omarchy-nopasswd-1001"
(
source "$transformed_install"
post_remove
) || fail "$package_name removal handles administrator OS selector state"
[[ $(readlink "$removal_root/etc/os-release") == ../administrator/os-release ]] ||
fail "$package_name removal overwrites an administrator OS selector"
[[ ! -e $removal_sudoers/99-omarchy-nopasswd-1001 ]] ||
fail "$package_name removal grant cleanup depends on OS selector state"
(
source "$transformed_install"
_etc_overrides_apply() { :; }
if post_install; then exit 1; fi
[[ -f $removal_root/run/omarchy-sudo-passwordless-package-removing ]]
: >"$removal_root/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
post_install
[[ ! -e $removal_root/run/omarchy-sudo-passwordless-package-removing ]]
: >"$removal_sudoers/99-omarchy-nopasswd-1002"
pre_upgrade
[[ ! -e $removal_sudoers/99-omarchy-nopasswd-1002 ]]
post_upgrade
[[ ! -e $removal_root/run/omarchy-sudo-passwordless-package-removing ]]
) || fail "$package_name restores grant availability only after boot cleanup is installed"
source "$library"
enable_locked 1000 15
assert_status "$expected" env "$failure=1" TEST_EUID=0 SUDO_UID=1000 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" __enable 1000 30
[[ ! -e $(rule_file 1000) ]]
)
done
pass "settings package transitions revoke grants and preserve unrelated configuration"
pass "timer, publication, post-publication and cancellation failures revoke renewed access"
# Exercise the production flock wrapper under contention. mkdir is an atomic
# overlap detector; all workers must enter and leave the protected region.
lock_dir="$test_tmp/lock-runtime"
mkdir "$lock_dir"
lock_lib="$test_tmp/lock-lib.sh"
function_prefix |
sed -e "s#/run/omarchy/sudo-passwordless#$lock_dir#g" \
-e "s#/run/lock/omarchy-sudo-passwordless.lock#$test_tmp/passwordless.lock#g" \
-e 's#/usr/bin/chown root:root "$LOCK_FILE"#/usr/bin/true#' >"$lock_lib"
worker="$test_tmp/worker.sh"
cat >"$worker" <<'WORKER'
#!/bin/bash
set -euo pipefail
source "$LOCK_LIB"
prepare_root_state() { :; }
critical() {
mkdir "$LOCK_SENTINEL"
sleep 0.03
rmdir "$LOCK_SENTINEL"
printf x >>"$LOCK_RESULTS"
}
with_root_lock critical
WORKER
chmod +x "$worker"
for _ in {1..8}; do
LOCK_LIB="$lock_lib" LOCK_SENTINEL="$test_tmp/held" LOCK_RESULTS="$test_tmp/results" bash "$worker" &
done
wait
[[ $(wc -c <"$test_tmp/results") == 8 ]] || fail "concurrent passwordless operations serialize"
pass "passwordless sudo serializes concurrent operations"
reset_grant
(
source "$library"
TEST_POST_PUBLISH_FAIL=1 TEST_DELETE_FAIL=1 assert_status 1 enable_locked 1000 15
[[ -e $(rule_file 1000) ]]
! grep -q '^systemctl stop ' "$test_tmp/commands"
)
pass "failed policy deletion retains the timer and reports failure"
# Same-boot expiry calls the fixed installed cleanup command, and cleanup
# removes policy before touching a timer so timer failures cannot extend it.
grep -F '"$INSTALLED_SELF" __expire "$uid" "$timer"' "$command_path" >/dev/null
cleanup_body=$(awk '/^cleanup_uid_locked\(\) \{/ { in_body=1 } in_body { print } in_body && /^}/ { exit }' "$command_path")
rm_line=$(grep -n '/usr/bin/rm -f' <<<"$cleanup_body" | head -1 | cut -d: -f1)
stop_line=$(grep -n 'stop_timer' <<<"$cleanup_body" | tail -1 | cut -d: -f1)
((rm_line < stop_line)) || fail "expiry removes sudo policy before timer cleanup"
pass "same-boot expiration is fixed-target and fail closed"
reset_grant
(
source "$library"
printf 'audituser ALL=(ALL) NOPASSWD: /usr/bin/true\n' >"$(rule_file 1000)"
cp "$(rule_file 1000)" "$test_tmp/admin-rule"
assert_status 2 status_locked 1000
assert_status 1 enable_locked 1000 15
assert_status 1 cleanup_uid_locked 1000
cmp "$(rule_file 1000)" "$test_tmp/admin-rule"
rm "$(rule_file 1000)"
ln -s "$test_tmp/admin-rule" "$(rule_file 1000)"
assert_status 2 status_locked 1000
assert_status 1 cleanup_uid_locked 1000
[[ -L $(rule_file 1000) ]]
)
pass "grant operations preserve administrator policies and reject symlinks"
reset_grant
(
source "$library"
TEST_BAD_PATH="$test_tmp/etc/sudoers.d" assert_status 1 enable_locked 1000 15
[[ ! -e $(rule_file 1000) ]]
rm "$PACKAGE_HOOK"
assert_status 1 enable_locked 1000 15
)
pass "publication requires trusted paths and the packaged cleanup hook"
reset_grant
cp "$ROOT/default/libalpm/hooks/05-omarchy-passwordless-revoke.hook" "$test_tmp/hooks/"
(
source "$library"
TEST_ACCOUNT='buildbot$' enable_locked 1000 15
read_grant 1000
[[ $GRANT_NAME == 'buildbot$' ]]
/usr/sbin/visudo -cf "$(rule_file 1000)" >/dev/null
cleanup_uid_locked 1000
[[ ! -e $(rule_file 1000) ]]
)
pass "trailing-dollar accounts publish and revoke valid native policy"
+114 -213
View File
@@ -2,234 +2,135 @@
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
children=()
cleanup() {
local status=$?
trap - EXIT
if (( ${#children[@]} )); then
kill "${children[@]}" 2>/dev/null || true
wait "${children[@]}" 2>/dev/null || true
fi
rm -rf "$test_tmp"
exit "$status"
}
trap cleanup EXIT
# All policy, state, locks and command mutations stay in this private fixture.
# Native visudo validates inert fragments; no test installs host sudo policy.
mkdir -p "$test_tmp/bin" "$test_tmp/state" "$test_tmp/etc/sudoers.d" "$test_tmp/etc/tmpfiles.d" "$test_tmp/run/lock" "$test_tmp/hooks"
export TEST_GRANT_ROOT="$test_tmp"
cat >"$test_tmp/bin/stat" <<'STUB'
#!/bin/bash
case $2 in
'%u') printf '0\n' ;;
'%a') if [[ -d ${@: -1} ]]; then printf '755\n'; else printf '644\n'; fi ;;
'%u %a') if [[ -d ${@: -1} ]]; then printf '0 755\n'; else printf '0 644\n'; fi ;;
*) exec /usr/bin/stat "$@" ;;
esac
STUB
cat >"$test_tmp/bin/install" <<'STUB'
#!/bin/bash
args=()
while (($#)); do
case $1 in -o|-g) shift 2 ;; *) args+=("$1"); shift ;; esac
done
exec /usr/bin/install "${args[@]}"
STUB
cat >"$test_tmp/bin/rm" <<'STUB'
#!/bin/bash
for path in "$@"; do
if [[ ${TEST_FAIL_TEMP_CLEANUP:-0} == 1 && $path == "$TEST_GRANT_ROOT/state/".sudoers.* ]]; then exit 1; fi
if [[ ${TEST_FAIL_RULE_DELETE:-0} == 1 && $path == "$TEST_GRANT_ROOT/etc/sudoers.d/"* ]]; then exit 1; fi
done
exec /usr/bin/rm "$@"
STUB
cat >"$test_tmp/bin/systemctl" <<'STUB'
#!/bin/bash
printf '%s\n' "$*" >>"$TEST_GRANT_ROOT/systemctl.log"
exit 0
STUB
chmod +x "$test_tmp/bin/"*
library="$test_tmp/grant-functions.sh"
{
printf 'source %q\n' "$ROOT/bin/omarchy-security-functions"
awk '/^set -euo pipefail$/ { functions=1 } /^case "\$\{1:-\}" in$/ { exit } functions { print }' "$ROOT/bin/omarchy-sudo-passwordless"
} | sed \
-e "s|/var/lib/omarchy/sudo-passwordless|$test_tmp/state|g" \
-e "s|/etc/sudoers.d|$test_tmp/etc/sudoers.d|g" \
-e "s|/etc/tmpfiles.d|$test_tmp/etc/tmpfiles.d|g" \
-e "s|/usr/share/libalpm/hooks|$test_tmp/hooks|g" \
-e "s|/run/lock/omarchy-sudo-passwordless.lock|$test_tmp/run/lock/omarchy-sudo-passwordless.lock|g" \
-e "s|/run/omarchy-sudo-passwordless-package-removing|$test_tmp/run/omarchy-sudo-passwordless-package-removing|g" \
-e "s|/usr/bin/stat|$test_tmp/bin/stat|g" \
-e "s|/usr/bin/install|$test_tmp/bin/install|g" \
-e "s|/usr/bin/rm|$test_tmp/bin/rm|g" \
-e "s|/usr/bin/systemctl|$test_tmp/bin/systemctl|g" \
-e 's|/usr/bin/chown|/usr/bin/true|g' >"$library"
cp "$ROOT/default/libalpm/hooks/05-omarchy-passwordless-revoke.hook" "$test_tmp/hooks/"
printf 'r! /etc/sudoers.d/99-omarchy-nopasswd-*\n' >"$test_tmp/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
# The expected policy text is mapped along with its filename in this fixture.
sed -i "s|/etc/sudoers.d|$test_tmp/etc/sudoers.d|" "$test_tmp/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
source "$SHELL_TEST_DIR/fixtures/passwordless-sudo-test.sh"
(
source "$library"
for name in 'buildbot$' audituser aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; do
valid_account_name "$name" || fail "supported account name rejected: $name"
printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$name" >"$test_tmp/name-policy"
/usr/sbin/visudo -cf "$test_tmp/name-policy" >/dev/null
done
for name in 'a$b' '$' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; do
! valid_account_name "$name" || fail "invalid account name accepted"
done
! valid_uid 18446744073709551617 || fail "overflowed UID accepted"
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$"
remove_known_legacy_rules
[[ ! -e $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-buildbot\$ ]]
) || fail "supported account names and legacy cleanup disagree"
pass "provisioning-compatible names validate as sudoers and clean up correctly"
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
[[ ! -e $(rule_file 1000) && -e $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-custom ]]
! compgen -G "$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-buildbot*"
)
pass "legacy cleanup removes generated orphan rules and preserves custom policy"
transaction_setup() {
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
prepare_root_state() { :; }
start_expiry_timer() { printf '%s\n' "$3" >>"$test_tmp/armed"; }
stop_timer() { printf '%s\n' "$1" >>"$test_tmp/stopped"; }
# Run the actual migration queue for separate temporary homes. Sudo only calls
# the mapped helper and can be refused without requesting host authorization.
mkdir -p "$test_tmp/source/migrations"
sed "s|/usr/bin/omarchy-sudo-passwordless|$test_tmp/omarchy-sudo-passwordless|g" \
"$ROOT/migrations/1788163635.sh" >"$test_tmp/source/migrations/1788163635.sh"
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"
printf 'audituser ALL=(ALL) NOPASSWD: ALL\n' >"$(rule_file 1000)"
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 ]]
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"
transaction_setup
TEST_FAIL_TEMP_CLEANUP=1 enable_locked 1000 15 && exit 1
[[ ! -e $(rule_file 1000) && ! -e $(state_file 1000) && -s $test_tmp/stopped ]]
) || fail "post-publication cleanup failure did not revoke before timer cleanup"
pass "failed temporary cleanup after publication revokes the live policy"
rm -f "$test_tmp/stopped"
(
source "$library"
transaction_setup
TEST_FAIL_TEMP_CLEANUP=1 TEST_FAIL_RULE_DELETE=1 enable_locked 1000 15 && exit 1
[[ -f $(rule_file 1000) && -f $(state_file 1000) && ! -e $test_tmp/stopped ]]
if TEST_FAIL_RULE_DELETE=1 revoke_inactive_grant 1000; then exit 1; else status=$?; fi
(( status == 2 ))
) || fail "failed policy revocation disarmed expiry or claimed inactive status"
pass "failed revocation preserves expiry jobs and returns a distinct error"
(
source "$library"
transaction_setup
current_timer=$(read_state_timer 1000)
expire_locked 1000 omarchy-nopasswd-expire-1000-ffffffffffffffffffffffffffffffff
[[ -f $(rule_file 1000) ]]
expire_locked 1000
[[ -f $(rule_file 1000) ]]
expire_locked 1000 "$current_timer"
[[ ! -e $(rule_file 1000) ]]
) || fail "a predecessor timer invalidates its replacement"
pass "old and legacy timer callbacks preserve a newer valid grant"
(
source "$library"
transaction_setup
start_expiry_timer() {
: >"$REMOVAL_BLOCKER"
return 0
}
enable_locked 1000 15 && exit 1
[[ ! -e $(rule_file 1000) ]]
) || fail "publication ignores a lost package prerequisite"
rm "$test_tmp/run/omarchy-sudo-passwordless-package-removing"
pass "grant publication rechecks package availability after timer setup"
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
package_script="$pkgs_path/omarchy-settings/omarchy-settings.install"
[[ -f $package_script ]] || fail "package checkout is required for shared lifecycle coverage"
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" "$package_script" >"$test_tmp/package.install"
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 ]]
)
done
pass "both settings packages revoke grants, block publication, and recover on installation"
worker="$test_tmp/publisher.sh"
{
printf '#!/bin/bash\nset -euo pipefail\nsource %q\n' "$library"
declare -f transaction_setup
printf 'test_tmp=%q\ntransaction_setup\n' "$test_tmp"
cat <<'WORKER'
publish_rule() {
: >"$test_tmp/publisher.entered"
while [[ ! -e $test_tmp/publisher.release ]]; do sleep 0.02; done
printf 'audituser ALL=(ALL) NOPASSWD: ALL\n' >"$(rule_file "$1")"
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 enable_locked 1000 15
with_root_lock critical
WORKER
} >"$worker"
bash "$worker" >"$test_tmp/publisher.output" 2>&1 &
children+=("$!")
for ((attempt = 0; attempt < 250; attempt++)); do
[[ ! -e $test_tmp/publisher.entered ]] || break
sleep 0.02
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
[[ -e $test_tmp/publisher.entered ]] || fail "grant publisher did not enter the shared lock"
bash -euo pipefail -c 'source "$1"; : >"$2"; pre_remove; post_remove' bash \
"$test_tmp/package.install" "$test_tmp/removal.started" >"$test_tmp/removal.output" 2>&1 &
children+=("$!")
for ((attempt = 0; attempt < 250; attempt++)); do
[[ ! -e $test_tmp/removal.started ]] || break
sleep 0.02
done
[[ -e $test_tmp/removal.started ]] || fail "package removal did not start"
touch "$test_tmp/publisher.release"
for child in "${children[@]}"; do wait "$child" || fail "shared lifecycle worker failed"; 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")
touch "$test_tmp/release"
wait "$publisher" || fail "publisher failed" "$(cat "$test_tmp/publisher.log")"
wait "$removal" || fail "removal failed" "$(cat "$test_tmp/removal.log")"
children=()
[[ ! -e $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000 ]] || fail "removal left a concurrently published grant"
[[ -f $test_tmp/run/omarchy-sudo-passwordless-package-removing ]] || fail "removal did not block later publication"
(
source "$library"
transaction_setup
! with_root_lock enable_locked 1000 15
) || fail "a publisher can create a grant after package removal begins"
pass "package removal shares the grant lock and blocks later publication"
[[ ! -e $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000 ]]
[[ -f $test_tmp/run/omarchy-sudo-passwordless-package-removing ]]
pass "native lock serializes grant publication with package removal"
printf 'audituser ALL=(ALL) NOPASSWD: ALL\n' >"$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000"
if TEST_FAIL_RULE_DELETE=1 bash -euo pipefail -c 'source "$1"; post_remove' bash "$test_tmp/package.install" >"$test_tmp/removal-failure.output" 2>&1; then
fail "package removal hid a failed policy deletion"
fi
grep -q 'Administrator cleanup is required' "$test_tmp/removal-failure.output" || fail "package deletion failure lacks recovery guidance"
pass "package removal reports cleanup failures instead of successful revocation"
(
source "$library"
transaction_setup
rm -f "$REMOVAL_BLOCKER"
enable_locked 1000 5
record=$(read_state_record 1000)
expiry=${record#*$'\t'}
expiry=${expiry%%$'\t'*}
deadline=$(/usr/bin/date -u -d "@$expiry" +%Y%m%d%H%M%SZ)
[[ $(cat "$(rule_file 1000)") == "audituser ALL=(ALL) NOTAFTER=$deadline NOPASSWD: ALL" ]]
/usr/sbin/visudo -cf "$(rule_file 1000)" >/dev/null
classify_generated_rule "$(rule_file 1000)"
rm -f "$(state_file 1000)"
remove_known_legacy_rules
[[ ! -e $(rule_file 1000) ]]
) || fail "native sudo deadline or state-independent bounded rule cleanup is incorrect"
pass "sudo policy contains the same deadline and bounded orphan rules are recognized"
(
source "$library"
transaction_setup
rm -f "$REMOVAL_BLOCKER"
enable_locked 1000 5
if TEST_FAIL_RULE_DELETE=1 package_removing_locked; then exit 1; fi
[[ -f $REMOVAL_BLOCKER && -f $(rule_file 1000) ]]
! enable_locked 1000 5
package_removing_locked
[[ ! -e $(rule_file 1000) ]]
rm -f "$REMOVAL_BLOCKER" "$PACKAGE_HOOK"
! enable_locked 1000 5
) || fail "pre-transaction revocation error or missing hook does not prevent new grants"
pass "package hook fails closed and grants require its installed policy"
# 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"