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 , 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...) }