Files
mesh-controller/internal/inventory/read_state.go
T
mesh-tools 09229d85cb Read the settings before the first pass resolves any machine, and refuse one machine alone for what it cannot keep or run (issues 1382, 1383)
The first pass of the adapter resolved every machine with no settings in the Input, so a provider whose served fact says ${setting:domain} could not be settled and the whole Input failed (the equality try on the live facts). The settings of every module that has any are read first; a module's settings that do not read are an error only for a machine that resolves to it. A recorded module that cannot be kept at the build a machine runs refuses that machine alone, as planFor did, and no longer the whole Input. The bus's refusal (no user has a credential) is carried for the machine that runs the bus.
2026-10-20 09:27:31 +00:00

254 lines
9.9 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 := range kept {
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()
}