Genesis can raise a mesh on the new bus, and the carried user list is checked against the composer
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.
This commit is contained in:
@@ -136,3 +136,30 @@ func (i *Inventory) ForgetBusUsersOf(ctx context.Context, node string) error {
|
||||
_, err := i.store.Pool().Exec(ctx, `delete from bus_user where node = $1`, node)
|
||||
return err
|
||||
}
|
||||
|
||||
// SeedBusUser records a hash of a credential the mesh did not mint, so a composition contains it.
|
||||
//
|
||||
// **Genesis is the reason this exists.** The controller's own user is created before the controller
|
||||
// runs — by the installer, at a well-known bootstrap password, the way the store's and the old bus's
|
||||
// are (`postgres:bootstrap`, `guest:guest`). Nothing minted it, so nothing recorded a hash for it, and
|
||||
// the controller's first composition would leave itself out of the very file it was writing: a bus
|
||||
// nothing can connect to, produced by the thing connected to it.
|
||||
//
|
||||
// Idempotent, and it does not overwrite. A credential the mesh *did* mint is the one that counts, so
|
||||
// once there is a row this does nothing — otherwise a restart would put the bootstrap password back
|
||||
// over a rotated one.
|
||||
func (i *Inventory) SeedBusUser(ctx context.Context, u BusUser, password string) error {
|
||||
if u.Username == "" || u.Kind == "" || password == "" {
|
||||
return errors.New("a bus user needs a username, a kind and the credential it is using")
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot hash a bus password: %w", err)
|
||||
}
|
||||
_, err = i.store.Pool().Exec(ctx,
|
||||
`insert into bus_user (username, kind, node, module, password_hash)
|
||||
values ($1, $2, $3, $4, $5)
|
||||
on conflict (username) do nothing`,
|
||||
u.Username, u.Kind, u.Node, u.Module, string(hash))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -121,3 +121,40 @@ func TestForgettingTheUsersOfNoNodeIsRefused(t *testing.T) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user