//go:build integration package inventory import ( "context" "strings" "testing" "time" "github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/catalogue" "golang.org/x/crypto/bcrypt" ) func aMeshWith(t *testing.T, manifests ...catalogue.Manifest) (*Inventory, context.Context) { t.Helper() inv := ForTest(t) ctx := context.Background() for _, m := range manifests { if err := inv.RegisterModule(ctx, m, Source{Repository: "/r"}); err != nil { t.Fatal(err) } } return inv, ctx } func TestAnAssignedModuleBecomesAUserWithWhatItDeclared(t *testing.T) { // It declares where its account is delivered: a module with no own secret named broker can never // be issued one, and is no user at all (novox/hq issue 195) — the case asserted below. shop := catalogue.Manifest{ Module: "shop", Version: "1", Emits: []string{"order.placed"}, Tools: []string{"price"}, Uses: []string{"telegram-sender"}, OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/run/broker"}}, } quiet := catalogue.Manifest{Module: "quiet", Version: "1", Emits: []string{"thing.happened"}} inv, ctx := aMeshWith(t, theSeatDeclarer(), shop, quiet) if _, err := inv.AddNode(ctx, "one"); err != nil { t.Fatal(err) } for _, module := range []string{"shop", "quiet"} { if _, err := inv.Assign(ctx, "one", module); err != nil { t.Fatal(err) } } records, err := inv.BusRecords(ctx) if err != nil { t.Fatal(err) } var on []broker.Declared for _, d := range records.Assigned["one"] { if d.Module == "shop" { on = append(on, d) } } if len(on) != 1 || on[0].NoAccount { t.Fatalf("the machine's modules read as %+v", records.Assigned["one"]) } if len(on[0].Uses) != 1 || on[0].Uses[0].Accepts[0] != "send" { t.Fatalf("the seat it uses carries no protocol: %+v — so it would be granted nothing on a "+ "seat it was assigned to send to", on[0].Uses) } if len(on[0].Serves) != 1 || on[0].Serves[0] != "price" { t.Fatalf("its tools read as %v, and a module that cannot subscribe its own tool subject "+ "serves nothing", on[0].Serves) } // And it derives into a user the server would accept. users, err := broker.Users(records) if err != nil { t.Fatal(err) } var found bool for _, u := range users { if u.Username() != "one.shop" { continue } found = true perms, err := broker.PermissionsFor(u) if err != nil { t.Fatal(err) } // Its tools are every one under its own name — the list in the manifest is a person's // vocabulary for asking, not the module's permission to answer. if !granted(perms.Publish, "mesh.mod.shop.event.order.placed") || !granted(perms.Publish, "mesh.seat.telegram-sender.accept.send") || !granted(perms.Subscribe, "mesh.mod.shop.tool.>") { t.Fatalf("one.shop's authority is not what it declared: %+v", perms) } } if !found { t.Fatal("no user was derived for the assigned module") } for _, u := range users { if u.Username() == "one.quiet" { t.Fatal("a module with nowhere to read an account was made a user (novox/hq issue 195)") } } } func TestOnlyAMachineWithALiveTokenHasAnEnrolmentUser(t *testing.T) { inv, ctx := aMeshWith(t) for _, name := range []string{"live", "expired", "none"} { if _, err := inv.AddNode(ctx, name); err != nil { t.Fatal(err) } } if _, err := inv.IssueToken(ctx, "live", time.Hour); err != nil { t.Fatal(err) } // Briefly, then waited out: a token with no lifetime is refused at issue, which is the right // refusal and leaves this as the way to have an expired one. if _, err := inv.IssueToken(ctx, "expired", 10*time.Millisecond); err != nil { t.Fatal(err) } time.Sleep(50 * time.Millisecond) records, err := inv.BusRecords(ctx) if err != nil { t.Fatal(err) } if strings.Join(records.Enrolling, ",") != "live" { t.Fatalf("machines with a live token read as %v", records.Enrolling) } } func TestIssuingATokenRecordsTheAccountItIsThePasswordOf(t *testing.T) { inv, ctx := aMeshWith(t) if _, err := inv.AddNode(ctx, "joiner"); err != nil { t.Fatal(err) } issued, err := inv.IssueToken(ctx, "joiner", time.Hour) if err != nil { t.Fatal(err) } name := broker.Principal{Kind: broker.KindEnrolment, Node: "joiner"}.Username() users, err := inv.BusUsers(ctx) if err != nil { t.Fatal(err) } user, has := users[name] if !has { t.Fatalf("no bus account for %q; the composer would leave the enrolment out and the "+ "machine would be refused before the mesh heard of it: %v", name, users) } if user.Kind != BusEnrolment || user.Node != "joiner" { t.Errorf("the account is %+v, not this node's enrolment", user) } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(issued.Secret)); err != nil { t.Error("the account does not accept the secret the token carries, so presenting the " + "token would be refused by the server") } // And the composition contains it, which is the thing the server reads. records, err := inv.BusRecords(ctx) if err != nil { t.Fatal(err) } derived, err := broker.Users(records) if err != nil { t.Fatal(err) } hashes := map[string]string{} for n, u := range users { hashes[n] = u.PasswordHash } _, missing := broker.WithPasswords(derived, hashes) for _, m := range missing { if m == name { t.Fatal("the enrolment user is composed without a password, which is a user nobody can be") } } } func TestTheBussOwnModuleBecomesTheSnapshotUser(t *testing.T) { bus := catalogue.Manifest{ Module: "nats", Version: "1", Provides: []catalogue.Offer{{Name: "mesh-bus", Scope: catalogue.ScopeMesh}}, Claims: []catalogue.Claim{{Name: catalogue.BrokerSeat, Scope: catalogue.ScopeMesh}}, Tools: []string{"nats_streams"}, OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/run/broker"}}, } shop := catalogue.Manifest{Module: "shop", Version: "1", Emits: []string{"order.placed"}, OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/run/broker"}}} inv, ctx := aMeshWith(t, bus, shop) if _, err := inv.AddNode(ctx, "one"); err != nil { t.Fatal(err) } for _, module := range []string{"nats", "shop"} { if _, err := inv.Assign(ctx, "one", module); err != nil { t.Fatal(err) } } records, err := inv.BusRecords(ctx) if err != nil { t.Fatal(err) } users, err := broker.Users(records) if err != nil { t.Fatal(err) } seen := map[string]bool{} for _, u := range users { perms, err := broker.PermissionsFor(u) if err != nil { t.Fatal(err) } snapshots := granted(perms.Publish, "$JS.API.STREAM.SNAPSHOT.*") switch u.Username() { case "one.nats": seen[u.Username()] = true if !snapshots || len(perms.Publish) != len(broker.BusSnapshotGrants.Publish) { t.Fatalf("the bus's own user is granted %v, not the snapshot API alone", perms.Publish) } case "one.shop": seen[u.Username()] = true if snapshots { t.Fatal("a module that is not the bus may snapshot it") } } } if !seen["one.nats"] || !seen["one.shop"] { t.Fatalf("users derived: %v", seen) } } func TestABarIsGrantedTheStateItsMachinesModulesOfferIt(t *testing.T) { bar := catalogue.Manifest{Module: "a-bar", Version: "1", Claims: []catalogue.Claim{{Name: catalogue.BarSeat, Scope: catalogue.ScopeNode}}} power := catalogue.Manifest{Module: "power", Version: "1", State: []catalogue.StateDeclaration{{Name: "draw"}}, Contributions: []catalogue.SeatContribution{{Seat: catalogue.BarSeat, Kind: catalogue.BarKindBlock, Data: map[string]any{"bar": "bottom", "place": "status", "shows": "state", "options": map[string]any{"state": "power.draw"}}}}} inv, ctx := aMeshWith(t, bar, power) for node, modules := range map[string][]string{"laptop": {"a-bar", "power"}, "desk": {"a-bar"}} { if _, err := inv.AddNode(ctx, node); err != nil { t.Fatal(err) } for _, module := range modules { if _, err := inv.Assign(ctx, node, module); err != nil { t.Fatal(err) } } } records, err := inv.BusRecords(ctx) if err != nil { t.Fatal(err) } reads := func(node, module string) []broker.KeyedRead { for _, d := range records.Assigned[node] { if d.Module == module { if len(d.Reads) != 0 { t.Fatalf("%s on %s reads whole buckets: %v", module, node, d.Reads) } return d.KeyedReads } } t.Fatalf("%s is not on %s", module, node) return nil } // The laptop's own key, and no other (novox/hq ADR 0260). if got := reads("laptop", "a-bar"); len(got) != 1 || got[0] != (broker.KeyedRead{Read: "power.draw", Key: "laptop"}) { t.Fatalf("the laptop's bar reads %v", got) } if got := reads("desk", "a-bar"); len(got) != 0 { t.Fatalf("a bar with no power beside it reads %v", got) } if got := reads("laptop", "power"); len(got) != 0 { t.Fatalf("power reads %v", got) } } func TestTheProvisionerClientsMachineHasAnAccountOfItsOwn(t *testing.T) { provisioner := catalogue.Manifest{ Module: "mesh-provisioner", Version: "1", DefinesSeats: []catalogue.SeatDeclaration{{Name: broker.NodeProvisionerSeat, Scope: catalogue.ScopeNode, Serves: []catalogue.Verb{{Name: "get"}}}}, Claims: []catalogue.Claim{{Name: broker.NodeProvisionerSeat, Scope: catalogue.ScopeNode}}, OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/run/broker"}, broker.ProvisionerSecret: {Path: "/run/provisioner-broker"}}, } unread := provisioner unread.Module = "provisioner-unread" unread.DefinesSeats = nil unread.OwnSecrets = catalogue.OwnSecrets{"broker": {Path: "/run/broker"}} inv, ctx := aMeshWith(t, provisioner, unread) for node, module := range map[string]string{"one": "mesh-provisioner", "two": "provisioner-unread"} { if _, err := inv.AddNode(ctx, node); err != nil { t.Fatal(err) } if _, err := inv.Assign(ctx, node, module); err != nil { t.Fatal(err) } } records, err := inv.BusRecords(ctx) if err != nil { t.Fatal(err) } users, err := broker.Users(records) if err != nil { t.Fatal(err) } var clients []string for _, u := range users { if u.Kind == broker.KindProvisioner { clients = append(clients, u.Username()+" for "+u.Module) } } if strings.Join(clients, ",") != "provisioner.one for mesh-provisioner" { t.Fatalf("the provisioner clients' accounts are %v, want one for the machine whose module reads it", clients) } }