mesh-grants: a retired verb and a multiply claimed mesh seat's added subjects are named, not counted, in the comparison (issue 1945)
This commit is contained in:
@@ -699,6 +699,28 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("compare excuses a retired verb and a multiply claimed mesh seat's added subjects, nothing else", func(t *testing.T) {
|
||||
const retired, store, storeInfo = "mesh.seat.mesh-delivery.tool.mark-ready", "mesh.seat.mesh-store.tool.databases", "$SRV.INFO.mesh-store.>"
|
||||
for _, row := range []struct {
|
||||
name string
|
||||
shadow, bus []string // the node-tools user's subscribe in the shadow list and in the loaded list
|
||||
multi []string
|
||||
differs bool
|
||||
excusedCount int
|
||||
}{
|
||||
{"a retired verb the shadow dropped", []string{"x"}, []string{"x", retired}, nil, false, 1},
|
||||
{"a verb of a live seat the shadow lost", []string{"x"}, []string{"x", store}, []string{"mesh-store"}, true, 0},
|
||||
{"a seat claimed on two machines, added", []string{"x", store, storeInfo}, []string{"x"}, []string{"mesh-store"}, false, 2},
|
||||
{"the same added for a seat claimed once", []string{"x", store}, []string{"x"}, nil, true, 0},
|
||||
} {
|
||||
c := grants.Compare([]grants.User{{User: "a.node-tools", Subscribe: row.shadow}}, []grants.User{{User: "a.node-tools", Subscribe: row.bus}}, nil).
|
||||
Excusing(grants.ExcusedBy(row.multi))
|
||||
if c.Equal() == row.differs || len(c.Excused) != row.excusedCount {
|
||||
t.Errorf("%s: equal %v, excused %v", row.name, c.Equal(), c.Excused)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("tools", func(t *testing.T) {
|
||||
b := fixture()
|
||||
b.loaded = shadowOf(t, b)
|
||||
|
||||
@@ -190,7 +190,7 @@ func (p *Part) Compose() Result {
|
||||
users = WithHashes(users, in.Hashes)
|
||||
st.Digest, st.Users = Digest(users), len(users)
|
||||
loaded, err := LoadedUsers(p.Call)
|
||||
st.Compare = Compare(users, loaded, Placed(in))
|
||||
st.Compare = Compare(users, loaded, Placed(in)).Excusing(ExcusedBy(MultiHeld(recs)))
|
||||
if err != nil {
|
||||
st.Compare.Unread = err.Error()
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// User is one bus user and its grants, as the bus module's nats_users answers them: never a password hash.
|
||||
@@ -234,7 +236,9 @@ type Comparison struct {
|
||||
Dropped []string `json:"dropped"`
|
||||
Gone []string `json:"gone,omitempty"`
|
||||
Differ []UserDiff `json:"differ"`
|
||||
Same int `json:"same"`
|
||||
// Excused are the subjects the comparison does not count, each with why (Comparison.Excusing).
|
||||
Excused []Excuse `json:"excused,omitempty"`
|
||||
Same int `json:"same"`
|
||||
// Unread says the loaded list could not be read, and why: nothing is compared then.
|
||||
Unread string `json:"unread,omitempty"`
|
||||
}
|
||||
@@ -325,3 +329,93 @@ func Placed(in Inputs) func(user string) bool {
|
||||
return slices.Contains(in.Nodes, parts[0]) && slices.Contains(in.Assigned[parts[0]], parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Excuse is one subject the comparison does not count as a difference between the lists, and why.
|
||||
type Excuse struct {
|
||||
User string `json:"user"`
|
||||
Subject string `json:"subject"`
|
||||
Why string `json:"why"`
|
||||
}
|
||||
|
||||
// Excusing removes from the differences the subjects f excuses (added: the shadow grants it and the loaded list does not),
|
||||
// naming each in Excused; a user left with no difference counts as the same. Pure.
|
||||
func (c Comparison) Excusing(f func(subject string, added bool) string) Comparison {
|
||||
var differ []UserDiff
|
||||
for _, d := range c.Differ {
|
||||
keep := func(list []string, added bool) []string {
|
||||
var out []string
|
||||
for _, s := range list {
|
||||
if why := f(s, added); why != "" {
|
||||
c.Excused = append(c.Excused, Excuse{User: d.User, Subject: s, Why: why})
|
||||
continue
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
d.PublishAdded, d.PublishDropped = keep(d.PublishAdded, true), keep(d.PublishDropped, false)
|
||||
d.SubscribeAdded, d.SubscribeDropped = keep(d.SubscribeAdded, true), keep(d.SubscribeDropped, false)
|
||||
if len(d.PublishAdded)+len(d.PublishDropped)+len(d.SubscribeAdded)+len(d.SubscribeDropped) == 0 && d.Responses == "" {
|
||||
c.Same++
|
||||
continue
|
||||
}
|
||||
differ = append(differ, d)
|
||||
}
|
||||
if differ == nil {
|
||||
differ = []UserDiff{}
|
||||
}
|
||||
c.Differ = differ
|
||||
return c
|
||||
}
|
||||
|
||||
var seatSubject = regexp.MustCompile(`^mesh\.seat\.([^.]+)\.(?:call|tool)\.([^.]+)(?:\.\*)?$`)
|
||||
var seatService = regexp.MustCompile(`^\$SRV\.(?:INFO|PING|STATS)\.([^.]+)(?:\.>)?$`)
|
||||
|
||||
// ExcusedBy is the excuse of a comparison until placement lives in the catalogue (the operator, 2026-10-26): a verb the
|
||||
// seat catalogue retired on purpose, which the shadow no longer grants, is no difference; and what the shadow grants more
|
||||
// for a mesh seat that is claimed on several machines (the standing gap seat holdings: it composes the seat as held on each)
|
||||
// is named and not counted, since a subscription to a seat's verbs on a machine whose holder does not answer them harms
|
||||
// nothing and is gone when holdings follow the catalogue. Nothing else is excused: a grant the shadow gives for any other
|
||||
// reason, or the loss of any other subject, keeps the lists apart.
|
||||
func ExcusedBy(multiHeld []string) func(subject string, added bool) string {
|
||||
return func(subject string, added bool) string {
|
||||
if m := seatSubject.FindStringSubmatch(subject); m != nil {
|
||||
if !added && retiredVerb(m[1], m[2]) {
|
||||
return "a verb the seat catalogue retired"
|
||||
}
|
||||
if added && slices.Contains(multiHeld, m[1]) {
|
||||
return "seat " + m[1] + " is claimed on several machines: composed as held on each until holdings follow the catalogue"
|
||||
}
|
||||
}
|
||||
if m := seatService.FindStringSubmatch(subject); m != nil && added && slices.Contains(multiHeld, m[1]) {
|
||||
return "seat " + m[1] + " is claimed on several machines: composed as held on each until holdings follow the catalogue"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// MultiHeld are the mesh seats (held once for the mesh) that more than one machine's module claims, sorted.
|
||||
func MultiHeld(r buscompose.Records) []string {
|
||||
nodes := map[string]map[string]bool{}
|
||||
for node, declared := range r.Assigned {
|
||||
for _, d := range declared {
|
||||
for _, s := range d.Holds {
|
||||
if s.Scope != "" && s.Scope != catalogue.ScopeMesh {
|
||||
continue
|
||||
}
|
||||
if nodes[s.Name] == nil {
|
||||
nodes[s.Name] = map[string]bool{}
|
||||
}
|
||||
nodes[s.Name][node] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
var out []string
|
||||
for seat, ns := range nodes {
|
||||
if len(ns) > 1 {
|
||||
out = append(out, seat)
|
||||
}
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user