Files
mesh-controller/internal/inventory/read_state.go
T
mesh-tools 7515320b72 Read the credentials and the builds into mesh-sdk's composition input, and make nothing (issue 1383)
How each pair credential a node takes stands (held with its sealed consumer copy, the next send makes it, only a person's to give and nobody gave, or refused in the controller's words), the credentials made for each provider's consumers, the own secrets with their origin and key and value, the vault's kept boxes, the bus's users, minted users and memberships, the mesh's authority and each machine's serving key and kept certificate, the repositories built, the current builds and the build of each commit a machine that reads them is sent. A credential no row holds is not made (planFor made it in every mode): the adapter returns it as would-make for perform (issue 1385). New reads in the inventory (OwnSecretRows, PairCredentialState, BusMinted) and the identity (KnownAuthority, KeptCertificate) write nothing.
2026-10-20 08:58:32 +00:00

235 lines
9.4 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
}