carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3// The sidecar pause (sidecarsgate.go), through the real Server, its router
4// and the cliRoute auth stack the rest of apisidecars_test.go uses. The
5// properties, in the order they would hurt if lost: with the allowlist set,
6// an account it does not name cannot enable or create a sidecar by any
7// credential that reaches those doors, and nothing is written to the bucket
8// when it is refused; a stranger still gets the byte-identical 404, so the
9// gate never confirms an account exists; existing sidecars stay listable,
10// wakeable and deletable; a named account is unaffected; and with the
11// allowlist unset nothing changes at all.
12
13import (
14 "context"
15 "encoding/json"
16 "net/http"
17 "slices"
18 "testing"
19 "time"
20
21 "github.com/carlosframework/platform/internal/provision"
22 "github.com/carlosframework/platform/internal/release"
23 "github.com/carlosframework/platform/internal/svcrole"
24)
25
26// sidecarAppKeys is every bucket key under the fixture app's prefix, sorted: the
27// refused calls must leave it exactly as it was.
28func sidecarAppKeys(t *testing.T, fx *consoleFixture) []string {
29 t.Helper()
30 keys, err := fx.rel.List(context.Background(), release.AppPrefix("bab", "titogo"))
31 if err != nil {
32 t.Fatal(err)
33 }
34 slices.Sort(keys)
35 return keys
36}
37
38func assertSidecarsPaused(t *testing.T, what string, status int, body string) {
39 t.Helper()
40 if status != http.StatusForbidden {
41 t.Fatalf("%s = %d %s, want 403", what, status, body)
42 }
43 var got struct{ Error, Code string }
44 if err := json.Unmarshal([]byte(body), &got); err != nil {
45 t.Fatalf("%s body %q: %v", what, body, err)
46 }
47 if got.Code != "sidecars-paused" || got.Error != sidecarsPausedMsg {
48 t.Fatalf("%s body = %+v, want code sidecars-paused and the paused sentence", what, got)
49 }
50}
51
52// TestSidecarsPausedRefusesEnableAndCreate: the fixture's account (bab) is
53// not on the list, so enable and create are refused for the member, the
54// legacy app token and an instance-role service principal, and the bucket
55// is untouched.
56func TestSidecarsPausedRefusesEnableAndCreate(t *testing.T) {
57 fx, member := instanceFixture(t, "code-side-paused")
58 fx.srv.cfg.SidecarAccounts = []string{"cdef"}
59 u := sidecarsURL(fx, "bab", "titogo")
60
61 // Enable with no manifest yet: refused, and no manifest appears.
62 before := sidecarAppKeys(t, fx)
63 status, _, out := rawReq(t, http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), member, `{"channel":"beta"}`)
64 assertSidecarsPaused(t, "paused enable, no manifest", status, out)
65 if after := sidecarAppKeys(t, fx); !slices.Equal(before, after) {
66 t.Fatalf("a refused enable changed the bucket: %v -> %v", before, after)
67 }
68
69 // An operator-written manifest (store mode, not a console door) is
70 // there; enable still may not rewrite it.
71 enableSidecars(t, fx, "titogo", titoSidecarManifest())
72 manifest, _, err := fx.rel.Get(context.Background(), provision.SidecarManifestKey("bab", "titogo"))
73 if err != nil {
74 t.Fatal(err)
75 }
76 status, _, out = rawReq(t, http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), member, `{"channel":"beta","args":["evil"]}`)
77 assertSidecarsPaused(t, "paused enable over a manifest", status, out)
78 if now, _, err := fx.rel.Get(context.Background(), provision.SidecarManifestKey("bab", "titogo")); err != nil || string(now) != string(manifest) {
79 t.Fatalf("a refused enable rewrote the manifest: %s -> %s (%v)", manifest, now, err)
80 }
81
82 // Create, by every credential the create door admits.
83 appTok, err := fx.st.NewAppToken(OpsAccountID, "titogo", "alice@keymail.dev", "signup", time.Now())
84 if err != nil {
85 t.Fatal(err)
86 }
87 svc := serviceToken(t, fx.st, OpsAccountID, "router", "titogo", svcrole.Instance)
88 before = sidecarAppKeys(t, fx)
89 for _, c := range []struct{ what, token string }{
90 {"member create", member},
91 {"app token create", appTok},
92 {"instance service principal create", svc},
93 } {
94 status, _, out := rawReq(t, http.MethodPost, u, c.token, `{"name":"bot","region":"ie"}`)
95 assertSidecarsPaused(t, c.what, status, out)
96 }
97 if after := sidecarAppKeys(t, fx); !slices.Equal(before, after) {
98 t.Fatalf("a refused create changed the bucket: %v -> %v", before, after)
99 }
100 if keys, err := fx.rel.List(context.Background(), provision.SidecarsPrefix("bab", "titogo")); err != nil || len(keys) != 0 {
101 t.Fatalf("refused creates left records %v (%v)", keys, err)
102 }
103
104 // The gate comes after membership: a stranger and an unknown account
105 // still get the byte-identical 404, never the 403 that would confirm
106 // the account exists and is paused.
107 want := canonicalNotFound(t)
108 eve := cliToken(t, fx, "eve@keymail.dev", "code-side-paused-eve")
109 for _, c := range []struct{ what, method, url, token, body string }{
110 {"non-member enable", http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), eve, `{}`},
111 {"non-member create", http.MethodPost, u, eve, `{"name":"bot","region":"ie"}`},
112 {"unknown account create", http.MethodPost, sidecarsURL(fx, "zzzz", "titogo"), member, `{"name":"bot","region":"ie"}`},
113 {"unclaimed app create", http.MethodPost, sidecarsURL(fx, "bab", "ghost-app"), member, `{"name":"bot","region":"ie"}`},
114 } {
115 status, h, out := rawReq(t, c.method, c.url, c.token, c.body)
116 assertNotFound(t, c.what, want, status, h, out)
117 }
118 // And validation still answers first for a member.
119 if status, _, out := rawReq(t, http.MethodPost, u, member, `{"name":"-nope-","region":"ie"}`); status != http.StatusBadRequest {
120 t.Fatalf("paused create with a bad name = %d %s, want the 400 first", status, out)
121 }
122}
123
124// TestSidecarsPausedLeavesExistingSidecarsAlone: a sidecar created before the
125// pause is still listed, read, woken and deleted by the paused account, by
126// the member and by the instance service principal.
127func TestSidecarsPausedLeavesExistingSidecarsAlone(t *testing.T) {
128 fx, member := instanceFixture(t, "code-side-paused-existing")
129 enableSidecars(t, fx, "titogo", titoSidecarManifest())
130 u := sidecarsURL(fx, "bab", "titogo")
131 for _, name := range []string{"bot", "bot2"} {
132 if status, _, out := rawReq(t, http.MethodPost, u, member, `{"name":"`+name+`","region":"ie"}`); status != http.StatusCreated {
133 t.Fatalf("create %s before the pause = %d %s", name, status, out)
134 }
135 }
136 fx.srv.cfg.SidecarAccounts = []string{"cdef"}
137 svc := serviceToken(t, fx.st, OpsAccountID, "router", "titogo", svcrole.Instance)
138
139 for _, c := range []struct{ what, method, url string }{
140 {"list", http.MethodGet, u},
141 {"get", http.MethodGet, u + "/bot"},
142 {"wake", http.MethodPost, u + "/bot/wake"},
143 } {
144 if status, _, out := rawReq(t, c.method, c.url, member, ""); status != http.StatusOK {
145 t.Fatalf("paused %s = %d %s, want 200", c.what, status, out)
146 }
147 }
148 for _, c := range []struct{ name, token string }{{"bot", member}, {"bot2", svc}} {
149 if status, _, out := rawReq(t, http.MethodDelete, u+"/"+c.name, c.token, ""); status != http.StatusOK {
150 t.Fatalf("paused delete of %s = %d %s, want 200", c.name, status, out)
151 }
152 if _, _, err := fx.rel.Get(context.Background(), provision.SidecarRecordKey("bab", "titogo", c.name)); err == nil {
153 t.Fatalf("the record for %s survived its delete", c.name)
154 }
155 }
156}
157
158// TestSidecarsPauseAllowlistAndUnset: an account the list names, and a
159// console with no list at all, enable and create exactly as before.
160func TestSidecarsPauseAllowlistAndUnset(t *testing.T) {
161 for _, c := range []struct {
162 what string
163 accts []string
164 }{
165 {"allowlisted", []string{"cdef", "bab"}},
166 {"unset", nil},
167 } {
168 t.Run(c.what, func(t *testing.T) {
169 fx, member := instanceFixture(t, "code-side-allow-"+c.what)
170 fx.srv.cfg.SidecarAccounts = c.accts
171 status, _, out := rawReq(t, http.MethodPut, sidecarEnableURL(fx, "bab", "titogo"), member, `{"channel":"stable"}`)
172 if status != http.StatusOK {
173 t.Fatalf("enable = %d %s, want 200", status, out)
174 }
175 // The pool and regions are operator policy the enable door never
176 // sets; put them as the operator would so create can place.
177 enableSidecars(t, fx, "titogo", titoSidecarManifest())
178 u := sidecarsURL(fx, "bab", "titogo")
179 if status, _, out := rawReq(t, http.MethodPost, u, member, `{"name":"bot","region":"ie"}`); status != http.StatusCreated {
180 t.Fatalf("member create = %d %s, want 201", status, out)
181 }
182 svc := serviceToken(t, fx.st, OpsAccountID, "router", "titogo", svcrole.Instance)
183 if status, _, out := rawReq(t, http.MethodPost, u, svc, `{"name":"bot2","region":"ie"}`); status != http.StatusCreated {
184 t.Fatalf("service principal create = %d %s, want 201", status, out)
185 }
186 for _, name := range []string{"bot", "bot2"} {
187 if _, err := provision.GetSidecarRecord(context.Background(), fx.rel, "bab", "titogo", name); err != nil {
188 t.Fatalf("record %s: %v", name, err)
189 }
190 }
191 })
192 }
193}
194