Files
mesh-delivery/modules/mesh-grants/internal/grants/refused.go
T
jschoubben 031cfb8277 Compose the user list principal by principal, so one refused principal never freezes it (issue 774)
Met twice on 2026-10-30 (the engine's acts, then mesh-host_health): the composer refused one principal as a
second writer, and that refusal ended the whole composition, so the list in force stood for every principal
while changes piled up; the next list that composed moved 52 users at once (16:37:28).

Quick fix: leave the refused principal out and write the rest (locks that principal out of the bus).
Thorough fix, chosen because the kind was met twice and it hides every other change: ComposeEach composes each
principal on its own; a refused one keeps its grants of the list in force (the part's sealed grants), written as
the composer writes an entry; one the list in force does not carry stays out; a list that would drop one the
list in force carries, whose grants were not kept, is held, said. The refusal is raised as the warning
mesh-grants.principal-refused, by name and why, cleared once it composes, and listed in the pass
(principals_refused). The entry renderer mirrors buscompose.ComposeAccounts's lines and is pinned to them by the
test (the entry the bus loaded is written back byte for byte); it belongs in mesh-sdk as an exported entry writer.
2026-10-30 18:10:26 +01:00

158 lines
6.2 KiB
Go

package grants
import (
"fmt"
"slices"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/buscompose"
)
// **One refused principal never freezes the list** (issue 774, sweep 2026-10-30; met twice that day: the engine's acts,
// then mesh-host_health). The composer refuses a principal's grants when they would make it a second writer of a state
// (buscompose.CheckWriters): that refusal used to end the whole composition, so the list in force stood for every principal
// while each change since piled up, and the next list that composed moved 52 users at once. Now each principal is composed
// on its own: one refused keeps the grants it holds in the list in force (the part's own record of that list, sealed
// grants), the rest of the list is composed and written as ever, and the refusal is raised as the warning
// mesh-grants.principal-refused, naming each principal and why, until it composes again. A refused principal the list in
// force does not carry stays out of the list: it never had those grants, and is refused them now.
// RefusedCondition is the key of the warning raised while a principal's grants are refused.
const RefusedCondition = "mesh-grants.principal-refused"
// ComposeEach is the shadow list composed principal by principal: every user whose grants compose, and each refused one
// by user with why. err is only for principals that cannot be derived at all (two users of one name). Pure.
func ComposeEach(r buscompose.Records) ([]User, map[string]string, error) {
principals, err := buscompose.Users(r)
if err != nil {
return nil, nil, err
}
out := make([]User, 0, len(principals))
refused := map[string]string{}
for _, p := range principals {
perms, err := buscompose.PermissionsFor(p)
if err != nil {
refused[p.Username()] = err.Error()
continue
}
out = append(out, User{User: p.Username(), Publish: set(perms.Publish), Subscribe: set(perms.Subscribe),
AllowResponses: perms.AllowResponses, PublishDeny: set(perms.PublishDeny)})
}
sort.Slice(out, func(i, j int) bool { return out[i].User < out[j].User })
return out, refused, nil
}
// keptOf is, for each refused user, its grants in the list in force as the part sealed them; the users it holds none of
// (not in the list in force, or that list's grants not kept) are the second answer, sorted.
func (p *Part) keptOf(refused map[string]string) (map[string]User, []string) {
if len(refused) == 0 {
return nil, nil
}
var inForce map[string]User
if g, _, had := p.Sealed(); had && g.Digest != "" && p.Store != nil {
inForce, _ = p.sealedGrantsOf(g.Digest)
}
kept := map[string]User{}
var none []string
for _, user := range sortedKeys(refused) {
if u, ok := inForce[user]; ok {
kept[user] = u
continue
}
none = append(none, user)
}
return kept, none
}
// withKept is users with each kept user in its place, sorted.
func withKept(users []User, kept map[string]User) []User {
out := slices.Clone(users)
for _, u := range kept {
out = append(out, u)
}
sort.Slice(out, func(i, j int) bool { return out[i].User < out[j].User })
return out
}
// keptEntry is a user of the accounts block as buscompose.ComposeAccounts writes one, from grants kept rather than
// composed: the same lines, so the bus reads it as it read the list in force.
func keptEntry(u User, hash string) string {
var b strings.Builder
fmt.Fprintf(&b, " { user: %q, password: %q, permissions: {\n", u.User, hash)
if len(u.PublishDeny) > 0 {
fmt.Fprintf(&b, " publish: { allow: [%s], deny: [%s] }\n", quotedList(u.Publish), quotedList(u.PublishDeny))
} else {
fmt.Fprintf(&b, " publish: { allow: [%s] }\n", quotedList(u.Publish))
}
fmt.Fprintf(&b, " subscribe: { allow: [%s] }\n", quotedList(u.Subscribe))
if u.AllowResponses {
fmt.Fprintf(&b, " allow_responses: { max: 1, ttl: \"%dm\" }\n", int(buscompose.ResponseTTL/time.Minute))
}
b.WriteString(" } }\n")
return b.String()
}
func quotedList(in []string) string {
q := make([]string, len(in))
for i, s := range in {
q[i] = fmt.Sprintf("%q", s)
}
return strings.Join(q, ", ")
}
// withEntries puts entries at the end of the accounts block's users, before it closes.
func withEntries(list string, entries []string) (string, error) {
if len(entries) == 0 {
return list, nil
}
start := strings.Index(list, " users = [\n")
if start < 0 {
return "", fmt.Errorf("the composed list holds no users block to keep a refused user's grants in")
}
end := strings.Index(list[start:], "\n ]\n")
if end < 0 {
return "", fmt.Errorf("the composed list's users block does not close")
}
at := start + end + 1
return list[:at] + strings.Join(entries, "") + list[at:], nil
}
// sayPrincipalsRefused raises RefusedCondition while a principal's grants are refused, naming each with why and what
// became of it, and clears it once none is; said once per set, again when it changes, and at the first pass after a start.
func (p *Part) sayPrincipalsRefused(refused map[string]string, kept map[string]User) {
if p.Say == nil {
return
}
var lines []string
for _, user := range sortedKeys(refused) {
became := "left out of the list: the list in force does not carry it"
if _, ok := kept[user]; ok {
became = "keeps its grants of the list in force"
}
lines = append(lines, user+" ("+became+"): "+refused[user])
}
why := strings.Join(lines, "; ")
p.holdMu.Lock()
defer p.holdMu.Unlock()
if p.refusedKnown && why == p.refusedSaid {
return
}
c := asks.Condition{Key: RefusedCondition, Subject: asks.Scope{Scope: "module", ID: "mesh-grants"}}
if why == "" {
c.State = asks.StateCleared
} else {
c.Severity, c.State = asks.SeverityWarning, asks.StateRaised
c.Summary = fmt.Sprintf("%d principal(s) refused their composed grants: %s", len(refused), why)
c.Headline, c.Needs = "A principal's bus grants are refused", "a fix of the manifest or the writers table"
c.Explanation = asks.NeedsYou + "a fix of what makes it a second writer. The rest of the user list is written as ever; " +
"each refused principal keeps the grants the list in force gives it, or stays out where it holds none: " + why
c.Resolved = "every principal's grants compose again"
}
if p.Say(c) == nil {
p.refusedSaid, p.refusedKnown = why, true
}
}