carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package objstore
2
3import (
4 "context"
5 "errors"
6 "testing"
7 "time"
8
9 "github.com/carlosframework/platform/internal/release"
10)
11
12// Bucket and IAM user names join account, app and environment with "-", and
13// app and environment names may both contain "-". So app "shop" with
14// environment "eu" and app "shop-eu" mint the SAME bucket and the SAME user
15// (#218). Everything here pins that two different stores can never end up
16// sharing either one, whichever is created first.
17
18// forgetNameClaims deletes every name claim, which is what a store granted
19// before claims existed looks like.
20func forgetNameClaims(t *testing.T, s release.Store) {
21 t.Helper()
22 ctx := context.Background()
23 keys, err := s.List(ctx, nameClaimsPrefix)
24 if err != nil {
25 t.Fatal(err)
26 }
27 for _, k := range keys {
28 if err := s.Delete(ctx, k); err != nil {
29 t.Fatal(err)
30 }
31 }
32}
33
34func grantStore(t *testing.T, s release.Store, p *MemProvisioner, acct, app, env, adopt string) (*Record, error) {
35 t.Helper()
36 _, pub := testSealKeypair(t)
37 return GrantIn(context.Background(), s, p, testDeployment(), pub, acct, app, env, "op", GrantSourceOperator, adopt, time.Now().UTC())
38}
39
40func TestCollidingStoresResolveToTheSameNames(t *testing.T) {
41 // The premise of every test below: if this ever stops being true the
42 // collision is gone and these tests are guarding nothing.
43 d := testDeployment()
44 if MintedBucketIn(d, "bab", "shop", "eu") != MintedBucketIn(d, "bab", "shop-eu", "") {
45 t.Fatal("bucket names no longer collide")
46 }
47 if UserNameIn("bab", "shop", "eu") != UserNameIn("bab", "shop-eu", "") {
48 t.Fatal("user names no longer collide")
49 }
50}
51
52// The attack in #218: shop-eu has a store; a token bound to shop creates an
53// environment "eu" store. It must be refused before it touches the estate —
54// no grant block, and shop-eu's key still alive.
55func TestGrantRefusesStoreThatCollidesWithAnExistingOne(t *testing.T) {
56 for _, legacy := range []bool{false, true} {
57 name := "claimed"
58 if legacy {
59 name = "granted-before-claims"
60 }
61 t.Run(name, func(t *testing.T) {
62 s := release.NewDirStore(t.TempDir())
63 p := NewMemProvisioner("eu-west-1")
64 declare(t, s, "bab", "shop-eu", "SHOP_S3")
65 victim, err := grantStore(t, s, p, "bab", "shop-eu", "", "")
66 if err != nil {
67 t.Fatal(err)
68 }
69 if legacy {
70 forgetNameClaims(t, s)
71 }
72 declareIn(t, s, "bab", "shop", "eu", "SHOP_S3")
73 _, err = grantStore(t, s, p, "bab", "shop", "eu", "")
74 if !errors.Is(err, ErrStoreNameTaken) {
75 t.Fatalf("colliding grant = %v, want ErrStoreNameTaken", err)
76 }
77 if _, ok := p.Users[victim.Provisioned.IAMUser].Keys[victim.Provisioned.KeyID]; !ok {
78 t.Error("the refused grant reaped the existing store's key")
79 }
80 rec, _, err := GetIn(context.Background(), s, "bab", "shop", "eu")
81 if err != nil {
82 t.Fatal(err)
83 }
84 if rec.Grant != nil {
85 t.Error("the refused grant wrote a grant block — Teardown would treat the other store's estate as its own")
86 }
87 // And the refused store's teardown leaves the other estate standing.
88 if err := TeardownIn(context.Background(), s, p, nil, "bab", "shop", "eu"); err != nil {
89 t.Fatal(err)
90 }
91 if _, ok := p.Buckets[victim.Provisioned.Bucket]; !ok {
92 t.Error("tearing down the refused store deleted the other store's bucket")
93 }
94 if _, ok := p.Users[victim.Provisioned.IAMUser]; !ok {
95 t.Error("tearing down the refused store deleted the other store's user")
96 }
97 })
98 }
99}
100
101// The same pair the other way round: the environment store first, then an
102// app named for the join.
103func TestGrantRefusesCollisionInEitherOrder(t *testing.T) {
104 s := release.NewDirStore(t.TempDir())
105 p := NewMemProvisioner("eu-west-1")
106 declareIn(t, s, "bab", "shop", "eu", "SHOP_S3")
107 if _, err := grantStore(t, s, p, "bab", "shop", "eu", ""); err != nil {
108 t.Fatal(err)
109 }
110 declare(t, s, "bab", "shop-eu", "SHOP_S3")
111 if _, err := grantStore(t, s, p, "bab", "shop-eu", "", ""); !errors.Is(err, ErrStoreNameTaken) {
112 t.Fatalf("colliding grant = %v, want ErrStoreNameTaken", err)
113 }
114 // A three-part join collides with two different splits.
115 declareIn(t, s, "bab", "shop-eu", "west", "SHOP_S3")
116 if _, err := grantStore(t, s, p, "bab", "shop-eu", "west", ""); err != nil {
117 t.Fatal(err)
118 }
119 declareIn(t, s, "bab", "shop", "eu-west", "SHOP_S3")
120 if _, err := grantStore(t, s, p, "bab", "shop", "eu-west", ""); !errors.Is(err, ErrStoreNameTaken) {
121 t.Fatalf("three-part colliding grant = %v, want ErrStoreNameTaken", err)
122 }
123}
124
125// A pair that already collides in production (both granted before this
126// change) must not let a teardown of one destroy the other's estate.
127func TestTeardownLeavesAnEstateAnotherStoreShares(t *testing.T) {
128 ctx := context.Background()
129 s := release.NewDirStore(t.TempDir())
130 p := NewMemProvisioner("eu-west-1")
131 declare(t, s, "bab", "shop-eu", "SHOP_S3")
132 owner, err := grantStore(t, s, p, "bab", "shop-eu", "", "")
133 if err != nil {
134 t.Fatal(err)
135 }
136 forgetNameClaims(t, s)
137 // The second store's record, as the pre-fix walk would have left it.
138 shared := &Record{
139 Declared: owner.Declared,
140 Grant: owner.Grant,
141 Provisioned: owner.Provisioned,
142 }
143 if _, err := PutIn(ctx, s, "bab", "shop", "eu", shared, ""); err != nil {
144 t.Fatal(err)
145 }
146 if err := TeardownIn(ctx, s, p, nil, "bab", "shop", "eu"); err != nil {
147 t.Fatal(err)
148 }
149 if _, ok := p.Buckets[owner.Provisioned.Bucket]; !ok {
150 t.Error("teardown deleted a bucket another store's record still names")
151 }
152 if _, ok := p.Users[owner.Provisioned.IAMUser]; !ok {
153 t.Error("teardown deleted a user another store's record still names")
154 }
155 if rec, _, _ := GetIn(ctx, s, "bab", "shop", "eu"); rec != nil {
156 t.Error("teardown kept the record of the store it was tearing down")
157 }
158}
159
160// Teardown gives the names back: once a store is gone, a store that would
161// have collided with it can be granted.
162func TestTeardownReleasesNames(t *testing.T) {
163 ctx := context.Background()
164 s := release.NewDirStore(t.TempDir())
165 p := NewMemProvisioner("eu-west-1")
166 declare(t, s, "bab", "shop-eu", "SHOP_S3")
167 if _, err := grantStore(t, s, p, "bab", "shop-eu", "", ""); err != nil {
168 t.Fatal(err)
169 }
170 if err := Teardown(ctx, s, p, nil, "bab", "shop-eu"); err != nil {
171 t.Fatal(err)
172 }
173 if keys, _ := s.List(ctx, nameClaimsPrefix); len(keys) != 0 {
174 t.Errorf("claims left after teardown: %v", keys)
175 }
176 declareIn(t, s, "bab", "shop", "eu", "SHOP_S3")
177 if _, err := grantStore(t, s, p, "bab", "shop", "eu", ""); err != nil {
178 t.Fatalf("grant after the colliding store was torn down: %v", err)
179 }
180}
181
182// An operator adopting a bucket another store's record already names is
183// refused, in any account, whether or not that store holds a claim.
184func TestGrantRefusesAdoptingAnotherStoresBucket(t *testing.T) {
185 for _, legacy := range []bool{false, true} {
186 name := "claimed"
187 if legacy {
188 name = "granted-before-claims"
189 }
190 t.Run(name, func(t *testing.T) {
191 s := release.NewDirStore(t.TempDir())
192 p := NewMemProvisioner("eu-west-1")
193 declare(t, s, "bab", "titogo", "TITO_S3")
194 victim, err := grantStore(t, s, p, "bab", "titogo", "", "")
195 if err != nil {
196 t.Fatal(err)
197 }
198 if legacy {
199 forgetNameClaims(t, s)
200 }
201 declare(t, s, "bac", "other", "OTHER_S3")
202 if _, err := grantStore(t, s, p, "bac", "other", "", victim.Provisioned.Bucket); !errors.Is(err, ErrStoreNameTaken) {
203 t.Fatalf("adopting another store's bucket = %v, want ErrStoreNameTaken", err)
204 }
205 // The same app adopting a bucket nobody names is fine.
206 if _, err := grantStore(t, s, p, "bac", "other", "", "acme-uploads"); err != nil {
207 t.Fatal(err)
208 }
209 })
210 }
211}
212
213// #191: a teardown that stopped after deleting the bucket keeps its claim on
214// the name, so nothing else can be granted that bucket until the teardown is
215// finished — and a re-run never deletes a bucket another store now holds.
216func TestRetriedTeardownCannotDeleteABucketGrantedInBetween(t *testing.T) {
217 ctx := context.Background()
218 s := release.NewDirStore(t.TempDir())
219 p := NewMemProvisioner("eu-west-1")
220 minted := MintedBucket(testDeployment(), "bab", "titogo")
221
222 declare(t, s, "bac", "other", "OTHER_S3")
223 if _, err := grantStore(t, s, p, "bac", "other", "", minted); err != nil {
224 t.Fatal(err)
225 }
226 p.FailNext["DeleteUser"] = errors.New("iam down")
227 if err := Teardown(ctx, s, p, nil, "bac", "other"); err == nil {
228 t.Fatal("teardown should have failed at DeleteUser")
229 }
230 declare(t, s, "bab", "titogo", "TITO_S3")
231 if _, err := grantStore(t, s, p, "bab", "titogo", "", ""); !errors.Is(err, ErrStoreNameTaken) {
232 t.Fatalf("grant of a bucket a half-finished teardown still names = %v, want ErrStoreNameTaken", err)
233 }
234 if err := Teardown(ctx, s, p, nil, "bac", "other"); err != nil {
235 t.Fatal(err)
236 }
237 if _, err := grantStore(t, s, p, "bab", "titogo", "", ""); err != nil {
238 t.Fatal(err)
239 }
240
241 // The legacy shape of the same race: the half-finished teardown predates
242 // claims. The minted grant's first run still finds the adopting record —
243 // a minted name is no guarantee nobody adopted it — and is refused.
244 s2 := release.NewDirStore(t.TempDir())
245 p2 := NewMemProvisioner("eu-west-1")
246 declare(t, s2, "bac", "other", "OTHER_S3")
247 if _, err := grantStore(t, s2, p2, "bac", "other", "", minted); err != nil {
248 t.Fatal(err)
249 }
250 p2.FailNext["DeleteUser"] = errors.New("iam down")
251 if err := Teardown(ctx, s2, p2, nil, "bac", "other"); err == nil {
252 t.Fatal("teardown should have failed at DeleteUser")
253 }
254 forgetNameClaims(t, s2)
255 declare(t, s2, "bab", "titogo", "TITO_S3")
256 if _, err := grantStore(t, s2, p2, "bab", "titogo", "", ""); !errors.Is(err, ErrStoreNameTaken) {
257 t.Fatalf("minted grant of a name an unclaimed record adopted = %v, want ErrStoreNameTaken", err)
258 }
259
260 // And if that grant happened before this change, the re-run of the
261 // teardown leaves titogo's bucket alone: titogo's next walk claims it.
262 otherRaw, _, err := s2.Get(ctx, Key("bac", "other"))
263 if err != nil {
264 t.Fatal(err)
265 }
266 if err := s2.Delete(ctx, Key("bac", "other")); err != nil {
267 t.Fatal(err)
268 }
269 if _, err := grantStore(t, s2, p2, "bab", "titogo", "", ""); err != nil {
270 t.Fatal(err)
271 }
272 if _, err := s2.Put(ctx, Key("bac", "other"), otherRaw, ""); err != nil {
273 t.Fatal(err)
274 }
275 p2.Buckets[minted]["data"] = []byte("titogo's")
276 if err := Teardown(ctx, s2, p2, nil, "bac", "other"); err != nil {
277 t.Fatal(err)
278 }
279 if _, ok := p2.Buckets[minted]["data"]; !ok {
280 t.Error("retried teardown emptied a bucket granted to another store in between")
281 }
282}
283
284// A user another store holds survives a teardown, but this store's own keys
285// on it do not: the tenant has seen them, and the user's policy names the
286// other store's bucket.
287func TestTeardownOfASharedUserDeletesThisStoresKeys(t *testing.T) {
288 ctx := context.Background()
289 s := release.NewDirStore(t.TempDir())
290 p := NewMemProvisioner("eu-west-1")
291 declare(t, s, "bab", "shop-eu", "SHOP_S3")
292 owner, err := grantStore(t, s, p, "bab", "shop-eu", "", "")
293 if err != nil {
294 t.Fatal(err)
295 }
296 forgetNameClaims(t, s)
297 user := owner.Provisioned.IAMUser
298 mine, _, err := p.MintKey(ctx, user)
299 if err != nil {
300 t.Fatal(err)
301 }
302 pending, _, err := p.MintKey(ctx, user)
303 if err != nil {
304 t.Fatal(err)
305 }
306 shared := &Record{
307 Declared: owner.Declared,
308 Grant: owner.Grant,
309 Provisioned: &Provisioned{Bucket: owner.Provisioned.Bucket, Region: owner.Provisioned.Region,
310 IAMUser: user, UserARN: owner.Provisioned.UserARN, KeyID: mine, PrevKeyID: pending, At: time.Now()},
311 }
312 if _, err := PutIn(ctx, s, "bab", "shop", "eu", shared, ""); err != nil {
313 t.Fatal(err)
314 }
315 if err := TeardownIn(ctx, s, p, nil, "bab", "shop", "eu"); err != nil {
316 t.Fatal(err)
317 }
318 keys := p.Users[user].Keys
319 for _, id := range []string{mine, pending} {
320 if _, ok := keys[id]; ok {
321 t.Errorf("key %s minted for the torn-down store is still live on the shared user", id)
322 }
323 }
324 if _, ok := keys[owner.Provisioned.KeyID]; !ok {
325 t.Error("teardown deleted the other store's key")
326 }
327}
328
329// A grant that took its claims and died before writing its grant block
330// leaves a declared-only record; tearing that down gives both claims back,
331// including one for an adopted bucket that no record names.
332func TestTeardownOfADeclaredOnlyStoreReleasesItsClaims(t *testing.T) {
333 ctx := context.Background()
334 s := release.NewDirStore(t.TempDir())
335 declare(t, s, "bab", "titogo", "TITO_S3")
336 now := time.Now()
337 if err := claimStoreNames(ctx, s, "bab", "titogo", "", "titogo-uploads", UserName("bab", "titogo"), true, now); err != nil {
338 t.Fatal(err)
339 }
340 if err := Teardown(ctx, s, nil, nil, "bab", "titogo"); err != nil {
341 t.Fatal(err)
342 }
343 if keys, _ := s.List(ctx, nameClaimsPrefix); len(keys) != 0 {
344 t.Errorf("claims left after a declared-only teardown: %v", keys)
345 }
346}
347