The mesh writes its own user list, and at genesis there is no mesh yet to write it. So the installer carries the first one — the controller's own account at a well-known bootstrap password, exactly as the store is reached at `postgres:bootstrap` and the old bus at `guest:guest`, and rotated with them. From the controller's first composition onward the file is the controller's. That left a gap I would not have found by reading: the controller's own account is created before there is a controller to mint one, so nothing recorded a hash for it, and its first composition would have left the writer out of the file it was writing — a bus nothing can connect to, produced by the thing connected to it. It now records a hash of the credential it is actually using, and only if none is recorded, so a restart cannot put the bootstrap password back over a rotated one. The carried list and the derived one are two statements of one fact, so a test compares them: every subject the controller derives must be in the template, and nothing wider. It earned itself immediately — the composer was granting both a role's whole event branch and the one event it actually follows, which is a wider way of saying the same thing, and the wider one wins. Only the submitting half of a role is granted now; what comes back is named exactly. Getting this wrong is the worst kind of silent. A controller whose carried permissions are narrower than the ones it derives comes up, connects, and is refused on the first thing it tries, with an authorisation error naming a subject and not the template that forgot it — and a mesh cannot be raised twice to find out.
161 lines
5.4 KiB
Go
161 lines
5.4 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// The bus's users as records — against a real store, because what is being checked is that the
|
|
// column exists, the upsert behaves, and a plaintext is returned exactly once.
|
|
|
|
func aBusUser(module string) BusUser {
|
|
return BusUser{Username: "one." + module, Kind: BusModule, Node: "one", Module: module}
|
|
}
|
|
|
|
// The plaintext comes back once and the store keeps only a hash that verifies against it. **A
|
|
// credential recoverable from the mesh's store is one whose blast radius is the store's**, so what
|
|
// is asserted is that the password is not in there.
|
|
func TestABusPasswordIsReturnedOnceAndOnlyItsHashIsKept(t *testing.T) {
|
|
inv := ForTest(t)
|
|
ctx := context.Background()
|
|
|
|
password, err := inv.MintBusPassword(ctx, aBusUser("shop"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if password == "" {
|
|
t.Fatal("no password came back, so nothing can be sealed to the module")
|
|
}
|
|
|
|
hash, known, err := inv.BusUserHash(ctx, "one.shop")
|
|
if err != nil || !known {
|
|
t.Fatalf("the user was not recorded: %v %v", known, err)
|
|
}
|
|
if hash == password {
|
|
t.Fatal("the store holds the password itself")
|
|
}
|
|
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)); err != nil {
|
|
t.Fatalf("the recorded hash does not verify the password it was made from: %v", err)
|
|
}
|
|
}
|
|
|
|
// Minting again replaces what was there rather than failing or adding a second row: that is a
|
|
// rotation, and the old credential stops working at the next composition.
|
|
func TestMintingAgainRotatesRatherThanAddsAUser(t *testing.T) {
|
|
inv := ForTest(t)
|
|
ctx := context.Background()
|
|
|
|
first, err := inv.MintBusPassword(ctx, aBusUser("shop"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := inv.MintBusPassword(ctx, aBusUser("shop"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first == second {
|
|
t.Fatal("minting twice produced the same password")
|
|
}
|
|
users, err := inv.BusUsers(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(users) != 1 {
|
|
t.Fatalf("%d users after two mints for one name", len(users))
|
|
}
|
|
hash := users["one.shop"].PasswordHash
|
|
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(second)); err != nil {
|
|
t.Fatal("the kept hash is not the newest password's")
|
|
}
|
|
if bcrypt.CompareHashAndPassword([]byte(hash), []byte(first)) == nil {
|
|
t.Fatal("the previous password still verifies, so a rotation revoked nothing")
|
|
}
|
|
}
|
|
|
|
// Forgetting a node takes every credential that belonged to it — its host's and every module
|
|
// assigned to it. What a forgotten node leaves behind otherwise is a working set of credentials for
|
|
// a machine the mesh no longer knows.
|
|
func TestForgettingANodeTakesItsBusUsersWithIt(t *testing.T) {
|
|
inv := ForTest(t)
|
|
ctx := context.Background()
|
|
|
|
for _, u := range []BusUser{
|
|
{Username: "node.one", Kind: BusNode, Node: "one"},
|
|
aBusUser("shop"),
|
|
{Username: "node.two", Kind: BusNode, Node: "two"},
|
|
{Username: "controller", Kind: BusController},
|
|
} {
|
|
if _, err := inv.MintBusPassword(ctx, u); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := inv.ForgetBusUsersOf(ctx, "one"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
users, err := inv.BusUsers(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, still := users["node.one"]; still {
|
|
t.Fatal("a forgotten node's host credential still works")
|
|
}
|
|
if _, still := users["one.shop"]; still {
|
|
t.Fatal("a module on a forgotten node still has a credential")
|
|
}
|
|
// And nothing else went with it: the controller has no node, and another machine's user is
|
|
// another machine's.
|
|
for _, kept := range []string{"node.two", "controller"} {
|
|
if _, ok := users[kept]; !ok {
|
|
t.Fatalf("%s was removed with another node's users", kept)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Forgetting the users of no node would forget every user that has none — the controller and every
|
|
// person — so it is refused rather than run.
|
|
func TestForgettingTheUsersOfNoNodeIsRefused(t *testing.T) {
|
|
inv := ForTest(t)
|
|
if err := inv.ForgetBusUsersOf(context.Background(), ""); err == nil {
|
|
t.Fatal("forgetting the bus users of no node was allowed")
|
|
}
|
|
}
|
|
|
|
// The controller's own user is created by the installer, so the mesh has to be able to record a
|
|
// credential it did not mint — or the first composition leaves the writer out of the file it writes.
|
|
func TestACredentialTheMeshDidNotMintIsRecordedOnceAndNotOverwritten(t *testing.T) {
|
|
inv := ForTest(t)
|
|
ctx := context.Background()
|
|
|
|
if err := inv.SeedBusUser(ctx, BusUser{Username: "controller", Kind: BusController},
|
|
"bootstrap"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hash, known, err := inv.BusUserHash(ctx, "controller")
|
|
if err != nil || !known {
|
|
t.Fatalf("the credential was not recorded: %v %v", known, err)
|
|
}
|
|
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte("bootstrap")); err != nil {
|
|
t.Fatalf("what was recorded does not verify the credential given: %v", err)
|
|
}
|
|
|
|
// Minted since, then seeded again — which is what a restart does. The rotation must stand, or
|
|
// every restart would put the bootstrap password back over it.
|
|
minted, err := inv.MintBusPassword(ctx, BusUser{Username: "controller", Kind: BusController})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.SeedBusUser(ctx, BusUser{Username: "controller", Kind: BusController},
|
|
"bootstrap"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hash, _, err = inv.BusUserHash(ctx, "controller")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(minted)); err != nil {
|
|
t.Fatal("a restart put the bootstrap credential back over a rotated one")
|
|
}
|
|
}
|