211 lines
6.9 KiB
Go
211 lines
6.9 KiB
Go
package link
|
|
|
|
import (
|
|
"errors"
|
|
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
|
|
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
)
|
|
|
|
// 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[meshvaultclient.FirstLogin(node)]; !ok {
|
|
l.values[meshvaultclient.FirstLogin(node)] = first
|
|
}
|
|
}
|
|
if l.current == "" {
|
|
l.current = meshvaultclient.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 := meshvaultclient.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 meshvault.NodeLoginsByNodeValue) (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 meshvault.NodeLoginsByNodeValue, 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")
|
|
}
|