package hostguard import ( "bytes" "context" "encoding/json" "errors" "io" "os" "strings" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/rootstate" ) type InstallationRegistration struct { Op, Kind, Phase, RowID string Session uint64 } type InstallationArtifacts struct { Schema int StateDirectory bool Timers string } type RootInstallation struct { Artifacts InstallationArtifacts `json:",omitzero"` Registration InstallationRegistration `json:",omitempty"` Empty bool Host, Incarnation, Account, App, Record, Box, Enrollment string Stamp, Migration, InventoryRevision uint64 } type RootAuthority struct { store rootstate.Store inventoryRevision func(context.Context, string, Captured) (uint64, error) } func NewRootAuthority(store rootstate.Store) *RootAuthority { return &RootAuthority{store: store} } type rootRecord struct { MigrationDenies []MigrationDeny `json:",omitempty"` Quarantined bool `json:",omitempty"` Settled bool `json:",omitempty"` RebindCommit []byte `json:",omitempty"` Schema int Installation RootInstallation Retired []Captured Barrier *FenceTarget PendingInventory []byte `json:",omitempty"` PendingInstallation *RootInstallation `json:",omitempty"` State record } func (a *RootAuthority) read(host string) (record, error) { if a == nil || a.store == nil || validateFileHost(host) != nil { return record{}, ErrInvalidInput } b, err := a.store.Read(host + ".json") if errors.Is(err, os.ErrNotExist) { return record{}, ErrNoRecord } if err != nil { return record{}, err } var saved rootRecord dec := json.NewDecoder(bytes.NewReader(b)) dec.DisallowUnknownFields() if dec.Decode(&saved) != nil || dec.Decode(new(any)) != io.EOF || saved.Schema != 1 || saved.Installation.Host != host || saved.Installation.Incarnation != saved.State.Incarnation || saved.Installation.Migration == 0 { return record{}, ErrMalformedRecord } r := saved.State if r.ActiveStamp != 0 { if saved.Installation.Stamp != r.ActiveStamp { return record{}, ErrMalformedRecord } } else { // Older retired records cleared both stamps. Recover only the last // exact root-retained tuple, never an agent supplied installation. if len(saved.Retired) == 0 { return record{}, ErrMalformedRecord } last := saved.Retired[len(saved.Retired)-1] if last.Incarnation != r.Incarnation || last.Stamp == 0 || (saved.Installation.Stamp != 0 && saved.Installation.Stamp != last.Stamp) { return record{}, ErrMalformedRecord } saved.Installation.Stamp = last.Stamp } r.migrationDenies = saved.MigrationDenies for _, deny := range r.migrationDenies { if deny.Incarnation == "" || deny.Stamp == 0 || deny.Session == 0 { return record{}, ErrMalformedRecord } } if saved.Settled && !saved.Quarantined { return record{}, ErrMalformedRecord } r.quarantined, r.settled = saved.Quarantined, saved.Settled r.root = true r.installation = saved.Installation r.rootRetired = saved.Retired r.barrier = saved.Barrier r.rebindCommit = saved.RebindCommit r.pendingInventory = saved.PendingInventory r.pendingInstallation = saved.PendingInstallation if (len(r.pendingInventory) > 0) != (r.pendingInstallation != nil) { return record{}, ErrMalformedRecord } if err := r.validate(host); err != nil { return record{}, err } return r, nil } func (a *RootAuthority) write(r record) error { if a == nil || a.store == nil || !r.root || r.validate(r.Host) != nil { return ErrMalformedRecord } r.installation.Host, r.installation.Incarnation = r.Host, r.Incarnation if r.ActiveStamp != 0 { r.installation.Stamp = r.ActiveStamp } b, err := json.Marshal(rootRecord{MigrationDenies: r.migrationDenies, Quarantined: r.quarantined, Settled: r.settled, RebindCommit: r.rebindCommit, Schema: 1, Installation: r.installation, Retired: r.rootRetired, Barrier: r.barrier, PendingInventory: r.pendingInventory, PendingInstallation: r.pendingInstallation, State: r}) if err != nil { return err } return a.store.Write(r.Host+".json", b) } func (g Guard) Capture(host string) (Captured, error) { if g.Root == nil { return Capture(g.DataDir, host) } r, err := g.Root.read(host) if err != nil { return Captured{}, err } return Captured{Incarnation: r.Incarnation, Stamp: r.ActiveStamp}, nil } func (g Guard) EstablishRoot(ctx context.Context, in RootInstallation, check func(*hostclaim.Claim) error) error { return g.CommitRootInstallation(ctx, in, check, func() error { return nil }) } func (g Guard) CommitRootInstallation(ctx context.Context, in RootInstallation, check func(*hostclaim.Claim) error, commit func() error) error { if commit == nil { return ErrInvalidInput } return g.CommitRootInventoryArtifacts(ctx, in, nil, check, nil, func(RootInstallation) error { return commit() }, nil) } func (g Guard) CommitRootInventoryArtifacts(ctx context.Context, in RootInstallation, intent []byte, check func(*hostclaim.Claim) error, validate func(RootInstallation) error, commit func(RootInstallation) error, artifacts func(RootInstallation) (InstallationArtifacts, error)) error { if g.Root == nil || check == nil || commit == nil || in.Host == "" || in.Incarnation == "" || in.Stamp == 0 || in.Migration == 0 || in.Account == "" || in.App == "" || in.Record == "" || in.Box == "" || in.Enrollment == "" { return ErrInvalidInput } return g.locked(ctx, in.Host, func(d *recordDir) error { c, err := g.fresh(ctx, in.Host) if err != nil { return err } if err := check(c); err != nil { return err } if c.Holder.Incarnation != in.Incarnation || c.Holder.Stamp != in.Stamp || c.Holder.Account != in.Account || c.Holder.App != in.App { return ErrClaimIncarnation } r, err := d.read(in.Host) if err != nil && !errors.Is(err, ErrNoRecord) { return err } if errors.Is(err, ErrNoRecord) { r = record{V: 1, Host: in.Host, Incarnation: in.Incarnation, root: true} } want := Captured{Incarnation: in.Incarnation, Stamp: in.Stamp} for _, old := range r.rootRetired { if old == want { return ErrRetiredStamp } } if r.Incarnation == in.Incarnation && r.ActiveStamp == in.Stamp { prior := r.installation if validate != nil { if err := validate(prior); err != nil { return err } } if prior.Account != in.Account || prior.App != in.App || prior.Record != in.Record || prior.Box != in.Box || prior.Enrollment != in.Enrollment || prior.Migration != in.Migration || prior.InventoryRevision > in.InventoryRevision { return ErrRecordIncarnation } } if migrationAdmissionDenied(r.migrationDenies, r.FenceJournal, want, c.Holder.Session) { return ErrFenced } if r.ActiveFence != nil && r.ActiveFence.matchesSession(c, want) { return ErrFenced } for _, receipt := range r.FenceJournal { if receipt.Target.matchesSession(c, want) { return ErrFenced } } if r.barrier != nil { return ErrFenced } if r.Incarnation == in.Incarnation && r.ActiveStamp > in.Stamp { return ErrOldStamp } prior := r.installation if prior.Incarnation == in.Incarnation && prior.Stamp == in.Stamp && in.Registration == (InstallationRegistration{}) { in.Registration = prior.Registration } if artifacts != nil { in.Artifacts, err = artifacts(prior) if err != nil { return err } } else if prior.Incarnation == in.Incarnation && in.Artifacts == (InstallationArtifacts{}) { in.Artifacts = prior.Artifacts } if r.pendingInstallation != nil && (*r.pendingInstallation != in || !bytes.Equal(r.pendingInventory, intent)) { return ErrFenced } if validate != nil { if err := validate(prior); err != nil { return err } } if len(intent) > 0 { r.pendingInventory = append([]byte(nil), intent...) pending := in r.pendingInstallation = &pending if r.ActiveStamp == 0 { r.ActiveStamp = in.Stamp r.installation = in r.installation.InventoryRevision = 0 } if err := d.write(r); err != nil { return err } } if err := commit(prior); err != nil { return err } if r.Incarnation != in.Incarnation || r.ActiveStamp != in.Stamp { r.retire() r.RetiredStamps = nil } r.pendingInventory, r.pendingInstallation = nil, nil r.Incarnation, r.ActiveStamp, r.installation = in.Incarnation, in.Stamp, in r.quarantined, r.settled = false, false return d.write(r) }) } func NewRootAuthorityWithInventory(store rootstate.Store, revision func(context.Context, string, Captured) (uint64, error)) *RootAuthority { return &RootAuthority{store: store, inventoryRevision: revision} } func (g Guard) refreshRootInventory(ctx context.Context, r *record) error { if g.Root == nil || g.Root.inventoryRevision == nil { return nil } revision, err := g.Root.inventoryRevision(ctx, r.Host, Captured{Incarnation: r.Incarnation, Stamp: r.ActiveStamp}) if err != nil { return err } if revision == 0 { return ErrNoRecord } r.installation.InventoryRevision = revision return nil } func (a *RootAuthority) PendingInstallations() ([][]byte, error) { names, err := a.store.Names() if err != nil { return nil, err } var out [][]byte for _, name := range names { if !strings.HasSuffix(name, ".json") { continue } r, err := a.read(strings.TrimSuffix(name, ".json")) if err != nil { return nil, err } if r.pendingInstallation != nil { out = append(out, append([]byte(nil), r.pendingInventory...)) } } return out, nil } func (a *RootAuthority) PendingInstallation(ctx context.Context, host string) (RootInstallation, []byte, error) { if err := ctx.Err(); err != nil { return RootInstallation{}, nil, err } r, err := a.read(host) if err != nil { return RootInstallation{}, nil, err } if r.pendingInstallation == nil { return RootInstallation{}, nil, ErrNoRecord } return *r.pendingInstallation, append([]byte(nil), r.pendingInventory...), nil } // AbandonRootInventory resolves a pending root inventory intent that a // fresh claim proves superseded: the exact intent bytes, then superseded // over the guard's fresh claim read, under the host lock. The installation // ends in the same terminal state as an obsolete-intent settlement // (resolveRootInventory): quarantined and settled, still refused for every // execution and admission, inspectable by the migration census, with a // landed commit's revision reconciled first (fix round 2: an abandoned // installation no longer blocks cleanup startup on every restart). func (g Guard) AbandonRootInventory(ctx context.Context, host string, intent []byte, superseded func(*hostclaim.Claim) bool, applied func(context.Context, RootInstallation) (bool, error)) error { if superseded == nil { return ErrInvalidInput } return g.resolveRootInventory(ctx, host, intent, superseded, applied) } // SettleObsoleteRootInventory settles a pending root inventory intent that // root's startup found obsolete: stamped under a roster binding other than // the startup binding (roster binding N3). The binding difference alone // supersedes it, so no claim is consulted. Under the host lock, with the // exact intent bytes, the intent is cleared and the installation is // quarantined and marked settled: every execution and admission path still // refuses it, and only the refusal-only migration census may inspect it. // // applied reports whether the pending installation's inventory already // holds this intent: root's commit landed and only the final record write // was lost. The record then takes the installation that commit would have // recorded, exactly as CommitRootInventoryArtifacts records it, so the // guard and the inventory agree on the revision. Otherwise the record keeps // the installation it had. func (g Guard) SettleObsoleteRootInventory(ctx context.Context, host string, intent []byte, applied func(context.Context, RootInstallation) (bool, error)) error { return g.resolveRootInventory(ctx, host, intent, nil, applied) } // resolveRootInventory is the one terminal resolution of a pending root // inventory intent, for an obsolete binding (superseded nil: the binding // difference alone supersedes it, no claim is read) or a proven superseded // claim (superseded over the fresh claim, ErrFenced when it does not hold). // The intent must match exactly. applied reconciles a commit that landed // before its record write was lost; then the intent is cleared and the // record marked quarantined and settled. A settled record is terminal: no // pending intent remains to resolve it again, so it must stay inspectable // by the census, and the quarantine keeps every execution path refusing. func (g Guard) resolveRootInventory(ctx context.Context, host string, intent []byte, superseded func(*hostclaim.Claim) bool, applied func(context.Context, RootInstallation) (bool, error)) error { if g.Root == nil || applied == nil || len(intent) == 0 { return ErrInvalidInput } return g.readLocked(ctx, host, func(d *recordDir) error { r, err := d.read(host) if err != nil { return err } if !bytes.Equal(r.pendingInventory, intent) || r.pendingInstallation == nil { return ErrInvalidInput } if superseded != nil { c, err := g.fresh(ctx, host) if err != nil { return err } if !superseded(c) { return ErrFenced } } pending := *r.pendingInstallation landed, err := applied(ctx, pending) if err != nil { return err } if landed { if r.Incarnation != pending.Incarnation || r.ActiveStamp != pending.Stamp { r.retire() r.RetiredStamps = nil } r.Incarnation, r.ActiveStamp, r.installation = pending.Incarnation, pending.Stamp, pending } r.pendingInventory, r.pendingInstallation = nil, nil r.quarantined, r.settled = true, true return d.write(r) }) }