| 1 | package objstore |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "context" |
| 6 | "encoding/json" |
| 7 | "testing" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/release" |
| 11 | ) |
| 12 | |
| 13 | // A seed's mark (Declared.Seed) survives only on a store its own seed |
| 14 | // granted. Any other first grant of a marked record clears the mark in the |
| 15 | // same write as the grant block, so a seed's finish can never tear down a |
| 16 | // store another path made (seeded-instances spec §4 Storage). |
| 17 | func TestFirstGrantClearsAnotherSeedsMark(t *testing.T) { |
| 18 | const seedA, seedB = "sd_aaaaaaaaaaaaaaaaaaaaaaaaaa", "sd_bbbbbbbbbbbbbbbbbbbbbbbbbb" |
| 19 | for name, c := range map[string]struct { |
| 20 | mark, grantFor, want string |
| 21 | }{ |
| 22 | "an unseeded grant": {seedB, "", ""}, |
| 23 | "another seed's grant": {seedB, seedA, ""}, |
| 24 | "the marking seed's grant": {seedB, seedB, seedB}, |
| 25 | } { |
| 26 | t.Run(name, func(t *testing.T) { |
| 27 | ctx := context.Background() |
| 28 | s := release.NewDirStore(t.TempDir()) |
| 29 | _, pub := testSealKeypair(t) |
| 30 | at := time.Now().UTC().Add(-time.Hour) |
| 31 | decl := &Declared{EnvPrefix: DefaultEnvPrefix, At: at, By: "member@example.com", Seed: c.mark} |
| 32 | if _, err := PutIn(ctx, s, "bab", "shop", "canary", &Record{Declared: decl}, ""); err != nil { |
| 33 | t.Fatal(err) |
| 34 | } |
| 35 | rec, err := GrantInFor(ctx, s, NewMemProvisioner("eu-west-1"), testDeployment(), pub, |
| 36 | "bab", "shop", "canary", "op", GrantSourceOperator, "", c.grantFor, time.Now().UTC()) |
| 37 | if err != nil { |
| 38 | t.Fatal(err) |
| 39 | } |
| 40 | stored, _, err := GetIn(ctx, s, "bab", "shop", "canary") |
| 41 | if err != nil { |
| 42 | t.Fatal(err) |
| 43 | } |
| 44 | for _, r := range []*Record{rec, stored} { |
| 45 | if r.Declared.Seed != c.want { |
| 46 | t.Fatalf("Declared.Seed = %q, want %q", r.Declared.Seed, c.want) |
| 47 | } |
| 48 | if r.Declared.By != decl.By || !r.Declared.At.Equal(at) || r.Declared.EnvPrefix != decl.EnvPrefix { |
| 49 | t.Fatalf("declaration = %+v, want only the mark changed from %+v", r.Declared, decl) |
| 50 | } |
| 51 | } |
| 52 | }) |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | // A re-run over a store its seed already granted keeps the mark only when |
| 57 | // it is that seed's own re-run. An unseeded re-run (an operator grant, a |
| 58 | // provision, an ordinary create sharing the environment) or another seed's |
| 59 | // clears it, so the aborted seed's finish cannot tear down a store someone |
| 60 | // else now holds; its bucket leaks instead, the safe direction. |
| 61 | func TestGrantRerunClearsAMarkThatIsNotItsOwn(t *testing.T) { |
| 62 | const seedA, seedB = "sd_aaaaaaaaaaaaaaaaaaaaaaaaaa", "sd_bbbbbbbbbbbbbbbbbbbbbbbbbb" |
| 63 | for name, c := range map[string]struct{ rerunFor, want string }{ |
| 64 | "the same seed's re-run": {seedB, seedB}, |
| 65 | "an unseeded re-run": {"", ""}, |
| 66 | "another seed's re-run": {seedA, ""}, |
| 67 | } { |
| 68 | t.Run(name, func(t *testing.T) { |
| 69 | ctx := context.Background() |
| 70 | s := release.NewDirStore(t.TempDir()) |
| 71 | _, pub := testSealKeypair(t) |
| 72 | p := NewMemProvisioner("eu-west-1") |
| 73 | decl := &Declared{EnvPrefix: DefaultEnvPrefix, At: time.Now().UTC(), By: "member@example.com", Seed: seedB} |
| 74 | if _, err := PutIn(ctx, s, "bab", "shop", "canary", &Record{Declared: decl}, ""); err != nil { |
| 75 | t.Fatal(err) |
| 76 | } |
| 77 | first, err := GrantInFor(ctx, s, p, testDeployment(), pub, "bab", "shop", "canary", "op", GrantSourceOperator, "", seedB, time.Now().UTC()) |
| 78 | if err != nil { |
| 79 | t.Fatal(err) |
| 80 | } |
| 81 | rec, err := GrantInFor(ctx, s, p, testDeployment(), pub, "bab", "shop", "canary", "op", GrantSourceOperator, "", c.rerunFor, time.Now().UTC()) |
| 82 | if err != nil { |
| 83 | t.Fatal(err) |
| 84 | } |
| 85 | stored, _, err := GetIn(ctx, s, "bab", "shop", "canary") |
| 86 | if err != nil { |
| 87 | t.Fatal(err) |
| 88 | } |
| 89 | for _, r := range []*Record{rec, stored} { |
| 90 | if r.Declared.Seed != c.want { |
| 91 | t.Fatalf("Declared.Seed after the re-run = %q, want %q", r.Declared.Seed, c.want) |
| 92 | } |
| 93 | if r.Grant == nil || *r.Grant != *first.Grant || r.Provisioned == nil || r.Provisioned.KeyID != first.Provisioned.KeyID { |
| 94 | t.Fatalf("re-run changed the estate: grant %+v provisioned %+v, want %+v %+v", r.Grant, r.Provisioned, first.Grant, first.Provisioned) |
| 95 | } |
| 96 | } |
| 97 | }) |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | // A record with no mark is granted exactly as before: its declaration |
| 102 | // block comes out byte-identical, and no seed key appears. |
| 103 | func TestGrantLeavesAnUnmarkedDeclarationAsItWas(t *testing.T) { |
| 104 | ctx := context.Background() |
| 105 | s := release.NewDirStore(t.TempDir()) |
| 106 | _, pub := testSealKeypair(t) |
| 107 | decl := &Declared{EnvPrefix: "SHOP_FILES", At: time.Now().UTC().Add(-time.Hour), By: "member@example.com"} |
| 108 | if _, err := PutIn(ctx, s, "bab", "shop", "canary", &Record{Declared: decl}, ""); err != nil { |
| 109 | t.Fatal(err) |
| 110 | } |
| 111 | before, _ := json.Marshal(decl) |
| 112 | if _, err := GrantIn(ctx, s, NewMemProvisioner("eu-west-1"), testDeployment(), pub, "bab", "shop", "canary", "op", GrantSourceOperator, "", time.Now().UTC()); err != nil { |
| 113 | t.Fatal(err) |
| 114 | } |
| 115 | raw, _, err := s.Get(ctx, KeyIn("bab", "shop", "canary")) |
| 116 | if err != nil { |
| 117 | t.Fatal(err) |
| 118 | } |
| 119 | var stored struct { |
| 120 | Declared json.RawMessage `json:"declared"` |
| 121 | } |
| 122 | if err := json.Unmarshal(raw, &stored); err != nil { |
| 123 | t.Fatal(err) |
| 124 | } |
| 125 | var compact bytes.Buffer |
| 126 | if err := json.Compact(&compact, stored.Declared); err != nil { |
| 127 | t.Fatal(err) |
| 128 | } |
| 129 | if !bytes.Equal(compact.Bytes(), before) { |
| 130 | t.Fatalf("declaration after the grant = %s, want %s", compact.Bytes(), before) |
| 131 | } |
| 132 | if bytes.Contains(raw, []byte(`"seed"`)) { |
| 133 | t.Fatalf("record = %s, want no seed key", raw) |
| 134 | } |
| 135 | } |
| 136 | |