Two pieces the composer has been waiting for since it was written. **The credential has to outlive its own minting.** On the bus the mesh runs on today an account is a management call: mint a password, hand it over, seal the plaintext to whoever will use it, keep nothing — which works because the broker remembers. Here the users are one file, rewritten whenever any of it changes, so keeping nothing would mean the first person's access change silently blanking every module's password. So a bus user's bcrypt hash is now recorded, keyed by the username the file needs, and the plaintext comes back exactly once. Verified against a real store that the hash verifies the password it was made from, that the password itself is not in there, that minting again rotates rather than adds, and that forgetting a node takes its host's and its modules' credentials with it. **Permissions are not stored, and that is the point.** Only the credential is kept. Authority is derived from what each module declares, every time the file is written (ADR 0043) — a stored permission list would be a second account of a user's authority, able to disagree with the records it came from, and both would look internally consistent while they did. `Users` derives the list: the controller always first and always present, one user per node, one per module per node, one per live token, one per person. Two users with one name is refused where both can be named, rather than left to be whichever one the server happened to read. A user the mesh has never minted a password for is *named* rather than dropped or written as a user anybody is: that is an ordinary situation with an obvious remedy, and the caller decides whether a partial file is worth writing. What remains of 1.7: delivering the file to the node that runs the server, and minting at enrolment and assignment — which is transport-coupled, because a node on the old bus must not be handed a credential for the new one.
124 lines
4.0 KiB
Go
124 lines
4.0 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")
|
|
}
|
|
}
|