diff --git a/go/process/call_integration_test.go b/go/process/call_integration_test.go index 5da784ec..4b0a90f3 100644 --- a/go/process/call_integration_test.go +++ b/go/process/call_integration_test.go @@ -6,6 +6,7 @@ import ( "context" "encoding/json" "errors" + "git.novox.be/novox/mesh-sdk/go/seatdef" "strings" "sync" "sync/atomic" @@ -285,9 +286,14 @@ func TestCallOnReachesTheNamedNodeAlone(t *testing.T) { } } +// brokerSeat is the fixture seat's definition as a seat list holds it: its three verbs, each its class's default. +func brokerSeat() (seatdef.Definition, bool) { + return seatdef.Definition{Seat: "broker-seat", Verbs: []seatdef.Verb{{Name: "ping"}, {Name: "fail"}, {Name: "boom"}}}, true +} + // A-107: a process answers its seat's verbs over the bus. Given two instances of one holder answering seat // `broker-seat`'s verbs, a call on the caller-named subject reaches exactly one, which is handed the caller the -// subject names (dots written `~`) and replies {result, node}; a call on the subject naming none is handed ""; a +// subject names (dots written `~`) and replies {result, node}; a call on the subject naming none is refused; a // handler's error is the reply's error, a panic too; and after stop nothing answers. func TestAnswerSeatServesVerbsWithTheProvedCallerInOneQueueGroup(t *testing.T) { url, account := grantedSendProcess(t) @@ -316,7 +322,7 @@ func TestAnswerSeatServesVerbsWithTheProvedCallerInOneQueueGroup(t *testing.T) { t.Fatal(err) } defer conn.Close() - stop, err := conn.AnswerSeat("broker-seat", verbs(name)) + stop, err := conn.AnswerSeat("broker-seat", brokerSeat, verbs(name)) if err != nil { t.Fatalf("AnswerSeat: %v", err) } @@ -343,14 +349,18 @@ func TestAnswerSeatServesVerbsWithTheProvedCallerInOneQueueGroup(t *testing.T) { if err != nil || r.Result["caller"] != "novox.mesh-delivery-builder" || r.Result["body"] != `{"x":1}` || r.Node != "novox" { t.Fatalf("a caller-named call: %+v, %v", r, err) } + // An unproven caller is refused before the handler runs (design 47 G §610; lead answers A-165, A-179, A-186). r, err = ask("mesh.seat.broker-seat.tool.ping") - if err != nil || r.Result["caller"] != "" { + if err != nil || r.Result != nil || !strings.Contains(r.Error, "broker-seat.ping takes no unproven caller") { t.Fatalf("an unproven call: %+v, %v", r, err) } + if _, err := ask("mesh.seat.broker-seat.call.ping.novox~mesh-delivery-builder"); err != nil { + t.Fatal(err) + } total := int32(0) heard.Range(func(_, v any) bool { total += atomic.LoadInt32(v.(*int32)); return true }) if total != 2 { - t.Errorf("two calls were answered %d times in all, want once each (one queue group)", total) + t.Errorf("two proven calls were answered %d times in all, want once each (one queue group) and the unproven none", total) } if r, err = ask("mesh.seat.broker-seat.call.fail.x"); err != nil || r.Error != "refused: not yours" { t.Errorf("a handler's error: %+v, %v", r, err) diff --git a/go/process/call_test.go b/go/process/call_test.go index 750e981f..3ea2413a 100644 --- a/go/process/call_test.go +++ b/go/process/call_test.go @@ -3,6 +3,7 @@ package process import ( "context" "encoding/json" + "git.novox.be/novox/mesh-sdk/go/seatdef" "strings" "testing" "time" @@ -138,7 +139,9 @@ func TestACallIsAskedAgainOnlyWhenItReachedNoOne(t *testing.T) { } // A-107: AnswerSeat reads the caller from a call's last token (dots written `~`), "" for anything that is not a -// bus user's name; and it refuses what it cannot serve before subscribing. +// bus user's name; and it refuses what it cannot serve before subscribing. A-179, A-186: before a handler runs, an +// unproven caller is refused, and every caller while the seat's definition is unread, names no such verb or opens it to +// none; a proven caller of a verb the definition names runs it. func TestUserOfTokenAndAnswerSeatRefusals(t *testing.T) { for token, want := range map[string]string{"novox~mesh-delivery-builder": "novox.mesh-delivery-builder", "person~jo": "person.jo", "plain": "plain", "": "", "a b": "", "a*": "", "~": ""} { @@ -148,9 +151,36 @@ func TestUserOfTokenAndAnswerSeatRefusals(t *testing.T) { } c := &Conn{} ok := func(string, json.RawMessage) (any, error) { return nil, nil } + defined := func() (seatdef.Definition, bool) { + return seatdef.Definition{Seat: "seat", Verbs: []seatdef.Verb{{Name: "v"}}}, true + } for name, verbs := range map[string]map[string]SeatHandler{"none": nil, "nil handler": {"v": nil}, "bad verb": {"a b": ok}, "wildcard": {"*": ok}} { - if _, err := c.AnswerSeat("seat", verbs); err == nil { + if _, err := c.AnswerSeat("seat", defined, verbs); err == nil { t.Errorf("AnswerSeat accepted %s", name) } } + if _, err := c.AnswerSeat("seat", nil, map[string]SeatHandler{"v": ok}); err == nil { + t.Error("AnswerSeat accepted no definition") + } + ran := 0 + run := func(string, json.RawMessage) (any, error) { ran++; return "ran", nil } + for _, row := range []struct { + name, caller string + def func() (seatdef.Definition, bool) + says string + }{ + {"an unproven caller", "", defined, "takes no unproven caller"}, + {"the definition unread", "novox.mesh-recovery", func() (seatdef.Definition, bool) { return seatdef.Definition{}, false }, "not yet read"}, + {"a verb the definition does not name", "novox.mesh-recovery", func() (seatdef.Definition, bool) { return seatdef.Definition{Seat: "seat"}, true }, "names no verb v"}, + {"a verb open to none", "novox.mesh-recovery", func() (seatdef.Definition, bool) { + return seatdef.Definition{Seat: "seat", Verbs: []seatdef.Verb{{Name: "v", Callers: &seatdef.Callers{List: []string{}}}}}, true + }, "open to no caller"}, + {"a proven caller", "novox.mesh-recovery", defined, ""}, + } { + before := ran + _, err := admitted("seat", "v", row.def, run)(row.caller, nil) + if row.says == "" && (err != nil || ran != before+1) || row.says != "" && (err == nil || !strings.Contains(err.Error(), row.says) || ran != before) { + t.Errorf("%s: %v, ran %d times", row.name, err, ran-before) + } + } } diff --git a/go/process/serve.go b/go/process/serve.go index 03bbc79b..6fb217db 100644 --- a/go/process/serve.go +++ b/go/process/serve.go @@ -10,6 +10,7 @@ import ( "github.com/nats-io/nats.go" "git.novox.be/novox/mesh-sdk/go/calladdress" + "git.novox.be/novox/mesh-sdk/go/seatdef" ) // SeatHandler answers one call of a seat's verb: caller is the bus user the subject the call arrived on names @@ -35,7 +36,16 @@ func userOfToken(token string) string { // of the seat's holder subscribes in one queue group per seat, so a call reaches one of them. The reply is // {result, error, node}, as Call reads it. The returned stop ends every subscription. Nothing is subscribed when // any verb is malformed or a subscription is refused. -func (c *Conn) AnswerSeat(seat string, verbs map[string]SeatHandler) (stop func(), err error) { +// +// Before a handler runs, the call's caller is checked against the verb's effective callers (design 47 G §610; lead +// answers A-165, A-179, A-186): definition answers the seat's definition from the seat list the process follows, false +// while it is not read. An unproven caller is refused, and so is every caller while the definition is unread, names no +// such verb or opens it to none; a proven caller is among them, since the grants part composes a verb's call subject +// only for its effective callers. +func (c *Conn) AnswerSeat(seat string, definition func() (seatdef.Definition, bool), verbs map[string]SeatHandler) (stop func(), err error) { + if definition == nil { + return nil, fmt.Errorf("AnswerSeat(%q) is given no definition of the seat to read its verbs' callers from", seat) + } if len(verbs) == 0 { return nil, fmt.Errorf("AnswerSeat(%q) answers no verb", seat) } @@ -58,7 +68,7 @@ func (c *Conn) AnswerSeat(seat string, verbs map[string]SeatHandler) (stop func( } } for _, verb := range names { - h := verbs[verb] + h := admitted(seat, verb, definition, verbs[verb]) base := "mesh.seat." + seat for _, subject := range []string{base + ".call." + verb + ".*", base + ".tool." + verb} { subject := subject @@ -77,6 +87,31 @@ func (c *Conn) AnswerSeat(seat string, verbs map[string]SeatHandler) (stop func( return undo, nil } +// admitted is h behind the check of its caller against the verb's effective callers (A-179, A-186). +func admitted(seat, verb string, definition func() (seatdef.Definition, bool), h SeatHandler) SeatHandler { + return func(caller string, body json.RawMessage) (any, error) { + if caller == "" { + return nil, fmt.Errorf("%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, read := definition() + if !read { + return nil, fmt.Errorf("the definition of the seat %s is not yet read by this process's seat list, so no caller of %s.%s "+ + "is admitted", seat, seat, verb) + } + for _, v := range def.Verbs { + if v.Name != verb { + continue + } + if len(v.EffectiveCallers()) == 0 { + return nil, fmt.Errorf("%s.%s is open to no caller", seat, verb) + } + return h(caller, body) + } + return nil, fmt.Errorf("the seat %s's definition names no verb %s, so no caller is admitted", seat, verb) + } +} + // answerCall runs h for one call and replies {result, error, node}; a panic in h is the call's error, never silence. func (c *Conn) answerCall(msg *nats.Msg, pattern string, h SeatHandler) { caller := ""