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)
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

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).
This commit is contained in:
2026-10-25 20:27:30 +01:00
parent 79ac62ffbc
commit fbf14b68db
9 changed files with 234 additions and 32 deletions
+41 -13
View File
@@ -141,30 +141,58 @@ func (i *Inventory) LockBusAccount(ctx context.Context, username string) (func()
}
// ForgetBusAccountsOf forgets the bus accounts of module on node, and the credentials sealed to the node for them (its
// `broker`, each process's `broker-<id>`, its provisioner client's): an unassigned module keeps no account, so an
// assignment again mints and seals afresh, never trusting a row whose credential the node no longer holds (issue 1945,
// live on 2026-10-25 at 20:04: ace's mesh-build-go rows from 17:19 survived its unassignment, nothing was minted or
// sealed again, and the module found no credential).
func (i *Inventory) ForgetBusAccountsOf(ctx context.Context, node, module string) error {
// `broker`, each process's `broker-<id>`, and with nodeOwned its provisioner client's): a module that no longer resolves
// on a node keeps no account there, so an assignment again mints and seals afresh, never trusting a row whose credential
// the node no longer holds (issue 1945, live on 2026-10-25 at 20:04: ace's mesh-build-go rows from 17:19 outlived its
// unassignment, nothing was issued again, and the module found no credential). The provisioner client's account is the
// node's (`provisioner.<node>`), forgotten only with nodeOwned. Each account's lock is held across its delete, so an issue
// in flight never writes a row back after it. It answers the accounts forgotten.
func (i *Inventory) ForgetBusAccountsOf(ctx context.Context, node, module string, nodeOwned bool) ([]string, error) {
if node == "" || module == "" {
return errors.New("forgetting the bus accounts of no node or no module would forget others' accounts")
return nil, errors.New("forgetting the bus accounts of no node or no module would forget others' accounts")
}
rows, err := i.store.Pool().Query(ctx,
`select username from bus_user where node = $1 and module = $2 and kind <> $3 and ($4 or kind <> $5)
order by username`, node, module, BusController, nodeOwned, BusProvisioner)
if err != nil {
return nil, err
}
var names []string
for rows.Next() {
var n string
if err := rows.Scan(&n); err != nil {
rows.Close()
return nil, err
}
names = append(names, n)
}
rows.Close()
if err := rows.Err(); err != nil {
return nil, err
}
for _, n := range names { // in order, one lock at a time per issuer: no two hold each other's
release, err := i.LockBusAccount(ctx, n)
if err != nil {
return nil, err
}
defer release()
}
tx, err := i.store.Pool().Begin(ctx)
if err != nil {
return err
return nil, err
}
defer func() { _ = tx.Rollback(context.WithoutCancel(ctx)) }()
if _, err := tx.Exec(ctx, `delete from bus_user where node = $1 and module = $2 and kind <> $3`,
node, module, BusController); err != nil {
return fmt.Errorf("the bus accounts of %s on %s are not forgotten: %w", module, node, err)
if _, err := tx.Exec(ctx, `delete from bus_user where username = any($1)`, names); err != nil {
return nil, fmt.Errorf("the bus accounts of %s on %s are not forgotten: %w", module, node, err)
}
if _, err := tx.Exec(ctx,
`delete from module_secret s using node n
where s.node = n.id and n.name = $1 and s.module = $2
and (s.name = 'broker' or s.name like 'broker-%' or s.name = 'provisioner-broker')`, node, module); err != nil {
return fmt.Errorf("the bus credentials of %s on %s are not forgotten: %w", module, node, err)
and (s.name = 'broker' or s.name like 'broker-%' or ($3 and s.name = 'provisioner-broker'))`,
node, module, nodeOwned); err != nil {
return nil, fmt.Errorf("the bus credentials of %s on %s are not forgotten: %w", module, node, err)
}
return tx.Commit(ctx)
return names, tx.Commit(ctx)
}
// MintBusPasswordUnsealed mints u's password, replacing whatever stands, with no seal yet: the account is not issued
@@ -105,7 +105,7 @@ func TestMintingAgainRotatesAndMintingIfAbsentHasOneWinner(t *testing.T) {
}
release()
<-waiting
if err := inv.ForgetBusAccountsOf(ctx, "one", "racer"); err != nil {
if _, err := inv.ForgetBusAccountsOf(ctx, "one", "racer", true); err != nil {
t.Fatal(err)
}
if minted, _, _ := inv.BusAccountIssued(ctx, "one.racer"); minted {
-4
View File
@@ -670,10 +670,6 @@ func (i *Inventory) Unassign(ctx context.Context, nodeName, module string) error
if err := i.chainUnassign(ctx, c, node, module); err != nil {
return err
}
// Its bus accounts on the node go with it: an assignment again issues them afresh (issue 1945).
if err := i.ForgetBusAccountsOf(ctx, nodeName, module); err != nil {
return err
}
// The seat it held falls back to derivation rather than pointing at nothing: what the store's foreign key did.
if _, err := i.store.Pool().Exec(ctx, `delete from seat_holding where node = $1 and module = $2`, node.ID, module); err != nil {
return err