A module may invoke tools, and a manifest is checked where it is written
invokes: a manifest word that becomes exactly the publish grant a person's account gets (ADR 0152), derived by the same composition; refused at parse when it names no tool. module check <file|dir>... runs what registration runs with no store, for a manifest in any repository (hq issue 148).
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A module that says it calls a tool may publish exactly that subject (novox/hq ADR 0152): the same
|
||||
// grant a person gets, derived the same way, so one list answers "what may this ask" for everybody.
|
||||
func TestAModuleMayAskOnlyTheToolsItInvokes(t *testing.T) {
|
||||
perms, err := PermissionsFor(Principal{Kind: KindModule, Node: "desk", Module: "mesh-console",
|
||||
Invokes: []string{"shop.price"}, PasswordHash: "x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
has(t, perms.Publish, "mesh.mod.shop.tool.price")
|
||||
hasNot(t, perms.Publish, "mesh.mod.shop.tool.refund")
|
||||
hasNot(t, perms.Publish, "mesh.mod.*.tool.>")
|
||||
}
|
||||
|
||||
// The console's grant: every tool, as one subject, and it reads as one.
|
||||
func TestAModuleInvokingEverythingMayAskAnyTool(t *testing.T) {
|
||||
perms, err := PermissionsFor(Principal{Kind: KindModule, Node: "desk", Module: "mesh-console",
|
||||
Invokes: []string{"*"}, PasswordHash: "x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
has(t, perms.Publish, "mesh.mod.*.tool.>")
|
||||
}
|
||||
|
||||
// **A grant to call widens nothing else.** A module that invokes may not publish an event it did not
|
||||
// declare, may not answer as another module, and subscribes nothing it did not consume — the
|
||||
// difference between the console and a person is that the console is on a machine, not that it may
|
||||
// do more.
|
||||
func TestInvokingGrantsNothingButTheCall(t *testing.T) {
|
||||
perms, err := PermissionsFor(Principal{Kind: KindModule, Node: "desk", Module: "mesh-console",
|
||||
Invokes: []string{"*"}, PasswordHash: "x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, p := range perms.Publish {
|
||||
if strings.Contains(p, ".event.") {
|
||||
t.Errorf("a module that only invokes may publish %q, an event it never declared", p)
|
||||
}
|
||||
if strings.HasPrefix(p, "mesh.seat.") {
|
||||
t.Errorf("a module that only invokes may publish %q, a seat it neither holds nor uses", p)
|
||||
}
|
||||
}
|
||||
for _, s := range perms.Subscribe {
|
||||
if strings.Contains(s, ".tool.") && !strings.HasPrefix(s, "mesh.mod.mesh-console.") {
|
||||
t.Errorf("a module that invokes may subscribe %q, another module's tools", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A module that declares no invokes calls nothing, which is every module but the console.
|
||||
func TestAModuleThatInvokesNothingCallsNothing(t *testing.T) {
|
||||
perms, err := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "shop",
|
||||
Emits: []string{"order.placed"}, PasswordHash: "x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, p := range perms.Publish {
|
||||
if strings.Contains(p, ".tool.") {
|
||||
t.Errorf("a module with no invokes may publish %q", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The malformed entry is refused for a module as it is for a person, and in the same words.
|
||||
func TestAModulesToolGrantThatNamesNoToolIsRefused(t *testing.T) {
|
||||
if _, err := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "shop",
|
||||
Invokes: []string{"telegram"}, PasswordHash: "x"}); err == nil {
|
||||
t.Fatal("a grant naming a module but no tool was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
// What a declaration says reaches the composed user, so a manifest's `invokes` is the grant.
|
||||
func TestADeclaredInvokeReachesTheComposedUser(t *testing.T) {
|
||||
users, err := Users(Records{
|
||||
Nodes: []string{"desk"},
|
||||
Assigned: map[string][]Declared{"desk": {{Module: "mesh-console", Invokes: []string{"*"}}}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
perms, err := PermissionsFor(users[len(users)-1])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
has(t, perms.Publish, "mesh.mod.*.tool.>")
|
||||
}
|
||||
+40
-14
@@ -70,12 +70,14 @@ type Principal struct {
|
||||
// a namespace no such module owns. Every service started and the graph stayed empty.
|
||||
Watches []Seat
|
||||
|
||||
// Invokes are the tools a person may call, as `<module>.<tool>`; a single `*` is every tool,
|
||||
// for an administrator. Only meaningful for KindPerson.
|
||||
// Invokes are the tools this principal may call, as `<module>.<tool>`; a single `*` is every
|
||||
// tool. A person's whole authority (design 25 §7), and a module's only if its manifest says so
|
||||
// (novox/hq ADR 0152) — the console's does, and nothing else's.
|
||||
//
|
||||
// **A list, not a role.** A person is not a module and holds no seat: nothing is addressed
|
||||
// to them, nothing is delivered to them, and they have no durable consumer to acknowledge.
|
||||
// What they have is permission to ask.
|
||||
// What they have is permission to ask. A module that invokes gains exactly the same
|
||||
// permission and nothing beside it.
|
||||
Invokes []string
|
||||
|
||||
// PasswordHash is the bcrypt hash the mesh minted. The plaintext is sealed to the principal
|
||||
@@ -224,18 +226,11 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
case KindPerson:
|
||||
// Tools, and nothing else. Every subject a person may publish is a tool call; a person
|
||||
// who could publish an event would be able to claim a module said something.
|
||||
for _, t := range p.Invokes {
|
||||
if t == "*" {
|
||||
pub = append(pub, "mesh.mod.*.tool.>")
|
||||
continue
|
||||
}
|
||||
module, tool, ok := strings.Cut(t, ".")
|
||||
if !ok {
|
||||
return Permissions{}, fmt.Errorf(
|
||||
"%q does not name a tool: a person invokes <module>.<tool>, or * for every one", t)
|
||||
}
|
||||
pub = append(pub, "mesh.mod."+module+".tool."+tool)
|
||||
invoked, err := invokedSubjects(p.Invokes)
|
||||
if err != nil {
|
||||
return Permissions{}, err
|
||||
}
|
||||
pub = append(pub, invoked...)
|
||||
|
||||
case KindEnrolment:
|
||||
// A leaked token is useless for anything but enrolling: it cannot read a declaration, hear
|
||||
@@ -293,6 +288,16 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
// still granted per tool, by name, on the publish side.
|
||||
sub = append(sub, own+".tool.>")
|
||||
|
||||
// 1b. The tools it calls, if its manifest says it calls any (novox/hq ADR 0152). The same
|
||||
// grant a person gets and derived the same way, so "what may this module ask" is
|
||||
// answered by the one list that answers it for everybody. Publish only: an answer
|
||||
// arrives on its own inbox, which every principal has below.
|
||||
invoked, err := invokedSubjects(p.Invokes)
|
||||
if err != nil {
|
||||
return Permissions{}, err
|
||||
}
|
||||
pub = append(pub, invoked...)
|
||||
|
||||
// 2. What it consumes, by the emitter's own subject — an event is addressed to its
|
||||
// emitter, because the emitter's identity is the meaning (ADR 0118).
|
||||
for _, c := range p.Consumes {
|
||||
@@ -601,3 +606,24 @@ func quoted(values []string) string {
|
||||
}
|
||||
return strings.Join(out, ", ")
|
||||
}
|
||||
|
||||
// invokedSubjects is the publish side of a grant to call tools: one subject per `<module>.<tool>`,
|
||||
// or the whole tool namespace for `*`. A person's authority and a module's `invokes` are both this
|
||||
// (novox/hq ADR 0152), so a malformed entry is refused in one place, before it could be widened into
|
||||
// something that happens to parse.
|
||||
func invokedSubjects(invokes []string) ([]string, error) {
|
||||
var out []string
|
||||
for _, t := range invokes {
|
||||
if t == "*" {
|
||||
out = append(out, "mesh.mod.*.tool.>")
|
||||
continue
|
||||
}
|
||||
module, tool, ok := strings.Cut(t, ".")
|
||||
if !ok || module == "" || tool == "" {
|
||||
return nil, fmt.Errorf(
|
||||
"%q does not name a tool: one invokes <module>.<tool>, or * for every one", t)
|
||||
}
|
||||
out = append(out, "mesh.mod."+module+".tool."+tool)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ type Declared struct {
|
||||
Uses []Seat
|
||||
// Watches are the seats whose events it consumes.
|
||||
Watches []Seat
|
||||
// Invokes are the tools it calls, `<module>.<tool>` or `*` (novox/hq ADR 0152).
|
||||
Invokes []string
|
||||
}
|
||||
|
||||
// Records is what composing a user list needs to know about the mesh, and nothing more.
|
||||
@@ -61,7 +63,7 @@ func Users(r Records) ([]Principal, error) {
|
||||
out = append(out, Principal{
|
||||
Kind: KindModule, Node: node, Module: d.Module,
|
||||
Emits: d.Emits, Consumes: d.Consumes, Serves: d.Serves,
|
||||
Holds: d.Holds, Uses: d.Uses, Watches: d.Watches,
|
||||
Holds: d.Holds, Uses: d.Uses, Watches: d.Watches, Invokes: d.Invokes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user