#!/bin/bash set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-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 ) pass "passwordless sudo validates a bounded positive duration" # 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" # 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" 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" done 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" 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" # 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 fi done [[ -n $pkgs_root ]] || fail "omarchy-pkgs checkout found for passwordless package-removal coverage" 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 "$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" done pass "settings package transitions revoke grants and preserve unrelated configuration" # 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" # 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"