A-165 callers: the seat server refuses a caller outside a verb's effective callers before its handler
conn.AnswerSeat takes the seat's definition from its caller (the seat list the process follows) and, before a handler runs, refuses an unproven caller, and 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 the call subject only for the effective callers. Every caller of AnswerSeat passes the definition. Builds: design 47 G §610; lead answers A-165, A-179, A-186 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -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)
|
||||
|
||||
+32
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
-2
@@ -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.<caller>", 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 := ""
|
||||
|
||||
Reference in New Issue
Block a user