Files
mesh-delivery/modules/mesh-grants/internal/grants/grantsfor.go
T
jschoubben a4fcf3de51 GR12: the seat mesh-grants claims its seat and serves grants-for
The part claims mesh-grants, serving grants-for: a run by its group run-<id> or a
source revision by the branch and the new optional source of mesh-catalog's
revision (contract regenerated), compared with the branch's newest by the
permissions the composed accounts gain and lose with the candidate's entries in.

Builds: design 47 G §401, §441, §449, A-205

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 06:02:44 +01:00

189 lines
6.1 KiB
Go

package grants
import (
"encoding/json"
"errors"
"fmt"
"maps"
"slices"
"sort"
"strconv"
"strings"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshgrants"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// **The seat mesh-grants' read `grants-for`** (design 47 G §441, §401, A-205): the grants a candidate would compose, per
// module, added and removed against the branch's newest. The candidate's catalog half is taken by mesh-catalog's `revision` (a
// run by its group `run-<id>`, a source revision by the branch and `source`), the branch's newest by `revision` with the branch
// alone; the answer is what the composed memberships' permissions gain and lose with the candidate's entries swapped in for the
// branch's newest.
// RunGroup is the group of a run's candidate in mesh-catalog's `revision` (A-205).
const RunGroup = "run-"
// CallRevision is the seat mesh-catalog's `revision`, as the part calls it.
const CallRevision = "seat:" + meshcatalog.Seat + "." + meshcatalog.VerbRevision
// revisionEntries reads a catalog graph revision's entries, page after page, as manifests by module. none says the branch has
// no revision yet.
func (p *Part) revisionEntries(in meshcatalog.RevisionInput) (manifests map[string]catalogue.Manifest, none bool, err error) {
manifests = map[string]catalogue.Manifest{}
for {
raw, err := p.Call.Call(CallRevision, in)
if err != nil {
return nil, false, fmt.Errorf("%s does not answer: %w", CallRevision, err)
}
var a meshcatalog.RevisionAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return nil, false, fmt.Errorf("%s answered what is not a revision: %w", CallRevision, err)
}
if a.NoneYet {
return nil, true, nil
}
for _, e := range a.Entries {
body, err := json.Marshal(e.Manifest)
if err != nil || len(e.Manifest) == 0 {
continue
}
m, err := catalogue.Decode(body)
if err != nil {
return nil, false, fmt.Errorf("the manifest of %s in the revision does not decode: %w", e.Module, err)
}
manifests[e.Module] = m
}
if a.Next == "" || a.Next == in.After {
return manifests, false, nil
}
in.After = a.Next
}
}
// grantsOfEntries is, by module, the subjects the composed accounts of that module may publish or subscribe, composed from in with
// the entries' manifests in place of the trunk's and of what runs.
func grantsOfEntries(in Inputs, entries map[string]catalogue.Manifest) (map[string]map[string]bool, error) {
in.Manifests = maps.Clone(in.Manifests)
in.Running = maps.Clone(in.Running)
for module, m := range entries {
in.Manifests[module] = m
for key := range in.Running {
if strings.HasSuffix(key, "."+module) {
delete(in.Running, key)
}
}
}
recs, _ := ListRecords(in)
users, refused, err := ComposeEach(recs)
if err != nil {
return nil, err
}
if len(refused) > 0 {
user := sortedKeys(refused)[0]
return nil, fmt.Errorf("the grants of %s do not compose: %s", user, refused[user])
}
out := map[string]map[string]bool{}
for _, u := range users {
_, module, ok := strings.Cut(u.User, ".")
if _, named := entries[module]; !ok || !named {
continue
}
if out[module] == nil {
out[module] = map[string]bool{}
}
for _, s := range append(slices.Clone(u.Publish), u.Subscribe...) {
out[module][s] = true
}
}
return out, nil
}
// verbOf names the verb, event or tool a subject is for: `mesh.seat.<seat>.<kind>.<name>…` and `mesh.mod.<module>.<kind>.<name>…`.
func verbOf(subject string) string {
parts := strings.Split(subject, ".")
if len(parts) >= 5 && parts[0] == "mesh" && (parts[1] == "seat" || parts[1] == "mod") {
return parts[4]
}
return ""
}
func grantList(subjects []string) []meshgrants.Grant {
out := []meshgrants.Grant{}
for _, s := range subjects {
out = append(out, meshgrants.Grant{Subject: s, Verb: verbOf(s)})
}
return out
}
// GrantsFor answers the verb grants-for.
func (p *Part) GrantsFor(in meshgrants.GrantsForInput) (meshgrants.GrantsFor, error) {
if in.Branch == "" {
return meshgrants.GrantsFor{}, errors.New("grants-for names the candidate's watched branch")
}
candidate := meshcatalog.RevisionInput{Branch: in.Branch}
switch {
case in.Run != "":
candidate.Group = RunGroup + in.Run
case in.Source != "":
n, err := strconv.ParseInt(in.Source, 10, 64)
if err != nil {
return meshgrants.GrantsFor{}, fmt.Errorf("the source revision %q is not a number", in.Source)
}
candidate.Source = n
default:
return meshgrants.GrantsFor{}, errors.New("grants-for names a source revision or a run")
}
cand, none, err := p.revisionEntries(candidate)
if err != nil || none {
return meshgrants.GrantsFor{Error: orWhy(err, "the candidate's catalog half is not held: the branch has no revision of it")}, nil
}
newest, none, err := p.revisionEntries(meshcatalog.RevisionInput{Branch: in.Branch})
if err != nil || none {
return meshgrants.GrantsFor{Error: orWhy(err, "the branch's newest catalog half is not held")}, nil
}
base := map[string]catalogue.Manifest{}
for module := range cand {
if m, ok := newest[module]; ok {
base[module] = m
}
}
input := p.inputs()
was, err := grantsOfEntries(input, base)
if err != nil {
return meshgrants.GrantsFor{Error: "the branch's newest does not compose: " + err.Error()}, nil
}
will, err := grantsOfEntries(input, cand)
if err != nil {
return meshgrants.GrantsFor{Error: "the candidate does not compose: " + err.Error()}, nil
}
var out meshgrants.GrantsFor
for _, module := range sortedKeys(will) {
var added, removed []string
for s := range will[module] {
if !was[module][s] {
added = append(added, s)
}
}
for s := range was[module] {
if !will[module][s] {
removed = append(removed, s)
}
}
if len(added)+len(removed) == 0 {
continue
}
sort.Strings(added)
sort.Strings(removed)
out.Modules = append(out.Modules, meshgrants.GrantsForModulesItem{Module: module, Added: grantList(added), Removed: grantList(removed)})
}
return out, nil
}
func orWhy(err error, why string) string {
if err != nil {
return err.Error()
}
return why
}