//go:build linux package host import ( "context" "errors" "fmt" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/park" "github.com/carlosframework/platform/internal/registry" "os" "path/filepath" "testing" "time" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/rootstate" ) func TestPlan14RootInventoryEstablishment(t *testing.T) { t.Parallel() for _, attack := range []string{"none", "record etag", "claim etag", "account", "revision", "foreign root"} { t.Run(attack, func(t *testing.T) { f := newRecoveryFixture(t) o := f.read() h := o.Claim.Holder rec := provision.Record{Schema: 1, Host: o.Host, Account: h.Account, App: h.App, Channel: "stable", CreatedAt: f.now} plan14PutRecord(t, f.claims, rec) _, etag, err := f.claims.Get(t.Context(), h.Key) if err != nil { t.Fatalf("%v: %v", err, errors.Unwrap(err)) } reg := restartTestRegistry(t) journals, err := hostbox.NewJournalService(f.journal) if err != nil { t.Fatalf("%v: %v", err, errors.Unwrap(err)) } guard := f.guard guard.Root = hostguard.NewRootAuthority(&rollbackMemory{files: map[string][]byte{}}) roots := map[string]string{"data": t.TempDir(), "opt": t.TempDir(), "units": t.TempDir()} actor := hostbox.Actor{Box: "box-1", Enrollment: "en", Revision: 1, Binding: "1", Region: "ie"} census := &hostbox.EmptyInstallationCensus{Inventory: journals.Inventory, Registry: reg, Launches: f.journal, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: "1", Roots: roots} root := ClaimRootHTTP{Journals: journals, Launches: f.journal, Roots: roots, InventoryActor: func(context.Context) (hostbox.Actor, error) { return actor, nil }, Reconcile: hostbox.ReconcileAdapter{Registry: reg, Inventory: journals.Inventory, Census: census, Box: actor.Box, Enrollment: actor.Enrollment}, Stop: ClaimStopHTTP{Coordinator: ClaimStopCoordinator{Guard: guard, Evidence: f.claims}}} expected := hostbox.InventoryExpectation{RecordKey: h.Key, RecordETag: etag, Target: hostguard.FenceTarget{Host: o.Host, Incarnation: h.Incarnation, Stamp: h.Stamp, Session: h.Session, Box: actor.Box, Enrollment: actor.Enrollment, ClaimETag: o.ETag, Op: o.Claim.Op.ID, Phase: o.Claim.Op.Phase}} in := hostbox.StopInventory{Complete: true, Host: o.Host, Incarnation: h.Incarnation, InstallationStamp: h.Stamp, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: "1"} switch attack { case "record etag": expected.RecordETag = "other" case "claim etag": expected.Target.ClaimETag = "other" case "account": expected.RecordKey = "apps/bap/jam/instances/" + o.Host + ".json" case "revision": expected.Revision = 8 case "foreign root": in.Entries = []hostbox.StopInventoryEntry{{Kind: "path", ManagedRoot: "opt", RelativePath: "secret", Ownership: "forged", ArtifactID: "forged", Materialization: "intended"}} } var request hostbox.JournalRequest _, client := hostbox.RemoteJournals(func(ctx context.Context, q hostbox.JournalRequest) (hostbox.JournalResponse, error) { request = q return root.commitRootInventory(ctx, q) }) _, err = client.WriteExpected(t.Context(), in, expected) if attack != "none" { if err == nil { t.Fatal("untrusted installation established") } if _, e := guard.Capture(o.Host); e == nil { t.Fatal("refusal wrote root installation") } return } if err != nil { t.Fatalf("%v: %v", err, errors.Unwrap(err)) } v, err := guard.Root.View(t.Context(), o.Host, hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}) if err != nil || v.Installation.InventoryRevision != 1 || v.Installation.Record != h.Key { t.Fatalf("installation snapshot: %+v %v", v, err) } if _, err := root.commitRootInventory(t.Context(), request); err != nil { t.Fatal("lost response replay", err) } _, rev, err := journals.Inventory.Snapshot(t.Context(), o.Host, v.Captured) if err != nil || rev != 1 { t.Fatalf("replay changed revision: %d %v", rev, err) } }) } } func TestPlan14InventoryRegistrationCompletesWithoutAgentLock(t *testing.T) { t.Parallel() testInventoryRegistration(t, false) } func TestPlan14RootInventoryPersistentProducer(t *testing.T) { testInventoryRegistration(t, true) } func TestPlan14RootInventoryRejectsOldBootMaterialization(t *testing.T) { t.Parallel() testInventoryRegistration(t, true, true) } func testInventoryRegistration(t *testing.T, persistent bool, oldBoot ...bool) { testInventoryRegistrationWith(t, persistent, oldBoot, nil, nil) } func testInventoryRegistrationWith(t *testing.T, persistent bool, oldBoot []bool, authority rootstate.Store, after func(*testing.T, *recoveryFixture, ClaimRootHTTP, registry.Route, hostbox.Actor)) { cases := []bool{false, true} if after != nil { cases = []bool{false} } for _, stale := range cases { t.Run(fmt.Sprint(stale), func(t *testing.T) { f := newRecoveryFixture(t) if after != nil { o := f.read() if _, err := hostclaim.Update(t.Context(), f.claims, o, hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, Box: "box-1", Region: "ie", Mode: "cold", NewGeneration: "first-generation"})); err != nil { t.Fatal(err) } } obs := f.read() holder := obs.Claim.Holder rec := provision.Record{Schema: 1, Host: obs.Host, Account: holder.Account, App: holder.App, Channel: "stable", CreatedAt: f.now} plan14PutRecord(t, f.claims, rec) reg := restartTestRegistry(t) journals, err := hostbox.NewJournalService(f.journal) if err != nil { t.Fatal(err) } rootGuard := f.guard rootGuard.Root = hostguard.NewRootAuthority(&rollbackDisk{dir: t.TempDir()}) if authority != nil { rootGuard.Root = hostguard.NewRootAuthority(authority) } roots := map[string]string{"data": f.guard.DataDir, "opt": t.TempDir(), "units": t.TempDir()} actor := hostbox.Actor{Box: "box-1", Enrollment: "en", Revision: 1, Binding: "1", Region: "ie"} census := &hostbox.EmptyInstallationCensus{Inventory: journals.Inventory, Registry: reg, Launches: f.journal, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: "1", Roots: roots, SharedPaths: map[string][]string{"data": {"hostguard"}}} root := ClaimRootHTTP{Journals: journals, Launches: f.journal, Roots: roots, InventoryActor: func(context.Context) (hostbox.Actor, error) { return actor, nil }, Reconcile: hostbox.ReconcileAdapter{Registry: reg, Inventory: journals.Inventory, Census: census, Box: actor.Box, Enrollment: actor.Enrollment}, Stop: ClaimStopHTTP{Coordinator: ClaimStopCoordinator{Guard: rootGuard, Evidence: f.claims}}} if persistent { root.inventoryIdentity = rootstate.ObjectIdentity } _, remote := hostbox.RemoteJournals(func(ctx context.Context, q hostbox.JournalRequest) (hostbox.JournalResponse, error) { if q.Op == "inventory-write" { if persistent { for i, e := range q.Inventory.Entries { if e.Kind == "path" { q.Inventory.Entries[i].Identity = rootstate.FileIdentity{Boot: e.Identity.Boot, Filesystem: "fake", Handle: "fake"} if len(oldBoot) > 0 && e.Materialization == "materialized" { q.Inventory.Entries[i].Identity.Boot = "previous-boot" } } } } var out hostbox.JournalResponse var err error if persistent { _, sender := hostbox.RemoteJournals(func(ctx context.Context, signed hostbox.JournalRequest) (hostbox.JournalResponse, error) { var e error out, e = root.commitRootInventory(ctx, signed) if e == nil { if _, replayErr := root.commitRootInventory(ctx, signed); replayErr != nil { t.Fatal("root identity replay", replayErr) } } return out, e }) _, err = sender.WriteExpected(ctx, q.Inventory, hostbox.InventoryExpectation{Revision: q.Revision, RecordKey: q.RecordKey, RecordETag: q.RecordETag, Target: q.Target}) out.ID = q.ID } else { out, err = root.commitRootInventory(ctx, q) } if stale && q.Revision == 1 && err == nil { rec.RecordSeq++ plan14PutRecord(t, f.claims, rec) } return out, err } return journals.Execute(ctx, actor.Box+"/"+actor.Enrollment, q) }) guard := f.guard guard.FenceView = rootGuard.Root.View source := hostbox.WorkSource{Guard: guard, Box: actor.Box, Region: "ie", Now: time.Now, Policy: func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error { return nil }, Local: func(context.Context, *hostclaim.Claim, hostbox.Selection) error { return nil }} adapter := hostbox.ReconcileAdapter{Registry: reg, Inventory: remote, Box: actor.Box, Enrollment: actor.Enrollment, DirectoryRevision: "1", Census: census, Reconciler: hostbox.Reconciler{Claims: f.claims, Guard: guard, Box: actor.Box, Region: "ie", Now: time.Now, Writes: hostwrite.Config{Enabled: true, Selector: source}, Local: park.Local{Guard: guard, Claims: f.claims, DataDir: guard.DataDir, Box: actor.Box}, CheckRecord: func(context.Context, hostbox.RecordSelection) error { return nil }, CheckLocal: func(context.Context, hostclaim.Observed, hostbox.RecordSelection) error { return nil }}} row := registry.Route{Host: rec.Host, Account: rec.Account, App: rec.App, Kind: "instance", DBPath: provision.NestedDBPath(guard.DataDir, rec.Host), Socket: provision.NestedSocket(rec.Host)} ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second) defer cancel() writes := 0 err = adapter.Install(ctx, provision.ClaimRecord{Host: rec.Host, Key: holder.Key, Account: rec.Account, App: rec.App, Family: "instance", CreatedAt: rec.CreatedAt, Value: rec, Route: row}, func(owner registry.ClaimOwnership) error { writes++ row.ClaimOwnership = owner return reg.Upsert(row) }) if stale { if err == nil || writes != 0 { t.Fatalf("stale response applied: %v %d", err, writes) } return } if err != nil || writes != 1 { t.Fatalf("registration failed or blocked: %v (%v), effects=%d", err, errors.Unwrap(err), writes) } _, revision, err := journals.Inventory.Snapshot(t.Context(), rec.Host, hostguard.Captured{Incarnation: holder.Incarnation, Stamp: holder.Stamp}) if err != nil || revision != 3 { t.Fatalf("registration did not complete all phases: %d %v", revision, err) } if err := os.Mkdir(filepath.Join(guard.DataDir, "instances"), 0700); err != nil { t.Fatal(err) } work, err := source.Select(t.Context(), rec.Host, "spawn") if err != nil { t.Fatal(err) } roles := &ClaimRoleAssembly{Inventory: remote, ManagedRoots: roots, Guard: guard, Claims: f.claims} err = roles.prepareStateRemote(ctx, f.read(), work, ClaimLaunchInputs{DBPath: row.DBPath}, actor) if len(oldBoot) > 0 { if err == nil { t.Fatal("old-boot numeric identity admitted new materialization") } saved, _, snapshotErr := journals.Inventory.Snapshot(t.Context(), rec.Host, hostguard.Captured{Incarnation: holder.Incarnation, Stamp: holder.Stamp}) if snapshotErr != nil { t.Fatal(snapshotErr) } for _, e := range saved.Entries { if e.Kind == "path" && e.Materialization == "materialized" { t.Fatal("refused old boot wrote materialized authority") } } return } if err != nil { t.Fatalf("state registration failed or blocked: %v", err) } saved, revision, err := journals.Inventory.Snapshot(t.Context(), rec.Host, hostguard.Captured{Incarnation: holder.Incarnation, Stamp: holder.Stamp}) if persistent && err == nil { for _, e := range saved.Entries { if e.Kind == "path" && (e.Identity.Boot == "" || e.Identity.Filesystem == "fake" || e.Identity.Handle == "fake") { t.Fatal("agent minted root identity", e) } } } if err != nil || revision != 5 { t.Fatalf("state phases missing: %d %v", revision, err) } if after != nil { after(t, f, root, row, actor) } }) } }