R-890: the test box (scratch 9202) may seed vaultwarden through its admin invite, inside the box
gates / gates (push) Successful in 7s
gates / gates (push) Successful in 7s
`09` §3 decision 149. box_admin_seed_allowed(): not the bench, FELHOM_BOX_ADMIN_SEED=1, demo-hp/9202 only, an app this run installed, the box on the drill catalog. The token is read inside the box and handed to curl on stdin; only HTTP codes come back. Tests: BoxAdminSeedGuard (red-proved). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -304,6 +304,75 @@ def bench_admin_seed_allowed(w):
|
||||
return True, ""
|
||||
|
||||
|
||||
# --- R-890 (`09` §3 decision 149): the TEST BOXES may seed vaultwarden through its admin route too ---------------------
|
||||
BOX_ADMIN_SEED_ENV = "FELHOM_BOX_ADMIN_SEED" # the run's explicit opt-in on a box walk
|
||||
# The test boxes the walk can reach, as (Proxmox host, guest). Scratch 9202 only: box_walk.py drives guests on demo-hp
|
||||
# alone (its HP), and 9201 there is demo-hp's household-shaped guest — a demo box's household apps are never seeded
|
||||
# through their admin (decision 149). The Tester 1 box is allowed by the ruling but the walk has no route to it; add
|
||||
# it here the day the walk can run there.
|
||||
BOX_ADMIN_SEED_GUESTS = {("demo-hp", "9202")}
|
||||
DRILL_CATALOG_MARK = "app-catalog-drill" # a test run points the box at the drill catalog (`09` §6.5)
|
||||
|
||||
|
||||
def box_admin_seed_allowed(w, app):
|
||||
"""(True, "") only on a TEST BOX walk; (False, why) everywhere else. ALL FIVE must hold:
|
||||
1. not the bench venue (the bench has its own guard, bench_admin_seed_allowed);
|
||||
2. the run opted in: FELHOM_BOX_ADMIN_SEED=1;
|
||||
3. the walk targets a listed test box (BOX_ADMIN_SEED_GUESTS) — never 9201, never a household box;
|
||||
4. THIS run installed the app (box_walk.deploy records it): an app the walk merely reused may be someone's;
|
||||
5. the box itself says it is on the drill catalog (its controller.yaml names it) — a household box never is.
|
||||
Pinned by scripts/test_upgrade_bench.py (BoxAdminSeedGuard: each condition alone refuses)."""
|
||||
if getattr(w, "VENUE", None) == "bench":
|
||||
return False, "the bench venue (its own guard decides there)"
|
||||
if os.environ.get(BOX_ADMIN_SEED_ENV) != "1":
|
||||
return False, "%s=1 is not set for this run" % BOX_ADMIN_SEED_ENV
|
||||
where = (getattr(w, "HP", None), str(getattr(w, "GUEST", None)))
|
||||
if where not in BOX_ADMIN_SEED_GUESTS:
|
||||
return False, "%s guest %s is not a test box" % where
|
||||
if app not in (getattr(w, "DEPLOYED_THIS_RUN", None) or set()):
|
||||
return False, "this run did not install %s (a reused app may be a household's)" % app
|
||||
conf = w.guest("grep -A3 '^git:' /var/lib/docker/volumes/felhom-controller-data/_data/controller.yaml")
|
||||
if DRILL_CATALOG_MARK not in (conf or ""):
|
||||
return False, "the box is not on the drill catalog — not a test run"
|
||||
return True, ""
|
||||
|
||||
|
||||
# Runs INSIDE the guest. The admin token is read from the container's own environment into a shell variable and handed
|
||||
# to curl on stdin (`printf` is a builtin: never on a command line); the admin session cookie lives in a 0600 file that
|
||||
# is shredded. Only HTTP codes come back. The cookie is sent by hand because vaultwarden marks it Secure (its DOMAIN is
|
||||
# https) and the call goes to the container's own address over plain http.
|
||||
BOX_ADMIN_INVITE_SH = r"""set -u
|
||||
c=vaultwarden
|
||||
ip=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' "$c" 2>/dev/null | awk '{print $1}')
|
||||
[ -n "$ip" ] || { echo "RESULT noip"; exit 0; }
|
||||
T=$(docker exec "$c" printenv ADMIN_TOKEN 2>/dev/null)
|
||||
[ -n "$T" ] || { echo "RESULT notoken"; exit 0; }
|
||||
H=$(mktemp); chmod 600 "$H"
|
||||
code=$(printf %s "$T" | curl -s -o /dev/null -D "$H" -w '%{http_code}' --max-time 30 --data-urlencode token@- "http://$ip:80/admin")
|
||||
T=
|
||||
ck=$(grep -i '^set-cookie: *VW_ADMIN=' "$H" | head -1 | sed -e 's/^[Ss]et-[Cc]ookie: *//' -e 's/;.*//' | tr -d '
|
||||
')
|
||||
if [ -z "$ck" ]; then shred -u "$H"; echo "RESULT signin=$code session=no"; exit 0; fi
|
||||
printf 'Cookie: %s
|
||||
' "$ck" > "$H"; ck=
|
||||
inv=$(curl -s -o /dev/null -w '%{http_code}' --max-time 30 -H @"$H" -H 'Content-Type: application/json' --data '{"email":"__EMAIL__"}' "http://$ip:80/admin/invite")
|
||||
shred -u "$H"
|
||||
echo "RESULT signin=$code session=yes invite=$inv"
|
||||
"""
|
||||
|
||||
|
||||
def box_admin_invite(w, email):
|
||||
"""Invite `email` through vaultwarden's admin page, inside the test box (see BOX_ADMIN_INVITE_SH). Returns
|
||||
(signin code, session yes/no, invite code) as the box printed them, or None when it printed no RESULT line."""
|
||||
if not re.fullmatch(r"[a-z0-9.-]+@gate\.invalid", email):
|
||||
raise ValueError("only a drill address is ever invited")
|
||||
out = w.guest(BOX_ADMIN_INVITE_SH.replace("__EMAIL__", email))
|
||||
m = re.search(r"^RESULT (.*)$", out or "", re.M)
|
||||
if not m:
|
||||
return None
|
||||
kv = dict(p.split("=", 1) for p in m.group(1).split() if "=" in p)
|
||||
return kv.get("signin", m.group(1)), kv.get("session", "no"), kv.get("invite", "")
|
||||
|
||||
|
||||
def _curl_with_header_file(w, sub, path, header, *extra, **kw):
|
||||
"""w.app_curl with ONE secret header read by curl from a 0600 temp file (`-H @file`), so the value is never
|
||||
@@ -377,9 +446,17 @@ class Vaultwarden:
|
||||
return {"email": email, "key": key}
|
||||
tried = f"POST /identity/accounts/register -> {code} (sign-up closed by design)"
|
||||
ok, why = bench_admin_seed_allowed(w)
|
||||
self.tried = tried + f"; the admin invite was NOT tried: {why}"
|
||||
say(f" vaultwarden: {self.tried}")
|
||||
return None
|
||||
if not ok:
|
||||
okb, whyb = box_admin_seed_allowed(w, "vaultwarden")
|
||||
if not okb:
|
||||
self.tried = tried + f"; the admin invite was NOT tried: bench — {why}; box — {whyb}"
|
||||
say(f" vaultwarden: {self.tried}")
|
||||
return None
|
||||
got = box_admin_invite(w, email)
|
||||
say(f" vaultwarden: test-box admin sign-in and invite (inside the box) -> {got}")
|
||||
if not got or got[1] != "yes" or got[2] != "200":
|
||||
self.tried = tried + f"; the test-box admin invite -> {got}"
|
||||
return None
|
||||
return self._invited(w, sub, email, key, tried, say)
|
||||
token = (getattr(w, "GENERATED", {}).get("vaultwarden") or {}).get("ADMIN_TOKEN") or ""
|
||||
if not token:
|
||||
@@ -403,6 +480,10 @@ class Vaultwarden:
|
||||
say(f" vaultwarden: bench admin invite http={code}")
|
||||
if code != "200":
|
||||
self.tried = tried + f"; POST /admin/invite -> {code}"
|
||||
return None
|
||||
return self._invited(w, sub, email, key, tried, say)
|
||||
|
||||
def _invited(self, w, sub, email, key, tried, say):
|
||||
"""The invited address registers through the household's own front door."""
|
||||
rc, code, out = self._register(w, sub, email, key)
|
||||
say(f" vaultwarden: invited registration http={code}")
|
||||
|
||||
Reference in New Issue
Block a user