package console // The sidecar pause (sidecarsgate.go), through the real Server, its router // and the cliRoute auth stack the rest of apisidecars_test.go uses. The // properties, in the order they would hurt if lost: with the allowlist set, // an account it does not name cannot enable or create a sidecar by any // credential that reaches those doors, and nothing is written to the bucket // when it is refused; a stranger still gets the byte-identical 404, so the // gate never confirms an account exists; existing sidecars stay listable, // wakeable and deletable; a named account is unaffected; and with the // allowlist unset nothing changes at all. import ( "context" "encoding/json" "net/http" "slices" "testing" "time" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/svcrole" ) // sidecarAppKeys is every bucket key under the fixture app's prefix, sorted: the // refused calls must leave it exactly as it was. func sidecarAppKeys(t *testing.T, fx *consoleFixture) []string { t.Helper() keys, err := fx.rel.List(context.Background(), release.AppPrefix("bab", "titogo")) if err != nil { t.Fatal(err) } slices.Sort(keys) return keys } func assertSidecarsPaused(t *testing.T, what string, status int, body string) { t.Helper() if status != http.StatusForbidden { t.Fatalf("%s = %d %s, want 403", what, status, body) } var got struct{ Error, Code string } if err := json.Unmarshal([]byte(body), &got); err != nil { t.Fatalf("%s body %q: %v", what, body, err) } if got.Code != "sidecars-paused" || got.Error != sidecarsPausedMsg { t.Fatalf("%s body = %+v, want code sidecars-paused and the paused sentence", what, got) } } // TestSidecarsPausedRefusesEnableAndCreate: the fixture's account (bab) is // not on the list, so enable and create are refused for the member, the // legacy app token and an instance-role service principal, and the bucket // is untouched. func TestSidecarsPausedRefusesEnableAndCreate(t *testing.T) { fx, member := instanceFixture(t, "code-side-paused") fx.srv.cfg.SidecarAccounts = []string{"cdef"} u := sidecarsURL(fx, "bab", "titogo") // Enable with no manifest yet: refused, and no manifest appears. before := sidecarAppKeys(t, fx) status, _, out := rawReq(t, http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), member, `{"channel":"beta"}`) assertSidecarsPaused(t, "paused enable, no manifest", status, out) if after := sidecarAppKeys(t, fx); !slices.Equal(before, after) { t.Fatalf("a refused enable changed the bucket: %v -> %v", before, after) } // An operator-written manifest (store mode, not a console door) is // there; enable still may not rewrite it. enableSidecars(t, fx, "titogo", titoSidecarManifest()) manifest, _, err := fx.rel.Get(context.Background(), provision.SidecarManifestKey("bab", "titogo")) if err != nil { t.Fatal(err) } status, _, out = rawReq(t, http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), member, `{"channel":"beta","args":["evil"]}`) assertSidecarsPaused(t, "paused enable over a manifest", status, out) if now, _, err := fx.rel.Get(context.Background(), provision.SidecarManifestKey("bab", "titogo")); err != nil || string(now) != string(manifest) { t.Fatalf("a refused enable rewrote the manifest: %s -> %s (%v)", manifest, now, err) } // Create, by every credential the create door admits. appTok, err := fx.st.NewAppToken(OpsAccountID, "titogo", "alice@keymail.dev", "signup", time.Now()) if err != nil { t.Fatal(err) } svc := serviceToken(t, fx.st, OpsAccountID, "router", "titogo", svcrole.Instance) before = sidecarAppKeys(t, fx) for _, c := range []struct{ what, token string }{ {"member create", member}, {"app token create", appTok}, {"instance service principal create", svc}, } { status, _, out := rawReq(t, http.MethodPost, u, c.token, `{"name":"bot","region":"ie"}`) assertSidecarsPaused(t, c.what, status, out) } if after := sidecarAppKeys(t, fx); !slices.Equal(before, after) { t.Fatalf("a refused create changed the bucket: %v -> %v", before, after) } if keys, err := fx.rel.List(context.Background(), provision.SidecarsPrefix("bab", "titogo")); err != nil || len(keys) != 0 { t.Fatalf("refused creates left records %v (%v)", keys, err) } // The gate comes after membership: a stranger and an unknown account // still get the byte-identical 404, never the 403 that would confirm // the account exists and is paused. want := canonicalNotFound(t) eve := cliToken(t, fx, "eve@keymail.dev", "code-side-paused-eve") for _, c := range []struct{ what, method, url, token, body string }{ {"non-member enable", http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), eve, `{}`}, {"non-member create", http.MethodPost, u, eve, `{"name":"bot","region":"ie"}`}, {"unknown account create", http.MethodPost, sidecarsURL(fx, "zzzz", "titogo"), member, `{"name":"bot","region":"ie"}`}, {"unclaimed app create", http.MethodPost, sidecarsURL(fx, "bab", "ghost-app"), member, `{"name":"bot","region":"ie"}`}, } { status, h, out := rawReq(t, c.method, c.url, c.token, c.body) assertNotFound(t, c.what, want, status, h, out) } // And validation still answers first for a member. if status, _, out := rawReq(t, http.MethodPost, u, member, `{"name":"-nope-","region":"ie"}`); status != http.StatusBadRequest { t.Fatalf("paused create with a bad name = %d %s, want the 400 first", status, out) } } // TestSidecarsPausedLeavesExistingSidecarsAlone: a sidecar created before the // pause is still listed, read, woken and deleted by the paused account, by // the member and by the instance service principal. func TestSidecarsPausedLeavesExistingSidecarsAlone(t *testing.T) { fx, member := instanceFixture(t, "code-side-paused-existing") enableSidecars(t, fx, "titogo", titoSidecarManifest()) u := sidecarsURL(fx, "bab", "titogo") for _, name := range []string{"bot", "bot2"} { if status, _, out := rawReq(t, http.MethodPost, u, member, `{"name":"`+name+`","region":"ie"}`); status != http.StatusCreated { t.Fatalf("create %s before the pause = %d %s", name, status, out) } } fx.srv.cfg.SidecarAccounts = []string{"cdef"} svc := serviceToken(t, fx.st, OpsAccountID, "router", "titogo", svcrole.Instance) for _, c := range []struct{ what, method, url string }{ {"list", http.MethodGet, u}, {"get", http.MethodGet, u + "/bot"}, {"wake", http.MethodPost, u + "/bot/wake"}, } { if status, _, out := rawReq(t, c.method, c.url, member, ""); status != http.StatusOK { t.Fatalf("paused %s = %d %s, want 200", c.what, status, out) } } for _, c := range []struct{ name, token string }{{"bot", member}, {"bot2", svc}} { if status, _, out := rawReq(t, http.MethodDelete, u+"/"+c.name, c.token, ""); status != http.StatusOK { t.Fatalf("paused delete of %s = %d %s, want 200", c.name, status, out) } if _, _, err := fx.rel.Get(context.Background(), provision.SidecarRecordKey("bab", "titogo", c.name)); err == nil { t.Fatalf("the record for %s survived its delete", c.name) } } } // TestSidecarsPauseAllowlistAndUnset: an account the list names, and a // console with no list at all, enable and create exactly as before. func TestSidecarsPauseAllowlistAndUnset(t *testing.T) { for _, c := range []struct { what string accts []string }{ {"allowlisted", []string{"cdef", "bab"}}, {"unset", nil}, } { t.Run(c.what, func(t *testing.T) { fx, member := instanceFixture(t, "code-side-allow-"+c.what) fx.srv.cfg.SidecarAccounts = c.accts status, _, out := rawReq(t, http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), member, `{"channel":"stable"}`) if status != http.StatusOK { t.Fatalf("enable = %d %s, want 200", status, out) } // The pool and regions are operator policy the enable door never // sets; put them as the operator would so create can place. enableSidecars(t, fx, "titogo", titoSidecarManifest()) u := sidecarsURL(fx, "bab", "titogo") if status, _, out := rawReq(t, http.MethodPost, u, member, `{"name":"bot","region":"ie"}`); status != http.StatusCreated { t.Fatalf("member create = %d %s, want 201", status, out) } svc := serviceToken(t, fx.st, OpsAccountID, "router", "titogo", svcrole.Instance) if status, _, out := rawReq(t, http.MethodPost, u, svc, `{"name":"bot2","region":"ie"}`); status != http.StatusCreated { t.Fatalf("service principal create = %d %s, want 201", status, out) } for _, name := range []string{"bot", "bot2"} { if _, err := provision.GetSidecarRecord(context.Background(), fx.rel, "bab", "titogo", name); err != nil { t.Fatalf("record %s: %v", name, err) } } }) } }