The controller could not answer an enrolment, and a probe on an open server said it could
Found while reasoning about issue 127's replay question, in code committed earlier today. The controller's permissions granted no inbox at all, so the answer to every enrolment on the mesh would have been refused — "Permissions Violation for Publish to _INBOX.enrol.anchor…" — while the controller logged that it had enrolled the node. **`allow_responses` does not cover it, and that is the trap.** It permits one reply to the reply subject of a message the user received, and a message a JetStream consumer delivers has had that field claimed for the consumer's own ack address (design 25 §2). The address the controller actually answers is the one the request carried in its *payload*, which the server does not recognise as a reply subject at all. The two mechanisms look interchangeable and are not. **My earlier verification could not have caught this.** The live enrolment tests run against a server with no accounts and no permissions, so they exercise the subjects and the round trip and nothing about authority. Composing the real configuration and running a server on it is what found it. Granted the enrolment inbox space and nothing wider: nothing but an enrolling node ever subscribes under that prefix, each scoped to its own token's, so the controller publishing there is the mesh answering enrolments and reaches nothing else. Confirmed against the permissioned server both ways — the answer arrives, and a node's own inbox is still refused. Pinned as a rule that needs no server: whatever an enrolling node subscribes, the controller must be able to publish to, and a node's, a module's and a person's inbox must stay out of reach. That check is a subject-pattern match rather than a string compare, so a grant that widened by a wildcard would not slip past it. It also bears on 127's open question about who replays a build announcement: an answer to a *module's* inbox would need `_INBOX.>`, which is exactly the blanket grant design 25 §4 refuses. So the catch-up cannot become an inbox reply.
This commit is contained in:
@@ -256,3 +256,71 @@ func TestAToolGrantThatNamesNoToolIsRefused(t *testing.T) {
|
||||
t.Fatal("a grant naming a module but no tool was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
// The controller can answer an enrolment, and reach no other inbox.
|
||||
//
|
||||
// **`allow_responses` does not cover this and that is the trap.** It permits one reply to the reply
|
||||
// subject of a message the user received — and a message a JetStream consumer delivers has had that
|
||||
// field claimed for the consumer's own ack address, so the address the controller actually answers is
|
||||
// the one the request carried in its payload, which the server does not recognise as a reply subject
|
||||
// at all.
|
||||
//
|
||||
// Found against a real server, after a live test on an *unpermissioned* one had passed: every
|
||||
// enrolment on the mesh would have timed out while the controller logged success.
|
||||
func TestTheControllerCanAnswerAnEnrolmentAndReachNoOtherInbox(t *testing.T) {
|
||||
ctl, err := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
enrolling, err := PermissionsFor(Principal{Kind: KindEnrolment, Node: "anchor", PasswordHash: "x"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Whatever the enrolling node waits on, the controller must be able to publish to.
|
||||
if len(enrolling.Subscribe) != 1 {
|
||||
t.Fatalf("an enrolling node subscribes %v, and this test knows only how to check one",
|
||||
enrolling.Subscribe)
|
||||
}
|
||||
waitsOn := enrolling.Subscribe[0]
|
||||
if !covers(ctl.Publish, waitsOn) {
|
||||
t.Fatalf("the controller may publish %v, none of which reaches %s — so every enrolment on "+
|
||||
"the mesh times out while the controller logs success", ctl.Publish, waitsOn)
|
||||
}
|
||||
|
||||
// And nothing wider. A node's own inbox and a module's are not the controller's to write into:
|
||||
// that is the blanket grant design 25 §4 refuses.
|
||||
for _, other := range []string{"_INBOX.node.anchor.x", "_INBOX.one.shop.x", "_INBOX.person.ada.x"} {
|
||||
if covers(ctl.Publish, other) {
|
||||
t.Errorf("the controller can publish to %s, which is an inbox privacy the permission "+
|
||||
"list is the only thing protecting", other)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// covers says whether any granted subject pattern admits one concrete subject, with NATS's own
|
||||
// wildcard meanings: `*` is one token, `>` is the rest.
|
||||
func covers(granted []string, subject string) bool {
|
||||
want := strings.Split(subject, ".")
|
||||
for _, pattern := range granted {
|
||||
if admits(strings.Split(pattern, "."), want) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func admits(pattern, subject []string) bool {
|
||||
for i, token := range pattern {
|
||||
if token == ">" {
|
||||
return i < len(subject)
|
||||
}
|
||||
if i >= len(subject) {
|
||||
return false
|
||||
}
|
||||
if token != "*" && token != subject[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(pattern) == len(subject)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user