carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package hostactors
2
3import (
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.
14func (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.
39func 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.
69func (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