The delivery question, decided. The alternative was a manifest field enumerating the server's ports, TLS paths and store directory so the controller could write a whole configuration file. That is wrong: those are properties of the container the module raises, they live in its image and its mounts, and the controller would have to be kept in step with a Dockerfile it never sees. So the mesh writes only what only the mesh knows — who may connect — and the module's own configuration includes it. `ComposeAccounts` is that file. A test says what must *not* be in it as plainly as what must: no port, no tls block, no store_dir. Each of those in the mesh's file is a value the controller would then own, and the module could no longer change its own image without the mesh agreeing. `bus-users` is where a module wants it written, and **asking is not enough to receive it**: the file holds every user's password hash, so a module that could ask for it could read every credential on the bus. The claim on `mesh-broker` authorises it, checked from the manifest alone. A holder with nothing composed is refused rather than given an empty file, for the reason a certificate is — a bus with no user list refuses every connection in the mesh and looks like a machine problem. Six claims checked against a running server before any of this was committed to, and two of them changed what got written: **An absolute include path is resolved relative to the including file's directory.** `include /etc/nats/accounts.conf` from /etc/nats-server/nats.conf makes the server look for /etc/nats-server/etc/nats/accounts.conf and refuse to start. So both files share one directory, and the module declares its own as a file resource beside the mesh's. **`verify: true` was refusing every connection in the mesh.** It makes the server demand a *client* certificate, and nothing in the mesh presents one: a host pins this server's exact certificate and authenticates with the password the mesh minted, and so does a module's runtime. Every connection died at the TLS handshake before any password was looked at, with an error — "client didn't provide a certificate" — that reads as a fault in the client. Removed. TLS is still required; verify only decides whether client certificates are checked. The other four: a user in an included file authenticates, an unknown user is refused so the include is the whole authority rather than an addition, a publish outside a grant is refused, and rewriting the mesh's half alone makes a new user appear — noticed by the module's own watcher, with no signal from outside, and without dropping the connection the mesh already had. That last one is task 1.2's payoff, collected.
81 lines
2.7 KiB
Go
81 lines
2.7 KiB
Go
package catalogue
|
|
|
|
import "testing"
|
|
|
|
// The mesh's user list reaches the module holding the bus, and nothing else.
|
|
//
|
|
// Three refusals and one delivery, because each of the refusals would be silent in a different way:
|
|
// a module that asked and was given it could read every credential on the bus; a bus given an empty
|
|
// file refuses every connection in the mesh and looks like a machine problem; and a bus that never
|
|
// asked gets nothing rather than a file it does not read.
|
|
func TestTheMeshsUserListGoesOnlyToTheModuleHoldingTheBus(t *testing.T) {
|
|
theBus := func() Manifest {
|
|
return Manifest{
|
|
Module: "nats", Version: "1",
|
|
Claims: []Claim{{Name: "mesh-broker", Scope: ScopeMesh}},
|
|
BusUsers: "/var/lib/nats-module/conf/accounts.conf",
|
|
Resources: []map[string]any{},
|
|
}
|
|
}
|
|
|
|
on := func(t *testing.T, m Manifest, with Rendering) ([]map[string]any, error) {
|
|
t.Helper()
|
|
return Resolution{Node: "anchor", Modules: []Manifest{m}}.Declaration(with)
|
|
}
|
|
|
|
t.Run("the holder is given it", func(t *testing.T) {
|
|
resources, err := on(t, theBus(), Rendering{BusUsers: "accounts { MESH { users = [] } }"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Prefixed with the module it came from, like every resource: two modules may reasonably
|
|
// both call something "config", and without the prefix the second would silently replace
|
|
// the first.
|
|
var found map[string]any
|
|
for _, r := range resources {
|
|
if r["id"] == "nats."+BusUsersID() {
|
|
found = r
|
|
}
|
|
}
|
|
if found == nil {
|
|
t.Fatalf("the bus was given no user list: %+v", resources)
|
|
}
|
|
if found["path"] != "/var/lib/nats-module/conf/accounts.conf" {
|
|
t.Errorf("written to %v rather than where the module asked", found["path"])
|
|
}
|
|
if found["mode"] != "0600" {
|
|
t.Errorf("mode %v: a list of every user in the mesh belongs to the one process that "+
|
|
"needs it", found["mode"])
|
|
}
|
|
})
|
|
|
|
t.Run("a module that does not claim the seat is refused", func(t *testing.T) {
|
|
m := theBus()
|
|
m.Claims = nil
|
|
if _, err := on(t, m, Rendering{BusUsers: "accounts {}"}); err == nil {
|
|
t.Fatal("a module that claims nothing was handed every user's password hash")
|
|
}
|
|
})
|
|
|
|
t.Run("the holder with nothing composed is refused", func(t *testing.T) {
|
|
if _, err := on(t, theBus(), Rendering{}); err == nil {
|
|
t.Fatal("the bus was given an empty user list, so it would refuse every connection in " +
|
|
"the mesh and look like a machine problem")
|
|
}
|
|
})
|
|
|
|
t.Run("a module that did not ask gets nothing", func(t *testing.T) {
|
|
m := theBus()
|
|
m.BusUsers = ""
|
|
resources, err := on(t, m, Rendering{BusUsers: "accounts {}"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, r := range resources {
|
|
if r["id"] == "nats."+BusUsersID() {
|
|
t.Fatal("a module that asked for no user list was given one")
|
|
}
|
|
}
|
|
})
|
|
}
|