Integrate the shared sudo lifecycle foundation
This commit is contained in:
commit
75e58f034f
26 files changed
+1596
-164
No files matched your search
@@ -31,8 +31,9 @@ chmod +x "$mock_bin"/*
|
||||
launch_log="$test_tmp/launch-log"
|
||||
PATH="$mock_bin:$PATH" OMARCHY_TEST_INSTALLED=true OMARCHY_TEST_LOG="$launch_log" \
|
||||
bash "$ROOT/bin/omarchy-launch-1password"
|
||||
grep -Fxq 'launch:-- 1password' "$launch_log" || fail "1Password launcher starts the installed app"
|
||||
pass "1Password launcher starts the installed app"
|
||||
grep -Fxq 'launch:-- 1password --force-device-scale-factor=1' "$launch_log" ||
|
||||
fail "1Password launcher starts the installed app at a fixed scale factor"
|
||||
pass "1Password launcher starts the installed app at a fixed scale factor"
|
||||
|
||||
PATH="$mock_bin:$PATH" OMARCHY_TEST_INSTALLED=false OMARCHY_TEST_LOG="$launch_log" \
|
||||
bash "$ROOT/bin/omarchy-launch-1password"
|
||||
|
||||
Regular → Executable
+438
-103
@@ -2,122 +2,457 @@
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
script="$ROOT/bin/omarchy-sudo-passwordless"
|
||||
tmpfiles_file="$ROOT/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
|
||||
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
|
||||
|
||||
mock_bin="$test_tmp/bin"
|
||||
grant="$test_tmp/grant"
|
||||
calls="$test_tmp/calls"
|
||||
mkdir -p "$mock_bin"
|
||||
|
||||
cat >"$mock_bin/gum" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
SH
|
||||
|
||||
cat >"$mock_bin/systemctl" <<'SH'
|
||||
#!/bin/bash
|
||||
|
||||
printf 'systemctl %s\n' "$*" >>"$TEST_CALLS"
|
||||
[[ ${1:-} == "is-active" && ${TEST_TIMER_ACTIVE:-false} == "true" ]]
|
||||
SH
|
||||
|
||||
cat >"$mock_bin/sudo" <<'SH'
|
||||
#!/bin/bash
|
||||
|
||||
printf 'sudo %s\n' "$*" >>"$TEST_CALLS"
|
||||
|
||||
case ${1:-} in
|
||||
test)
|
||||
[[ ${2:-} == "-f" && -f $TEST_GRANT ]]
|
||||
;;
|
||||
tee)
|
||||
/usr/bin/tee "$TEST_GRANT"
|
||||
;;
|
||||
chmod)
|
||||
/usr/bin/chmod "$2" "$TEST_GRANT"
|
||||
;;
|
||||
systemd-run)
|
||||
[[ ${TEST_FAIL_SYSTEMD_RUN:-false} != "true" ]]
|
||||
;;
|
||||
rm)
|
||||
/usr/bin/rm -f -- "$TEST_GRANT"
|
||||
;;
|
||||
systemctl)
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "unexpected sudo command: $*" >&2
|
||||
exit 90
|
||||
;;
|
||||
esac
|
||||
SH
|
||||
|
||||
chmod +x "$mock_bin/gum" "$mock_bin/sudo" "$mock_bin/systemctl"
|
||||
|
||||
run_command() {
|
||||
TEST_CALLS="$calls" TEST_GRANT="$grant" PATH="$mock_bin:$PATH" USER=alice \
|
||||
"$script" "$@"
|
||||
function_prefix() {
|
||||
printf 'source %q\n' "$security_library_path"
|
||||
awk '/^set -euo pipefail$/ { functions=1 } /^case "\$\{1:-\}" in$/ { exit } functions { print }' "$command_path"
|
||||
}
|
||||
|
||||
: >"$calls"
|
||||
enable_output=$(run_command 15)
|
||||
[[ -f $grant ]] || fail "successful timer setup leaves the passwordless sudo grant enabled"
|
||||
[[ $(cat "$grant") == "alice ALL=(ALL) NOPASSWD: ALL" ]] ||
|
||||
fail "the enabled grant belongs to the current user" "$(cat "$grant")"
|
||||
grep -q '^sudo systemd-run --on-active=15m .* rm -f -- /etc/sudoers.d/99-omarchy-nopasswd-alice$' "$calls" ||
|
||||
fail "enabling arms the expiry timer" "$(cat "$calls")"
|
||||
[[ $enable_output == *"automatically disable in 15 minutes"* ]] ||
|
||||
fail "success is reported after the timer is armed" "$enable_output"
|
||||
pass "enabling arms expiry before reporting success"
|
||||
# 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"
|
||||
|
||||
: >"$calls"
|
||||
rm -f "$grant"
|
||||
if failure_output=$(TEST_FAIL_SYSTEMD_RUN=true run_command 15 2>&1); then
|
||||
fail "enabling fails when the expiry timer cannot be armed"
|
||||
# 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
|
||||
[[ ! -e $grant ]] || fail "timer setup failure revokes the new passwordless sudo grant"
|
||||
[[ $failure_output == *"Revoking access now"* ]] ||
|
||||
fail "timer setup failure explains the fail-closed revocation" "$failure_output"
|
||||
[[ $failure_output != *"Passwordless sudo has been ENABLED"* ]] ||
|
||||
fail "timer setup failure does not report that passwordless sudo was enabled" "$failure_output"
|
||||
pass "timer setup failure revokes a new grant"
|
||||
|
||||
: >"$calls"
|
||||
printf 'alice ALL=(ALL) NOPASSWD: ALL\n' >"$grant"
|
||||
if update_output=$(TEST_TIMER_ACTIVE=true TEST_FAIL_SYSTEMD_RUN=true run_command 30 2>&1); then
|
||||
fail "updating fails when the replacement expiry timer cannot be armed"
|
||||
if [[ ${1:-} == -k ]]; then
|
||||
rm -f -- "$TEST_PUBLIC_TOKEN"
|
||||
exit 0
|
||||
fi
|
||||
[[ ! -e $grant ]] || fail "timer update failure revokes the existing passwordless sudo grant"
|
||||
[[ $update_output != *"timer updated"* ]] ||
|
||||
fail "timer update failure does not report success" "$update_output"
|
||||
pass "timer update failure revokes the existing grant"
|
||||
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
|
||||
|
||||
mapfile -t tmpfiles_rules < <(grep -vE '^[[:space:]]*(#|$)' "$tmpfiles_file")
|
||||
(( ${#tmpfiles_rules[@]} == 1 )) ||
|
||||
fail "passwordless sudo ships one tmpfiles rule" "${tmpfiles_rules[*]}"
|
||||
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"
|
||||
|
||||
fake_root="$test_tmp/root"
|
||||
# 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"
|
||||
grant_names=(alice buildbot-2 user.123 'service$')
|
||||
for grant_name in "${grant_names[@]}"; do
|
||||
touch "$sudoers_dir/99-omarchy-nopasswd-$grant_name"
|
||||
for name in alice buildbot-2 424242; do
|
||||
: >"$sudoers_dir/99-omarchy-nopasswd-$name"
|
||||
done
|
||||
touch "$sudoers_dir/omarchy-dns"
|
||||
: >"$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"
|
||||
|
||||
systemd-tmpfiles --root="$fake_root" --remove --inline "${tmpfiles_rules[@]}"
|
||||
[[ -f $sudoers_dir/99-omarchy-nopasswd-alice ]] ||
|
||||
fail "boot-only cleanup leaves a live grant alone outside boot"
|
||||
|
||||
systemd-tmpfiles --root="$fake_root" --remove --boot --inline "${tmpfiles_rules[@]}"
|
||||
for grant_name in "${grant_names[@]}"; do
|
||||
stale_grant="$sudoers_dir/99-omarchy-nopasswd-$grant_name"
|
||||
[[ ! -e $stale_grant ]] || fail "boot cleanup removes every generated grant" "$stale_grant"
|
||||
# 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
|
||||
[[ -f $sudoers_dir/omarchy-dns ]] || fail "boot cleanup preserves unrelated sudoers rules"
|
||||
pass "systemd-tmpfiles removes generated grants only during boot"
|
||||
[[ -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"
|
||||
@@ -0,0 +1,235 @@
|
||||
#!/bin/bash
|
||||
|
||||
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 "$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 '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"
|
||||
|
||||
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"; }
|
||||
}
|
||||
|
||||
(
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
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")"
|
||||
}
|
||||
with_root_lock enable_locked 1000 15
|
||||
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
|
||||
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
|
||||
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"
|
||||
|
||||
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"
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
test_home="$test_tmp/home"
|
||||
mock_bin="$test_tmp/bin"
|
||||
mkdir -p "$mock_bin"
|
||||
|
||||
# Run the real theme refresh, renderer and publisher in a throwaway home.
|
||||
# Only package installation, the T3 CLI and desktop launch are stubbed.
|
||||
cat >"$mock_bin/omarchy-pkg-add" <<'SH'
|
||||
#!/bin/bash
|
||||
[[ $1 == "t3code-bin" ]]
|
||||
SH
|
||||
|
||||
cat >"$mock_bin/t3" <<'SH'
|
||||
#!/bin/bash
|
||||
[[ $1 == "theme" && $2 == "set" && $3 == "omarchy" && $4 == "--base-dir" && $5 == "$T3CODE_HOME" ]] || exit 1
|
||||
palette="$T3CODE_HOME/userdata/themes/omarchy.json"
|
||||
if [[ ! -f $palette ]] || ! jq -e '.name == "Omarchy" and (.accent | test("^#[0-9a-fA-F]{6}$"))' "$palette" >/dev/null; then
|
||||
echo "No published Omarchy theme"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ${OMARCHY_TEST_T3_FAIL:-0} == "1" ]]; then
|
||||
echo "T3 theme selection failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "T3 selected Omarchy"
|
||||
SH
|
||||
|
||||
cat >"$mock_bin/setsid" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$mock_bin"/*
|
||||
|
||||
run_installer() {
|
||||
HOME="$test_home" T3CODE_HOME="$test_tmp/custom t3 home" \
|
||||
OMARCHY_PATH="$ROOT" PATH="$mock_bin:$ROOT/bin:$PATH" \
|
||||
OMARCHY_THEME_HEADLESS=1 XDG_RUNTIME_DIR="$test_tmp" \
|
||||
"$ROOT/bin/omarchy-install-ai-t3-code" >"$test_tmp/output" 2>&1
|
||||
}
|
||||
|
||||
state="$test_home/.local/state/omarchy/current"
|
||||
palette="$test_tmp/custom t3 home/userdata/themes/omarchy.json"
|
||||
settings="$test_tmp/custom t3 home/userdata/settings.json"
|
||||
mkdir -p "$state/theme"
|
||||
echo 'tokyo-night' >"$state/theme.name"
|
||||
cp "$ROOT/themes/tokyo-night/colors.toml" "$state/theme/colors.toml"
|
||||
|
||||
run_installer || fail "installing after an update succeeds" "$(cat "$test_tmp/output")"
|
||||
[[ -f $state/theme/t3code.json && -f $palette ]] || fail "the missing palette is rendered and published"
|
||||
cmp -s "$state/theme/t3code.json" "$palette" || fail "T3 receives the current theme's palette"
|
||||
grep -q 'T3 selected Omarchy' "$test_tmp/output" || fail "the published palette is selected"
|
||||
grep -q 'Opening T3 Code' "$test_tmp/output" || fail "the themed app opens"
|
||||
[[ ! -e $test_home/.t3 ]] || fail "the custom T3 home is respected"
|
||||
pass "an update with no staged T3 palette renders, publishes and selects it before launch"
|
||||
|
||||
# Preserve an already rendered palette, including a user-supplied color.
|
||||
jq '.accent = "#abcdef"' "$state/theme/t3code.json" >"$test_tmp/palette.json"
|
||||
cp "$test_tmp/palette.json" "$state/theme/t3code.json"
|
||||
run_installer || fail "installing with an existing palette succeeds" "$(cat "$test_tmp/output")"
|
||||
cmp -s "$test_tmp/palette.json" "$palette" || fail "an existing palette is published without re-staging"
|
||||
pass "an existing palette is used without refreshing the current theme"
|
||||
|
||||
printf '{"defaultTheme":"dark","keep":"existing settings"}\n' >"$settings"
|
||||
cp "$settings" "$test_tmp/settings-before.json"
|
||||
if OMARCHY_TEST_T3_FAIL=1 run_installer; then
|
||||
fail "a failed T3 theme selection fails the installer"
|
||||
fi
|
||||
grep -q 'T3 theme selection failed' "$test_tmp/output" || fail "the CLI failure is visible"
|
||||
if grep -Eq 'Opening T3 Code|T3 Code has been installed' "$test_tmp/output"; then
|
||||
fail "a failed selection does not report successful setup"
|
||||
fi
|
||||
cmp -s "$test_tmp/settings-before.json" "$settings" || fail "a failed selection does not rewrite settings"
|
||||
pass "a CLI failure remains visible and leaves the existing settings intact"
|
||||
@@ -207,7 +207,7 @@ pass "a theme name cannot climb out of the theme directories"
|
||||
# generates. Every generated theme file is either denied to an installed theme or
|
||||
# recorded here as carrying colour, so a new template fails until it is placed.
|
||||
denied=(alacritty.toml foot.ini ghostty.conf kitty.conf gum_env.lua hyprland.lua neovim.lua vscode.json)
|
||||
colour_only=(btop.theme chromium.theme claude.json helix.toml hermes.yaml hyprland-preview-share-picker.css keyboard.rgb obsidian.css pi.json shell.toml vscode-theme.json)
|
||||
colour_only=(btop.theme chromium.theme claude.json helix.toml hermes.yaml hyprland-preview-share-picker.css keyboard.rgb obsidian.css pi.json shell.toml t3code.json vscode-theme.json)
|
||||
|
||||
for tpl in "$ROOT"/default/themed/*.tpl; do
|
||||
generated=$(basename "$tpl" .tpl)
|
||||
|
||||
@@ -51,3 +51,36 @@ pass "bar on is idempotent"
|
||||
HOME="$test_home" omarchy-toggle-bar off
|
||||
[[ ! -f $bar_flag ]] || fail "bar off disables bar-off toggle"
|
||||
pass "bar off disables bar-off toggle"
|
||||
|
||||
# The gaps half of full screen copies a flag file in and reloads Hyprland, so
|
||||
# give it this checkout to copy from and a hyprctl that answers without a
|
||||
# compositor.
|
||||
export OMARCHY_PATH="$ROOT"
|
||||
stub_bin="$TMPDIR/bin"
|
||||
mkdir -p "$stub_bin"
|
||||
printf '#!/bin/bash\nexit 0\n' >"$stub_bin/hyprctl"
|
||||
chmod +x "$stub_bin/hyprctl"
|
||||
export PATH="$stub_bin:$PATH"
|
||||
|
||||
gaps_flag="$test_home/.local/state/omarchy/toggles/hypr/window-no-gaps.lua"
|
||||
|
||||
HOME="$test_home" omarchy-toggle-fullscreen-desktop
|
||||
[[ -f $bar_flag && -f $gaps_flag ]] || fail "fullscreen toggle hides the bar and the gaps together"
|
||||
pass "fullscreen toggle hides the bar and the gaps together"
|
||||
|
||||
HOME="$test_home" omarchy-toggle-fullscreen-desktop
|
||||
[[ ! -f $bar_flag && ! -f $gaps_flag ]] || fail "fullscreen toggle restores the bar and the gaps together"
|
||||
pass "fullscreen toggle restores the bar and the gaps together"
|
||||
|
||||
HOME="$test_home" omarchy-toggle-bar on
|
||||
HOME="$test_home" omarchy-toggle-fullscreen-desktop
|
||||
[[ -f $bar_flag && -f $gaps_flag ]] || fail "fullscreen toggle pulls a half-hidden desktop into full screen"
|
||||
pass "fullscreen toggle pulls a half-hidden desktop into full screen"
|
||||
|
||||
HOME="$test_home" omarchy-toggle-fullscreen-desktop off
|
||||
[[ ! -f $bar_flag && ! -f $gaps_flag ]] || fail "fullscreen off leaves full screen"
|
||||
pass "fullscreen off leaves full screen"
|
||||
|
||||
HOME="$test_home" omarchy-toggle-fullscreen-desktop on
|
||||
[[ -f $bar_flag && -f $gaps_flag ]] || fail "fullscreen on enters full screen"
|
||||
pass "fullscreen on enters full screen"
|
||||
Reference in new issue
Block a user