On node-logins' ready the engine fetches the new value through the vault's own hand-out (own/<node>/mesh-host/<login>, opened with its sealing key), dials a second connection as the new login on the pinned certificate, and switches only if that authenticates, then calls mesh-vault.node-account-confirm over the link as the new login (the vault is asked in the name of the login the open link authenticated as, and only once that link is open). It keeps the old value until the key says the rotation is done and drops it then, never before; an abandon, after the switch before or after the confirmation, switches back before the new value is dropped; repeated refusals of the new login's credentials before rotation-done bring it back to the old one, said as the finding node-account-fallback (in the journal: no route for an engine's finding to the watchman is decided), and the same rotation is not tried again. Tests: two added, paid by merging two identity sealing tests into one; more to be paid by deleting the identity token tests that move to mesh-sdk with the token (774/S64). (issue 774, 774/S62, 774/S63) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
211 lines
6.8 KiB
Go
211 lines
6.8 KiB
Go
package link
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// Logins is which of its two logins this node-engine dials the bus as, and the password of each it holds (novox/hq ADR 0583 §1,
|
|
// issue 774). A node's bus account alternates between node.<n> and node.<n>--b with the node's permissions and its inbox
|
|
// _INBOX.node.<n> unchanged; the login in use is the vault's state node-logins' key of this node, which the engine reads and
|
|
// follows. Nothing here fixes the login: a membership with no Logins dials node.<n> with its own password, as before any
|
|
// rotation. The passwords are the engine's own copies (the old one is kept until the vault says the rotation is done, ADR
|
|
// 0583 §4); Save is called whenever they change, so a restart dials as the login in use.
|
|
type Logins struct {
|
|
mu sync.Mutex
|
|
node string
|
|
current string
|
|
values map[string]string
|
|
save func(LoginsState) error
|
|
changed chan struct{}
|
|
// OnDial is told how each dial as a login went: authenticated, or refused for its credentials (the Rotator's fallback).
|
|
OnDial func(user string, authenticated bool)
|
|
}
|
|
|
|
// LoginsState is what the engine keeps of its logins between runs.
|
|
type LoginsState struct {
|
|
// Current is the login in use ("" is node.<n>) and Values the password of each login held.
|
|
Current string `json:"current,omitempty"`
|
|
Values map[string]string `json:"values,omitempty"`
|
|
}
|
|
|
|
// NewLogins is the logins of node: first is the password of node.<n> (the membership's), saved what was kept last time (none
|
|
// on a first run), and save writes the state down; nil keeps nothing between runs.
|
|
func NewLogins(node, first string, saved LoginsState, save func(LoginsState) error) *Logins {
|
|
l := &Logins{node: node, current: saved.Current, values: map[string]string{}, save: save, changed: make(chan struct{}, 1)}
|
|
for login, password := range saved.Values {
|
|
l.values[login] = password
|
|
}
|
|
if first != "" {
|
|
if _, ok := l.values[delivery.FirstLogin(node)]; !ok {
|
|
l.values[delivery.FirstLogin(node)] = first
|
|
}
|
|
}
|
|
if l.current == "" {
|
|
l.current = delivery.FirstLogin(node)
|
|
}
|
|
return l
|
|
}
|
|
|
|
// Current is the login in use and its password.
|
|
func (l *Logins) Current() (login, password string) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
return l.current, l.values[l.current]
|
|
}
|
|
|
|
// Value is the password held for login.
|
|
func (l *Logins) Value(login string) (string, bool) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
v, ok := l.values[login]
|
|
return v, ok && v != ""
|
|
}
|
|
|
|
// Hold keeps the password of login, written down at once; a login of another node is refused.
|
|
func (l *Logins) Hold(login, password string) error {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
if n, ok := delivery.LoginNode(login); !ok || n != l.node {
|
|
return &loginError{login: login, node: l.node}
|
|
}
|
|
l.values[login] = password
|
|
return l.persist()
|
|
}
|
|
|
|
// Use makes login the one dialled next, when its password is held; the next dial then uses it.
|
|
func (l *Logins) Use(login string) error {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
if v := l.values[login]; v == "" {
|
|
return &loginError{login: login, node: l.node, unheld: true}
|
|
}
|
|
if l.current != login {
|
|
l.current = login
|
|
select {
|
|
case l.changed <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
return l.persist()
|
|
}
|
|
|
|
// Changed says the login in use changed since the last time it was read: the link open is dropped, and opened again as it.
|
|
func (l *Logins) Changed() <-chan struct{} { return l.changed }
|
|
|
|
// Drop forgets the password of login (never the one in use), written down at once.
|
|
func (l *Logins) Drop(login string) error {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
if login == l.current {
|
|
return &loginError{login: login, node: l.node, inUse: true}
|
|
}
|
|
delete(l.values, login)
|
|
return l.persist()
|
|
}
|
|
|
|
// Held is every login whose password is held.
|
|
func (l *Logins) Held() []string {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
var out []string
|
|
for login := range l.values {
|
|
out = append(out, login)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (l *Logins) persist() error {
|
|
if l.save == nil {
|
|
return nil
|
|
}
|
|
state := LoginsState{Current: l.current, Values: map[string]string{}}
|
|
for k, v := range l.values {
|
|
state.Values[k] = v
|
|
}
|
|
return l.save(state)
|
|
}
|
|
|
|
type loginError struct {
|
|
login, node string
|
|
unheld, inUse bool
|
|
}
|
|
|
|
func (e *loginError) Error() string {
|
|
switch {
|
|
case e.unheld:
|
|
return "this engine holds no password for the login " + e.login
|
|
case e.inUse:
|
|
return "the login " + e.login + " is the one in use, and its password is not dropped"
|
|
}
|
|
return e.login + " is no login of the node " + e.node
|
|
}
|
|
|
|
// Credential is the bus user and password a dial of m uses: the login in use when m names its logins, the node's own
|
|
// node.<n> and the membership's password otherwise (the inbox, _INBOX.node.<n>, is the node's whichever login it is).
|
|
func (m Membership) Credential() (user, password string) {
|
|
if m.Logins != nil {
|
|
return m.Logins.Current()
|
|
}
|
|
return "node." + m.Node, m.Password
|
|
}
|
|
|
|
// User is the bus user a dial of m uses.
|
|
func (m Membership) User() string {
|
|
user, _ := m.Credential()
|
|
return user
|
|
}
|
|
|
|
// Heard takes this node's key of the vault's node-logins as read (a key read, or the empty value of a key gone): the login
|
|
// in use becomes the one it names when its password is held. A key naming a login whose password is not held changes
|
|
// nothing and says so; no key leaves the login as it is.
|
|
func (l *Logins) Heard(nl delivery.NodeLogin) (said string) {
|
|
if nl.Node != l.node || nl.Login == "" {
|
|
return ""
|
|
}
|
|
cur, _ := l.Current()
|
|
if nl.Login == cur {
|
|
return ""
|
|
}
|
|
if rotatingToUs(nl, cur) {
|
|
return "" // the rotation moves the engine, which switches after its test-dial (Rotator)
|
|
}
|
|
if err := l.Use(nl.Login); err != nil {
|
|
return "node-logins names " + nl.Login + " in use and this engine cannot dial as it: " + err.Error()
|
|
}
|
|
return "node-logins names " + nl.Login + " in use: dialled as it from now"
|
|
}
|
|
|
|
// rotatingToUs says the key's rotating login is the one in use already: the engine switched ahead of the vault's own state.
|
|
func rotatingToUs(nl delivery.NodeLogin, current string) bool {
|
|
return nl.Rotating != "" && nl.Rotating == current
|
|
}
|
|
|
|
// dialed tells OnDial how a dial as user went; err is the dial's, nil for a connection made.
|
|
func (l *Logins) dialed(user string, err error) {
|
|
if l == nil || l.OnDial == nil {
|
|
return
|
|
}
|
|
switch {
|
|
case err == nil:
|
|
l.OnDial(user, true)
|
|
case refusedForCredentials(err):
|
|
l.OnDial(user, false)
|
|
}
|
|
}
|
|
|
|
// refusedForCredentials says a failed dial was the bus refusing the user or its password, not the network failing.
|
|
func refusedForCredentials(err error) bool {
|
|
if errors.Is(err, nats.ErrAuthorization) || errors.Is(err, nats.ErrAuthExpired) {
|
|
return true
|
|
}
|
|
text := strings.ToLower(err.Error())
|
|
return strings.Contains(text, "authorization violation") || strings.Contains(text, "authentication error") ||
|
|
strings.Contains(text, "user authentication")
|
|
}
|