| 1 | package hostbox |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/x509" |
| 6 | "github.com/carlosframework/platform/internal/park" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/boxid" |
| 9 | "github.com/carlosframework/platform/internal/hostclaim" |
| 10 | "github.com/carlosframework/platform/internal/hostwrite" |
| 11 | ) |
| 12 | |
| 13 | // Actor is the verified projection of the sole control/host-actors.json roster. |
| 14 | // Roster authenticates that document and its revision before returning a row. |
| 15 | type Actor struct { |
| 16 | FleetHomeRegion string |
| 17 | HomeRegion string |
| 18 | ControlBucket string |
| 19 | Box, Enrollment string |
| 20 | Revision uint64 |
| 21 | Region, Bucket, Namespace string |
| 22 | Cloud CloudIdentity |
| 23 | Eligible bool |
| 24 | Quarantined bool |
| 25 | // Binding is hostactors.BoxBinding of this box's row in the roster read |
| 26 | // that produced the actor: the digest installed state is stamped with and |
| 27 | // compared against. Unlike Revision, the roster's write counter, it moves |
| 28 | // only when this box's identity or state or the deployment's binding facts |
| 29 | // change (a quarantine, a readmission, a re-enrolment, a floor publish), |
| 30 | // never on holds, activations or other boxes' rows. |
| 31 | Binding string |
| 32 | } |
| 33 | |
| 34 | // Bound returns the actor with the roster write counter cleared: the part of |
| 35 | // the actor that names one enrollment under one binding. Persist this, not the |
| 36 | // actor, wherever a later comparison must survive unrelated roster writes. |
| 37 | func (a Actor) Bound() Actor { |
| 38 | a.Revision = 0 |
| 39 | return a |
| 40 | } |
| 41 | |
| 42 | // SameBinding reports whether two actors are the same enrollment under the |
| 43 | // same binding. Every field but Revision must match; Binding carries the |
| 44 | // quarantine revision and every other bound fact, so a box quarantined and |
| 45 | // readmitted since a is never SameBinding with it. |
| 46 | func SameBinding(a, b Actor) bool { |
| 47 | return a.Bound() == b.Bound() |
| 48 | } |
| 49 | |
| 50 | type CloudIdentity struct{ Provider, Account, Region, InstanceID string } |
| 51 | type Roster interface { |
| 52 | Actor(context.Context, string) (Actor, error) |
| 53 | } |
| 54 | |
| 55 | // BoxEvidence loads the exact bytes stored at boxid.Key(box). |
| 56 | type BoxEvidence interface { |
| 57 | Load(context.Context, string) ([]byte, error) |
| 58 | } |
| 59 | |
| 60 | type IdentityProvider struct { |
| 61 | HomeRegion string |
| 62 | AWSRegion string |
| 63 | Roster Roster |
| 64 | Evidence BoxEvidence |
| 65 | Account, Box, Region, Bucket, Namespace string |
| 66 | Certs map[string]*x509.Certificate |
| 67 | } |
| 68 | |
| 69 | func (p IdentityProvider) Verify(ctx context.Context) (Actor, error) { |
| 70 | if p.Roster == nil || p.Evidence == nil || p.Account == "" || p.Box == "" || p.Region == "" || p.AWSRegion == "" || p.Bucket == "" || p.Namespace == "" || len(p.Certs) == 0 { |
| 71 | return Actor{}, ErrUnsupported |
| 72 | } |
| 73 | a, err := p.Roster.Actor(ctx, p.Box) |
| 74 | if err != nil { |
| 75 | return Actor{}, err |
| 76 | } |
| 77 | if !p.actorMatches(a) { |
| 78 | return Actor{}, ErrChanged |
| 79 | } |
| 80 | raw, err := p.Evidence.Load(ctx, p.Box) |
| 81 | if err != nil { |
| 82 | return Actor{}, err |
| 83 | } |
| 84 | // The roster entry is the expected binding: Enroll refuses a record whose |
| 85 | // signed instance is not the one this reference is enrolled as. |
| 86 | cloud := boxid.Cloud{Provider: a.Cloud.Provider, Account: a.Cloud.Account, Region: a.Cloud.Region, InstanceID: a.Cloud.InstanceID} |
| 87 | ev, err := (boxid.Verifier{Account: p.Account, Certs: p.Certs}).Enroll(raw, boxid.Expect{ |
| 88 | Box: p.Box, |
| 89 | AWSRegion: a.Cloud.Region, |
| 90 | Roster: []boxid.Enrolled{{Box: a.Box, Cloud: cloud}}, |
| 91 | }) |
| 92 | if err != nil { |
| 93 | return Actor{}, err |
| 94 | } |
| 95 | if ev.Box != p.Box || ev.Cloud != cloud { |
| 96 | return Actor{}, ErrChanged |
| 97 | } |
| 98 | if p.HomeRegion != "" && (p.HomeRegion != a.FleetHomeRegion || p.HomeRegion != a.Region || park.PlacementRegion(p.HomeRegion) != p.HomeRegion) { |
| 99 | return Actor{}, ErrChanged |
| 100 | } |
| 101 | a.HomeRegion = p.HomeRegion |
| 102 | return a, nil |
| 103 | } |
| 104 | |
| 105 | func (p IdentityProvider) actorMatches(a Actor) bool { |
| 106 | return p.AWSRegion != "" && a.Box == p.Box && a.Enrollment != "" && a.Revision > 0 && a.Binding != "" && a.Eligible && !a.Quarantined && a.Region == p.Region && a.Bucket == p.Bucket && a.Namespace == p.Namespace && a.Cloud.Provider == "aws" && a.Cloud.Account == p.Account && a.Cloud.Region == p.AWSRegion && a.Cloud.InstanceID != "" |
| 107 | } |
| 108 | |
| 109 | // LocalAuthority is Plan 8's nonblocking local-state reader. It checks its own |
| 110 | // boot-clock deadline, loss latch, controller identity, quarantine and fences. |
| 111 | // No second execution lease state machine is introduced here. |
| 112 | type LocalAuthority interface { |
| 113 | Check(context.Context, Actor, *hostclaim.Claim, Selection) error |
| 114 | } |
| 115 | |
| 116 | func (p IdentityProvider) Local(authority LocalAuthority) func(context.Context, *hostclaim.Claim, Selection) error { |
| 117 | return func(ctx context.Context, c *hostclaim.Claim, s Selection) error { |
| 118 | if authority == nil { |
| 119 | return ErrUnsupported |
| 120 | } |
| 121 | actor, err := p.Verify(ctx) |
| 122 | if err != nil { |
| 123 | return err |
| 124 | } |
| 125 | if s.Box != actor.Box || s.Region != actor.Region || s.HomeRegion != actor.HomeRegion || s.FleetHomeRegion != actor.FleetHomeRegion { |
| 126 | return ErrChanged |
| 127 | } |
| 128 | return authority.Check(ctx, actor, c, s) |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | // Startup evaluates the switch before constructing any authority provider. |
| 133 | func Startup(ctx context.Context, getenv func(string) string, construct func(context.Context) (hostwrite.Selector, error)) (hostwrite.Config, error) { |
| 134 | c := hostwrite.Config{Enabled: hostwrite.Enabled(getenv)} |
| 135 | if !c.Enabled { |
| 136 | return c, nil |
| 137 | } |
| 138 | if construct == nil { |
| 139 | return c, hostwrite.ErrUnavailable |
| 140 | } |
| 141 | selector, err := construct(ctx) |
| 142 | if err != nil { |
| 143 | return c, err |
| 144 | } |
| 145 | if selector == nil { |
| 146 | return c, hostwrite.ErrUnavailable |
| 147 | } |
| 148 | c.Selector = selector |
| 149 | return c, nil |
| 150 | } |
| 151 | |