Files
mesh-host/internal/link/logins.go
T

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")
}