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.
138 lines
5.6 KiB
Go
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
|
|
}
|
|
}
|