package console import ( "context" "net/http" "net/url" "os" "os/exec" "path/filepath" "runtime/debug" "strings" "sync" "testing" "time" "github.com/carlosframework/platform/internal/objstore" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/svcrole" ) // controlPlaneFixture is a console holding ONE control-plane app: `console` // in the ops account (bab), owned by paul, with the flag set. It returns the // fixture and a live CLI token for paul, so a test can go straight to the // door it is about. // // Deliberately minimal, because later tasks build on it: nothing is shipped, // no channel is pointed anywhere, and the ship rate limiter is left alone — // a test that ships more than twice calls liftShipBudget itself (apicli_test.go). func controlPlaneFixture(t *testing.T, label string) (*consoleFixture, string) { t.Helper() fx := newConsoleFixture(t) if _, err := fx.st.CreateAppIn(OpsAccountID, "console", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } if err := fx.st.SetAppControlPlane(OpsAccountID, "console", true); err != nil { t.Fatal(err) } return fx, cliToken(t, fx, "paul@keymail.dev", label) } // TestControlPlaneAppRefusesTrash: the console's release history is not // something to delete behind a confirmation dialog. Refused outright, not // "with fresh auth" — a gesture is not the guard here, the flag is. func TestControlPlaneAppRefusesTrash(t *testing.T) { fx, token := controlPlaneFixture(t, "code-cp-trash") status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/console/delete", map[string]string{}, token) if status != http.StatusForbidden { t.Fatalf("trash a control-plane app = %d %v, want 403", status, out) } msg := str(t, out, "error") if !strings.Contains(msg, "control plane") { t.Fatalf("refusal %q does not name the flag the operator has to clear", msg) } // And WHERE to clear it. A CLI caller has no page in front of them, so a // refusal that names the flag without naming the card strands exactly the // operator it just stopped. if !strings.Contains(msg, "/a/bab/apps/console/settings/advanced") { t.Fatalf("refusal %q does not point at the settings card that can clear the flag", msg) } present, err := fx.st.HasAppIn(OpsAccountID, "console") if err != nil || !present { t.Fatalf("app row after a refused trash: HasAppIn = %v %v, want true (untouched)", present, err) } } // TestControlPlaneAppAllowsAPersonToPromote is the other half, and the one // that matters most: gating promotion is what an earlier draft got wrong. // `stable`'s fresh-auth 403 has no API-side step-up and carlos deploy // log.Fatalfs on a failed promote AFTER the ship has landed, so a promote gate // would break the very command this work exists to create. func TestControlPlaneAppAllowsAPersonToPromote(t *testing.T) { fx, token := controlPlaneFixture(t, "code-cp-promote") // A real linux-arm64 binary stamped cp00001: the ship door checks both of // those on a control-plane app (checkControlPlaneArtifacts), so a // placeholder byte would never reach the promote this test is about. status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token, []shipPart{metaPart(t, "cp00001", "binary"), {name: "linux-arm64", body: buildTestELF(t, "linux", "arm64", "cp00001")}}) if status != http.StatusCreated { t.Fatalf("ship = %d %v", status, out) } status, out = postJSON(t, fx.ts.URL+"/api/cli/apps/bab/console/promote", map[string]string{"version": "cp00001", "channel": "canary/console"}, token) if status != http.StatusOK { t.Fatalf("a PERSON promoting a control-plane app = %d %v, want 200 — gating this breaks carlos deploy", status, out) } } // TestControlPlaneAppRefusesAServicePromote: a person with ops membership may // roll the console; a CI credential may not. This is what the flag buys on the // promote path instead of a fresh-auth gate. func TestControlPlaneAppRefusesAServicePromote(t *testing.T) { fx, token := controlPlaneFixture(t, "code-cp-svc") status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token, []shipPart{metaPart(t, "cp00002", "binary"), {name: "linux-arm64", body: buildTestELF(t, "linux", "arm64", "cp00002")}}) if status != http.StatusCreated { t.Fatalf("ship = %d %v", status, out) } svc := serviceToken(t, fx.st, OpsAccountID, "ci", "console", svcrole.Publish) status, out = postJSON(t, fx.ts.URL+"/api/cli/apps/bab/console/promote", map[string]string{"version": "cp00002", "channel": "canary/console"}, svc) if status != http.StatusForbidden { t.Fatalf("a SERVICE promoting a control-plane app = %d %v, want 403", status, out) } } // TestControlPlaneAppRefusesAServiceRollback: rollback moves the same pointer // promote does, so it wears the same service refusal — otherwise a CI // credential rolls the control plane by taking the other verb. func TestControlPlaneAppRefusesAServiceRollback(t *testing.T) { fx, token := controlPlaneFixture(t, "code-cp-svc-rollback") liftShipBudget(fx) for _, v := range []string{"cp00003", "cp00004"} { status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token, []shipPart{metaPart(t, v, "binary"), {name: "linux-arm64", body: buildTestELF(t, "linux", "arm64", v)}}) if status != http.StatusCreated { t.Fatalf("ship %s = %d %v", v, status, out) } status, out = postJSON(t, fx.ts.URL+"/api/cli/apps/bab/console/promote", map[string]string{"version": v, "channel": "canary/console"}, token) if status != http.StatusOK { t.Fatalf("promote %s = %d %v", v, status, out) } } svc := serviceToken(t, fx.st, OpsAccountID, "ci", "console", svcrole.Publish) status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/console/rollback", map[string]string{"channel": "canary/console"}, svc) if status != http.StatusForbidden { t.Fatalf("a SERVICE rolling back a control-plane app = %d %v, want 403", status, out) } // And the pointer did not move. if got := fx.channelVersion(t, "console", "canary/console").Version; got != "cp00004" { t.Fatalf("channel after a refused rollback = %q, want cp00004", got) } } // TestControlPlaneAppRefusesBrowserRename: the browser doors render pages, so // their refusal is a flash and a redirect rather than JSON — but it is the // same refusal, ahead of the confirm card and ahead of any passkey. func TestControlPlaneAppRefusesBrowserRename(t *testing.T) { fx, _ := controlPlaneFixture(t, "code-cp-rename") c := signInPaul(t, fx) csrf := sessionToken(t, fx, c) status, loc, _ := postForm(t, c, fx.u("/apps/console/rename"), url.Values{"csrf": {csrf}, "to": {"console2"}}) if status != http.StatusSeeOther { t.Fatalf("rename a control-plane app = %d, want a redirect carrying the refusal", status) } if !strings.Contains(loc, "/apps/console/settings") { t.Fatalf("rename refusal redirected to %q, want the settings tab", loc) } if renamed, err := fx.st.HasAppIn(OpsAccountID, "console"); err != nil || !renamed { t.Fatalf("app row after a refused rename: HasAppIn(console) = %v %v, want true", renamed, err) } if moved, err := fx.st.HasAppIn(OpsAccountID, "console2"); err != nil || moved { t.Fatalf("HasAppIn(console2) = %v %v, want false — the rename must not have landed", moved, err) } } // TestControlPlaneAppRefusesPurge is the refusal this whole flag exists for: // "Delete now" is the console's one irreversible button, and its target is // apps//console/** — the release history the fleet is running. The // refusal must land BEFORE requireFreshAuth, so the answer is the trash page // with a flash, never a trip into Keymail for a passkey that cannot help. func TestControlPlaneAppRefusesPurge(t *testing.T) { fx, _ := controlPlaneFixture(t, "code-cp-purge") c := signInPaul(t, fx) csrf := sessionToken(t, fx, c) status, loc, _ := postForm(t, c, fx.u("/trash/delete"), url.Values{"csrf": {csrf}, "app": {"console"}, "confirm": {"console"}}) if status != http.StatusSeeOther { t.Fatalf("purge a control-plane app = %d, want a redirect carrying the refusal", status) } if loc != fx.path("/trash") { t.Fatalf("purge refusal redirected to %q, want %q — a step-up redirect here means the flag was checked too late", loc, fx.path("/trash")) } if present, err := fx.st.HasAppIn(OpsAccountID, "console"); err != nil || !present { t.Fatalf("app row after a refused purge: HasAppIn = %v %v, want true (untouched)", present, err) } } // TestOrdinaryAppUnaffected: every refusal above is scoped to the flag. func TestOrdinaryAppUnaffected(t *testing.T) { fx := newConsoleFixture(t) if _, err := fx.st.CreateAppIn(OpsAccountID, "titogo", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } token := cliToken(t, fx, "paul@keymail.dev", "code-cp-ordinary") status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/titogo/delete", map[string]string{}, token) if status == http.StatusForbidden { t.Fatalf("trash an ORDINARY app = %d %v, want it to work as before", status, out) } } // TestControlPlaneRefusalIsNotAMembershipOracle: a non-member must meet the // byte-identical 404 in apiAccountApp before the flag is ever consulted, or // the refusal becomes a way to learn which accounts hold a control-plane app. func TestControlPlaneRefusalIsNotAMembershipOracle(t *testing.T) { fx, _ := controlPlaneFixture(t, "code-cp-oracle") stranger := cliToken(t, fx, "eve@keymail.dev", "code-cp-stranger") // postRaw, not postJSON: apiAccountApp's 404 is http.NotFound's plain // text, and the point of this test is that those bytes do not vary. status, body := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/console/delete", map[string]string{}, stranger) if status != http.StatusNotFound { t.Fatalf("a non-member trashing the control plane = %d %q, want the byte-identical 404", status, body) } otherStatus, otherBody := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/nosuchapp/delete", map[string]string{}, stranger) if otherStatus != status || otherBody != body { t.Fatalf("control-plane 404 (%d %q) differs from unknown-app 404 (%d %q) — the refusal is an oracle", status, body, otherStatus, otherBody) } } // TestControlPlaneAppRefusesAccountDeletion closes the door one level up. // Refusing trash, purge, rename and transfer on the app while the ACCOUNT that // holds it can be retired would be a guard with a documented bypass — worse // than no guard, because it reads as protection. // // Deliberately asserted with postForm, not postGated: the refusal must arrive // BEFORE requireFreshAuth, so the answer is the settings tab and never a // redirect into Keymail for a passkey that cannot help. (The pre-existing // "account still holds apps" refusal sits BELOW step-up, so a passkey demand // here would mean the control-plane guard never fired.) func TestControlPlaneAppRefusesAccountDeletion(t *testing.T) { fx, c, csrf, sqid := deletableFixture(t) acct, err := fx.st.AccountBySqid(sqid) if err != nil || acct == nil { t.Fatalf("AccountBySqid(%q) = %+v %v", sqid, acct, err) } // TWO flagged apps, because the refusal LOOPS: a guard that stopped at the // first row would name one app and leave the operator to discover the // other by trying again (coordinator ruling 7 — an account may hold more // than one). for _, app := range []string{"console", "keymail"} { if _, err := fx.st.CreateAppIn(acct.ID, app, "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } if err := fx.st.SetAppControlPlane(acct.ID, app, true); err != nil { t.Fatal(err) } } status, loc, body := postForm(t, c, fx.ts.URL+"/a/"+sqid+"/delete", url.Values{"csrf": {csrf}, "confirm": {"Testco"}}) if status != http.StatusSeeOther { t.Fatalf("delete an account holding the control plane = %d, want the flash redirect:\n%s", status, body) } if loc != "/a/"+sqid+"/settings" { t.Fatalf("refusal redirected to %q, want the settings tab — a Keymail redirect means the guard ran too late", loc) } if a, _ := fx.st.AccountBySqid(sqid); a == nil { t.Fatal("the account was deleted despite holding a control-plane app") } // The flash must name the app and the flag, or the operator cannot act. _, _, page := get(t, c, fx.ts.URL+"/a/"+sqid+"/settings") if !strings.Contains(page, "control plane") { t.Fatalf("refusal flash does not name the flag:\n%s", page) } for _, app := range []string{"console", "keymail"} { if !strings.Contains(page, app) { t.Fatalf("refusal flash does not name %q — the loop stopped early:\n%s", app, page) } } } // TestAccountWithNoControlPlaneAppStillDeletes is the negative half, and it // is the one that keeps the guard honest: a lookup that fell back to "refuse" // for every account would turn one refusal into "no account may ever be // deleted", and nothing else in the suite would notice. func TestAccountWithNoControlPlaneAppStillDeletes(t *testing.T) { fx, c, csrf, sqid := deletableFixture(t) status, _, body := postGated(t, fx, c, fx.ts.URL+"/a/"+sqid+"/delete", url.Values{"csrf": {csrf}, "confirm": {"Testco"}}) if status != http.StatusSeeOther { t.Fatalf("delete an ordinary empty account = %d, want it to work as before:\n%s", status, body) } if a, _ := fx.st.AccountBySqid(sqid); a != nil { t.Fatal("an account with no control-plane app survived its own deletion") } } // TestControlPlaneRetentionRefusesAServiceButNotAPerson: retention is how the // console's rollback DEPTH gets shortened, so a CI credential must not reach // it on a control-plane app. A person still may — how much history to keep is // an operator's decision, and PruneEligible already refuses to touch anything // promoted, tagged or recent. func TestControlPlaneRetentionRefusesAServiceButNotAPerson(t *testing.T) { fx, token := controlPlaneFixture(t, "code-cp-retention") svc := serviceToken(t, fx.st, OpsAccountID, "ci", "console", svcrole.Publish) body := map[string]any{"keep": release.KeepFloor} status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/console/retention", body, svc) if status != http.StatusForbidden { t.Fatalf("a SERVICE setting retention on the control plane = %d %v, want 403", status, out) } status, out = postJSON(t, fx.ts.URL+"/api/cli/apps/bab/console/retention", body, token) if status != http.StatusOK { t.Fatalf("a PERSON setting retention on the control plane = %d %v, want 200 — only the service is barred", status, out) } } // TestServiceMayPreviewRetentionButNotApplyIt: a preview writes nothing, and // refusing it would leave CI no way to see what a policy WOULD do before a // person applies it. The refusal is scoped to the branch that mutates. func TestServiceMayPreviewRetentionButNotApplyIt(t *testing.T) { fx, _ := controlPlaneFixture(t, "code-cp-dryrun") svc := serviceToken(t, fx.st, OpsAccountID, "ci", "console", svcrole.Publish) status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/console/retention", map[string]any{"keep": release.KeepFloor, "dry_run": true}, svc) if status != http.StatusOK { t.Fatalf("a SERVICE previewing retention on the control plane = %d %v, want 200", status, out) } if dry, _ := out["dry_run"].(bool); !dry { t.Fatalf("preview answer is not a dry run: %v", out) } // The same credential, the same door, the branch that writes: refused. status, out = postJSON(t, fx.ts.URL+"/api/cli/apps/bab/console/retention", map[string]any{"keep": release.KeepFloor}, svc) if status != http.StatusForbidden { t.Fatalf("a SERVICE applying retention = %d %v, want 403", status, out) } // And `off`, which is a write of its own shape. status, out = postJSON(t, fx.ts.URL+"/api/cli/apps/bab/console/retention", map[string]any{"off": true}, svc) if status != http.StatusForbidden { t.Fatalf("a SERVICE clearing retention = %d %v, want 403", status, out) } } // TestOwnerClearsTheControlPlaneFlag is the door the refusals point at. Every // refusal in this file says "clear the control-plane flag first"; without a // way to do that, the sentence instructs an operator to do something // impossible on the exact path where they are already blocked. // // Browser-only and passkey-gated by design (spec §6): setting the flag is // easy (the CLI, at claim time), clearing it is hard. There is deliberately // no API clear door — the console's fresh-auth 403 is a dead end for an API // caller, so a CLI clear would be either ungated or unusable. func TestOwnerClearsTheControlPlaneFlag(t *testing.T) { fx, token := controlPlaneFixture(t, "code-cp-clear") c := signInPaul(t, fx) csrf := sessionToken(t, fx, c) // The page has to OFFER it, or the door might as well not exist. if _, _, page := get(t, c, fx.u("/apps/console/settings/advanced")); !strings.Contains(page, "/apps/console/control-plane/clear") { t.Fatalf("the settings page offers no way to clear the flag:\n%s", page) } status, loc, body := postGated(t, fx, c, fx.u("/apps/console/control-plane/clear"), url.Values{"csrf": {csrf}}) if status != http.StatusSeeOther { t.Fatalf("clear the flag = %d -> %q:\n%s", status, loc, body) } cp, _, err := fx.st.AppFlags(OpsAccountID, "console") if err != nil || cp { t.Fatalf("AppFlags after a confirmed clear = %v %v, want false", cp, err) } // The card is state-driven: with the flag gone, so is the button. if _, _, page := get(t, c, fx.u("/apps/console/settings/advanced")); strings.Contains(page, "/apps/console/control-plane/clear") { t.Fatalf("the clear form is still offered on an app that is no longer flagged:\n%s", page) } // And the verb that was refused now works — the whole point of the door. status2, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/console/delete", map[string]string{}, token) if status2 != http.StatusOK { t.Fatalf("trash after clearing the flag = %d %v, want 200", status2, out) } } // TestClearingTheControlPlaneFlagDemandsAPasskey: without the step-up the // clear does not happen. postForm, not postGated, so the passkey round trip // is visible rather than walked. func TestClearingTheControlPlaneFlagDemandsAPasskey(t *testing.T) { fx, _ := controlPlaneFixture(t, "code-cp-clear-nopasskey") c := signInPaul(t, fx) csrf := sessionToken(t, fx, c) status, loc, body := postForm(t, c, fx.u("/apps/console/control-plane/clear"), url.Values{"csrf": {csrf}}) if status != http.StatusSeeOther || !strings.HasPrefix(loc, fx.fk.srv.URL) { t.Fatalf("clear without a passkey = %d -> %q, want the step-up redirect:\n%s", status, loc, body) } if cp, _, err := fx.st.AppFlags(OpsAccountID, "console"); err != nil || !cp { t.Fatalf("AppFlags after an unconfirmed clear = %v %v, want true (still flagged)", cp, err) } } // TestMemberCannotClearTheControlPlaneFlag proves the gate ORDER as much as // the permission: a member meets the byte-identical 404, and never a redirect // into Keymail — requireFreshAuth is LAST, so no step-up is ever offered to // someone who should have got a 404. func TestMemberCannotClearTheControlPlaneFlag(t *testing.T) { fx, _ := controlPlaneFixture(t, "code-cp-clear-member") if err := fx.st.AddMember(OpsAccountID, "kim@keymail.dev", RoleMember, time.Now()); err != nil { t.Fatal(err) } fx.setUser("kim@keymail.dev") c := newBrowser(t) signUp(t, fx, c, "code-cp-clear-kim") csrf := sessionToken(t, fx, c) status, loc, _ := postForm(t, c, fx.u("/apps/console/control-plane/clear"), url.Values{"csrf": {csrf}}) if status != http.StatusNotFound { t.Fatalf("a member clearing the flag = %d -> %q, want the byte-identical 404", status, loc) } if loc != "" { t.Fatalf("a member was redirected to %q — step-up must never be offered ahead of the 404", loc) } if cp, _, err := fx.st.AppFlags(OpsAccountID, "console"); err != nil || !cp { t.Fatalf("AppFlags after a member's attempt = %v %v, want true (still flagged)", cp, err) } // The 404 above has to be the OWNER GATE's, not a missing route: an // unregistered path answers a member exactly the same way, so without this // the test would still pass with the door deleted. fx.setUser("paul@keymail.dev") owner := newBrowser(t) signUp(t, fx, owner, "code-cp-clear-owner-probe") status, loc, _ = postForm(t, owner, fx.u("/apps/console/control-plane/clear"), url.Values{"csrf": {sessionToken(t, fx, owner)}}) if status != http.StatusSeeOther || !strings.HasPrefix(loc, fx.fk.srv.URL) { t.Fatalf("the OWNER on the same URL = %d -> %q, want the handler's step-up — the member's 404 may just be a missing route", status, loc) } } // TestOwnerSetsTheControlPlaneFlagWithoutAPasskey is the other half of the // card, and it is what stops the flag being a one-way trap: an owner who // clears it on a live console must be able to put it back without a database // write on the box. // // postForm, not postGated: setting the flag is the SAFE direction (spec §6), // so it deliberately demands no step-up — a redirect into Keymail here would // be the asymmetry inverted. func TestOwnerSetsTheControlPlaneFlagWithoutAPasskey(t *testing.T) { fx := newConsoleFixture(t) if _, err := fx.st.CreateAppIn(OpsAccountID, "titogo", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } token := cliToken(t, fx, "paul@keymail.dev", "code-cp-set") c := signInPaul(t, fx) csrf := sessionToken(t, fx, c) if _, _, page := get(t, c, fx.u("/apps/titogo/settings/advanced")); !strings.Contains(page, "/apps/titogo/control-plane/set") { t.Fatalf("the settings page offers no way to set the flag:\n%s", page) } status, loc, body := postForm(t, c, fx.u("/apps/titogo/control-plane/set"), url.Values{"csrf": {csrf}}) if status != http.StatusSeeOther { t.Fatalf("set the flag = %d -> %q:\n%s", status, loc, body) } if loc != fx.path("/apps/titogo/settings/advanced") { t.Fatalf("set redirected to %q, want the settings tab — a Keymail redirect means setting grew a passkey it must not have", loc) } if cp, _, err := fx.st.AppFlags(OpsAccountID, "titogo"); err != nil || !cp { t.Fatalf("AppFlags after a set = %v %v, want true", cp, err) } // The refusals came back with it — the set is not decoration. status2, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/titogo/delete", map[string]string{}, token) if status2 != http.StatusForbidden { t.Fatalf("trash after setting the flag = %d %v, want 403", status2, out) } } // TestMemberCannotSetTheControlPlaneFlag: declaring an app to be the platform // is an owner's act, and a member meets the same byte-identical 404 the clear // door gives them — with no step-up ever offered ahead of it. func TestMemberCannotSetTheControlPlaneFlag(t *testing.T) { fx := newConsoleFixture(t) if _, err := fx.st.CreateAppIn(OpsAccountID, "titogo", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } if err := fx.st.AddMember(OpsAccountID, "kim@keymail.dev", RoleMember, time.Now()); err != nil { t.Fatal(err) } fx.setUser("kim@keymail.dev") c := newBrowser(t) signUp(t, fx, c, "code-cp-set-kim") csrf := sessionToken(t, fx, c) status, loc, _ := postForm(t, c, fx.u("/apps/titogo/control-plane/set"), url.Values{"csrf": {csrf}}) if status != http.StatusNotFound || loc != "" { t.Fatalf("a member setting the flag = %d -> %q, want the byte-identical 404", status, loc) } if cp, _, err := fx.st.AppFlags(OpsAccountID, "titogo"); err != nil || cp { t.Fatalf("AppFlags after a member's attempt = %v %v, want false", cp, err) } // Same proof as the clear door's: an owner must REACH the handler on this // URL, or the member's 404 is indistinguishable from a route that is not // registered at all. fx.setUser("paul@keymail.dev") owner := newBrowser(t) signUp(t, fx, owner, "code-cp-set-owner-probe") status, loc, _ = postForm(t, owner, fx.u("/apps/titogo/control-plane/set"), url.Values{"csrf": {sessionToken(t, fx, owner)}}) if status != http.StatusSeeOther || loc != fx.path("/apps/titogo/settings/advanced") { t.Fatalf("the OWNER on the same URL = %d -> %q, want the handler's redirect — the member's 404 may just be a missing route", status, loc) } if cp, _, err := fx.st.AppFlags(OpsAccountID, "titogo"); err != nil || !cp { t.Fatalf("AppFlags after the owner's probe = %v %v, want true", cp, err) } } // TestControlPlaneAppRefusesStoreDestroy: the sixth destructive door. Its // ceremony — owner, CSRF, the typed BUCKET name, a passkey — is exactly the // ceremony spec §6 rules insufficient for the control plane, and what it // destroys (bucket, objects, IAM principal, credentials, record) has, by its // own doc, no trash and no undo. Refused before the typed word and before the // passkey, so the answer is the settings tab rather than a Keymail round trip. func TestControlPlaneAppRefusesStoreDestroy(t *testing.T) { fx, _ := controlPlaneFixture(t, "code-cp-store") putRecord(t, fx, "console", &objstore.Record{ Declared: &objstore.Declared{EnvPrefix: objstore.DefaultEnvPrefix, At: time.Now(), By: "paul@keymail.dev"}, }) c := signInPaul(t, fx) csrf := sessionToken(t, fx, c) status, loc, body := postForm(t, c, fx.u("/apps/console/store/destroy"), url.Values{"csrf": {csrf}, "confirm": {"console"}}) if status != http.StatusSeeOther { t.Fatalf("destroy a control-plane app's store = %d -> %q:\n%s", status, loc, body) } if loc != fx.path("/apps/console/settings/storage") { t.Fatalf("refusal redirected to %q, want the settings tab — a Keymail redirect means the guard ran too late", loc) } if rec := storedRecord(t, fx, "console"); rec == nil { t.Fatal("the store record was destroyed despite the control-plane flag") } // Cleared, the same door works — the refusal is the flag's, not the store's. if err := fx.st.SetAppControlPlane(OpsAccountID, "console", false); err != nil { t.Fatal(err) } status, loc, body = postGated(t, fx, c, fx.u("/apps/console/store/destroy"), url.Values{"csrf": {csrf}, "confirm": {"console"}}) if status != http.StatusSeeOther || loc != fx.path("/apps/console/settings/storage") { t.Fatalf("destroy after clearing the flag = %d -> %q:\n%s", status, loc, body) } if rec := storedRecord(t, fx, "console"); rec != nil { t.Fatalf("the declaration survived a destroy that should have worked: %+v", rec) } } // TestControlPlaneLookupFailureRefuses pins the named project constraint: "a // lookup failure must REFUSE, not fall through". Treating an unreadable row as // "not the control plane" would make a database blip the way past every guard // in this file — and `cp, _, _ := s.store.AppFlags(...)` reads like harmless // tidying, so this is the branch a later simplification inverts. // // The column is dropped rather than the database closed, deliberately: closing // it would break the token lookup and the app 404 first, and the request would // never reach the flag at all. Dropping ONE column leaves every other reader // working and fails exactly AppFlags. func TestControlPlaneLookupFailureRefuses(t *testing.T) { fx, token := controlPlaneFixture(t, "code-cp-blip") c := signInPaul(t, fx) csrf := sessionToken(t, fx, c) if _, err := fx.st.db.Exec(`ALTER TABLE apps DROP COLUMN control_plane`); err != nil { t.Fatalf("drop the flag column: %v", err) } // The JSON door: 503, not the 200 a fall-through would give. status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/console/delete", map[string]string{}, token) if status != http.StatusServiceUnavailable { t.Fatalf("trash with an unreadable flag = %d %v, want 503", status, out) } if msg := str(t, out, "error"); !strings.Contains(msg, "Could not check") { t.Fatalf("refusal %q does not say the check itself failed", msg) } if present, err := fx.st.HasAppIn(OpsAccountID, "console"); err != nil || !present { t.Fatalf("app row after a refused trash: HasAppIn = %v %v, want true (untouched)", present, err) } // The browser door: the flash, the trash page, and no passkey demanded — // postForm, so a step-up redirect would show rather than be walked. status, loc, body := postForm(t, c, fx.u("/trash/delete"), url.Values{"csrf": {csrf}, "app": {"console"}, "confirm": {"console"}}) if status != http.StatusSeeOther || loc != fx.path("/trash") { t.Fatalf("purge with an unreadable flag = %d -> %q, want the trash page's flash:\n%s", status, loc, body) } if _, _, page := get(t, c, fx.ts.URL+fx.path("/trash")); !strings.Contains(page, "Could not check") { t.Fatalf("purge refusal flash does not say the check failed:\n%s", page) } } // --- Ship-time refusals ----------------------------------------------------- // testELFCache holds one compiled binary per (goos, goarch, build): several // tests here ship the same artifact, and a link is cheap but not free. var testELFCache sync.Map // testELFSource is the smallest program that is still a real Go binary — it // has to be one, because what is under test is that debug/elf and // debug/buildinfo read back what a real build writes. const testELFSource = `package main var Build = "dev" func main() { println(Build) } ` // buildTestELF compiles a real binary for goos/goarch, stamped with the // release recipe's own ldflags, and returns its bytes. // // A REAL build rather than a handcrafted ELF header, deliberately: the claim // these checks rest on is that `-s -w` strips neither the machine field nor // the recorded -ldflags, and a fixture assembled by hand would only prove // that the checks can read the fixture. This is measured against the // toolchain that actually links the console. // // The -X names the PLATFORM's symbol path even though this throwaway module // has no such symbol. The linker ignores an unknown -X target silently and // records the flag either way, and the flag record is exactly what // checkControlPlaneArtifacts parses — spelling it any other way would test a // spelling no console build uses. func buildTestELF(t *testing.T, goos, goarch, build string) []byte { t.Helper() key := goos + "/" + goarch + "/" + build if b, ok := testELFCache.Load(key); ok { return b.([]byte) } if _, err := exec.LookPath("go"); err != nil { t.Skipf("no go toolchain available to build a %s test binary: %v", key, err) } dir := t.TempDir() for name, body := range map[string]string{ "go.mod": "module carlostestelf\n\ngo 1.24\n", "main.go": testELFSource, } { if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil { t.Fatal(err) } } out := filepath.Join(dir, "artifact") // An EMPTY build is the forgotten-ldflag case, so the flag is left off // altogether rather than passed with an empty value: `-X path=` would // still record a -X, and the binary under test has to be the one an // operator actually produces by forgetting. ldflags := "-s -w" if build != "" { ldflags += " -X github.com/carlosframework/platform/internal/version.Build=" + build } cmd := exec.Command("go", "build", "-ldflags", ldflags, "-o", out, ".") cmd.Dir = dir // GOWORK=off and CGO_ENABLED=0 so the build does not depend on this // checkout's module graph or on a C toolchain for the host arch. cmd.Env = append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch, "CGO_ENABLED=0", "GOWORK=off") if msg, err := cmd.CombinedOutput(); err != nil { t.Fatalf("build a %s test binary: %v\n%s", key, err, msg) } b, err := os.ReadFile(out) if err != nil { t.Fatal(err) } testELFCache.Store(key, b) return b } // TestControlPlaneShipRefusesTheWrongArchitecture: failure mode #1, an amd64 // binary shipped to Graviton boxes. -target linux-arm64 is carlos deploy's // default, which protects the careful caller; this closes the path a hand-run // ship, a CI job or a future script takes into the same channel. func TestControlPlaneShipRefusesTheWrongArchitecture(t *testing.T) { fx, token := controlPlaneFixture(t, "code-cp-arch") liftShipBudget(fx) amd64ELF := buildTestELF(t, "linux", "amd64", "cp00003") status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token, []shipPart{metaPart(t, "cp00003", "binary"), {name: "linux-arm64", body: amd64ELF}}) if status != http.StatusBadRequest { t.Fatalf("amd64 ELF as linux-arm64 = %d %v, want 400", status, out) } msg := str(t, out, "error") if !strings.Contains(msg, "linux-arm64") || !strings.Contains(msg, "amd64") { t.Fatalf("refusal %q must name both the declared target and what was found", msg) } // Refused BEFORE anything was written: a refusal that leaves the release // stored is not a refusal. if _, err := release.GetManifest(context.Background(), fx.rel, release.OpsAccount, "console", "cp00003"); err == nil { t.Fatal("the release landed despite the refusal") } } // TestControlPlaneShipRefusesAMismatchedEmbeddedBuild: closes the ldflags trap // at the door. Build without -X …version.Build and the roll succeeds while the // probe reports "dev" forever — deploy reports failure on a healthy deploy, // the exact inverse of the lie the probe exists to kill. func TestControlPlaneShipRefusesAMismatchedEmbeddedBuild(t *testing.T) { fx, token := controlPlaneFixture(t, "code-cp-embed") liftShipBudget(fx) wrong := buildTestELF(t, "linux", "arm64", "notthesha") status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token, []shipPart{metaPart(t, "cp00004", "binary"), {name: "linux-arm64", body: wrong}}) if status != http.StatusBadRequest { t.Fatalf("embedded build != -version = %d %v, want 400", status, out) } if msg := str(t, out, "error"); !strings.Contains(msg, "notthesha") || !strings.Contains(msg, "cp00004") { t.Fatalf("refusal %q must name both builds", msg) } // "Refused before any byte is written" is the load-bearing claim of where // this check sits, so every refusal pins it, not just the first. if _, err := release.GetManifest(context.Background(), fx.rel, release.OpsAccount, "console", "cp00004"); err == nil { t.Fatal("the release landed despite the refusal") } } // TestControlPlaneShipRefusesAMissingBuildStamp is the COMMON half of the // same trap — the ldflag simply forgotten — and it earns its own sentence: // "built with version.Build=\"\"" is true and tells the operator nothing, // where naming the flag is the whole fix. func TestControlPlaneShipRefusesAMissingBuildStamp(t *testing.T) { fx, token := controlPlaneFixture(t, "code-cp-nostamp") liftShipBudget(fx) unstamped := buildTestELF(t, "linux", "arm64", "") status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token, []shipPart{metaPart(t, "cp00006", "binary"), {name: "linux-arm64", body: unstamped}}) if status != http.StatusBadRequest { t.Fatalf("unstamped binary = %d %v, want 400", status, out) } msg := str(t, out, "error") if !strings.Contains(msg, "internal/version.Build=") { t.Fatalf("refusal %q does not name the link flag that is missing", msg) } if !strings.Contains(msg, "cp00006") { t.Fatalf("refusal %q does not say what to stamp the build as", msg) } if _, err := release.GetManifest(context.Background(), fx.rel, release.OpsAccount, "console", "cp00006"); err == nil { t.Fatal("the release landed despite the refusal") } } // TestControlPlaneShipAcceptsAMatchingArtifact. func TestControlPlaneShipAcceptsAMatchingArtifact(t *testing.T) { fx, token := controlPlaneFixture(t, "code-cp-ok") liftShipBudget(fx) good := buildTestELF(t, "linux", "arm64", "cp00005") status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token, []shipPart{metaPart(t, "cp00005", "binary"), {name: "linux-arm64", body: good}}) if status != http.StatusCreated { t.Fatalf("matching artifact = %d %v, want 201", status, out) } } // TestControlPlaneStaticShipIsNotELFChecked: a static release ships a // tarball, so both checks are exempt by kind — reading it as an ELF would // refuse every static ship the control plane could ever make. func TestControlPlaneStaticShipIsNotELFChecked(t *testing.T) { fx, token := controlPlaneFixture(t, "code-cp-static") liftShipBudget(fx) status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token, []shipPart{metaPart(t, "cp00007", "static"), {name: "site.tar.gz", body: []byte("not an ELF, and never was")}}) if status != http.StatusCreated { t.Fatalf("static ship of a control-plane app = %d %v, want 201", status, out) } } // TestControlPlaneShipIgnoresAnUnknownTarget: the guard catches a wrong build // of a target we KNOW, and does not bound the target vocabulary — targetRe // already does that. A part named for a machine this map has never heard of // passes, rather than the platform refusing a target it has not been taught. func TestControlPlaneShipIgnoresAnUnknownTarget(t *testing.T) { fx, token := controlPlaneFixture(t, "code-cp-unknown-target") liftShipBudget(fx) status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token, []shipPart{metaPart(t, "cp00008", "binary"), {name: "plan9-riscv64", body: []byte("whatever that is")}}) if status != http.StatusCreated { t.Fatalf("unknown target = %d %v, want 201", status, out) } } // TestOrdinaryAppShipIsNotArchChecked: a tenant may legitimately ship an // artifact we would guess wrong about; refusing on our inference is not our // call. Only the app that IS the control plane, on boxes we own and know are // arm64, gets the check. func TestOrdinaryAppShipIsNotArchChecked(t *testing.T) { fx := newConsoleFixture(t) if _, err := fx.st.CreateAppIn(OpsAccountID, "titogo", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } token := cliToken(t, fx, "paul@keymail.dev", "code-cp-tenant") liftShipBudget(fx) status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/titogo/releases", token, []shipPart{metaPart(t, "t0000001", "binary"), {name: "linux-arm64", body: []byte("not an ELF at all")}}) if status != http.StatusCreated { t.Fatalf("ordinary app shipping a non-ELF = %d %v, want 201 (unaffected)", status, out) } } // TestEmbeddedBuildReadsOnlyTheBuildSymbol is the parser's own table. It // exists because the field it reads is a STRING the linker recorded, not a // structured value: everything about "which build is this" rests on cutting // that string in exactly the right place. // // The decoy row is the regression guard. An earlier version searched each // raw field for "internal/version.Build=" and returned whatever followed, so // a binary with NO build stamp could report another flag's value as its // build — a false MATCH, which is the one failure direction these checks // must never have. func TestEmbeddedBuildReadsOnlyTheBuildSymbol(t *testing.T) { const sym = "github.com/carlosframework/platform/internal/version.Build" cases := []struct { name string ldflags string want string }{ {"the release recipe", "-s -w -X " + sym + "=abc1234", "abc1234"}, {"one-argument spelling", "-X=" + sym + "=abc1234", "abc1234"}, {"no -X at all (the forgotten ldflag)", "-s -w", ""}, {"a vendored or renamed prefix still counts", "-X example.com/fork/internal/version.Build=abc1234", "abc1234"}, {"a decoy inside another flag's value", "-s -w -X main.Notes=internal/version.Build=decoy", ""}, {"a neighbouring symbol", "-X " + sym + "Date=2026-08-25", ""}, {"a foreign symbol that merely ends in Build", "-X example.com/app/internal/versionx.Build=nope", ""}, {"stamp among other stamps", "-X main.who=paul -X " + sym + "=abc1234 -X main.when=now", "abc1234"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { bi := &debug.BuildInfo{Settings: []debug.BuildSetting{ {Key: "-compiler", Value: "gc"}, {Key: "-ldflags", Value: c.ldflags}, }} if got := embeddedBuild(bi); got != c.want { t.Fatalf("embeddedBuild(%q) = %q, want %q", c.ldflags, got, c.want) } }) } // No -ldflags setting at all — a `go build` with no flags, which is the // shape of every unstamped local build. if got := embeddedBuild(&debug.BuildInfo{Settings: []debug.BuildSetting{{Key: "-compiler", Value: "gc"}}}); got != "" { t.Fatalf("embeddedBuild with no -ldflags setting = %q, want \"\"", got) } } // TestControlPlaneShipRefusalNamesTheMintTrap: a version MINTED at the door // cannot be in a binary the operator linked before the request, so every // minted ship of a flagged app is refused — and the refusal has to say why, // or it sends them in a circle: rebuild with the stamp it names, and the next // mint moves the target again. func TestControlPlaneShipRefusalNamesTheMintTrap(t *testing.T) { fx, token := controlPlaneFixture(t, "code-cp-mint") liftShipBudget(fx) ctx := context.Background() if err := release.SetTarget(ctx, fx.rel, release.OpsAccount, "console", "0.5.0", "t", time.Now().UTC()); err != nil { t.Fatalf("set target: %v", err) } status, out := postShip(t, fx.ts.URL+"/api/cli/apps/bab/console/releases", token, []shipPart{metaPart(t, "", "binary"), {name: "linux-arm64", body: buildTestELF(t, "linux", "arm64", "cp00009")}}) if status != http.StatusBadRequest { t.Fatalf("minted ship of a control-plane app = %d %v, want 400", status, out) } msg := str(t, out, "error") if !strings.Contains(msg, "--version") || !strings.Contains(msg, "minted") { t.Fatalf("refusal %q must say the version was minted here and that --version is the way out", msg) } if _, err := release.GetManifest(ctx, fx.rel, release.OpsAccount, "console", "0.5.0-1"); err == nil { t.Fatal("the minted release landed despite the refusal") } // An ORDINARY app minting is untouched — the trap is the flag's, not the // mint's, and nothing about minting became worse for a tenant. if _, err := fx.st.CreateAppIn(OpsAccountID, "titogo", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } if err := release.SetTarget(ctx, fx.rel, release.OpsAccount, "titogo", "0.5.0", "t", time.Now().UTC()); err != nil { t.Fatalf("set target: %v", err) } status, out = postShip(t, fx.ts.URL+"/api/cli/apps/bab/titogo/releases", token, []shipPart{metaPart(t, "", "binary"), {name: "linux-arm64", body: []byte("not an ELF at all")}}) if status != http.StatusCreated { t.Fatalf("minted ship of an ordinary app = %d %v, want 201", status, out) } } // --- The instance doors ------------------------------------------------------ // // The eighth door, found mid-rollout. `carlos deploy` resolves hosts through // instance records, so the console needs one — but a record is an AUTHORITY // TRANSFER (AGENTS.md): once it exists and the route is Provisioned, the // provisioning reconciler owns the row, and its removal phase retires the // route the moment the record disappears. So a single DELETE on this family // takes the control plane off the air, and PATCH repoints it. // // The split is the one the flag already draws everywhere else: destruction is // refused outright; the WRITES a person legitimately performs stay open to a // person and closed to a CI credential. // cpInstanceFixture is controlPlaneFixture with the instance feature enabled // on the flagged `console` app and on an ordinary `widget` beside it, so // every test below can assert the refusal AND that a tenant's identical call // is untouched by it. // // Custom domains deliberately, not the platform apex: the sibling-apex rail // in handleAPIInstanceCreate only fires inside ., and these // tests are about the flag, not about that rail. func cpInstanceFixture(t *testing.T, label string) (*consoleFixture, string) { t.Helper() fx, token := controlPlaneFixture(t, label) enableInstances(t, fx, "console", provision.Manifest{Schema: 1, App: "console", Domain: "cp.tito.io", Pool: "tito", Channel: "stable", Regions: []string{"ie"}}) if _, err := fx.st.CreateAppIn(OpsAccountID, "widget", "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } enableInstances(t, fx, "widget", provision.Manifest{Schema: 1, App: "widget", Domain: "widget.tito.io", Pool: "tito", Channel: "stable", Regions: []string{"ie"}}) return fx, token } // TestControlPlaneAppRefusesInstanceDelete is the whole reason this slice // exists. Deleting the console's instance record is not an edit to a bucket // object — it is the instruction the reconciler's removal phase acts on, and // the act it performs is retiring the control plane's route. Refused // OUTRIGHT, for a person as much as for a service: the same class as trash, // purge and store-destroy, and the same way out (clear the flag first). func TestControlPlaneAppRefusesInstanceDelete(t *testing.T) { fx, token := cpInstanceFixture(t, "code-cp-inst-del") u := instancesURL(fx, "bab", "console") // The record exists first — created by the person the next test proves // may create it — so the refusal below is refusing a delete that would // otherwise have succeeded. if status, _, out := rawReq(t, http.MethodPost, u, token, `{"host":"box1.cp.tito.io","region":"ie"}`); status != http.StatusCreated { t.Fatalf("create = %d %s", status, out) } status, _, out := rawReq(t, http.MethodDelete, u+"/box1.cp.tito.io", token, "") if status != http.StatusForbidden { t.Fatalf("a PERSON deleting the control plane's instance record = %d %s, want 403", status, out) } if !strings.Contains(out, "control plane") { t.Fatalf("refusal %q does not name the flag the operator has to clear", out) } if !strings.Contains(out, "/a/bab/apps/console/settings/advanced") { t.Fatalf("refusal %q does not point at the settings card that can clear the flag", out) } // The record survived. This is the assertion the door exists for: the // reconciler never sees the record go, so the route is never retired. if _, err := provision.GetRecord(context.Background(), fx.rel, "bab", "console", "box1.cp.tito.io"); err != nil { t.Fatalf("the control plane's instance record did not survive a refused delete: %v", err) } // A service credential holding the role this door grants meets the same // wall — it is refused for what the app IS, not for what the caller is. svc := serviceToken(t, fx.st, OpsAccountID, "ci", "console", svcrole.Instance) if status, _, out := rawReq(t, http.MethodDelete, u+"/box1.cp.tito.io", svc, ""); status != http.StatusForbidden { t.Fatalf("a SERVICE deleting the control plane's instance record = %d %s, want 403", status, out) } // A traversal host on the flagged app is refused by the flag, not by the // shape check: nothing below the guard runs for a control-plane app. if status, _, out := rawReq(t, http.MethodDelete, u+"/..%2f..%2finstances.json", token, ""); status != http.StatusForbidden { t.Fatalf("traversal delete on a flagged app = %d %s, want the flag's 403", status, out) } // And an ORDINARY app deletes exactly as it always did. wu := instancesURL(fx, "bab", "widget") if status, _, out := rawReq(t, http.MethodPost, wu, token, `{"host":"box1.widget.tito.io","region":"ie"}`); status != http.StatusCreated { t.Fatalf("ordinary create = %d %s", status, out) } if status, _, out := rawReq(t, http.MethodDelete, wu+"/box1.widget.tito.io", token, ""); status != http.StatusOK { t.Fatalf("ordinary delete = %d %s, want 200 — the flag is not a fleet-wide freeze", status, out) } } // TestControlPlaneAppAllowsAPersonToCreateAndPatchItsInstance is the half // that must keep working, and the runbook step this whole slice unblocks: // the console has no instance record because it predates provisioning, and // `carlos deploy -app console` cannot resolve a host until a PERSON writes // one. A create gate here would close the door the flag exists to keep open. func TestControlPlaneAppAllowsAPersonToCreateAndPatchItsInstance(t *testing.T) { fx, token := cpInstanceFixture(t, "code-cp-inst-person") letStateUpstreams(t, fx, "paul@keymail.dev") u := instancesURL(fx, "bab", "console") if status, _, out := rawReq(t, http.MethodPost, u, token, `{"host":"box1.cp.tito.io","region":"ie","kind":"service","addrs":["127.0.0.1:9001"]}`); status != http.StatusCreated { t.Fatalf("a PERSON creating the control plane's instance record = %d %s, want 201 — this is the runbook step", status, out) } if _, err := provision.GetRecord(context.Background(), fx.rel, "bab", "console", "box1.cp.tito.io"); err != nil { t.Fatalf("the record a person created is not there: %v", err) } if status, _, out := rawReq(t, http.MethodPatch, updateURL(fx, "bab", "console", "box1.cp.tito.io"), token, `{"addrs":["127.0.0.1:9002"]}`); status != http.StatusOK { t.Fatalf("a PERSON patching the control plane's instance record = %d %s, want 200", status, out) } rec, err := provision.GetRecord(context.Background(), fx.rel, "bab", "console", "box1.cp.tito.io") if err != nil { t.Fatal(err) } if len(rec.Addrs) != 1 || rec.Addrs[0] != "127.0.0.1:9002" { t.Fatalf("addrs after a person's patch = %v, want the requested one", rec.Addrs) } } // TestControlPlaneAppRefusesAServiceInstanceCreate: the record a create // writes is the thing the reconciler then owns, so minting one for the // control plane is acquiring authority over the console's route. A person // may; a CI credential may not. func TestControlPlaneAppRefusesAServiceInstanceCreate(t *testing.T) { fx, _ := cpInstanceFixture(t, "code-cp-inst-svc-create") svc := serviceToken(t, fx.st, OpsAccountID, "ci", "console", svcrole.Instance) status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "console"), svc, `{"host":"box2.cp.tito.io","region":"ie"}`) if status != http.StatusForbidden { t.Fatalf("a SERVICE creating the control plane's instance record = %d %s, want 403", status, out) } if !strings.Contains(out, "a person must") { t.Fatalf("refusal %q does not say a person is the principal this needs", out) } if _, err := provision.GetRecord(context.Background(), fx.rel, "bab", "console", "box2.cp.tito.io"); err == nil { t.Fatal("a record exists after only a refused service create") } // The same credential shape, bound to an ordinary app, creates freely: // nothing about service provisioning became worse for a tenant. ws := serviceToken(t, fx.st, OpsAccountID, "ci-widget", "widget", svcrole.Instance) if status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "widget"), ws, `{"host":"box2.widget.tito.io","region":"ie"}`); status != http.StatusCreated { t.Fatalf("a SERVICE creating an ORDINARY app's instance = %d %s, want 201", status, out) } } // TestControlPlaneAppRefusesAServiceInstancePatch: the provisioning // reconciler converges a route's Channel and Environment FROM this record on // every pass, so a PATCH repoints the control plane. Same split as create. func TestControlPlaneAppRefusesAServiceInstancePatch(t *testing.T) { fx, token := cpInstanceFixture(t, "code-cp-inst-svc-patch") letStateUpstreams(t, fx, "paul@keymail.dev") u := instancesURL(fx, "bab", "console") if status, _, out := rawReq(t, http.MethodPost, u, token, `{"host":"box3.cp.tito.io","region":"ie","kind":"service","addrs":["127.0.0.1:9001"]}`); status != http.StatusCreated { t.Fatalf("create = %d %s", status, out) } svc := serviceToken(t, fx.st, OpsAccountID, "ci", "console", svcrole.Instance) status, _, out := rawReq(t, http.MethodPatch, updateURL(fx, "bab", "console", "box3.cp.tito.io"), svc, `{"addrs":["10.0.0.1:1"]}`) if status != http.StatusForbidden { t.Fatalf("a SERVICE patching the control plane's instance record = %d %s, want 403", status, out) } if !strings.Contains(out, "a person must") { t.Fatalf("refusal %q does not say a person is the principal this needs", out) } rec, err := provision.GetRecord(context.Background(), fx.rel, "bab", "console", "box3.cp.tito.io") if err != nil { t.Fatal(err) } if len(rec.Addrs) != 1 || rec.Addrs[0] != "127.0.0.1:9001" { t.Fatalf("addrs after a refused service patch = %v, want the created pair untouched", rec.Addrs) } // An ordinary app's record is still a service's to patch. An operator // states its upstream (only one may: refuseMemberUpstreams); the service // then restates it, which is not a new one. ws := serviceToken(t, fx.st, OpsAccountID, "ci-widget", "widget", svcrole.Instance) if status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "widget"), token, `{"host":"box3.widget.tito.io","region":"ie","kind":"service","addrs":["127.0.0.1:9001"]}`); status != http.StatusCreated { t.Fatalf("ordinary create = %d %s", status, out) } if status, _, out := rawReq(t, http.MethodPatch, updateURL(fx, "bab", "widget", "box3.widget.tito.io"), ws, `{"addrs":["127.0.0.1:9001"]}`); status != http.StatusOK { t.Fatalf("a SERVICE patching an ORDINARY app's instance = %d %s, want 200", status, out) } } // TestControlPlaneInstanceRefusalIsNotAnOracle: the three guards sit AFTER // the family's membership gate, so a stranger meets the byte-identical 404 // and never the refusal. A control-plane app must not be discoverable by // asking a door which sentence it answers with. func TestControlPlaneInstanceRefusalIsNotAnOracle(t *testing.T) { fx, _ := cpInstanceFixture(t, "code-cp-inst-oracle") eve := cliToken(t, fx, "eve@keymail.dev", "code-cp-inst-eve") want := canonicalNotFound(t) u := instancesURL(fx, "bab", "console") for _, c := range []struct{ what, method, url, body string }{ {"non-member create on a flagged app", http.MethodPost, u, `{"host":"box9.cp.tito.io","region":"ie"}`}, {"non-member patch on a flagged app", http.MethodPatch, u + "/box9.cp.tito.io", `{"addrs":[]}`}, {"non-member delete on a flagged app", http.MethodDelete, u + "/box9.cp.tito.io", ""}, } { status, h, out := rawReq(t, c.method, c.url, eve, c.body) assertNotFound(t, c.what, want, status, h, out) } } // --- The detach doors -------------------------------------------------------- // // The NINTH door, and the same fuse as the eighth: handleAPIDomainDetach's // first-route branch calls the very provision.DeleteRecord the instance // delete does, on the same app, and the route is registered // svcrole.Allow(svcrole.Publish, svcrole.Instance) — so a service credential // reaches it. Its browser twin is detachDomain, shared by // handleAppDomainDetach. // // Refused OUTRIGHT rather than only on the record-deleting branch. A // conditional guard would have to re-derive "does this claim mint the app's // route" ahead of the code that derives it, and the two would drift; and // releasing a hostname the fleet reaches the control plane by is not // something to do in passing. // cpDetachFixture attaches a hostname to `kass` through the shared core and // THEN sets the flag — the order a real control plane arrives in (the console // had its hostname long before anyone thought to mark it), and the only order // in which the attach half is reachable at all. // // The control in these tests is the SAME app with the flag cleared rather // than a sibling app, deliberately: it isolates the flag as the only variable // between a refusal and a 200, which a second app with its own routes and // claims could not do as cleanly. func cpDetachFixture(t *testing.T, label, host string) (*consoleFixture, string) { t.Helper() fx := newConsoleFixture(t) acct, err := fx.st.AccountBySqid(fx.sqid) if err != nil || acct == nil { t.Fatalf("fixture account %s: %v", fx.sqid, err) } if _, ref := fx.srv.attachDomain(context.Background(), acct, "kass", host, "", "", "paul@keymail.dev"); ref != nil { t.Fatalf("attach setup: refused %+v", ref) } if err := fx.st.SetAppControlPlane(OpsAccountID, "kass", true); err != nil { t.Fatal(err) } return fx, cliToken(t, fx, "paul@keymail.dev", label) } // TestControlPlaneAppRefusesDomainDetach: the API door. Refused for a person // and for BOTH service roles the route admits, the claim survives, and // clearing the flag is the only way through — which is what proves the // refusal is the flag's and nothing else's. func TestControlPlaneAppRefusesDomainDetach(t *testing.T) { const host = "docs.example.com" fx, token := cpDetachFixture(t, "code-cp-detach", host) u := fx.ts.URL + "/api/cli/apps/bab/kass/domains/" + host status, _, out := rawReq(t, http.MethodDelete, u, token, "") if status != http.StatusForbidden { t.Fatalf("a PERSON detaching a control-plane app's domain = %d %s, want 403", status, out) } if !strings.Contains(out, "control plane") { t.Fatalf("refusal %q does not name the flag the operator has to clear", out) } if !strings.Contains(out, "/a/bab/apps/kass/settings/advanced") { t.Fatalf("refusal %q does not point at the settings card that can clear the flag", out) } claimOf(t, fx, host) // Both roles the route admits, since either would otherwise walk round // the instance family's guard by taking this door instead. for _, role := range []svcrole.Role{svcrole.Publish, svcrole.Instance} { svc := serviceToken(t, fx.st, OpsAccountID, "ci-"+string(role), "kass", role) if status, _, out := rawReq(t, http.MethodDelete, u, svc, ""); status != http.StatusForbidden { t.Fatalf("a SERVICE (%s) detaching a control-plane app's domain = %d %s, want 403", role, status, out) } } claimOf(t, fx, host) // A malformed host answers the SAME 403, not the hostname check's 400: // on a flagged app this door's answer is constant, so it leaks nothing // about which hosts are even well-shaped here. if status, _, out := rawReq(t, http.MethodDelete, fx.ts.URL+"/api/cli/apps/bab/kass/domains/..%2f..%2fclaims", token, ""); status != http.StatusForbidden { t.Fatalf("malformed host on a flagged app = %d %s, want the flag's 403", status, out) } // Clear the flag and the identical call goes through. The flag is the // only variable between these two lines. if err := fx.st.SetAppControlPlane(OpsAccountID, "kass", false); err != nil { t.Fatal(err) } status, _, out = rawReq(t, http.MethodDelete, u, token, "") if status != http.StatusOK || !strings.Contains(out, `"removed":true`) { t.Fatalf("detach once the flag is cleared = %d %s, want 200 removed true", status, out) } claimAbsent(t, fx, host) } // TestControlPlaneAppRefusesBrowserDomainDetach: the browser twin deletes the // same record through the same core, so a guard only on the API door would // leave the route retirable by a form post. Rendered as a page, like every // other browser refusal, and ahead of the claim read. func TestControlPlaneAppRefusesBrowserDomainDetach(t *testing.T) { const host = "docs.example.com" fx, _ := cpDetachFixture(t, "code-cp-detach-browser", host) c := signInPaul(t, fx) csrf := sessionToken(t, fx, c) status, _, body := postForm(t, c, fx.u("/apps/kass/domains/detach"), url.Values{"csrf": {csrf}, "host": {host}, "confirm": {host}}) if status != http.StatusForbidden { t.Fatalf("owner detaching a control-plane app's domain in a browser = %d, want 403:\n%s", status, body) } if !strings.Contains(body, "control plane") { t.Fatalf("browser refusal does not name the flag:\n%s", body) } if !strings.Contains(body, "/a/bab/apps/kass/settings/advanced") { t.Fatalf("browser refusal does not point at the settings card that can clear the flag:\n%s", body) } claimOf(t, fx, host) // Cleared, the same form post lands. if err := fx.st.SetAppControlPlane(OpsAccountID, "kass", false); err != nil { t.Fatal(err) } if status, loc, body := postForm(t, c, fx.u("/apps/kass/domains/detach"), url.Values{"csrf": {csrf}, "host": {host}, "confirm": {host}}); status != http.StatusSeeOther || loc != fx.path("/apps/kass/settings/domains") { t.Fatalf("browser detach once the flag is cleared = %d -> %q, want 303 -> settings:\n%s", status, loc, body) } claimAbsent(t, fx, host) } // TestControlPlaneDomainDetachRefusalIsNotAnOracle: the guard sits after // domainsGate, so a stranger meets the family's byte-identical 404 and never // the sentence that would tell them this account holds a control-plane app. func TestControlPlaneDomainDetachRefusalIsNotAnOracle(t *testing.T) { const host = "docs.example.com" fx, _ := cpDetachFixture(t, "code-cp-detach-oracle", host) eve := cliToken(t, fx, "eve@keymail.dev", "code-cp-detach-eve") status, h, out := rawReq(t, http.MethodDelete, fx.ts.URL+"/api/cli/apps/bab/kass/domains/"+host, eve, "") assertNotFound(t, "non-member detach on a flagged app", canonicalNotFound(t), status, h, out) claimOf(t, fx, host) } // TestControlPlaneAppRefusesAServiceDomainAttach closes the CREATE half of // the provision-record lifecycle at the door that is not the instance one. // The first-route branch of `POST .../domains` writes a declared // provision.Record, which is the same authority transfer `POST .../instances` // refuses a service for — so a CI credential must not be able to acquire it // by taking the other door. // // Service-only, not outright: a PERSON attaching a domain to the control // plane is an ordinary operator act. Removal is the asymmetric one. // // Both service roles the route admits, and both request SHAPES — the alias // form and the `kind:"static"` first-route form — because the guard is // deliberately not conditional on which branch would have run. func TestControlPlaneAppRefusesAServiceDomainAttach(t *testing.T) { fx := newConsoleFixture(t) if err := fx.st.SetAppControlPlane(OpsAccountID, "kass", true); err != nil { t.Fatal(err) } u := fx.ts.URL + "/api/cli/apps/bab/kass/domains" const host = "docs.example.com" for _, role := range []svcrole.Role{svcrole.Publish, svcrole.Instance} { svc := serviceToken(t, fx.st, OpsAccountID, "ci-"+string(role), "kass", role) for what, body := range map[string]string{ "alias form": attachBody(host, "kass.carlosapp.dev"), "first-route form": `{"host":"` + host + `","kind":"static"}`, } { status, _, out := rawReq(t, http.MethodPost, u, svc, body) if status != http.StatusForbidden { t.Fatalf("a SERVICE (%s) attaching a domain to the control plane, %s = %d %s, want 403", role, what, status, out) } if !strings.Contains(out, "a person must") { t.Fatalf("refusal %q does not say a person is the principal this needs", out) } } claimAbsent(t, fx, host) } // The half that must keep working: a person attaches. token := cliToken(t, fx, "paul@keymail.dev", "code-cp-attach") if status, _, out := rawReq(t, http.MethodPost, u, token, attachBody(host, "kass.carlosapp.dev")); status != http.StatusOK { t.Fatalf("a PERSON attaching a domain to the control plane = %d %s, want 200", status, out) } claimOf(t, fx, host) // A non-member still meets the family's byte-identical 404, never the // refusal: the guard sits after domainsGate, so it is not an oracle for // which accounts hold a control-plane app. eve := cliToken(t, fx, "eve@keymail.dev", "code-cp-attach-eve") status, h, out := rawReq(t, http.MethodPost, u, eve, attachBody("eve.example.com", "kass.carlosapp.dev")) assertNotFound(t, "non-member attach on a flagged app", canonicalNotFound(t), status, h, out) claimAbsent(t, fx, "eve.example.com") // The control: with the flag cleared, the identical service call lands. // The flag is the only variable between these two outcomes. if err := fx.st.SetAppControlPlane(OpsAccountID, "kass", false); err != nil { t.Fatal(err) } svc := serviceToken(t, fx.st, OpsAccountID, "ci-cleared", "kass", svcrole.Publish) if status, _, out := rawReq(t, http.MethodPost, u, svc, attachBody("other.example.com", "kass.carlosapp.dev")); status != http.StatusOK { t.Fatalf("a SERVICE attaching once the flag is cleared = %d %s, want 200", status, out) } claimOf(t, fx, "other.example.com") } // TestControlPlaneAppRefusesAServicePathPublish is the prefix-map door's place // in this family. A published prefix map splits a hostname's traffic by leading // URL segment, and the reconcile pass admits any target route in the same // account — so a map on the CONTROL PLANE's own hostname hands a slice of the // console's URL space (`/admin`, `/api`) to whatever else that account runs. // That is not a decision a CI credential should be able to take. // // Service-only, like the attach above and unlike detach: a PERSON publishing a // prefix map for the control plane is an ordinary operator act. BOTH service // roles the route admits are exercised (Publish and Instance — see // TestAPIDomainPathsReachesPublishAndInstance), because this guard, not the // role table, is what keeps either of them off the console's own hostname. func TestControlPlaneAppRefusesAServicePathPublish(t *testing.T) { const host = "docs.example.com" fx, person := cpDetachFixture(t, "code-cp-paths", host) u := fx.ts.URL + "/api/cli/apps/bab/kass/domains/" + host + "/paths" body := pathsBody(t, map[string]string{"paulca": "kass.carlosapp.dev"}) svc := serviceToken(t, fx.st, OpsAccountID, "ci", "kass", svcrole.Publish) status, _, out := rawReq(t, http.MethodPut, u, svc, body) if status != http.StatusForbidden { t.Fatalf("a SERVICE publishing paths for the control plane = %d %s, want 403", status, out) } if !strings.Contains(out, "a person must") { t.Fatalf("refusal %q does not say a person is the principal this needs", out) } if c := claimOf(t, fx, host); len(c.Paths) != 0 { t.Fatalf("claim paths = %v, want none — a refused publish must not land", c.Paths) } // The other role the door admits, refused identically: an app's router // holding a CARLOS_INSTANCE_TOKEN reaches this handler (that is the // widening in spec §7) and meets the same guard. inst := serviceToken(t, fx.st, OpsAccountID, "router", "kass", svcrole.Instance) instStatus, _, instOut := rawReq(t, http.MethodPut, u, inst, body) if instStatus != status || instOut != out { t.Fatalf("an INSTANCE publishing paths for the control plane = %d %s, want the same %d %s a PUBLISH gets", instStatus, instOut, status, out) } if c := claimOf(t, fx, host); len(c.Paths) != 0 { t.Fatalf("claim paths = %v, want none — a refused publish must not land", c.Paths) } // The guard sits BEFORE the hostname is normalized, so its answer cannot // vary with host SHAPE: a flagged app refuses a malformed host and a valid // one with the identical status and the identical body. Were the order // reversed, the 400 "invalid hostname" would reach a prober first and this // door would report whether a name is well-formed before reporting that // the app is off limits — a small oracle, but the same class as the ones // the domains family is built to close. // // Only shapes that actually REACH the handler are comparable here. A // segment of "." or ".." is collapsed by net/http's own path cleaning // before routing, so it lands on a different pattern entirely (405) and // says nothing about this guard's ordering. The percent-encoded traversal // does reach it, which is the case the normalization below the guard // exists for. for _, bad := range []string{"not a host", "no-dot", "%2e%2e%2fclaims", "UPPER.example.com."} { badStatus, _, badOut := rawReq(t, http.MethodPut, fx.ts.URL+"/api/cli/apps/bab/kass/domains/"+bad+"/paths", svc, body) if badStatus != status || badOut != out { t.Fatalf("host %q answered %d %q; the flagged-app refusal must be constant across host shapes (%d %q)", bad, badStatus, badOut, status, out) } } // A non-member still meets the family's byte-identical 404, never the // refusal: the guard sits after domainsGate, so it is not an oracle for // which accounts hold a control-plane app. eve := cliToken(t, fx, "eve@keymail.dev", "code-cp-paths-eve") eveStatus, eveH, eveOut := rawReq(t, http.MethodPut, u, eve, body) assertNotFound(t, "non-member path publish on a flagged app", canonicalNotFound(t), eveStatus, eveH, eveOut) // The half that must keep working: a person publishes. if status, _, out := rawReq(t, http.MethodPut, u, person, body); status != http.StatusOK { t.Fatalf("a PERSON publishing paths for the control plane = %d %s, want 200", status, out) } if c := claimOf(t, fx, host); c.Paths["paulca"] != "kass.carlosapp.dev" { t.Fatalf("claim paths = %v, want the person's publish to have landed", c.Paths) } // The control: with the flag cleared, the identical service call lands. // The flag is the only variable between these two outcomes. if err := fx.st.SetAppControlPlane(OpsAccountID, "kass", false); err != nil { t.Fatal(err) } cleared := pathsBody(t, map[string]string{"bigcorp": "kass.carlosapp.dev"}) if status, _, out := rawReq(t, http.MethodPut, u, svc, cleared); status != http.StatusOK { t.Fatalf("a SERVICE publishing once the flag is cleared = %d %s, want 200", status, out) } if c := claimOf(t, fx, host); c.Paths["bigcorp"] != "kass.carlosapp.dev" { t.Fatalf("claim paths = %v, want the service's publish to have landed once the flag was cleared", c.Paths) } }