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.
254 lines
9.9 KiB
Go
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, ¬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 := 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()
|
|
}
|