The controller plans config, templates and moved providers' provisions, hands bus accounts to the vault and judges the new manifest words, on the feature's SDK and node-engine validator (feature 57, config and provisioning).
219 lines
8.2 KiB
Go
219 lines
8.2 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
// The move of the controller's secrets into the vault (novox/hq ADR 0349 clause 7; design 57's V3, issue 676; the
|
|
// operator's decision of 2026-10-14 on that issue).
|
|
//
|
|
// The controller opens none of the values it holds. Each row is moved by the node-engine of a machine it was sealed
|
|
// to: a module's own secret by its machine's, a pair credential by either end's. This is what the controller reads to
|
|
// ask each machine — every row sealed to it, as the store holds it — and where it keeps what moved. Nothing here
|
|
// opens, makes or changes a value: the rows are read as they are.
|
|
|
|
// SecretToMove is one row of the controller's store as the move asks a machine to hand it in.
|
|
type SecretToMove struct {
|
|
// Secret is the vault's name for the row (VaultNameOfOwn, VaultNameOfPair).
|
|
Secret string
|
|
// Node is the machine asked: the module's machine for an own secret, the end being asked for a pair.
|
|
Node string
|
|
// Module and Name are an own secret's; empty for a pair.
|
|
Module, Name string
|
|
// Provider, Provision, Consumer, ConsumerModule and Local are a pair credential's; empty for an own secret.
|
|
Provider, Provision, Consumer, ConsumerModule, Local string
|
|
Origin string
|
|
// Generation is the controller's stamp of the act that made the value (ADR 0158, ADR 0305 §5); empty for none.
|
|
Generation string
|
|
MadeAt time.Time
|
|
// Sealed is the value sealed to Node, and SealedTo the key the row says it was sealed to.
|
|
Sealed, SealedTo string
|
|
// Current says SealedTo is Node's sealing key now: only then can its engine open it.
|
|
Current bool
|
|
// Digest is the digest of the row's sealed text as the store holds it — both ends' for a pair, so either end
|
|
// reads the same — by which a row the controller changed after it moved is told from one that did not.
|
|
Digest string
|
|
// Moved is the record of the row having moved, by either end, or nil.
|
|
Moved *SecretMoved
|
|
}
|
|
|
|
// Own says the row is a module's own secret.
|
|
func (s SecretToMove) Own() bool { return s.Module != "" }
|
|
|
|
// SealedDigest is sha256 of a row's sealed texts, each on its own line, in hex: nothing of the value is kept.
|
|
func SealedDigest(sealed ...string) string {
|
|
h := sha256.New()
|
|
for _, s := range sealed {
|
|
h.Write([]byte(s + "\n"))
|
|
}
|
|
return "sha256:" + hex.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
// SecretMoved is a row the vault took: which machine's engine handed it in, the vault's generation, the digest of the
|
|
// sealed text it was moved from, and when.
|
|
type SecretMoved struct {
|
|
Node string
|
|
Generation int
|
|
Digest string
|
|
At time.Time
|
|
}
|
|
|
|
// VaultNameOfOwn is the vault's name for a module's own secret (mesh-vault OwnSecret).
|
|
func VaultNameOfOwn(node, module, name string) string {
|
|
return "own/" + node + "/" + module + "/" + name
|
|
}
|
|
|
|
// VaultNameOfPair is the vault's name for one row of the pair credentials (mesh-vault PairSecret): both ends, the
|
|
// consuming module, and the local name last when it has one.
|
|
func VaultNameOfPair(provider, provision, consumer, consumerModule, local string) string {
|
|
name := "pair/" + provider + "/" + provision + "/" + consumer + "/" + consumerModule
|
|
if local != "" {
|
|
name += "/" + local
|
|
}
|
|
return name
|
|
}
|
|
|
|
// SecretsToMoveFrom is every row sealed to a machine, as the store holds it: its modules' own secrets, and each pair
|
|
// credential it is an end of, sealed to it; each with whether its key is the machine's now and whether it moved.
|
|
func (i *Inventory) SecretsToMoveFrom(ctx context.Context, node string) ([]SecretToMove, error) {
|
|
record, err := i.NodeByName(ctx, node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
current, err := i.SealingKeyOf(ctx, node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
moved, err := i.secretsMoved(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []SecretToMove
|
|
rows, err := i.store.Pool().Query(ctx,
|
|
`select module, name, origin, coalesce(generation, ''), made_at, sealed, node_key from module_secret
|
|
where node = $1 order by module, name`, record.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
s := SecretToMove{Node: node}
|
|
if err := rows.Scan(&s.Module, &s.Name, &s.Origin, &s.Generation, &s.MadeAt, &s.Sealed, &s.SealedTo); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
s.Secret, s.Digest = VaultNameOfOwn(node, s.Module, s.Name), SealedDigest(s.Sealed)
|
|
out = append(out, s)
|
|
}
|
|
rows.Close()
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err = i.store.Pool().Query(ctx,
|
|
`select p.name, s.name, c.name, s.consumer_module, s.local, s.origin, coalesce(s.generation, ''), s.created_at,
|
|
case when s.consumer = $1 then s.for_consumer else s.for_provider end,
|
|
case when s.consumer = $1 then s.consumer_key else s.provider_key end,
|
|
s.for_consumer, s.for_provider
|
|
from secret s join node p on p.id = s.provider join node c on c.id = s.consumer
|
|
where s.consumer = $1 or s.provider = $1
|
|
order by p.name, s.name, c.name, s.consumer_module, s.local`, record.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
s := SecretToMove{Node: node}
|
|
var forConsumer, forProvider string
|
|
if err := rows.Scan(&s.Provider, &s.Provision, &s.Consumer, &s.ConsumerModule, &s.Local, &s.Origin,
|
|
&s.Generation, &s.MadeAt, &s.Sealed, &s.SealedTo, &forConsumer, &forProvider); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
s.Digest = SealedDigest(forConsumer, forProvider)
|
|
s.Secret = VaultNameOfPair(s.Provider, s.Provision, s.Consumer, s.ConsumerModule, s.Local)
|
|
out = append(out, s)
|
|
}
|
|
rows.Close()
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
for n := range out {
|
|
out[n].Current = current != "" && out[n].SealedTo == current
|
|
if m, ok := moved[out[n].Secret]; ok {
|
|
out[n].Moved = &m
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (i *Inventory) secretsMoved(ctx context.Context) (map[string]SecretMoved, error) {
|
|
rows, err := i.store.Pool().Query(ctx,
|
|
`select m.secret, n.name, m.generation, m.sealed_digest, m.moved_at from secret_moved m join node n on n.id = m.node`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := map[string]SecretMoved{}
|
|
for rows.Next() {
|
|
var name string
|
|
var m SecretMoved
|
|
if err := rows.Scan(&name, &m.Node, &m.Generation, &m.Digest, &m.At); err != nil {
|
|
return nil, err
|
|
}
|
|
out[name] = m
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// RecordSecretMoved keeps that the vault took a row, read back the same, from a machine's engine. Kept once: the
|
|
// first record of a row stands, so the other end of a pair moving it again changes nothing.
|
|
func (i *Inventory) RecordSecretMoved(ctx context.Context, s SecretToMove, generation int) error {
|
|
record, err := i.NodeByName(ctx, s.Node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = i.store.Pool().Exec(ctx,
|
|
`insert into secret_moved (secret, node, generation, sealed_digest) values ($1, $2, $3, $4)
|
|
on conflict (secret) do nothing`, s.Secret, record.ID, generation, s.Digest)
|
|
return err
|
|
}
|
|
|
|
// SecretMove is the move as started: when, by whom, and when the record of asks was copied.
|
|
type SecretMove struct {
|
|
StartedAt time.Time
|
|
StartedBy string
|
|
AsksMovedAt *time.Time
|
|
}
|
|
|
|
// StartSecretMove records that the move started, the first time; later starts keep the first. It answers the move
|
|
// as recorded.
|
|
func (i *Inventory) StartSecretMove(ctx context.Context, by string) (SecretMove, error) {
|
|
if _, err := i.store.Pool().Exec(ctx,
|
|
`insert into secret_move (started_by) values ($1) on conflict (one) do nothing`, by); err != nil {
|
|
return SecretMove{}, err
|
|
}
|
|
m, _, err := i.SecretMoveStarted(ctx)
|
|
return m, err
|
|
}
|
|
|
|
// SecretMoveStarted is the move as recorded, and whether it was started: a machine heard from is moved only then.
|
|
func (i *Inventory) SecretMoveStarted(ctx context.Context) (SecretMove, bool, error) {
|
|
var m SecretMove
|
|
err := i.store.Pool().QueryRow(ctx, `select started_at, started_by, asks_moved_at from secret_move`).
|
|
Scan(&m.StartedAt, &m.StartedBy, &m.AsksMovedAt)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return SecretMove{}, false, nil
|
|
}
|
|
return m, err == nil, err
|
|
}
|
|
|
|
// RecordAsksMoved keeps that the record of asks was copied into the vault.
|
|
func (i *Inventory) RecordAsksMoved(ctx context.Context) error {
|
|
_, err := i.store.Pool().Exec(ctx, `update secret_move set asks_moved_at = now() where asks_moved_at is null`)
|
|
return err
|
|
}
|