package link import ( "crypto/rand" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "sort" "strconv" "strings" "sync" "time" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/asker" "git.novox.be/novox/mesh-sdk/go/asks" ) // The node seat node-engine's acts (novox/hq ADR 0563). // // A hand-over and an access act are the engine's own asks, performed only on the warrant (§3): a call performs nothing. // The engine judges every value as it does for the controller's signed ask, then refuses in words or asks the operator // under its own asker name (asks.EngineAsker) at the level approve, binding the digest of the verb, the node, every // argument and the owner, group and mode it will set. A warrant is claimed once by compare-and-set in the engine's own // state (asker.Client.Take), held to the ask it answers, and to the act bound and the disk as judged, judged again now; // only then is the act performed and recorded with the warrant. A decline, an expiry or a changed disk says refused and // changes nothing. One ask is open per path or access. // // The read-only verbs need no warrant (§2): judge-directories changes nothing; setuid-search is bounded here, one at a // time and at most one started in an hour. Every act, with who asked as the runtime proved it, is kept in the engine's own // declared state acts (ActsBucket, created by the grants part), under its own node's keys alone, read by the verb acts. // The acts of the seat, as its verbs name them. const ( ActHandOver = "hand-over" ActAccess = "access-act" ActJudgeDirectories = "judge-directories" ActSetuidSearch = "setuid-search" ) // The states of an act. const ( HandAsked = "asked" HandDone = "done" HandRefused = "refused" HandAnswered = "answered" ) // SearchEvery is how often a setuid search may be started on a node (ADR 0563 §2): one in any hour. const SearchEvery = time.Hour // askMargin is how long past an ask's expiry the router is given to say it ended, before the engine closes it itself. const askMargin = 5 * time.Minute // ActsBucket is the node-engine's declared state acts (its manifest's state, ADR 0563 §2, §3), the bucket the grants // part creates (mesh-sdk buscompose EngineActsBucket): keys `.act.`, `.open..` and // `.ask.`, each written by this node's engine alone. const ActsBucket = "mesh-host_acts" // Judged is what the engine judged of one act before it asks: what it is about, what it will set (the owner, group and // mode, in words), and what the disk holds now (Disk), compared again when the warrant arrives. type Judged struct { // Subject is the path of a hand-over, or the access of an access act. Subject string // Path is the directory the act changes, as the engine states it. Path string // Sets is the owner, group and mode the act will set, in words; bound in the ask's digest. Sets string // Disk is what the engine read on disk when it judged: owner, group, mode and the file it is. Disk string // Words is what the operator is shown of it, without a path (ADR 0234 §6): the path is in the whole words alone. Words string } // HandAct is one act asked through the seat, as acts reads it. type HandAct struct { ID string `json:"id"` Act string `json:"act"` Subject string `json:"subject,omitempty"` Caller string `json:"caller"` Why string `json:"why,omitempty"` At time.Time `json:"at"` State string `json:"state"` Said string `json:"said,omitempty"` Ended time.Time `json:"ended,omitempty"` // Ask is the operator's ask of a hand-over or an access act; Sets and Disk what was judged when it was asked. Ask string `json:"ask,omitempty"` Sets string `json:"sets,omitempty"` Disk string `json:"disk,omitempty"` Path string `json:"path,omitempty"` // Expires is when the ask ends unanswered. Expires time.Time `json:"expires,omitempty"` // By, Via, Proofs are the operator's warrant, recorded with the act (ADR 0563 §3). By string `json:"by,omitempty"` Via string `json:"via,omitempty"` Proofs []string `json:"proofs,omitempty"` // Auto says the engine asked it itself, on finding a library's root holding data outside its group (§4). Auto bool `json:"auto,omitempty"` } // RootVerdict is the engine's answer to root-free (ADR 0563 §6), in the controller's words. type RootVerdict struct { Machine string `json:"machine"` Free bool `json:"free"` Why string `json:"why"` Judged time.Time `json:"judged"` } // HandActs is the engine's side of the seat. Every func is the engine's own judgement or act; nil refuses. type HandActs struct { Node string // State is where the acts and the asks are kept: this node's keys of ActsBucket (BusState), each key one act. State ActStore // Asker asks the operator as asks.EngineAsker(Node), with State as its store. Asker *asker.Client // JudgeHandOver judges a hand-over's path, refusing in words; HandOver records it on the warrant. JudgeHandOver func(path string) (Judged, error) HandOver HandOverFunc // JudgeAccess judges an access act on an access, refusing in words; AccessAct runs it on the warrant. JudgeAccess func(act, access string) (Judged, error) AccessAct AccessActFunc // JudgeDirectories, SetuidSearch and Searching are the read-only verbs' and whether a search runs now. JudgeDirectories JudgeDirectoriesFunc SetuidSearch SetuidSearchFunc Searching func() bool // RootFree is this machine's root-free verdict now. RootFree func() RootVerdict // Say says one of the seat's events, asked, done or refused; nil says nothing. Say func(event string, body any) Now func() time.Time // Nonce keeps two asks of one second apart; nil is random. Nonce func() string mu sync.Mutex } func (h *HandActs) now() time.Time { if h.Now != nil { return h.Now() } return time.Now() } func (h *HandActs) nonce() string { if h.Nonce != nil { return h.Nonce() } var b [2]byte _, _ = rand.Read(b[:]) return hex.EncodeToString(b[:]) } func (h *HandActs) say(event string, a HandAct) { if h.Say != nil { h.Say(event, map[string]any{"act": a.Act, "subject": a.Subject, "caller": a.Caller, "ask": a.Ask, "state": a.State, "said": a.Said, "node": h.Node}) } } // Call is the seat's one entry: a verb, the caller as the runtime proved it, and its arguments. func (h *HandActs) Call(c EngineCall) (any, error) { arg := func(k string) string { s, _ := c.Args[k].(string); return strings.TrimSpace(s) } switch c.Verb { case ActHandOver: return h.AskHandOver(c.Caller, arg("path"), arg("why"), false) case ActAccess: return h.AskAccess(c.Caller, arg("act"), arg("access"), arg("why"), false) case ActJudgeDirectories: return h.Judge(c.Caller) case ActSetuidSearch: return h.Search(c.Caller, arg("why")) case "root-free": if h.RootFree == nil { return nil, errors.New("this engine judges no machine root-free") } return map[string]any{"machines": []RootVerdict{h.RootFree()}}, nil case "acts": if id := arg("id"); id != "" { return h.Act(id) } limit, _ := strconv.Atoi(arg("limit")) if f, ok := c.Args["limit"].(float64); ok { limit = int(f) } return h.Acts(limit) } return nil, fmt.Errorf("the node seat %s serves no verb %q", EngineSeat, c.Verb) } // ---- the record -------------------------------------------------------------------------------------------------- // ActStore is the engine's own state, this node's keys alone (BusState prefixes them with the node): each act one key, // written by compare-and-set on its revision, an open ask one key made by a create. type ActStore interface { Get(key string) (*stdio.StateEntry, error) Create(key string, value any) (uint64, error) Update(key string, value any, revision uint64) (uint64, error) Delete(key string) error // Keys are the keys under prefix. Keys(prefix string) ([]string, error) } func actKey(id string) string { return "act." + id } // openKey is the one key an open ask of act on subject holds: made by a create, so a second ask of it is refused. func openKey(act, subject string) string { sum := sha256.Sum256([]byte(subject)) return "open." + act + "." + hex.EncodeToString(sum[:12]) } // read is every act kept, oldest first. func (h *HandActs) read() ([]HandAct, error) { keys, err := h.State.Keys("act.") if err != nil { return nil, fmt.Errorf("the engine's acts cannot be listed: %w", err) } var out []HandAct for _, k := range keys { a, _, err := h.readOne(k) if err != nil { return nil, err } if a != nil { out = append(out, *a) } } sort.SliceStable(out, func(i, j int) bool { return out[i].At.Before(out[j].At) }) return out, nil } func (h *HandActs) readOne(key string) (*HandAct, uint64, error) { e, err := h.State.Get(key) if err != nil || e == nil { return nil, 0, err } var a HandAct if err := json.Unmarshal(e.Value, &a); err != nil { return nil, 0, fmt.Errorf("the engine's act %s cannot be read: %w", key, err) } return &a, e.Revision, nil } func (h *HandActs) record(a HandAct) error { _, err := h.State.Create(actKey(a.ID), a) return err } // end marks the open act of an ask ended, once, by compare-and-set on its revision, and frees its open key: false when // it was not open. func (h *HandActs) end(ask string, set func(*HandAct)) (HandAct, bool, error) { for range 5 { a, rev, err := h.readOne(actKey(ask)) if err != nil || a == nil || a.State != HandAsked { return HandAct{}, false, err } set(a) a.Ended = h.now().UTC() if _, err := h.State.Update(actKey(ask), *a, rev); errors.Is(err, stdio.ErrChanged) { continue } else if err != nil { return HandAct{}, false, err } _ = h.State.Delete(openKey(a.Act, a.Subject)) return *a, true, nil } return HandAct{}, false, errors.New("the engine's act " + ask + " kept changing under its write") } // Act is the verb acts with an id: that one act. func (h *HandActs) Act(id string) (any, error) { a, _, err := h.readOne(actKey(id)) if err != nil { return nil, err } if a == nil { return nil, fmt.Errorf("this engine keeps no act %q", id) } return map[string]any{"node": h.Node, "acts": []HandAct{*a}}, nil } // Acts is the verb acts: every act kept, newest first, at most limit when given. func (h *HandActs) Acts(limit int) (any, error) { acts, err := h.read() if err != nil { return nil, err } out := make([]HandAct, 0, len(acts)) for i := len(acts) - 1; i >= 0; i-- { out = append(out, acts[i]) } if limit > 0 && len(out) > limit { out = out[:limit] } return map[string]any{"node": h.Node, "acts": out}, nil } // ---- the read-only verbs (§2) ------------------------------------------------------------------------------------- // Judge is judge-directories: the engine's judgement, read-only, recorded with its caller. func (h *HandActs) Judge(caller string) (any, error) { if h.JudgeDirectories == nil { return nil, errors.New("this engine judges no directory") } said, err := h.JudgeDirectories(caller) a := HandAct{ID: h.id(ActJudgeDirectories), Act: ActJudgeDirectories, Caller: caller, At: h.now().UTC(), State: HandAnswered} if err != nil { a.State, a.Said = HandRefused, err.Error() } if rerr := h.record(a); rerr != nil && err == nil { return nil, rerr } if err != nil { return nil, err } return map[string]any{"node": h.Node, "said": said}, nil } // Search is setuid-search: refused while one runs and when one was started within the hour, naming when the next may // start; otherwise the last search is thrown away and a full one starts, recorded with its caller. func (h *HandActs) Search(caller, why string) (any, error) { if h.SetuidSearch == nil { return nil, errors.New("this engine keeps no search for setuid programs") } h.mu.Lock() defer h.mu.Unlock() now := h.now().UTC() if h.Searching != nil && h.Searching() { return nil, errors.New("a search for setuid programs runs on this machine now; no second is started while it does") } // The last start is a key of its own, claimed by compare-and-set before the search starts: no number of other acts, // nor the record of acts, moves it (ADR 0563 §2). if err := h.claimSearch(now, caller); err != nil { return nil, err } said, err := h.SetuidSearch(caller) a := HandAct{ID: h.id(ActSetuidSearch), Act: ActSetuidSearch, Caller: caller, Why: why, At: now, State: HandDone, Said: said} if err != nil { a.State, a.Said = HandRefused, err.Error() } if rerr := h.record(a); rerr != nil { return nil, rerr } if err != nil { return nil, err } return map[string]any{"node": h.Node, "said": said}, nil } // searchKey keeps the last search's start on this node, and libKey each library's own take ask: keys of their own, which // no other act's key moves, kept for the bucket's age (ninety days, the node-engine's manifest). const searchKey = "search.last" func libKey(access string) string { sum := sha256.Sum256([]byte(access)) return "lib." + hex.EncodeToString(sum[:12]) + ".asked" } // started is the last search's start as kept, and who asked it. type started struct { At time.Time `json:"at"` By string `json:"by"` } // claimSearch refuses a search within SearchEvery of the last start, naming when the next may start, and otherwise // claims this one by compare-and-set: a second claim at the same moment loses. func (h *HandActs) claimSearch(now time.Time, caller string) error { e, err := h.State.Get(searchKey) if err != nil { return err } var rev uint64 if e != nil { var last started if err := json.Unmarshal(e.Value, &last); err != nil { return fmt.Errorf("the last search's start cannot be read, so none is started: %w", err) } if now.Sub(last.At) < SearchEvery { return fmt.Errorf("a search was started at %s by %s, and one is started in any hour: the next may start "+ "at %s", last.At.Format(time.RFC3339), last.By, last.At.Add(SearchEvery).Format(time.RFC3339)) } rev = e.Revision } claim := started{At: now, By: caller} if rev == 0 { _, err = h.State.Create(searchKey, claim) } else { _, err = h.State.Update(searchKey, claim, rev) } if errors.Is(err, stdio.ErrChanged) { return errors.New("another search was started at this moment; no second is started") } return err } // ---- the asks (§3, §4) -------------------------------------------------------------------------------------------- func (h *HandActs) id(act string) string { return act + "-" + h.now().UTC().Format("20060102t150405") + "-" + h.nonce() } // bound is the act an ask's yes binds: the verb, the node, every argument and what it will set (ADR 0563 §3). func (h *HandActs) bound(a HandAct) asks.Act { act := asks.Act{"verb": a.Act, "node": h.Node, "arg.subject": a.Subject, "arg.why": a.Why, "sets": a.Sets} if a.Path != "" { act["path"] = a.Path } return act } // AskHandOver is hand-over: judged, refused in words or asked of the operator. auto is the engine asking by itself. func (h *HandActs) AskHandOver(caller, path, why string, auto bool) (any, error) { if h.JudgeHandOver == nil || h.HandOver == nil { return nil, errors.New("this engine takes no hand-over") } j, err := h.JudgeHandOver(path) if err != nil { return nil, err } return h.ask(ActHandOver, caller, why, auto, j) } // AskAccess is access-act: the act take on an access, judged on the library's home, refused or asked of the operator. func (h *HandActs) AskAccess(caller, act, access, why string, auto bool) (any, error) { if h.JudgeAccess == nil || h.AccessAct == nil { return nil, errors.New("this engine runs no act on an accessed folder") } if act != ActTake { return nil, fmt.Errorf("the act asked is %q, and the one act on an access is %q", act, ActTake) } j, err := h.JudgeAccess(act, access) if err != nil { return nil, err } return h.ask(ActAccess, caller, why, auto, j) } // AskedFor says whether an act on subject was ever asked by the engine itself: a library's take is asked by itself once // (ADR 0563 §4), and again only by hand. func (h *HandActs) AskedFor(act, subject string) bool { if act == ActAccess { e, err := h.State.Get(libKey(subject)) return err != nil || e != nil // unread is never a reason to ask again } acts, err := h.read() if err != nil { return true } for _, a := range acts { if a.Act == act && a.Subject == subject && (a.Auto || a.State == HandAsked) { return true } } return false } func (h *HandActs) ask(act, caller, why string, auto bool, j Judged) (any, error) { if h.Asker == nil { return nil, errors.New("this engine reaches no operator to ask: nothing was asked and nothing changed") } h.mu.Lock() defer h.mu.Unlock() now := h.now().UTC() a := HandAct{ID: h.id(act), Act: act, Subject: j.Subject, Path: j.Path, Caller: caller, Why: why, At: now, State: HandAsked, Sets: j.Sets, Disk: j.Disk, Expires: now.Add(asks.ApproveLasts - time.Minute), Auto: auto} a.Ask = a.ID // One ask open per path or access: its open key made by a create, which a second ask cannot make. if _, err := h.State.Create(openKey(act, j.Subject), map[string]string{"ask": a.Ask}); errors.Is(err, stdio.ErrChanged) { held := "another" if e, _ := h.State.Get(openKey(act, j.Subject)); e != nil { var o struct { Ask string `json:"ask"` } if json.Unmarshal(e.Value, &o) == nil && o.Ask != "" { held = o.Ask } } return nil, fmt.Errorf("%s of %s is asked already: the ask %s waits for the operator's answer", act, j.Subject, held) } else if err != nil { return nil, err } if err := h.record(a); err != nil { _ = h.State.Delete(openKey(act, j.Subject)) return nil, err } bind, err := asks.ActDigest(h.bound(a)) if err != nil { return nil, err } headline, does, stays := "Hand a directory on "+h.Node+" to the mesh?", "The mesh gives it "+j.Sets+".", "The directory stays as it was found." if act == ActAccess { headline, does, stays = "Keep the library "+strings.TrimPrefix(j.Subject, "library:")+" on "+h.Node+"?", "The library's group, group write and setgid are set on everything under its root.", "The library waits and nothing under its root changes." } if len([]rune(headline)) > asks.HeadlineLength { headline = string([]rune(headline)[:asks.HeadlineLength]) } asked := asks.Ask{ID: a.Ask, Headline: headline, Who: asks.Operator, Explanation: j.Words + " Your yes sets " + j.Sets + "; nothing changes until you answer.", Whole: j.Words + " (" + j.Path + ") Your yes sets " + j.Sets + "; nothing changes until you answer.", Details: "Asked by " + caller + " on " + h.Node + orWhy(why), Options: []asks.Option{{ID: "yes", Label: "Yes", Does: does, Level: asks.Approve, Binds: bind}, {ID: "no", Label: "No, leave it", Does: stays, Level: asks.Acknowledge}}, Expires: a.Expires, OnExpiry: stays, About: EngineSeat + "-" + h.Node + "-" + act} if err := h.Asker.Ask(asked); err != nil { _, _, _ = h.end(a.Ask, func(x *HandAct) { x.State, x.Said = HandRefused, "the operator could not be asked: "+err.Error() }) return nil, fmt.Errorf("the operator could not be asked, and nothing changed: %w", err) } // Once asked, a library's own take ask is kept's own take ask: once, in a key of its own (ADR 0563 §4). if auto && act == ActAccess { _, _ = h.State.Create(libKey(j.Subject), map[string]any{"ask": a.Ask, "at": now}) } h.say(HandAsked, a) return map[string]any{"asked": a.Ask, "node": h.Node, "says": "the operator is asked on their channel; only " + "their yes changes anything. acts shows where it stands."}, nil } func orWhy(why string) string { if why == "" { return "" } return ": " + why } // Warranted is the act on the operator's answer, handed once by asker.Client.Take after it held the warrant to the // ask: a no is recorded refused; a yes is held to the act the option bound and the disk as judged, judged again now, // and only then performed and recorded done with the warrant. func (h *HandActs) Warranted(o asks.Option, w asks.Warrant) error { a, _, err := h.readOne(actKey(w.Ask)) if err != nil { return err } if a == nil || a.State != HandAsked { return fmt.Errorf("the warrant answers the ask %s, which is not open here: nothing is done", w.Ask) } refuse := func(why string) error { ended, ok, err := h.end(w.Ask, func(x *HandAct) { x.State, x.Said = HandRefused, why; warrantOn(x, w) }) if err == nil && ok { h.say(HandRefused, ended) } return err } if o.ID != "yes" { return refuse("the operator said no") } var j Judged switch a.Act { case ActHandOver: j, err = h.JudgeHandOver(a.Subject) case ActAccess: j, err = h.JudgeAccess(ActTake, a.Subject) default: err = fmt.Errorf("the act %s is not performed on a warrant", a.Act) } if err != nil { return refuse("judged again on the operator's yes, it is refused: " + err.Error()) } now := *a now.Sets, now.Path = j.Sets, j.Path if err := o.Performs(h.bound(now)); err != nil { return refuse("what the yes bound is not what would be done now: " + err.Error()) } if j.Disk != a.Disk { return refuse("the disk changed since the operator was asked (" + a.Disk + ", now " + j.Disk + "): ask again") } // Recorded with the way it came (ADR 0563 §5): the operator's yes to this engine's ask, not a terminal's line. by := operatorOf(w) + ", to " + EngineSeat + "'s ask " + w.Ask var said string switch a.Act { case ActHandOver: said, err = h.HandOver(a.Subject, by) case ActAccess: said, err = h.AccessAct(AccessAct{Act: ActTake, Access: a.Subject, Path: j.Path, Warrant: w.Ask, By: by}) } if err != nil { return refuse(err.Error()) } ended, ok, err := h.end(w.Ask, func(x *HandAct) { x.State, x.Said = HandDone, said; warrantOn(x, w) }) if err == nil && ok { h.say(HandDone, ended) } return err } func warrantOn(a *HandAct, w asks.Warrant) { a.Proofs = w.Proofs if w.By != nil { a.By, a.Via = w.By.Who, w.By.Kind if w.By.Verified != "" { a.Via += " (" + w.By.Verified + ")" } } } func operatorOf(w asks.Warrant) string { if w.By == nil { return "the operator" } return "the " + w.By.Who + " via " + w.By.Kind } // Heard is a warrant heard on the engine's decided subject: taken once (asker.Client.Take), and an ask that ended with // no choice recorded refused. func (h *HandActs) Heard(w asks.Warrant) { if h.Asker == nil { return } // A warrant the ask kept here does not hold to (another digest, another option, too late) ends nothing: it is no // answer to the ask, which stays open for the operator's. if handled, _ := h.Asker.Take(w, h.Warranted); handled == asker.Recorded { why := "the ask ended " + string(w.Outcome) if w.Words != "" { why += ": " + w.Words } if ended, ok, _ := h.end(w.Ask, func(x *HandAct) { x.State, x.Said = HandRefused, why; warrantOn(x, w) }); ok { h.say(HandRefused, ended) } } } // Reconcile catches up what the engine missed while it was away: a warrant the router holds for an open ask is taken, // and an ask past its expiry and a margin is closed refused, expired. func (h *HandActs) Reconcile() { if h.Asker == nil { return } acts, err := h.read() if err != nil { return } for _, a := range acts { if a.State != HandAsked || a.Ask == "" { continue } if _, w, err := h.Asker.Where(a.Ask); err == nil && w != nil { h.Heard(*w) continue } if !a.Expires.IsZero() && h.now().After(a.Expires.Add(askMargin)) { if ended, ok, _ := h.end(a.Ask, func(x *HandAct) { x.State, x.Said = HandRefused, "the ask expired unanswered" }); ok { h.say(HandRefused, ended) } } } } // MemState is an ActStore in memory: an engine's acts while no bus is reached in a test. type MemState struct { mu sync.Mutex keys map[string]stdio.StateEntry top uint64 } func (m *MemState) Get(key string) (*stdio.StateEntry, error) { m.mu.Lock() defer m.mu.Unlock() if e, ok := m.keys[key]; ok { return &e, nil } return nil, nil } func (m *MemState) Create(key string, value any) (uint64, error) { return m.put(key, value, 0, true) } func (m *MemState) Update(key string, value any, revision uint64) (uint64, error) { return m.put(key, value, revision, false) } func (m *MemState) put(key string, value any, revision uint64, create bool) (uint64, error) { raw, err := json.Marshal(value) if err != nil { return 0, err } m.mu.Lock() defer m.mu.Unlock() was, kept := m.keys[key] if (create && kept) || (!create && (!kept || was.Revision != revision)) { return 0, stdio.ErrChanged } if m.keys == nil { m.keys = map[string]stdio.StateEntry{} } m.top++ m.keys[key] = stdio.StateEntry{Key: key, Value: raw, Revision: m.top} return m.top, nil } func (m *MemState) Delete(key string) error { m.mu.Lock() defer m.mu.Unlock() delete(m.keys, key) return nil } func (m *MemState) Keys(prefix string) ([]string, error) { m.mu.Lock() defer m.mu.Unlock() var out []string for k := range m.keys { if strings.HasPrefix(k, prefix) { out = append(out, k) } } sort.Strings(out) return out, nil }