take-over waits on a hash for every user of the loaded list, and the vault holds no value for the controller's own account, the node accounts and the enrolled devices. The bus node's engine now hands every loaded user's hash in the bus module's have, sealed to the vault's key (mesh-host and mesh-sdk on the same branch). The vault answers its key in node-key, reads the haves (manifest reads node-engine.have, granted from the manifest, no controller change), watches the bus module's have, opens the handing with its own key, and writes each user it holds no value for into bus-accounts marked adopted, with the list and node it came from, saying account-hashed. It never touches a user whose covered account it holds or whose key is its own hash, writes an adopted key again only for another hash, and refuses a handing from any node the assignments do not name as holding the bus module. Tools: account_adopt adopts now from the have as the watch does; secret_own, unnarrowed by module, lists the adopted accounts and the last adoption.
274 lines
9.2 KiB
Go
274 lines
9.2 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 adopted with nothing refused
|
|
failed string // why the last look found nothing to adopt, or ""
|
|
}
|
|
|
|
// busNode is the node the assignments name as holding the bus module: exactly one.
|
|
func (v *Vault) busNode() (string, error) {
|
|
if v.Assignments.Keys == nil {
|
|
return "", errors.New("the vault cannot read the assignments, so it cannot tell which node holds the bus")
|
|
}
|
|
keys, err := v.Assignments.Keys()
|
|
if err != nil {
|
|
return "", fmt.Errorf("the assignments' keys could not be read: %w", err)
|
|
}
|
|
var nodes []string
|
|
for _, k := range keys {
|
|
if node, module, ok := strings.Cut(k, "."); ok && module == BusModule {
|
|
nodes = append(nodes, node)
|
|
}
|
|
}
|
|
sort.Strings(nodes)
|
|
switch len(nodes) {
|
|
case 1:
|
|
return nodes[0], nil
|
|
case 0:
|
|
return "", fmt.Errorf("no node is assigned the bus module %s", BusModule)
|
|
}
|
|
return "", fmt.Errorf("%d nodes are assigned the bus module %s (%s): the vault adopts from one alone", len(nodes),
|
|
BusModule, strings.Join(nodes, ", "))
|
|
}
|
|
|
|
// 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 adoption
|
|
// took whole.
|
|
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 || 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)
|
|
}
|
|
}
|
|
}
|
|
if len(a.Refused) == 0 {
|
|
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.failed != "" {
|
|
out["why"] = v.adoption.failed
|
|
}
|
|
return out
|
|
}
|