230 lines
10 KiB
Go
230 lines
10 KiB
Go
package broker
|
|
|
|
import (
|
|
"slices"
|
|
"testing"
|
|
)
|
|
|
|
// The provisioner client's own bus account (novox/hq ADR 0349 clauses 6 and 11, design 57 package P3, issue
|
|
// 669): one per machine running it, apart from the machine's tool runner, granted to read its own machine's keys
|
|
// in each domain's bucket, to say what it applied on that machine, and to ask the vault in its own name; nothing
|
|
// else.
|
|
|
|
func provisionerRecords() Records {
|
|
client := Declared{Module: "mesh-provisioner", ProvisionsNode: true}
|
|
return Records{
|
|
Nodes: []string{"g14", "shanks", "ace"},
|
|
Assigned: map[string][]Declared{
|
|
"g14": {client, {Module: RuntimeModule}},
|
|
"shanks": {client},
|
|
"ace": {{Module: "dunst"}},
|
|
},
|
|
}
|
|
}
|
|
|
|
// One account per machine that runs the client, named for the machine and apart from its tool runner's: the
|
|
// vault tells the client from an agent on that machine by the name the bus proves.
|
|
func TestEachMachineRunningTheProvisionerClientHasAnAccountOfItsOwn(t *testing.T) {
|
|
users, err := Users(provisionerRecords())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got []string
|
|
for _, u := range users {
|
|
if u.Kind == KindProvisioner {
|
|
got = append(got, u.Username())
|
|
if u.Module != "mesh-provisioner" {
|
|
t.Errorf("%s is delivered to %q, want the module that runs it", u.Username(), u.Module)
|
|
}
|
|
}
|
|
}
|
|
if want := []string{"provisioner.g14", "provisioner.shanks"}; !slices.Equal(got, want) {
|
|
t.Fatalf("the provisioner clients' accounts are %v, want %v (none for a machine that does not run it)", got, want)
|
|
}
|
|
for _, u := range users {
|
|
if u.Kind == KindNodeTools && u.Username() == "provisioner.g14" {
|
|
t.Fatal("the client shares the tool runner's account")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Its whole authority, subject by subject: a read of its own machine's keys in each domain's bucket, a read and a
|
|
// write of its own machine's keys in the provisioner server's needs, applied and collections, a read of the
|
|
// server's registry (design 57 P4), a publish of what it applied on its own machine, the vault's `get` in its own
|
|
// name, its own inbox and the vault's secret-changed.
|
|
func TestTheProvisionerClientIsGrantedItsMachinesKeysAndNothingElse(t *testing.T) {
|
|
perms, err := PermissionsFor(Principal{Kind: KindProvisioner, Node: "g14", Module: "mesh-provisioner",
|
|
BoundBuckets: []string{"postgres_bound"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var wantPub []string
|
|
for _, bucket := range []string{"mesh-config_resolved", "postgres_bound"} {
|
|
stream := "KV_" + bucket
|
|
wantPub = append(wantPub,
|
|
"$JS.API.STREAM.INFO."+stream,
|
|
"$JS.API.DIRECT.GET."+stream+".$KV."+bucket+".g14.>",
|
|
"$JS.API.CONSUMER.CREATE."+stream+".*.$KV."+bucket+".g14.>",
|
|
"$JS.API.CONSUMER.DELETE."+stream+".>",
|
|
"$JS.FC."+stream+".>")
|
|
}
|
|
// The assignments mirror whole, read and never written: a provider's consumers are bound to it in their own
|
|
// machines' plans, which its machine's client reads to fill ${consumers:<provision>} (novox/hq ADR 0373, issue
|
|
// 679). The mirror holds placement and plans, never a value of a secret.
|
|
wantPub = append(wantPub,
|
|
"$JS.API.STREAM.INFO.KV_mesh-controller_assignments",
|
|
"$JS.API.DIRECT.GET.KV_mesh-controller_assignments.>",
|
|
"$JS.API.CONSUMER.CREATE.KV_mesh-controller_assignments.>",
|
|
"$JS.API.CONSUMER.DELETE.KV_mesh-controller_assignments.>",
|
|
"$JS.FC.KV_mesh-controller_assignments.>")
|
|
for _, bucket := range []string{"mesh-provisioner_needs", "mesh-provisioner_applied", "mesh-provisioner_collect"} {
|
|
stream := "KV_" + bucket
|
|
wantPub = append(wantPub,
|
|
"$JS.API.STREAM.INFO."+stream,
|
|
"$JS.API.DIRECT.GET."+stream+".$KV."+bucket+".g14.>",
|
|
"$JS.API.CONSUMER.CREATE."+stream+".*.$KV."+bucket+".g14.>",
|
|
"$JS.API.CONSUMER.DELETE."+stream+".>",
|
|
"$JS.FC."+stream+".>",
|
|
"$KV."+bucket+".g14.>")
|
|
}
|
|
wantPub = append(wantPub,
|
|
"$JS.API.STREAM.INFO.KV_mesh-provisioner_registry",
|
|
"$JS.API.DIRECT.GET.KV_mesh-provisioner_registry.>",
|
|
"$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_registry.>",
|
|
"$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_registry.>",
|
|
"$JS.FC.KV_mesh-provisioner_registry.>")
|
|
wantPub = append(wantPub, "prov.applied.g14.>", "mesh.seat.mesh-vault.call.get.provisioner~g14",
|
|
"prov.changed.g14.>")
|
|
slices.Sort(wantPub)
|
|
if !slices.Equal(perms.Publish, wantPub) {
|
|
t.Errorf("publish:\n have %q\n want %q", perms.Publish, wantPub)
|
|
}
|
|
// Its own inbox, what the vault says changed (`secret-changed`: which, by whom, never a value), so a rotated
|
|
// secret reaches its file without a new declaration (design 57 V4), and its own machine's tool runner asking
|
|
// it for a carried module's config (ADR 0370 §3), which it answers on the ask's reply.
|
|
if want := []string{"_INBOX.provisioner.g14.>", "mesh.mod.mesh-vault.event.secret-changed", "prov.ask.g14"}; !slices.Equal(perms.Subscribe, want) {
|
|
t.Errorf("subscribe:\n have %q\n want %q", perms.Subscribe, want)
|
|
}
|
|
if !perms.AllowResponses || len(perms.PublishDeny) > 0 {
|
|
t.Errorf("it answers what it is asked and needs no deny: %+v", perms)
|
|
}
|
|
// No write of any bucket but its own machine's keys of the server's needs, applied and collections: a domain
|
|
// writes its own (ADR 0349 clause 3, research 051), and the registry is the server's alone.
|
|
for _, p := range perms.Publish {
|
|
if !SubjectsOverlap(p, "$KV.>") {
|
|
continue
|
|
}
|
|
if !slices.Contains([]string{"$KV.mesh-provisioner_needs.g14.>", "$KV.mesh-provisioner_applied.g14.>",
|
|
"$KV.mesh-provisioner_collect.g14.>"}, p) {
|
|
t.Errorf("it may write a bucket: %s", p)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Each provider's bucket of entries, `<module>_bound` (mesh-sdk go/provider, issue 678), is read by every machine's
|
|
// client for that machine's consumers' keys alone (`<node>.>`), to fill ${bound:<provision>:<key>} (issue 679): the
|
|
// buckets of every module the mesh places that keeps state named bound, wherever it runs, and no other.
|
|
func TestTheProvisionerClientReadsItsMachinesKeysOfEachProvidersBoundBucket(t *testing.T) {
|
|
records := provisionerRecords()
|
|
records.Assigned["ace"] = append(records.Assigned["ace"],
|
|
Declared{Module: "postgres", State: []Bucket{{Module: "postgres", Name: "bound"}, {Module: "postgres", Name: "kept"}}},
|
|
Declared{Module: "keycloak", State: []Bucket{{Module: "keycloak", Name: "bound"}}})
|
|
records.Assigned["shanks"] = append(records.Assigned["shanks"],
|
|
Declared{Module: "postgres", State: []Bucket{{Module: "postgres", Name: "bound"}}})
|
|
users, err := Users(records)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, u := range users {
|
|
if u.Kind != KindProvisioner {
|
|
continue
|
|
}
|
|
if want := []string{"keycloak_bound", "postgres_bound"}; !slices.Equal(u.BoundBuckets, want) {
|
|
t.Errorf("%s reads the bound buckets %v, want %v", u.Username(), u.BoundBuckets, want)
|
|
}
|
|
perms, err := PermissionsFor(u)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mine := "$JS.API.DIRECT.GET.KV_postgres_bound.$KV.postgres_bound." + u.Node + ".>"
|
|
if !slices.Contains(perms.Publish, mine) {
|
|
t.Errorf("%s may not read its machine's keys of postgres_bound", u.Username())
|
|
}
|
|
for _, p := range perms.Publish {
|
|
if SubjectsOverlap(p, "$JS.API.DIRECT.GET.KV_postgres_kept.>") || SubjectsOverlap(p, "$KV.postgres_bound.>") ||
|
|
SubjectsOverlap(p, "$JS.API.DIRECT.GET.KV_postgres_bound.$KV.postgres_bound.ace.>") {
|
|
t.Errorf("%s is granted %s: another bucket, a write, or another machine's consumers", u.Username(), p)
|
|
}
|
|
}
|
|
}
|
|
if _, err := PermissionsFor(Principal{Kind: KindProvisioner, Node: "g14", Module: "mesh-provisioner",
|
|
BoundBuckets: []string{"postgres_bound.>"}}); err == nil {
|
|
t.Error("a bound bucket that is a pattern composed")
|
|
}
|
|
}
|
|
|
|
// A machine with no usable name reaches nothing: its grants would otherwise be a pattern over every machine.
|
|
func TestAProvisionerClientNamingNoMachineIsRefused(t *testing.T) {
|
|
for _, node := range []string{"", "g14.>", "*"} {
|
|
if _, err := PermissionsFor(Principal{Kind: KindProvisioner, Node: node, Module: "mesh-provisioner"}); err == nil {
|
|
t.Errorf("a provisioner client on %q composed", node)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A whole mesh composes with the clients beside every other kind: nobody is granted a second writer's subject.
|
|
func TestAMeshWithProvisionerClientsComposes(t *testing.T) {
|
|
users, err := Users(provisionerRecords())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i := range users {
|
|
users[i].PasswordHash = "x"
|
|
}
|
|
if _, err := ComposeAccounts(users); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// The client is asked on a subject its own machine's tool runner alone may publish (novox/hq ADR 0370 §3, issue
|
|
// 685): across a whole mesh, no principal but g14's tool runner may ask g14's client, none but g14's client hears
|
|
// it, and the client's word of what moved is heard by g14's tool runner alone.
|
|
func TestTheProvisionerClientIsAskedByItsOwnMachinesToolRunnerAlone(t *testing.T) {
|
|
records := provisionerRecords()
|
|
records.Assigned["shanks"] = append(records.Assigned["shanks"], Declared{Module: RuntimeModule},
|
|
Declared{Module: "dunst"})
|
|
records.Assigned["g14"] = append(records.Assigned["g14"], Declared{Module: "claude-code"})
|
|
users, err := Users(records)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
users = append(users, Principal{Kind: KindPerson, Module: "jochen", Invokes: []string{"*"}})
|
|
var asks, hears, told []string
|
|
for _, u := range users {
|
|
perms, err := PermissionsFor(u)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", u.Username(), err)
|
|
}
|
|
if slices.ContainsFunc(perms.Publish, func(p string) bool { return SubjectsOverlap(p, ProvisionerAskSubject("g14")) }) {
|
|
asks = append(asks, u.Username())
|
|
}
|
|
if slices.ContainsFunc(perms.Subscribe, func(p string) bool { return SubjectsOverlap(p, ProvisionerAskSubject("g14")) }) {
|
|
hears = append(hears, u.Username())
|
|
}
|
|
if slices.ContainsFunc(perms.Subscribe, func(p string) bool {
|
|
return SubjectsOverlap(p, ProvisionerChangedSubject("g14", "claude-code"))
|
|
}) {
|
|
told = append(told, u.Username())
|
|
}
|
|
}
|
|
if want := []string{"g14.node-tools"}; !slices.Equal(asks, want) {
|
|
t.Errorf("g14's client may be asked by %v, want %v alone", asks, want)
|
|
}
|
|
if want := []string{"provisioner.g14"}; !slices.Equal(hears, want) {
|
|
t.Errorf("g14's client's asks are heard by %v, want %v alone", hears, want)
|
|
}
|
|
if want := []string{"g14.node-tools"}; !slices.Equal(told, want) {
|
|
t.Errorf("what g14's client says moved is heard by %v, want %v alone", told, want)
|
|
}
|
|
}
|