The bus on NATS: both transports behind seams, and the rollout switch #87
+27
-2
@@ -13,6 +13,7 @@
|
|||||||
package broker
|
package broker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -94,7 +95,16 @@ func (p Principal) Username() string {
|
|||||||
case KindController:
|
case KindController:
|
||||||
return "controller"
|
return "controller"
|
||||||
case KindEnrolment:
|
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 ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -172,8 +182,23 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
|||||||
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).
|
||||||
|
//
|
||||||
|
// **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"}
|
pub = []string{"mesh.control.enrol"}
|
||||||
sub = []string{}
|
sub = []string{p.inbox()}
|
||||||
|
|
||||||
case KindNode:
|
case KindNode:
|
||||||
// A host publishes its own node's control traffic and subscribes its own declaration —
|
// 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"},
|
TLSCert: "/tls/tls.crt", TLSKey: "/tls/tls.key", TLSCA: "/tls/ca.crt"},
|
||||||
[]Principal{
|
[]Principal{
|
||||||
{Kind: KindController, PasswordHash: "$2a$11$cccccccccccccccccccccc"},
|
{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: KindNode, Node: "one", PasswordHash: "$2a$11$nnnnnnnnnnnnnnnnnnnnnn"},
|
||||||
{Kind: KindModule, Node: "one", Module: "telegram", Holds: []Seat{seat},
|
{Kind: KindModule, Node: "one", Module: "telegram", Holds: []Seat{seat},
|
||||||
Serves: []string{"status"}, PasswordHash: "$2a$11$tttttttttttttttttttttt"},
|
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).
|
// A leaked enrolment token is useless for anything but enrolling (design 25 §6).
|
||||||
func TestTheEnrolmentUserCanOnlyEnrol(t *testing.T) {
|
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" {
|
if len(perms.Publish) != 1 || perms.Publish[0] != "mesh.control.enrol" {
|
||||||
t.Fatalf("enrolment may publish %v", perms.Publish)
|
t.Fatalf("enrolment may publish %v", perms.Publish)
|
||||||
}
|
}
|
||||||
if len(perms.Subscribe) != 0 {
|
// Its own inbox and nothing else. **Nothing else** is the point: no declaration, no event, and
|
||||||
t.Fatalf("enrolment may subscribe %v, and should hear nothing", perms.Subscribe)
|
// 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"] }
|
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" }
|
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"] }
|
publish: { allow: ["mesh.control.enrol"] }
|
||||||
subscribe: { allow: [] }
|
subscribe: { allow: ["_INBOX.enrol.one.>"] }
|
||||||
} }
|
} }
|
||||||
{ user: "node.one", password: "$2a$11$nnnnnnnnnnnnnnnnnnnnnn", permissions: {
|
{ user: "node.one", password: "$2a$11$nnnnnnnnnnnnnnnnnnnnnn", permissions: {
|
||||||
publish: { allow: ["$JS.ACK.NODES.one.>", "mesh.control.one.>"] }
|
publish: { allow: ["$JS.ACK.NODES.one.>", "mesh.control.one.>"] }
|
||||||
|
|||||||
Reference in New Issue
Block a user