Files
mesh-catalog/modules/mesh-vault/cmd/mesh-vault/adopt.go
T

254 lines
8.7 KiB
Go

package main
// The vault adopts the hashes of the bus accounts it holds no value for (novox/hq ADR 0547 clause 9, issue 1945).
//
// The controller's own account, the node accounts and the enrolled devices were never handed to the vault, so it cannot
// hash them, and the grants part's list would leave them out. The one part that has their hashes is the node-engine of
// the node holding mesh-broker, in the list the bus loaded: it hands every loaded user's hash in the bus module's have,
// sealed to this vault's key (mesh-sdk delivery.AccountsHanded), which it learns from the vault's answer to node-key.
// The vault reads that have (its manifest `reads` node-engine.have), as a watch of the bus module's have and again by
// hand with the tool `account_adopt`, and opens it with its own key.
//
// Of the users handed it takes only those it holds no value for: a user whose covered account it holds, or whose key in
// bus-accounts is its own hash, is never touched (the hash it made from the value is the one that holds). Each other
// user's hash is written to bus-accounts.<user> marked adopted, with the list it came from, saying account-hashed; an
// adopted key is written again only when the loaded list carries another hash for the user. A handing from any node but
// the one the assignments name as holding the bus module is refused: a node-engine writes only its own node's haves, so
// that node is the one that loaded the list.
import (
"errors"
"fmt"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// HaveRead is the node-engines' have bucket as the vault reads it (its manifest's `reads`).
const HaveRead = "node-engine.have"
// BusModule is the module holding mesh-broker, whose have on its node carries the handing.
const BusModule = "nats"
// HaveReader reads one key of the have bucket: nil when there is none.
type HaveReader func(key string) ([]byte, error)
// Adoption is what one adoption did: never a hash.
type Adoption struct {
Node string `json:"node"`
// From is the digest of the loaded list the handing is of; Handed how many users it holds.
From string `json:"from,omitempty"`
Handed int `json:"handed"`
// Adopted are the users whose hash was written now; Same those adopted before with the same hash; Own those the
// vault holds a value for, never adopted.
Adopted []string `json:"adopted"`
Same int `json:"same"`
Own int `json:"own"`
Refused map[string]string `json:"refused,omitempty"`
At time.Time `json:"at"`
By string `json:"by"`
}
// adoptionKept is what the vault keeps of its last adoption, for its tools and to skip a have that says the same.
type adoptionKept struct {
last *Adoption
handed string // the From and To of the handing last taken in a complete pass
failed string // why the last look found nothing to adopt, or ""
}
// AdoptNow is the tool account_adopt: the bus node's have read now and adopted, though it says the same as before.
func (v *Vault) AdoptNow(caller string) (Adoption, error) {
if v.Haves == nil {
return Adoption{}, errors.New("the vault cannot read the have bucket (" + HaveRead + ")")
}
node, err := v.busNode()
if err != nil {
return Adoption{}, err
}
raw, err := v.Haves(node + "." + BusModule)
if err != nil {
return Adoption{}, fmt.Errorf("%s %s.%s could not be read: %w", HaveRead, node, BusModule, err)
}
if raw == nil {
return Adoption{}, fmt.Errorf("%s holds no have of %s on %s", HaveRead, BusModule, node)
}
return v.AdoptFrom(node, raw, someoneOr(caller, "a tool"), true)
}
// AdoptFrom adopts from the have of the bus module on node: unless force, nothing when it hands what the last complete
// adoption took.
func (v *Vault) AdoptFrom(node string, raw []byte, by string, force bool) (Adoption, error) {
a, err := v.adopt(node, raw, by, force)
v.mu.Lock()
switch {
case err != nil:
v.adoption.failed = err.Error()
case a.At.IsZero():
default:
v.adoption.failed = ""
v.adoption.last = &a
}
v.mu.Unlock()
if err == nil && len(a.Adopted) > 0 {
v.log("adopted the hashes of %d bus accounts it holds no value for from %s's loaded list %s (%s), asked by %s",
len(a.Adopted), node, a.From, strings.Join(a.Adopted, ", "), by)
}
if len(a.Refused) > 0 {
v.log("ADOPTION REFUSED %d of the users %s handed: %v", len(a.Refused), node, a.Refused)
}
return a, err
}
func (v *Vault) adopt(node string, raw []byte, by string, force bool) (Adoption, error) {
if v.Accounts == nil {
return Adoption{}, errors.New("the vault has no state of bus accounts to write to")
}
bus, err := v.busNode()
if err != nil {
return Adoption{}, err
}
if node != bus {
return Adoption{}, fmt.Errorf("%s does not hold the bus (the assignments name %s): its handing is not adopted",
node, bus)
}
have, err := delivery.DecodeHave(raw)
if err != nil {
return Adoption{}, fmt.Errorf("the have of %s on %s is not a have: %w", BusModule, node, err)
}
if have.UserList != nil {
v.LoadedListHeard(have.UserList.Revision)
}
if have.UserList == nil || have.UserList.Handed == nil {
return Adoption{}, fmt.Errorf("the have of %s on %s hands no hashes yet (its node-engine hands them once it "+
"heard the vault's key in its answer to node-key, and writes the list)", BusModule, node)
}
h := *have.UserList.Handed
handed := h.From + " " + h.To
v.mu.Lock()
seen := v.adoption.handed == handed
v.mu.Unlock()
if seen && !force {
return Adoption{}, nil
}
accounts, err := delivery.OpenHanded(h, v.Own.PublicKey, v.Own.PrivateKey)
if err != nil {
return Adoption{}, err
}
v.hashMu.Lock()
defer v.hashMu.Unlock()
held, err := v.heldUsers()
if err != nil {
return Adoption{}, err
}
a := Adoption{Node: node, From: h.From, Handed: len(accounts), Adopted: []string{}, At: v.Now().UTC(), By: by}
refuse := func(user, why string) {
if a.Refused == nil {
a.Refused = map[string]string{}
}
a.Refused[user] = why
}
for _, acc := range accounts {
if !delivery.IsBcrypt(acc.Hash) || !namePart.MatchString(acc.User) {
refuse(acc.User, "not a bus user with a bcrypt hash")
continue
}
if held[acc.User] {
a.Own++
continue
}
was, err := v.Accounts.Get(acc.User)
switch {
case err != nil:
refuse(acc.User, "its key could not be read: "+err.Error())
continue
case was != nil && !was.Adopted:
a.Own++
continue
case was != nil && was.Hash == acc.Hash:
a.Same++
continue
}
if err := v.Accounts.Put(AccountHash{User: acc.User, Hash: acc.Hash, At: a.At, Adopted: true, From: h.From,
Node: node}); err != nil {
refuse(acc.User, "its key could not be written: "+err.Error())
continue
}
a.Adopted = append(a.Adopted, acc.User)
if v.Emit != nil {
if err := v.Emit(AccountHashedEvent, map[string]any{"user": acc.User, "adopted": true, "from": h.From,
"node": node, "at": a.At.Format(time.RFC3339Nano)}); err != nil {
v.log("the adopted hash of %s is written, but %s could not be said: %v", acc.User, AccountHashedEvent, err)
}
}
}
// A complete pass, refusals and all: the same handing is not opened again at the next change of the have (a
// refused user stays refused until the list changes, and the last adoption names it).
v.mu.Lock()
v.adoption.handed = handed
v.mu.Unlock()
return a, nil
}
// heldUsers is the bus user of every covered account the vault holds a value for.
func (v *Vault) heldUsers() (map[string]bool, error) {
records, err := v.Store.List()
if err != nil {
return nil, err
}
out := map[string]bool{}
for _, r := range records {
if !Covered(r.Name) {
continue
}
value, err := openAtRest(r, v)
if err != nil {
return nil, fmt.Errorf("the vault could not open its copy of %s, so it cannot tell whose value it holds: %w",
r.Name, err)
}
if user, _, err := accountOf(r.Name, value); err == nil {
out[user] = true
}
}
return out, nil
}
// adoptedRows is every adopted key of bus-accounts, for secret_own: never a hash.
func (v *Vault) adoptedRows() (rows []map[string]any, err error) {
if v.Accounts == nil {
return nil, nil
}
users, err := v.Accounts.Users()
if err != nil {
return nil, err
}
sort.Strings(users)
for _, u := range users {
a, err := v.Accounts.Get(u)
if err != nil || a == nil || !a.Adopted {
continue
}
rows = append(rows, map[string]any{"user": a.User, "adoptedAt": a.At, "from": a.From, "node": a.Node})
if len(rows) >= ownRowsMax {
break
}
}
return rows, nil
}
// adoptionSaid is the last adoption and why the last look adopted nothing, for the tools.
func (v *Vault) adoptionSaid() map[string]any {
v.mu.Lock()
defer v.mu.Unlock()
out := map[string]any{"last": v.adoption.last}
if v.adoption.last != nil && len(v.adoption.last.Refused) > 0 {
out["refused"] = v.adoption.last.Refused
}
if v.adoption.failed != "" {
out["why"] = v.adoption.failed
}
return out
}