Files
omarchy/test/shell.d/passwordless-grant-lifecycle-test.sh
T

236 lines
9.4 KiB
Bash

#!/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"