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).
256 lines
10 KiB
Go
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, ¬Given):
|
|
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()
|
|
}
|