| 1 | package hostactors |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "errors" |
| 7 | "github.com/carlosframework/platform/internal/termfence" |
| 8 | "os" |
| 9 | "time" |
| 10 | ) |
| 11 | |
| 12 | // TerminationFence reads authenticated worker-owned, create-only evidence. |
| 13 | // Object access failures and invalid bindings are errors, never exclusions. |
| 14 | func (d Directory) TerminationFence(ctx context.Context, o Observed, ref string, now time.Time) (bool, error) { |
| 15 | b, ok := o.Roster.Boxes[ref] |
| 16 | if !ok || !token.MatchString(b.Enrollment) || d.Store == nil { |
| 17 | return false, ErrRoster |
| 18 | } |
| 19 | raw, _, err := d.Store.Get(ctx, "control/box-fences/"+b.Enrollment+".json") |
| 20 | if errors.Is(err, os.ErrNotExist) { |
| 21 | return false, nil |
| 22 | } |
| 23 | if err != nil { |
| 24 | return false, err |
| 25 | } |
| 26 | var f termfence.Record |
| 27 | if json.Unmarshal(raw, &f) != nil { |
| 28 | return false, ErrRoster |
| 29 | } |
| 30 | if err := d.ValidateTerminationFenceAuthority(ctx, o, ref, f, now); err != nil { |
| 31 | return false, err |
| 32 | } |
| 33 | return true, nil |
| 34 | } |
| 35 | |
| 36 | // ValidateTerminationFence checks the record against the roster alone. It |
| 37 | // cannot see the writer's own fence or quarantine evidence; every consumer |
| 38 | // that decides exclusion uses ValidateTerminationFenceAuthority instead. |
| 39 | func ValidateTerminationFence(o Observed, ref string, f termfence.Record, now time.Time) error { |
| 40 | b, ok := o.Roster.Boxes[ref] |
| 41 | at, err := time.Parse(time.RFC3339Nano, f.ObservedAt) |
| 42 | if !ok || err != nil || at.IsZero() || at.After(now.Add(5*time.Second)) || f.V != 1 || f.Box != ref || f.Enrollment != b.Enrollment || f.Cloud != b.Cloud { |
| 43 | return ErrRoster |
| 44 | } |
| 45 | if (f.Via != termfence.ViaDescribe || f.RequestID == "") && (f.Via != termfence.ViaEvent || f.EventID == "") { |
| 46 | return ErrRoster |
| 47 | } |
| 48 | // A box never witnesses its own termination. |
| 49 | if f.WrittenBy.Box == ref { |
| 50 | return ErrRoster |
| 51 | } |
| 52 | writer, ok := o.Roster.Boxes[f.WrittenBy.Box] |
| 53 | if !ok || writer.State != "active" || writer.Enrollment != f.WrittenBy.Enrollment || writer.Region != b.Region || writer.Cloud.Account != b.Cloud.Account { |
| 54 | return ErrRoster |
| 55 | } |
| 56 | _, err = termfence.Verify( |
| 57 | termfence.Box{Ref: ref, Enrollment: b.Enrollment, Cloud: b.Cloud}, |
| 58 | termfence.Box{Ref: f.WrittenBy.Box, Enrollment: writer.Enrollment, Cloud: writer.Cloud}, |
| 59 | termfence.Observation{Via: f.Via, Found: true, InstanceID: f.Cloud.InstanceID, Account: f.Cloud.Account, Region: f.Cloud.Region, State: f.State, RequestID: f.RequestID, EventID: f.EventID}, at) |
| 60 | if err != nil { |
| 61 | return ErrRoster |
| 62 | } |
| 63 | return nil |
| 64 | } |
| 65 | |
| 66 | // ValidateTerminationFenceAuthority requires a currently active, unfenced |
| 67 | // writer. A self-asserted observation time cannot establish historical authority. |
| 68 | // Missing evidence is accepted only when the store proves the object absent. |
| 69 | func (d Directory) ValidateTerminationFenceAuthority(ctx context.Context, o Observed, ref string, f termfence.Record, now time.Time) error { |
| 70 | if err := ValidateTerminationFence(o, ref, f, now); err != nil { |
| 71 | return err |
| 72 | } |
| 73 | if d.Store == nil { |
| 74 | return ErrRoster |
| 75 | } |
| 76 | writer := o.Roster.Boxes[f.WrittenBy.Box] |
| 77 | _, _, err := d.Store.Get(ctx, "control/box-fences/"+writer.Enrollment+".json") |
| 78 | if errors.Is(err, os.ErrNotExist) { |
| 79 | return nil |
| 80 | } |
| 81 | if err != nil { |
| 82 | return err |
| 83 | } |
| 84 | return ErrRoster |
| 85 | } |
| 86 | |