254 lines
8.7 KiB
Go
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
|
|
}
|