A person's account (step 4.4, the account half)
Design 25 §7. 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. What they have is permission to ask, as a list of tools or `*` for an administrator. Four properties the tests hold it to, each of which is a way of being wrong that would not announce itself: a person reaches nothing but tools, so one cannot claim a module said something; no ack subject, because authority over a consumer that does not exist is authority nobody would audit; no allow_responses, because a person who can answer a request is impersonating a module on a bus where anyone may serve a tool; and two people do not share an inbox.
This commit is contained in:
@@ -29,6 +29,10 @@ const (
|
|||||||
KindNode Kind = "node"
|
KindNode Kind = "node"
|
||||||
KindController Kind = "controller"
|
KindController Kind = "controller"
|
||||||
KindEnrolment Kind = "enrolment"
|
KindEnrolment Kind = "enrolment"
|
||||||
|
// KindPerson is somebody reaching the mesh's tools from a workstation (design 25 §7). Its
|
||||||
|
// authority is a list of tools and nothing else — not control, not declarations, not builds,
|
||||||
|
// and no ability to answer anything, because a person asks.
|
||||||
|
KindPerson Kind = "person"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Seat is a role on the bus as a principal relates to it: the subjects it accepts, and those it
|
// Seat is a role on the bus as a principal relates to it: the subjects it accepts, and those it
|
||||||
@@ -56,6 +60,14 @@ type Principal struct {
|
|||||||
Holds []Seat
|
Holds []Seat
|
||||||
Uses []Seat
|
Uses []Seat
|
||||||
|
|
||||||
|
// Invokes are the tools a person may call, as `<module>.<tool>`; a single `*` is every tool,
|
||||||
|
// for an administrator. Only meaningful for KindPerson.
|
||||||
|
//
|
||||||
|
// **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.
|
||||||
|
Invokes []string
|
||||||
|
|
||||||
// PasswordHash is the bcrypt hash the mesh minted. The plaintext is sealed to the principal
|
// PasswordHash is the bcrypt hash the mesh minted. The plaintext is sealed to the principal
|
||||||
// and never appears here: this file is written to a node's disk and read by a server, and a
|
// and never appears here: this file is written to a node's disk and read by a server, and a
|
||||||
// secret that can be read from a configuration file is a secret with a wider blast radius
|
// secret that can be read from a configuration file is a secret with a wider blast radius
|
||||||
@@ -73,6 +85,8 @@ var safeSubject = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
|||||||
// applies, kept.
|
// applies, kept.
|
||||||
func (p Principal) Username() string {
|
func (p Principal) Username() string {
|
||||||
switch p.Kind {
|
switch p.Kind {
|
||||||
|
case KindPerson:
|
||||||
|
return "person." + p.Module
|
||||||
case KindModule:
|
case KindModule:
|
||||||
return p.Node + "." + p.Module
|
return p.Node + "." + p.Module
|
||||||
case KindNode:
|
case KindNode:
|
||||||
@@ -129,6 +143,22 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
|||||||
pub = []string{"mesh.control.>", "mesh.node.>", "mesh.build.>", "$JS.API.>"}
|
pub = []string{"mesh.control.>", "mesh.node.>", "mesh.build.>", "$JS.API.>"}
|
||||||
sub = []string{"mesh.control.>", "mesh.build.>", "$JS.API.>"}
|
sub = []string{"mesh.control.>", "mesh.build.>", "$JS.API.>"}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
case KindEnrolment:
|
case KindEnrolment:
|
||||||
// A leaked token is useless for anything but enrolling: it cannot read a declaration, hear
|
// A leaked token is useless for anything but enrolling: it cannot read a declaration, hear
|
||||||
// an event, or subscribe any inbox but the one its own token derives (design 25 §6).
|
// an event, or subscribe any inbox but the one its own token derives (design 25 §6).
|
||||||
@@ -190,6 +220,12 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.Kind == KindPerson {
|
||||||
|
// An inbox to hear answers in, and nothing else. No ack subject: a person has no durable
|
||||||
|
// consumer, because nothing is delivered to a person — they ask and are answered.
|
||||||
|
sub = append(sub, p.inbox())
|
||||||
|
}
|
||||||
|
|
||||||
if p.Kind == KindModule || p.Kind == KindNode || p.Kind == KindController {
|
if p.Kind == KindModule || p.Kind == KindNode || p.Kind == KindController {
|
||||||
// Its own reply space, and nothing wider.
|
// Its own reply space, and nothing wider.
|
||||||
sub = append(sub, p.inbox())
|
sub = append(sub, p.inbox())
|
||||||
|
|||||||
@@ -164,3 +164,80 @@ func TestAUserWithoutAPasswordIsRefused(t *testing.T) {
|
|||||||
t.Fatal("composed a user with no password hash")
|
t.Fatal("composed a user with no password hash")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A person reaches the mesh's tools from a workstation (design 25 §7). Their authority is a list
|
||||||
|
// of tools and nothing else.
|
||||||
|
func TestAPersonMayAskOnlyTheToolsTheyWereGiven(t *testing.T) {
|
||||||
|
perms, err := PermissionsFor(Principal{Kind: KindPerson, Module: "jo",
|
||||||
|
Invokes: []string{"shop.price", "telegram.status"}, PasswordHash: "x"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
has(t, perms.Publish, "mesh.mod.shop.tool.price")
|
||||||
|
has(t, perms.Publish, "mesh.mod.telegram.tool.status")
|
||||||
|
hasNot(t, perms.Publish, "mesh.mod.shop.tool.refund")
|
||||||
|
hasNot(t, perms.Publish, "mesh.mod.*.tool.>")
|
||||||
|
}
|
||||||
|
|
||||||
|
// An administrator gets every tool, which is a different grant and looks like one.
|
||||||
|
func TestAnAdministratorMayAskAnyTool(t *testing.T) {
|
||||||
|
perms, _ := PermissionsFor(Principal{Kind: KindPerson, Module: "jo",
|
||||||
|
Invokes: []string{"*"}, PasswordHash: "x"})
|
||||||
|
has(t, perms.Publish, "mesh.mod.*.tool.>")
|
||||||
|
}
|
||||||
|
|
||||||
|
// **Nothing but tools.** A person who could publish an event would be able to claim a module
|
||||||
|
// said something; one who could publish control traffic would be a second controller.
|
||||||
|
func TestAPersonReachesNothingButTools(t *testing.T) {
|
||||||
|
perms, _ := PermissionsFor(Principal{Kind: KindPerson, Module: "jo",
|
||||||
|
Invokes: []string{"*"}, PasswordHash: "x"})
|
||||||
|
for _, p := range perms.Publish {
|
||||||
|
if !strings.Contains(p, ".tool.") {
|
||||||
|
t.Errorf("a person may publish %q, which is not a tool call", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range perms.Subscribe {
|
||||||
|
if !strings.HasPrefix(s, "_INBOX.person.") {
|
||||||
|
t.Errorf("a person may subscribe %q; only their own inbox should be reachable", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A person has no durable consumer, because nothing is delivered to a person — so no ack
|
||||||
|
// subject, and an ack permission would be authority over something that does not exist.
|
||||||
|
func TestAPersonHasNoAckSubject(t *testing.T) {
|
||||||
|
perms, _ := PermissionsFor(Principal{Kind: KindPerson, Module: "jo",
|
||||||
|
Invokes: []string{"*"}, PasswordHash: "x"})
|
||||||
|
for _, p := range perms.Publish {
|
||||||
|
if strings.HasPrefix(p, "$JS.ACK") {
|
||||||
|
t.Errorf("a person was granted %q, and has no consumer to acknowledge", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A person asks and is answered; they never answer. allow_responses would let a person reply to
|
||||||
|
// a request — which, on a bus where anyone may serve a tool, is somebody impersonating a module.
|
||||||
|
func TestAPersonMayNotAnswer(t *testing.T) {
|
||||||
|
perms, _ := PermissionsFor(Principal{Kind: KindPerson, Module: "jo",
|
||||||
|
Invokes: []string{"*"}, PasswordHash: "x"})
|
||||||
|
if perms.AllowResponses {
|
||||||
|
t.Fatal("a person may answer a request, which is impersonating a module")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two people do not share an inbox, or one would read the other's answers.
|
||||||
|
func TestTwoPeopleDoNotShareAnInbox(t *testing.T) {
|
||||||
|
a, _ := PermissionsFor(Principal{Kind: KindPerson, Module: "jo", Invokes: []string{"*"}, PasswordHash: "x"})
|
||||||
|
b, _ := PermissionsFor(Principal{Kind: KindPerson, Module: "sam", Invokes: []string{"*"}, PasswordHash: "x"})
|
||||||
|
if a.Subscribe[0] == b.Subscribe[0] {
|
||||||
|
t.Fatalf("both read %s", a.Subscribe[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A malformed grant is refused rather than widened into something that happens to parse.
|
||||||
|
func TestAToolGrantThatNamesNoToolIsRefused(t *testing.T) {
|
||||||
|
if _, err := PermissionsFor(Principal{Kind: KindPerson, Module: "jo",
|
||||||
|
Invokes: []string{"shop"}, PasswordHash: "x"}); err == nil {
|
||||||
|
t.Fatal("a grant naming a module but no tool was accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user