Files

206 lines
9.5 KiB
Bash

#!/bin/bash
# The private tier, end to end, against a local stand-in for everything remote.
#
# Nothing here touches the real endpoint, the real repos or the real secrets
# manager, so it can be run as often as it takes and costs nothing when it
# fails. What it does exercise is the real code: dotup's prompt loop, the real
# credential splitting, chezmoi's real seven-question TUI, and the private
# tier's real `dotsecrets` -- copied verbatim into the fake source tree, so it
# is the shipping implementation being measured, not a rewrite of it.
#
# The first password is deliberately wrong. These credentials are asked for at
# the very end of a run, so before the retry loop existed one typo meant redoing
# the entire install. Getting it wrong on purpose is the only way to prove the
# recovery path is there and that the URL and username survive the mistake.
set -u
fail() { echo "FAIL: $*"; exit 1; }
ok() { echo " ok $*"; }
umask 022 # modes below assume it; do not let the daemon's umask decide
sudo apt-get update -qq && sudo apt-get install -y -qq git expect unzip >/dev/null 2>&1
echo " NOTE installed git by hand -- see ISSUE-1"
# ---- public tier first: the private tier is a continuation, never a start ----
sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply "$PUB_URL" >/tmp/init.log 2>&1 \
|| { tail -5 /tmp/init.log; fail "public tier init"; }
export PATH="$HOME/.local/bin:$HOME/bin:$PATH"
D=$HOME/.local/bin/dotup
[ -x "$D" ] || fail "no dotup after the public apply"
ok "public tier applied"
# absence assertions BEFORE the private tier exists, so their later presence
# means something
for f in "$HOME/.config/zsh/secrets.zsh" "$HOME/.config/bitwarden/bws-token" \
"$HOME/.local/share/dotfiles-private"; do
[ ! -e "$f" ] || fail "$f exists on a public-only machine"
done
ok "public-only machine carries no token, no secrets, no private source"
# ---- a fake bws, so nothing reaches the network -----------------------------
# `ensure_bws` checks `have bws` first and `have` searches ~/.local/bin, so
# putting the stub there is enough to keep the real download out of this run.
# The real ensure_bws (pin, checksum, musl target) is a separate scenario --
# faking it here would only prove the fake works.
mkdir -p "$HOME/.local/bin"
cat > "$HOME/.local/bin/bws" <<'BWS'
#!/bin/sh
# Stand-in for the Bitwarden Secrets Manager CLI. Answers exactly the call
# dotsecrets makes: `bws secret get <uuid> -o env`, printing KEY=VALUE.
# LAB_BWS_MODE bends it to drive the failure branches.
case "${LAB_BWS_MODE:-ok}" in
fail) exit 1 ;;
wrong) printf 'SOMETHING_ELSE=x\n'; exit 0 ;;
empty) printf 'LAB_ALPHA_API_KEY=\n'; exit 0 ;;
esac
[ "$1" = secret ] && [ "$2" = get ] || { echo "unsupported: $*" >&2; exit 2; }
case "$3" in
*0001) k=LAB_ALPHA_API_KEY ;; *0002) k=LAB_BRAVO_API_KEY ;;
*0003) k=LAB_CHARLIE_API_KEY ;; *0004) k=LAB_DELTA_API_KEY ;;
*0005) k=LAB_ECHO_API_KEY ;; *0006) k=LAB_FOXTROT_API_KEY ;;
*0007) k=LAB_GOLF_API_KEY ;; *) echo "unknown id $3" >&2; exit 1 ;;
esac
printf '%s=lab-value-for-%s\n' "$k" "$k"
BWS
chmod 755 "$HOME/.local/bin/bws"
ok "fake bws in place; this run reaches no network"
# ---- tick the private rows, as the picker would ------------------------------
S=${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles
mkdir -p "$S"; printf 'private/private-repo\nprivate/bws-secrets\n' > "$S/selected"
# ---- drive it exactly as a person sitting at the keyboard would --------------
export E2E_NAME='Lab Tester' E2E_EMAIL='lab@example.invalid'
timeout 420 expect -f - >/tmp/private.log 2>&1 <<'EXP'
set timeout 120
log_user 1
spawn -noecho env PATH=$env(PATH) $env(HOME)/.local/bin/dotup private
# A bare `expect -re {pat} {}` treats a timeout as "carry on", so a missed
# prompt falls through silently and every later expect waits out its own
# timeout, surfacing much later as an unattributed hang. Name what was missed.
proc wait_for {pat what} {
expect {
-re $pat {}
timeout { send_user "\nTIMEOUT: never saw $what\n"; exit 3 }
eof { send_user "\nEOF before $what -- dotup exited early\n"; exit 4 }
}
}
# dotup's own three prompts are plain `read` in cooked mode, so the text is the
# whole signal.
wait_for {Bootstrap URL:} "the bootstrap URL prompt"
send -- "$env(BOOT_URL)\r"
wait_for {Username:} "the username prompt"
send -- "$env(BOOT_USER)\r"
wait_for {Password:} "the password prompt"
send -- "wrong-on-purpose\r"
wait_for {wrong username or password} "the 401 message naming the actual fault"
# The bracketed default is the proof that the URL and username were retained,
# so only the password has to be retyped.
wait_for {Bootstrap URL \[} "the retry prompt with the URL kept"
send -- "\r"
wait_for {Username \[} "the retry prompt with the username kept"
send -- "\r"
wait_for {Password:} "the retry password prompt"
send -- "$env(BOOT_PW)\r"
# chezmoi's seven are a full-screen TUI, and matching the prompt TEXT is not
# enough. chezmoi writes the prompt while the tty is still in cooked mode and
# only then switches to raw with TCSAFLUSH, which DISCARDS anything already
# buffered. An answer sent on the text alone can land in that window and be
# thrown away -- the field sits unsubmitted and the run burns its whole
# timeout. `\033[?2004h` is bracketed-paste-on, emitted only after raw mode is
# established, so waiting for it turns "probably ready" into "demonstrably
# ready". Each prompt emits its own.
proc ask {pat val} {
wait_for $pat "chezmoi prompt $pat"
wait_for "\033\\\[\\?2004h" "raw mode after $pat (the TUI never became ready)"
send -- "$val\r"
}
ask {user\.name} "$env(E2E_NAME)"
ask {user\.email} "$env(E2E_EMAIL)"
ask {signing key} ""
ask {WAN ssh} ""
ask {LAN ssh} ""
ask {WAN web} ""
ask {LAN web} ""
expect eof
catch wait result
exit [lindex $result 3]
EXP
rc=$?
red() { sed -e "s|$BOOT_PW|<BOOT_PW>|g" -e "s|$BOOT_URL|<BOOT_URL>|g"; }
case $rc in
124) red </tmp/private.log | tail -8 | sed 's/^/ /'
fail "hit the 420s ceiling -- expect wedged past its own handlers" ;;
3|4) red </tmp/private.log | tr -d '\r' | tail -8 | sed 's/^/ /'
fail "a prompt went unanswered -- the TIMEOUT/EOF line above names it" ;;
esac
red </tmp/private.log | tr -d '\r' | tail -6 | sed 's/^/ | /'
# ---- what must be true afterwards -------------------------------------------
echo "-- verification --"
m() { stat -c '%a' "$1" 2>/dev/null; }
[ -d "$HOME/.local/share/dotfiles-private" ] || fail "private source not cloned"
case $(m "$HOME/.local/share/dotfiles-private") in *00) ;; *)
fail "private source is mode $(m "$HOME/.local/share/dotfiles-private") — group/other can read it" ;; esac
ok "private source cloned, go-rwx"
[ "$(m "$HOME/.config/bitwarden/bws-token")" = 600 ] || fail "bws token mode $(m "$HOME/.config/bitwarden/bws-token"), want 600"
ok "bws token written, mode 600"
SEC=$HOME/.config/zsh/secrets.zsh
[ -r "$SEC" ] || fail "secrets.zsh not generated"
[ "$(m "$SEC")" = 600 ] || fail "secrets.zsh mode $(m "$SEC"), want 600"
n=$(grep -c '^export ' "$SEC")
[ "$n" -eq 8 ] || fail "secrets.zsh has $n exports, want 8 (7 secrets + 1 alias)"
grep -q '^export LAB_GOLF_ALIAS_KEY=' "$SEC" || fail "the alias export is missing"
ok "secrets.zsh mode 600, 8 exports, alias present"
for p in "$HOME/.config/zsh:700" "$HOME/.ssh:700" "$HOME/.ssh/config:600" \
"$HOME/.config/zsh/local.zsh:600" "$HOME/.local/bin/dotsecrets:700"; do
want=${p##*:}; path=${p%:*}
[ "$(m "$path")" = "$want" ] || fail "$path is mode $(m "$path"), want $want"
done
ok "every private destination carries the mode it claims"
# The credential must survive as a file and nowhere else.
CRED=${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles/private-credentials
[ "$(m "$CRED")" = 600 ] || fail "credential file mode $(m "$CRED"), want 600"
GC=$HOME/.local/share/dotfiles-private/.git/config
grep -qE '://[^/@[:space:]]*:[^/@[:space:]]+@' "$GC" \
&& fail "the clone's .git/config still carries the token"
grep -qE '://[^/@[:space:]]*:[^/@[:space:]]+@' /tmp/private.log \
&& fail "a credential-bearing URL appears in dotup's own output"
ok "token is in the 600-mode credential file only — not in .git/config, not in the log"
git -C "$HOME/.local/share/dotfiles-private" fetch -q 2>/dev/null \
|| fail "a later fetch cannot authenticate — the credential helper did not survive"
ok "a later fetch still authenticates from that file"
# ---- the failure branches: yesterday's keys beat no keys --------------------
before=$(sha256sum "$SEC" | cut -d' ' -f1)
for mode in fail wrong empty; do
LAB_BWS_MODE=$mode "$HOME/.local/bin/dotsecrets" >/dev/null 2>&1
[ "$(sha256sum "$SEC" | cut -d' ' -f1)" = "$before" ] \
|| fail "bws mode '$mode' modified secrets.zsh — it must be left alone on failure"
done
ls "$HOME/.config/zsh"/.secrets.zsh.* >/dev/null 2>&1 && fail "a temp file survived a failed refresh"
ok "a failed refresh leaves secrets.zsh byte-identical and no temp file behind"
# ---- GitHub reads stay anonymous https after the tier applies ---------------
# The tier ships a git@github.com: rewrite for PUSHES. If it ever regresses to
# a blanket insteadOf, every https clone on a keyless fresh box breaks -- the
# first casualty was nvim's plugin bootstrap (live, 2026-08-22).
# Plain `git config --get`, not --global: --global does not follow the
# [include] seam by default, which made this pair pass vacuously.
git config --get 'url.git@github.com:.insteadof' >/dev/null 2>&1 \
&& fail "GitHub https READS are rewritten to ssh — a keyless box cannot clone plugins"
git config --get 'url.git@github.com:.pushinsteadof' >/dev/null 2>&1 \
|| fail "the GitHub push-over-ssh rewrite is gone entirely"
ok "GitHub reads stay https; only pushes are rewritten to ssh"
echo "PRIVATE TIER PASS"