Files
mesh-host/internal/link/handacts.go
T

662 lines
23 KiB
Go

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 `<node>.act.<id>`, `<node>.open.<act>.<digest>` and
// `<node>.ask.<id>`, 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")
}
acts, err := h.read()
if err != nil {
return nil, err
}
for i := len(acts) - 1; i >= 0; i-- {
a := acts[i]
if a.Act == ActSetuidSearch && a.State == HandDone && now.Sub(a.At) < SearchEvery {
return nil, fmt.Errorf("a search was started at %s by %s, and one is started in any hour: the next may start "+
"at %s", a.At.Format(time.RFC3339), a.Caller, a.At.Add(SearchEvery).Format(time.RFC3339))
}
}
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
}
// ---- 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 {
acts, err := h.read()
if err != nil {
return true // unread is never a reason to ask again
}
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)
}
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
}