Files
mesh-controller/internal/inventory/read_state.go
T
jschoubben 79ac62ffbc Issue an account whose credential is not sealed, and forget an unassigned module's accounts (issue 1945, ADR 0543)
Met a second time live on 2026-10-25 at 20:04 (note 35): mesh-build-go was
assigned to ace again, and its account rows, minted at 17:19, had outlived the
unassignment. The issue minted only when no row stood, so it minted and sealed
nothing; the node had no credential and the bus no user, and the module
crash-looped 20 times. The watch still said "the grants written".

- A bus account now says when its credential was sealed to its node (migration
  0108; every account standing before is taken as sealed). An issue under the
  assign act, the assignments' watch or a registration mints only an account
  that is not issued, a row with no seal counting as none, under the store's
  lock on the account across its look, mint and seal, and marks it sealed once
  sealed. A seal that fails leaves it unsealed for the next issue.
- Unassigning a module forgets its bus accounts and their credentials on that
  node, so an assignment again issues them afresh.
- Only sealed accounts count as minted for a composition and are named in a
  grant, so the assigner, which now wants a node only once the grant names its
  account (mesh-delivery, same branch), holds the node until it is.
- The assignment's log line says which accounts were issued and sealed, or that
  they stood issued and sealed.

The process-only issue drops the atomic insert of the review of 9ff47f6a for
the lock, which also covers the seal. Tests: cases in the existing tables.
Research 072: the grants piece (memberships and credentials heard from placement).
2026-10-25 20:17:46 +01:00

256 lines
10 KiB
Go

package inventory
import (
"context"
"errors"
"fmt"
"sort"
"github.com/jackc/pgx/v5"
"github.com/novox/mesh-controller/internal/secrets"
)
// The reads of what a composition would make or hold, in one place and writing nothing (design 47 R4b, issue 1383): the
// controller's adapter from its stores to mesh-sdk's composition input asks them once for the whole mesh, where the
// composition's own reads (SecretFor, SharedSecretFor, ModuleSecretIfIssued, GivenOwnSecret) ask for one pair or one
// secret and make what is not held.
// OwnSecretRow is one own secret held on a machine (a module_secret row): who made it (`made`, or `accepted` for a
// value a person gave), the sealing key it is sealed to, and the value sealed to that key.
type OwnSecretRow struct {
Node, Module, Name, Origin, SealedTo, Sealed string
}
// OwnSecretRows is every own secret held on every machine, by machine, module and name. It writes nothing.
func (i *Inventory) OwnSecretRows(ctx context.Context) ([]OwnSecretRow, error) {
rows, err := i.store.Pool().Query(ctx,
`select n.name, m.module, m.name, m.origin, m.node_key, m.sealed
from module_secret m join node n on n.id = m.node
order by n.name, m.module, m.name`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []OwnSecretRow
for rows.Next() {
var r OwnSecretRow
if err := rows.Scan(&r.Node, &r.Module, &r.Name, &r.Origin, &r.SealedTo, &r.Sealed); err != nil {
return nil, err
}
out = append(out, r)
}
return out, rows.Err()
}
// PairState is how one pair credential stands, as the composition's reads of it would find it (what SecretFor,
// SharedSecretFor and GivenPairSecret answer), without making anything.
type PairState struct {
// State is one of the Pair constants.
State string
// Sealed is the consumer's copy as it is sealed to the consumer's key, for a held credential.
Sealed string
// Why is the controller's words for a credential not given or refused.
Why string
}
// The states of a pair credential.
const (
// PairHeld is a credential held, sealed to the keys both ends have now.
PairHeld = "held"
// PairWouldMake is a credential the next send makes.
PairWouldMake = "would-make"
// PairNotGiven is a credential only a person gives (novox/hq ADR 0337), which nobody gave under the keys the ends have now.
PairNotGiven = "not-given"
// PairRefused is a credential the next send would be refused when it made it, for the reason Why gives.
PairRefused = "refused"
)
// PairCredentialKind says which of the controller's reads a pair is answered by.
type PairCredentialKind int
const (
// PairPlain is a credential made for the pair (SecretFor).
PairPlain PairCredentialKind = iota
// PairShared is the provider's one credential sealed to the consumer too (SharedSecretFor; Own names the provider's own secret).
PairShared
// PairOutside is a credential only a person gives (GivenPairSecret).
PairOutside
)
// PairCredential is one pair credential, by the ends it joins.
type PairCredential struct {
Kind PairCredentialKind
Name, Consumer, ConsumerModule, Provider, ProviderModule, Local string
// Own is, for a shared credential, the provider's own secret it is.
Own string
}
// PairCredentialState says how a pair credential stands now, by the rules the next send applies and without applying
// them: nothing is inserted, remade or sealed. A credential the send would make is PairWouldMake, unless the send would be
// refused (a machine with no sealing key, a value given and sealed to a key one end no longer has), which is PairRefused
// in the send's own words.
func (i *Inventory) PairCredentialState(ctx context.Context, p PairCredential) (PairState, error) {
switch p.Kind {
case PairOutside:
return i.outsideState(ctx, p)
case PairShared:
return i.sharedState(ctx, p)
}
return i.plainState(ctx, p)
}
func (i *Inventory) outsideState(ctx context.Context, p PairCredential) (PairState, error) {
got, err := i.GivenPairSecret(ctx, p.Name, p.Consumer, p.ConsumerModule, p.Provider, p.ProviderModule, p.Local)
var notGiven *NotGivenError
switch {
case errors.As(err, &notGiven):
return PairState{State: PairNotGiven, Why: notGiven.Why}, nil
case err != nil:
return PairState{}, err
}
return PairState{State: PairHeld, Sealed: got.ForConsumer}, nil
}
func (i *Inventory) plainState(ctx context.Context, p PairCredential) (PairState, error) {
consumerKey, err := i.SealingKeyOf(ctx, p.Consumer)
if err != nil {
return PairState{}, err
}
providerKey, err := i.SealingKeyOf(ctx, p.Provider)
if err != nil {
return PairState{}, err
}
consumerNode, err := i.NodeByName(ctx, p.Consumer)
if err != nil {
return PairState{}, err
}
providerNode, err := i.NodeByName(ctx, p.Provider)
if err != nil {
return PairState{}, err
}
var held Secret
err = i.store.Pool().QueryRow(ctx,
`select for_consumer, for_provider, consumer_key, provider_key, origin from secret
where name = $1 and consumer = $2 and consumer_module = $3 and provider = $4 and local = $5`,
p.Name, consumerNode.ID, p.ConsumerModule, providerNode.ID, p.Local).
Scan(&held.ForConsumer, &held.ForProvider, &held.ConsumerKey, &held.ProviderKey, &held.Origin)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return PairState{}, err
}
if err == nil && held.ConsumerKey == consumerKey && held.ProviderKey == providerKey {
return PairState{State: PairHeld, Sealed: held.ForConsumer}, nil
}
if err == nil && held.Origin == OriginAccepted {
return PairState{State: PairRefused, Why: fmt.Sprintf(
"%s's %q credential from %s was accepted from a person, and a sealing key at one end "+
"has changed since. The mesh cannot re-seal a value it does not hold: accept it "+
"again with `secret accept %s %s %s --provider %s%s`",
p.ConsumerModule, p.Name, p.Provider, p.Consumer, p.ConsumerModule, p.Name, p.Provider, localFlag(p.Local))}, nil
}
// What making one would meet: the operator's key and the two sealing keys, by the same call that seals them.
operator, err := i.OperatorKey(ctx)
if err != nil {
return PairState{}, err
}
if _, _, merr := secrets.MakeWithOperator(consumerKey, providerKey, operator); merr != nil {
return PairState{State: PairRefused, Why: merr.Error()}, nil
}
return PairState{State: PairWouldMake}, nil
}
func (i *Inventory) sharedState(ctx context.Context, p PairCredential) (PairState, error) {
consumerKey, err := i.SealingKeyOf(ctx, p.Consumer)
if err != nil {
return PairState{}, err
}
consumerNode, err := i.NodeByName(ctx, p.Consumer)
if err != nil {
return PairState{}, err
}
providerNode, err := i.NodeByName(ctx, p.Provider)
if err != nil {
return PairState{}, err
}
providerKey, err := i.SealingKeyOf(ctx, p.Provider)
if err != nil {
return PairState{}, err
}
if consumerKey == "" || providerKey == "" {
return PairState{State: PairRefused, Why: fmt.Sprintf("%s and %s both need a sealing key before %s can be shared",
p.Consumer, p.Provider, p.Name)}, nil
}
var ownGeneration, ownOrigin, ownKey *string
err = i.store.Pool().QueryRow(ctx,
`select generation, origin, node_key from module_secret where node = $1 and module = $2 and name = $3`,
providerNode.ID, p.ProviderModule, p.Own).Scan(&ownGeneration, &ownOrigin, &ownKey)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return PairState{}, err
}
var held Secret
var pairGeneration *string
err = i.store.Pool().QueryRow(ctx,
`select for_consumer, for_provider, consumer_key, provider_key, origin, generation from secret
where name = $1 and consumer = $2 and consumer_module = $3 and provider = $4 and local = $5`,
p.Name, consumerNode.ID, p.ConsumerModule, providerNode.ID, p.Local).
Scan(&held.ForConsumer, &held.ForProvider, &held.ConsumerKey, &held.ProviderKey, &held.Origin, &pairGeneration)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return PairState{}, err
}
current := ownGeneration != nil && pairGeneration != nil && *ownGeneration == *pairGeneration &&
held.ConsumerKey == consumerKey && held.ProviderKey == providerKey && ownKey != nil && *ownKey == providerKey
if current {
return PairState{State: PairHeld, Sealed: held.ForConsumer}, nil
}
if _, outside, err := i.ownIssuedOutside(ctx, p.ProviderModule, p.Own); err != nil {
return PairState{}, err
} else if outside && (ownOrigin == nil || *ownOrigin != OriginAccepted) {
return PairState{State: PairNotGiven,
Why: "nobody gave " + p.ProviderModule + "'s own secret " + p.Own + ", which is issued outside the mesh"}, nil
}
if ownOrigin != nil && *ownOrigin == OriginAccepted {
return PairState{State: PairRefused, Why: fmt.Sprintf(
"%s on %s needs %s from %s, whose credential is %s's own secret %q — a value given to the "+
"mesh, which cannot seal it to a consumer that binds later (ADR 0158): `secret accept %s %s %s` "+
"again, which seals it to every current consumer",
p.ConsumerModule, p.Consumer, p.Name, p.Provider, p.ProviderModule, p.Own, p.Provider, p.ProviderModule, p.Own)}, nil
}
return PairState{State: PairWouldMake}, nil
}
// BusMinted is the name of every bus user the mesh has minted a password hash for: `<node>.<module>` for a module and
// `provisioner.<node>` for a machine's provisioner client.
func (i *Inventory) BusMinted(ctx context.Context) ([]string, error) {
kept, err := i.BusUsers(ctx)
if err != nil {
return nil, err
}
out := make([]string, 0, len(kept))
for name, u := range kept {
if u.Sealed { // an account whose credential is not sealed to its node is not issued (ADR 0543 §2)
out = append(out, name)
}
}
sort.Strings(out)
return out, nil
}
// ModulesWithSettings is every module that has a settings row, mesh-wide or for a node: the modules whose layers
// SettingsFor can answer with anything.
func (i *Inventory) ModulesWithSettings(ctx context.Context) (map[string]bool, error) {
rows, err := i.store.Pool().Query(ctx, `select distinct module from settings`)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]bool{}
for rows.Next() {
var module string
if err := rows.Scan(&module); err != nil {
return nil, err
}
out[module] = true
}
return out, rows.Err()
}