The layout check skips comments, but the already-fixed check matched `include system-auth` anywhere on a line, so a comment such as `# auth include system-auth` made the migration skip a stack that still listed bare pam_unix, and it was marked complete with the lockout still missing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
180 lines
7.4 KiB
Bash
180 lines
7.4 KiB
Bash
#!/bin/bash
|
|
|
|
set -euo pipefail
|
|
|
|
source "$(dirname "$0")/base-test.sh"
|
|
|
|
# Exercises migrations/1788256455.sh, which repairs an Omarchy-created
|
|
# /etc/pam.d/polkit-1 that lists pam_unix directly (dropping pam_faillock)
|
|
# instead of including system-auth. The migration keeps its production path
|
|
# fixed; as in sshd-hardening-migration-test.sh, this test rewrites that one
|
|
# assignment in the input fed to bash and stubs sudo so nothing touches the
|
|
# host's /etc.
|
|
|
|
test_dir=$(mktemp -d)
|
|
trap 'rm -rf "$test_dir"' EXIT
|
|
|
|
migration="$ROOT/migrations/1788256455.sh"
|
|
stub_bin="$test_dir/bin"
|
|
mkdir -p "$stub_bin"
|
|
|
|
# sudo stub: log, optionally refuse (SUDO_ALLOWED=0), and optionally make the
|
|
# `sed` rewrite empty the file and report success (WRITE_BREAKS=1) so the
|
|
# verification/restore path is exercised. Otherwise run the real command so
|
|
# cp/sed act on the temp file.
|
|
cat >"$stub_bin/sudo" <<'STUB'
|
|
#!/bin/bash
|
|
printf 'sudo %s\n' "$*" >>"${CALL_LOG:?}"
|
|
if [[ ${SUDO_ALLOWED:-1} != "1" ]]; then
|
|
exit 1
|
|
elif [[ ${WRITE_BREAKS:-0} == "1" && $1 == "sed" ]]; then
|
|
: >"${!#}"
|
|
else
|
|
exec "$@"
|
|
fi
|
|
STUB
|
|
chmod +x "$stub_bin"/*
|
|
|
|
# Run the migration against a polkit-1 file seeded with $1; leaves the result in
|
|
# "$test_dir/<scenario>/polkit-1" and records the exit status in migrate_rc.
|
|
migrate_rc=0
|
|
run_migration() {
|
|
local scenario=$1 content=$2
|
|
local dir="$test_dir/$scenario"
|
|
local polkit="$dir/polkit-1"
|
|
mkdir -p "$dir"
|
|
printf '%s' "$content" >"$polkit"
|
|
: >"$test_dir/$scenario.calls"
|
|
|
|
migrate_rc=0
|
|
sed "s|^polkit=/etc/pam.d/polkit-1\$|polkit=$polkit|" "$migration" |
|
|
CALL_LOG="$test_dir/$scenario.calls" PATH="$stub_bin:$PATH" \
|
|
SUDO_ALLOWED="${SUDO_ALLOWED:-1}" WRITE_BREAKS="${WRITE_BREAKS:-0}" \
|
|
bash -euo pipefail >/dev/null 2>&1 || migrate_rc=$?
|
|
}
|
|
|
|
result() { cat "$test_dir/$1/polkit-1"; }
|
|
|
|
# The four layouts the old setup / remove commands leave behind.
|
|
fingerprint_stack='auth [success=1 default=ignore] pam_exec.so quiet /usr/bin/omarchy-hw-laptop-closed
|
|
auth sufficient pam_fprintd.so
|
|
auth required pam_unix.so
|
|
|
|
account required pam_unix.so
|
|
password required pam_unix.so
|
|
session required pam_unix.so
|
|
'
|
|
fido2_stack='auth sufficient pam_u2f.so cue authfile=/etc/fido2/fido2
|
|
auth required pam_unix.so
|
|
|
|
account required pam_unix.so
|
|
password required pam_unix.so
|
|
session required pam_unix.so
|
|
'
|
|
both_stack='auth [success=1 default=ignore] pam_exec.so quiet /usr/bin/omarchy-hw-laptop-closed
|
|
auth sufficient pam_fprintd.so
|
|
auth sufficient pam_u2f.so cue authfile=/etc/fido2/fido2
|
|
auth required pam_unix.so
|
|
|
|
account required pam_unix.so
|
|
password required pam_unix.so
|
|
session required pam_unix.so
|
|
'
|
|
# What both remove commands leave: their own marker lines stripped, the bare
|
|
# pam_unix stack (and no marker) behind.
|
|
markerless_stack='auth required pam_unix.so
|
|
|
|
account required pam_unix.so
|
|
password required pam_unix.so
|
|
session required pam_unix.so
|
|
'
|
|
fixed_stack='auth sufficient pam_fprintd.so
|
|
auth include system-auth
|
|
account include system-auth
|
|
password include system-auth
|
|
session include system-auth
|
|
'
|
|
# An administrator's own stack that happens to use pam_fprintd but carries an
|
|
# extra directive Omarchy never writes.
|
|
admin_stack='auth sufficient pam_fprintd.so
|
|
auth required pam_unix.so
|
|
auth optional pam_permit.so
|
|
account required pam_unix.so
|
|
password required pam_unix.so
|
|
session required pam_unix.so
|
|
'
|
|
|
|
# Every phase of a repaired stack must defer to system-auth and no bare pam_unix
|
|
# may remain in the account/password/session block.
|
|
assert_repaired() {
|
|
local scenario=$1 phase
|
|
for phase in auth account password session; do
|
|
grep -qE "^${phase}[[:space:]]+include[[:space:]]+system-auth" <<<"$(result "$scenario")" ||
|
|
fail "$scenario: $phase defers to system-auth" "$(result "$scenario")"
|
|
done
|
|
! grep -qE '^(account|password|session)[[:space:]]+required[[:space:]]+pam_unix' <<<"$(result "$scenario")" ||
|
|
fail "$scenario: no bare pam_unix remains" "$(result "$scenario")"
|
|
}
|
|
|
|
run_migration fingerprint "$fingerprint_stack"
|
|
(( migrate_rc == 0 )) || fail "fingerprint stack migrates cleanly"
|
|
assert_repaired fingerprint
|
|
grep -qF 'pam_fprintd.so' <<<"$(result fingerprint)" || fail "fingerprint line is preserved"
|
|
grep -qF 'omarchy-hw-laptop-closed' <<<"$(result fingerprint)" || fail "clamshell gate is preserved"
|
|
pass "migration repairs the fingerprint stack and keeps its hardware-auth lines"
|
|
|
|
run_migration fido2 "$fido2_stack"
|
|
(( migrate_rc == 0 )) || fail "fido2 stack migrates cleanly"
|
|
assert_repaired fido2
|
|
grep -qF 'pam_u2f.so cue authfile=/etc/fido2/fido2' <<<"$(result fido2)" || fail "FIDO2 line is preserved"
|
|
pass "migration repairs the FIDO2 stack and keeps its hardware-auth line"
|
|
|
|
run_migration both "$both_stack"
|
|
(( migrate_rc == 0 )) || fail "combined stack migrates cleanly"
|
|
assert_repaired both
|
|
grep -qF 'pam_fprintd.so' <<<"$(result both)" && grep -qF 'pam_u2f.so' <<<"$(result both)" ||
|
|
fail "both hardware-auth lines are preserved"
|
|
pass "migration repairs a combined fingerprint+FIDO2 stack"
|
|
|
|
run_migration markerless "$markerless_stack"
|
|
(( migrate_rc == 0 )) || fail "markerless stack migrates cleanly"
|
|
assert_repaired markerless
|
|
pass "migration repairs the markerless post-removal stack"
|
|
|
|
run_migration comment "# managed by omarchy
|
|
$fingerprint_stack"
|
|
(( migrate_rc == 0 )) || fail "commented stack migrates cleanly"
|
|
assert_repaired comment
|
|
grep -qxF '# managed by omarchy' <<<"$(result comment)" || fail "comments are preserved through the rewrite"
|
|
pass "migration repairs a commented stack and preserves the comment"
|
|
|
|
run_migration commented-include "# auth include system-auth
|
|
$fingerprint_stack"
|
|
(( migrate_rc == 0 )) || fail "a stack with a commented-out include migrates cleanly"
|
|
assert_repaired commented-include
|
|
pass "migration repairs a stack whose only include is commented out"
|
|
|
|
run_migration fixed "$fixed_stack"
|
|
[[ "$(result fixed)" == "$(printf '%s' "$fixed_stack")" ]] || fail "an already-fixed stack is left byte-for-byte unchanged"
|
|
! grep -q '^sudo ' "$test_dir/fixed.calls" || fail "an already-fixed stack triggers no privileged writes"
|
|
pass "migration is idempotent: an already-fixed stack is untouched"
|
|
|
|
run_migration admin "$admin_stack"
|
|
[[ "$(result admin)" == "$(printf '%s' "$admin_stack")" ]] || fail "an administrator-authored stack is left unchanged"
|
|
! grep -q '^sudo ' "$test_dir/admin.calls" || fail "an administrator-authored stack triggers no privileged writes"
|
|
pass "migration refuses a stack carrying non-Omarchy directives"
|
|
|
|
# Privilege failure is the retryable case: the migration must exit non-zero so
|
|
# omarchy-migrate does not record it complete, and must leave the file unchanged.
|
|
SUDO_ALLOWED=0 run_migration no-sudo "$fingerprint_stack"
|
|
(( migrate_rc != 0 )) || fail "the migration stays pending when privileges are unavailable"
|
|
[[ "$(result no-sudo)" == "$(printf '%s' "$fingerprint_stack")" ]] || fail "a failed repair leaves the original file intact"
|
|
pass "migration exits non-zero and preserves the file when sudo is refused"
|
|
|
|
# A write that does not take effect must be caught by verification, restored,
|
|
# and reported as a failure rather than silently marked complete.
|
|
WRITE_BREAKS=1 run_migration verify-fail "$fingerprint_stack"
|
|
(( migrate_rc != 0 )) || fail "a failed verification exits non-zero"
|
|
[[ "$(result verify-fail)" == "$(printf '%s' "$fingerprint_stack")" ]] || fail "a failed verification restores the original file"
|
|
pass "migration exits non-zero and restores when the write cannot be verified"
|