package hostresolve import ( "context" "encoding/json" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/hostquarantine" "reflect" "slices" "strings" "time" ) // Readmit keeps the global deny until every declaration, claim and receipt // check is complete. It grants no executor or writer session. func (s Service) Readmit(ctx context.Context, box, enrollment string, revision uint64, lossIDs []string) error { before, err := s.Directory.Read(ctx) if err != nil { return err } b, ok := before.Roster.Boxes[box] if !ok || b.Enrollment != enrollment || b.State != "quarantined" || b.QuarantineRevision != revision || !reflect.DeepEqual(b.LossIDs, lossIDs) { return ErrResolve } fenced, err := s.Directory.TerminationFence(ctx, before, box, s.Now()) if err != nil { return err } if fenced { return ErrResolve } report, err := s.Directory.PrivateBoxReport(ctx, box, b) if err != nil { return err } if report.Enrollment != enrollment || report.InstanceID != b.Cloud.InstanceID || report.BootID == "" || report.GeneratedAt.IsZero() || s.Now().Sub(report.GeneratedAt) > 15*time.Minute || report.GeneratedAt.After(s.Now().Add(5*time.Second)) { return ErrResolve } floor, err := hostfloor.Read(ctx, s.Directory.Store, s.Directory.PublicKey, s.Directory.Bucket) if err != nil { return err } evidence := hostactors.ExactEvidence{PublicKey: s.Directory.PublicKey, Store: s.Directory.Store, Boxes: s.Directory.PrivateBoxReport} if err = hostactors.VerifyReport(ctx, evidence, s.Directory.PublicKey, floor, report, enrollment, b.Cloud.InstanceID, s.Now()); err != nil { return err } if !report.HostClaimEnabled { return ErrResolve } key, err := hostquarantine.RetirementKey(box, enrollment, revision) if err != nil { return err } raw, _, err := s.Directory.Store.Get(ctx, key) if err != nil { return err } if report.RetirementReceiptSHA256 != hostfloor.Digest(raw) { return ErrResolve } var receipt hostquarantine.RetirementCompletion if json.Unmarshal(raw, &receipt) != nil || receipt.Validate() != nil || !receipt.Complete || receipt.Box != box || receipt.Enrollment != enrollment || receipt.InstanceID != b.Cloud.InstanceID || receipt.QuarantineRevision != revision || receipt.BootID != report.BootID || receipt.CompletedAt.After(report.GeneratedAt) { return ErrResolve } losses := make(map[string][]hostclaim.LossRecord) for _, id := range lossIDs { loss, err := hostclaim.ReadLossRecord(ctx, s.Directory.Store, s.Directory.PublicKey, id) if err != nil { return err } if (loss.Record.State != "committed" && loss.Record.State != "conflicted") || loss.Record.Box != box || loss.Record.Enrollment != enrollment || loss.Record.QuarantineRevision > revision { return ErrResolve } proven := false for _, entry := range receipt.Entries { if entry.Host == loss.Record.Host && entry.Incarnation == loss.Record.HolderIncarnation && entry.Stamp == loss.Record.HolderStamp { proven = true } } if !proven && (loss.Record.State == "conflicted" || len(loss.Record.ConfirmedConsequences.DischargeLocal) == 0) { return ErrResolve } losses[loss.Record.Host] = append(losses[loss.Record.Host], loss.Record) } // Store.List is a complete listing in both real implementations. Its S3 // implementation follows every continuation token; any error refuses. keys, err := s.Directory.Store.List(ctx, "control/hosts/") if err != nil { return err } var claims []hostclaim.Observed for _, key := range keys { host, ok := claimHost(key) if !ok { continue } observed, err := hostclaim.Read(ctx, s.Directory.Store, host) if err != nil { return err } if observed.Absent { continue } c := observed.Claim for _, o := range c.Obligations { if o.Kind == "park-local" && o.Box == box { return ErrResolve } } if slices.Contains(c.Lost, box) { covered := false for _, loss := range losses[host] { if loss.State == "committed" && loss.QuarantineRevision == c.LostRevisions[box] && slices.Contains(c.LossRefs, loss.ID) { covered = true break } } if !covered { return ErrResolve } claims = append(claims, observed) } } for _, observed := range claims { if _, err = hostclaim.Update(ctx, s.Directory.Store, observed, hostclaim.Readmit(box, observed.Claim.LostRevisions[box])); err != nil { return err } } // CAS against the original roster prevents a concurrent declaration, // reservation or membership change being overwritten by this readmission. next := hostactors.Clone(before.Roster) b.State = "active" next.Boxes[box] = b _, err = s.Directory.Replace(ctx, s.Signer, before, next) return err } func claimHost(key string) (string, bool) { s, ok := strings.CutPrefix(key, "control/hosts/") if !ok || strings.Contains(s, "/") { return "", false } host, ok := strings.CutSuffix(s, ".json") return host, ok && host != "" }