package grants import ( "fmt" "slices" "sort" "strings" "git.novox.be/novox/mesh-sdk/go/asks" "mesh-grants/internal/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 } }