Replace a value given by hand like one the mesh made (hq ADR 0228)
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.
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
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...)
|
||||
}
|
||||
Reference in New Issue
Block a user