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
189 lines
6.1 KiB
Go
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
|
|
}
|