package host import ( "context" "encoding/json" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/provision" "os" "path/filepath" "testing" ) func plan14RecoveryRebind(t *testing.T) (*recoveryFixture, ClaimRootHTTP, *rollbackDisk, recoveryRebindRequest) { t.Helper() f, o, h := plan12Handover(t, hostclaim.KindRestoring, "stop") holder := o.Claim.Holder plan14PutRecord(t, f.claims, provision.Record{Schema: 1, Host: o.Host, Account: holder.Account, App: holder.App, Channel: "stable", CreatedAt: f.now}) disk := &rollbackDisk{dir: t.TempDir()} g := f.guard g.Root = hostguard.NewRootAuthority(disk) in := hostguard.RootInstallation{Host: o.Host, Incarnation: holder.Incarnation, Stamp: holder.Stamp, Migration: 1, InventoryRevision: 1, Account: holder.Account, App: holder.App, Record: holder.Key, Box: "box-1", Enrollment: "enrollment-1"} if err := g.EstablishRoot(t.Context(), in, func(*hostclaim.Claim) error { return nil }); err != nil { t.Fatal(err) } next, err := hostclaim.Update(t.Context(), f.claims, o, hostclaim.AdmitWriter(hostclaim.Admission{ObservedOpID: o.Claim.Op.ID, ObservedPhase: o.Claim.Op.Phase, ObservedSession: holder.Session, ObservedGeneration: holder.Generation, Box: "box-1", Mode: "cold", NewGeneration: "recovered", Region: "ie", PredecessorRegion: "ie"})) if err != nil { t.Fatal(err) } n := next.Claim.Holder _, etag, err := f.claims.Get(t.Context(), holder.Key) if err != nil { t.Fatal(err) } actor := hostbox.Actor{Box: "box-1", Enrollment: "enrollment-1", Revision: 7, Binding: "7"} root := ClaimRootHTTP{Stop: ClaimStopHTTP{Coordinator: ClaimStopCoordinator{Guard: g, Evidence: f.claims}}, InventoryActor: func(context.Context) (hostbox.Actor, error) { return actor, nil }, Roots: map[string]string{"data": g.DataDir}, Reconcile: hostbox.ReconcileAdapter{Inventory: h.Inventory, Registry: h.Registry, Box: actor.Box, Enrollment: actor.Enrollment}} q := recoveryRebindRequest{V: 1, Prior: hostguard.Captured{Incarnation: holder.Incarnation, Stamp: holder.Stamp}, Revision: 1, RecordKey: holder.Key, RecordETag: etag, Next: hostbox.ColdSuccessor{Host: next.Host, Incarnation: n.Incarnation, Stamp: n.Stamp, Session: n.Session, Generation: n.Generation, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: "7", ETag: next.ETag, Op: next.Claim.Op.ID, Phase: next.Claim.Op.Phase}} return f, root, disk, q } func TestPlan14RecoveryRebindAuthority(t *testing.T) { t.Parallel() for _, attack := range []string{"none", "claim etag", "stamp", "session", "generation", "op", "phase", "box", "enrollment", "record etag", "record binding", "revision", "source identity", "missing root", "missing inventory", "plain stopped report", "before commit"} { t.Run(attack, func(t *testing.T) { f, root, disk, q := plan14RecoveryRebind(t) switch attack { case "claim etag": q.Next.ETag = "stale" case "stamp": q.Next.Stamp++ case "session": q.Next.Session++ case "generation": q.Next.Generation = "other" case "op": q.Next.Op = "other" case "phase": q.Next.Phase = "other" case "box": q.Next.Box = "other" case "enrollment": q.Next.Enrollment = "other" case "record etag": q.RecordETag = "stale" case "record binding": body, oldETag, err := f.claims.Get(t.Context(), q.RecordKey) if err != nil { t.Fatal(err) } var rec provision.Record json.Unmarshal(body, &rec) rec.Account = "other" body, _ = json.Marshal(rec) q.RecordETag, err = f.claims.Put(t.Context(), q.RecordKey, body, oldETag) if err != nil { t.Fatal(err) } case "revision": q.Revision++ case "source identity": q.Prior.Stamp++ case "missing root": if err := os.Remove(filepath.Join(disk.dir, q.Next.Host+".json")); err != nil { t.Fatal(err) } case "missing inventory": root.Reconcile.Inventory, _ = openLegacyInventory(t, filepath.Join(t.TempDir(), "missing.db")) t.Cleanup(func() { root.Reconcile.Inventory.Close() }) case "plain stopped report": o := f.read() o.Claim.Holder.Admissions = nil body, _ := json.Marshal(o.Claim) var err error key, _ := hostclaim.Key(o.Host) q.Next.ETag, err = f.claims.Put(t.Context(), key, body, o.ETag) if err != nil { t.Fatal(err) } case "before commit": disk.afterWrite = func(string, []byte) { disk.afterWrite = nil o := f.read() _, err := hostclaim.Update(t.Context(), f.claims, o, func(c *hostclaim.Claim) error { c.Op.Phase = "changed"; return nil }) if err != nil { t.Fatal(err) } } } before, _ := disk.Read(q.Next.Host + ".json") row, _ := root.Reconcile.Registry.Get(q.Next.Host) err := root.rebindRecoveryInventory(t.Context(), q) if attack != "none" { if err == nil { t.Fatal("untrusted rebind accepted") } after, _ := disk.Read(q.Next.Host + ".json") if attack != "before commit" && string(before) != string(after) { t.Fatal("refusal changed root") } _, rev, e := root.Reconcile.Inventory.Snapshot(t.Context(), q.Next.Host, hostguard.Captured{Incarnation: q.Next.Incarnation, Stamp: q.Next.Stamp}) if e != nil || rev != 0 { t.Fatal("refusal wrote successor", rev, e) } return } if err != nil { t.Fatal(err) } after, _ := root.Reconcile.Registry.Get(q.Next.Host) if after.ClaimOwnership != row.ClaimOwnership { t.Fatal("root rebound registry") } saved, _ := disk.Read(q.Next.Host + ".json") if err = root.rebindRecoveryInventory(t.Context(), q); err != nil { t.Fatal("replay", err) } again, _ := disk.Read(q.Next.Host + ".json") if string(saved) != string(again) { t.Fatal("replay changed root") } _, rev, err := root.Reconcile.Inventory.Snapshot(t.Context(), q.Next.Host, hostguard.Captured{Incarnation: q.Next.Incarnation, Stamp: q.Next.Stamp}) if err != nil || rev != 1 { t.Fatal(rev, err) } }) } }