package objstore import ( "context" "errors" "testing" "time" "github.com/carlosframework/platform/internal/release" ) // Bucket and IAM user names join account, app and environment with "-", and // app and environment names may both contain "-". So app "shop" with // environment "eu" and app "shop-eu" mint the SAME bucket and the SAME user // (#218). Everything here pins that two different stores can never end up // sharing either one, whichever is created first. // forgetNameClaims deletes every name claim, which is what a store granted // before claims existed looks like. func forgetNameClaims(t *testing.T, s release.Store) { t.Helper() ctx := context.Background() keys, err := s.List(ctx, nameClaimsPrefix) if err != nil { t.Fatal(err) } for _, k := range keys { if err := s.Delete(ctx, k); err != nil { t.Fatal(err) } } } func grantStore(t *testing.T, s release.Store, p *MemProvisioner, acct, app, env, adopt string) (*Record, error) { t.Helper() _, pub := testSealKeypair(t) return GrantIn(context.Background(), s, p, testDeployment(), pub, acct, app, env, "op", GrantSourceOperator, adopt, time.Now().UTC()) } func TestCollidingStoresResolveToTheSameNames(t *testing.T) { // The premise of every test below: if this ever stops being true the // collision is gone and these tests are guarding nothing. d := testDeployment() if MintedBucketIn(d, "bab", "shop", "eu") != MintedBucketIn(d, "bab", "shop-eu", "") { t.Fatal("bucket names no longer collide") } if UserNameIn("bab", "shop", "eu") != UserNameIn("bab", "shop-eu", "") { t.Fatal("user names no longer collide") } } // The attack in #218: shop-eu has a store; a token bound to shop creates an // environment "eu" store. It must be refused before it touches the estate — // no grant block, and shop-eu's key still alive. func TestGrantRefusesStoreThatCollidesWithAnExistingOne(t *testing.T) { for _, legacy := range []bool{false, true} { name := "claimed" if legacy { name = "granted-before-claims" } t.Run(name, func(t *testing.T) { s := release.NewDirStore(t.TempDir()) p := NewMemProvisioner("eu-west-1") declare(t, s, "bab", "shop-eu", "SHOP_S3") victim, err := grantStore(t, s, p, "bab", "shop-eu", "", "") if err != nil { t.Fatal(err) } if legacy { forgetNameClaims(t, s) } declareIn(t, s, "bab", "shop", "eu", "SHOP_S3") _, err = grantStore(t, s, p, "bab", "shop", "eu", "") if !errors.Is(err, ErrStoreNameTaken) { t.Fatalf("colliding grant = %v, want ErrStoreNameTaken", err) } if _, ok := p.Users[victim.Provisioned.IAMUser].Keys[victim.Provisioned.KeyID]; !ok { t.Error("the refused grant reaped the existing store's key") } rec, _, err := GetIn(context.Background(), s, "bab", "shop", "eu") if err != nil { t.Fatal(err) } if rec.Grant != nil { t.Error("the refused grant wrote a grant block — Teardown would treat the other store's estate as its own") } // And the refused store's teardown leaves the other estate standing. if err := TeardownIn(context.Background(), s, p, nil, "bab", "shop", "eu"); err != nil { t.Fatal(err) } if _, ok := p.Buckets[victim.Provisioned.Bucket]; !ok { t.Error("tearing down the refused store deleted the other store's bucket") } if _, ok := p.Users[victim.Provisioned.IAMUser]; !ok { t.Error("tearing down the refused store deleted the other store's user") } }) } } // The same pair the other way round: the environment store first, then an // app named for the join. func TestGrantRefusesCollisionInEitherOrder(t *testing.T) { s := release.NewDirStore(t.TempDir()) p := NewMemProvisioner("eu-west-1") declareIn(t, s, "bab", "shop", "eu", "SHOP_S3") if _, err := grantStore(t, s, p, "bab", "shop", "eu", ""); err != nil { t.Fatal(err) } declare(t, s, "bab", "shop-eu", "SHOP_S3") if _, err := grantStore(t, s, p, "bab", "shop-eu", "", ""); !errors.Is(err, ErrStoreNameTaken) { t.Fatalf("colliding grant = %v, want ErrStoreNameTaken", err) } // A three-part join collides with two different splits. declareIn(t, s, "bab", "shop-eu", "west", "SHOP_S3") if _, err := grantStore(t, s, p, "bab", "shop-eu", "west", ""); err != nil { t.Fatal(err) } declareIn(t, s, "bab", "shop", "eu-west", "SHOP_S3") if _, err := grantStore(t, s, p, "bab", "shop", "eu-west", ""); !errors.Is(err, ErrStoreNameTaken) { t.Fatalf("three-part colliding grant = %v, want ErrStoreNameTaken", err) } } // A pair that already collides in production (both granted before this // change) must not let a teardown of one destroy the other's estate. func TestTeardownLeavesAnEstateAnotherStoreShares(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) p := NewMemProvisioner("eu-west-1") declare(t, s, "bab", "shop-eu", "SHOP_S3") owner, err := grantStore(t, s, p, "bab", "shop-eu", "", "") if err != nil { t.Fatal(err) } forgetNameClaims(t, s) // The second store's record, as the pre-fix walk would have left it. shared := &Record{ Declared: owner.Declared, Grant: owner.Grant, Provisioned: owner.Provisioned, } if _, err := PutIn(ctx, s, "bab", "shop", "eu", shared, ""); err != nil { t.Fatal(err) } if err := TeardownIn(ctx, s, p, nil, "bab", "shop", "eu"); err != nil { t.Fatal(err) } if _, ok := p.Buckets[owner.Provisioned.Bucket]; !ok { t.Error("teardown deleted a bucket another store's record still names") } if _, ok := p.Users[owner.Provisioned.IAMUser]; !ok { t.Error("teardown deleted a user another store's record still names") } if rec, _, _ := GetIn(ctx, s, "bab", "shop", "eu"); rec != nil { t.Error("teardown kept the record of the store it was tearing down") } } // Teardown gives the names back: once a store is gone, a store that would // have collided with it can be granted. func TestTeardownReleasesNames(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) p := NewMemProvisioner("eu-west-1") declare(t, s, "bab", "shop-eu", "SHOP_S3") if _, err := grantStore(t, s, p, "bab", "shop-eu", "", ""); err != nil { t.Fatal(err) } if err := Teardown(ctx, s, p, nil, "bab", "shop-eu"); err != nil { t.Fatal(err) } if keys, _ := s.List(ctx, nameClaimsPrefix); len(keys) != 0 { t.Errorf("claims left after teardown: %v", keys) } declareIn(t, s, "bab", "shop", "eu", "SHOP_S3") if _, err := grantStore(t, s, p, "bab", "shop", "eu", ""); err != nil { t.Fatalf("grant after the colliding store was torn down: %v", err) } } // An operator adopting a bucket another store's record already names is // refused, in any account, whether or not that store holds a claim. func TestGrantRefusesAdoptingAnotherStoresBucket(t *testing.T) { for _, legacy := range []bool{false, true} { name := "claimed" if legacy { name = "granted-before-claims" } t.Run(name, func(t *testing.T) { s := release.NewDirStore(t.TempDir()) p := NewMemProvisioner("eu-west-1") declare(t, s, "bab", "titogo", "TITO_S3") victim, err := grantStore(t, s, p, "bab", "titogo", "", "") if err != nil { t.Fatal(err) } if legacy { forgetNameClaims(t, s) } declare(t, s, "bac", "other", "OTHER_S3") if _, err := grantStore(t, s, p, "bac", "other", "", victim.Provisioned.Bucket); !errors.Is(err, ErrStoreNameTaken) { t.Fatalf("adopting another store's bucket = %v, want ErrStoreNameTaken", err) } // The same app adopting a bucket nobody names is fine. if _, err := grantStore(t, s, p, "bac", "other", "", "acme-uploads"); err != nil { t.Fatal(err) } }) } } // #191: a teardown that stopped after deleting the bucket keeps its claim on // the name, so nothing else can be granted that bucket until the teardown is // finished — and a re-run never deletes a bucket another store now holds. func TestRetriedTeardownCannotDeleteABucketGrantedInBetween(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) p := NewMemProvisioner("eu-west-1") minted := MintedBucket(testDeployment(), "bab", "titogo") declare(t, s, "bac", "other", "OTHER_S3") if _, err := grantStore(t, s, p, "bac", "other", "", minted); err != nil { t.Fatal(err) } p.FailNext["DeleteUser"] = errors.New("iam down") if err := Teardown(ctx, s, p, nil, "bac", "other"); err == nil { t.Fatal("teardown should have failed at DeleteUser") } declare(t, s, "bab", "titogo", "TITO_S3") if _, err := grantStore(t, s, p, "bab", "titogo", "", ""); !errors.Is(err, ErrStoreNameTaken) { t.Fatalf("grant of a bucket a half-finished teardown still names = %v, want ErrStoreNameTaken", err) } if err := Teardown(ctx, s, p, nil, "bac", "other"); err != nil { t.Fatal(err) } if _, err := grantStore(t, s, p, "bab", "titogo", "", ""); err != nil { t.Fatal(err) } // The legacy shape of the same race: the half-finished teardown predates // claims. The minted grant's first run still finds the adopting record — // a minted name is no guarantee nobody adopted it — and is refused. s2 := release.NewDirStore(t.TempDir()) p2 := NewMemProvisioner("eu-west-1") declare(t, s2, "bac", "other", "OTHER_S3") if _, err := grantStore(t, s2, p2, "bac", "other", "", minted); err != nil { t.Fatal(err) } p2.FailNext["DeleteUser"] = errors.New("iam down") if err := Teardown(ctx, s2, p2, nil, "bac", "other"); err == nil { t.Fatal("teardown should have failed at DeleteUser") } forgetNameClaims(t, s2) declare(t, s2, "bab", "titogo", "TITO_S3") if _, err := grantStore(t, s2, p2, "bab", "titogo", "", ""); !errors.Is(err, ErrStoreNameTaken) { t.Fatalf("minted grant of a name an unclaimed record adopted = %v, want ErrStoreNameTaken", err) } // And if that grant happened before this change, the re-run of the // teardown leaves titogo's bucket alone: titogo's next walk claims it. otherRaw, _, err := s2.Get(ctx, Key("bac", "other")) if err != nil { t.Fatal(err) } if err := s2.Delete(ctx, Key("bac", "other")); err != nil { t.Fatal(err) } if _, err := grantStore(t, s2, p2, "bab", "titogo", "", ""); err != nil { t.Fatal(err) } if _, err := s2.Put(ctx, Key("bac", "other"), otherRaw, ""); err != nil { t.Fatal(err) } p2.Buckets[minted]["data"] = []byte("titogo's") if err := Teardown(ctx, s2, p2, nil, "bac", "other"); err != nil { t.Fatal(err) } if _, ok := p2.Buckets[minted]["data"]; !ok { t.Error("retried teardown emptied a bucket granted to another store in between") } } // A user another store holds survives a teardown, but this store's own keys // on it do not: the tenant has seen them, and the user's policy names the // other store's bucket. func TestTeardownOfASharedUserDeletesThisStoresKeys(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) p := NewMemProvisioner("eu-west-1") declare(t, s, "bab", "shop-eu", "SHOP_S3") owner, err := grantStore(t, s, p, "bab", "shop-eu", "", "") if err != nil { t.Fatal(err) } forgetNameClaims(t, s) user := owner.Provisioned.IAMUser mine, _, err := p.MintKey(ctx, user) if err != nil { t.Fatal(err) } pending, _, err := p.MintKey(ctx, user) if err != nil { t.Fatal(err) } shared := &Record{ Declared: owner.Declared, Grant: owner.Grant, Provisioned: &Provisioned{Bucket: owner.Provisioned.Bucket, Region: owner.Provisioned.Region, IAMUser: user, UserARN: owner.Provisioned.UserARN, KeyID: mine, PrevKeyID: pending, At: time.Now()}, } if _, err := PutIn(ctx, s, "bab", "shop", "eu", shared, ""); err != nil { t.Fatal(err) } if err := TeardownIn(ctx, s, p, nil, "bab", "shop", "eu"); err != nil { t.Fatal(err) } keys := p.Users[user].Keys for _, id := range []string{mine, pending} { if _, ok := keys[id]; ok { t.Errorf("key %s minted for the torn-down store is still live on the shared user", id) } } if _, ok := keys[owner.Provisioned.KeyID]; !ok { t.Error("teardown deleted the other store's key") } } // A grant that took its claims and died before writing its grant block // leaves a declared-only record; tearing that down gives both claims back, // including one for an adopted bucket that no record names. func TestTeardownOfADeclaredOnlyStoreReleasesItsClaims(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) declare(t, s, "bab", "titogo", "TITO_S3") now := time.Now() if err := claimStoreNames(ctx, s, "bab", "titogo", "", "titogo-uploads", UserName("bab", "titogo"), true, now); err != nil { t.Fatal(err) } if err := Teardown(ctx, s, nil, nil, "bab", "titogo"); err != nil { t.Fatal(err) } if keys, _ := s.List(ctx, nameClaimsPrefix); len(keys) != 0 { t.Errorf("claims left after a declared-only teardown: %v", keys) } }