package console // The member half of the object-storage door (issue #55): declare a store, // then ask what became of it. What is pinned here, in the order it would hurt // if lost: a stranger probing either door learns nothing (the byte-identical // 404 the whole /api/cli surface keeps); the env prefix a provisioned store // delivers under cannot be changed out from under the app; and `status` tells // the truth about delivery — which variables have NOT arrived, and whether a // rotation is still half-done — because that is the only sight a member with // no box access has of either. import ( "context" "encoding/json" "errors" "net/http" "strings" "testing" "time" "github.com/carlosframework/platform/internal/appconfig" "github.com/carlosframework/platform/internal/objstore" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/usage" ) func storeFixture(t *testing.T, code string) (*consoleFixture, string) { t.Helper() fx := newConsoleFixture(t) now := time.Now() if _, err := fx.st.CreateAppIn(OpsAccountID, "titogo", "paul@keymail.dev", now); err != nil { t.Fatal(err) } // A MEMBER, not an owner: declaring your own app's store is a member // action, like promote below stable — the operator half (grant, rotate, // destroy) is a different door with a different authority. if err := fx.st.AddMember(OpsAccountID, "alice@keymail.dev", RoleMember, now); err != nil { t.Fatal(err) } return fx, cliToken(t, fx, "alice@keymail.dev", code) } func storeURL(fx *consoleFixture, app string) string { return fx.ts.URL + "/api/cli/apps/bab/" + app + "/store" } // storedRecord reads the record straight out of the bucket — the check that a // 200 actually landed, and that a refusal left nothing behind. nil means no // object at all. func storedRecord(t *testing.T, fx *consoleFixture, app string) *objstore.Record { t.Helper() rec, _, err := objstore.Get(context.Background(), fx.rel, release.OpsAccount, app) if err != nil { t.Fatalf("read store record: %v", err) } return rec } // putRecord hand-writes a record the console itself would only reach through // the operator doors (Task 9's grant/rotate) — the state this door has to // REPORT on, and refuse to contradict, long before that door exists. func putRecord(t *testing.T, fx *consoleFixture, app string, rec *objstore.Record) { t.Helper() if _, err := objstore.Put(context.Background(), fx.rel, release.OpsAccount, app, rec, ""); err != nil { t.Fatalf("seed store record: %v", err) } } func strList(t *testing.T, out map[string]any, key string) []string { t.Helper() raw, ok := out[key].([]any) if !ok { t.Fatalf("%s = %#v, want a list", key, out[key]) } got := make([]string, len(raw)) for i, v := range raw { s, ok := v.(string) if !ok { t.Fatalf("%s[%d] = %#v, want a string", key, i, v) } got[i] = s } return got } func block(t *testing.T, out map[string]any, key string) map[string]any { t.Helper() m, ok := out[key].(map[string]any) if !ok { t.Fatalf("%s = %#v, want an object", key, out[key]) } return m } // ---- declare ---------------------------------------------------------------- // TestAPIStoreDeclareWritesTheRecord: the whole happy path. An empty body // means the default prefix, the record lands in the bucket with the caller as // its author, the answer names the variables the app will eventually be given, // and NO serial is bumped — nothing a box reads has changed, so waking the // fleet would be noise. func TestAPIStoreDeclareWritesTheRecord(t *testing.T) { fx, token := storeFixture(t, "code-store-declare") if rec := storedRecord(t, fx, "titogo"); rec != nil { t.Fatalf("a store record exists before any declaration: %+v", rec) } status, out := postJSON(t, storeURL(fx, "titogo"), map[string]any{}, token) if status != http.StatusOK { t.Fatalf("declare = %d %v, want 200", status, out) } if str(t, out, "app") != "titogo" { t.Fatalf("app = %v, want titogo", out["app"]) } declared := block(t, out, "declared") if declared["env_prefix"] != objstore.DefaultEnvPrefix { t.Fatalf("env_prefix = %v, want the default %q", declared["env_prefix"], objstore.DefaultEnvPrefix) } if declared["by"] != "alice@keymail.dev" { t.Fatalf("declared.by = %v, want the caller", declared["by"]) } if out["grant"] != nil || out["provisioned"] != nil { t.Fatalf("grant/provisioned = %v/%v, want null on a fresh declaration", out["grant"], out["provisioned"]) } // This fixture configures no backend, and the answer says so — the field // is what separates "queued behind an operator" from "this deployment // will never provision one". if out["backend"] != "" { t.Fatalf("backend = %v, want \"\" on a deployment with no object storage", out["backend"]) } // No endpoint name: this deployment provisions none, and a name the app // will never be given would send someone hunting for a missing variable. wantNames := []string{"CARLOS_STORE_BUCKET", "CARLOS_STORE_REGION", "CARLOS_STORE_ACCESS_KEY", "CARLOS_STORE_SECRET_KEY"} if got := strList(t, out, "env_names"); strings.Join(got, ",") != strings.Join(wantNames, ",") { t.Fatalf("env_names = %v, want %v", got, wantNames) } if out["pending_finish"] != false { t.Fatalf("pending_finish = %v, want false", out["pending_finish"]) } if got := strList(t, out, "missing_vars"); len(got) != 0 { t.Fatalf("missing_vars = %v, want empty until the store is provisioned", got) } rec := storedRecord(t, fx, "titogo") if rec == nil || rec.Declared == nil { t.Fatalf("record after a 200 = %+v, want a declaration in the bucket", rec) } if rec.App != "titogo" || rec.Schema != objstore.Schema { t.Fatalf("record = %+v, want app titogo at schema %d", rec, objstore.Schema) } if rec.Declared.EnvPrefix != objstore.DefaultEnvPrefix || rec.Declared.By != "alice@keymail.dev" { t.Fatalf("record declaration = %+v", rec.Declared) } // Nothing box-visible changed: no version moved, no config was written. if got := serial(t, fx); got != 0 { t.Fatalf("serial after a declaration = %d, want 0 — declaring delivers nothing to a box", got) } auditedBy(t, fx, "titogo", "store-create", "alice@keymail.dev", objstore.DefaultEnvPrefix) } // TestAPIStoreDeclareHonoursThePrefixAndIsIdempotent: a chosen prefix renames // every delivered variable, and re-declaring the same prefix preserves the // original declaration. A second member can safely run create again. func TestAPIStoreDeclareHonoursThePrefixAndIsIdempotent(t *testing.T) { fx, token := storeFixture(t, "code-store-prefix") status, out := postJSON(t, storeURL(fx, "titogo"), map[string]any{"env_prefix": "BLOBS"}, token) if status != http.StatusOK { t.Fatalf("declare = %d %v", status, out) } wantNames := []string{"BLOBS_BUCKET", "BLOBS_REGION", "BLOBS_ACCESS_KEY", "BLOBS_SECRET_KEY"} if got := strList(t, out, "env_names"); strings.Join(got, ",") != strings.Join(wantNames, ",") { t.Fatalf("env_names = %v, want %v", got, wantNames) } if err := fx.st.AddMember(OpsAccountID, "bob@keymail.dev", RoleMember, time.Now()); err != nil { t.Fatal(err) } bob := cliToken(t, fx, "bob@keymail.dev", "code-store-prefix-bob") status, out = postJSON(t, storeURL(fx, "titogo"), map[string]any{"env_prefix": "BLOBS"}, bob) if status != http.StatusOK { t.Fatalf("same-prefix re-declare = %d %v, want 200", status, out) } rec := storedRecord(t, fx, "titogo") if rec == nil || rec.Declared == nil || rec.Declared.EnvPrefix != "BLOBS" { t.Fatalf("record after re-declare = %+v", rec) } // A retry preserves the original author and does not race a grant write. if rec.Declared.By != "alice@keymail.dev" { t.Fatalf("declared.by = %q, want the original caller", rec.Declared.By) } } // TestAPIStoreDeclareRefusesAnInvalidPrefix: the refusal names the charset, // because a caller who typed a lowercase prefix has to learn the rule from the // answer — and nothing is written, so a corrected retry starts from nothing. func TestAPIStoreDeclareRefusesAnInvalidPrefix(t *testing.T) { fx, token := storeFixture(t, "code-store-badprefix") for _, bad := range []string{"blobs", "9BLOBS", "BLOBS-X", strings.Repeat("A", 33)} { status, out := postJSON(t, storeURL(fx, "titogo"), map[string]any{"env_prefix": bad}, token) if status != http.StatusBadRequest { t.Fatalf("declare %q = %d %v, want 400", bad, status, out) } if !strings.Contains(str(t, out, "error"), "^[A-Z][A-Z0-9_]{0,31}$") { t.Fatalf("declare %q error = %q, want the charset named", bad, out["error"]) } if rec := storedRecord(t, fx, "titogo"); rec != nil { t.Fatalf("a refused declaration wrote a record: %+v", rec) } } } // TestAPIStoreNamesTheDeploymentBackend: a member with no infra access has no // other way to tell a declaration waiting on an operator from one waiting on a // deployment that has no object storage at all — and both doors have to agree // about it, since `carlos store create` is where most people will look first. func TestAPIStoreNamesTheDeploymentBackend(t *testing.T) { fx, token := storeFixture(t, "code-store-backend") fx.srv.cfg.StoreDeployment = objstore.Deployment{ Backend: "aws", Region: "eu-west-1", BucketPrefix: "carlos-", } status, out := postJSON(t, storeURL(fx, "titogo"), map[string]any{}, token) if status != http.StatusOK { t.Fatalf("declare = %d %v", status, out) } if out["backend"] != "aws" { t.Fatalf("declare backend = %v, want aws", out["backend"]) } status, out = getJSON(t, storeURL(fx, "titogo"), token) if status != http.StatusOK { t.Fatalf("status = %d %v", status, out) } if out["backend"] != "aws" { t.Fatalf("status backend = %v, want aws", out["backend"]) } } // TestAPIStorePrefixIsFixedOnceGranted: the mid-grant window. The grant block // is written FIRST and the variables are delivered several steps before // `provisioned` lands, so a guard keyed on `provisioned` alone would accept a // rename in exactly the window where vars already exist under the old prefix — // orphaning them against a key the next pass reaps. func TestAPIStorePrefixIsFixedOnceGranted(t *testing.T) { fx, token := storeFixture(t, "code-store-grant-locked") now := time.Now() putRecord(t, fx, "titogo", &objstore.Record{ Declared: &objstore.Declared{EnvPrefix: "BLOBS", At: now, By: "alice@keymail.dev"}, Grant: &objstore.GrantBlock{Backend: "aws", At: now, By: "paul@keymail.dev"}, }) status, out := postJSON(t, storeURL(fx, "titogo"), map[string]any{"env_prefix": "OTHER"}, token) if status != http.StatusConflict { t.Fatalf("re-declare mid-grant = %d %v, want 409", status, out) } if msg := str(t, out, "error"); !strings.Contains(msg, "destroy and re-create") { t.Fatalf("error = %q, want the way out named", msg) } // It must not claim a bucket exists — nothing has been provisioned yet. if msg := str(t, out, "error"); strings.Contains(msg, "already provisioned") { t.Fatalf("error = %q, but this store is granted, not provisioned", msg) } if rec := storedRecord(t, fx, "titogo"); rec.Declared.EnvPrefix != "BLOBS" { t.Fatalf("a refused re-declare changed the prefix to %q", rec.Declared.EnvPrefix) } // The same prefix remains a no-op: a re-run of `carlos store create` must // not turn into an error the moment an operator picks the request up. if status, out := postJSON(t, storeURL(fx, "titogo"), map[string]any{"env_prefix": "BLOBS"}, token); status != http.StatusOK { t.Fatalf("same-prefix re-declare mid-grant = %d %v, want 200", status, out) } } // TestAPIStorePrefixIsFixedOnceProvisioned: the delivered variable names are // what a RUNNING app reads. Renaming them under a provisioned store would // leave the app looking for variables nothing will ever write again, so the // door refuses and says how to actually change it. Same prefix still passes: // a re-run of `carlos store create` must not become an error the day the // store gets provisioned. func TestAPIStorePrefixIsFixedOnceProvisioned(t *testing.T) { fx, token := storeFixture(t, "code-store-locked") now := time.Now() putRecord(t, fx, "titogo", &objstore.Record{ Declared: &objstore.Declared{EnvPrefix: "BLOBS", At: now, By: "alice@keymail.dev"}, Grant: &objstore.GrantBlock{Backend: "aws", At: now, By: "paul@keymail.dev"}, Provisioned: &objstore.Provisioned{Bucket: "carlos-bab-titogo", Region: "eu-west-1", IAMUser: "carlos-store-bab-titogo", KeyID: "AKIAEXAMPLE", At: now}, }) status, out := postJSON(t, storeURL(fx, "titogo"), map[string]any{"env_prefix": "OTHER"}, token) if status != http.StatusConflict { t.Fatalf("re-declare with a new prefix = %d %v, want 409", status, out) } if msg := str(t, out, "error"); !strings.Contains(msg, "destroy and re-create") { t.Fatalf("error = %q, want the way out named", msg) } rec := storedRecord(t, fx, "titogo") if rec.Declared.EnvPrefix != "BLOBS" { t.Fatalf("a refused re-declare changed the prefix to %q", rec.Declared.EnvPrefix) } status, out = postJSON(t, storeURL(fx, "titogo"), map[string]any{"env_prefix": "BLOBS"}, token) if status != http.StatusOK { t.Fatalf("same-prefix re-declare on a provisioned store = %d %v, want 200", status, out) } // The provisioning survives the no-op — the reply echoes the whole // record, not just the half this door writes. if got := block(t, out, "provisioned")["bucket"]; got != "carlos-bab-titogo" { t.Fatalf("provisioned.bucket = %v after a re-declare", got) } if rec := storedRecord(t, fx, "titogo"); rec.Provisioned == nil || rec.Provisioned.KeyID != "AKIAEXAMPLE" { t.Fatalf("re-declare clobbered the provisioning: %+v", rec) } } // ---- status ----------------------------------------------------------------- // TestAPIStoreStatusIs404BeforeAnyDeclaration: an app with no store answers // 404 — but a SPEAKING one. This 404 fires only after the member gate has // passed, so the caller is entitled to the sentence; the gate's own refusals // (below) stay byte-identical with the stdlib's. func TestAPIStoreStatusIs404BeforeAnyDeclaration(t *testing.T) { fx, token := storeFixture(t, "code-store-absent") status, out := getJSON(t, storeURL(fx, "titogo"), token) if status != http.StatusNotFound { t.Fatalf("status with no store = %d %v, want 404", status, out) } if msg := str(t, out, "error"); !strings.Contains(msg, "no store is declared") { t.Fatalf("status error = %q, want it to say no store is declared", msg) } } // TestAPIStoreStatusReportsDeliveryAndPendingFinish: the two facts a member // with no box access cannot see any other way — which delivered variables have // NOT arrived in the app's config, and whether a rotation is still holding the // old key alive. missing_vars is computed against the app's real manifest, so // a half-delivered store reads as half-delivered rather than "provisioned". func TestAPIStoreStatusReportsDeliveryAndPendingFinish(t *testing.T) { fx, token := storeFixture(t, "code-store-status") now := time.Now() putRecord(t, fx, "titogo", &objstore.Record{ Declared: &objstore.Declared{EnvPrefix: "BLOBS", At: now, By: "alice@keymail.dev"}, Grant: &objstore.GrantBlock{Backend: "aws", At: now, By: "paul@keymail.dev"}, Provisioned: &objstore.Provisioned{ Bucket: "carlos-bab-titogo", Region: "eu-west-1", Endpoint: "https://s3.example.com", IAMUser: "carlos-store-bab-titogo", UserARN: "arn:aws:iam::1:user/carlos-store-bab-titogo", KeyID: "AKIANEW", PrevKeyID: "AKIAOLD", At: now, }, }) // Only the bucket coordinate ever landed in the app's config. if _, err := appconfig.Apply(context.Background(), fx.rel, release.OpsAccount, "titogo", map[string]appconfig.Var{"BLOBS_BUCKET": {Kind: "env", Value: "carlos-bab-titogo"}}, nil); err != nil { t.Fatal(err) } status, out := getJSON(t, storeURL(fx, "titogo"), token) if status != http.StatusOK { t.Fatalf("status = %d %v", status, out) } prov := block(t, out, "provisioned") if prov["key_id"] != "AKIANEW" || prov["prev_key_id"] != "AKIAOLD" { t.Fatalf("provisioned = %v, want both key ids echoed (they are not secrets)", prov) } if out["pending_finish"] != true { t.Fatalf("pending_finish = %v, want true while a previous key is still live", out["pending_finish"]) } // The endpoint name appears only because this store HAS an endpoint. wantNames := []string{"BLOBS_BUCKET", "BLOBS_REGION", "BLOBS_ENDPOINT", "BLOBS_ACCESS_KEY", "BLOBS_SECRET_KEY"} if got := strList(t, out, "env_names"); strings.Join(got, ",") != strings.Join(wantNames, ",") { t.Fatalf("env_names = %v, want %v", got, wantNames) } wantMissing := []string{"BLOBS_REGION", "BLOBS_ENDPOINT", "BLOBS_ACCESS_KEY", "BLOBS_SECRET_KEY"} if got := strList(t, out, "missing_vars"); strings.Join(got, ",") != strings.Join(wantMissing, ",") { t.Fatalf("missing_vars = %v, want %v", got, wantMissing) } } // TestAPIStoreStatusOfADeclaredButUnprovisionedStore: no bucket exists yet, so // there is nothing to be missing — an app that has merely ASKED for a store // must not read as four undelivered variables, which is what a caller would // chase down. func TestAPIStoreStatusOfADeclaredButUnprovisionedStore(t *testing.T) { fx, token := storeFixture(t, "code-store-declared-only") if status, out := postJSON(t, storeURL(fx, "titogo"), map[string]any{}, token); status != http.StatusOK { t.Fatalf("declare = %d %v", status, out) } status, out := getJSON(t, storeURL(fx, "titogo"), token) if status != http.StatusOK { t.Fatalf("status = %d %v", status, out) } if got := strList(t, out, "missing_vars"); len(got) != 0 { t.Fatalf("missing_vars = %v, want empty for a store nobody has provisioned", got) } if out["pending_finish"] != false { t.Fatalf("pending_finish = %v, want false", out["pending_finish"]) } } // ---- the gate --------------------------------------------------------------- // TestAPIStoreGateIsByteIdentical: on BOTH doors, a token that belongs to no // account, an account segment that doesn't exist and an app the account never // claimed are one and the same 404 — byte-identical to http.NotFound's own // output, so probing this API teaches a token holder nothing about which // accounts or apps exist, and never that one has a store. func TestAPIStoreGateIsByteIdentical(t *testing.T) { fx, token := storeFixture(t, "code-store-gate") eve := cliToken(t, fx, "eve@keymail.dev", "code-store-gate-eve") doors := []struct { name string call func(u, bearer string) (int, string) }{ {"GET", func(u, bearer string) (int, string) { return getRaw(t, u, bearer) }}, {"POST", func(u, bearer string) (int, string) { return postRaw(t, u, map[string]any{"env_prefix": "BLOBS"}, bearer) }}, } for _, d := range doors { t.Run(d.name, func(t *testing.T) { // (a) a member of nothing, against a real account and a real app. statusA, bodyA := d.call(storeURL(fx, "titogo"), eve) // (b) a member, against an account that does not exist. statusB, bodyB := d.call(fx.ts.URL+"/api/cli/apps/zzzz/titogo/store", token) // (c) a member of the account, against an app it never claimed. statusC, bodyC := d.call(storeURL(fx, "ghost-app"), token) if statusA != http.StatusNotFound || statusB != http.StatusNotFound || statusC != http.StatusNotFound { t.Fatalf("%s gate = %d / %d / %d, want 404 for all three", d.name, statusA, statusB, statusC) } if bodyA != bodyB || bodyB != bodyC { t.Fatalf("%s refusals are not byte-identical:\n(a) %q\n(b) %q\n(c) %q", d.name, bodyA, bodyB, bodyC) } if bodyA != "404 page not found\n" { t.Fatalf("%s refusal body = %q, want the stdlib http.NotFound output", d.name, bodyA) } }) } if rec := storedRecord(t, fx, "titogo"); rec != nil { t.Fatalf("a refused request wrote a record: %+v", rec) } } // TestAPIStoreNoTokenIs401: the token gate is first, before any name is // resolved — cliUser's one 401 on both doors. func TestAPIStoreNoTokenIs401(t *testing.T) { fx, _ := storeFixture(t, "code-store-notoken") if status, _ := getJSON(t, storeURL(fx, "titogo"), ""); status != http.StatusUnauthorized { t.Fatalf("status with no token = %d, want 401", status) } if status, _ := postJSON(t, storeURL(fx, "titogo"), map[string]any{}, ""); status != http.StatusUnauthorized { t.Fatalf("declare with no token = %d, want 401", status) } } // TestAPIStoreDeclareIsRateLimited: the shared write budget, keyed on the // ADDRESS like every other write door here — a budget belongs to the person, // not the terminal they typed on. func TestAPIStoreDeclareIsRateLimited(t *testing.T) { fx, token := storeFixture(t, "code-store-limit") fx.srv.cli.write = newRateLimiter(1, time.Minute) if status, out := postJSON(t, storeURL(fx, "titogo"), map[string]any{}, token); status != http.StatusOK { t.Fatalf("first declare = %d %v", status, out) } if status, out := postJSON(t, storeURL(fx, "titogo"), map[string]any{}, token); status != http.StatusTooManyRequests { t.Fatalf("second declare on an exhausted budget = %d %v, want 429", status, out) } } // TestAPIStoreWithoutABucketIs503: the record IS a bucket object. A console // with nowhere to put it — or to read it from — must say so rather than // answer for a store it cannot see. func TestAPIStoreWithoutABucketIs503(t *testing.T) { fx, token := storeFixture(t, "code-store-nobucket") fx.srv.cfg.Releases = nil if status, out := postJSON(t, storeURL(fx, "titogo"), map[string]any{}, token); status != http.StatusServiceUnavailable { t.Fatalf("declare with no bucket = %d %v, want 503", status, out) } if status, out := getJSON(t, storeURL(fx, "titogo"), token); status != http.StatusServiceUnavailable { t.Fatalf("status with no bucket = %d %v, want 503", status, out) } } // ---- grant, rotate, finish (the operator half) ------------------------------- // // What these pin, in the order it would hurt if lost: nobody outside // CARLOS_STORE_OPERATORS can make this deployment spend money, and the member // gate still runs FIRST so the allowlist never becomes an oracle; a grant that // is refused for any reason leaves the backend untouched; a grant that // succeeds delivers the minted secret sealed into the app's own config and // announces itself to the fleet; and the rotation state machine keeps its // two-key discipline — rotate leaves both keys live and bumps, finish kills // the old one and bumps nothing. // storeArmed is storeFixture with a whole deployment behind it: an in-memory // backend, a deployment that mints carlos-- buckets in eu-west-1, a // secrets key the console can seal with, and alice on the operator allowlist. // // It hands back the fake (assert on the estate it actually built), the // member's token, and the base64 PRIVATE key — the half this console never // holds — so a test can prove the secret delivered into config is the one the // backend minted, rather than merely that four variables exist. func storeArmed(t *testing.T, code string) (*consoleFixture, *objstore.MemProvisioner, string, string) { t.Helper() fx, token := storeFixture(t, code) pub, priv, _ := sealedFor(t, "irrelevant") fake := objstore.NewMemProvisioner("eu-west-1") fx.srv.cfg.SecretsPub = pub fx.srv.cfg.StoreProvisioner = fake fx.srv.cfg.StoreDeployment = objstore.Deployment{Backend: "mem", Region: "eu-west-1", BucketPrefix: "carlos-"} fx.srv.cfg.CallbackSigner = callbackTestSigner(t, "test-current-webhook-master", "") // Spelled with different capitals on purpose: an operator list is typed by // a human into a unit file, and addresses are case-insensitive. fx.srv.cfg.StoreOperators = []string{"Alice@Keymail.dev"} return fx, fake, token, priv } // declareStore is `carlos store create` — the member half every operator verb // below needs to exist before it has anything to act on. func declareStore(t *testing.T, fx *consoleFixture, token string) { t.Helper() if status, out := postJSON(t, storeURL(fx, "titogo"), map[string]any{}, token); status != http.StatusOK { t.Fatalf("declare = %d %v", status, out) } } // appVars reads the app's config manifest straight out of the bucket — the // only place a delivered store credential ever lands. func appVars(t *testing.T, fx *consoleFixture, app string) map[string]appconfig.Var { t.Helper() m, err := appconfig.Get(context.Background(), fx.rel, release.OpsAccount, app) if err != nil { t.Fatalf("read config: %v", err) } return m.Vars } // openSecret decrypts a delivered secret with the private key the console has // never seen. func openSecret(t *testing.T, priv string, v appconfig.Var) string { t.Helper() pk, err := appconfig.ParsePrivateKey(priv, "test") if err != nil { t.Fatal(err) } plain, err := appconfig.Open(pk, appconfig.Binding{Account: release.OpsAccount, App: "titogo"}, v.Envelope) if err != nil { t.Fatalf("open envelope: %v", err) } return string(plain) } // untouched asserts the backend has nothing in it — the check every refusal // owes: a door that refuses AFTER creating a bucket has already spent money. func untouched(t *testing.T, fake *objstore.MemProvisioner, when string) { t.Helper() if len(fake.Buckets) != 0 || len(fake.Users) != 0 { t.Fatalf("%s touched the backend: buckets %v, users %v", when, fake.Buckets, fake.Users) } } // TestAPIStoreGrantNeedsADeploymentOperator: a member of the account — the // person who declared the store — cannot grant it. Provisioning spends real // money in the DEPLOYMENT's cloud account, so the authority belongs to // whoever runs the deployment; the refusal names the variable that carries // that list, because the only person who can fix it is the one who can read // it. An empty list is fail-closed: nobody may grant. func TestAPIStoreGrantNeedsADeploymentOperator(t *testing.T) { fx, fake, token, _ := storeArmed(t, "code-store-grant-operator") declareStore(t, fx, token) fx.srv.cfg.StoreOperators = []string{"paul@keymail.dev"} // alice is not on it status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{}, token) if status != http.StatusForbidden { t.Fatalf("grant by a non-operator member = %d %v, want 403", status, out) } if msg := str(t, out, "error"); !strings.Contains(msg, "CARLOS_STORE_OPERATORS") { t.Fatalf("error = %q, want the operator list named", msg) } untouched(t, fake, "a non-operator grant") if rec := storedRecord(t, fx, "titogo"); rec.Grant != nil { t.Fatalf("a refused grant wrote a grant block: %+v", rec.Grant) } if got := serial(t, fx); got != 0 { t.Fatalf("serial after a refused grant = %d, want 0", got) } // Unset is not "everybody" — the direction a spending gate must fail. fx.srv.cfg.StoreOperators = nil if status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{}, token); status != http.StatusForbidden { t.Fatalf("grant with no operator list = %d %v, want 403", status, out) } // The allowlist runs BEFORE the deployment-shape 503s. A non-operator who // could tell "this deployment has no object storage" from "you may not // grant" would have turned the grant door into a config oracle for every // member of every account. fx.srv.cfg.StoreProvisioner = nil if status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{}, token); status != http.StatusForbidden { t.Fatalf("grant by a non-operator on a backend-less console = %d %v, want 403 — not the 503", status, out) } fx.srv.cfg.Releases = nil if status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{}, token); status != http.StatusForbidden { t.Fatalf("grant by a non-operator on a bucket-less console = %d %v, want 403", status, out) } } // TestAPIStoreGrantAllowlistDoesNotFold: the allowlist is an EXACT match on // the canonical (ASCII-lowercased) address every entry point in this codebase // writes — not a Unicode fold. U+017F (ſ) folds to "s" and is already // lowercase, so it sails through every ToLower on the way in and ValidAddress // accepts it; under strings.EqualFold, owner@example.teſt would be handed the // authority to spend this deployment's money on behalf of owner@example.test. func TestAPIStoreGrantAllowlistDoesNotFold(t *testing.T) { fx, fake, token, _ := storeArmed(t, "code-store-grant-fold") declareStore(t, fx, token) fx.srv.cfg.StoreOperators = []string{"owner@example.test"} const homoglyph = "owner@example.teſt" // owner@example.teſt if err := fx.st.AddMember(OpsAccountID, homoglyph, RoleMember, time.Now()); err != nil { t.Fatal(err) } impostor := cliToken(t, fx, homoglyph, "code-store-grant-fold-imp") // The member gate passes for this address — so a 403 below is the // allowlist refusing, not the account resolution failing by accident. if status, out := getJSON(t, storeURL(fx, "titogo"), impostor); status != http.StatusOK { t.Fatalf("status as the ſ member = %d %v, want 200 — the member gate must be what passed", status, out) } status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{}, impostor) if status != http.StatusForbidden { t.Fatalf("grant as %q against an allowlist of owner@example.test = %d %v, want 403", homoglyph, status, out) } untouched(t, fake, "a fold-matched grant") if rec := storedRecord(t, fx, "titogo"); rec.Grant != nil { t.Fatalf("a fold-matched grant wrote a grant block: %+v", rec.Grant) } // The address the list actually names still works, however it was typed. fx.srv.cfg.StoreOperators = []string{" Alice@Keymail.dev "} if status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{}, token); status != http.StatusOK { t.Fatalf("grant by the listed operator = %d %v, want 200", status, out) } } // TestAPIStoreOperatorDoorsGateIsByteIdentical: the member gate runs BEFORE // the allowlist on all three doors — eve here IS a deployment operator, and // she still gets the same 404 as an unknown account and an app that isn't // there. If the allowlist ran first, a 403 would tell an operator which of a // deployment's accounts and apps exist; if it ran instead of the member gate, // it would let one in. func TestAPIStoreOperatorDoorsGateIsByteIdentical(t *testing.T) { fx, _, token, _ := storeArmed(t, "code-store-op-gate") fx.srv.cfg.StoreOperators = []string{"eve@keymail.dev", "alice@keymail.dev"} eve := cliToken(t, fx, "eve@keymail.dev", "code-store-op-gate-eve") for _, door := range []string{"/grant", "/rotate", "/rotate/finish"} { t.Run(door, func(t *testing.T) { // (a) an operator who is a member of nothing, against a real app. statusA, bodyA := postRaw(t, storeURL(fx, "titogo")+door, map[string]any{}, eve) // (b) a member, against an account that does not exist. statusB, bodyB := postRaw(t, fx.ts.URL+"/api/cli/apps/zzzz/titogo/store"+door, map[string]any{}, token) // (c) a member of the account, against an app it never claimed. statusC, bodyC := postRaw(t, storeURL(fx, "ghost-app")+door, map[string]any{}, token) if statusA != http.StatusNotFound || statusB != http.StatusNotFound || statusC != http.StatusNotFound { t.Fatalf("%s gate = %d / %d / %d, want 404 for all three", door, statusA, statusB, statusC) } if bodyA != bodyB || bodyB != bodyC { t.Fatalf("%s refusals are not byte-identical:\n(a) %q\n(b) %q\n(c) %q", door, bodyA, bodyB, bodyC) } if bodyA != "404 page not found\n" { t.Fatalf("%s refusal body = %q, want the stdlib http.NotFound output", door, bodyA) } }) } if status, _ := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{}, ""); status != http.StatusUnauthorized { t.Fatalf("grant with no token = %d, want 401", status) } } // TestAPIStoreGrantWithoutABackendIs503: a deployment with no object storage // wired up refuses the verbs that would create something, and names the // variable that would wire it. The member's declaration still stands — it is a // request an operator fulfils, and the operator is exactly who this sentence // is for. func TestAPIStoreGrantWithoutABackendIs503(t *testing.T) { fx, _, token, _ := storeArmed(t, "code-store-grant-nobackend") declareStore(t, fx, token) fx.srv.cfg.StoreProvisioner = nil for _, door := range []string{"/grant", "/rotate", "/rotate/finish"} { status, out := postJSON(t, storeURL(fx, "titogo")+door, map[string]any{}, token) if status != http.StatusServiceUnavailable { t.Fatalf("%s with no backend = %d %v, want 503", door, status, out) } if msg := str(t, out, "error"); !strings.Contains(msg, "CARLOS_STORE_BACKEND") { t.Fatalf("%s error = %q, want the backend variable named", door, msg) } } if rec := storedRecord(t, fx, "titogo"); rec.Grant != nil { t.Fatalf("a refused grant wrote a grant block: %+v", rec.Grant) } } // TestAPIStoreGrantProvisionsAndDelivers: the whole authority path. One call // mints a bucket, an IAM principal and a key, seals both credential halves // into the app's own config, records what it built, and bumps the serial so // the fleet picks the config up in seconds rather than at the hourly floor. // // The secret is checked by DECRYPTING it with the key the console never holds: // "four variables are present" would pass just as happily on a delivery that // sealed the wrong bytes. func TestAPIStoreGrantProvisionsAndDelivers(t *testing.T) { fx, fake, token, priv := storeArmed(t, "code-store-grant-happy") declareStore(t, fx, token) if got := serial(t, fx); got != 0 { t.Fatalf("serial before the grant = %d, want 0", got) } status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{}, token) if status != http.StatusOK { t.Fatalf("grant = %d %v, want 200", status, out) } prov := block(t, out, "provisioned") const bucket = "carlos-bab-titogo" const user = "carlos-store-bab-titogo" if prov["bucket"] != bucket || prov["region"] != "eu-west-1" || prov["iam_user"] != user { t.Fatalf("provisioned = %v, want the minted bucket, its region and the app's principal", prov) } keyID, _ := prov["key_id"].(string) if keyID == "" { t.Fatalf("provisioned.key_id = %v, want the minted key id", prov["key_id"]) } if out["pending_finish"] != false { t.Fatalf("pending_finish = %v, want false on a first grant", out["pending_finish"]) } if got := strList(t, out, "missing_vars"); len(got) != 0 { t.Fatalf("missing_vars = %v, want nothing missing right after a delivery", got) } if block(t, out, "grant")["by"] != "alice@keymail.dev" { t.Fatalf("grant.by = %v, want the operator who asked", block(t, out, "grant")["by"]) } // The estate the fake actually built: one bucket, one principal, one key. if _, ok := fake.Buckets[bucket]; !ok { t.Fatalf("buckets = %v, want %s", fake.Buckets, bucket) } u, ok := fake.Users[user] if !ok || len(u.Keys) != 1 { t.Fatalf("users = %v, want %s holding exactly one key", fake.Users, user) } secret, ok := u.Keys[keyID] if !ok { t.Fatalf("keys = %v, want the recorded key id %q among them", u.Keys, keyID) } // Delivery: coordinates in the clear, both credential halves sealed. vars := appVars(t, fx, "titogo") if v := vars["CARLOS_STORE_BUCKET"]; v.Kind != "env" || v.Value != bucket { t.Fatalf("CARLOS_STORE_BUCKET = %+v, want the bucket as a plain env var", v) } if v := vars["CARLOS_STORE_REGION"]; v.Kind != "env" || v.Value != "eu-west-1" { t.Fatalf("CARLOS_STORE_REGION = %+v", v) } if _, present := vars["CARLOS_STORE_ENDPOINT"]; present { t.Fatalf("CARLOS_STORE_ENDPOINT was delivered by a deployment that has no endpoint") } access := vars["CARLOS_STORE_ACCESS_KEY"] if access.Kind != "secret" || access.Value != "" { t.Fatalf("CARLOS_STORE_ACCESS_KEY = %+v, want a sealed secret and no plaintext", access) } if got := openSecret(t, priv, access); got != keyID { t.Fatalf("sealed access key = %q, want the minted key id %q", got, keyID) } sealedSecret := vars["CARLOS_STORE_SECRET_KEY"] if sealedSecret.Kind != "secret" || sealedSecret.Value != "" { t.Fatalf("CARLOS_STORE_SECRET_KEY = %+v, want a sealed secret and no plaintext", sealedSecret) } if got := openSecret(t, priv, sealedSecret); got != secret { t.Fatalf("sealed secret half = %q, want the secret the backend minted", got) } // Recorded, announced, and audited. rec := storedRecord(t, fx, "titogo") if rec.Provisioned == nil || rec.Provisioned.KeyID != keyID || rec.Provisioned.Bucket != bucket { t.Fatalf("record after a grant = %+v", rec.Provisioned) } if rec.Grant == nil || rec.Grant.Backend != "mem" { t.Fatalf("grant block = %+v, want this deployment's backend", rec.Grant) } if got := serial(t, fx); got != 1 { t.Fatalf("serial after a grant = %d, want 1 — the app's config changed", got) } auditedBy(t, fx, "titogo", "store-grant", "alice@keymail.dev", bucket) // Idempotent: the operator's verb is one people re-run, and a healthy // store must not mint a second key for it. if status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{}, token); status != http.StatusOK { t.Fatalf("re-grant = %d %v, want 200", status, out) } if u := fake.Users[user]; len(u.Keys) != 1 || u.Keys[keyID] != secret { t.Fatalf("keys after a re-grant = %v, want the same single key", u.Keys) } } // TestAPIStoreGrantAdoptsANamedBucket: the operator can point a store at a // bucket that already exists — the migration path for data that predates the // platform — and the record says so, so nothing later re-derives a minted name // over the top of it. func TestAPIStoreGrantAdoptsANamedBucket(t *testing.T) { fx, fake, token, _ := storeArmed(t, "code-store-grant-adopt") declareStore(t, fx, token) status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{"bucket": "titogo-uploads"}, token) if status != http.StatusOK { t.Fatalf("adopt grant = %d %v, want 200", status, out) } if got := block(t, out, "provisioned")["bucket"]; got != "titogo-uploads" { t.Fatalf("provisioned.bucket = %v, want the adopted name", got) } if _, ok := fake.Buckets["carlos-bab-titogo"]; ok { t.Fatalf("an adopting grant also minted the default bucket: %v", fake.Buckets) } rec := storedRecord(t, fx, "titogo") if rec.Grant == nil || rec.Grant.Bucket != "titogo-uploads" { t.Fatalf("grant block = %+v, want the adopted bucket recorded", rec.Grant) } auditedBy(t, fx, "titogo", "store-grant", "alice@keymail.dev", "titogo-uploads") } // TestAPIStoreGrantRefusesAnotherStoresBucket (#218): adopting a bucket that // another app's store record already names would scope this app's credential // to that app's data — and tearing this store down later would delete it. The // door refuses before anything is created or recorded, including for a record // written before name claims existed, which holds no claim to find. func TestAPIStoreGrantRefusesAnotherStoresBucket(t *testing.T) { fx, fake, token, _ := storeArmed(t, "code-store-grant-taken") declareStore(t, fx, token) putRecord(t, fx, "other", &objstore.Record{ Declared: &objstore.Declared{EnvPrefix: "OTHER_S3", At: time.Now(), By: "paul@keymail.dev"}, Grant: &objstore.GrantBlock{Backend: "mem", Bucket: "other-uploads", At: time.Now(), By: "op"}, }) status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{"bucket": "other-uploads"}, token) if status != http.StatusConflict { t.Fatalf("adopting another store's bucket = %d %v, want 409", status, out) } if msg := str(t, out, "error"); !strings.Contains(msg, "another store") { t.Fatalf("error = %q, want it to say the name belongs to another store", msg) } untouched(t, fake, "adopting another store's bucket") if rec := storedRecord(t, fx, "titogo"); rec.Grant != nil { t.Fatalf("a refused grant wrote a grant block: %+v", rec.Grant) } } // TestAPIStoreGrantRefusesAnInvalidBucketName: an adopt name is interpolated // into the IAM policy ARN the credential is scoped by, so a bad one is refused // AT THE DOOR — before the record is read and before a single backend call, so // nothing exists to clean up and a corrected retry starts from where the // operator left off. func TestAPIStoreGrantRefusesAnInvalidBucketName(t *testing.T) { fx, fake, token, _ := storeArmed(t, "code-store-grant-badbucket") declareStore(t, fx, token) for _, bad := range []string{"Uploads", "up", "up_loads", "-uploads", "uploads-", strings.Repeat("u", 64), "uploads/*"} { status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{"bucket": bad}, token) if status != http.StatusBadRequest { t.Fatalf("grant with bucket %q = %d %v, want 400", bad, status, out) } untouched(t, fake, "an invalid bucket name") if rec := storedRecord(t, fx, "titogo"); rec.Grant != nil { t.Fatalf("a refused grant wrote a grant block: %+v", rec.Grant) } } } // TestAPIStoreGrantWithoutADeclarationIs409: the operator's verb never invents // the member's half. The env prefix is the member's choice and a store nobody // asked for has nowhere to be delivered, so the refusal names the command that // fixes it — and nothing is created. func TestAPIStoreGrantWithoutADeclarationIs409(t *testing.T) { fx, fake, token, _ := storeArmed(t, "code-store-grant-undeclared") status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{}, token) if status != http.StatusConflict { t.Fatalf("grant with no declaration = %d %v, want 409", status, out) } if msg := str(t, out, "error"); !strings.Contains(msg, "store create") { t.Fatalf("error = %q, want the member's command named", msg) } untouched(t, fake, "a grant with no declaration") if got := serial(t, fx); got != 0 { t.Fatalf("serial = %d, want 0 — nothing was delivered", got) } } // TestAPIStoreGrantNeedsASealingKey: a console with no CARLOS_SECRETS_PUBKEY // cannot seal, therefore cannot DELIVER, therefore must not MINT — a key // created here would exist in the backend with its secret already lost. Same // for a rotation. Finish is the exception, and deliberately: it seals nothing, // it deletes a key and records that, and a console that lost its pubkey must // still be able to close a two-key window. func TestAPIStoreGrantNeedsASealingKey(t *testing.T) { fx, fake, token, _ := storeArmed(t, "code-store-grant-nokey") declareStore(t, fx, token) pub := fx.srv.cfg.SecretsPub fx.srv.cfg.SecretsPub = "" status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{}, token) if status != http.StatusServiceUnavailable { t.Fatalf("grant with no secrets key = %d %v, want 503", status, out) } untouched(t, fake, "a grant with no sealing key") // A key that is present but unreadable is the same refusal: what matters // is whether this console can seal, not whether someone set the variable. fx.srv.cfg.SecretsPub = "not-a-key" if status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{}, token); status != http.StatusServiceUnavailable { t.Fatalf("grant with an unreadable secrets key = %d %v, want 503", status, out) } untouched(t, fake, "a grant with an unreadable sealing key") // Provision and rotate for real, then take the key away again. fx.srv.cfg.SecretsPub = pub if status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{}, token); status != http.StatusOK { t.Fatalf("grant = %d %v", status, out) } if status, out := postJSON(t, storeURL(fx, "titogo")+"/rotate", map[string]any{}, token); status != http.StatusOK { t.Fatalf("rotate = %d %v", status, out) } fx.srv.cfg.SecretsPub = "" if status, out := postJSON(t, storeURL(fx, "titogo")+"/rotate", map[string]any{}, token); status != http.StatusServiceUnavailable { t.Fatalf("rotate with no secrets key = %d %v, want 503", status, out) } status, out = postJSON(t, storeURL(fx, "titogo")+"/rotate/finish", map[string]any{}, token) if status != http.StatusOK { t.Fatalf("finish with no secrets key = %d %v, want 200 — finishing seals nothing", status, out) } if out["pending_finish"] != false { t.Fatalf("pending_finish = %v after a finish, want false", out["pending_finish"]) } } // TestAPIStoreRotateAndFinish: the two-key state machine, end to end, driven // by a MEMBER who is not a deployment operator — rotating touches only the // estate an operator already paid for, and a member must not have to find one // to cycle their own credential. // // Rotate leaves BOTH keys live and bumps the serial (the app's config // changed). Finish kills the old key and bumps NOTHING: no object a box reads // moved, and waking the fleet for it would be noise. func TestAPIStoreRotateAndFinish(t *testing.T) { fx, fake, token, priv := storeArmed(t, "code-store-rotate") declareStore(t, fx, token) if status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{}, token); status != http.StatusOK { t.Fatalf("grant = %d %v", status, out) } firstKey := storedRecord(t, fx, "titogo").Provisioned.KeyID if err := fx.st.AddMember(OpsAccountID, "bob@keymail.dev", RoleMember, time.Now()); err != nil { t.Fatal(err) } bob := cliToken(t, fx, "bob@keymail.dev", "code-store-rotate-bob") status, out := postJSON(t, storeURL(fx, "titogo")+"/rotate", map[string]any{}, bob) if status != http.StatusOK { t.Fatalf("rotate by a member = %d %v, want 200 — rotation is a member verb", status, out) } prov := block(t, out, "provisioned") newKey, _ := prov["key_id"].(string) if newKey == "" || newKey == firstKey { t.Fatalf("key_id = %v, want a fresh key (was %q)", prov["key_id"], firstKey) } if prov["prev_key_id"] != firstKey { t.Fatalf("prev_key_id = %v, want the key the app is still holding (%q)", prov["prev_key_id"], firstKey) } if out["pending_finish"] != true { t.Fatalf("pending_finish = %v after a rotate, want true", out["pending_finish"]) } const user = "carlos-store-bab-titogo" if u := fake.Users[user]; len(u.Keys) != 2 { t.Fatalf("keys after a rotate = %v, want both live until finish", u.Keys) } if got := openSecret(t, priv, appVars(t, fx, "titogo")["CARLOS_STORE_SECRET_KEY"]); got != fake.Users[user].Keys[newKey] { t.Fatalf("delivered secret = %q, want the newly minted one", got) } if got := serial(t, fx); got != 2 { t.Fatalf("serial after grant+rotate = %d, want 2 — the app's config changed again", got) } auditedBy(t, fx, "titogo", "store-rotate", "bob@keymail.dev", newKey) status, out = postJSON(t, storeURL(fx, "titogo")+"/rotate/finish", map[string]any{}, bob) if status != http.StatusOK { t.Fatalf("finish = %d %v, want 200", status, out) } if out["pending_finish"] != false { t.Fatalf("pending_finish = %v after a finish, want false", out["pending_finish"]) } if block(t, out, "provisioned")["key_id"] != newKey { t.Fatalf("finish moved the live key: %v", block(t, out, "provisioned")) } u := fake.Users[user] if len(u.Keys) != 1 { t.Fatalf("keys after a finish = %v, want only the live one", u.Keys) } if _, ok := u.Keys[firstKey]; ok { t.Fatalf("finish left the previous key %q alive", firstKey) } if got := serial(t, fx); got != 2 { t.Fatalf("serial after a finish = %d, want it unmoved at 2 — nothing a box reads changed", got) } auditedBy(t, fx, "titogo", "store-rotate-finish", "bob@keymail.dev", newKey) // Finishing twice refuses rather than succeeding vacuously: a member who // believes they just retired a key, when an earlier call already did, has a // different picture of the estate than the estate does. status, out = postJSON(t, storeURL(fx, "titogo")+"/rotate/finish", map[string]any{}, bob) if status != http.StatusConflict { t.Fatalf("second finish = %d %v, want 409", status, out) } if msg := str(t, out, "error"); !strings.Contains(msg, "pending") { t.Fatalf("error = %q, want it to say nothing is pending", msg) } } // TestAPIStoreRotateWithoutAProvisionedStoreIs409: rotation edits an existing // credential estate — it never creates one, because the coordinates it reseals // come from the record and there is no defensible guess for them. Both verbs // point at the one that does create it. func TestAPIStoreRotateWithoutAProvisionedStoreIs409(t *testing.T) { fx, fake, token, _ := storeArmed(t, "code-store-rotate-unprovisioned") declareStore(t, fx, token) for _, door := range []string{"/rotate", "/rotate/finish"} { status, out := postJSON(t, storeURL(fx, "titogo")+door, map[string]any{}, token) if status != http.StatusConflict { t.Fatalf("%s on a declared-only store = %d %v, want 409", door, status, out) } if msg := str(t, out, "error"); !strings.Contains(msg, "grant") { t.Fatalf("%s error = %q, want the grant named as the way forward", door, msg) } } untouched(t, fake, "a rotate with nothing provisioned") if got := serial(t, fx); got != 0 { t.Fatalf("serial = %d, want 0", got) } } // racingStore lands a competing write on one key immediately BEFORE the // handler's own Put reaches the bucket — the interleaving that turns a walk's // etag stale. One shot, so only the write under test races. Mirrors the // wrapper objstore's walk tests use, at the console's own seam: the fixture // owns Config.Releases, so swapping it in is all it takes to make a // two-console race reproducible in one process. type racingStore struct { release.Store raceKeyOnce string race func(inner release.Store) } func (r *racingStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) { if key != "" && key == r.raceKeyOnce { r.raceKeyOnce = "" r.race(r.Store) } return r.Store.Put(ctx, key, body, ifMatch) } // TestAPIStoreRotateConflictSaysReRun: the highest-stakes sentence in this // file. A rotation that loses its final CAS to ANOTHER rotation has already // minted a key and delivered it into the app's config — the walk refuses to // overwrite the competitor's record precisely because that record names a key // somebody else delivered. So the answer must never read as "nothing // happened": a member told their rotation failed would go looking for a key // that is already live, and might reach for a grant, which reaps. func TestAPIStoreRotateConflictSaysReRun(t *testing.T) { fx, _, token, _ := storeArmed(t, "code-store-rotate-conflict") declareStore(t, fx, token) if status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{}, token); status != http.StatusOK { t.Fatalf("grant = %d %v", status, out) } firstKey := storedRecord(t, fx, "titogo").Provisioned.KeyID // From here on, the console writes through a store that lets a competing // rotation land in the window between this walk's Get and its Put. racer := &racingStore{Store: fx.rel, raceKeyOnce: objstore.Key(release.OpsAccount, "titogo")} racer.race = func(in release.Store) { other, etag, err := objstore.Get(context.Background(), in, release.OpsAccount, "titogo") if err != nil { t.Fatal(err) } // Another rotation got there first and delivered a key of its own. other.Provisioned.KeyID = "MEMKEY99" other.Provisioned.PrevKeyID = firstKey if _, err := objstore.Put(context.Background(), in, release.OpsAccount, "titogo", other, etag); err != nil { t.Fatal(err) } } fx.srv.cfg.Releases = racer status, out := postJSON(t, storeURL(fx, "titogo")+"/rotate", map[string]any{}, token) if status != http.StatusConflict { t.Fatalf("rotate over a concurrent rotation = %d %v, want 409", status, out) } if msg := str(t, out, "error"); msg != "another rotation is in progress — re-run to converge" { t.Fatalf("error = %q, want the re-run-to-converge sentence", msg) } // The words that would be a lie: this caller HAS minted and delivered. for _, forbidden := range []string{"nothing", "failed", "try again"} { if strings.Contains(strings.ToLower(str(t, out, "error")), forbidden) { t.Fatalf("error = %q, must not suggest the rotation left nothing behind", out["error"]) } } // The competitor's record stands — the whole reason the walk gave up. if rec := storedRecord(t, fx, "titogo"); rec.Provisioned.KeyID != "MEMKEY99" { t.Fatalf("the competing rotation was clobbered: %+v", rec.Provisioned) } } // TestAPIStoreGrantFailureSaysResume: a backend that refuses gets ONE fixed // sentence, and the answer carries no bucket name, ARN, endpoint or request id // — those go to the console log, where the operator can read them and a // tenant cannot. The sentence's promise is what the walk's ordering // guarantees: the grant block naming the bucket is written BEFORE anything is // created, so nothing exists that only a bill would find, and the same verb // run again resumes from where it stopped. func TestAPIStoreGrantFailureSaysResume(t *testing.T) { fx, fake, token, _ := storeArmed(t, "code-store-grant-failure") declareStore(t, fx, token) fake.FailNext["EnsureBucket"] = errors.New( "AccessDenied: arn:aws:s3:::carlos-bab-titogo — status code 403, request id 8EXAMPLE7") status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{}, token) if status != http.StatusBadGateway { t.Fatalf("grant against a refusing backend = %d %v, want 502", status, out) } msg := str(t, out, "error") if msg != "could not provision the store — nothing unrecorded was left behind; re-run the grant to resume" { t.Fatalf("error = %q, want the fixed resume sentence", msg) } for _, leak := range []string{"carlos-bab-titogo", "AccessDenied", "arn:aws", "403", "8EXAMPLE7"} { if strings.Contains(msg, leak) { t.Fatalf("error = %q, leaks backend detail %q", msg, leak) } } // Recorded before created: the bucket the walk was going to make is named // in the record, which is exactly why the sentence above is true. rec := storedRecord(t, fx, "titogo") if rec.Grant == nil || rec.Grant.Bucket != "carlos-bab-titogo" { t.Fatalf("grant block after a failed walk = %+v, want the bucket recorded", rec.Grant) } if rec.Provisioned != nil { t.Fatalf("provisioned = %+v after a failed walk", rec.Provisioned) } if got := serial(t, fx); got != 0 { t.Fatalf("serial after a failed grant = %d, want 0 — nothing was delivered", got) } // The one-shot failure is spent; re-running resumes and completes. if status, out := postJSON(t, storeURL(fx, "titogo")+"/grant", map[string]any{}, token); status != http.StatusOK { t.Fatalf("re-run grant = %d %v, want 200", status, out) } if _, ok := fake.Buckets["carlos-bab-titogo"]; !ok { t.Fatalf("buckets after the resume = %v", fake.Buckets) } } // TestAPIStoreStatusCarriesTheSweptSize: `size` is copied from the newest // storage sweep, dated. It is null when no sweep has measured the store, and // a sweep that did not see the store provisioned is never shown (its 0 would // read as an empty bucket). func TestAPIStoreStatusCarriesTheSweptSize(t *testing.T) { fx, token := storeFixture(t, "code-store-size") ctx := context.Background() now := time.Now() putRecord(t, fx, "titogo", &objstore.Record{ Declared: &objstore.Declared{EnvPrefix: "BLOBS", At: now, By: "alice@keymail.dev"}, Grant: &objstore.GrantBlock{Backend: "aws", At: now, By: "paul@keymail.dev"}, Provisioned: &objstore.Provisioned{Bucket: "carlos-bab-titogo", Region: "eu-west-1", IAMUser: "u", KeyID: "AKIA", At: now}, }) key := release.OpsAccount + "/titogo" seed := func(day time.Time, st usage.AppStorage) { t.Helper() body, _ := json.Marshal(usage.StorageDay{V: 1, Date: usage.DayOf(day), Apps: map[string]usage.AppStorage{key: st}}) if _, err := fx.rel.Put(ctx, usage.StorageDayKey(usage.DayOf(day)), body, ""); err != nil { t.Fatal(err) } } size := func() map[string]any { t.Helper() code, out := getJSON(t, storeURL(fx, "titogo"), token) if code != http.StatusOK { t.Fatalf("status = %d %v", code, out) } if out["size"] == nil { return nil } return block(t, out, "size") } if got := size(); got != nil { t.Fatalf("size = %v with no sweep at all, want null", got) } // A sweep that ran before the grant: the store was not there to size. seed(now.AddDate(0, 0, -2), usage.AppStorage{BucketBytes: 10}) if got := size(); got != nil { t.Fatalf("size = %v from a sweep that did not see the store, want null", got) } seed(now.AddDate(0, 0, -1), usage.AppStorage{BucketBytes: 10, StoreBytes: 5 << 20, StoreProvisioned: true}) got := size() if got == nil || got["store_bytes"] != float64(5<<20) || got["as_of"] != usage.DayOf(now.AddDate(0, 0, -1)) { t.Fatalf("size = %v, want yesterday's 5MB", got) } // Today's sweep could not read the bucket: the newest answer says so. seed(now, usage.AppStorage{BucketBytes: 10, StoreUnswept: true}) if got := size(); got == nil || got["unswept"] != true || got["as_of"] != usage.DayOf(now) { t.Fatalf("size = %v, want today's unswept marker", got) } }