Files
mesh-delivery/modules/mesh-grants/internal/grants/callers.go
T
jschoubben 6182730923 GR2: grants_callers and grants_why_refused
The first reads the callers each verb and accepted kind of the seat definitions
admits, the second says which call subject an account lacks (the bus's own
nats_user_can) beside the definition's callers.

Builds: design 47 G §83, §391, §395

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 05:58:25 +01:00

140 lines
4.9 KiB
Go

package grants
import (
"encoding/json"
"errors"
"fmt"
"slices"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"mesh-grants/internal/buscompose"
)
// The tools that say who may call and why a call is refused (design 47 G §83, GR2).
const (
ToolCallers = "grants_callers"
ToolWhyRefused = "grants_why_refused"
)
// CallerLine is one verb's, or one accepted kind's, callers as a seat definition gives them.
type CallerLine struct {
Seat string `json:"seat"`
// Verb is the verb's name, or the accepted kind's.
Verb string `json:"verb"`
// Kind is `verb` or `accepts`.
Kind string `json:"kind"`
Class string `json:"class,omitempty"`
// Callers are the entries the verb admits: the list the definition names, or its class's default.
Callers []string `json:"callers"`
// Named says the definition names the list; false is the class's default (§391).
Named bool `json:"named"`
}
// definitions are the seat definitions the part composes from, or why it has none.
func (p *Part) definitions() (map[string]seatdef.Definition, error) {
d, ok := p.SeatList.(SeatDefinitions)
if !ok {
return nil, errors.New("the seat list is not read here: the definitions' callers are unknown")
}
defs := d.Definitions()
if len(defs) == 0 {
return nil, errors.New("the seat list holds no definitions yet")
}
return defs, nil
}
// CallerLines are the callers each verb and each accepted kind admits, from the definitions, of the seat named (every seat
// when empty) and the verb named (every verb when empty).
func CallerLines(defs map[string]seatdef.Definition, seat, verb string) []CallerLine {
var out []CallerLine
for _, d := range defs {
if seat != "" && d.Seat != seat {
continue
}
for _, v := range d.Verbs {
if verb == "" || v.Name == verb {
out = append(out, CallerLine{Seat: d.Seat, Verb: v.Name, Kind: "verb", Class: v.Class,
Callers: v.EffectiveCallers(), Named: v.Callers != nil && !v.Callers.IsText()})
}
}
for _, a := range d.Accepts {
if verb == "" || a.Name == verb {
l := CallerLine{Seat: d.Seat, Verb: a.Name, Kind: "accepts"}
if a.Callers != nil && !a.Callers.IsText() {
l.Callers, l.Named = a.Callers.List, true
}
out = append(out, l)
}
}
}
sort.Slice(out, func(i, j int) bool {
a, b := out[i], out[j]
return a.Seat+"\x00"+a.Kind+"\x00"+a.Verb < b.Seat+"\x00"+b.Kind+"\x00"+b.Verb
})
return out
}
// Callers answers the tool grants_callers.
func (p *Part) Callers(seat, verb string) (any, error) {
defs, err := p.definitions()
if err != nil {
return nil, err
}
lines := CallerLines(defs, seat, verb)
if len(lines) == 0 {
return nil, fmt.Errorf("no seat definition names a verb or an accepted kind matching seat %q verb %q", seat, verb)
}
return map[string]any{"callers": lines, "count": len(lines)}, nil
}
// WhyRefused answers the tool grants_why_refused: the grant an account lacks to call address (`<seat>.<verb>`), and the
// definition's line that leaves it out. The bus answers whether the account may publish the verb's call subject
// (nats.nats_user_can, the account's grants as the server holds them); the definition's callers say who was meant to.
func (p *Part) WhyRefused(account, address, node string) (any, error) {
seat, verb, ok := strings.Cut(address, ".")
if account == "" || !ok || seat == "" || verb == "" {
return nil, errors.New("why_refused names an account and a verb as <seat>.<verb>")
}
defs, err := p.definitions()
if err != nil {
return nil, err
}
lines := CallerLines(defs, seat, verb)
if len(lines) == 0 {
return nil, fmt.Errorf("no seat definition names the verb %s", address)
}
def := defs[seat]
s := buscompose.Seat{Name: seat, Scope: def.Scope}
subject := buscompose.CalledSubject(buscompose.SeatToolSubject(s, verb, node), account)
if subject == "" {
return nil, fmt.Errorf("%s is not the name of a bus user", account)
}
out := map[string]any{"account": account, "verb": address, "subject": subject, "definition": lines}
raw, err := p.Call.Call(CallUserCan, map[string]any{"user": account, "action": "publish", "subject": subject})
if err != nil {
out["bus"] = "not read: " + err.Error()
return out, nil
}
var can struct {
Allowed *bool `json:"allowed"`
}
_ = json.Unmarshal(raw, &can)
out["bus"] = json.RawMessage(raw)
switch {
case can.Allowed == nil:
out["says"] = "the bus's answer names no verdict; read it beside the definition's line"
case *can.Allowed:
out["says"] = "not refused at the bus: the account holds the grant to publish " + subject
default:
callers := lines[0].Callers
out["says"] = fmt.Sprintf("the account lacks the grant to publish %s; the definition admits %s (%s), and the grants part grants a call only to a caller one of those entries names",
subject, strings.Join(callers, ", "), map[bool]string{true: "named by the definition", false: "the class's default"}[lines[0].Named])
out["lacks"] = subject
out["admitted_by"] = slices.Clone(callers)
}
return out, nil
}