package broker import ( "reflect" "sort" "strings" "testing" ) // The view (novox/hq research 036): one read-only user, composed like every other, whose whole // authority is a list here — so a grant that is not on the list fails a test, not a review. // Exactly what it hears, exactly what it asks, and nothing it could write or answer. A mutation that // adds a publish grant — `$KV..>`, an event, a tool — fails here. func TestTheViewHearsAndReadsAndCanPublishNothingElse(t *testing.T) { perms, err := PermissionsFor(Principal{Kind: KindView}) if err != nil { t.Fatal(err) } wantSub := append(append([]string(nil), ViewHears...), "_INBOX.view.>") sort.Strings(wantSub) if !reflect.DeepEqual(perms.Subscribe, wantSub) { t.Errorf("the view subscribes\n %v\nand should subscribe exactly\n %v", perms.Subscribe, wantSub) } wantPub := ViewReads() sort.Strings(wantPub) if !reflect.DeepEqual(perms.Publish, wantPub) { t.Errorf("the view publishes\n %v\nand should publish exactly\n %v", perms.Publish, wantPub) } if len(perms.PublishDeny) != 0 { t.Errorf("the view needs no deny, because nothing it may publish reaches the controller's own: %v", perms.PublishDeny) } if perms.AllowResponses { t.Error("the view may answer, and nothing is ever asked of it") } // Every publish grant is a JetStream API request about the one bucket's stream, or its flow control. // **The mutation this holds against**: a write grant of any shape. stream := "KV_" + ViewBucket for _, p := range perms.Publish { readOnly := strings.HasPrefix(p, "$JS.API.STREAM.INFO."+stream) || strings.HasPrefix(p, "$JS.API.DIRECT.GET."+stream+".") || strings.HasPrefix(p, "$JS.API.CONSUMER.CREATE."+stream+".") || strings.HasPrefix(p, "$JS.API.CONSUMER.INFO."+stream+".") || strings.HasPrefix(p, "$JS.API.CONSUMER.DELETE."+stream+".") || strings.HasPrefix(p, "$JS.FC."+stream+".") if !readOnly { t.Errorf("the view is granted a publish on %q, which is not a read of %s", p, ViewBucket) } } for _, refused := range []string{ "$KV." + ViewBucket + ".365", // a put or a delete "$KV.mesh-controller_conditions.x", // another bucket "$JS.API.STREAM.CREATE." + stream, // defining the stream "$JS.API.STREAM.PURGE." + stream, // emptying it "$JS.API.STREAM.DELETE." + stream, // deleting it "$JS.API.STREAM.MSG.DELETE." + stream, // deleting a message "$JS.API.CONSUMER.CREATE.KV_mesh-controller_conditions.x", // reading another bucket "$JS.API.STREAM.INFO.EVENTS", // the events stream "$JS.API.INFO", // the account "mesh.mod.mesh-issues.event.opened", // claiming the tracker said something "mesh.mod.mesh-issues.tool.open", // opening an issue "mesh.seat.issue-tracker.tool.open", // through the seat "mesh.seat.issue-tracker.tool.open.novox", // on one machine "mesh.seat.mesh-controller.tool.status", // the controller's verbs "mesh.seat.mesh-controller.event.plan-moved", "$SRV.PING", "_INBOX.controller.x", } { if MayPublish(perms, refused) { t.Errorf("the view may publish %q", refused) } } for _, refused := range []string{ "mesh.mod.mesh-issues.tool.open", // a tool asked of the tracker "mesh.mod.telegram.event.received", // another module's events "mesh.seat.mesh-controller.event.applied", "mesh.control.novox.report", "_INBOX.controller.x", "_INBOX.person.jochen.x", "_DELIVER.controller.EVENTS", } { if MaySubscribe(perms, refused) { t.Errorf("the view may subscribe %q", refused) } } for _, heard := range []string{ "mesh.mod.mesh-issues.event.opened", "mesh.mod.mesh-issues.event.moved", "mesh.mod.mesh-issues.event.noted", "mesh.mod.mesh-issues.event.linked", "mesh.seat.mesh-controller.event.plan-moved", "mesh.seat.mesh-controller.event.condition-raised", "mesh.seat.mesh-controller.event.condition-changed", "mesh.seat.mesh-controller.event.condition-cleared", "mesh.mod.mesh-delivery.event.transition", "mesh.mod.mesh-delivery.event.group", "_INBOX.view.abc", } { if !MaySubscribe(perms, heard) { t.Errorf("the view cannot subscribe %q", heard) } } } // Composed once the mesh minted its credential, and not before: its row is the whole record of it. func TestTheViewIsComposedOnlyOnceItsCredentialIsMinted(t *testing.T) { without, err := Users(Records{Nodes: []string{"anchor"}}) if err != nil { t.Fatal(err) } for _, p := range without { if p.Kind == KindView { t.Fatal("the view is composed before its credential was minted") } } with, err := Users(Records{Nodes: []string{"anchor"}, View: true}) if err != nil { t.Fatal(err) } views := 0 for _, p := range with { if p.Kind == KindView { views++ if p.Username() != ViewUser { t.Errorf("the view is called %q, and its row is %q", p.Username(), ViewUser) } } } if views != 1 { t.Fatalf("%d view users composed; there is one view", views) } // And without its hash it is named as missing, like any user — never written as a user anybody is. _, missing := WithPasswords(with, map[string]string{}) found := false for _, m := range missing { found = found || m == ViewUser } if !found { t.Error("a view with no password was not named as missing one") } }