The bus on NATS: both transports behind seams, and the rollout switch #87
+27
-2
@@ -13,6 +13,7 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
@@ -94,7 +95,16 @@ func (p Principal) Username() string {
|
||||
case KindController:
|
||||
return "controller"
|
||||
case KindEnrolment:
|
||||
return "enrolment"
|
||||
// Per token, not one shared user. **The inbox is the reason**: with a single `enrolment`
|
||||
// user every machine enrolling at once could read every other's answer, and an answer
|
||||
// carries that node's credentials sealed to it. Design 25 §6 says the inbox a token
|
||||
// derives, and a permission belongs to a user, so the user is per token.
|
||||
//
|
||||
// Named after the node, which **is** the token's id: a token is issued for a node record,
|
||||
// the mesh holds one live claim per record, and the node's name is the one identifier both
|
||||
// sides already have before anything else is agreed. It is also exactly what the other
|
||||
// transport does, where the account is named after the node and the secret is its password.
|
||||
return "enrol." + p.Node
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -172,8 +182,23 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
case KindEnrolment:
|
||||
// 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).
|
||||
//
|
||||
// **The inbox was missing and the handshake could not have completed without it.** An
|
||||
// enrolling node publishes its request and waits on an address it states in the payload;
|
||||
// with nothing to subscribe it waits out its timeout against a mesh that answered. Its own
|
||||
// and no wider: `_INBOX.enrol.<node>.>`, so what is sealed to one machine cannot be read by
|
||||
// another enrolling beside it.
|
||||
if p.Node == "" {
|
||||
// Refused rather than composed into `_INBOX.enrol..>`, which is a subject with an empty
|
||||
// token in it — and worse, one every nameless enrolment user would share. A shared
|
||||
// enrolment inbox is one machine able to read the credentials sealed to another.
|
||||
return Permissions{}, errors.New(
|
||||
"an enrolment user names no node, so its inbox would be shared with every other " +
|
||||
"enrolment: a token is issued for a node record, and that record's name is " +
|
||||
"the token's id")
|
||||
}
|
||||
pub = []string{"mesh.control.enrol"}
|
||||
sub = []string{}
|
||||
sub = []string{p.inbox()}
|
||||
|
||||
case KindNode:
|
||||
// A host publishes its own node's control traffic and subscribes its own declaration —
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestTheComposedConfigMatchesTheGolden(t *testing.T) {
|
||||
TLSCert: "/tls/tls.crt", TLSKey: "/tls/tls.key", TLSCA: "/tls/ca.crt"},
|
||||
[]Principal{
|
||||
{Kind: KindController, PasswordHash: "$2a$11$cccccccccccccccccccccc"},
|
||||
{Kind: KindEnrolment, PasswordHash: "$2a$11$eeeeeeeeeeeeeeeeeeeeee"},
|
||||
{Kind: KindEnrolment, Node: "one", PasswordHash: "$2a$11$eeeeeeeeeeeeeeeeeeeeee"},
|
||||
{Kind: KindNode, Node: "one", PasswordHash: "$2a$11$nnnnnnnnnnnnnnnnnnnnnn"},
|
||||
{Kind: KindModule, Node: "one", Module: "telegram", Holds: []Seat{seat},
|
||||
Serves: []string{"status"}, PasswordHash: "$2a$11$tttttttttttttttttttttt"},
|
||||
|
||||
@@ -115,12 +115,27 @@ func TestAHostIsConfinedToItsOwnNode(t *testing.T) {
|
||||
|
||||
// A leaked enrolment token is useless for anything but enrolling (design 25 §6).
|
||||
func TestTheEnrolmentUserCanOnlyEnrol(t *testing.T) {
|
||||
perms, _ := PermissionsFor(Principal{Kind: KindEnrolment, PasswordHash: "x"})
|
||||
perms, err := PermissionsFor(Principal{Kind: KindEnrolment, Node: "anchor", PasswordHash: "x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(perms.Publish) != 1 || perms.Publish[0] != "mesh.control.enrol" {
|
||||
t.Fatalf("enrolment may publish %v", perms.Publish)
|
||||
}
|
||||
if len(perms.Subscribe) != 0 {
|
||||
t.Fatalf("enrolment may subscribe %v, and should hear nothing", perms.Subscribe)
|
||||
// Its own inbox and nothing else. **Nothing else** is the point: no declaration, no event, and
|
||||
// no other machine's answer — and the inbox itself is needed, because a node that cannot
|
||||
// subscribe one waits out its timeout against a mesh that answered.
|
||||
if len(perms.Subscribe) != 1 || perms.Subscribe[0] != "_INBOX.enrol.anchor.>" {
|
||||
t.Fatalf("enrolment may subscribe %v, which is not its own inbox alone", perms.Subscribe)
|
||||
}
|
||||
}
|
||||
|
||||
// An enrolment user that names no node is refused: its inbox would be an empty subject token, and
|
||||
// one that every nameless enrolment user shared — which is one machine reading the credentials
|
||||
// sealed to another.
|
||||
func TestAnEnrolmentUserWithoutANodeIsRefused(t *testing.T) {
|
||||
if _, err := PermissionsFor(Principal{Kind: KindEnrolment, PasswordHash: "x"}); err == nil {
|
||||
t.Fatal("an enrolment user with no node was composed, so its inbox is shared")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -24,9 +24,9 @@ accounts {
|
||||
subscribe: { allow: ["$JS.API.>", "_INBOX.controller.>", "mesh.build.>", "mesh.control.>", "mesh.mod.mesh-catalog.event.module.mesh-catalog.catching-up", "mesh.mod.mesh-catalog.event.module.mesh-catalog.upgraded"] }
|
||||
allow_responses: { max: 1, ttl: "1m" }
|
||||
} }
|
||||
{ user: "enrolment", password: "$2a$11$eeeeeeeeeeeeeeeeeeeeee", permissions: {
|
||||
{ user: "enrol.one", password: "$2a$11$eeeeeeeeeeeeeeeeeeeeee", permissions: {
|
||||
publish: { allow: ["mesh.control.enrol"] }
|
||||
subscribe: { allow: [] }
|
||||
subscribe: { allow: ["_INBOX.enrol.one.>"] }
|
||||
} }
|
||||
{ user: "node.one", password: "$2a$11$nnnnnnnnnnnnnnnnnnnnnn", permissions: {
|
||||
publish: { allow: ["$JS.ACK.NODES.one.>", "mesh.control.one.>"] }
|
||||
|
||||
Reference in New Issue
Block a user