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
This commit is contained in:
@@ -1281,6 +1281,8 @@ type RevisionInput struct {
|
||||
Heads string `json:"heads,omitempty"`
|
||||
// a kept revision; absent: the newest of trunk, or of the candidate named
|
||||
Revision int64 `json:"revision,omitempty"`
|
||||
// a candidate's source revision: the revision of the branch's series taken in from it, with its entries (design 47 G section 441)
|
||||
Source int64 `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
// RevisionAnswer is what revision answers.
|
||||
|
||||
@@ -266,6 +266,10 @@
|
||||
"type": "string",
|
||||
"description": "a candidate's heads"
|
||||
},
|
||||
"source": {
|
||||
"type": "integer",
|
||||
"description": "a candidate's source revision: the revision of the branch's series taken in from it, with its entries (design 47 G section 441)"
|
||||
},
|
||||
"after": {
|
||||
"type": "string",
|
||||
"description": "the last module of the page before; absent: the first page"
|
||||
|
||||
@@ -731,6 +731,8 @@ export interface RevisionInput {
|
||||
"heads"?: string;
|
||||
/** a kept revision; absent: the newest of trunk, or of the candidate named */
|
||||
"revision"?: number;
|
||||
/** a candidate's source revision: the revision of the branch's series taken in from it, with its entries (design 47 G section 441) */
|
||||
"source"?: number;
|
||||
}
|
||||
|
||||
/** is what revision answers. */
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshgrants"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
|
||||
@@ -269,6 +270,11 @@ func toolsOf(p *grants.Part) []stdio.Tool {
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
return p.WhyRefused(grants.Answer(a, "account"), grants.Answer(a, "verb"), grants.Answer(a, "node"))
|
||||
}},
|
||||
{Name: meshgrants.Seat + "." + meshgrants.VerbGrantsFor,
|
||||
Description: "The grants a candidate would compose, per module (design 47 G §441): the grants it would add and remove against the branch's newest.",
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
return p.GrantsFor(meshgrants.GrantsForInput{Branch: grants.Answer(a, "branch"), Run: grants.Answer(a, "run"), Source: grants.Answer(a, "source")})
|
||||
}},
|
||||
{Name: grants.ToolRelease, Description: "Release one held granting wait by a person's act with why, recorded as a hand act " +
|
||||
"(the seat mesh-hand-acts) before it is released (design 47 G §276). grants_waits lists them.",
|
||||
Input: schema(map[string]string{"module": "the held entry's module", "why": "why, kept in the hand act"}, "module", "why"),
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshgrants"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
@@ -91,8 +92,11 @@ type fakeBus struct {
|
||||
maxValue int
|
||||
// entries are the catalog half's entries by fingerprint, the manifest each holds as the indexer's manifests state has it
|
||||
// (entry); entriesAsked says every one is asked, not answered: the half cannot be read yet.
|
||||
entries map[string]string
|
||||
entriesAsked bool
|
||||
entries map[string]string
|
||||
entriesAsked bool
|
||||
// revision is the catalog half of mesh-catalog's `revision`: the branch's `newest` and a `candidate` (a source or a group), each
|
||||
// module's manifest.
|
||||
revision map[string]map[string]string
|
||||
catalogWaited []string
|
||||
}
|
||||
|
||||
@@ -363,6 +367,21 @@ func (b *fakeBus) Call(key string, body any) (json.RawMessage, error) {
|
||||
}
|
||||
}
|
||||
return json.Marshal(map[string]any{"users": out})
|
||||
case grants.CallRevision:
|
||||
var in meshcatalog.RevisionInput
|
||||
raw, _ := json.Marshal(body)
|
||||
_ = json.Unmarshal(raw, &in)
|
||||
which := "newest"
|
||||
if in.Group != "" || in.Source != 0 {
|
||||
which = "candidate"
|
||||
}
|
||||
var out meshcatalog.RevisionAnswer
|
||||
for module, manifest := range b.revision[which] {
|
||||
var m map[string]any
|
||||
_ = json.Unmarshal([]byte(manifest), &m)
|
||||
out.Entries = append(out.Entries, meshcatalog.CatalogEntry{Module: module, Manifest: m})
|
||||
}
|
||||
return json.Marshal(out)
|
||||
case grants.CallStreams:
|
||||
if stream, _ := args["stream"].(string); stream != "" {
|
||||
return json.Marshal(map[string]any{"detail": []map[string]any{{"name": stream, "consumer_detail": b.consumers[stream]}}})
|
||||
@@ -602,10 +621,9 @@ func TestGrants(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("mesh-sdk cannot decode the manifest: %v", err)
|
||||
}
|
||||
// The part claims no seat: the seat mesh-grants served the lease write alone, which went with the controller
|
||||
// (novox/hq ADR 0590 §3, issue 1126).
|
||||
if m.Module != "mesh-grants" || len(m.Claims) != 0 {
|
||||
t.Errorf("module %q claims %v: the part claims no seat", m.Module, m.Claims)
|
||||
// The part claims the seat mesh-grants, which serves `grants-for` (design 47 G §401, §441).
|
||||
if m.Module != "mesh-grants" || len(m.Claims) != 1 || m.Claims[0].Name != "mesh-grants" || !slices.Equal(m.Claims[0].Serves, []string{"grants-for"}) {
|
||||
t.Errorf("module %q claims %v: the part claims the seat mesh-grants alone, serving grants-for", m.Module, m.Claims)
|
||||
}
|
||||
for _, row := range []struct {
|
||||
what string
|
||||
@@ -5095,3 +5113,37 @@ func TestAnOldStateSectionDeclaresItsCap(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// grants-for answers what the composed accounts gain and lose with a candidate's entries in place of the branch's newest (design
|
||||
// 47 G §441, A-205): a tool the candidate adds is an added grant naming its verb, and an input that names no source nor run is
|
||||
// refused.
|
||||
func TestGrantsFor(t *testing.T) {
|
||||
b := fixture()
|
||||
candidateX := strings.Replace(strings.Replace(xManifest, `,"logs":[{"name":"history","max-age-days":30}]`, "", 1), `"state"`, `"emits":["hello"],"state"`, 1)
|
||||
b.revision = map[string]map[string]string{"newest": {"mod-x": xManifest}, "candidate": {"mod-x": candidateX}}
|
||||
p := partOn(b)
|
||||
got, err := p.GrantsFor(meshgrants.GrantsForInput{Branch: "main", Run: "r1"})
|
||||
if err != nil || got.Error != "" || len(got.Modules) != 1 || got.Modules[0].Module != "mod-x" {
|
||||
t.Fatalf("grants-for: %+v, %v", got, err)
|
||||
}
|
||||
var verbs []string
|
||||
for _, g := range got.Modules[0].Added {
|
||||
verbs = append(verbs, g.Verb+" "+g.Subject)
|
||||
}
|
||||
if !slices.ContainsFunc(got.Modules[0].Added, func(g meshgrants.Grant) bool {
|
||||
return g.Verb == "hello" && strings.HasPrefix(g.Subject, "mesh.mod.mod-x.")
|
||||
}) {
|
||||
t.Errorf("the event the candidate adds is not an added grant: %v / %+v", verbs, got.Modules[0])
|
||||
}
|
||||
if len(got.Modules[0].Removed) == 0 {
|
||||
t.Error("the log the candidate drops is no removed grant")
|
||||
}
|
||||
b.revision["candidate"] = b.revision["newest"]
|
||||
same, _ := p.GrantsFor(meshgrants.GrantsForInput{Branch: "main", Source: "7"})
|
||||
if len(same.Modules) != 0 {
|
||||
t.Errorf("a candidate equal to the newest changes grants: %+v", same.Modules)
|
||||
}
|
||||
if _, err := p.GrantsFor(meshgrants.GrantsForInput{Branch: "main"}); err == nil {
|
||||
t.Error("grants-for with neither a source nor a run answered")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
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
|
||||
}
|
||||
@@ -14,8 +14,8 @@ import (
|
||||
"mesh-grants/internal/buscompose"
|
||||
)
|
||||
|
||||
// The part's tools, as its manifest lists them. The part claims no seat: the seat mesh-grants served the lease write
|
||||
// alone, which went with the controller (novox/hq ADR 0590 §3, issue 1126).
|
||||
// The part's tools, as its manifest lists them. The part claims the seat mesh-grants, which serves `grants-for` (design 47 G
|
||||
// §401, §441); its own tools are named below.
|
||||
const (
|
||||
ToolGrants = "grants_grants"
|
||||
ToolMembership = "grants_membership"
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
"module": "mesh-grants",
|
||||
"version": "0.1",
|
||||
"slug": "grants",
|
||||
"claims": [
|
||||
{
|
||||
"name": "mesh-grants",
|
||||
"scope": "mesh",
|
||||
"serves": [
|
||||
"grants-for"
|
||||
]
|
||||
}
|
||||
],
|
||||
"reads": [
|
||||
"mesh-config.resolved",
|
||||
"node-engine.have",
|
||||
|
||||
Reference in New Issue
Block a user