fix(users): the root-door resolver matches whole fields, not substrings
Caught in review. root_door_of matched unanchored substrings, so any value that EXTENDS a real one resolved as that value: `root-door=closedish` read as `closed` and PASSED close-root's gate -- the one arm in this repo that authorizes an irreversible act -- and `class=humanoid` did the same through the compat arm. Both contradicted the function's own header, which promises a value outside the set resolves empty and fails closed. Only reachable by hand-editing a marker, so it was never a live incident. It gets fixed anyway because this is the single function every consumer trusts -- close-root's gate, apply's root-SSH note, and bootstrap-tenant's machine guard all ask it -- and a resolver that is nearly right about a root door is the wrong kind of nearly. The marker is one line of space-separated key=value fields (bootstrap writes it with a single printf), so padding both ends and matching on field boundaries is exact rather than heuristic. Whitespace is normalised first so a hand-edit using tabs still reads correctly -- anchoring must not trade one silent misread for another. BOTH vocabularies are anchored. Fixing only the current spelling would have left the hole open on every box bootstrapped before #77, which is precisely the population the compat arm exists to serve. Tests pin the resolver and the end-to-end refusal, since the resolver returning "" is only safe because consumers treat it as one. Reverting the anchoring turns the suite red (447/4); restoring it returns 451/0. The original compat proof still holds: removing the class= arm gives 441/10. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b1c1357f2b
commit
4bbf1babe0
3 changed files with 60 additions and 7 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -67,6 +67,21 @@ on the way to cutting its first release, and this file starts there.
|
|||
told to re-run bootstrap — never a door welded shut on a machine whose only
|
||||
entrance it was.
|
||||
|
||||
The resolver matches **whole fields, not substrings** — the marker is one
|
||||
line of space-separated `key=value` pairs, so it pads both ends and matches
|
||||
on field boundaries. Review caught the first cut doing unanchored matching,
|
||||
which resolved any value that *extended* a real one: `root-door=closedish`
|
||||
read as `closed` and passed close-root's gate — the single arm that
|
||||
authorizes an irreversible act — and `class=humanoid` did the same through
|
||||
the compat arm, both contradicting the resolver's own promise that a value
|
||||
outside the set resolves empty and fails closed. Only reachable by hand-
|
||||
editing a marker, so never a live incident, but this is the one function
|
||||
every consumer trusts and it owes them exactness rather than nearly. Both
|
||||
vocabularies are anchored: fixing only the current spelling would have left
|
||||
every pre-#77 box carrying the hole. Whitespace is normalised first, so a
|
||||
hand-edit using tabs reads the same rather than trading one silent misread
|
||||
for another.
|
||||
|
||||
**New markers are written in the new vocabulary only.** Writing both would
|
||||
keep an old rig reading a new marker, but it would entrench the retired
|
||||
spelling on every box rig ever converges and make the disagreement case
|
||||
|
|
|
|||
|
|
@ -158,14 +158,27 @@ read_role_marker() {
|
|||
# unchanged from before: a marker that names no door policy cannot authorize
|
||||
# shutting a door.
|
||||
root_door_of() {
|
||||
local marker="$1" new="" old=""
|
||||
case "$marker" in
|
||||
*root-door=closed*) new=closed ;;
|
||||
*root-door=open*) new=open ;;
|
||||
local marker="$1" new="" old="" padded
|
||||
# FIELD-ANCHORED, not substring. The marker is one line of space-separated
|
||||
# `key=value` fields (bootstrap writes it with a single printf), so padding
|
||||
# both ends and matching whole fields is exact. Unanchored patterns matched
|
||||
# any value that EXTENDS a real one: `root-door=closedish` resolved as
|
||||
# `closed` and passed close-root's gate — the one arm that authorizes an
|
||||
# irreversible act — and `class=humanoid` did the same through the compat
|
||||
# arm. That contradicted this function's own promise above, that a value
|
||||
# outside the set resolves empty and fails closed. Only reachable by hand
|
||||
# editing, but this is the function every consumer trusts, so it owes them
|
||||
# exactness rather than "close enough" (found in review on #77).
|
||||
# Whitespace is normalised first so a hand-edit using tabs or double spaces
|
||||
# is read the same way rather than silently failing to match.
|
||||
padded=" ${marker//[[:space:]]/ } "
|
||||
case "$padded" in
|
||||
*" root-door=closed "*) new=closed ;;
|
||||
*" root-door=open "*) new=open ;;
|
||||
esac
|
||||
case "$marker" in
|
||||
*class=human*) old=closed ;;
|
||||
*class=server*) old=open ;;
|
||||
case "$padded" in
|
||||
*" class=human "*) old=closed ;;
|
||||
*" class=server "*) old=open ;;
|
||||
esac
|
||||
if [ -n "$new" ] && [ -n "$old" ] && [ "$new" != "$old" ]; then
|
||||
printf 'conflict'; return 0
|
||||
|
|
|
|||
25
test/cli.sh
25
test/cli.sh
|
|
@ -1604,6 +1604,31 @@ check "root_door_of: a tenant marker names no door at all" 0 "[]" \
|
|||
door_of 'role=claude-box tenant=yes host=no'
|
||||
check "root_door_of: disagreement is a conflict, not a coin flip" 0 "[conflict]" \
|
||||
door_of 'role=custom root-door=open class=human host=no join=login'
|
||||
# FIELD-ANCHORED, not substring (found in review on #77). A value that EXTENDS
|
||||
# a real one must resolve EMPTY and fail closed, exactly as the function's
|
||||
# header promises — before anchoring, `closedish` read as `closed` and PERMITTED
|
||||
# close-root, the one arm that authorizes an irreversible act. Both vocabularies
|
||||
# are checked: the compat arm had the identical hole, and a fix that anchored
|
||||
# only the current spelling would leave every pre-#77 box exposed to it.
|
||||
check "root_door_of: a value EXTENDING the current spelling resolves empty" 0 "[]" \
|
||||
door_of 'role=x root-door=closedish host=no'
|
||||
check "root_door_of: a value extending the pre-#77 spelling resolves empty too" 0 "[]" \
|
||||
door_of 'role=x class=humanoid host=no'
|
||||
check "root_door_of: a value PREFIXED by junk does not match either" 0 "[]" \
|
||||
door_of 'role=x notroot-door=closed host=no'
|
||||
# ...and the gate itself must refuse on those, not merely resolve empty: the
|
||||
# resolver returning "" is only safe because every consumer treats it as a
|
||||
# refusal, so the end-to-end behaviour is what gets pinned.
|
||||
DOOR_FIX="$(mktemp -d)"
|
||||
printf 'role=x root-door=closedish host=no\n' > "$DOOR_FIX/bogus"
|
||||
# shellcheck disable=SC2016 # $1/$2 are the inner shell's positionals, not ours
|
||||
check "close-root: refuses a marker whose door value merely LOOKS closed" 1 "names no root-door policy" \
|
||||
bash -c '. "$1/commands/lib/users-config.sh"; assert_marker_closes_root "$2"' _ "$ROOT" "$DOOR_FIX/bogus"
|
||||
# Whitespace normalisation: a hand-edit using tabs is still a real marker and
|
||||
# must read the same, or anchoring would trade one silent misread for another.
|
||||
check "root_door_of: tab-separated fields read the same as space-separated" 0 "[closed]" \
|
||||
door_of "$(printf 'role=x\troot-door=closed\thost=no')"
|
||||
rm -rf "$DOOR_FIX"
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
check "users close-root: refuses non-root" 1 "must run as root" "$ROOT/commands/users-close-root.sh"
|
||||
else
|
||||
|
|
|
|||
Loading…
Reference in a new issue