A fall back to the old login during a rotation is now delivery.NodeHealth's Fallback (the node, the login fallen back from, why, when), written with the node's health report, from which the watchman raises node-account-fallback; it is said no more once the vault's key of the node moves on (blocker 7, ADR 0583 §6). mesh-host enrol's fall back to the controller's enrolment subject says it is the path before the lease write, which the bus refuses once the lease is written. Tests: rows in the existing rotation test; none added. (issue 774) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
155 lines
5.3 KiB
Go
155 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
|
|
"github.com/novox/mesh-host/internal/identity"
|
|
"github.com/novox/mesh-host/internal/link"
|
|
)
|
|
|
|
// loginsFile is where this engine keeps the logins it holds between runs (novox/hq ADR 0583 §1): beside its identity, readable by
|
|
// its owner alone, as the identity's membership password is.
|
|
func loginsFile(state string) string {
|
|
return filepath.Join(filepath.Dir(identity.Path(state)), "logins.json")
|
|
}
|
|
|
|
// loadLogins is the engine's logins: what it kept, with the membership's password as the first login's.
|
|
func loadLogins(state, node, first string) *link.Logins {
|
|
path := loginsFile(state)
|
|
var saved link.LoginsState
|
|
if raw, err := os.ReadFile(path); err == nil {
|
|
_ = json.Unmarshal(raw, &saved)
|
|
}
|
|
return link.NewLogins(node, first, saved, func(s link.LoginsState) error { return writeLogins(path, s) })
|
|
}
|
|
|
|
func writeLogins(path string, s link.LoginsState) error {
|
|
raw, err := json.Marshal(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmp := path + ".new"
|
|
if err := os.WriteFile(tmp, raw, 0o600); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp, path)
|
|
}
|
|
|
|
// loginsFollowAgain bounds the wait before a watch of node-logins that could not be made is tried again on the same link:
|
|
// the vault may not serve rotation yet, and until it does this node is not granted the read.
|
|
var loginsFollowAgain = 2 * time.Minute
|
|
|
|
// followLogins follows this node's key of the vault's node-logins on every link, as a watch (never a poll), for as long as ctx
|
|
// lasts, and hands each value to hear. A watch that cannot be made is said once and tried again.
|
|
func followLogins(ctx context.Context, session *link.Session, node string, hear func(delivery.NodeLogin), say link.Announce) {
|
|
said := ""
|
|
for ctx.Err() == nil {
|
|
linked, cancel, err := session.WhenLinked(ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
watching, stop := context.WithCancel(linked)
|
|
updates, werr := session.KVWatch(watching, delivery.NodeLoginsBucket, node)
|
|
if werr != nil {
|
|
stop()
|
|
if line := "this node's login is not followed yet (" + werr.Error() + "): it dials as it last did, and looks again in " + loginsFollowAgain.String(); line != said {
|
|
said = line
|
|
say(line)
|
|
}
|
|
} else {
|
|
said = ""
|
|
for u := range updates {
|
|
if u.Caught || u.Deleted || len(u.Value) == 0 {
|
|
continue
|
|
}
|
|
nl, err := delivery.DecodeNodeLogin(u.Value)
|
|
if err != nil {
|
|
say("the key " + node + " of " + delivery.NodeLoginsRead + " is not read: " + err.Error())
|
|
continue
|
|
}
|
|
hear(nl)
|
|
}
|
|
stop()
|
|
}
|
|
select {
|
|
case <-linked.Done():
|
|
case <-time.After(loginsFollowAgain):
|
|
}
|
|
cancel()
|
|
}
|
|
}
|
|
|
|
// loginSecretModule is the module the vault names this engine's login values under: own/<node>/mesh-host/<login> (ADR 0583 §3,
|
|
// insight).
|
|
const loginSecretModule = "mesh-host"
|
|
|
|
// fetchLogin asks the vault, through its existing hand-out of a machine's own secrets, for the value of login, sealed to this
|
|
// node's key, and opens it with the sealing key; no new verb is asked for it.
|
|
func fetchLogin(queue *link.Session, statePath string) func(context.Context, string) (string, error) {
|
|
unseal := sealOpener(statePath)
|
|
return func(ctx context.Context, login string) (string, error) {
|
|
raw, err := queue.AskVault(ctx, "own", map[string]any{})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var a struct {
|
|
Own []struct {
|
|
Module string `json:"module"`
|
|
Name string `json:"name"`
|
|
Sealed json.RawMessage `json:"sealed"`
|
|
} `json:"own"`
|
|
}
|
|
if err := json.Unmarshal(raw, &a); err != nil {
|
|
return "", fmt.Errorf("the vault's own answer is not one: %w", err)
|
|
}
|
|
for _, o := range a.Own {
|
|
if o.Module != loginSecretModule || o.Name != login {
|
|
continue
|
|
}
|
|
sealed := string(o.Sealed)
|
|
var text string
|
|
if json.Unmarshal(o.Sealed, &text) == nil {
|
|
sealed = text
|
|
}
|
|
value, err := unseal(sealed)
|
|
if err != nil {
|
|
return "", fmt.Errorf("the value of %s cannot be opened with this node's sealing key: %w", login, err)
|
|
}
|
|
return string(value), nil
|
|
}
|
|
return "", fmt.Errorf("the vault hands this node no value for %s (own/%s/%s/%s)", login, queueNode(queue), loginSecretModule, login)
|
|
}
|
|
}
|
|
|
|
func queueNode(q *link.Session) string { return q.Membership.Node }
|
|
|
|
// newRotator is the engine's side of a rotation over this session (ADR 0583 §3-§6): the value fetched through the vault's own
|
|
// hand-out, the test-dial on a second connection, the confirmation over the new link, and a fall back carried in the health
|
|
// report.
|
|
func newRotator(queue *link.Session, statePath string, timeout time.Duration, say link.Announce) *link.Rotator {
|
|
r := &link.Rotator{Logins: queue.Membership.Logins, Say: say,
|
|
Fetch: fetchLogin(queue, statePath),
|
|
TestDial: func(ctx context.Context, login, password string) error {
|
|
return link.TestDial(ctx, queue.Membership, login, password, timeout)
|
|
},
|
|
Confirm: func(ctx context.Context) error {
|
|
login, _ := queue.Membership.Logins.Current()
|
|
if err := queue.WhenLinkedAs(ctx, login); err != nil {
|
|
return err
|
|
}
|
|
_, err := queue.AskVault(ctx, "node-account-confirm", map[string]any{})
|
|
return err
|
|
}}
|
|
r.Watch()
|
|
// The fall back is carried in this node's health report, from which the watchman raises node-account-fallback (ADR 0583 §6).
|
|
queue.Fallback = r.Fallback
|
|
return r
|
|
}
|