Files
mesh-delivery/modules/mesh-grants/internal/grants/refused.go
T
jschoubben 788969bcf8 Write a kept user through mesh-sdk's AccountsEntry, so its lines are ComposeAccounts' own (issue 774)
The grants part repeated the composer's entry lines to keep a refused principal's grants of the list in force;
mesh-sdk 61b67d6 exports them as buscompose.AccountsEntry, so the copy goes. The byte-for-byte test of the kept
entry still passes. Needs the mesh-sdk pin at 61b67d6 or later.
2026-10-30 18:20:28 +01:00

138 lines
5.6 KiB
Go

package grants
import (
"fmt"
"slices"
"sort"
"strings"
"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 from grants kept rather than composed, in ComposeAccounts' own lines
// (buscompose.AccountsEntry), so the bus reads it as it read the list in force.
func keptEntry(u User, hash string) string {
return buscompose.AccountsEntry(u.User, hash, buscompose.Permissions{Publish: u.Publish, PublishDeny: u.PublishDeny,
Subscribe: u.Subscribe, AllowResponses: u.AllowResponses})
}
// 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
}
}