Files
mesh-controller/cmd/mesh-controller/grants_test.go
T
jschoubben fbf14b68db
mesh/delivery held for a person: merged without a passing check: only a person decides what follows
mesh/delivery-group group fix/1945-credential-when-row-without-seal held: a member is held: novox/mesh-controller@fbf14b68dbab (merged without a passing check:…
mesh/check checking fbf14b68dbab on 40034be479f8
Forget the accounts of modules no longer on their nodes at the start's look, and grant sealed accounts alone (issue 1945, ADR 0543, review)
Migration 0108 marks every standing account sealed, ace's stale mesh-build-go
rows from 17:19 among them, and the forgetting ran only at a later unassign:
the next assignment to ace would have issued nothing again.

- The start's look first forgets, from each node's own resolution (assigned
  and required), the accounts of every module that no longer resolves on its
  node, and says each; only from a reading that holds assignments, and a node
  whose resolution is unknown is left alone.
- Unassign forgets a module's accounts only once it no longer resolves on the
  node (a module another requires keeps running), and never the node's own
  provisioner account; the forgetting moved from the inventory to the act.
- Forgetting holds each account's lock across its delete, so an issue in flight
  never writes a row back after it.
- A module whose accounts are not all sealed is granted its sealed accounts
  alone, and its grant is dropped when none is: an older grant naming ace no
  longer lets the assigner want it.
- An issue's locked section has a deadline of two minutes, since it holds a
  connection of the store's pool across the vault and the bus.
Tests: cases in the existing tables.
Research 072: the grants piece (memberships and credentials heard from placement).
2026-10-25 20:27:30 +01:00

120 lines
4.9 KiB
Go

package main
import (
"context"
"strings"
"testing"
"time"
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/nats-io/nats.go/jetstream"
"github.com/novox/mesh-controller/internal/broker"
)
func grantFP(c byte) string { return sdkdelivery.SourcePrefix2 + strings.Repeat(string(c), 64) }
// 1394/AC (unit half): the list sent issues the credential of every module whose accounts all have a password, at the
// fingerprint its build names; a module with an account the list leaves out, and one registered at a fingerprint not of
// the new form, get no grant; a module with no account of its own is granted with none.
func TestTheGrantsAListIssues(t *testing.T) {
x := broker.Principal{Kind: broker.KindModule, Node: "novox", Module: "mesh-x"}
xp := broker.Principal{Kind: broker.KindProcess, Node: "novox", Module: "mesh-x", Process: "run"}
y := broker.Principal{Kind: broker.KindModule, Node: "ace", Module: "mesh-y"}
node := broker.Principal{Kind: broker.KindNode, Node: "novox"}
hashes := map[string]string{x.Username(): "h1", xp.Username(): "h2", node.Username(): "h3"}
// mesh-z is sealed on novox and not on ace (issue 1945: an older grant must not name ace): its grant names novox alone.
zn := broker.Principal{Kind: broker.KindModule, Node: "novox", Module: "mesh-z"}
za := broker.Principal{Kind: broker.KindModule, Node: "ace", Module: "mesh-z"}
hashes[zn.Username()] = "h4"
got, drop := grantsFrom([]broker.Principal{x, xp, y, node, zn, za}, hashes,
map[string]string{"mesh-x": grantFP('a'), "mesh-y": grantFP('b'), "web": grantFP('c'), "old": "src1:abc",
"mesh-z": grantFP('d')})
xKey, _ := sdkdelivery.ControllerGrantKey("mesh-x", grantFP('a'))
yKey, _ := sdkdelivery.ControllerGrantKey("mesh-y", grantFP('b'))
zKey, _ := sdkdelivery.ControllerGrantKey("mesh-z", grantFP('d'))
webKey, _ := sdkdelivery.ControllerGrantKey("web", grantFP('c'))
if len(got) != 3 {
t.Fatalf("grants = %v, want mesh-x's, mesh-z's and web's", got)
}
if g := got[zKey]; len(g.Accounts) != 1 || g.Accounts[0] != zn.Username() {
t.Errorf("mesh-z's grant = %+v, want its sealed account alone", g)
}
if len(drop) != 1 || drop[0] != yKey {
t.Errorf("dropped %v, want mesh-y's grant, none of whose accounts is sealed", drop)
}
if g := got[xKey]; len(g.Accounts) != 2 || g.Accounts[0] != x.Username() && g.Accounts[1] != x.Username() {
t.Errorf("mesh-x's grant = %+v, want both its accounts", g)
}
if g, ok := got[webKey]; !ok || len(g.Accounts) != 0 {
t.Errorf("web, with no account of its own, is granted %+v, %v", g, ok)
}
}
// fakeGrants is the grants bucket in memory.
type fakeGrants struct {
values map[string][]byte
creates int
updates int
}
type fakeEntry struct {
key string
value []byte
rev uint64
}
func (e fakeEntry) Bucket() string { return broker.GrantsBucket }
func (e fakeEntry) Key() string { return e.key }
func (e fakeEntry) Value() []byte { return e.value }
func (e fakeEntry) Revision() uint64 { return e.rev }
func (e fakeEntry) Created() time.Time { return time.Time{} }
func (e fakeEntry) Delta() uint64 { return 0 }
func (e fakeEntry) Operation() jetstream.KeyValueOp { return jetstream.KeyValuePut }
func (f *fakeGrants) Get(_ context.Context, key string) (jetstream.KeyValueEntry, error) {
v, ok := f.values[key]
if !ok {
return nil, jetstream.ErrKeyNotFound
}
return fakeEntry{key: key, value: v, rev: 1}, nil
}
func (f *fakeGrants) Create(_ context.Context, key string, value []byte, _ ...jetstream.KVCreateOpt) (uint64, error) {
if _, ok := f.values[key]; ok {
return 0, jetstream.ErrKeyExists
}
f.values[key] = value
f.creates++
return 1, nil
}
func (f *fakeGrants) Update(_ context.Context, key string, value []byte, _ uint64) (uint64, error) {
f.values[key] = value
f.updates++
return 2, nil
}
// 1394/AC (unit half): a grant is created once; a second send of the same list writes nothing; a reissue for other
// accounts is a new revision of the same key.
func TestAGrantIsWrittenOnceAndMovedOnAReissue(t *testing.T) {
kv := &fakeGrants{values: map[string][]byte{}}
key, _ := sdkdelivery.ControllerGrantKey("mesh-x", grantFP('a'))
grants := map[string]sdkdelivery.ControllerGrant{key: {Accounts: []string{"novox.mesh-x"}}}
for range 2 {
if _, err := keepGrants(t.Context(), kv, grants); err != nil {
t.Fatal(err)
}
}
if kv.creates != 1 || kv.updates != 0 {
t.Errorf("creates %d, updates %d, want one create", kv.creates, kv.updates)
}
grants[key] = sdkdelivery.ControllerGrant{Accounts: []string{"ace.mesh-x"}}
if wrote, err := keepGrants(t.Context(), kv, grants); err != nil || wrote != 1 || kv.updates != 1 {
t.Errorf("a reissue wrote %d (%v), updates %d", wrote, err, kv.updates)
}
if g, err := sdkdelivery.DecodeControllerGrant(kv.values[key]); err != nil || g.Accounts[0] != "ace.mesh-x" {
t.Errorf("the grant reads %+v, %v", g, err)
}
}