settings-2 final review: the fleet store door serves a box its own account's promotion records
Astra final A3, ruling R17. aclRead grants GET of exactly releases/<sqid>/<app>/promoted/<version> for the box's own account: five segments, a valid app and version, nothing else under releases/. Listing (aclDir, aclListScope) and writes (aclWrite) are unchanged. Without it every record read as absent through the door, so a record-only build could never stage on a fleet box. Tests: ACL matrix allow/deny rows (another sqid, a sixth segment, a missing version, the pointer-digests marker, writes, deletes, traversal), a listing-denial test, and an end-to-end test through the real edge door and fleetapi client: ReadPromotionRecord and AuthorizedManifest read an own-account record, another account's is refused, and a replaced manifest is ErrManifestDigest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+135
−1
internal/edge/fleetapi_e2e_test.go+64 −0internal/edge/storedoor.go+28 −1internal/edge/storedoor_test.go+43 −0
diff --git a/internal/edge/fleetapi_e2e_test.go b/internal/edge/fleetapi_e2e_test.go| index 9f6e5e6d6..f6ab8c353 100644 |
| --- a/internal/edge/fleetapi_e2e_test.go |
| +++ b/internal/edge/fleetapi_e2e_test.go |
| @@ -619,3 +619,67 @@ func TestFleetBoxCatchallYieldsToAnotherTenantsClaim(t *testing.T) { |
| t.Fatalf("aliases = %+v, want acme.%s derived for the unclaimed name", aliases, parent) |
| } |
| } |
| + |
| +// Settings spec §4.1, ruling R17: a fleet box reads its own account's |
| +// promotion records through the store door, so a build authorized by record |
| +// alone (no pointer names it) stages on a fleet box, and a manifest that does |
| +// not hash to the record's digest is refused. Another account's record and a |
| +// listing of the records stay out of reach, and a box cannot write one. |
| +func TestFleetBoxReadsItsOwnPromotionRecords(t *testing.T) { |
| + ctx := context.Background() |
| + w := newFleetDoorWorld(t) |
| + const version = "v7" |
| + for _, acct := range []string{w.sqid, w.other} { |
| + if err := release.Ship(ctx, w.store, acct, release.Manifest{ |
| + App: fapiApp, Version: version, Kind: "binary", |
| + Artifacts: map[string]release.Artifact{fapiPlatform: {}}, |
| + }, map[string][]byte{fapiPlatform: []byte("authorized bytes")}); err != nil { |
| + t.Fatalf("Ship(%s): %v", acct, err) |
| + } |
| + _, raw, err := release.GetManifestRaw(ctx, w.store, acct, fapiApp, version) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := release.RootSigner(w.priv).WritePromotionRecord(ctx, w.store, release.PromotionRecord{ |
| + Account: acct, App: fapiApp, Version: version, ManifestDigest: release.ManifestDigest(raw), Authorized: true, |
| + }); err != nil { |
| + t.Fatalf("WritePromotionRecord(%s): %v", acct, err) |
| + } |
| + } |
| + _, raw, err := release.GetManifestRaw(ctx, w.store, w.sqid, fapiApp, version) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + |
| + rec, err := release.ReadPromotionRecord(ctx, w.boxA, w.pub, w.sqid, fapiApp, version) |
| + if err != nil || rec.ManifestDigest != release.ManifestDigest(raw) { |
| + t.Fatalf("ReadPromotionRecord through the door = %+v, %v; want the record signing %s", rec, err, release.ManifestDigest(raw)) |
| + } |
| + if m, err := release.AuthorizedManifest(ctx, w.boxA, w.pub, w.sqid, fapiApp, version); err != nil || m.Version != version { |
| + t.Fatalf("AuthorizedManifest through the door = %+v, %v; want the record-only authorized %s", m, err, version) |
| + } |
| + |
| + if _, err := release.ReadPromotionRecord(ctx, w.boxA, w.pub, w.other, fapiApp, version); err == nil { |
| + t.Fatal("a fleet box read another account's promotion record through the door") |
| + } |
| + keys, _ := w.boxA.List(ctx, "releases/"+w.sqid+"/") |
| + for _, k := range keys { |
| + if strings.HasPrefix(k, "releases/") { |
| + t.Fatalf("a fleet box listed promotion records through the door: %v", keys) |
| + } |
| + } |
| + if _, err := w.boxA.Put(ctx, release.PromotionRecordKey(w.sqid, fapiApp, "v8"), []byte("{}"), ""); err == nil { |
| + t.Fatal("a fleet box wrote a promotion record through the door") |
| + } |
| + |
| + _, etag, err := w.store.Get(ctx, release.ManifestKey(w.sqid, fapiApp, version)) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := w.store.Put(ctx, release.ManifestKey(w.sqid, fapiApp, version), append(raw, '\n'), etag); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := release.AuthorizedManifest(ctx, w.boxA, w.pub, w.sqid, fapiApp, version); !errors.Is(err, release.ErrManifestDigest) { |
| + t.Fatalf("AuthorizedManifest over a replaced manifest = %v; want ErrManifestDigest", err) |
| + } |
| +} |
diff --git a/internal/edge/storedoor.go b/internal/edge/storedoor.go| index ca7c447d5..e341b9712 100644 |
| --- a/internal/edge/storedoor.go |
| +++ b/internal/edge/storedoor.go |
| @@ -380,6 +380,9 @@ func canonicalPrefix(raw string) (string, bool) { |
| // the box (knownAccounts -> ParseRecordKey) |
| // apps/<sqid>/** its owning account's whole app estate |
| // fleets/<fleet>/** its own corner |
| +// releases/<sqid>/<app>/promoted/<version> |
| +// a promotion record of its owning account's |
| +// build — see promotionRecordRead |
| // |
| // domains/ is NOT here, and is served by its own two paths instead |
| // (serveClaim, serveClaimListing): a box can list every claimed hostname, |
| @@ -411,6 +414,20 @@ func canonicalPrefix(raw string) (string, bool) { |
| // NAMES are claimed at all, whoever by, so the listing stays whole |
| // (serveClaimListing). Write stays console-only: aclWrite never answers true |
| // for a domains/ key. |
| +// |
| +// A promotion record (settings spec §4.1, release.PromotionRecordKey) is |
| +// granted because a box needs it and it holds nothing secret: it is the |
| +// root-signed digest of one of the account's own builds, readable by anyone |
| +// holding the public key it verifies under. HeldAuthorized reads it before |
| +// staging a held release, and AuthorizedManifest before any install the |
| +// settings add. Without the grant every record reads as absent through the |
| +// door, so a build authorized by record alone could never stage on a fleet |
| +// box, and one a legacy pointer still names would fall back to that |
| +// pointer's weaker word. The grant is one object shape, not the prefix the |
| +// records sit under: releases/ also holds the account's pointer digest |
| +// marker, which a box has no use for. Listing is not granted (aclDir and |
| +// aclListScope are unchanged), and write stays console-only: aclWrite never |
| +// answers true for a releases/ key. |
| func aclRead(sqid, fleet, key string) bool { |
| if sqid == "" || fleet == "" { |
| return false |
| @@ -419,7 +436,7 @@ func aclRead(sqid, fleet, key string) bool { |
| case "control/serial", "bootstrap/carlos", release.LayoutIntentKey(): |
| return true |
| } |
| - if systemReadKey(key) { |
| + if systemReadKey(key) || promotionRecordRead(sqid, key) { |
| return true |
| } |
| return strings.HasPrefix(key, "accounts/"+sqid+"/") || |
| @@ -427,6 +444,16 @@ func aclRead(sqid, fleet, key string) bool { |
| strings.HasPrefix(key, "fleets/"+fleet+"/") |
| } |
| +// promotionRecordRead is the one releases/ shape aclRead grants: exactly |
| +// releases/<sqid>/<app>/promoted/<version>, five segments, the box's own |
| +// account, and a valid app and version name (release.PromotionRecordKey's |
| +// inputs). canonicalKey has already refused any "." or ".." segment. |
| +func promotionRecordRead(sqid, key string) bool { |
| + seg := strings.Split(key, "/") |
| + return len(seg) == 5 && seg[0] == "releases" && seg[1] == sqid && |
| + release.ValidApp(seg[2]) && seg[3] == "promoted" && release.ValidVersion(seg[4]) |
| +} |
| + |
| // systemReadKey is the two shapes the system-update spec (§3) grants every |
| // fleet box: its platform channel pointer and a mirrored platform artifact. |
| // Shapes, not prefixes — exactly three segments each, so nothing else that |
diff --git a/internal/edge/storedoor_test.go b/internal/edge/storedoor_test.go| index b1a89f5e2..c67b38de6 100644 |
| --- a/internal/edge/storedoor_test.go |
| +++ b/internal/edge/storedoor_test.go |
| @@ -178,6 +178,24 @@ func TestStoreDoorACLMatrix(t *testing.T) { |
| {"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"}, |
| @@ -377,6 +395,31 @@ func TestACLDirIsPerTenant(t *testing.T) { |
| } |
| } |
| +// 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 |