Files
mesh-controller/internal/inventory/bususers_integration_test.go
T
jschoubben fbf14b68db
mesh/delivery held for a person: merged without a passing check: only a person decides what follows
mesh/delivery-group group fix/1945-credential-when-row-without-seal held: a member is held: novox/mesh-controller@fbf14b68dbab (merged without a passing check:…
mesh/check checking fbf14b68dbab on 40034be479f8
Forget the accounts of modules no longer on their nodes at the start's look, and grant sealed accounts alone (issue 1945, ADR 0543, review)
Migration 0108 marks every standing account sealed, ace's stale mesh-build-go
rows from 17:19 among them, and the forgetting ran only at a later unassign:
the next assignment to ace would have issued nothing again.

- The start's look first forgets, from each node's own resolution (assigned
  and required), the accounts of every module that no longer resolves on its
  node, and says each; only from a reading that holds assignments, and a node
  whose resolution is unknown is left alone.
- Unassign forgets a module's accounts only once it no longer resolves on the
  node (a module another requires keeps running), and never the node's own
  provisioner account; the forgetting moved from the inventory to the act.
- Forgetting holds each account's lock across its delete, so an issue in flight
  never writes a row back after it.
- A module whose accounts are not all sealed is granted its sealed accounts
  alone, and its grant is dropped when none is: an older grant naming ace no
  longer lets the assigner want it.
- An issue's locked section has a deadline of two minutes, since it holds a
  connection of the store's pool across the vault and the bus.
Tests: cases in the existing tables.
Research 072: the grants piece (memberships and credentials heard from placement).
2026-10-25 20:27:30 +01:00

196 lines
6.0 KiB
Go

//go:build integration
package inventory
import (
"context"
"slices"
"testing"
"time"
"golang.org/x/crypto/bcrypt"
)
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)
}
}
func TestMintingAgainRotatesAndMintingIfAbsentHasOneWinner(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")
}
// **An issue's mint is not issued until its seal is marked, and forgotten with its assignment** (ADR 0543 §2, issue
// 1945, live on 2026-10-25 at 20:04): a row whose credential the node does not hold is not issued.
if minted, sealed, err := inv.BusAccountIssued(ctx, "one.shop"); err != nil || !minted || !sealed {
t.Fatalf("a mint that seals in its own act: minted %v sealed %v (%v)", minted, sealed, err)
}
if _, err := inv.MintBusPasswordUnsealed(ctx, aBusUser("racer")); err != nil {
t.Fatal(err)
}
if minted, sealed, err := inv.BusAccountIssued(ctx, "one.racer"); err != nil || !minted || sealed {
t.Fatalf("an issue's mint before its seal: minted %v sealed %v (%v)", minted, sealed, err)
}
if names, err := inv.BusMinted(ctx); err != nil || slices.Contains(names, "one.racer") {
t.Fatalf("an account not sealed counts as minted: %v (%v)", names, err)
}
if err := inv.MarkBusAccountSealed(ctx, "one.racer"); err != nil {
t.Fatal(err)
}
if _, sealed, _ := inv.BusAccountIssued(ctx, "one.racer"); !sealed {
t.Fatal("an account marked sealed is not")
}
// The lock on an account holds a second issuer until the first lets go.
release, err := inv.LockBusAccount(ctx, "one.racer")
if err != nil {
t.Fatal(err)
}
waiting := make(chan struct{})
go func() {
again, err := inv.LockBusAccount(ctx, "one.racer")
if err == nil {
again()
}
close(waiting)
}()
select {
case <-waiting:
t.Fatal("a second issuer took the lock the first held")
case <-time.After(200 * time.Millisecond):
}
release()
<-waiting
if _, err := inv.ForgetBusAccountsOf(ctx, "one", "racer", true); err != nil {
t.Fatal(err)
}
if minted, _, _ := inv.BusAccountIssued(ctx, "one.racer"); minted {
t.Fatal("an unassigned module's account was kept")
}
if minted, _, _ := inv.BusAccountIssued(ctx, "one.shop"); !minted {
t.Fatal("forgetting one module's accounts forgot another's")
}
}
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)
}
}
}
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")
}
}
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")
}
}