A given own secret the module reads at start is held by nobody but that module, so the mesh need not read it to replace it: secret rotate now works on it, and a value given through secret accept is replaced on its own after the module's first good start under the mesh. Only a value an outside party issues (own-secrets "issued-by": "outside") or one the module applies stays as given, refused with the reason.
182 lines
7.3 KiB
Go
182 lines
7.3 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
// A value given by hand lives only until the module's first good start (novox/hq ADR 0228).
|
|
//
|
|
// **A person gives a module's own secret for one reason**: to adopt something already running that
|
|
// holds that value. A module the mesh installs fresh needs none — the mesh makes it. So for a secret
|
|
// the mesh may make (catalogue.OwnSecret.MeshMayMake: read at start, issued by nobody outside), a
|
|
// given value is kept until the module has started under the mesh on it, and then replaced with one
|
|
// the mesh makes, sealed and sent like any other. Nothing a person handled is left in force.
|
|
//
|
|
// What stays as given: a value an outside party issued (an API key, a bot token, a licence), which
|
|
// no value of the mesh's would replace; a value a module applies to a backend, until the staged
|
|
// rotation exists (ADR 0114); and a value given before this rule, which waits for a person to ask
|
|
// `secret rotate` — the mesh does not decide for them that what was given long ago is used nowhere
|
|
// else.
|
|
|
|
// AcceptGivenSecret is `secret accept` for a module's own secret: the value a person gave, sealed as
|
|
// AcceptSecretForModule seals it, and — for a secret the mesh may make — marked to be replaced after
|
|
// the module's first good start. It answers whether it was so marked.
|
|
//
|
|
// **Only the person's path marks.** The mesh's own code accepts values too — a bus account it
|
|
// issued, the controller's bus address — and those are the mesh's word to a broker, which a fresh
|
|
// random value would break; they go through AcceptSecretForModule and are never marked.
|
|
func (i *Inventory) AcceptGivenSecret(ctx context.Context, node, module, name, value string) (untilStart bool, err error) {
|
|
return i.acceptOwn(ctx, node, module, name, value, true)
|
|
}
|
|
|
|
// OwnSecretOrigin is where a module's own secret on a machine came from — OriginMade or
|
|
// OriginAccepted — and when it was made or given; empty for one it does not hold yet.
|
|
func (i *Inventory) OwnSecretOrigin(ctx context.Context, node, module, name string) (string, time.Time, error) {
|
|
record, err := i.NodeByName(ctx, node)
|
|
if err != nil {
|
|
return "", time.Time{}, err
|
|
}
|
|
var origin string
|
|
var at time.Time
|
|
err = i.store.Pool().QueryRow(ctx,
|
|
`select origin, made_at from module_secret where node = $1 and module = $2 and name = $3`,
|
|
record.ID, module, name).Scan(&origin, &at)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return "", time.Time{}, nil
|
|
}
|
|
return origin, at, err
|
|
}
|
|
|
|
// givenAgo is ", given <date>, N days ago" for a refusal about a value given to the mesh, and empty
|
|
// when the mesh holds none: how old an outside party's key is, said where a person learns it cannot
|
|
// be rotated by the mesh.
|
|
func (i *Inventory) givenAgo(ctx context.Context, nodeID any, module, name string) string {
|
|
var origin string
|
|
var at time.Time
|
|
err := i.store.Pool().QueryRow(ctx,
|
|
`select origin, made_at from module_secret where node = $1 and module = $2 and name = $3`,
|
|
nodeID, module, name).Scan(&origin, &at)
|
|
if err != nil || origin != OriginAccepted {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("; the value held was given %s, %d day(s) ago",
|
|
at.UTC().Format("2006-01-02"), int(time.Since(at).Hours()/24))
|
|
}
|
|
|
|
// GivenReplaced is one given value the mesh replaced after its module's first good start.
|
|
type GivenReplaced struct {
|
|
Module, Name string
|
|
// Given is when the replaced value was given.
|
|
Given time.Time
|
|
// Machines are the machines to send so the module, and every holder of a shared credential,
|
|
// starts again on the new value.
|
|
Machines []string
|
|
}
|
|
|
|
// ReplaceGivenAfterStart replaces every given value on a machine whose module has now started well
|
|
// under the mesh on it (novox/hq ADR 0228), and answers what it replaced; the caller sends the
|
|
// machines each names.
|
|
//
|
|
// **The signal is the machine's clean report of the declaration it was last sent** — every resource
|
|
// applied, nothing failed or refused — **when that declaration was sent after the value was given**,
|
|
// so it is the start on the given value that counts, not an older one. On an adopted machine the
|
|
// module must also be taken: until then the mesh runs nothing of it, and nothing has started under
|
|
// the mesh. Each row is claimed by clearing its mark before it is remade, so two reports arriving
|
|
// together replace a value once; a remake that fails puts the mark back, and the next good report
|
|
// tries again.
|
|
func (i *Inventory) ReplaceGivenAfterStart(ctx context.Context, node, declared string) ([]GivenReplaced, error) {
|
|
if declared == "" {
|
|
return nil, nil
|
|
}
|
|
rows, err := i.store.Pool().Query(ctx,
|
|
`select s.node, s.module, s.name, s.replace_after_start
|
|
from module_secret s
|
|
join node n on n.id = s.node
|
|
join assignment a on a.node = s.node and a.module = s.module
|
|
where n.name = $1 and n.sent = $2 and n.sent_at > s.replace_after_start
|
|
and s.origin = 'accepted' and s.replace_after_start is not null
|
|
and (not n.adopted or exists (select 1 from taken t where t.node = s.node and t.module = s.module))
|
|
order by s.module, s.name`, node, declared)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
type due struct {
|
|
nodeID any
|
|
module, name string
|
|
given time.Time
|
|
}
|
|
var dues []due
|
|
for rows.Next() {
|
|
var d due
|
|
if err := rows.Scan(&d.nodeID, &d.module, &d.name, &d.given); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
dues = append(dues, d)
|
|
}
|
|
rows.Close()
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(dues) == 0 {
|
|
return nil, nil
|
|
}
|
|
key, err := i.SealingKeyOf(ctx, node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if key == "" {
|
|
return nil, fmt.Errorf("%s has no sealing key, so the given values it holds cannot be replaced", node)
|
|
}
|
|
|
|
var out []GivenReplaced
|
|
var errs []error
|
|
for _, d := range dues {
|
|
claimed, err := i.store.Pool().Exec(ctx,
|
|
`update module_secret set replace_after_start = null
|
|
where node = $1 and module = $2 and name = $3 and origin = 'accepted'
|
|
and replace_after_start = $4`, d.nodeID, d.module, d.name, d.given)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
continue
|
|
}
|
|
if claimed.RowsAffected() != 1 {
|
|
continue // replaced by another report, or given again since
|
|
}
|
|
m, err := i.declared(ctx, d.module)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
continue
|
|
}
|
|
// The definition is asked again now, not only when the value was given: one that has since
|
|
// said the value is an outside party's, or applied, keeps it as given, and the mark stays gone.
|
|
if own, ok := m.OwnSecrets[d.name]; !ok || !own.MeshMayMake() {
|
|
continue
|
|
}
|
|
if err := i.remakeOwn(ctx, d.nodeID, key, m, d.module, d.name); err != nil {
|
|
if _, back := i.store.Pool().Exec(ctx,
|
|
`update module_secret set replace_after_start = $4
|
|
where node = $1 and module = $2 and name = $3 and origin = 'accepted'
|
|
and replace_after_start is null`, d.nodeID, d.module, d.name, d.given); back != nil {
|
|
err = errors.Join(err, fmt.Errorf("and its mark could not be put back: %w", back))
|
|
}
|
|
errs = append(errs, fmt.Errorf("%s's given %q on %s was not replaced: %w", d.module, d.name, node, err))
|
|
continue
|
|
}
|
|
machines, err := i.SharedHolders(ctx, node, d.module, d.name)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
if len(machines) == 0 {
|
|
machines = []string{node}
|
|
}
|
|
out = append(out, GivenReplaced{Module: d.module, Name: d.name, Given: d.given, Machines: machines})
|
|
}
|
|
return out, errors.Join(errs...)
|
|
}
|