forked from heavy-duty/rig
fix(bootstrap): refuse a users file that names no users
An empty, comments-only or whitespace-only users file is not a parse error, so it walked straight through the requirement #51 built: pre-flight passed, apply converged nothing, and the box came up root-only — the exact outcome --no-users exists to make explicit, reached by the flag added to guarantee the opposite. `--users ./empty` and `--no-users` produced the identical box and only one of them said so. Catch the zero-user parse in bootstrap's pre-flight, where the file is already parsed for validation and before apt, the hostname change, or a spent pre-auth key. The refusal names --no-users: the root-only box is reachable, it just has to be asked for out loud. Deliberately narrow. This is bootstrap's contract, not the parser's and not apply's: zero users is a legal file, and a standalone `rig users apply` against an emptied file is a real de-provisioning operation that must stay possible. Negative-grep tests pin both. Closes #57 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f579ce038f
commit
b982399d3c
4 changed files with 88 additions and 1 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -104,6 +104,16 @@ on the way to cutting its first release, and this file starts there.
|
||||||
the socket, and rig falls back to removing the group itself, as it also
|
the socket, and rig falls back to removing the group itself, as it also
|
||||||
does where box is not installed. Every fallback path carries the session
|
does where box is not installed. Every fallback path carries the session
|
||||||
warning, because the silence was the bug.
|
warning, because the silence was the bug.
|
||||||
|
- **`rig bootstrap` refuses a users file that names no users** (#57) — an
|
||||||
|
empty, comments-only or whitespace-only file is not a parse error, so it
|
||||||
|
passed pre-flight, converged nothing, and left the box root-only: the exact
|
||||||
|
outcome `--no-users` exists to make explicit, reached by the flag added to
|
||||||
|
guarantee the opposite. Bootstrap's pre-flight now catches the zero-user
|
||||||
|
parse — before `apt`, the hostname change, or a spent pre-auth key — and
|
||||||
|
refuses, naming `--no-users` as the way to ask for a root-only box out loud.
|
||||||
|
Scoped to `rig bootstrap`'s contract only: a standalone `rig users apply`
|
||||||
|
against an emptied file is a real de-provisioning operation and is
|
||||||
|
unchanged.
|
||||||
|
|
||||||
## 0.1.0 — 2026-07-19
|
## 0.1.0 — 2026-07-19
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,13 @@ error too — rig will not silently pick a winner.
|
||||||
A bad users file is caught **up front**, in the same breath as a bad
|
A bad users file is caught **up front**, in the same breath as a bad
|
||||||
`--class`: bootstrap pre-flights it with the same parser apply uses, before
|
`--class`: bootstrap pre-flights it with the same parser apply uses, before
|
||||||
`apt`, before the hostname change, and before a single-use pre-auth key is
|
`apt`, before the hostname change, and before a single-use pre-auth key is
|
||||||
spent. And on `host=yes` with `RIG_SKIP_BOX_INSTALL=1`, a box-role user with
|
spent. A file that names **no users** is refused there too — empty,
|
||||||
|
comments-only and whitespace-only files all parse fine, but bootstrapping
|
||||||
|
with one converges the same root-only box `--no-users` asks for, via the flag
|
||||||
|
that exists to guarantee the opposite. The refusal names `--no-users`, because
|
||||||
|
that box is reachable; it just has to be asked for out loud. This is
|
||||||
|
bootstrap's contract only: `rig users apply` against an emptied file is a
|
||||||
|
genuine de-provisioning operation and stays available. And on `host=yes` with `RIG_SKIP_BOX_INSTALL=1`, a box-role user with
|
||||||
no `incus` group refuses immediately rather than a hundred lines later —
|
no `incus` group refuses immediately rather than a hundred lines later —
|
||||||
that is the one case where the outcome is already certain, since the run has
|
that is the one case where the outcome is already certain, since the run has
|
||||||
been told it will not install box. rig still **never** installs Incus or runs
|
been told it will not install box. rig still **never** installs Incus or runs
|
||||||
|
|
|
||||||
|
|
@ -227,6 +227,37 @@ if [ -n "$USERS_FILE" ]; then
|
||||||
if ! USERS_PARSED="$(parse_users_file "$USERS_FILE")"; then
|
if ! USERS_PARSED="$(parse_users_file "$USERS_FILE")"; then
|
||||||
die "invalid users file: $USERS_FILE — every error is listed above; nothing was changed" 2
|
die "invalid users file: $USERS_FILE — every error is listed above; nothing was changed" 2
|
||||||
fi
|
fi
|
||||||
|
# A file that parses to ZERO users is not a parse error — empty, comments-only
|
||||||
|
# and whitespace-only files are all perfectly valid input, and the parser is
|
||||||
|
# right to accept them. But they walk straight through the requirement #51
|
||||||
|
# built: `--users ./empty-file` and `--no-users` converge the identical
|
||||||
|
# root-only box, and only one of them says so. The ambiguity the required flag
|
||||||
|
# exists to kill would survive in a narrower form — an operator who pointed at
|
||||||
|
# the wrong path and one who meant root-only would again produce indis-
|
||||||
|
# tinguishable boxes, which is precisely the state the flag was made to end.
|
||||||
|
#
|
||||||
|
# Refuse, and name --no-users: the outcome is reachable, it just has to be
|
||||||
|
# said out loud. That is the whole shape of #51's contract — both answers are
|
||||||
|
# available, neither is a side effect of what you did not type.
|
||||||
|
#
|
||||||
|
# The sharper reason this belongs at pre-flight rather than nowhere: against a
|
||||||
|
# box that ALREADY has operators, a truncated file does not converge nothing,
|
||||||
|
# it revokes every one of them. That is apply's correct and documented
|
||||||
|
# drop-semantics and it warns per user, so it is loud rather than silent — but
|
||||||
|
# a stray '>' is all it takes to produce that file, and every other failure
|
||||||
|
# mode on this command was deliberately made to fail before apt, the hostname
|
||||||
|
# change, or a spent pre-auth key. This one should not be the exception that
|
||||||
|
# fails after them.
|
||||||
|
#
|
||||||
|
# Deliberately scoped to BOOTSTRAP, not to the parser and not to apply. The
|
||||||
|
# lib stays a parser — "zero users is not allowed here" is bootstrap's policy,
|
||||||
|
# not a property of the file format — and a standalone `rig users apply`
|
||||||
|
# against an emptied file remains a real de-provisioning operation that must
|
||||||
|
# keep working. Bootstrap is where the claim "this box's people are these" is
|
||||||
|
# being made, so bootstrap is where an empty answer is a contradiction.
|
||||||
|
if [ -z "$USERS_PARSED" ]; then
|
||||||
|
die "users file names no users: $USERS_FILE parsed to zero operators (it is empty, or only comments and blank lines). Bootstrapping with it would converge a box only root can enter — the same outcome as --no-users, reached by the flag that exists to guarantee the opposite. Check the path, or pass --no-users to leave this box root-only deliberately" 2
|
||||||
|
fi
|
||||||
# Does anyone in the file carry role box? That single fact decides whether the
|
# Does anyone in the file carry role box? That single fact decides whether the
|
||||||
# incus precondition below applies at all — a users file naming only admins
|
# incus precondition below applies at all — a users file naming only admins
|
||||||
# converges perfectly well on a host that has never seen Incus, and refusing
|
# converges perfectly well on a host that has never seen Incus, and refusing
|
||||||
|
|
|
||||||
40
test/cli.sh
40
test/cli.sh
|
|
@ -213,6 +213,46 @@ check "bootstrap: the invalid-file refusal carries the parser's own line error"
|
||||||
# ('--no-users' then apply by hand) named.
|
# ('--no-users' then apply by hand) named.
|
||||||
check "bootstrap: --users - is refused, naming the pre-auth key prompt" 2 "pre-auth key prompt" \
|
check "bootstrap: --users - is refused, naming the pre-auth key prompt" 2 "pre-auth key prompt" \
|
||||||
"$ROOT/commands/bootstrap.sh" workload --users -
|
"$ROOT/commands/bootstrap.sh" workload --users -
|
||||||
|
# A file that parses to ZERO users (#57). Not a parse error — the parser is
|
||||||
|
# right to accept empty, comments-only and whitespace-only files — but it walks
|
||||||
|
# straight through #51's requirement: `--users ./empty` and `--no-users`
|
||||||
|
# converge the identical root-only box, and only one of them says so. All three
|
||||||
|
# shapes are tested separately because they take different paths through the
|
||||||
|
# parser's skip rules, and an implementation that checked, say, file size alone
|
||||||
|
# would pass one and fail the others.
|
||||||
|
: > "$BOOT_USERS/empty"
|
||||||
|
cat > "$BOOT_USERS/comments" <<'USERS'
|
||||||
|
# the operators for this box
|
||||||
|
#dan admin ssh-ed25519 AAAAC3fixture dan@laptop
|
||||||
|
USERS
|
||||||
|
printf ' \n\t\n\n' > "$BOOT_USERS/blank"
|
||||||
|
check "bootstrap: an empty users file exits 2" 2 "names no users" \
|
||||||
|
"$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/empty"
|
||||||
|
check "bootstrap: a comments-only users file exits 2" 2 "names no users" \
|
||||||
|
"$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/comments"
|
||||||
|
check "bootstrap: a whitespace-only users file exits 2" 2 "names no users" \
|
||||||
|
"$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/blank"
|
||||||
|
# The refusal must name --no-users, for the same reason the missing-flag one
|
||||||
|
# does: the root-only box IS reachable, it just has to be said out loud. An
|
||||||
|
# error that only reported "no users" would leave the operator who genuinely
|
||||||
|
# wants root-only with no named way to ask for it.
|
||||||
|
check "bootstrap: the zero-user refusal names --no-users as the way to say it" 2 "pass --no-users to leave this box root-only" \
|
||||||
|
"$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/empty"
|
||||||
|
# It must NOT over-refuse: a file that names even one operator passes pre-flight
|
||||||
|
# untouched. Reaching the root check (exit 1) is the proof — same idiom as the
|
||||||
|
# incus precondition's negative cases below.
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
check "bootstrap: a users file naming operators still passes pre-flight" 1 "must run as root" \
|
||||||
|
env TS_AUTHKEY=x "$ROOT/commands/bootstrap.sh" workload --users "$BOOT_USERS/ok"
|
||||||
|
fi
|
||||||
|
# Scope guard (#57): the refusal is BOOTSTRAP's contract, not the parser's and
|
||||||
|
# not apply's. A standalone `rig users apply` against an emptied file is a real
|
||||||
|
# de-provisioning operation and must stay possible — greps that find nothing
|
||||||
|
# (exit 1) are the pass, the repo's negative-law idiom.
|
||||||
|
check "users apply: an empty file is still a legal de-provisioning input" 1 "" \
|
||||||
|
grep -nE 'names no users' "$ROOT/commands/users-apply.sh"
|
||||||
|
check "users-config: zero users stays bootstrap policy, not a parser error" 1 "" \
|
||||||
|
grep -nE 'names no users' "$ROOT/commands/lib/users-config.sh"
|
||||||
# The host=yes box-role precondition, surfaced EARLY — but only where the
|
# The host=yes box-role precondition, surfaced EARLY — but only where the
|
||||||
# outcome is already proven: RIG_SKIP_BOX_INSTALL=1 means this run will not
|
# outcome is already proven: RIG_SKIP_BOX_INSTALL=1 means this run will not
|
||||||
# install box, so a missing incus group can no longer be rescued by the install
|
# install box, so a missing incus group can no longer be rescued by the install
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue