diff --git a/node-tools/cmd/node-tools/main.go b/node-tools/cmd/node-tools/main.go index 302afde..788dd82 100644 --- a/node-tools/cmd/node-tools/main.go +++ b/node-tools/cmd/node-tools/main.go @@ -85,7 +85,7 @@ func main() { Build: os.Getenv(runtime.FingerprintWord), Hear: hearOne(conn)}) // A failed read or write of the state approvals is made again at the next membership change the runner hears (A-157). conn.OnMembership(func(bus.Membership) { go gate.StateAgain() }) - opts = append(opts, runtime.WithGate(gate)) + opts = append(opts, runtime.WithGate(gate), runtime.WithSeatDefinitions(definitionsOf(seatList))) // A run still unrecorded is asked again at each start and when the hand-act seat's holder is heard again (lead answer A-157). if stopBounds, err := gate.WatchBounds(); err != nil { log.Printf("[mesh-tools] the gate's bound keys are not watched (%v): an unanswered hand-act is said in the log only", err) diff --git a/node-tools/internal/runtime/caller_test.go b/node-tools/internal/runtime/caller_test.go index 7958b39..46c4c95 100644 --- a/node-tools/internal/runtime/caller_test.go +++ b/node-tools/internal/runtime/caller_test.go @@ -2,9 +2,12 @@ package runtime import ( "encoding/json" + "strings" "testing" "time" + "git.novox.be/novox/mesh-sdk/go/seatdef" + "github.com/nats-io/nats.go" "github.com/novox/mesh-tools/node-tools/internal/bus" @@ -24,7 +27,7 @@ func TestACallOnTheCallerNamedSubjectReachesTheModuleWithItsCaller(t *testing.T) mesh := mt.New(t) mesh.Issue(t, mt.MembershipOf("ledger", "anchor", true, map[string][]string{"node-desk": {"who"}})) nodeTools := connect(t, "node-tools", "anchor") - stop, err := Run(nodeTools, []Served{{"ledger", []string{mt.Fixture("caller-echo.py")}}}, nil, func(string, ...any) {}) + stop, err := Run(nodeTools, []Served{{"ledger", []string{mt.Fixture("caller-echo.py")}}}, nil, func(string, ...any) {}, deskDefined) if err != nil { t.Fatal(err) } @@ -62,13 +65,20 @@ func TestACallOnTheCallerNamedSubjectReachesTheModuleWithItsCaller(t *testing.T) } } -// The subject that names no caller is still served, for one release (retired by hq issue 464), and says so: -// the module is named no caller, whatever the body claims. +// deskDefined is the seat list's definition of the fixture seat node-desk, its verb who taking its class's default. +var deskDefined = WithSeatDefinitions(func() map[string]seatdef.Definition { + return map[string]seatdef.Definition{"node-desk": {Seat: "node-desk", Verbs: []seatdef.Verb{{Name: "who"}}}} +}) + +// The subject that names no caller is still served for a module's tool, for one release (retired by hq issue 464), and says +// so: the module is named no caller, whatever the body claims. A seat's verb refuses it before the bundle runs (design 47 G +// §610; lead answers A-165, A-179), and refuses every caller while the seat's definition is unread, names no such verb or +// opens it to none. func TestTheSubjectThatNamesNoCallerStillWorksAndNamesNone(t *testing.T) { mesh := mt.New(t) mesh.Issue(t, mt.MembershipOf("ledger", "anchor", true, map[string][]string{"node-desk": {"who"}})) nodeTools := connect(t, "node-tools", "anchor") - stop, err := Run(nodeTools, []Served{{"ledger", []string{mt.Fixture("caller-echo.py")}}}, nil, func(string, ...any) {}) + stop, err := Run(nodeTools, []Served{{"ledger", []string{mt.Fixture("caller-echo.py")}}}, nil, func(string, ...any) {}, deskDefined) if err != nil { t.Fatal(err) } @@ -91,8 +101,29 @@ func TestTheSubjectThatNamesNoCallerStillWorksAndNamesNone(t *testing.T) { } return err }) + if strings.HasPrefix(subject, "mesh.seat.") { + if !strings.Contains(reply.Error, "node-desk.who takes no unproven caller") || reply.Result.Call != "" { + t.Errorf("%s: an unproven caller of a seat's verb was answered %+v (error %q), want refused before the bundle", subject, reply.Result, reply.Error) + } + continue + } if reply.Error != "" || reply.Result.Caller != "" { t.Errorf("%s: the module was named caller %q (error %q), want none", subject, reply.Result.Caller, reply.Error) } } + for _, c := range []struct { + defs map[string]seatdef.Definition + says string + }{ + {nil, "is not yet read"}, + {map[string]seatdef.Definition{"node-desk": {Seat: "node-desk"}}, "names no verb who"}, + {map[string]seatdef.Definition{"node-desk": {Seat: "node-desk", Verbs: []seatdef.Verb{{Name: "who", Callers: &seatdef.Callers{List: []string{}}}}}}, "open to no caller"}, + {map[string]seatdef.Definition{"node-desk": {Seat: "node-desk", Verbs: []seatdef.Verb{{Name: "who", Class: seatdef.ClassAct}}}}, ""}, + } { + defs := c.defs + err := (&consumers{definitions: func() map[string]seatdef.Definition { return defs }}).admitted("node-desk", "who", "shanks.node-tools") + if c.says == "" && err != nil || c.says != "" && (err == nil || !strings.Contains(err.Error(), c.says)) { + t.Errorf("a proven caller with the definitions %v: %v, want %q", c.defs, err, c.says) + } + } } diff --git a/node-tools/internal/runtime/gate.go b/node-tools/internal/runtime/gate.go index 649bec5..7c8020e 100644 --- a/node-tools/internal/runtime/gate.go +++ b/node-tools/internal/runtime/gate.go @@ -1,6 +1,10 @@ package runtime import ( + "fmt" + + "git.novox.be/novox/mesh-sdk/go/seatdef" + "github.com/novox/mesh-tools/node-tools/internal/approval" "github.com/novox/mesh-tools/node-tools/internal/launch" ) @@ -33,6 +37,40 @@ func WithGate(g Gate) Option { return func(c *consumers) { c.gate = g } } var _ Gate = (*approval.Gate)(nil) +// WithSeatDefinitions refuses, where a seat's verb is answered, a caller outside the verb's effective callers, before the +// bundle runs (design 47 G §610; lead answers A-165, A-179): defs are the seat definitions the runner's seat list holds. +// Without it a verb is answered for every caller, as before. +func WithSeatDefinitions(defs func() map[string]seatdef.Definition) Option { + return func(c *consumers) { c.definitions = defs } +} + +// admitted says why a seat's verb is refused to a caller, nil when it is answered: an unproven caller is refused, and so is +// every caller while the seat's definition is not read, names no such verb, or the verb's effective callers are empty; a +// proven caller is among them, since the grants part composes a verb's call subject only for its effective callers (A-179). +func (c *consumers) admitted(seat, verb, caller string) error { + if c.definitions == nil { + return nil + } + if caller == "" { + return fmt.Errorf("Not run: %s.%s takes no unproven caller: call it on its subject that names its caller, "+ + "mesh.seat.%s.call.%s.", seat, verb, seat, verb) + } + def, ok := c.definitions()[seat] + if !ok { + return fmt.Errorf("Not run: the definition of the seat %s is not yet read by this tool runner's seat list, so no caller "+ + "of %s.%s is admitted", seat, seat, verb) + } + for _, v := range def.Verbs { + if v.Name == verb { + if len(v.EffectiveCallers()) == 0 { + return fmt.Errorf("Not run: %s.%s is open to no caller", seat, verb) + } + return nil + } + } + return fmt.Errorf("Not run: the seat %s's definition names no verb %s, so no caller is admitted", seat, verb) +} + // unservedMarks is, of the module's marked tools, those its bundle's registrations under its own name do not register. func (r *runner) unservedMarks(module string, regs []launch.Registration) []string { if r.events.gate == nil { diff --git a/node-tools/internal/runtime/runtime.go b/node-tools/internal/runtime/runtime.go index 5bbea33..15bfdc2 100644 --- a/node-tools/internal/runtime/runtime.go +++ b/node-tools/internal/runtime/runtime.go @@ -16,6 +16,8 @@ import ( "sync" "time" + "git.novox.be/novox/mesh-sdk/go/seatdef" + "github.com/novox/mesh-tools/node-tools/internal/announce" "github.com/novox/mesh-tools/node-tools/internal/approval" "github.com/novox/mesh-tools/node-tools/internal/bus" @@ -304,7 +306,7 @@ func Run(conn *bus.Conn, served []Served, envs map[string]map[string]string, log } logf("%s", line) conn.OnMembership(func(bus.Membership) { go func() { r.refreshRepairs(); conn.Flush() }() }) - stopSeats, again := serveSeats(conn, r.modules, r.registrations, r.events.gated, r.fingerprintOf, logf) + stopSeats, again := serveSeats(conn, r.modules, r.registrations, r.events.gated, r.events.admitted, r.fingerprintOf, logf) r.seatsAgain = again r.stops = append(r.stops, stopSeats) @@ -707,8 +709,8 @@ func subjectsOf(conn *bus.Conn, module, tool string) []string { // the tool of the same name registered under the seat's name (ADR 0159, 0160) — and serves again // whenever a membership changes, and when again is called because a bundle was loaded again. Whether // this machine holds the seat is the bus's to decide. It answers its stop and again. -func serveSeats(conn *bus.Conn, modules []string, current func() []registration, gated gatedRun, fingerprintOf func(string) string, - logf func(string, ...any)) (func(), func()) { +func serveSeats(conn *bus.Conn, modules []string, current func() []registration, gated gatedRun, + admitted func(seat, verb, caller string) error, fingerprintOf func(string) string, logf func(string, ...any)) (func(), func()) { var mu sync.Mutex var stops []func() stopped := false @@ -747,9 +749,13 @@ func serveSeats(conn *bus.Conn, modules []string, current func() []registration, logf("[mesh-tools] %s claims %s and implements no %s, which that seat promises; not served", module, v.Seat, v.Verb) continue } - // Through the approval gate, before the bundle (design 46 7-approval-mark §20). + // Its callers checked, then through the approval gate, before the bundle (design 47 G §610, A-179; design 46 + // 7-approval-mark §20). module, v := module, v stop, err := conn.HandleSubjectCalled(v.Subject, func(caller string, body json.RawMessage) (any, error) { + if err := admitted(v.Seat, v.Verb, caller); err != nil { + return nil, err + } return gated(approval.Call{Address: v.Seat + "." + v.Verb, Module: module, Seat: v.Seat, Name: v.Verb, Fingerprint: fingerprintOf(module), Caller: caller, Args: argsOf(body), Description: t.Description}, t, caller) }) @@ -794,6 +800,8 @@ type consumers struct { sessions sessionTable // gate is the approval gate every call is served through (gate.go); nil serves every call as before. gate Gate + // definitions are the seat definitions a seat verb's callers are read from (gate.go); nil admits every caller. + definitions func() map[string]seatdef.Definition } type moduleEvents struct {