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
140 lines
4.9 KiB
Go
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
|
|
}
|