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. and node.--b with the node's permissions and its inbox // _INBOX.node. 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. 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.) 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. (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. and the membership's password otherwise (the inbox, _INBOX.node., 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") }