diff --git a/clientdocs/sweep-receipt.txt b/clientdocs/sweep-receipt.txt index a45c7fd..65964ce 100644 --- a/clientdocs/sweep-receipt.txt +++ b/clientdocs/sweep-receipt.txt @@ -38,5 +38,5 @@ 927aa7261aa2d9fabe2cb7342499e6c0d0840a93ffbb323ee7003ac8eefe14a7 runbooks/tenant-onboarding-v2-DRAFT.md b8ad0768a1cad3e9a3e329f3cda39e6a151b80c0df5f0402efb65cb290b6b0b3 scripts/tenant-acceptance.sh ad00be489480069d52df1b978be0360d7fb04dbf954a8cf487162872f2da0593 scripts/tenant-assert.sh -45f338d88c816d01a4f0b33f2e3ede1f4b081e701efad1508e9d8e3d102e0485 scripts/tenant-offboard.sh -08f9540cc57d325ebe9d62a44eab3aaf27fb327e06008a1edb28505a14961af7 scripts/tenant-onboard.sh +014099679916bb8fe51d8c78f0377440bf5c1039d1590c181aba89d48345ce6d scripts/tenant-offboard.sh +d221f6ed0aabaf7faab12e16ccbc655a324d3ea873c8ed183cbcd9353e24973a scripts/tenant-onboard.sh diff --git a/docs/session-ledger.md b/docs/session-ledger.md index b231ac8..2a29776 100644 --- a/docs/session-ledger.md +++ b/docs/session-ledger.md @@ -31,8 +31,8 @@ - **OPEN security rows:** SEC-001 (rotate at v1 close, re-ruled 2026-07-06), SEC-003 (custodians + second-person unseal -- DEFERRED; keeps d011-06 MANUAL), SEC-004 (flip repo to private at v1 close -- deferral reaffirmed). -- **Next-free numbers:** D = 076, DOCFIX = 126, BUNDLEFIX = 012. - (Seeded from the 2026-07-08 scan, post-addendum-44.) +- **Next-free numbers:** D = 076, DOCFIX = 128, BUNDLEFIX = 012. + (Seeded from the 2026-07-08 scan, post-addendum-45.) --- @@ -326,9 +326,12 @@ glossary.md. Package re-instantiation for the changed set = next step. FLAGGED: package handover-pack.md has unfilled {{CUSTODIAN_1/2}} (operator value; pre-existing, devteam placeholder-custodian test). - 3. Gated script fixes: BOTH approved -- offboard Phase-E0 always-403 fix + - cred-.txt-file dashboard_url field. Build offline + harness; live verify - gated. IN PROGRESS. + 3. Gated script fixes: BOTH DONE (addendum 45). DOCFIX-126 offboard E0 + always-403 no longer FATALs before Phase F (harness case added, 21/21); + DOCFIX-127 onboard cred files now carry dashboard_url (env-overridable, + as-built https://10.12.4.58/horizon). LIVE VERIFY STILL GATED: next real + onboard/offboard confirms end-to-end (onboard cred-write stages 1/2 have + no unit coverage -- pre-existing; logged future harness item). 4. DOCFIX list CLOSED (operator); handover-pack.md fine as-is (already carries {{DASHBOARD_URL}}/{{AUTH_URL}}). - **Offboard E0 defect (for the fix):** admin-side diff --git a/docs/v1-redeploy-changelog.md b/docs/v1-redeploy-changelog.md index 5ce808d..099a3c5 100644 --- a/docs/v1-redeploy-changelog.md +++ b/docs/v1-redeploy-changelog.md @@ -3093,4 +3093,40 @@ - Numbers consumed: DOCFIX-125. REVERT: git checkout HEAD~ -- clientdocs/tenant-skill/references/day2-operations.md clientdocs/sweep-receipt.txt docs/v1-redeploy-changelog.md docs/session-ledger.md; then bash scripts/repo-lint.sh --record-clientdocs-sweep. Package is a jumphost artifact (re-run the instantiation helper to rebuild). -Next-free after this push (per scan, keep this line unwrapped): D-076, DOCFIX-126, BUNDLEFIX-012. +### 2026-07-08 (addendum 45, jumphost stream) -- gated script fixes: DOCFIX-126 offboard E0 always-403 + DOCFIX-127 onboard cred-file dashboard_url + +Both operator-approved (build offline + harness; live verification gated). + +- **DOCFIX-126 -- tenant-offboard.sh Phase E0 always-403:** admin CANNOT list + another user's application credentials (base policy + identity:list_application_credentials = admin_or_owner; measured DOCFIX-095). + The E0 inventory previously counted that 403 as an identity failure and + FATAL'd at exit 23 BEFORE Phase F (local cred-dir retirement) -- exactly what + the first live offboard (foil1, addendum 40) hit, stranding ~/tenant-foil1 + for a manual mv. Fix: a new branch recognizes the expected 403 (matches + identity:list_application_credentials / not-authorized / HTTP 403), reports it + as "cannot inventory as admin (not owner) -- creds cascade on user delete, OK", + and does NOT increment ID_FAIL. App creds still delete via keystone cascade on + user delete, so nothing is left behind. Harness: new case + e0-appcred-403-not-fatal (MOCK_SCEN=appcred403) asserts exit 0 + the OK line + + NO "FATAL: identity teardown"; tenant-offboard 20->21, gauntlet 38/38. +- **DOCFIX-127 -- tenant-onboard.sh cred-file dashboard_url:** the generated + ~/tenant-/*.txt handover cred files carried auth_url but not the web + dashboard URL (operator's "add the horizon url" item). Added a tagged, + env-overridable DASHBOARD_URL default (the dashboard is a web UI, NOT an API + service, so it is not in the keystone catalog and cannot be discovered -- + same literal-with-override pattern as KEYSTONE_VIP; as-built + https://10.12.4.58/horizon). A dashboard_url= field is now written to the + domain-admin cred (stage1) and every per-user cred (stage2). Consumers parse + these by name (awk /^field=/), so the added field is order-independent and + breaks nothing (tenant-offboard auth_url parse, stage7 canary password parse + both verified name-based). + - COVERAGE NOTE (honest): the onboard harness exercises stages 3/4/7 only and + mocks cred files as INPUTS; the cred-WRITING stages (1/2) have no unit + coverage (pre-existing boundary). So this field's write is confirmed at the + gated live onboard, as the cred-writing code always has been. Logged future + item: add stage1/2 cred-write harness coverage. +- Numbers consumed: DOCFIX-126, DOCFIX-127. +REVERT: git checkout HEAD~ -- scripts/tenant-offboard.sh scripts/tenant-onboard.sh tests/tenant-offboard/run-tests.sh clientdocs/sweep-receipt.txt docs/v1-redeploy-changelog.md docs/session-ledger.md; then bash scripts/repo-lint.sh --record-clientdocs-sweep. + +Next-free after this push (per scan, keep this line unwrapped): D-076, DOCFIX-128, BUNDLEFIX-012. diff --git a/scripts/tenant-offboard.sh b/scripts/tenant-offboard.sh index 75eb550..12900b5 100644 --- a/scripts/tenant-offboard.sh +++ b/scripts/tenant-offboard.sh @@ -334,6 +334,14 @@ if [ "$ACRC" != 0 ]; then if grep -q "No user with a name or ID" <<<"$ACOUT"; then echo " app creds ($U): owner already gone -- SKIP (idempotent re-run)" + elif grep -qiE "identity:list_application_credentials|not authorized to perform the requested action.*application|HTTP 403" <<<"$ACOUT"; then + # DOCFIX-126: admin CANNOT list another user's application credentials -- + # base policy identity:list_application_credentials is admin_or_owner, and + # admin is not the owner (measured DOCFIX-095). This 403 is EXPECTED, not a + # failure: the app creds cascade-delete with their owning user regardless. + # The inventory here is best-effort; do NOT count this as an identity + # failure (it previously FATAL'd at exit 23 before Phase F cred retirement). + echo " app creds ($U): cannot inventory as admin (not owner; base policy) -- creds cascade on user delete, OK" else printf '%s\n' "$ACOUT" | sed 's/^/ /' echo " ^ FAILED: application credential list --user $U" diff --git a/scripts/tenant-onboard.sh b/scripts/tenant-onboard.sh index 99a6b4b..10a7ad3 100644 --- a/scripts/tenant-onboard.sh +++ b/scripts/tenant-onboard.sh @@ -24,6 +24,10 @@ CA="${OS_CACERT:-$HOME/vault-init/vault-ca-root.pem}" OUT="$HOME/tenant-${CLIENT}" # 0600 credential handover dir AUTH_URL="https://${KEYSTONE_VIP}:5000/v3" +# Web dashboard URL for the handover cred files. The dashboard is a web UI, NOT +# an API service, so it is not in the keystone catalog and cannot be discovered; +# it is a tagged, env-overridable literal (as-built value; override per cloud). +DASHBOARD_URL="${DASHBOARD_URL:-https://10.12.4.58/horizon}" die(){ echo "FATAL: $*" >&2; exit 1; } [ -n "$CLIENT" ] || die "usage: tenant-onboard.sh [stage0|1|2|3|4|5|6|7|all] (7 = optional canary access-proof, explicit only)" [ -s "$CA" ] || die "OS_CACERT not found: $CA" @@ -63,7 +67,7 @@ local RA; RA=$(openstack role assignment list --user "$MUID" --names -f value -c Role &1 || true) grep -qw manager <<<"$RA" || die "manager grant failed" # capture-then-test: the raced form false-died on a SUCCESSFUL grant local MF="$OUT/${CLIENT}-domain-admin-cred.txt"; : > "$MF"; chmod 600 "$MF" - printf 'domain=%s\ndomain_id=%s\nusername=%s-domain-admin\nuser_id=%s\npassword=%s\nauth_url=%s\n' "$CLIENT" "$DOM" "$CLIENT" "$MUID" "$MPW" "$AUTH_URL" > "$MF"; chmod 600 "$MF" + printf 'domain=%s\ndomain_id=%s\nusername=%s-domain-admin\nuser_id=%s\npassword=%s\nauth_url=%s\ndashboard_url=%s\n' "$CLIENT" "$DOM" "$CLIENT" "$MUID" "$MPW" "$AUTH_URL" "$DASHBOARD_URL" > "$MF"; chmod 600 "$MF" echo " domain=$DOM manager=$MUID cred -> $MF" echo " (set project quota after stage2 creates ${CLIENT}-prod, or pre-create the project as operator)" } @@ -87,7 +91,7 @@ local UID2; UID2=$(openstack user show "${CLIENT}-$U" --domain "$DOM" -f value -c id &1); [[ "$UID2" =~ ^[0-9a-f]{32}$ ]] || { echo "user $U FAIL"; exit 1; } for R in member load-balancer_member; do openstack role add --project "$PID" --user "$UID2" "$R" &1 || true; done local F="$OUT/${CLIENT}-$U-cred.txt"; umask 077; : > "$F"; chmod 600 "$F" - printf 'username=%s-%s\nuser_id=%s\nuser_domain_id=%s\nproject_id=%s\nauth_url=%s\npassword=%s\n' "$CLIENT" "$U" "$UID2" "$DOM" "$PID" "$AUTH_URL" "$PW" > "$F"; chmod 600 "$F" + printf 'username=%s-%s\nuser_id=%s\nuser_domain_id=%s\nproject_id=%s\nauth_url=%s\ndashboard_url=%s\npassword=%s\n' "$CLIENT" "$U" "$UID2" "$DOM" "$PID" "$AUTH_URL" "$DASHBOARD_URL" "$PW" > "$F"; chmod 600 "$F" echo " ${CLIENT}-$U=$UID2 (member+load-balancer_member) cred -> $F" done # anti-escalation self-check (must be DENIED) diff --git a/tests/tenant-offboard/run-tests.sh b/tests/tenant-offboard/run-tests.sh index 455bfc1..b36f48b 100644 --- a/tests/tenant-offboard/run-tests.sh +++ b/tests/tenant-offboard/run-tests.sh @@ -86,8 +86,12 @@ "trust delete"*) if [ "$S" = trustdelfail ]; then echo "Unable to delete trust (HTTP 500)"; exit 1; fi mut "$*"; touch "$ST/tdel";; - "application credential list --user $U1"*) python3 -c "print('c'*32)";; - "application credential list --user"*) : ;; + "application credential list --user $U1"*) + [ "$S" = appcred403 ] && { echo "You are not authorized to perform the requested action: identity:list_application_credentials. (HTTP 403)"; exit 1; } + python3 -c "print('c'*32)";; + "application credential list --user"*) + [ "$S" = appcred403 ] && { echo "You are not authorized to perform the requested action: identity:list_application_credentials. (HTTP 403)"; exit 1; } + : ;; "loadbalancer list --project "*"-c provisioning_status") [ -f "$ST/lbdel" ] || echo "$LBU old-lb ERROR";; "loadbalancer list --project "*"-c id") if [ "$S" = lbstuck ]; then echo "$LBU"; else [ -f "$ST/lbdel" ] || echo "$LBU"; fi;; @@ -178,6 +182,14 @@ if [ "$RC" = 0 ] && grep -q "app-cred stage: no users remain in domain -- SKIP (idempotent re-run)" "$W/e0.out"; then echo "PASS: e0-skip-idempotent (exit 0, SKIP line)"; P=$((P+1)) else echo "FAIL: e0-skip-idempotent (exit $RC; want 0 + SKIP line)"; F=$((F+1)); fi +# DOCFIX-126 Phase E0 always-403: admin CANNOT list another user's app creds (base +# policy admin_or_owner). That 403 is EXPECTED (creds cascade on user delete) and must +# NOT count as an identity failure -- pre-fix it FATAL'd at exit 23 BEFORE Phase F +# (cred-dir retirement), which is exactly what happened on the first live foil1 offboard. +rm -rf "$W/state"; HOME="$W" MOCK_SCEN=appcred403 PATH="$W/bin:$PATH" python3 "$W/ptyrun.py" bash "$SCRIPT" acme --apply acme > "$W/ac403.out" 2>&1; RC=$? +if [ "$RC" = 0 ] && grep -q "cannot inventory as admin" "$W/ac403.out" && ! grep -q "FATAL: identity teardown" "$W/ac403.out"; then + echo "PASS: e0-appcred-403-not-fatal (exit 0, 403 expected, no FATAL, Phase F reached)"; P=$((P+1)) +else echo "FAIL: e0-appcred-403-not-fatal (exit $RC; want 0 + OK line + no FATAL)"; F=$((F+1)); fi # ---- stderr-separation + row-validation hardening ---- # stderr warnings on list commands must be SURFACED as NOTEs, never become # delete argv (pre-fix: merged 2>&1 word-split "WARNING: ..." into the loops).