package edge // Task 2: the store door — the ACL is the whole security story, so it is // tested as a matrix BEFORE any HTTP shape exists: every key class from the // spec's #73 inventory × operation × own/other tenancy, with the exact keys a // real box uses. A row that flips here is a tenancy breach, not a test nit. import ( "context" "encoding/json" "errors" "fmt" "go/ast" "go/parser" "go/token" "io" "net/http" "net/http/httptest" "os" "path/filepath" "slices" "strings" "testing" "github.com/carlosframework/platform/internal/domains" "github.com/carlosframework/platform/internal/plans" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "time" ) // The two identities every row is judged against: fleet "pi" owned by account // sqid "ab12". "OTHER"/"zz99" is any other tenant. const ( testSqid = "ab12" testFleet = "pi" ) type aclOp int const ( opRead aclOp = iota opWrite opDelete ) func (o aclOp) String() string { switch o { case opRead: return "read" case opWrite: return "write" default: return "delete" } } // aclAllows is the door's decision for one (key, op), canonicalization // included — exactly the order the handler runs it in: canonicalKey first, so // a traversal key is refused before any ACL string ever sees it. func aclAllows(key string, op aclOp) bool { k, ok := canonicalKey(key) if !ok { return false } if op == opRead { return aclRead(testSqid, testFleet, k) } return aclWrite(testSqid, testFleet, k) } func TestEntitlementsAreNotReadableThroughTheEdgeDoor(t *testing.T) { for _, key := range []string{ "store/entitlements/" + testSqid + ".json", "store/entitlements/other.json", "accounts/" + testSqid + "/../../store/entitlements/" + testSqid + ".json", "apps/" + testSqid + "/../../store/entitlements/" + testSqid + ".json", } { for _, op := range []aclOp{opRead, opWrite, opDelete} { if aclAllows(key, op) { t.Errorf("%s allowed for %q", op, key) } } } for _, prefix := range []string{"store/", "store/entitlements/", "store/entitlements/" + testSqid} { if _, ok := aclListScope(testSqid, testFleet, prefix); ok { t.Errorf("listing allowed for %q", prefix) } if aclDir(testSqid, testFleet, prefix) { t.Errorf("directory visible: %q", prefix) } } } func TestStoreDoorACLMatrix(t *testing.T) { rows := []struct { key string op aclOp want bool why string }{ // --- the shared platform reads the spec allowlists (F3, F7) --- {"control/serial", opRead, true, "the 2s convergence poll"}, {"control/serial", opWrite, false, "a box must never bump the platform serial"}, {"control/serial", opDelete, false, "nor delete it"}, {"bootstrap/carlos", opRead, true, "F7: the sanctioned self-update path"}, {"bootstrap/carlos", opWrite, false, "a box must never publish the platform binary"}, {"control/serial2", opRead, false, "the allowlist is exact, not a prefix"}, {"control/", opRead, false, "no listing-shaped key stands in for the allowlist"}, {"bootstrap/carlos.sig", opRead, false, "exact key only"}, {"control/system/router/stable.json", opRead, true, "the platform channel pointer (system update spec §3)"}, {"control/system/router/canary.json", opRead, true, "…and the canary one"}, {"control/system/router/stable.json", opWrite, false, "a box never writes a pointer"}, {"control/system/router", opRead, false, "the pointer shape is exactly three segments ending .json"}, {"control/system/router/stable.json/x", opRead, false, "…not four"}, {"control/system/router/stable.txt", opRead, false, "…and .json"}, {"bootstrap/router/v0.18.0/carlos-linux-arm64", opRead, true, "a mirrored platform artifact"}, {"bootstrap/router/v0.18.0/carlos-linux-arm64", opWrite, false, "a box must never publish the platform binary"}, {"bootstrap/router/v0.18.0", opRead, false, "the artifact shape is exactly three segments"}, {"bootstrap/router/v0.18.0/a/b", opRead, false, "…not four"}, {"bootstrap/router.sig", opRead, false, "nothing else under bootstrap/"}, {"bootstrap/router/../../x", opRead, false, "traversal is refused by canonicalKey before the ACL"}, {"control/system/../serial", opRead, false, "…on both prefixes"}, {release.LayoutIntentKey(), opRead, true, "every box needs the fleet's layout intent to converge"}, {release.LayoutIntentKey(), opWrite, false, "a box must never author the fleet's layout intent"}, {release.LayoutIntentKey(), opDelete, false, "nor delete it"}, {"control/layout.json2", opRead, false, "the allowlist is exact, not a prefix"}, // --- own account estate --- // The account manifest is accounts//account.json // (domains.AccountManifestKey), NOT accounts/.json: a box that // cannot read it sees zero accounts, and provision.Reconcile then // retires every provisioned route on the box. {"accounts/ab12/account.json", opRead, true, "own account manifest"}, {"accounts/zz99/account.json", opRead, false, "another tenant's account manifest"}, {"accounts/ab12/account.json", opWrite, false, "reads only, even of its own account"}, {"accounts/ab12.json", opRead, false, "the manifest is a directory deeper; this key is not the account"}, {"accounts/ab12x/account.json", opRead, false, "sqid prefix must not match a longer sqid"}, {"accounts/ab12", opRead, false, "the account prefix itself is not an object"}, {"apps/ab12/web/channels/stable.json", opRead, true, "own app channel pointer"}, {"apps/ab12/web/releases/v1.2.3/manifest.json", opRead, true, "own release manifest"}, {"apps/ab12/web/releases/v1.2.3/app.tar.gz", opRead, true, "own artifact (F8: streamed, not presigned)"}, {"apps/ab12/web/config/production.json", opRead, true, "own config bundle"}, {"apps/ab12/web/restart-gen.json", opRead, true, "own restart generation"}, {"apps/ab12/web/channels/stable.json", opWrite, false, "a box never promotes"}, {"apps/ab12/web/releases/v1.2.3/app.tar.gz", opDelete, false, "retention is not a box's job (F6)"}, // --- own account's app LOGS: the second (and only other) write scope --- // applog.ChunkKey's shape, which is what logshipLoop PUTs and what its // hourly prune DELETEs. The rows below the allows are the regression // pins that keep this from becoming a blanket apps//** grant. {"apps/ab12/web/logs/box-a/2026081108/0000000000000001-42.jsonl.gz", opWrite, true, "own app's log chunk"}, {"apps/ab12/web/logs/box-a/2026081108/0000000000000001-42.jsonl.gz", opDelete, true, "logship prunes its own subtree hourly"}, {"apps/ab12/web/logs/box-a/2026081108/0000000000000001-42.jsonl.gz", opRead, true, "reads already covered by the app estate"}, {"apps/zz99/web/logs/box-a/2026081108/x.jsonl.gz", opWrite, false, "another account's app logs"}, {"apps/zz99/web/logs/box-a/2026081108/x.jsonl.gz", opDelete, false, "nor delete them"}, {"apps/ab12x/web/logs/box-a/x.jsonl.gz", opWrite, false, "sqid prefix must not match a longer sqid"}, {"apps/ab12/web/instances/h.example.json", opWrite, false, "REGRESSION: the logs grant must not mint instance records"}, {"apps/ab12/web/config/production.json", opWrite, false, "REGRESSION: nor rewrite config"}, {"apps/ab12/web/restart-gen.json", opWrite, false, "REGRESSION: nor request its own restarts"}, {"apps/ab12/web/releases/v1.2.3/manifest.json", opWrite, false, "REGRESSION: nor publish a release"}, {"apps/ab12/logs/box-a/x.jsonl.gz", opWrite, false, "logs must sit under an app segment, not the account root"}, {"apps/ab12/web/logs", opWrite, false, "the logs prefix itself is not an object"}, {"apps/ab12/web/logs/", opWrite, false, "nor is the bare prefix with a slash"}, {"apps/ab12/web/channels/logs/stable.json", opWrite, false, "\"logs\" deeper in the key grants nothing"}, {"apps/ab12/weblogs/box-a/x.jsonl.gz", opWrite, false, "the segment is logs/, not a suffix of the app name"}, {"apps/ab12/web/logs/../../zz99/web/logs/x.jsonl.gz", opWrite, false, "traversal out of the logs subtree"}, {"apps/ab12/web/logs/../channels/stable.json", opWrite, false, "traversal out of logs into channels"}, // --- sibling chunk families (errors-and-analytics spec §3.2) --- {"apps/ab12/web/errors/box-a/2026090710/0000000000000001-42.jsonl.gz", opWrite, true, "own app's error chunk"}, {"apps/ab12/web/errors/box-a/2026090710/0000000000000001-42.jsonl.gz", opDelete, true, "errship prunes its own subtree hourly"}, {"apps/ab12/web/analytics/box-a/days/2026-09-07.json", opWrite, true, "own app's analytics day file"}, {"apps/zz99/web/errors/box-a/x.jsonl.gz", opWrite, false, "another account's error chunks"}, {"apps/ab12/errors/box-a/x.jsonl.gz", opWrite, false, "errors must sit under an app segment"}, {"apps/ab12/web/errors", opWrite, false, "the bare family segment is not a chunk"}, {"apps/ab12/web/errors/", opWrite, false, "nor with a slash"}, {"apps/ab12/web/other/box-a/x.jsonl.gz", opWrite, false, "the family list is exact: logs, errors, analytics"}, {"apps/ab12/web/channels/errors/stable.json", opWrite, false, "\"errors\" deeper in the key grants nothing"}, {"apps/ab12/web/errorsx/box-a/x.jsonl.gz", opWrite, false, "the segment is errors/, not a prefix of a longer name"}, // --- own account's promotion records (settings spec §4.1, ruling R17) --- // Signed digests of the account's own builds, which HeldAuthorized // reads on a fleet box. One shape, exactly five segments; nothing else // under releases/ and never a write. {"releases/ab12/web/promoted/v1.2.3", opRead, true, "own account's promotion record"}, {"releases/zz99/web/promoted/v1.2.3", opRead, false, "another account's promotion record"}, {"releases/ab12x/web/promoted/v1.2.3", opRead, false, "sqid must match exactly, not as a prefix"}, {"releases/ab12/web/promoted/v1.2.3/x", opRead, false, "a sixth segment"}, {"releases/ab12/web/promoted", opRead, false, "a missing version"}, {"releases/ab12/web/promoted/", opRead, false, "an empty version"}, {"releases/ab12/pointer-digests", opRead, false, "the account's digest marker is not granted"}, {"releases/ab12/web/other/v1.2.3", opRead, false, "segment 3 is exactly promoted"}, {"releases/ab12/Web!/promoted/v1.2.3", opRead, false, "an invalid app"}, {"releases/ab12/web/promoted/v1~bad", opRead, false, "an invalid version"}, {"releases/ab12/web/promoted/v1.2.3", opWrite, false, "a box never writes a promotion record"}, {"releases/ab12/web/promoted/v1.2.3", opDelete, false, "nor deletes one"}, {"releases/ab12/web/promoted/../../zz99/web/promoted/v1", opRead, false, "traversal out of own account"}, // --- other tenants --- {"apps/zz99/web/channels/stable.json", opRead, false, "another account's app"}, {"apps/ab12x/web/channels/stable.json", opRead, false, "sqid prefix must not match a longer sqid"}, {"apps/ab1/web/channels/stable.json", opRead, false, "nor a shorter one"}, {"apps/ab12", opRead, false, "the account prefix itself is not an object"}, // --- own fleet corner: the only writable place there is --- {"fleets/pi/bindings/app.example.json", opRead, true, "the binding takeover guard reads before it CASes"}, {"fleets/pi/bindings/app.example.json", opWrite, true, "fleet bindings are the box's own message"}, {"fleets/pi/status/boxes/box-a.json", opWrite, true, "own heartbeat"}, {"fleets/pi/metrics/boxes/box-a/raw/1699.json", opWrite, true, "own metrics"}, {"fleets/pi/metrics/boxes/box-a/raw/1699.json", opDelete, true, "own 48h metrics prune"}, {"fleets/other/status/boxes/box-a.json", opRead, false, "another fleet's corner"}, {"fleets/other/status/boxes/box-a.json", opWrite, false, "and it cannot write there"}, {"fleets/other/status/boxes/box-a.json", opDelete, false, "nor delete there"}, {"fleets/pix/status/boxes/box-a.json", opWrite, false, "fleet prefix must not match a longer fleet name"}, {"fleets/pi", opWrite, false, "the fleet prefix itself is not an object"}, // --- domains/ claims: never through aclRead (#215) — serveClaim // decides per claim body, own account and verified only (see // TestStoreDoorClaimsAreOwnAccountAndRedacted); console-only write --- {"domains/app.example.com.json", opRead, false, "claims are served by serveClaim, not the prefix ACL"}, {"domains/app.example.com.json", opWrite, false, "boxes read claims, only the console writes them"}, {"domains/app.example.com.json", opDelete, false, "nor delete them"}, {"domains", opRead, false, "an object AT the bare name, not the claims prefix"}, // --- the self-serve storage entitlement is console-only (#381) --- // store/entitlements/.json carries the applicant's note, the // deciding operator's address and the decline REASON — an operator's // commentary about a tenant. It lives outside accounts/ precisely so // that no fleet credential can read it; had it gone to the obvious // accounts//store.json, the account's own boxes could. {"store/entitlements/" + testSqid + ".json", opRead, false, "a box never reads its account's entitlement, own sqid or not"}, {"store/entitlements/zz99.json", opRead, false, "still less another account's"}, {"store/entitlements/" + testSqid + ".json", opWrite, false, "and never writes one"}, {"store/entitlements/" + testSqid + ".json", opDelete, false, "nor deletes one"}, {"store/", opRead, false, "no listing-shaped key reaches the prefix either"}, {"store", opRead, false, "nor an object at the bare name"}, // --- shapes canonicalKey must kill before the ACL is consulted --- {"apps/ab12/../zz99/web/channels/stable.json", opRead, false, "traversal out of own account"}, {"fleets/pi/../other/status/boxes/box-a.json", opWrite, false, "traversal out of own fleet"}, {"../etc/passwd", opRead, false, "traversal below the keyspace"}, {"/control/serial", opRead, false, "leading slash"}, {"apps//ab12/web/channels/stable.json", opRead, false, "empty segment"}, {"apps/ab12/./web/channels/stable.json", opRead, false, "dot segment"}, {"", opRead, false, "empty key"}, } for _, row := range rows { got := aclAllows(row.key, row.op) if got != row.want { t.Errorf("acl %s %q = %v, want %v (%s)", row.op, row.key, got, row.want, row.why) } } } // TestAclReadLayoutIntentNeedsBothIdentities pins the same empty-sqid, // empty-fleet guard aclRead already applies to every other key: an // authenticated identity is required before the allowlist is even // consulted, so a request that never resolved a fleet row cannot read the // layout intent either. func TestAclReadLayoutIntentNeedsBothIdentities(t *testing.T) { key := release.LayoutIntentKey() if !aclRead(testSqid, testFleet, key) { t.Errorf("aclRead(%q, %q, %q) = false, want true", testSqid, testFleet, key) } if aclRead("", testFleet, key) { t.Errorf("aclRead(\"\", %q, %q) = true, want false", testFleet, key) } if aclRead(testSqid, "", key) { t.Errorf("aclRead(%q, \"\", %q) = true, want false", testSqid, key) } if aclWrite(testSqid, testFleet, key) { t.Errorf("aclWrite(%q, %q, %q) = true, want false", testSqid, testFleet, key) } } // TestCanonicalKeyRefusesShapes pins the pre-ACL gate on its own: the ACL is // prefix-string exact, which is only safe because nothing that could re-point // a prefix ever reaches it. func TestCanonicalKeyRefusesShapes(t *testing.T) { bad := []string{ "", "/apps/ab12/x", "apps//ab12/x", "apps/ab12/../zz99/x", "..", "../x", "apps/ab12/x/..", "apps/./ab12/x", "apps/ab12/x/", "apps/ab12/\x00x", "apps/ab12/x\ny", strings.Repeat("a", 1025), } for _, k := range bad { if got, ok := canonicalKey(k); ok { t.Errorf("canonicalKey(%q) = (%q, true), want refused", k, got) } } good := []string{ "control/serial", "bootstrap/carlos", "accounts/ab12.json", "apps/ab12/web/releases/v1.2.3/app.tar.gz", "fleets/pi/bindings/app.example.json", strings.Repeat("a", 1024), } for _, k := range good { got, ok := canonicalKey(k) if !ok || got != k { t.Errorf("canonicalKey(%q) = (%q, %v), want (%q, true)", k, got, ok, k) } } } // TestACLListScope: a listing is scope-MAPPED, never scope-checked — asking // for "apps/" gets you your own account's subtree, not a refusal, because // that is what makes an unmodified release.Store caller work through the door. func TestACLListScope(t *testing.T) { cases := []struct { prefix string want string ok bool }{ {"apps/", "apps/ab12/", true}, {"apps", "apps/ab12/", true}, {"apps/ab12", "apps/ab12/", true}, {"apps/ab12/", "apps/ab12/", true}, {"apps/ab12/web/releases/", "apps/ab12/web/releases/", true}, {"apps/zz99/", "", false}, {"apps/ab1", "", false}, {"apps/ab12x/", "", false}, {"accounts/", "accounts/", true}, {"accounts", "accounts/", true}, {"accounts/zz99.json", "", false}, // DELIBERATE, and the trap worth naming: aclDir is STRICTER than // aclRead here. aclRead admits accounts//** as an object // read, but the listing scope map admits only the bare, SHARED // accounts/ prefix — a caller narrowing its listing to its own // account directory is refused. That is not an oversight to "fix": // every real caller of this prefix lists accounts/ whole // (provision.knownAccounts, domains.Reconcile, console.materialize) // and takes the per-tenant filter serveListing applies, so a // per-tenant mapping row here would widen the scope map for no // caller. If one ever appears, adding the row is the deliberate act // this pin makes visible. {"accounts/" + testSqid + "/", "", false}, {"accounts/" + testSqid, "", false}, // domains/ (#151): the OTHER shared scope — every claim is // cross-tenant-readable, so (unlike apps/ and fleets/) there is no // per-tenant subtree to narrow into; only the bare prefix maps. {"domains/", "domains/", true}, {"domains", "domains/", true}, {"domains/app.example.com.json", "", false}, {"fleets/", "fleets/pi/", true}, {"fleets/pi/status/boxes/", "fleets/pi/status/boxes/", true}, {"fleets/other/", "", false}, {"control/", "", false}, {"bootstrap/", "", false}, {"", "", false}, {"status/", "", false}, } for _, c := range cases { got, ok := aclListScope(testSqid, testFleet, c.prefix) if got != c.want || ok != c.ok { t.Errorf("aclListScope(%q) = (%q, %v), want (%q, %v)", c.prefix, got, ok, c.want, c.ok) } } } // TestACLDirIsPerTenant pins the ListDirs predicate on its own. A child // prefix cannot be judged by aclRead (it is a directory, not an object), and // judging it by "is it under the prefix we listed" would enumerate every // account on the platform the moment accounts/ is listed. func TestACLDirIsPerTenant(t *testing.T) { cases := map[string]bool{ "accounts/" + testSqid + "/": true, "accounts/zz99/": false, "accounts/" + testSqid + "x/": false, "accounts/": false, "apps/" + testSqid + "/web/": true, "apps/zz99/web/": false, "apps/" + testSqid + "x/web/": false, "fleets/" + testFleet + "/status/": true, "fleets/other/status/": false, "fleets/" + testFleet + "x/y/": false, "control/": false, // domains/ (#215): no longer a shared read scope; claims are flat // and serveClaimListing answers dirs with nothing. "domains/anything/": false, } for dir, want := range cases { if got := aclDir(testSqid, testFleet, dir); got != want { t.Errorf("aclDir(%q) = %v, want %v", dir, got, want) } } } // The promotion-record grant (ruling R17) is a read of one exact object // shape: no prefix under releases/ maps to a listing scope or shows as a // directory, the box's own account's included. func TestPromotionRecordsAreNotListable(t *testing.T) { for _, prefix := range []string{ "releases/", "releases", "releases/" + testSqid + "/", "releases/" + testSqid + "/web/", "releases/" + testSqid + "/web/promoted/", "releases/" + testSqid + "/web/promoted", } { if got, ok := aclListScope(testSqid, testFleet, prefix); ok { t.Errorf("aclListScope(%q) = (%q, true), want refused", prefix, got) } if aclDir(testSqid, testFleet, strings.TrimSuffix(prefix, "/")+"/") { t.Errorf("aclDir(%q) = true, want false", prefix) } } key := "releases/" + testSqid + "/web/promoted/v1" if aclRead("", testFleet, key) || aclRead(testSqid, "", key) { t.Errorf("aclRead(%q) without both identities = true, want false", key) } } // ---- handler tests ------------------------------------------------------ // The credentials every handler test runs with: a fleet channel // "/