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 app-declared private-storage policy (issue #412). What these pin, in
4// the order they would hurt if lost:
5//
6// - the create door provisions the store BEFORE it writes the record, so
7// a failed walk leaves no instance at all — never an instance that wakes
8// onto local disk because its bundle had no coordinates;
9// - the environment is the host's first label, and the coordinates land in
10// THAT bundle, not the app's default one;
11// - an app without the policy is byte-for-byte unchanged;
12// - the ceiling holds on both doors, and re-using an existing store is not
13// capped;
14// - declaring the policy is a person's act, refused in the two states
15// where it could only ever be a trap.
16
17import (
18 "context"
19 "encoding/json"
20 "errors"
21 "fmt"
22 "net/http"
23 "os"
24 "strings"
25 "testing"
26 "time"
27
28 "github.com/carlosframework/platform/internal/appconfig"
29 "github.com/carlosframework/platform/internal/objstore"
30 "github.com/carlosframework/platform/internal/provision"
31 "github.com/carlosframework/platform/internal/release"
32)
33
34// privateStorageFixture is instanceFixture with the store plane armed: a
35// backend, a sealing key, and an approved account entitlement — the three
36// things the policy spends, and the standing approval #408 §6 says is the
37// only one needed.
38func privateStorageFixture(t *testing.T, code string) (*consoleFixture, *objstore.MemProvisioner, string, string) {
39 t.Helper()
40 fx, token := instanceFixture(t, code)
41 pub, priv, _ := sealedFor(t, "irrelevant")
42 fake := objstore.NewMemProvisioner("eu-west-1")
43 fx.srv.cfg.SecretsPub = pub
44 fx.srv.cfg.StoreProvisioner = fake
45 fx.srv.cfg.StoreDeployment = objstore.Deployment{Backend: "mem", Region: "eu-west-1", BucketPrefix: "carlos-"}
46 approveStoreAccount(t, fx, "operator@keymail.dev")
47 return fx, fake, token, priv
48}
49
50// privateManifest is titoManifest carrying the policy. The domain is the
51// app's own (not the platform namespace), which keeps the sibling-apex
52// branch out of these tests — it has its own coverage.
53func privateManifest() provision.Manifest {
54 m := titoManifest()
55 m.PrivateStorage = true
56 return m
57}
58
59func createInstance(t *testing.T, fx *consoleFixture, token, body string) (int, string) {
60 t.Helper()
61 status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo"), token, body)
62 return status, out
63}
64
65func envStore(t *testing.T, fx *consoleFixture, env string) *objstore.Record {
66 t.Helper()
67 rec, _, err := objstore.GetIn(context.Background(), fx.rel, release.OpsAccount, "titogo", env)
68 if err != nil {
69 t.Fatalf("read %s store: %v", env, err)
70 }
71 return rec
72}
73
74func envVars(t *testing.T, fx *consoleFixture, env string) map[string]appconfig.Var {
75 t.Helper()
76 m, err := appconfig.LoadEnvironment(context.Background(), fx.rel, release.OpsAccount, "titogo", env)
77 if err != nil {
78 t.Fatalf("read %s bundle: %v", env, err)
79 }
80 return m.Vars
81}
82
83// The whole chain, as a signup path drives it: one instance create, and the
84// tenant has a bucket of its own whose coordinates are already in the bundle
85// the record binds to. No `store create`, no `store provision`, no operator.
86func TestInstanceCreateProvisionsPrivateStore(t *testing.T) {
87 fx, fake, token, priv := privateStorageFixture(t, "code-priv-chain")
88 enableInstances(t, fx, "titogo", privateManifest())
89
90 status, out := createInstance(t, fx, token, `{"host":"acme.go.tito.io","region":"ie"}`)
91 if status != http.StatusCreated {
92 t.Fatalf("create = %d %s, want 201", status, out)
93 }
94 var echoed struct {
95 Environment string `json:"environment"`
96 PrivateStoreBucket string `json:"private_store_bucket"`
97 }
98 if err := json.Unmarshal([]byte(out), &echoed); err != nil {
99 t.Fatalf("decode %s: %v", out, err)
100 }
101 // The environment is the host's FIRST LABEL, which is the one name
102 // predictable from the host (spec §6).
103 if echoed.Environment != "acme" {
104 t.Fatalf("echoed environment = %q, want acme", echoed.Environment)
105 }
106 if echoed.PrivateStoreBucket != "carlos-bab-titogo-acme" {
107 t.Fatalf("echoed bucket = %q, want carlos-bab-titogo-acme", echoed.PrivateStoreBucket)
108 }
109 // The record a box reconciles from carries the binding.
110 stored, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "acme.go.tito.io")
111 if err != nil {
112 t.Fatal(err)
113 }
114 if stored.Environment != "acme" {
115 t.Fatalf("stored environment = %q, want acme", stored.Environment)
116 }
117 // The store is real, provisioned, and its own.
118 rec := envStore(t, fx, "acme")
119 if rec == nil || rec.Provisioned == nil {
120 t.Fatalf("acme store = %+v, want a provisioned one", rec)
121 }
122 if rec.Provisioned.Bucket != "carlos-bab-titogo-acme" || rec.Provisioned.IAMUser != "carlos-store-bab-titogo-acme" {
123 t.Fatalf("provisioned = %+v, want the account/app/environment triple", rec.Provisioned)
124 }
125 // The grant's authority is the ACCOUNT entitlement, not an operator —
126 // which is the whole reason no new power was granted to anyone.
127 if rec.Grant == nil || rec.Grant.Source != objstore.GrantSourceAccountApproval {
128 t.Fatalf("grant = %+v, want source %q", rec.Grant, objstore.GrantSourceAccountApproval)
129 }
130 if _, ok := fake.Buckets["carlos-bab-titogo-acme"]; !ok {
131 t.Fatalf("backend buckets = %v, want the instance's own", fake.Buckets)
132 }
133 // Delivery goes to the INSTANCE's bundle. A credential in the app's
134 // default bundle would be read by every other instance — the isolation
135 // failing in exactly the direction that matters.
136 vars := envVars(t, fx, "acme")
137 if v, ok := vars["CARLOS_STORE_BUCKET"]; !ok || v.Value != "carlos-bab-titogo-acme" {
138 t.Fatalf("acme bundle CARLOS_STORE_BUCKET = %+v, want the instance's bucket", v)
139 }
140 secret, ok := vars["CARLOS_STORE_SECRET_KEY"]
141 if !ok || secret.Kind != "secret" {
142 t.Fatalf("acme bundle CARLOS_STORE_SECRET_KEY = %+v, want a sealed secret", secret)
143 }
144 if openSecret(t, priv, secret) == "" {
145 t.Fatal("the delivered secret did not open")
146 }
147 if got := appVars(t, fx, "titogo"); len(got) != 0 {
148 t.Fatalf("the app's default bundle = %v, want nothing — a shared credential is the isolation failing", got)
149 }
150}
151
152// An app without the policy is what it was before this branch: a record, no
153// store, no bundle, and no backend call. This is the test that fails if the
154// policy is ever read as on-by-default.
155func TestInstanceCreateWithoutPolicyTouchesNoStore(t *testing.T) {
156 fx, fake, token, _ := privateStorageFixture(t, "code-priv-off")
157 enableInstances(t, fx, "titogo", titoManifest())
158
159 status, out := createInstance(t, fx, token, `{"host":"acme.go.tito.io","region":"ie"}`)
160 if status != http.StatusCreated {
161 t.Fatalf("create = %d %s, want 201", status, out)
162 }
163 stored, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "acme.go.tito.io")
164 if err != nil {
165 t.Fatal(err)
166 }
167 if stored.Environment != "" {
168 t.Fatalf("stored environment = %q, want empty — no policy, no derived binding", stored.Environment)
169 }
170 if rec := envStore(t, fx, "acme"); rec != nil {
171 t.Fatalf("a store was created without the policy: %+v", rec)
172 }
173 untouched(t, fake, "a create with no private-storage policy")
174}
175
176// The policy fills a blank; it does not seize the axis. A caller who names
177// an environment gets a store for THAT one — which is still the invariant,
178// since what is promised is that the record's bundle carries the
179// coordinates, not that the bundle has any particular name.
180func TestInstanceCreateHonoursAnExplicitEnvironment(t *testing.T) {
181 fx, _, token, _ := privateStorageFixture(t, "code-priv-explicit")
182 enableInstances(t, fx, "titogo", privateManifest())
183
184 status, out := createInstance(t, fx, token,
185 `{"host":"acme.go.tito.io","region":"ie","environment":"shared-eu"}`)
186 if status != http.StatusCreated {
187 t.Fatalf("create = %d %s, want 201", status, out)
188 }
189 stored, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "acme.go.tito.io")
190 if err != nil {
191 t.Fatal(err)
192 }
193 if stored.Environment != "shared-eu" {
194 t.Fatalf("stored environment = %q, want the one the caller named", stored.Environment)
195 }
196 if rec := envStore(t, fx, "shared-eu"); rec == nil || rec.Provisioned == nil {
197 t.Fatalf("shared-eu store = %+v, want a provisioned one", rec)
198 }
199 if rec := envStore(t, fx, "acme"); rec != nil {
200 t.Fatalf("a store was also made for the derived name: %+v", rec)
201 }
202}
203
204// A DNS label is not an environment name, and the create door is where that
205// has to be said: labels run to 63 characters, environment names to 32, and
206// "default" is a legal label that names the app's own bundle. Every refusal
207// leaves nothing behind.
208func TestInstanceCreateRefusesALabelThatCannotNameAStore(t *testing.T) {
209 fx, fake, token, _ := privateStorageFixture(t, "code-priv-badlabel")
210 enableInstances(t, fx, "titogo", privateManifest())
211
212 for _, label := range []string{
213 "default",
214 "this-label-is-far-too-long-to-be-an-environment-name",
215 } {
216 host := label + ".go.tito.io"
217 status, out := createInstance(t, fx, token, fmt.Sprintf(`{"host":%q,"region":"ie"}`, host))
218 if status != http.StatusBadRequest {
219 t.Errorf("create at %s = %d %s, want 400", host, status, out)
220 continue
221 }
222 if _, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", host); !errors.Is(err, os.ErrNotExist) {
223 t.Errorf("a refused create at %s left a record (err %v)", host, err)
224 }
225 }
226 untouched(t, fake, "a create whose label cannot name a store")
227}
228
229// An EXPLICIT environment gets the same rails, and the same 400. Without
230// this the refusal comes from inside the walk instead — a 502 the caller
231// cannot act on, over a name they could have shortened.
232func TestInstanceCreateRefusesAnUnnameableExplicitEnvironment(t *testing.T) {
233 fx, fake, token, _ := privateStorageFixture(t, "code-priv-badenv")
234 enableInstances(t, fx, "titogo", privateManifest())
235 // 32 characters — a legal environment name, and an IAM principal of
236 // "carlos-store-bab-titogo-" + 32 = 56, under the ceiling. So this is
237 // the bucket half: "carlos-" + "bab-titogo-" + 32 = 50. Both fit, so
238 // the case that must refuse is the one the config plane itself refuses.
239 status, out := createInstance(t, fx, token,
240 `{"host":"acme.go.tito.io","region":"ie","environment":"UPPER"}`)
241 if status != http.StatusBadRequest {
242 t.Fatalf("create with an unusable environment = %d %s, want 400", status, out)
243 }
244 if _, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "acme.go.tito.io"); !errors.Is(err, os.ErrNotExist) {
245 t.Errorf("a refused create left a record (err %v)", err)
246 }
247 untouched(t, fake, "a create whose explicit environment cannot name a store")
248}
249
250// The IAM ceiling, and the arithmetic that makes it reachable at all. Both
251// names carry the same body: the bucket is <prefix><body> against S3's 63,
252// the principal is "carlos-store-" + <body> against IAM's 64. So under any
253// prefix longer than "carlos-store-" the BUCKET refusal always fires first,
254// and the IAM guard is only ever reached on a short prefix with a long app.
255// Encoded here so the guard cannot quietly become dead code.
256//
257// body = "bab" + "-" + app(25) + "-" + env(25) = 55.
258// bucket = "" + 55 = 55, legal. principal = 13 + 55 = 68, one refusal over.
259func TestInstanceCreateRefusesAnUnnameableIAMPrincipal(t *testing.T) {
260 const app = "a-rather-long-application" // 25
261 label := strings.Repeat("b", 25)
262
263 fx, token := instanceFixture(t, "code-priv-iamceiling")
264 if _, err := fx.st.CreateAppIn(OpsAccountID, app, "paul@keymail.dev", time.Now()); err != nil {
265 t.Fatal(err)
266 }
267 pub, _, _ := sealedFor(t, "irrelevant")
268 fake := objstore.NewMemProvisioner("eu-west-1")
269 fx.srv.cfg.SecretsPub = pub
270 fx.srv.cfg.StoreProvisioner = fake
271 fx.srv.cfg.StoreDeployment = objstore.Deployment{Backend: "mem", Region: "eu-west-1"}
272 approveStoreAccount(t, fx, "operator@keymail.dev")
273 enableInstances(t, fx, app, provision.Manifest{
274 Schema: 1, App: app, Domain: "go.tito.io", Channel: "stable", PrivateStorage: true})
275
276 host := label + ".go.tito.io"
277 status, _, out := rawReq(t, http.MethodPost, instancesURL(fx, "bab", app), token,
278 fmt.Sprintf(`{"host":%q}`, host))
279 if status != http.StatusBadRequest {
280 t.Fatalf("create = %d %s, want 400", status, out)
281 }
282 if !strings.Contains(out, "64") {
283 t.Errorf("refusal %q does not name AWS's ceiling", out)
284 }
285 if _, err := provision.GetRecord(context.Background(), fx.rel, "bab", app, host); !errors.Is(err, os.ErrNotExist) {
286 t.Errorf("a refused create left a record (err %v)", err)
287 }
288 untouched(t, fake, "a create whose principal cannot be named")
289}
290
291// A ceiling only the automatic path respected would be a ceiling anyone
292// could step over by hand.
293func TestStoreCreateRefusedAtTheCeiling(t *testing.T) {
294 fx, token := storeFixture(t, "code-store-ceiling")
295 fx.srv.cfg.StoreMaxPerApp = 1
296
297 if status, body := postRaw(t, storeEnvURL(fx, "titogo", "one"), map[string]any{}, token); status != http.StatusOK {
298 t.Fatalf("declare one = %d %s", status, body)
299 }
300 status, body := postRaw(t, storeEnvURL(fx, "titogo", "two"), map[string]any{}, token)
301 if status != http.StatusConflict {
302 t.Fatalf("declare past the cap = %d %s, want 409", status, body)
303 }
304 // Re-declaring the one that exists is not a new footprint, so it stands.
305 if status, body := postRaw(t, storeEnvURL(fx, "titogo", "one"), map[string]any{}, token); status != http.StatusOK {
306 t.Fatalf("re-declare one = %d %s, want 200", status, body)
307 }
308 // And the app's OWN store is not one of the capped collection.
309 if status, body := postRaw(t, storeURL(fx, "titogo"), map[string]any{}, token); status != http.StatusOK {
310 t.Fatalf("declare the app's own store = %d %s, want 200", status, body)
311 }
312}
313
314// An app that already declared a prefix reads those names in its code. An
315// instance store delivering CARLOS_STORE_BUCKET into the same app would be a
316// second vocabulary for one thing.
317func TestInstanceStoreInheritsTheAppsEnvPrefix(t *testing.T) {
318 fx, _, token, _ := privateStorageFixture(t, "code-priv-prefix")
319 enableInstances(t, fx, "titogo", privateManifest())
320 if status, out := postRaw(t, storeURL(fx, "titogo"), map[string]any{"env_prefix": "JELLY_S3"}, token); status != http.StatusOK {
321 t.Fatalf("declare the app store = %d %v", status, out)
322 }
323
324 if status, out := createInstance(t, fx, token, `{"host":"acme.go.tito.io","region":"ie"}`); status != http.StatusCreated {
325 t.Fatalf("create = %d %s", status, out)
326 }
327 vars := envVars(t, fx, "acme")
328 if _, ok := vars["JELLY_S3_BUCKET"]; !ok {
329 t.Fatalf("acme bundle = %v, want the app's own JELLY_S3 prefix", varNames(vars))
330 }
331 if _, ok := vars["CARLOS_STORE_BUCKET"]; ok {
332 t.Fatalf("acme bundle also carries the default prefix: %v", varNames(vars))
333 }
334}
335
336func varNames(m map[string]appconfig.Var) []string {
337 out := make([]string, 0, len(m))
338 for k := range m {
339 out = append(out, k)
340 }
341 return out
342}
343
344// Declaring the policy is refused in the two states where it could only ever
345// be a trap — a deployment with no object storage, and an account nobody
346// approved. It fails once here, to the person, instead of once per signup in
347// a door whose caller is a machine.
348func TestEnableRefusesAPolicyItCannotHonour(t *testing.T) {
349 t.Run("no backend", func(t *testing.T) {
350 fx, _, token, _ := privateStorageFixture(t, "code-priv-enable-nobackend")
351 fx.srv.cfg.AppsDomain = "oncarlos.test"
352 fx.srv.cfg.StoreProvisioner = nil
353 status, _, out := rawReq(t, http.MethodPut, enableURL(fx, "bab", "titogo"), token, `{"private_storage":true}`)
354 if status != http.StatusServiceUnavailable {
355 t.Fatalf("enable = %d %s, want 503", status, out)
356 }
357 if !strings.Contains(out, "CARLOS_STORE_BACKEND") {
358 t.Errorf("refusal %q does not name the variable", out)
359 }
360 assertNoPolicy(t, fx)
361 })
362 t.Run("unapproved account", func(t *testing.T) {
363 fx, token := instanceFixture(t, "code-priv-enable-unapproved")
364 fx.srv.cfg.AppsDomain = "oncarlos.test"
365 fx.srv.cfg.StoreProvisioner = objstore.NewMemProvisioner("eu-west-1")
366 status, _, out := rawReq(t, http.MethodPut, enableURL(fx, "bab", "titogo"), token, `{"private_storage":true}`)
367 if status != http.StatusForbidden {
368 t.Fatalf("enable = %d %s, want 403", status, out)
369 }
370 if !strings.Contains(out, "carlos store apply") {
371 t.Errorf("refusal %q does not name the way out", out)
372 }
373 assertNoPolicy(t, fx)
374 })
375}
376
377func assertNoPolicy(t *testing.T, fx *consoleFixture) {
378 t.Helper()
379 m, err := provision.GetManifest(context.Background(), fx.rel, "bab", "titogo")
380 if err == nil && m.PrivateStorage {
381 t.Fatal("a refused enable wrote the policy anyway")
382 }
383}
384
385// Turning it OFF is never refused, and the asymmetry is the point: an
386// account whose entitlement was later declined must be able to get its
387// create door working again without waiting on an operator.
388func TestEnableTurningPrivateStorageOffIsNeverRefused(t *testing.T) {
389 fx, token := instanceFixture(t, "code-priv-enable-off")
390 fx.srv.cfg.AppsDomain = "oncarlos.test"
391 enableInstances(t, fx, "titogo", provision.Manifest{
392 Schema: 1, App: "titogo", Domain: "bab.oncarlos.test", PrivateStorage: true})
393 // No backend, no entitlement — the state the ON path refuses in.
394 fx.srv.cfg.StoreProvisioner = nil
395
396 status, _, out := rawReq(t, http.MethodPut, enableURL(fx, "bab", "titogo"), token, `{"private_storage":false}`)
397 if status != http.StatusOK {
398 t.Fatalf("enable off = %d %s, want 200", status, out)
399 }
400 m, err := provision.GetManifest(context.Background(), fx.rel, "bab", "titogo")
401 if err != nil {
402 t.Fatal(err)
403 }
404 if m.PrivateStorage {
405 t.Fatal("the policy survived being turned off")
406 }
407}
408
409// An enable that never mentions storage preserves the policy — the same
410// absent-vs-explicit split channel and seed already have. A `carlos
411// instances enable -channel beta` that silently unset a tenant's isolation
412// is the failure this pins.
413func TestEnablePreservesPrivateStorage(t *testing.T) {
414 fx, _, token, _ := privateStorageFixture(t, "code-priv-enable-preserve")
415 fx.srv.cfg.AppsDomain = "oncarlos.test"
416 enableInstances(t, fx, "titogo", provision.Manifest{
417 Schema: 1, App: "titogo", Domain: "bab.oncarlos.test", PrivateStorage: true})
418
419 status, _, out := rawReq(t, http.MethodPut, enableURL(fx, "bab", "titogo"), token, `{"channel":"beta"}`)
420 if status != http.StatusOK {
421 t.Fatalf("enable = %d %s, want 200", status, out)
422 }
423 var res struct {
424 Channel string `json:"channel"`
425 PrivateStorage bool `json:"private_storage"`
426 }
427 if err := json.Unmarshal([]byte(out), &res); err != nil {
428 t.Fatal(err)
429 }
430 if res.Channel != "beta" || !res.PrivateStorage {
431 t.Fatalf("reply = %+v, want channel beta with the policy intact", res)
432 }
433 m, err := provision.GetManifest(context.Background(), fx.rel, "bab", "titogo")
434 if err != nil {
435 t.Fatal(err)
436 }
437 if !m.PrivateStorage {
438 t.Fatal("a channel-only enable dropped the private-storage policy")
439 }
440}
441
442// Declaring the policy stays a PERSON's act — the wall that makes this a
443// better answer than widening the instance role. An app token creates
444// instances all day and cannot write the manifest they are created under.
445func TestPrivateStoragePolicyStaysAHumanAct(t *testing.T) {
446 fx, _, token, _ := privateStorageFixture(t, "code-priv-human")
447 fx.srv.cfg.AppsDomain = "oncarlos.test"
448 appToken := mintInstanceToken(t, fx, token, "titogo")
449
450 status, _, out := rawReq(t, http.MethodPut, enableURL(fx, "bab", "titogo"), appToken, `{"private_storage":true}`)
451 if status != http.StatusForbidden && status != http.StatusUnauthorized {
452 t.Fatalf("app token enable = %d %s, want a refusal", status, out)
453 }
454 assertNoPolicy(t, fx)
455}
456
457// mintInstanceToken is `carlos instances token` — the service credential a
458// signup path holds.
459func mintInstanceToken(t *testing.T, fx *consoleFixture, personToken, app string) string {
460 t.Helper()
461 u := fx.ts.URL + "/api/cli/apps/bab/" + app + "/instance-token"
462 status, _, out := rawReq(t, http.MethodPost, u, personToken, `{}`)
463 if status != http.StatusCreated {
464 t.Fatalf("mint instance token = %d %s", status, out)
465 }
466 var res struct {
467 Token string `json:"token"`
468 }
469 if err := json.Unmarshal([]byte(out), &res); err != nil {
470 t.Fatal(err)
471 }
472 return res.Token
473}
474
475// The ceiling in force, and the two ways a deployment names it. Zero in the
476// config is "not configured", never "no stores allowed" — an unset variable
477// turning an app into one that can hold no store at all is the wrong
478// direction for a default to fail in.
479//
480// The number itself is pinned because it is a JUDGEMENT about AWS's quota
481// rather than an arithmetic consequence of anything: IAM allows 5,000 users
482// per cloud account, so one app's share is a fifth of that. An earlier draft
483// said 100, which would have refused an ordinary managed product at its
484// hundredth customer while protecting nothing (James, 2026-09-23). A future
485// change to the constant should be a decision someone took, not a drift.
486func TestStoreCeilingDefault(t *testing.T) {
487 s := &Server{}
488 if got := s.storeMaxPerApp(); got != 1000 {
489 t.Errorf("unconfigured ceiling = %d, want 1000", got)
490 }
491 s.cfg.StoreMaxPerApp = 25
492 if got := s.storeMaxPerApp(); got != 25 {
493 t.Errorf("configured ceiling = %d, want 25", got)
494 }
495}
496