Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
653cead338 | ||
|
|
d1ca199265 |
@@ -53,8 +53,6 @@ type AccountHash struct {
|
||||
Adopted bool `json:"adopted,omitempty"`
|
||||
From string `json:"from,omitempty"`
|
||||
Node string `json:"node,omitempty"`
|
||||
// Rotating is a rotation under way over a second login (rotation.go, ADR 0561 §4): absent when none is.
|
||||
Rotating *Rotating `json:"rotating,omitempty"`
|
||||
}
|
||||
|
||||
// AccountStore is the vault's state of bus-account hashes: read and written by the vault's instances alone.
|
||||
@@ -63,8 +61,6 @@ type AccountStore interface {
|
||||
Put(a AccountHash) error
|
||||
// Users is every user with a key.
|
||||
Users() ([]string, error)
|
||||
// Delete takes a user's key out: an account retired (retire.go).
|
||||
Delete(user string) error
|
||||
}
|
||||
|
||||
// busAccounts is AccountStore over the vault's own state through its runtime.
|
||||
@@ -84,8 +80,6 @@ func (busAccounts) Get(user string) (*AccountHash, error) {
|
||||
|
||||
func (busAccounts) Users() ([]string, error) { return stdio.State(BusAccountsState).Keys() }
|
||||
|
||||
func (busAccounts) Delete(user string) error { return stdio.State(BusAccountsState).Delete(user) }
|
||||
|
||||
func (busAccounts) Put(a AccountHash) error {
|
||||
_, err := stdio.State(BusAccountsState).Put(a.User, a)
|
||||
return err
|
||||
@@ -187,10 +181,6 @@ func (v *Vault) HashAccount(secret string, force bool) (Hashed, error) {
|
||||
if err != nil {
|
||||
return Hashed{}, err
|
||||
}
|
||||
if rec.Retired != nil {
|
||||
v.hashes.paid(secret)
|
||||
return Hashed{}, fmt.Errorf("%s is retired (%s): its hash is written again only when it is wanted again", secret, rec.Retired.Why)
|
||||
}
|
||||
user, password, err := accountOf(secret, value)
|
||||
if err != nil {
|
||||
return Hashed{}, err
|
||||
@@ -269,7 +259,7 @@ func (v *Vault) HashHeld() (written []Hashed, failed map[string]string) {
|
||||
return nil, failed
|
||||
}
|
||||
for _, r := range records {
|
||||
if !Covered(r.Name) || r.Retired != nil {
|
||||
if !Covered(r.Name) {
|
||||
continue
|
||||
}
|
||||
h, err := v.HashAccount(r.Name, false)
|
||||
|
||||
@@ -58,6 +58,32 @@ type adoptionKept struct {
|
||||
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 {
|
||||
@@ -117,9 +143,6 @@ func (v *Vault) adopt(node string, raw []byte, by string, force bool) (Adoption,
|
||||
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)
|
||||
|
||||
@@ -48,13 +48,6 @@ func (f *fakeAccounts) Put(a AccountHash) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeAccounts) Delete(user string) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
delete(f.keys, user)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeAccounts) Users() ([]string, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
package main
|
||||
|
||||
// The switch on the controller's lease (novox/hq ADR 0561 §3): minting is on only while the lease is handed to a holder
|
||||
// that is no controller, and off for a controller's value, a key deleted or expired, or one that cannot be read. The
|
||||
// write that hands it away turns it on and mints every wanted account the vault holds no value for; a lease handed back
|
||||
// turns it off. No clock and no setting turns it. A key's age passing is heard by no watch, so the lease is read afresh
|
||||
// before every mint (LeaseRead): a lease that expired since mints nothing.
|
||||
|
||||
import "git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
|
||||
// LeaseHeard is the watch of the controller's lease. When minting turns on or off, every wanted account is judged again:
|
||||
// minted when on, held back with the new why when off.
|
||||
func (v *Vault) LeaseHeard(raw []byte, found bool, readErr error) {
|
||||
if !v.setLease(raw, found, readErr) {
|
||||
return
|
||||
}
|
||||
on, why := v.mintingOn()
|
||||
v.log("minting is %s: %s", map[bool]string{true: "ON", false: "off"}[on], why)
|
||||
for _, user := range v.wantedUsers() {
|
||||
if w, ok := v.wanted(user); ok {
|
||||
if _, err := v.mintWanted(w, "the lease write"); err != nil {
|
||||
v.log("%s is not minted: %v", user, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setLease records what the lease says, and answers whether minting turned on or off by it.
|
||||
func (v *Vault) setLease(raw []byte, found bool, readErr error) bool {
|
||||
on, why := buscompose.LeaseHandedAway(raw, found, readErr)
|
||||
m := &v.minting
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
flipped := on != m.on || m.why == ""
|
||||
if flipped {
|
||||
m.since = v.Now().UTC()
|
||||
}
|
||||
m.on, m.why = on, why
|
||||
return flipped
|
||||
}
|
||||
|
||||
// leaseAfresh reads the lease again before a mint, where the vault can: a key that expired since is heard by no watch.
|
||||
func (v *Vault) leaseAfresh() {
|
||||
if v.LeaseRead == nil {
|
||||
return
|
||||
}
|
||||
raw, found, err := v.LeaseRead()
|
||||
v.setLease(raw, found, err)
|
||||
}
|
||||
@@ -51,8 +51,7 @@ func build() *Env {
|
||||
// Where it sees a consumer bind a provider (pair.go): the same mirror, every key.
|
||||
env.Vault = &Vault{Store: store, Own: own, Askers: Declared{Read: readAssignment, Store: store},
|
||||
Assignments: Mirror{Keys: assignmentKeys, Read: readAssignment}, Emit: emit, Ask: stdio.Ask,
|
||||
Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave, LeaseRead: readLease, WantRead: readWant,
|
||||
Placement: placement}
|
||||
Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave}
|
||||
return env
|
||||
}
|
||||
|
||||
@@ -96,74 +95,6 @@ func watchHandings(env *Env) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// watchWants mints from the grants part's wanted accounts as they change (mint.go, ADR 0561 §2): a watch, never a poll.
|
||||
// It answers whether the watch began; one that did not is begun again at the next minute pass.
|
||||
func watchWants(env *Env) bool {
|
||||
if env.Vault == nil {
|
||||
return true
|
||||
}
|
||||
err := stdio.State(WantedRead).Watch("", func(c stdio.StateChange) error {
|
||||
env.Vault.WantHeard(c.Key, c.Value, c.Op == "delete" || c.Op == "purge" || len(c.Value) == 0)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
say("the grants part's wanted accounts are not watched, so nothing is minted; begun again at the next pass: %v", err)
|
||||
return false
|
||||
}
|
||||
// The watch returns once every current key was handed: what has no key now is no longer wanted.
|
||||
env.Vault.WantsRead()
|
||||
return true
|
||||
}
|
||||
|
||||
// placement reads the indexer's placement through the runtime under the vault's own account (its manifest reads it).
|
||||
var placement = PlacementReader{
|
||||
Keys: func() ([]string, error) { return stdio.State(PlacementRead).Keys() },
|
||||
Read: func(key string) ([]byte, error) {
|
||||
e, err := stdio.State(PlacementRead).Get(key)
|
||||
if err != nil || e == nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.Value, nil
|
||||
},
|
||||
}
|
||||
|
||||
// readWant is one wanted account of the grants part, read afresh (issue-account).
|
||||
func readWant(user string) ([]byte, bool, error) {
|
||||
e, err := stdio.State(WantedRead).Get(user)
|
||||
if err != nil || e == nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return e.Value, true, nil
|
||||
}
|
||||
|
||||
// readLease is the controller's lease key, read through the runtime under the vault's own account (its manifest reads it).
|
||||
func readLease() ([]byte, bool, error) {
|
||||
e, err := stdio.State(LeaseRead).Get(LeaseKey)
|
||||
if err != nil || e == nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return e.Value, true, nil
|
||||
}
|
||||
|
||||
// watchLease turns minting on and off as the controller's lease is written (lease.go, ADR 0561 §3): a watch, never a
|
||||
// poll; and the lease is read afresh before every mint. It answers whether the watch began.
|
||||
func watchLease(env *Env) bool {
|
||||
if env.Vault == nil {
|
||||
return true
|
||||
}
|
||||
env.Vault.LeaseHeard(readLease())
|
||||
err := stdio.State(LeaseRead).Watch(LeaseKey, func(c stdio.StateChange) error {
|
||||
deleted := c.Op == "delete" || c.Op == "purge" || len(c.Value) == 0
|
||||
env.Vault.LeaseHeard(c.Value, !deleted, nil)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
say("the controller's lease is not watched, so minting stays as last read; begun again at the next pass: %v", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// adoptOperatorKey takes the operator key from the export the mesh writes to the vault's root (what
|
||||
// `operator key set` recorded; the controller keeps only a public half there), and seals again any
|
||||
// operator copy not yet sealed to the current key. Run every minute, so a failure partway is finished. It
|
||||
@@ -217,26 +148,14 @@ func main() {
|
||||
written, failed := env.Vault.HashHeld()
|
||||
say("hashed %d bus accounts at start; %d could not be, tried again every minute: %v", len(written), len(failed), failed)
|
||||
}
|
||||
// The operator key first: nothing is kept without it, so a want heard before it would not be minted.
|
||||
env.pass()
|
||||
watched := watchHandings(env)
|
||||
lease := watchLease(env)
|
||||
wants := watchWants(env)
|
||||
for {
|
||||
if !watched {
|
||||
watched = watchHandings(env)
|
||||
}
|
||||
if !lease {
|
||||
lease = watchLease(env)
|
||||
}
|
||||
if !wants {
|
||||
wants = watchWants(env)
|
||||
}
|
||||
env.pass()
|
||||
if env.Vault != nil {
|
||||
env.Vault.HashOwed()
|
||||
env.Vault.MintOwed()
|
||||
env.Vault.RetirePass()
|
||||
}
|
||||
env.pairPass()
|
||||
if env.Custody != nil {
|
||||
|
||||
@@ -1,376 +0,0 @@
|
||||
package main
|
||||
|
||||
// The vault mints every new bus account (novox/hq ADR 0561 §1 to §4, issue 774).
|
||||
//
|
||||
// The grants part says which accounts the mesh wants, in its state `accounts` that the vault alone reads (§2): one key
|
||||
// per bus user, holding what the controller's credential carries beside the password. The vault watches it, and for a
|
||||
// user it holds no value for and whose hash it did not adopt it makes a password, keeps the account whole (built by
|
||||
// mesh-sdk's HeldCredential, byte for byte the controller's shape) as the module's own secret on that node
|
||||
// (`own/<node>/<module>/<broker|broker-<process>|provisioner-broker>`), sealed, with the operator's copy; writes the
|
||||
// bcrypt hash into `bus-accounts`; and says `account-issued`, then `secret-changed`. The node's engine asks `own` again
|
||||
// on `secret-changed` and composes.
|
||||
//
|
||||
// **One issuer at every moment** (§3): minting is on only while the controller's lease is handed to a holder that is no
|
||||
// controller (mesh-sdk LeaseHandedAway). Until then the controller mints, the vault keeps hashing what it is handed, and
|
||||
// vault_status names every wanted account it holds back and why. **Nothing is minted anew** (§4): an account whose value
|
||||
// the vault holds, or whose hash it adopted, is never minted; the node accounts and the controller's own are never wanted.
|
||||
// **Never two values for one user** (§7): a user whose hash the vault already wrote from another of its secrets is held
|
||||
// back, said, and never replaced but by a rotation.
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
)
|
||||
|
||||
// WantedRead is the grants part's state of wanted accounts, as the vault's manifest reads it.
|
||||
const WantedRead = buscompose.GrantsAccounts
|
||||
|
||||
// LeaseRead is the controller's lease, as the vault's manifest reads it, and LeaseKey its one key.
|
||||
const (
|
||||
LeaseRead = buscompose.ControllerLease
|
||||
LeaseKey = buscompose.ControllerLeaseKey
|
||||
)
|
||||
|
||||
// AccountIssuedEvent is what the vault says when it minted an account (mesh-sdk buscompose.AccountIssued).
|
||||
const AccountIssuedEvent = buscompose.AccountIssuedEvent
|
||||
|
||||
// minting is the switch and what the vault knows of the wanted accounts, kept in memory and read again from the bus at
|
||||
// every start: the grants part's state and the lease are the record, never this.
|
||||
type minting struct {
|
||||
mu sync.Mutex
|
||||
on bool
|
||||
why string
|
||||
since time.Time
|
||||
// wants are the wanted accounts, by user, as last heard.
|
||||
wants map[string]buscompose.WantedAccount
|
||||
// heldBack are the wanted users the vault did not mint, with why (vault_status).
|
||||
heldBack map[string]string
|
||||
// owed are the wanted users whose mint failed, tried again at the pass (MintOwed).
|
||||
owed map[string]bool
|
||||
// minted are the accounts this vault minted since it started, by user (vault_status).
|
||||
minted map[string]Minted
|
||||
// loadedRevision is the highest revision of the user list the bus node said it loaded (rotation.go).
|
||||
loadedRevision uint64
|
||||
// retiring is what the passes keep of the accounts no longer wanted (retire.go).
|
||||
retiring retiring
|
||||
// mintMu keeps one mint at a time: a want heard, the switch turning on, and issue-account.
|
||||
mintMu sync.Mutex
|
||||
}
|
||||
|
||||
// Minted is what one mint did: the account and its secret, never its value or its hash.
|
||||
type Minted struct {
|
||||
User string `json:"user"`
|
||||
Secret string `json:"secret"`
|
||||
Generation int `json:"generation"`
|
||||
At time.Time `json:"at"`
|
||||
// Hashed says its hash is written; false leaves it owed, written at the next pass.
|
||||
Hashed bool `json:"hashed"`
|
||||
}
|
||||
|
||||
// MintState is what vault_status says of minting.
|
||||
type MintState struct {
|
||||
On bool `json:"on"`
|
||||
Why string `json:"why"`
|
||||
Since time.Time `json:"since,omitzero"`
|
||||
Wanted int `json:"wanted"`
|
||||
HeldBack map[string]string `json:"held_back"`
|
||||
Minted []Minted `json:"minted"`
|
||||
}
|
||||
|
||||
// MintState answers how minting stands.
|
||||
func (v *Vault) MintState() MintState {
|
||||
m := &v.minting
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
out := MintState{On: m.on, Why: m.why, Since: m.since, Wanted: len(m.wants), HeldBack: map[string]string{}, Minted: []Minted{}}
|
||||
if out.Why == "" {
|
||||
out.Why = "the controller's lease has not been read yet, so nothing is minted"
|
||||
}
|
||||
for u, why := range m.heldBack {
|
||||
out.HeldBack[u] = why
|
||||
}
|
||||
for _, x := range m.minted {
|
||||
out.Minted = append(out.Minted, x)
|
||||
}
|
||||
sort.Slice(out.Minted, func(i, j int) bool { return out.Minted[i].User < out.Minted[j].User })
|
||||
return out
|
||||
}
|
||||
|
||||
// mintingOn says whether the vault mints now, and why.
|
||||
func (v *Vault) mintingOn() (bool, string) {
|
||||
v.minting.mu.Lock()
|
||||
defer v.minting.mu.Unlock()
|
||||
if v.minting.why == "" {
|
||||
return false, "the controller's lease has not been read yet, so nothing is minted"
|
||||
}
|
||||
return v.minting.on, v.minting.why
|
||||
}
|
||||
|
||||
func (v *Vault) holdBack(user, why string) {
|
||||
m := &v.minting
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.heldBack == nil {
|
||||
m.heldBack = map[string]string{}
|
||||
}
|
||||
m.heldBack[user] = why
|
||||
}
|
||||
|
||||
func (v *Vault) noLongerHeld(user string) {
|
||||
m := &v.minting
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.heldBack, user)
|
||||
}
|
||||
|
||||
// AccountSecret is the vault's name of the own secret a wanted account is held in.
|
||||
func AccountSecret(w buscompose.WantedAccount) (string, error) {
|
||||
return OwnSecret(w.Node, w.Module, w.OwnSecret())
|
||||
}
|
||||
|
||||
// WantHeard is the watch of the grants part's wanted accounts: a key put is a want, minted when minting is on; a key
|
||||
// deleted is a want gone (retire.go).
|
||||
func (v *Vault) WantHeard(user string, raw []byte, deleted bool) {
|
||||
if deleted {
|
||||
v.wantGone(user)
|
||||
return
|
||||
}
|
||||
w, err := buscompose.ReadWantedAccount(user, raw)
|
||||
if err != nil {
|
||||
v.log("the wanted account %s is not read, so nothing is minted for it: %v", user, err)
|
||||
v.holdBack(user, "its wanted key is not readable: "+err.Error())
|
||||
return
|
||||
}
|
||||
v.minting.mu.Lock()
|
||||
if v.minting.wants == nil {
|
||||
v.minting.wants = map[string]buscompose.WantedAccount{}
|
||||
}
|
||||
v.minting.wants[user] = w
|
||||
v.minting.mu.Unlock()
|
||||
v.wantBack(user)
|
||||
if _, err := v.mintWanted(w, "the grants part's want of "+user); err != nil {
|
||||
v.log("%s is not minted: %v", user, err)
|
||||
}
|
||||
}
|
||||
|
||||
// errHeldBack is a want the vault does not mint now, with why: not an error of the vault.
|
||||
type errHeldBack struct{ why string }
|
||||
|
||||
func (e errHeldBack) Error() string { return e.why }
|
||||
|
||||
// mintWanted mints w when minting is on, the vault holds no value for it and adopted no hash of it; otherwise it says
|
||||
// why not and, where that is a hold, keeps it for vault_status.
|
||||
func (v *Vault) mintWanted(w buscompose.WantedAccount, by string) (Minted, error) {
|
||||
m, err := v.mintOnce(w, by)
|
||||
var held errHeldBack
|
||||
v.owe(w.User, err != nil && !errors.As(err, &held) && w.Valid() == nil)
|
||||
return m, err
|
||||
}
|
||||
|
||||
// mintOnce is one try of mintWanted.
|
||||
func (v *Vault) mintOnce(w buscompose.WantedAccount, by string) (Minted, error) {
|
||||
v.minting.mintMu.Lock()
|
||||
defer v.minting.mintMu.Unlock()
|
||||
if err := w.Valid(); err != nil {
|
||||
v.holdBack(w.User, err.Error())
|
||||
return Minted{}, err
|
||||
}
|
||||
secret, err := AccountSecret(w)
|
||||
if err != nil {
|
||||
v.holdBack(w.User, err.Error())
|
||||
return Minted{}, err
|
||||
}
|
||||
if _, held, err := v.Store.Read(secret); err != nil {
|
||||
return Minted{}, err
|
||||
} else if held {
|
||||
// Its value is held (the controller's, handed in, or one the vault minted): never minted anew (§4).
|
||||
v.noLongerHeld(w.User)
|
||||
return Minted{}, errHeldBack{secret + " is held already: an account the vault holds a value for is never minted anew (ADR 0561 §4)"}
|
||||
}
|
||||
if v.Accounts == nil {
|
||||
v.holdBack(w.User, "the vault has no state of bus accounts to write the hash to")
|
||||
return Minted{}, errors.New("the vault has no state of bus accounts to write the hash to")
|
||||
}
|
||||
was, err := v.Accounts.Get(w.User)
|
||||
if err != nil {
|
||||
v.holdBack(w.User, "its key in "+BusAccountsState+" cannot be read: "+err.Error())
|
||||
return Minted{}, err
|
||||
}
|
||||
if was != nil && was.Adopted {
|
||||
// The hash the bus loaded is kept (§4): not minted, and not held back either.
|
||||
v.noLongerHeld(w.User)
|
||||
return Minted{}, errHeldBack{w.User + "'s hash was adopted from the list the bus loaded: it keeps it, and is never minted (ADR 0561 §4)"}
|
||||
}
|
||||
if was != nil && was.Of != "" && was.Of != secret {
|
||||
why := fmt.Sprintf("%s holds the hash of %s for %s, not of %s: a user has one value, so nothing is minted until "+
|
||||
"a person sees why (ADR 0561 §7)", BusAccountsState, was.Of, w.User, secret)
|
||||
v.holdBack(w.User, why)
|
||||
return Minted{}, errHeldBack{why}
|
||||
}
|
||||
v.leaseAfresh()
|
||||
if on, why := v.mintingOn(); !on {
|
||||
v.holdBack(w.User, "held back: "+why)
|
||||
return Minted{}, errHeldBack{why}
|
||||
}
|
||||
raw := make([]byte, 30)
|
||||
if _, err := rand.Read(raw); err != nil {
|
||||
return Minted{}, err
|
||||
}
|
||||
value, err := buscompose.HeldCredential(w, base64.RawURLEncoding.EncodeToString(raw))
|
||||
if err != nil {
|
||||
v.holdBack(w.User, err.Error())
|
||||
return Minted{}, err
|
||||
}
|
||||
c, err := v.keepLocked(secret, string(value), "the vault", "", OriginMade, keepOptions{neverReplace: true, quiet: true})
|
||||
if err != nil {
|
||||
v.holdBack(w.User, "its value could not be kept: "+err.Error())
|
||||
return Minted{}, err
|
||||
}
|
||||
out := Minted{User: w.User, Secret: secret, Generation: c.Generation, At: c.At}
|
||||
// The hash first, then account-issued, then secret-changed: the grants part composes the user before the node asks.
|
||||
if _, err := v.HashAccount(secret, false); err != nil {
|
||||
v.hashes.owe(secret, err.Error())
|
||||
v.log("%s is minted and kept, and ITS HASH IS NOT WRITTEN, written at the next pass: %v", w.User, err)
|
||||
} else {
|
||||
out.Hashed = true
|
||||
if v.Emit != nil {
|
||||
e := buscompose.AccountIssued{User: w.User, Kind: w.Kind, Node: w.Node, Module: w.Module, Process: w.Process,
|
||||
Generation: c.Generation, At: c.At}
|
||||
body := map[string]any{}
|
||||
b, _ := json.Marshal(e)
|
||||
_ = json.Unmarshal(b, &body)
|
||||
if err := v.Emit(AccountIssuedEvent, body); err != nil {
|
||||
v.log("%s is minted and hashed, but %s could not be said: %v", w.User, AccountIssuedEvent, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
v.announce(c)
|
||||
v.minting.mu.Lock()
|
||||
if v.minting.minted == nil {
|
||||
v.minting.minted = map[string]Minted{}
|
||||
}
|
||||
v.minting.minted[w.User] = out
|
||||
v.minting.mu.Unlock()
|
||||
v.noLongerHeld(w.User)
|
||||
v.log("minted the bus account %s as %s (generation %d), for %s", w.User, secret, c.Generation, by)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// MintOwed tries again, at the vault's minute pass, every wanted account whose mint failed (its value could not be kept,
|
||||
// its key read, its password made): a want heard is never lost to one refusal. A want held back is not owed: the switch
|
||||
// or the want changing judges it again.
|
||||
func (v *Vault) MintOwed() {
|
||||
v.minting.mu.Lock()
|
||||
var owed []string
|
||||
for u := range v.minting.owed {
|
||||
owed = append(owed, u)
|
||||
}
|
||||
v.minting.mu.Unlock()
|
||||
sort.Strings(owed)
|
||||
for _, u := range owed {
|
||||
if w, ok := v.wanted(u); ok {
|
||||
if _, err := v.mintWanted(w, "the pass, after a mint that failed"); err != nil {
|
||||
v.log("%s is still not minted: %v", u, err)
|
||||
}
|
||||
} else {
|
||||
v.owe(u, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// owe marks user's mint as failed (tried again at the pass) or not.
|
||||
func (v *Vault) owe(user string, owed bool) {
|
||||
v.minting.mu.Lock()
|
||||
defer v.minting.mu.Unlock()
|
||||
if !owed {
|
||||
delete(v.minting.owed, user)
|
||||
return
|
||||
}
|
||||
if v.minting.owed == nil {
|
||||
v.minting.owed = map[string]bool{}
|
||||
}
|
||||
v.minting.owed[user] = true
|
||||
}
|
||||
|
||||
// heldBackOrMinted is a mint's answer as a tool gives it.
|
||||
func heldBackOrMinted(m Minted, err error) (any, error) {
|
||||
var held errHeldBack
|
||||
if errors.As(err, &held) {
|
||||
return map[string]any{"minted": false, "why": held.why}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{"minted": true, "account": m}, nil
|
||||
}
|
||||
|
||||
// IssueAccount is the seat's verb issue-account (ADR 0561 §9): one wanted user minted now, its key read afresh from the
|
||||
// grants part's state. The recovery when a want was not heard: it mints nothing a want would not, and is refused while
|
||||
// minting is off, for a node's account and the controller's (never minted here, §4), and for a user nobody wants.
|
||||
func (v *Vault) IssueAccount(caller, user string) (any, error) {
|
||||
user = strings.TrimSpace(user)
|
||||
if user == "" {
|
||||
return nil, errors.New("issue-account names a bus user (`user`), e.g. ace.mesh-build-go")
|
||||
}
|
||||
if nodeOrController(user) {
|
||||
return nil, fmt.Errorf("%s is never minted by the vault: a node's account and the controller's keep the hash they "+
|
||||
"have (ADR 0561 §4)", user)
|
||||
}
|
||||
v.leaseAfresh()
|
||||
if on, why := v.mintingOn(); !on {
|
||||
return nil, fmt.Errorf("not minted: %s", why)
|
||||
}
|
||||
raw, found, err := []byte(nil), false, error(nil)
|
||||
if v.WantRead != nil {
|
||||
raw, found, err = v.WantRead(user)
|
||||
} else if w, ok := v.wanted(user); ok {
|
||||
raw, err = json.Marshal(w)
|
||||
found = err == nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the wanted account %s could not be read: %w", user, err)
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("the grants part holds no wanted account %s: the vault mints only what placement composes "+
|
||||
"(grants_accounts lists them)", user)
|
||||
}
|
||||
w, err := buscompose.ReadWantedAccount(user, raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v.log("issue-account %s, asked by %s", user, someoneOr("", caller))
|
||||
return heldBackOrMinted(v.mintWanted(w, "issue-account by "+someoneOr("", caller)))
|
||||
}
|
||||
|
||||
// wantedUsers is every wanted user heard, sorted.
|
||||
func (v *Vault) wantedUsers() []string {
|
||||
v.minting.mu.Lock()
|
||||
defer v.minting.mu.Unlock()
|
||||
out := make([]string, 0, len(v.minting.wants))
|
||||
for u := range v.minting.wants {
|
||||
out = append(out, u)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func (v *Vault) wanted(user string) (buscompose.WantedAccount, bool) {
|
||||
v.minting.mu.Lock()
|
||||
defer v.minting.mu.Unlock()
|
||||
w, ok := v.minting.wants[user]
|
||||
return w, ok
|
||||
}
|
||||
|
||||
// nodeOrController says user is a node's account or the controller's: never minted here (ADR 0561 §4, §9).
|
||||
func nodeOrController(user string) bool {
|
||||
return user == ControllerCaller || strings.HasPrefix(user, "node.")
|
||||
}
|
||||
@@ -1,422 +0,0 @@
|
||||
//go:build integration
|
||||
|
||||
package main
|
||||
|
||||
// The vault whole against a real bus (novox/hq ADR 0561 §1 to §4, issue 774's acceptance 12): the built binary, played
|
||||
// over its real stdin and stdout as the node's tool runtime plays it (tools/call with the caller in _meta; mesh/state.*
|
||||
// and mesh/publish answered from a real nats-server's key-value buckets and subjects), its store in a directory of its
|
||||
// own. A want written to the grants part's state is minted only once the controller's lease is handed away: the hash is
|
||||
// in bus-accounts, own answers the value sealed to the node, the value is the controller's shape and its hash's
|
||||
// password, account-issued is said; an adopted user is never minted, and a lease handed back mints nothing.
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
"git.novox.be/novox/mesh-sdk/go/testbus"
|
||||
)
|
||||
|
||||
// wire is one line between the runtime and the bundle (the SDK's stdio message).
|
||||
type wire struct {
|
||||
ID json.RawMessage `json:"id,omitempty"`
|
||||
Method string `json:"method,omitempty"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
Error *struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// player is the node's tool runtime as far as the vault needs it, over a real bus.
|
||||
type player struct {
|
||||
t *testing.T
|
||||
ctx context.Context
|
||||
nc *nats.Conn
|
||||
js jetstream.JetStream
|
||||
|
||||
mu sync.Mutex
|
||||
in io.Writer
|
||||
next int
|
||||
pending map[string]chan wire
|
||||
said []string
|
||||
}
|
||||
|
||||
// bucketOf is the bucket a state names: the vault's own by its local name, another's as `<owner>.<name>`.
|
||||
func bucketOf(state string) string {
|
||||
if owner, name, ok := strings.Cut(state, "."); ok {
|
||||
return owner + "_" + name
|
||||
}
|
||||
return "mesh-vault_" + state
|
||||
}
|
||||
|
||||
func (p *player) send(m map[string]any) {
|
||||
raw, _ := json.Marshal(m)
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
_, _ = p.in.Write(append(raw, '\n'))
|
||||
}
|
||||
|
||||
func (p *player) answer(id json.RawMessage, result any, err error) {
|
||||
if err != nil {
|
||||
p.send(map[string]any{"id": id, "error": map[string]any{"code": -32000, "message": err.Error()}})
|
||||
return
|
||||
}
|
||||
p.send(map[string]any{"id": id, "result": result})
|
||||
}
|
||||
|
||||
func (p *player) kv(state string) (jetstream.KeyValue, error) {
|
||||
return p.js.KeyValue(p.ctx, bucketOf(state))
|
||||
}
|
||||
|
||||
// serve answers what the bundle asks, and hands the answers to what the test asked it.
|
||||
func (p *player) serve(out io.Reader) {
|
||||
scanner := bufio.NewScanner(out)
|
||||
scanner.Buffer(make([]byte, 1<<20), 1<<24)
|
||||
for scanner.Scan() {
|
||||
var m wire
|
||||
if json.Unmarshal(scanner.Bytes(), &m) != nil {
|
||||
continue
|
||||
}
|
||||
if m.Method == "" {
|
||||
var key string
|
||||
_ = json.Unmarshal(m.ID, &key)
|
||||
p.mu.Lock()
|
||||
ch := p.pending[key]
|
||||
delete(p.pending, key)
|
||||
p.mu.Unlock()
|
||||
if ch != nil {
|
||||
ch <- m
|
||||
}
|
||||
continue
|
||||
}
|
||||
go p.handle(m)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *player) handle(m wire) {
|
||||
var a struct {
|
||||
State string `json:"state"`
|
||||
Key string `json:"key"`
|
||||
Value json.RawMessage `json:"value"`
|
||||
Body json.RawMessage `json:"body"`
|
||||
}
|
||||
_ = json.Unmarshal(m.Params, &a)
|
||||
switch m.Method {
|
||||
case "mesh/state.get":
|
||||
kv, err := p.kv(a.State)
|
||||
if err != nil {
|
||||
p.answer(m.ID, nil, err)
|
||||
return
|
||||
}
|
||||
e, err := kv.Get(p.ctx, a.Key)
|
||||
if errors.Is(err, jetstream.ErrKeyNotFound) {
|
||||
p.answer(m.ID, nil, nil)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
p.answer(m.ID, nil, err)
|
||||
return
|
||||
}
|
||||
p.answer(m.ID, map[string]any{"key": e.Key(), "value": json.RawMessage(e.Value()), "revision": e.Revision()}, nil)
|
||||
case "mesh/state.put":
|
||||
kv, err := p.kv(a.State)
|
||||
if err == nil {
|
||||
var rev uint64
|
||||
rev, err = kv.Put(p.ctx, a.Key, a.Value)
|
||||
if err == nil {
|
||||
p.answer(m.ID, map[string]any{"revision": rev}, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
p.answer(m.ID, nil, err)
|
||||
case "mesh/state.keys":
|
||||
kv, err := p.kv(a.State)
|
||||
if err != nil {
|
||||
p.answer(m.ID, nil, err)
|
||||
return
|
||||
}
|
||||
keys, err := kv.Keys(p.ctx)
|
||||
if errors.Is(err, jetstream.ErrNoKeysFound) {
|
||||
keys, err = []string{}, nil
|
||||
}
|
||||
p.answer(m.ID, keys, err)
|
||||
case "mesh/state.delete":
|
||||
kv, err := p.kv(a.State)
|
||||
if err == nil {
|
||||
err = kv.Delete(p.ctx, a.Key)
|
||||
}
|
||||
p.answer(m.ID, map[string]any{}, err)
|
||||
case "mesh/state.watch":
|
||||
kv, err := p.kv(a.State)
|
||||
if err != nil {
|
||||
p.answer(m.ID, nil, err)
|
||||
return
|
||||
}
|
||||
key := strings.ReplaceAll(a.Key, "**", ">")
|
||||
if key == "" {
|
||||
key = ">"
|
||||
}
|
||||
w, err := kv.Watch(p.ctx, key)
|
||||
if err != nil {
|
||||
p.answer(m.ID, nil, err)
|
||||
return
|
||||
}
|
||||
var watch string
|
||||
_ = json.Unmarshal(m.ID, &watch)
|
||||
current := true
|
||||
for e := range w.Updates() {
|
||||
if e == nil {
|
||||
current = false
|
||||
p.answer(m.ID, map[string]any{}, nil)
|
||||
continue
|
||||
}
|
||||
op := "put"
|
||||
if e.Operation() != jetstream.KeyValuePut {
|
||||
op = "delete"
|
||||
}
|
||||
change := map[string]any{"state": a.State, "key": e.Key(), "op": op, "revision": e.Revision(), "current": current}
|
||||
if op == "put" {
|
||||
change["value"] = json.RawMessage(e.Value())
|
||||
}
|
||||
p.mu.Lock()
|
||||
p.next++
|
||||
id := fmt.Sprintf("w-%d", p.next)
|
||||
p.mu.Unlock()
|
||||
p.send(map[string]any{"id": id, "method": "mesh/state", "params": map[string]any{"watch": watch, "change": change}})
|
||||
}
|
||||
case "mesh/publish":
|
||||
var e struct {
|
||||
Key string `json:"key"`
|
||||
Body json.RawMessage `json:"body"`
|
||||
}
|
||||
_ = json.Unmarshal(m.Params, &e)
|
||||
p.mu.Lock()
|
||||
p.said = append(p.said, e.Key+" "+string(e.Body))
|
||||
p.mu.Unlock()
|
||||
p.answer(m.ID, map[string]any{}, p.nc.Publish("mesh.mod.mesh-vault.event."+e.Key, e.Body))
|
||||
default:
|
||||
// mesh/ask and the rest: nothing answers on this bus, as a call with no responder.
|
||||
p.answer(m.ID, nil, errors.New("nats: no responders available for request"))
|
||||
}
|
||||
}
|
||||
|
||||
// call is a tools/call of the vault as caller, answered as the tool's JSON.
|
||||
func (p *player) call(name, caller string, args map[string]any) (json.RawMessage, error) {
|
||||
p.mu.Lock()
|
||||
p.next++
|
||||
id := fmt.Sprintf("c-%d", p.next)
|
||||
ch := make(chan wire, 1)
|
||||
p.pending[id] = ch
|
||||
p.mu.Unlock()
|
||||
p.send(map[string]any{"id": id, "method": "tools/call", "params": map[string]any{"name": name, "arguments": args,
|
||||
"_meta": map[string]any{"mesh/caller": caller}}})
|
||||
select {
|
||||
case m := <-ch:
|
||||
var r struct {
|
||||
Content []struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
IsError bool `json:"isError"`
|
||||
}
|
||||
if m.Error != nil {
|
||||
return nil, errors.New(m.Error.Message)
|
||||
}
|
||||
if err := json.Unmarshal(m.Result, &r); err != nil || len(r.Content) != 1 {
|
||||
return nil, fmt.Errorf("%s answered %s", name, m.Result)
|
||||
}
|
||||
if r.IsError {
|
||||
return nil, errors.New(r.Content[0].Text)
|
||||
}
|
||||
return json.RawMessage(r.Content[0].Text), nil
|
||||
case <-time.After(30 * time.Second):
|
||||
return nil, fmt.Errorf("%s did not answer in 30s", name)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *player) put(t *testing.T, state, key string, v any) {
|
||||
t.Helper()
|
||||
kv, err := p.kv(state)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, _ := json.Marshal(v)
|
||||
if _, err := kv.Put(p.ctx, key, raw); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *player) hashOf(t *testing.T, user string) *AccountHash {
|
||||
t.Helper()
|
||||
kv, err := p.kv(BusAccountsState)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e, err := kv.Get(p.ctx, user)
|
||||
if errors.Is(err, jetstream.ErrKeyNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var a AccountHash
|
||||
if err := json.Unmarshal(e.Value(), &a); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &a
|
||||
}
|
||||
|
||||
func eventually(t *testing.T, what string, ok func() bool) {
|
||||
t.Helper()
|
||||
for end := time.Now().Add(30 * time.Second); time.Now().Before(end); time.Sleep(50 * time.Millisecond) {
|
||||
if ok() {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("never: %s", what)
|
||||
}
|
||||
|
||||
// 774/AC12.
|
||||
func TestTheVaultWholeMintsOnTheBusOnlyFromTheLeaseWrite(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
dir := t.TempDir()
|
||||
bin := filepath.Join(dir, "mesh-vault")
|
||||
build := exec.Command("go", "build", "-o", bin, ".")
|
||||
build.Stderr = os.Stderr
|
||||
if err := build.Run(); err != nil {
|
||||
t.Fatalf("the vault does not build: %v", err)
|
||||
}
|
||||
nc, err := nats.Connect(testbus.URL(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(nc.Close)
|
||||
js, err := jetstream.New(nc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, b := range []string{"mesh-vault_bus-accounts", "mesh-grants_accounts", "mesh-controller_lease",
|
||||
"mesh-controller_assignments", "node-engine_have", "mesh-delivery-indexer_placement"} {
|
||||
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 2}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// The operator key, as the mesh exports it to the vault's root.
|
||||
opPub, _, err := box.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root := filepath.Join(dir, "root")
|
||||
_ = os.MkdirAll(root, 0o700)
|
||||
export, _ := json.Marshal(map[string]string{"operator-key": base64.StdEncoding.EncodeToString(opPub[:])})
|
||||
if err := os.WriteFile(filepath.Join(root, "export.json"), export, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, bin)
|
||||
cmd.Env = append(os.Environ(), "MESH_VAULT_STORE="+filepath.Join(dir, "store"), "MESH_VAULT_STATE="+filepath.Join(dir, "state"),
|
||||
"MESH_VAULT_ROOT="+root, "MESH_VAULT_LEDGER="+filepath.Join(dir, "ledger"), "MESH_RECEIVES="+filepath.Join(dir, "mesh.json"),
|
||||
"MESH_NODE=novox", "MESH_MODULE=mesh-vault")
|
||||
stdin, _ := cmd.StdinPipe()
|
||||
stdout, _ := cmd.StdoutPipe()
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = stdin.Close(); cancel(); _ = cmd.Wait() })
|
||||
p := &player{t: t, ctx: ctx, nc: nc, js: js, in: stdin, pending: map[string]chan wire{}}
|
||||
go p.serve(stdout)
|
||||
|
||||
status := func() MintState {
|
||||
raw, err := p.call("vault_status", "person.jochen", nil)
|
||||
if err != nil {
|
||||
return MintState{}
|
||||
}
|
||||
var s struct {
|
||||
Minting MintState `json:"minting"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &s)
|
||||
return s.Minting
|
||||
}
|
||||
// The controller holds the lease: a want is held back, nothing is minted.
|
||||
p.put(t, "mesh-controller.lease", LeaseKey, map[string]any{"instance": "novox/1/2026-10-27", "epoch": 57})
|
||||
x, y, z := aWant("mod-x"), aWant("mod-y"), aWant("mod-z")
|
||||
p.put(t, "mesh-vault.bus-accounts", y.User, AccountHash{User: y.User, Hash: "$2a$04$adoptedadoptedadoptedadoptedadoptedadoptedadopte", Adopted: true})
|
||||
p.put(t, "mesh-grants.accounts", x.User, x)
|
||||
p.put(t, "mesh-grants.accounts", y.User, y)
|
||||
eventually(t, "the want held back under the controller's lease", func() bool {
|
||||
return strings.Contains(status().HeldBack[x.User], "controller holds the lease")
|
||||
})
|
||||
if p.hashOf(t, x.User) != nil {
|
||||
t.Fatal("minted under the controller's lease")
|
||||
}
|
||||
// The write that hands the lease away mints it.
|
||||
p.put(t, "mesh-controller.lease", LeaseKey, map[string]any{"handed-to": "the operator's hand-over"})
|
||||
eventually(t, "the account minted at the lease write", func() bool { return p.hashOf(t, x.User) != nil })
|
||||
nodePub, nodePriv := aSealingKey(t)
|
||||
if _, err := p.call(Seat+".node-key", "node.ace", map[string]any{"key": nodePub}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, err := p.call(Seat+".own", "node.ace", map[string]any{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var own OwnAnswer
|
||||
if err := json.Unmarshal(raw, &own); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(own.Own) != 1 || own.Own[0].Module != "mod-x" || own.Own[0].Name != "broker" || own.Own[0].Origin != OriginMade {
|
||||
t.Fatalf("own answers %+v", own.Own)
|
||||
}
|
||||
value, err := seal.Open(own.Own[0].Sealed, nodePriv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var held struct{ Password string }
|
||||
_ = json.Unmarshal([]byte(value), &held)
|
||||
shape, _ := buscompose.HeldCredential(x, held.Password)
|
||||
if value != string(shape) {
|
||||
t.Errorf("the minted value %q is not the controller's shape %q", value, shape)
|
||||
}
|
||||
if h := p.hashOf(t, x.User); bcrypt.CompareHashAndPassword([]byte(h.Hash), []byte(held.Password)) != nil || h.Adopted || h.Generation != 1 {
|
||||
t.Errorf("bus-accounts %s holds %+v, not the minted password's hash", x.User, h)
|
||||
}
|
||||
if h := p.hashOf(t, y.User); h == nil || !h.Adopted {
|
||||
t.Errorf("the adopted %s was replaced: %+v", y.User, h)
|
||||
}
|
||||
p.mu.Lock()
|
||||
said := strings.Join(p.said, "\n")
|
||||
p.mu.Unlock()
|
||||
if !strings.Contains(said, AccountIssuedEvent+` {"`) || !strings.Contains(said, `"user":"ace.mod-x"`) || strings.Contains(said, held.Password) {
|
||||
t.Errorf("the vault said: %s", said)
|
||||
}
|
||||
// Handed back to a controller: a new want mints nothing.
|
||||
p.put(t, "mesh-controller.lease", LeaseKey, map[string]any{"instance": "novox/2/2026-10-28", "epoch": 58})
|
||||
p.put(t, "mesh-grants.accounts", z.User, z)
|
||||
eventually(t, "the new want held back once the lease is the controller's again", func() bool {
|
||||
return strings.Contains(status().HeldBack[z.User], "controller holds the lease")
|
||||
})
|
||||
if p.hashOf(t, z.User) != nil {
|
||||
t.Error("minted after the lease was handed back")
|
||||
}
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
)
|
||||
|
||||
// aWant is a wanted account of a module on ace, as the grants part writes it.
|
||||
func aWant(module string) buscompose.WantedAccount {
|
||||
return buscompose.WantedAccount{User: "ace." + module, Kind: buscompose.KindModule, Node: "ace", Module: module,
|
||||
URL: "nats://novox.mesh:4222", Fingerprint: "ab:cd", Claims: []buscompose.ClaimedSeat{{Seat: "s", Scope: "node"}}}
|
||||
}
|
||||
|
||||
func wantRaw(t *testing.T, w buscompose.WantedAccount) []byte {
|
||||
t.Helper()
|
||||
raw, err := json.Marshal(w)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// mintingVault is a vault with a state of bus accounts, ace's sealing key stated, ace's modules assigned, and minting
|
||||
// turned on or off as on says.
|
||||
func mintingVault(t *testing.T, on bool) (testVault, *fakeAccounts, string) {
|
||||
t.Helper()
|
||||
plans := assignments{"ace.mod-x": `{"node":"ace","module":"mod-x"}`, "ace.mod-y": `{"node":"ace","module":"mod-y"}`}
|
||||
v := newVault(t, nil)
|
||||
v.Askers = Declared{Read: plans.read, Store: v.Store}
|
||||
accounts := &fakeAccounts{}
|
||||
v.Accounts = accounts
|
||||
pub, priv := aSealingKey(t)
|
||||
if err := v.recordNodeKey("ace", pub, "node.ace"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if on {
|
||||
v.LeaseHeard([]byte(`{"handed-to":"the operator's hand-over"}`), true, nil)
|
||||
} else {
|
||||
v.LeaseHeard([]byte(`{"instance":"novox/1/2026-10-27","epoch":57}`), true, nil)
|
||||
}
|
||||
return v, accounts, priv
|
||||
}
|
||||
|
||||
// 774/AC7 (novox/hq ADR 0561 §1, §2, §4): a want with no value, minting on, is minted once: kept whole in the controller's
|
||||
// shape as the module's broker, its hash in bus-accounts, said account-issued before secret-changed, and answered by own
|
||||
// sealed to the node; a value held or a hash adopted is never minted.
|
||||
func TestTheVaultMintsAWantedAccountOnceAndNothingItHolds(t *testing.T) {
|
||||
v, accounts, nodePriv := mintingVault(t, true)
|
||||
w := aWant("mod-x")
|
||||
before := len(v.bus.events)
|
||||
v.WantHeard(w.User, wantRaw(t, w), false)
|
||||
rec, held, err := v.Store.Read("own/ace/mod-x/broker")
|
||||
if err != nil || !held || rec.Generation != 1 || rec.Origin != OriginMade {
|
||||
t.Fatalf("the minted account is kept as %+v (held %v, %v)", rec, held, err)
|
||||
}
|
||||
value, err := seal.Open(rec.AtRest, v.Own.PrivateKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got struct{ Password string }
|
||||
_ = json.Unmarshal([]byte(value), &got)
|
||||
shape, _ := buscompose.HeldCredential(w, got.Password)
|
||||
if len(got.Password) != 40 || value != string(shape) {
|
||||
t.Errorf("the minted value is %q, not the controller's shape %q", value, shape)
|
||||
}
|
||||
if !accounts.hashOf(w.User, got.Password, 1) {
|
||||
t.Errorf("%s's hash is not the minted password's: %+v", w.User, accounts.keys[w.User])
|
||||
}
|
||||
var order []string
|
||||
for _, e := range v.bus.events[before:] {
|
||||
order = append(order, e["event"].(string))
|
||||
}
|
||||
if strings.Join(order, ",") != AccountHashedEvent+","+AccountIssuedEvent+","+ChangedEvent {
|
||||
t.Errorf("said %v, want the hash, account-issued, then secret-changed", order)
|
||||
}
|
||||
for _, raw := range v.bus.raw[before:] {
|
||||
if strings.Contains(raw, got.Password) || strings.Contains(raw, "$2a$") {
|
||||
t.Errorf("a value or a hash was said: %s", raw)
|
||||
}
|
||||
}
|
||||
own, err := v.OwnOfNode("node.ace")
|
||||
if err != nil || len(own.Own) != 1 {
|
||||
t.Fatalf("own answered %+v, %v", own, err)
|
||||
}
|
||||
if opened, err := seal.Open(own.Own[0].Sealed, nodePriv); err != nil || opened != value {
|
||||
t.Errorf("own's sealed value opens to %q, %v", opened, err)
|
||||
}
|
||||
|
||||
// Heard again, or for a value held, or for a hash adopted: nothing is minted, nothing is held back.
|
||||
v.WantHeard(w.User, wantRaw(t, w), false)
|
||||
if rec, _, _ := v.Store.Read("own/ace/mod-x/broker"); rec.Generation != 1 {
|
||||
t.Errorf("a want heard again minted generation %d", rec.Generation)
|
||||
}
|
||||
accounts.keys["ace.mod-y"] = AccountHash{User: "ace.mod-y", Hash: "$2a$04$adopted", Adopted: true}
|
||||
y := aWant("mod-y")
|
||||
v.WantHeard(y.User, wantRaw(t, y), false)
|
||||
if _, held, _ := v.Store.Read("own/ace/mod-y/broker"); held || accounts.keys["ace.mod-y"].Hash != "$2a$04$adopted" {
|
||||
t.Errorf("an adopted user was minted")
|
||||
}
|
||||
if st := v.MintState(); len(st.HeldBack) != 0 || len(st.Minted) != 1 {
|
||||
t.Errorf("vault_status says %+v: an adopted user is not held back, one account minted", st)
|
||||
}
|
||||
}
|
||||
|
||||
// 774/AC8 (novox/hq ADR 0561 §3): while the controller holds the lease nothing is minted and vault_status says why and
|
||||
// names each wanted account held back (never an adopted one); the write that hands the lease away mints them; a lease
|
||||
// handed back, expired or unreadable turns minting off again, read afresh before every mint.
|
||||
func TestNothingIsMintedUntilTheLeaseIsHandedAway(t *testing.T) {
|
||||
v, accounts, _ := mintingVault(t, false)
|
||||
accounts.keys = map[string]AccountHash{"ace.mod-y": {User: "ace.mod-y", Hash: "$2a$04$adopted", Adopted: true}}
|
||||
for _, m := range []string{"mod-x", "mod-y"} {
|
||||
w := aWant(m)
|
||||
v.WantHeard(w.User, wantRaw(t, w), false)
|
||||
}
|
||||
st := v.MintState()
|
||||
if st.On || !strings.Contains(st.Why, "controller holds the lease") || len(st.HeldBack) != 1 ||
|
||||
!strings.Contains(st.HeldBack["ace.mod-x"], "controller holds the lease") {
|
||||
t.Fatalf("under the controller's lease vault_status says %+v", st)
|
||||
}
|
||||
if _, held, _ := v.Store.Read("own/ace/mod-x/broker"); held {
|
||||
t.Fatal("minted while the controller holds the lease")
|
||||
}
|
||||
// Handed away: the wanted account is minted at that write, the adopted one is not.
|
||||
lease := []byte(`{"handed-to":"the operator's hand-over"}`)
|
||||
v.LeaseRead = func() ([]byte, bool, error) { return lease, true, nil }
|
||||
v.LeaseHeard(lease, true, nil)
|
||||
if _, held, _ := v.Store.Read("own/ace/mod-x/broker"); !held {
|
||||
t.Fatalf("not minted at the lease write: %+v", v.MintState())
|
||||
}
|
||||
if _, held, _ := v.Store.Read("own/ace/mod-y/broker"); held {
|
||||
t.Error("an adopted account was minted at the lease write")
|
||||
}
|
||||
if st := v.MintState(); !st.On || len(st.HeldBack) != 0 {
|
||||
t.Errorf("after the lease write vault_status says %+v", st)
|
||||
}
|
||||
// The key expired (no watch hears a key's age pass): read afresh before the next mint, nothing is minted.
|
||||
v.LeaseRead = func() ([]byte, bool, error) { return nil, false, nil }
|
||||
z := aWant("mod-z")
|
||||
v.WantHeard(z.User, wantRaw(t, z), false)
|
||||
if _, held, _ := v.Store.Read("own/ace/mod-z/broker"); held {
|
||||
t.Error("minted after the lease's key expired")
|
||||
}
|
||||
if st := v.MintState(); st.On || !strings.Contains(st.HeldBack["ace.mod-z"], "no lease is written") {
|
||||
t.Errorf("after the key expired vault_status says %+v", st)
|
||||
}
|
||||
}
|
||||
|
||||
// 774/AC10 issue-account (novox/hq ADR 0561 §9): the verb mints one wanted user now, read afresh; it is refused while
|
||||
// minting is off, for a node's account and the controller's, and for a user nobody wants; a user held already is said so.
|
||||
func TestIssueAccountMintsOneWantedUserNowAndRefusesTheRest(t *testing.T) {
|
||||
v, _, _ := mintingVault(t, false)
|
||||
w := aWant("mod-x")
|
||||
raw := wantRaw(t, w)
|
||||
v.WantRead = func(user string) ([]byte, bool, error) {
|
||||
if user == w.User {
|
||||
return raw, true, nil
|
||||
}
|
||||
return nil, false, nil
|
||||
}
|
||||
for user, says := range map[string]string{
|
||||
"ace.mod-x": "controller holds the lease",
|
||||
"node.ace": "never minted",
|
||||
"controller": "never minted",
|
||||
} {
|
||||
if _, err := v.IssueAccount("person.jochen", user); err == nil || !strings.Contains(err.Error(), says) {
|
||||
t.Errorf("%s: %v, want a refusal saying %q", user, err, says)
|
||||
}
|
||||
}
|
||||
v.LeaseHeard([]byte(`{"handed-to":"the operator's hand-over"}`), true, nil)
|
||||
if _, err := v.IssueAccount("person.jochen", "ace.nobody"); err == nil || !strings.Contains(err.Error(), "no wanted account") {
|
||||
t.Errorf("a user nobody wants: %v", err)
|
||||
}
|
||||
got, err := v.IssueAccount("person.jochen", w.User)
|
||||
if err != nil || got.(map[string]any)["minted"] != true {
|
||||
t.Fatalf("issue-account: %v, %v", got, err)
|
||||
}
|
||||
if again, err := v.IssueAccount("person.jochen", w.User); err != nil || again.(map[string]any)["minted"] != false ||
|
||||
!strings.Contains(again.(map[string]any)["why"].(string), "held already") {
|
||||
t.Errorf("issue-account again: %v, %v", again, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 774/AC10 the rotation's rule (novox/hq ADR 0561 §4): at most one rotation begins per revision of the loaded user list,
|
||||
// over the second login `<user>--b`, never of a node's account, the controller's or an adopted hash, and `rotate` of a
|
||||
// covered account stays refused, so nothing rotates in place.
|
||||
func TestOneRotationBeginsPerRevisionOfTheLoadedList(t *testing.T) {
|
||||
v, accounts, _ := mintingVault(t, true)
|
||||
for _, m := range []string{"mod-x", "mod-y"} {
|
||||
w := aWant(m)
|
||||
v.WantHeard(w.User, wantRaw(t, w), false)
|
||||
}
|
||||
if _, err := v.BeginRotation("ace.mod-x"); err == nil || !strings.Contains(err.Error(), "not known yet") {
|
||||
t.Errorf("began with no loaded revision known: %v", err)
|
||||
}
|
||||
v.LoadedListHeard(570)
|
||||
r, err := v.BeginRotation("ace.mod-x")
|
||||
if err != nil || r.Login != "ace.mod-x--b" || r.Revision != 570 || r.Generation != 2 || accounts.keys["ace.mod-x"].Rotating == nil {
|
||||
t.Fatalf("the first rotation: %+v, %v", r, err)
|
||||
}
|
||||
if _, err := v.BeginRotation("ace.mod-y"); err == nil || !strings.Contains(err.Error(), "one rotation begins per revision") {
|
||||
t.Errorf("a second rotation in one revision: %v", err)
|
||||
}
|
||||
v.LoadedListHeard(571)
|
||||
if _, err := v.BeginRotation("ace.mod-y"); err != nil {
|
||||
t.Errorf("the next revision's rotation: %v", err)
|
||||
}
|
||||
if _, err := v.BeginRotation("ace.mod-x"); err == nil || !strings.Contains(err.Error(), "rotating already") {
|
||||
t.Errorf("an account rotating began again: %v", err)
|
||||
}
|
||||
accounts.keys["node.ace"] = AccountHash{User: "node.ace", Hash: "$2a$04$n", Adopted: true}
|
||||
for _, u := range []string{"node.ace", "controller"} {
|
||||
if _, err := v.BeginRotation(u); err == nil {
|
||||
t.Errorf("%s began a rotation", u)
|
||||
}
|
||||
}
|
||||
if _, err := v.RotateFor(ControllerCaller, "own/ace/mod-x/broker", Narrow{}, "the test", "a rotation in place"); err == nil {
|
||||
t.Error("a covered account rotated in place")
|
||||
}
|
||||
}
|
||||
|
||||
// A mint that failed (here: no operator key yet, so nothing can be kept) is tried again at the pass, never lost.
|
||||
func TestAFailedMintIsTriedAgainAtThePass(t *testing.T) {
|
||||
v, accounts, _ := mintingVault(t, true)
|
||||
key, _ := v.Store.OperatorKey()
|
||||
if err := v.Store.setOperatorKey("", "the test", v.Now()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w := aWant("mod-x")
|
||||
v.WantHeard(w.User, wantRaw(t, w), false)
|
||||
if _, ok := accounts.keys[w.User]; ok {
|
||||
t.Fatal("minted with no operator key")
|
||||
}
|
||||
if err := v.Store.setOperatorKey(key, "the test", v.Now()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v.MintOwed()
|
||||
if _, ok := accounts.keys[w.User]; !ok {
|
||||
t.Errorf("not minted at the pass after the failure: %+v", v.MintState())
|
||||
}
|
||||
}
|
||||
@@ -84,14 +84,17 @@ func (v *Vault) credentialRefusal(node string, c CredentialAsked) string {
|
||||
if c.Consumer != node {
|
||||
return fmt.Sprintf("%s asks as a consumer on %s, and is not that machine", node, c.Consumer)
|
||||
}
|
||||
// The consuming module stands on the machine: read from the placement and the assignments, never assumed (one
|
||||
// that cannot be read refuses).
|
||||
stands, err := v.standsOn(node, c.ConsumerModule)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(notYetPrefix+"where %s stands could not be read: %v", c.ConsumerModule, err)
|
||||
// The consuming module stands on the machine: read, never assumed (an assignment that cannot be read refuses).
|
||||
read, why := v.assignmentReader(node)
|
||||
if why != "" {
|
||||
return why
|
||||
}
|
||||
if !stands {
|
||||
return fmt.Sprintf("%s is not on %s: neither placed nor assigned there, nor required by a module there", c.ConsumerModule, node)
|
||||
raw, err := read(node + "." + c.ConsumerModule)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(notYetPrefix+"the assignment %s.%s could not be read: %v", node, c.ConsumerModule, err)
|
||||
}
|
||||
if raw == nil {
|
||||
return fmt.Sprintf("%s is not on %s: neither assigned there nor required by a module there", c.ConsumerModule, node)
|
||||
}
|
||||
return ""
|
||||
case "provider":
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
package main
|
||||
|
||||
// Where modules stand, from the catalogue's placement (novox/hq ADR 0549 §3, §5; ADR 0561; issue 774). The controller's
|
||||
// assignments mirror goes with its lease, so the vault reads which module stands on which node from the indexer's
|
||||
// placement first, and the assignments beside it while both stand: a module either names stands. The provisioner's plan,
|
||||
// which only an assignment's value carries, is read from the assignments still: its home after the lease is a subtask of
|
||||
// its own (issue 774).
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// PlacementRead is the indexer's placement, as the vault's manifest reads it.
|
||||
const PlacementRead = delivery.IndexerModule + "." + delivery.PlacementState
|
||||
|
||||
// PlacementReader reads the indexer's placement: every key, and one key's value (nil when there is none).
|
||||
type PlacementReader struct {
|
||||
Keys func() ([]string, error)
|
||||
Read func(key string) ([]byte, error)
|
||||
}
|
||||
|
||||
// placements is every node's placement, by node; none when the vault reads no placement.
|
||||
func (v *Vault) placements() (map[string]delivery.Placement, error) {
|
||||
out := map[string]delivery.Placement{}
|
||||
if v.Placement.Keys == nil || v.Placement.Read == nil {
|
||||
return out, nil
|
||||
}
|
||||
keys, err := v.Placement.Keys()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the placement's keys could not be read: %w", err)
|
||||
}
|
||||
for _, k := range keys {
|
||||
node, err := delivery.ParsePlacementKey(k)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
raw, err := v.Placement.Read(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the placement of %s could not be read: %w", node, err)
|
||||
}
|
||||
if raw == nil {
|
||||
continue
|
||||
}
|
||||
p, err := delivery.DecodePlacement(raw)
|
||||
if err != nil || p.Node != node {
|
||||
continue
|
||||
}
|
||||
out[node] = p
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// placedOn says module is placed on node by the catalogue.
|
||||
func (v *Vault) placedOn(node, module string) (bool, error) {
|
||||
if v.Placement.Read == nil {
|
||||
return false, nil
|
||||
}
|
||||
key, err := delivery.PlacementKey(node)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
raw, err := v.Placement.Read(key)
|
||||
if err != nil || raw == nil {
|
||||
return false, err
|
||||
}
|
||||
p, err := delivery.DecodePlacement(raw)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("the placement of %s is not readable: %w", node, err)
|
||||
}
|
||||
return slices.Contains(p.Assigned(), module), nil
|
||||
}
|
||||
|
||||
// standsOn says module stands on node: placed there, or assigned there while the assignments stand. A read that fails is
|
||||
// an error, never "not there".
|
||||
func (v *Vault) standsOn(node, module string) (bool, error) {
|
||||
placed, err := v.placedOn(node, module)
|
||||
if err != nil || placed {
|
||||
return placed, err
|
||||
}
|
||||
d, ok := v.Askers.(Declared)
|
||||
if !ok || d.Read == nil {
|
||||
if v.Placement.Read == nil {
|
||||
return false, errors.New("the vault reads neither the placement nor the assignments")
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
raw, err := standing(d.Read)(node + "." + module)
|
||||
return raw != nil, err
|
||||
}
|
||||
|
||||
// busNode is the node running the bus's module: from the placement, and from the assignments while no placement names
|
||||
// one. The vault adopts from one node alone.
|
||||
func (v *Vault) busNode() (string, error) {
|
||||
placed, err := v.placements()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var nodes []string
|
||||
for node, p := range placed {
|
||||
if slices.Contains(p.Assigned(), BusModule) {
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
}
|
||||
from := "placed"
|
||||
if len(nodes) == 0 && v.Assignments.Keys != nil {
|
||||
from = "assigned"
|
||||
keys, err := v.Assignments.Keys()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("the assignments' keys could not be read: %w", err)
|
||||
}
|
||||
for _, k := range keys {
|
||||
if node, module, ok := strings.Cut(k, "."); ok && module == BusModule {
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
}
|
||||
} else if len(nodes) == 0 && len(placed) == 0 {
|
||||
return "", errors.New("the vault reads neither the placement nor the assignments, so it cannot tell which node holds the bus")
|
||||
}
|
||||
sort.Strings(nodes)
|
||||
switch len(nodes) {
|
||||
case 1:
|
||||
return nodes[0], nil
|
||||
case 0:
|
||||
return "", fmt.Errorf("no node is %s the bus module %s", from, BusModule)
|
||||
}
|
||||
return "", fmt.Errorf("%d nodes are %s the bus module %s (%s): the vault adopts from one alone", len(nodes), from,
|
||||
BusModule, strings.Join(nodes, ", "))
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// placementsOf is the indexer's placement of each node, as the vault reads it.
|
||||
func placementsOf(t *testing.T, nodes map[string][]string) PlacementReader {
|
||||
t.Helper()
|
||||
values := map[string][]byte{}
|
||||
for node, modules := range nodes {
|
||||
p := delivery.Placement{V: delivery.Version, Node: node}
|
||||
for _, m := range modules {
|
||||
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
|
||||
}
|
||||
raw, err := delivery.EncodePlacement(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
key, _ := delivery.PlacementKey(node)
|
||||
values[key] = raw
|
||||
}
|
||||
return PlacementReader{
|
||||
Keys: func() ([]string, error) {
|
||||
var out []string
|
||||
for k := range values {
|
||||
out = append(out, k)
|
||||
}
|
||||
return out, nil
|
||||
},
|
||||
Read: func(key string) ([]byte, error) { return values[key], nil },
|
||||
}
|
||||
}
|
||||
|
||||
// 774/AC11 (novox/hq ADR 0561, ADR 0549 §3, §5): with no assignments to read, the vault tells from the catalogue's
|
||||
// placement which node holds the bus and whether a consumer's module stands on its node; while both stand, a module
|
||||
// either names stands.
|
||||
func TestTheVaultReadsWhereModulesStandFromPlacement(t *testing.T) {
|
||||
v := newVault(t, nil)
|
||||
v.Placement = placementsOf(t, map[string][]string{"novox": {"nats", "web"}, "ace": {"shop"}})
|
||||
if node, err := v.busNode(); err != nil || node != "novox" {
|
||||
t.Errorf("the bus node from placement: %q, %v", node, err)
|
||||
}
|
||||
asked := func(node, module string) CredentialAsked {
|
||||
return CredentialAsked{Role: "consumer", Provision: "postgres", Consumer: node, ConsumerModule: module, Provider: "novox"}
|
||||
}
|
||||
if why := v.credentialRefusal("ace", asked("ace", "shop")); why != "" {
|
||||
t.Errorf("shop stands on ace by placement, refused: %s", why)
|
||||
}
|
||||
if why := v.credentialRefusal("ace", asked("ace", "web")); !strings.Contains(why, "is not on ace") {
|
||||
t.Errorf("web is not placed on ace, and is answered %q", why)
|
||||
}
|
||||
// While both stand, an assignment still names a module placement does not yet.
|
||||
v.Askers = Declared{Read: assignments{"ace.web": `{"node":"ace","module":"web"}`}.read, Store: v.Store}
|
||||
if why := v.credentialRefusal("ace", asked("ace", "web")); why != "" {
|
||||
t.Errorf("web assigned on ace, refused: %s", why)
|
||||
}
|
||||
// Two nodes placed with the bus: the vault adopts from neither.
|
||||
v.Placement = placementsOf(t, map[string][]string{"novox": {"nats"}, "ace": {"nats"}})
|
||||
if _, err := v.busNode(); err == nil || !strings.Contains(err.Error(), "2 nodes") {
|
||||
t.Errorf("two bus nodes: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,264 +0,0 @@
|
||||
package main
|
||||
|
||||
// A wanted account no longer wanted is retired within ADR 0230's bounds (novox/hq ADR 0561 §2, issue 774).
|
||||
//
|
||||
// The grants part deletes a wanted account's key when no input names its user any more. The vault then retires the
|
||||
// account: it takes its hash out of `bus-accounts`, so the grants part's next list drops the user, and marks the value
|
||||
// retired in its store, keeping it (a value is deleted only by a person, ADR 0230). As ADR 0230 §15 and §18 have it, the
|
||||
// same set of accounts gone must stand for five passes and ten minutes first, and a set of more than three, or more than
|
||||
// half of the accounts held, waits for a person, who retires them with `account_retire`. Wanted again, an account is
|
||||
// re-enabled at once: the mark taken off and its hash written again. Nothing is retired while the controller holds the
|
||||
// lease (it forgets the accounts it mints), or before the wanted accounts are read whole.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// retiring is what the passes keep of the accounts no longer wanted, under minting.mu.
|
||||
type retiring struct {
|
||||
wantsRead bool
|
||||
key string
|
||||
passes int
|
||||
first time.Time
|
||||
waiting []string
|
||||
why string
|
||||
retired []string
|
||||
}
|
||||
|
||||
// RetireState is what vault_status says of retirement.
|
||||
type RetireState struct {
|
||||
Gone []string `json:"gone"`
|
||||
Passes int `json:"passes"`
|
||||
Since time.Time `json:"since,omitzero"`
|
||||
Waiting []string `json:"waiting_for_a_person"`
|
||||
Why string `json:"why"`
|
||||
Retired []string `json:"retired"`
|
||||
}
|
||||
|
||||
// RetireState answers how retirement stands.
|
||||
func (v *Vault) RetireState() RetireState {
|
||||
m := &v.minting
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
r := m.retiring
|
||||
out := RetireState{Passes: r.passes, Since: r.first, Why: r.why, Waiting: append([]string{}, r.waiting...),
|
||||
Retired: append([]string{}, r.retired...), Gone: []string{}}
|
||||
if r.key != "" {
|
||||
out.Gone = strings.Split(r.key, " ")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// WantsRead says the wanted accounts were read whole once (the watch handed every current key): only then is an account
|
||||
// with no key known to be no longer wanted.
|
||||
func (v *Vault) WantsRead() {
|
||||
v.minting.mu.Lock()
|
||||
defer v.minting.mu.Unlock()
|
||||
v.minting.retiring.wantsRead = true
|
||||
}
|
||||
|
||||
// wantGone is a wanted account's key deleted: forgotten as wanted, its retirement left to the passes.
|
||||
func (v *Vault) wantGone(user string) {
|
||||
v.minting.mu.Lock()
|
||||
defer v.minting.mu.Unlock()
|
||||
delete(v.minting.wants, user)
|
||||
delete(v.minting.heldBack, user)
|
||||
}
|
||||
|
||||
// wantBack is a wanted account's key written: a retired account of it is re-enabled at once.
|
||||
func (v *Vault) wantBack(user string) {
|
||||
w, ok := v.wanted(user)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
secret, err := AccountSecret(w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rec, held, err := v.Store.Read(secret)
|
||||
if err != nil || !held || rec.Retired == nil {
|
||||
return
|
||||
}
|
||||
rec.Retired = nil
|
||||
if err := v.Store.Write(rec); err != nil {
|
||||
v.log("%s is wanted again and NOT RE-ENABLED: its mark could not be taken off: %v", user, err)
|
||||
return
|
||||
}
|
||||
if _, err := v.HashAccount(secret, true); err != nil {
|
||||
v.hashes.owe(secret, err.Error())
|
||||
v.log("%s is wanted again and its hash is not written yet, written at the next pass: %v", user, err)
|
||||
}
|
||||
v.minting.mu.Lock()
|
||||
r := &v.minting.retiring
|
||||
for i, u := range r.retired {
|
||||
if u == user {
|
||||
r.retired = append(r.retired[:i], r.retired[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
v.minting.mu.Unlock()
|
||||
v.log("%s is wanted again: re-enabled, its hash written again", user)
|
||||
}
|
||||
|
||||
// goneAccounts are the users whose hash the vault wrote from a value it holds and that no wanted key names, sorted, and
|
||||
// how many such hashes it holds in all.
|
||||
func (v *Vault) goneAccounts() ([]string, int, error) {
|
||||
users, err := v.Accounts.Users()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var gone []string
|
||||
held := 0
|
||||
for _, u := range users {
|
||||
a, err := v.Accounts.Get(u)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if a == nil || a.Adopted || a.Of == "" {
|
||||
continue
|
||||
}
|
||||
held++
|
||||
if _, wanted := v.wanted(u); !wanted {
|
||||
gone = append(gone, u)
|
||||
}
|
||||
}
|
||||
sort.Strings(gone)
|
||||
return gone, held, nil
|
||||
}
|
||||
|
||||
// RetirePass is one pass of retirement, at the vault's minute pass.
|
||||
func (v *Vault) RetirePass() {
|
||||
if v.Accounts == nil {
|
||||
return
|
||||
}
|
||||
v.leaseAfresh()
|
||||
on, leaseWhy := v.mintingOn()
|
||||
m := &v.minting
|
||||
m.mu.Lock()
|
||||
read := m.retiring.wantsRead
|
||||
m.mu.Unlock()
|
||||
reset := func(why string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.retiring.key, m.retiring.passes, m.retiring.first, m.retiring.waiting, m.retiring.why = "", 0, time.Time{}, nil, why
|
||||
}
|
||||
switch {
|
||||
case !read:
|
||||
reset("the wanted accounts are not read whole yet, so none is known to be gone")
|
||||
return
|
||||
case !on:
|
||||
reset("nothing is retired: " + leaseWhy)
|
||||
return
|
||||
}
|
||||
gone, held, err := v.goneAccounts()
|
||||
if err != nil {
|
||||
reset("the accounts held could not be read: " + err.Error())
|
||||
return
|
||||
}
|
||||
if len(gone) == 0 {
|
||||
reset("")
|
||||
return
|
||||
}
|
||||
now := v.Now().UTC()
|
||||
key := strings.Join(gone, " ")
|
||||
m.mu.Lock()
|
||||
r := &m.retiring
|
||||
if r.key != key {
|
||||
r.key, r.passes, r.first, r.waiting = key, 0, now, nil
|
||||
}
|
||||
r.passes++
|
||||
passes, since := r.passes, r.first
|
||||
m.mu.Unlock()
|
||||
setWhy := func(why string, waiting []string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.retiring.why, m.retiring.waiting = why, waiting
|
||||
}
|
||||
if passes < StablePasses || now.Sub(since) < StableFor {
|
||||
setWhy(fmt.Sprintf("%d no longer wanted (%s): retired once the same set stands for %d passes and %s (ADR 0230)",
|
||||
len(gone), key, StablePasses, StableFor), nil)
|
||||
return
|
||||
}
|
||||
if len(gone) > RetireAtOnce || float64(len(gone)) > RetireFraction*float64(held) {
|
||||
why := fmt.Sprintf("%d accounts are no longer wanted, more than %d or more than half of the %d held at once: they "+
|
||||
"wait for a person (account_retire), ADR 0230 §18", len(gone), RetireAtOnce, held)
|
||||
setWhy(why, gone)
|
||||
v.log("%s: %s", why, key)
|
||||
return
|
||||
}
|
||||
for _, u := range gone {
|
||||
if err := v.retireOne(u, "no longer wanted by the grants part", "the vault's pass"); err != nil {
|
||||
v.log("%s is not retired: %v", u, err)
|
||||
}
|
||||
}
|
||||
reset("")
|
||||
}
|
||||
|
||||
// retireOne marks the value of user's account retired and takes its hash out of bus-accounts.
|
||||
func (v *Vault) retireOne(user, why, by string) error {
|
||||
a, err := v.Accounts.Get(user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if a == nil || a.Adopted || a.Of == "" {
|
||||
return fmt.Errorf("the vault holds no account of %s it made or was handed: an adopted hash is never retired here", user)
|
||||
}
|
||||
rec, held, err := v.Store.Read(a.Of)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if held {
|
||||
rec.Retired = &RetiredMark{At: v.Now().UTC().Format(time.RFC3339), Why: why + ", by " + by}
|
||||
if err := v.Store.Write(rec); err != nil {
|
||||
return fmt.Errorf("its value could not be marked retired: %w", err)
|
||||
}
|
||||
}
|
||||
if err := v.Accounts.Delete(user); err != nil {
|
||||
return fmt.Errorf("its hash could not be taken out of %s: %w", BusAccountsState, err)
|
||||
}
|
||||
v.minting.mu.Lock()
|
||||
v.minting.retiring.retired = append(v.minting.retiring.retired, user)
|
||||
v.minting.mu.Unlock()
|
||||
v.log("retired the bus account %s (%s): its hash is out of %s and its value kept, marked; %s", user, a.Of,
|
||||
BusAccountsState, why)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RetireAccounts is the tool account_retire: a person retires accounts no longer wanted, past ADR 0230's bound, with why.
|
||||
func (v *Vault) RetireAccounts(caller string, users []string, why string) (any, error) {
|
||||
if strings.TrimSpace(why) == "" {
|
||||
return nil, errors.New("account_retire says why (`why`)")
|
||||
}
|
||||
if len(users) == 0 {
|
||||
return nil, errors.New("account_retire names the accounts (`users`), as vault_status lists them waiting")
|
||||
}
|
||||
// As the passes: never while the controller holds the lease (it forgets the accounts it mints), nor before the wanted
|
||||
// accounts are read whole (an account not read yet is not known to be gone).
|
||||
v.leaseAfresh()
|
||||
if on, why := v.mintingOn(); !on {
|
||||
return nil, fmt.Errorf("nothing was retired: %s", why)
|
||||
}
|
||||
v.minting.mu.Lock()
|
||||
read := v.minting.retiring.wantsRead
|
||||
v.minting.mu.Unlock()
|
||||
if !read {
|
||||
return nil, errors.New("nothing was retired: the wanted accounts are not read whole yet, so none is known to be gone")
|
||||
}
|
||||
for _, u := range users {
|
||||
if _, wanted := v.wanted(u); wanted {
|
||||
return nil, fmt.Errorf("%s is still wanted by the grants part: only an account no longer wanted is retired; nothing was retired", u)
|
||||
}
|
||||
}
|
||||
var done []string
|
||||
for _, u := range users {
|
||||
if err := v.retireOne(u, strings.TrimSpace(why), someoneOr("", caller)); err != nil {
|
||||
return map[string]any{"retired": done}, fmt.Errorf("%s: %w", u, err)
|
||||
}
|
||||
done = append(done, u)
|
||||
}
|
||||
return map[string]any{"retired": done}, nil
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 774/AC9 (novox/hq ADR 0561 §2, ADR 0230 §15, §18): a wanted account whose key is deleted is retired (its hash taken
|
||||
// out of bus-accounts, its value kept, marked) only once the same set has stood for five passes and ten minutes, and never
|
||||
// more than three, or more than half of those held, at once: past that it waits for a person (account_retire). Wanted
|
||||
// again, it is re-enabled at once; and a retired account is not hashed again at a start.
|
||||
func TestAWantGoneRetiresItsAccountOnlyWithinTheBounds(t *testing.T) {
|
||||
v, accounts, _ := mintingVault(t, true)
|
||||
clock := time.Date(2026, 10, 28, 9, 0, 0, 0, time.UTC)
|
||||
v.Now = func() time.Time { return clock }
|
||||
modules := []string{"m1", "m2", "m3", "m4", "m5", "m6", "m7", "m8"}
|
||||
for _, m := range modules {
|
||||
w := aWant(m)
|
||||
v.WantHeard(w.User, wantRaw(t, w), false)
|
||||
}
|
||||
v.WantsRead()
|
||||
if len(accounts.keys) != 8 {
|
||||
t.Fatalf("minted %d", len(accounts.keys))
|
||||
}
|
||||
pass := func(n int, every time.Duration) {
|
||||
for i := 0; i < n; i++ {
|
||||
clock = clock.Add(every)
|
||||
v.RetirePass()
|
||||
}
|
||||
}
|
||||
v.WantHeard("ace.m1", nil, true)
|
||||
pass(5, time.Second)
|
||||
if _, ok := accounts.keys["ace.m1"]; !ok {
|
||||
t.Fatal("retired after five passes in five seconds: ADR 0230 waits ten minutes too")
|
||||
}
|
||||
pass(1, 10*time.Minute)
|
||||
if _, ok := accounts.keys["ace.m1"]; ok {
|
||||
t.Fatalf("not retired after five passes and ten minutes: %+v", v.RetireState())
|
||||
}
|
||||
if rec, held, _ := v.Store.Read("own/ace/m1/broker"); !held || rec.Retired == nil {
|
||||
t.Fatalf("the retired account's value is %+v (held %v): kept, and marked", rec, held)
|
||||
}
|
||||
if written, _ := v.HashHeld(); len(written) != 0 {
|
||||
t.Errorf("a start hashed a retired account again: %+v", written)
|
||||
}
|
||||
// Wanted again: re-enabled at once.
|
||||
w := aWant("m1")
|
||||
v.WantHeard(w.User, wantRaw(t, w), false)
|
||||
if rec, _, _ := v.Store.Read("own/ace/m1/broker"); rec.Retired != nil || accounts.keys["ace.m1"].Hash == "" {
|
||||
t.Fatalf("wanted again, ace.m1 is %+v with hash %v", rec.Retired, accounts.keys["ace.m1"])
|
||||
}
|
||||
// Four gone at once is over the bound: it waits for a person, who retires them by hand.
|
||||
for _, m := range modules[4:] {
|
||||
v.WantHeard("ace."+m, nil, true)
|
||||
}
|
||||
pass(6, 3*time.Minute)
|
||||
st := v.RetireState()
|
||||
if len(accounts.keys) != 8 || len(st.Waiting) != 4 || !strings.Contains(st.Why, "more than 3") {
|
||||
t.Fatalf("over the bound: %d keys, %+v", len(accounts.keys), st)
|
||||
}
|
||||
if _, err := v.RetireAccounts("person.jochen", []string{"ace.m5"}, ""); err == nil {
|
||||
t.Error("retired by hand with no why")
|
||||
}
|
||||
if _, err := v.RetireAccounts("person.jochen", []string{"ace.m2"}, "test"); err == nil {
|
||||
t.Error("retired by hand an account still wanted")
|
||||
}
|
||||
// Never while the controller holds the lease, nor before the wants are read whole.
|
||||
held := v.LeaseRead
|
||||
v.LeaseRead = func() ([]byte, bool, error) { return []byte(`{"instance":"novox/1","epoch":57}`), true, nil }
|
||||
if _, err := v.RetireAccounts("person.jochen", []string{"ace.m5"}, "the modules left"); err == nil || !strings.Contains(err.Error(), "controller holds the lease") {
|
||||
t.Errorf("retired by hand under the controller's lease: %v", err)
|
||||
}
|
||||
v.LeaseRead = held
|
||||
v.LeaseHeard([]byte(`{"handed-to":"the operator's hand-over"}`), true, nil)
|
||||
v.minting.mu.Lock()
|
||||
v.minting.retiring.wantsRead = false
|
||||
v.minting.mu.Unlock()
|
||||
if _, err := v.RetireAccounts("person.jochen", []string{"ace.m5"}, "the modules left"); err == nil || !strings.Contains(err.Error(), "not read whole") {
|
||||
t.Errorf("retired by hand before the wants were read: %v", err)
|
||||
}
|
||||
v.WantsRead()
|
||||
if _, err := v.RetireAccounts("person.jochen", []string{"ace.m5", "ace.m6"}, "the modules left"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := accounts.keys["ace.m5"]; ok || len(accounts.keys) != 6 {
|
||||
t.Errorf("after a person's retirement %d keys remain", len(accounts.keys))
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package main
|
||||
|
||||
// The record of a bus account's rotation, and its one rule (novox/hq ADR 0561 §4; issue 774, the lead's decision of
|
||||
// 2026-10-27). A module's, a process's or a provisioner client's account rotates over two credentials (ADR 0114 §10):
|
||||
// a second login, `<user>--b`, carrying the first login's permissions verbatim, which the bus composer composes only while
|
||||
// the account's key in `bus-accounts` names it in `rotating`. The vault starts at most one rotation per revision of the
|
||||
// user list the bus loaded. What composes and delivers the second login is a subtask of its own, before the lease write;
|
||||
// until it is built nothing begins a rotation, and `rotate` of a covered account stays refused, so nothing rotates in
|
||||
// place.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SecondLoginSuffix makes an account's second login: reserved, so no module or process may be named to end in it.
|
||||
const SecondLoginSuffix = "--b"
|
||||
|
||||
// Rotating is a rotation under way, as the account's key in bus-accounts says it: the second login, the generation of
|
||||
// the value it carries, since when, and the revision of the loaded user list it began at.
|
||||
type Rotating struct {
|
||||
Login string `json:"login"`
|
||||
Generation int `json:"generation"`
|
||||
Since time.Time `json:"since"`
|
||||
Revision uint64 `json:"revision"`
|
||||
}
|
||||
|
||||
// LoadedListHeard keeps the revision of the user list the bus node says it loaded (its have of the bus module).
|
||||
func (v *Vault) LoadedListHeard(revision uint64) {
|
||||
v.minting.mu.Lock()
|
||||
defer v.minting.mu.Unlock()
|
||||
if revision > v.minting.loadedRevision {
|
||||
v.minting.loadedRevision = revision
|
||||
}
|
||||
}
|
||||
|
||||
// BeginRotation records a rotation of user's account at the loaded list's revision, under the one rule: none for a
|
||||
// node's account or the controller's, none while minting is off, none of an account the vault did not make or was not
|
||||
// handed, none twice for one account, and none when another began at this revision.
|
||||
func (v *Vault) BeginRotation(user string) (Rotating, error) {
|
||||
if nodeOrController(user) {
|
||||
return Rotating{}, fmt.Errorf("%s does not rotate here: a node's account and the controller's keep the hash they have (ADR 0561 §4)", user)
|
||||
}
|
||||
if on, why := v.mintingOn(); !on {
|
||||
return Rotating{}, fmt.Errorf("no rotation begins: %s", why)
|
||||
}
|
||||
v.minting.mu.Lock()
|
||||
revision := v.minting.loadedRevision
|
||||
v.minting.mu.Unlock()
|
||||
if revision == 0 {
|
||||
return Rotating{}, fmt.Errorf("no rotation begins: the revision of the user list the bus loaded is not known yet")
|
||||
}
|
||||
users, err := v.Accounts.Users()
|
||||
if err != nil {
|
||||
return Rotating{}, err
|
||||
}
|
||||
var mine *AccountHash
|
||||
began := ""
|
||||
for _, u := range users {
|
||||
a, err := v.Accounts.Get(u)
|
||||
if err != nil {
|
||||
return Rotating{}, err
|
||||
}
|
||||
if a == nil {
|
||||
continue
|
||||
}
|
||||
if a.Rotating != nil && a.Rotating.Revision == revision {
|
||||
began = u
|
||||
}
|
||||
if u == user {
|
||||
mine = a
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case mine == nil || mine.Adopted || mine.Of == "":
|
||||
return Rotating{}, fmt.Errorf("the vault holds no account of %s it made or was handed, so it rotates none", user)
|
||||
case mine.Rotating != nil:
|
||||
return Rotating{}, fmt.Errorf("%s is rotating already, since %s (second login %s)", user, mine.Rotating.Since.Format(time.RFC3339), mine.Rotating.Login)
|
||||
case strings.HasSuffix(user, SecondLoginSuffix):
|
||||
return Rotating{}, fmt.Errorf("%s is a second login, which never rotates of its own", user)
|
||||
case began != "":
|
||||
return Rotating{}, fmt.Errorf("no rotation of %s begins: %s's began at the loaded list's revision %d, and one "+
|
||||
"rotation begins per revision (ADR 0561 §4)", user, began, revision)
|
||||
}
|
||||
r := Rotating{Login: user + SecondLoginSuffix, Generation: mine.Generation + 1, Since: v.Now().UTC(), Revision: revision}
|
||||
next := *mine
|
||||
next.Rotating = &r
|
||||
if err := v.Accounts.Put(next); err != nil {
|
||||
return Rotating{}, err
|
||||
}
|
||||
v.log("a rotation of %s begins at the loaded list's revision %d: second login %s", user, revision, r.Login)
|
||||
return r, nil
|
||||
}
|
||||
@@ -48,8 +48,6 @@ type Record struct {
|
||||
// MovedFrom is, for a value the move took from the controller's store (design 57's V3), the node-engine that
|
||||
// handed it in and the controller's generation for it, kept as it was.
|
||||
MovedFrom *MovedFrom `json:"moved-from,omitempty"`
|
||||
// Retired marks a bus account no longer wanted (retire.go, ADR 0561 §2): its value kept, its hash taken out.
|
||||
Retired *RetiredMark `json:"retired,omitempty"`
|
||||
}
|
||||
|
||||
// OperatorCopy is a value sealed to the operator key named in Key.
|
||||
|
||||
@@ -251,20 +251,6 @@ func definitions(env *Env) []toolDef {
|
||||
// clause 9, adopt.go): a public half, never the private one.
|
||||
return map[string]any{"recorded": true, "vault_key": v.Own.PublicKey}, nil
|
||||
}},
|
||||
{Name: Seat + ".issue-account",
|
||||
Description: "Mint one bus account the grants part wants, now (novox/hq ADR 0561 §9): the recovery when the vault did not " +
|
||||
"hear a want. Reads the wanted key afresh from mesh-grants.accounts and mints it as a want would: only while " +
|
||||
"the controller's lease is handed away, never for a node's account or the controller's, never a user nobody " +
|
||||
"wants, and never anew for one whose value the vault holds or whose hash it adopted (it says so). Answers " +
|
||||
"whether it minted, the secret and generation, never the value or the hash.",
|
||||
Input: map[string]any{"user": str("the bus user, e.g. ace.mesh-build-go")},
|
||||
run: func(caller string, a map[string]any) (any, error) {
|
||||
v, err := env.ready()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v.IssueAccount(caller, arg(a, "user"))
|
||||
}},
|
||||
{Name: Seat + ".own",
|
||||
Description: "The own secrets of this machine's modules, each sealed to the machine's sealing key (novox/hq ADR 0547 §2, " +
|
||||
"ADR 0550 §5, issue 1126): from the node-engine of the machine alone, about its own machine (the bus proves which), " +
|
||||
@@ -438,28 +424,6 @@ func definitions(env *Env) []toolDef {
|
||||
}
|
||||
return env.Vault.HashUser(arg(a, "user"), caller)
|
||||
}},
|
||||
{Name: "account_retire",
|
||||
Description: "Retire bus accounts the grants part no longer wants, by a person's act with why (novox/hq ADR 0561 §2, " +
|
||||
"ADR 0230 §18): the vault takes each one's hash out of its state `bus-accounts`, so the grants part's next user " +
|
||||
"list drops the user, and keeps its value marked retired. For the set vault_status lists as waiting for a " +
|
||||
"person (more than three, or more than half, gone at once); refused for an account still wanted, an adopted " +
|
||||
"hash, and with no why. Wanted again, an account is re-enabled at once.",
|
||||
Input: map[string]any{"users": map[string]any{"type": "array", "items": str("a bus user")},
|
||||
"why": str("why they are retired")},
|
||||
run: func(caller string, a map[string]any) (any, error) {
|
||||
if env.Vault == nil {
|
||||
return nil, errors.New("the vault is not ready")
|
||||
}
|
||||
var users []string
|
||||
if list, ok := a["users"].([]any); ok {
|
||||
for _, u := range list {
|
||||
if s, ok := u.(string); ok {
|
||||
users = append(users, strings.TrimSpace(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
return env.Vault.RetireAccounts(caller, users, arg(a, "why"))
|
||||
}},
|
||||
{Name: "secret_vault_export",
|
||||
Description: "Every secret the vault itself holds, as its copy sealed to the operator key, in the shape of " +
|
||||
"`secret export`: saved to a file, `nox secret recover vault mesh-vault <name> --key <operator key file> " +
|
||||
@@ -528,9 +492,7 @@ func definitions(env *Env) []toolDef {
|
||||
run: func(string, map[string]any) (any, error) { return env.health(), nil }},
|
||||
{Name: "vault_status",
|
||||
Description: "What needs a person's eye in the vault: operator copies still sealed to an earlier key, the operator " +
|
||||
"key the mesh's export holds if the vault refused it, how often the record of hand-outs was rolled, and " +
|
||||
"minting (novox/hq ADR 0561 §3, §9): whether the vault mints bus accounts now and why (only while the " +
|
||||
"controller's lease is handed away), every wanted account it holds back with why, and those it minted.",
|
||||
"key the mesh's export holds if the vault refused it, and how often the record of hand-outs was rolled.",
|
||||
run: func(string, map[string]any) (any, error) {
|
||||
if env.Vault == nil {
|
||||
return map[string]any{"ready": false, "error": fmt.Sprint(env.Broken)}, nil
|
||||
@@ -542,7 +504,7 @@ func definitions(env *Env) []toolDef {
|
||||
key, _ := env.Vault.Store.OperatorKey()
|
||||
return map[string]any{"ready": true, "operator_key": key, "copies_on_an_earlier_key": left,
|
||||
"operator_key_refused": env.keyRefused(), "handed_rolled": env.Vault.Store.Rolled(),
|
||||
"minting": env.Vault.MintState(), "retiring": env.Vault.RetireState(), "ok": left == 0 && env.keyRefused() == "" && key != ""}, nil
|
||||
"ok": left == 0 && env.keyRefused() == "" && key != ""}, nil
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,14 +56,6 @@ type Vault struct {
|
||||
hashes hashing
|
||||
// adoption is the last adoption (adopt.go), under mu.
|
||||
adoption adoptionKept
|
||||
// WantRead reads one wanted account afresh (issue-account): nil reads the wants as the watch handed them.
|
||||
WantRead func(user string) ([]byte, bool, error)
|
||||
// Placement reads the catalogue's placement (placed.go): where modules stand once the assignments are gone.
|
||||
Placement PlacementReader
|
||||
// LeaseRead reads the controller's lease afresh before a mint (lease.go): nil reads it only as the watch hands it.
|
||||
LeaseRead func() ([]byte, bool, error)
|
||||
// minting is the switch on the lease and what the vault knows of the wanted bus accounts (mint.go).
|
||||
minting minting
|
||||
// callsMu guards pending: the keys of the calls open now (intake.go), never written anywhere.
|
||||
callsMu sync.Mutex
|
||||
pending map[string]*callKey
|
||||
@@ -112,10 +104,8 @@ func (v *Vault) keep(name, value, by, via, origin string) (Changed, error) {
|
||||
// was first made, and where it was moved from.
|
||||
type keepOptions struct {
|
||||
neverReplace bool
|
||||
// quiet keeps secret-changed for the caller to say once the rest is done (a minted account: mint.go).
|
||||
quiet bool
|
||||
madeAt time.Time
|
||||
movedFrom *MovedFrom
|
||||
madeAt time.Time
|
||||
movedFrom *MovedFrom
|
||||
}
|
||||
|
||||
// keepWith is keep with the move's options. A covered bus account kept anew has its hash written once it is kept
|
||||
@@ -196,9 +186,7 @@ func (v *Vault) keepLocked(name, value, by, via, origin string, opt keepOptions)
|
||||
return Changed{}, err
|
||||
}
|
||||
c := Changed{Name: name, Generation: rec.Generation, At: now, By: by}
|
||||
if !opt.quiet {
|
||||
v.announce(c)
|
||||
}
|
||||
v.announce(c)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -131,11 +131,9 @@ func filesUnder(t *testing.T, dir string) map[string]string {
|
||||
|
||||
// The manifest declares one bucket, and no secret is in it: the vault's only place for a secret is its own store on its
|
||||
// own disk (ADR 0349 §7, ADR 0201 §13). The one bucket is bus-accounts, the bcrypt hashes of the bus accounts it holds
|
||||
// (ADR 0547 clause 9), opened in accounts.go alone. It reads five buckets of others', and only reads them: the
|
||||
// controller's assignments, for what each node's plan names (askers.go, design 57 V4), the node-engines' haves, where
|
||||
// the bus node hands the loaded list's hashes sealed to the vault (adopt.go, ADR 0547 clause 9), the grants part's wanted
|
||||
// accounts and the controller's lease (mint.go, lease.go, ADR 0561 §2, §3), and the catalogue's placement (placed.go), in
|
||||
// main.go alone.
|
||||
// (ADR 0547 clause 9), opened in accounts.go alone. It reads two buckets of others', and only reads them: the
|
||||
// controller's assignments, for what each node's plan names (askers.go, design 57 V4), and the node-engines' haves, where
|
||||
// the bus node hands the loaded list's hashes sealed to the vault (adopt.go, ADR 0547 clause 9), in main.go alone.
|
||||
func TestNoBucketHoldsASecret(t *testing.T) {
|
||||
raw, err := os.ReadFile("../../module.json")
|
||||
if err != nil {
|
||||
@@ -149,9 +147,8 @@ func TestNoBucketHoldsASecret(t *testing.T) {
|
||||
t.Errorf("the manifest declares buckets %s: a secret is in no bucket, sealed or not (ADR 0201 §13), and the one "+
|
||||
"bucket is the accounts' hashes (ADR 0547 clause 9)", s)
|
||||
}
|
||||
if r, _ := json.Marshal(manifest["reads"]); string(r) != `["mesh-controller.assignments","node-engine.have","mesh-grants.accounts","mesh-controller.lease","mesh-delivery-indexer.placement"]` {
|
||||
t.Errorf("the manifest reads %s: the vault reads the controller's assignments, the haves and the grants part's "+
|
||||
"wanted accounts alone", r)
|
||||
if r, _ := json.Marshal(manifest["reads"]); string(r) != `["mesh-controller.assignments","node-engine.have"]` {
|
||||
t.Errorf("the manifest reads %s: the vault reads the controller's assignments and the haves alone", r)
|
||||
}
|
||||
sources, _ := filepath.Glob("*.go")
|
||||
bucket := regexp.MustCompile(`stdio\.State\(|nats-io|jetstream|KeyValue`)
|
||||
@@ -169,21 +166,11 @@ func TestNoBucketHoldsASecret(t *testing.T) {
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(BusAccountsState).Get(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(BusAccountsState).Put(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(BusAccountsState).Keys(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(BusAccountsState).Delete(", "", 1))
|
||||
}
|
||||
// The haves, in main.go alone: one read and one watch, never a write; and one watch of the wanted accounts, the
|
||||
// grants part's, which hold no secret (ADR 0561 §2).
|
||||
// The haves, in main.go alone: one read and one watch, never a write.
|
||||
if f == "main.go" {
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(HaveRead).Get(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(HaveRead).Watch(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Watch(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(WantedRead).Get(", "", 1))
|
||||
// And the controller's lease, one read and one watch (ADR 0561 §3).
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(LeaseRead).Get(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(LeaseRead).Watch(", "", 1))
|
||||
// And the catalogue's placement, where modules stand (placed.go): its keys and one key, read.
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(PlacementRead).Keys(", "", 1))
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(PlacementRead).Get(", "", 1))
|
||||
}
|
||||
if m := bucket.Find(code); m != nil {
|
||||
t.Errorf("%s opens a bucket (%s): the vault keeps every secret in its own store only", f, m)
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-vault
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027220144-3f7957e16972
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
golang.org/x/crypto v0.57.0
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027220144-3f7957e16972 h1:iYDYv4HpV1ic20Ban0MaOErryQfNT8ngjlh0bKW6sCU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261027220144-3f7957e16972/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021100209-cb54756c3865 h1:VLir9NZbyi1LdH1YwWDIVRuL+lDC+AYlm6FZPlvmlm4=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021100209-cb54756c3865/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026102838-174789941936 h1:HGBoWimZXYZp+Qn9VCstYDitOP84t9EUQYd1yM9j2eg=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026102838-174789941936/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376 h1:by2zBASLb8h5jA1IiYJWykXwLnHYpuZh/zFNa4NsbOU=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -15,15 +15,11 @@
|
||||
"provisioned",
|
||||
"rotated",
|
||||
"secret-changed",
|
||||
"account-hashed",
|
||||
"account-issued"
|
||||
"account-hashed"
|
||||
],
|
||||
"reads": [
|
||||
"mesh-controller.assignments",
|
||||
"node-engine.have",
|
||||
"mesh-grants.accounts",
|
||||
"mesh-controller.lease",
|
||||
"mesh-delivery-indexer.placement"
|
||||
"node-engine.have"
|
||||
],
|
||||
"state": [
|
||||
{
|
||||
@@ -160,7 +156,6 @@
|
||||
"secret_own",
|
||||
"account_hash",
|
||||
"account_adopt",
|
||||
"account_retire",
|
||||
"secret_asks",
|
||||
"secret_vault_export",
|
||||
"provisioner_retirement",
|
||||
@@ -183,8 +178,7 @@
|
||||
"move-in",
|
||||
"node-key",
|
||||
"move-asks",
|
||||
"own",
|
||||
"issue-account"
|
||||
"own"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
+1113
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,269 @@
|
||||
{
|
||||
"v": 1,
|
||||
"node": "g14",
|
||||
"decided": {
|
||||
"address": "10.10.0.4",
|
||||
"account": "jochen",
|
||||
"tunnel": {
|
||||
"interface": "wg0",
|
||||
"unit": "wg-quick@wg0",
|
||||
"config": "/etc/wireguard/wg0.conf",
|
||||
"port": 51820,
|
||||
"mtu": 1380,
|
||||
"address": "10.10.0.4/24",
|
||||
"range": "10.10.0.0/24",
|
||||
"public-key": "mq76YPW0FrExhAvt/ZG1SB2BK6/cPISKgXO53SEMazU="
|
||||
}
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"module": "adwaita"
|
||||
},
|
||||
{
|
||||
"module": "asus-zephyrus-g14"
|
||||
},
|
||||
{
|
||||
"module": "avahi"
|
||||
},
|
||||
{
|
||||
"module": "blueman"
|
||||
},
|
||||
{
|
||||
"module": "bluetooth"
|
||||
},
|
||||
{
|
||||
"module": "ca-trust"
|
||||
},
|
||||
{
|
||||
"module": "claude-code",
|
||||
"has-mesh-wide": true,
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "clipmenu"
|
||||
},
|
||||
{
|
||||
"module": "cups"
|
||||
},
|
||||
{
|
||||
"module": "dbus"
|
||||
},
|
||||
{
|
||||
"module": "dev-tools"
|
||||
},
|
||||
{
|
||||
"module": "disk-load"
|
||||
},
|
||||
{
|
||||
"module": "dmenu"
|
||||
},
|
||||
{
|
||||
"module": "docker"
|
||||
},
|
||||
{
|
||||
"module": "docker-compose"
|
||||
},
|
||||
{
|
||||
"module": "dunst",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "fail2ban"
|
||||
},
|
||||
{
|
||||
"module": "feh"
|
||||
},
|
||||
{
|
||||
"module": "flatpak"
|
||||
},
|
||||
{
|
||||
"module": "fonts"
|
||||
},
|
||||
{
|
||||
"module": "forticlient"
|
||||
},
|
||||
{
|
||||
"module": "gnome-keyring"
|
||||
},
|
||||
{
|
||||
"module": "go"
|
||||
},
|
||||
{
|
||||
"module": "hostname",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "i3"
|
||||
},
|
||||
{
|
||||
"module": "i3status-rust"
|
||||
},
|
||||
{
|
||||
"module": "kernel"
|
||||
},
|
||||
{
|
||||
"module": "lemurs"
|
||||
},
|
||||
{
|
||||
"module": "localization"
|
||||
},
|
||||
{
|
||||
"module": "logrotate"
|
||||
},
|
||||
{
|
||||
"module": "memory-pressure"
|
||||
},
|
||||
{
|
||||
"module": "mesh-build-go",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "mesh-build-node",
|
||||
"bindings": {
|
||||
"npm-package-registry": {
|
||||
"node": "novox",
|
||||
"module": "gitea"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "mesh-cli"
|
||||
},
|
||||
{
|
||||
"module": "mesh-files"
|
||||
},
|
||||
{
|
||||
"module": "mesh-host",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "mesh-mail-notifier"
|
||||
},
|
||||
{
|
||||
"module": "mesh-provisioner-client"
|
||||
},
|
||||
{
|
||||
"module": "mesh-reads"
|
||||
},
|
||||
{
|
||||
"module": "mesh-wireguard"
|
||||
},
|
||||
{
|
||||
"module": "netcheck"
|
||||
},
|
||||
{
|
||||
"module": "networkmanager"
|
||||
},
|
||||
{
|
||||
"module": "nextcloud-client"
|
||||
},
|
||||
{
|
||||
"module": "nftables"
|
||||
},
|
||||
{
|
||||
"module": "nm-applet"
|
||||
},
|
||||
{
|
||||
"module": "node-env"
|
||||
},
|
||||
{
|
||||
"module": "node-tools"
|
||||
},
|
||||
{
|
||||
"module": "nodejs"
|
||||
},
|
||||
{
|
||||
"module": "okioki",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "openrazer"
|
||||
},
|
||||
{
|
||||
"module": "pacman",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "picom"
|
||||
},
|
||||
{
|
||||
"module": "polychromatic"
|
||||
},
|
||||
{
|
||||
"module": "power",
|
||||
"has-mesh-wide": true,
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "powerlevel10k"
|
||||
},
|
||||
{
|
||||
"module": "restic"
|
||||
},
|
||||
{
|
||||
"module": "rofi"
|
||||
},
|
||||
{
|
||||
"module": "screen-lock"
|
||||
},
|
||||
{
|
||||
"module": "sensors"
|
||||
},
|
||||
{
|
||||
"module": "slack"
|
||||
},
|
||||
{
|
||||
"module": "snapd"
|
||||
},
|
||||
{
|
||||
"module": "ssh-client"
|
||||
},
|
||||
{
|
||||
"module": "sshd"
|
||||
},
|
||||
{
|
||||
"module": "sudo"
|
||||
},
|
||||
{
|
||||
"module": "systemd"
|
||||
},
|
||||
{
|
||||
"module": "systemd-resolved"
|
||||
},
|
||||
{
|
||||
"module": "time-sync"
|
||||
},
|
||||
{
|
||||
"module": "triggerhappy"
|
||||
},
|
||||
{
|
||||
"module": "xclip"
|
||||
},
|
||||
{
|
||||
"module": "xdg",
|
||||
"has-mesh-wide": true,
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "xorg"
|
||||
},
|
||||
{
|
||||
"module": "xterm"
|
||||
},
|
||||
{
|
||||
"module": "zsh"
|
||||
},
|
||||
{
|
||||
"module": "zsh-autosuggestions"
|
||||
},
|
||||
{
|
||||
"module": "zsh-syntax-highlighting"
|
||||
}
|
||||
],
|
||||
"pins": {
|
||||
"wildcard-resolution": {
|
||||
"node": "novox",
|
||||
"module": "dnsmasq"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,584 @@
|
||||
{
|
||||
"v": 1,
|
||||
"node": "novox",
|
||||
"decided": {
|
||||
"endpoint": "142.132.152.141:51820",
|
||||
"site": "hosting",
|
||||
"hub": true,
|
||||
"address": "10.10.0.1",
|
||||
"public-domain": "novox.be",
|
||||
"account": "jochens",
|
||||
"agent-account": "nox",
|
||||
"tunnel": {
|
||||
"interface": "wg0",
|
||||
"unit": "wg-quick@wg0",
|
||||
"config": "/etc/wireguard/wg0.conf",
|
||||
"port": 51820,
|
||||
"address": "10.10.0.1/24",
|
||||
"range": "10.10.0.0/24",
|
||||
"public-key": "xW0ETQwyR9rHi2XSfOJrKkairE+Fp70FPNNcirqMixY="
|
||||
},
|
||||
"peer-names": {
|
||||
"XT1l25Z0/YhRNQ8FQM+9XsAp7chx5MI0eLOOg1SUEXA=": "shanks",
|
||||
"mq76YPW0FrExhAvt/ZG1SB2BK6/cPISKgXO53SEMazU=": "g14",
|
||||
"zTKYP6Sz1hDwnwxIHgiGXYAeGcWbqti3tcSvKEA0Ew4=": "ace"
|
||||
}
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"module": "artifact-store-tools",
|
||||
"has-mesh-wide": true,
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "avahi"
|
||||
},
|
||||
{
|
||||
"module": "ca-trust"
|
||||
},
|
||||
{
|
||||
"module": "claude-code",
|
||||
"has-mesh-wide": true,
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "claude-licence-manager",
|
||||
"bindings": {
|
||||
"postgres-database": {
|
||||
"node": "novox",
|
||||
"module": "postgres"
|
||||
},
|
||||
"secret": {
|
||||
"node": "novox",
|
||||
"module": "mesh-vault"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "dbus"
|
||||
},
|
||||
{
|
||||
"module": "de-spiegel",
|
||||
"ports": {
|
||||
"35621": 20005
|
||||
},
|
||||
"bindings": {
|
||||
"smtp": {
|
||||
"node": "novox",
|
||||
"module": "mailu"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "desk-channel",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "dev-tools"
|
||||
},
|
||||
{
|
||||
"module": "disk-load"
|
||||
},
|
||||
{
|
||||
"module": "distribution",
|
||||
"layer": {
|
||||
"ports": {
|
||||
"5000": 5100
|
||||
}
|
||||
},
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "dnsmasq"
|
||||
},
|
||||
{
|
||||
"module": "docker"
|
||||
},
|
||||
{
|
||||
"module": "fail2ban"
|
||||
},
|
||||
{
|
||||
"module": "gitea",
|
||||
"ports": {
|
||||
"3000": 20000
|
||||
},
|
||||
"bindings": {
|
||||
"postgres-database": {
|
||||
"node": "novox",
|
||||
"module": "postgres"
|
||||
},
|
||||
"secret": {
|
||||
"node": "novox",
|
||||
"module": "mesh-vault"
|
||||
}
|
||||
},
|
||||
"layer": {
|
||||
"expose": {
|
||||
"22": "anywhere"
|
||||
},
|
||||
"ports": {
|
||||
"22": 222
|
||||
}
|
||||
},
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "go"
|
||||
},
|
||||
{
|
||||
"module": "hostname",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "invoicing",
|
||||
"ports": {
|
||||
"80": 20007,
|
||||
"9000": 20008
|
||||
},
|
||||
"bindings": {
|
||||
"mongodb-database": {
|
||||
"node": "novox",
|
||||
"module": "mongodb"
|
||||
},
|
||||
"s3-bucket": {
|
||||
"node": "novox",
|
||||
"module": "minio"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "kernel",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "keycloak",
|
||||
"ports": {
|
||||
"8080": 20009
|
||||
},
|
||||
"bindings": {
|
||||
"postgres-database": {
|
||||
"node": "novox",
|
||||
"module": "postgres"
|
||||
}
|
||||
},
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "link2pay"
|
||||
},
|
||||
{
|
||||
"module": "localization"
|
||||
},
|
||||
{
|
||||
"module": "logrotate"
|
||||
},
|
||||
{
|
||||
"module": "mailu",
|
||||
"ports": {
|
||||
"110": 110,
|
||||
"143": 143,
|
||||
"25": 25,
|
||||
"4243": 20004,
|
||||
"465": 465,
|
||||
"587": 587,
|
||||
"8080": 20010,
|
||||
"993": 993,
|
||||
"995": 995
|
||||
},
|
||||
"bindings": {
|
||||
"postgres-database": {
|
||||
"node": "novox",
|
||||
"module": "postgres"
|
||||
},
|
||||
"secret": {
|
||||
"node": "novox",
|
||||
"module": "mesh-vault"
|
||||
}
|
||||
},
|
||||
"has-mesh-wide": true,
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "mesh-build-go",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "mesh-build-node",
|
||||
"bindings": {
|
||||
"npm-package-registry": {
|
||||
"node": "novox",
|
||||
"module": "gitea"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "mesh-catalog",
|
||||
"bindings": {
|
||||
"postgres-database": {
|
||||
"node": "novox",
|
||||
"module": "postgres"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "mesh-cli"
|
||||
},
|
||||
{
|
||||
"module": "mesh-config"
|
||||
},
|
||||
{
|
||||
"module": "mesh-controller"
|
||||
},
|
||||
{
|
||||
"module": "mesh-delivery-checker"
|
||||
},
|
||||
{
|
||||
"module": "mesh-delivery-forge-bridge"
|
||||
},
|
||||
{
|
||||
"module": "mesh-delivery-gate",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "mesh-delivery-indexer"
|
||||
},
|
||||
{
|
||||
"module": "mesh-delivery-teller"
|
||||
},
|
||||
{
|
||||
"module": "mesh-delivery-watchman"
|
||||
},
|
||||
{
|
||||
"module": "mesh-files"
|
||||
},
|
||||
{
|
||||
"module": "mesh-grants"
|
||||
},
|
||||
{
|
||||
"module": "mesh-host",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "mesh-issues"
|
||||
},
|
||||
{
|
||||
"module": "mesh-knowledge",
|
||||
"bindings": {
|
||||
"postgres-database": {
|
||||
"node": "novox",
|
||||
"module": "postgres"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "mesh-mail",
|
||||
"bindings": {
|
||||
"postgres-database": {
|
||||
"node": "novox",
|
||||
"module": "postgres"
|
||||
},
|
||||
"s3-bucket": {
|
||||
"node": "novox",
|
||||
"module": "minio"
|
||||
}
|
||||
},
|
||||
"has-mesh-wide": true,
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "mesh-mail-reader",
|
||||
"bindings": {
|
||||
"postgres-database": {
|
||||
"node": "novox",
|
||||
"module": "postgres"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "mesh-mail-web",
|
||||
"bindings": {
|
||||
"oidc-client": {
|
||||
"node": "novox",
|
||||
"module": "keycloak"
|
||||
}
|
||||
},
|
||||
"layer": {
|
||||
"endpoints": {
|
||||
"web": {
|
||||
"reach": "internal"
|
||||
}
|
||||
}
|
||||
},
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "mesh-provisioner"
|
||||
},
|
||||
{
|
||||
"module": "mesh-provisioner-client"
|
||||
},
|
||||
{
|
||||
"module": "mesh-reads"
|
||||
},
|
||||
{
|
||||
"module": "mesh-vault"
|
||||
},
|
||||
{
|
||||
"module": "mesh-wireguard"
|
||||
},
|
||||
{
|
||||
"module": "messenger",
|
||||
"has-mesh-wide": true,
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "minio",
|
||||
"ports": {
|
||||
"9000": 20001,
|
||||
"9001": 20002
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "mongodb",
|
||||
"ports": {
|
||||
"27017": 20006
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "mssql",
|
||||
"layer": {
|
||||
"ports": {
|
||||
"1433": 4848
|
||||
}
|
||||
},
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "nats"
|
||||
},
|
||||
{
|
||||
"module": "netcheck"
|
||||
},
|
||||
{
|
||||
"module": "nextcloud",
|
||||
"ports": {
|
||||
"80": 20003
|
||||
},
|
||||
"bindings": {
|
||||
"postgres-database": {
|
||||
"node": "novox",
|
||||
"module": "postgres"
|
||||
},
|
||||
"s3-bucket": {
|
||||
"node": "novox",
|
||||
"module": "minio"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "nftables"
|
||||
},
|
||||
{
|
||||
"module": "node-env"
|
||||
},
|
||||
{
|
||||
"module": "node-tools"
|
||||
},
|
||||
{
|
||||
"module": "nodejs"
|
||||
},
|
||||
{
|
||||
"module": "only-office"
|
||||
},
|
||||
{
|
||||
"module": "pacman",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "photos",
|
||||
"ports": {
|
||||
"9000": 20011
|
||||
},
|
||||
"bindings": {
|
||||
"mongodb-database": {
|
||||
"node": "novox",
|
||||
"module": "mongodb"
|
||||
},
|
||||
"s3-bucket": {
|
||||
"node": "novox",
|
||||
"module": "minio"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "postgres",
|
||||
"layer": {
|
||||
"ports": {
|
||||
"5432": 6852
|
||||
}
|
||||
},
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "power",
|
||||
"has-mesh-wide": true,
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "powerlevel10k"
|
||||
},
|
||||
{
|
||||
"module": "records",
|
||||
"has-mesh-wide": true,
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "restic",
|
||||
"layer": {
|
||||
"places": {
|
||||
"repository": "/home/restic"
|
||||
}
|
||||
},
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "route-proxy"
|
||||
},
|
||||
{
|
||||
"module": "ssh-client"
|
||||
},
|
||||
{
|
||||
"module": "sshd"
|
||||
},
|
||||
{
|
||||
"module": "step-ca"
|
||||
},
|
||||
{
|
||||
"module": "sudo"
|
||||
},
|
||||
{
|
||||
"module": "systemd"
|
||||
},
|
||||
{
|
||||
"module": "systemd-networkd"
|
||||
},
|
||||
{
|
||||
"module": "telegram"
|
||||
},
|
||||
{
|
||||
"module": "time-sync"
|
||||
},
|
||||
{
|
||||
"module": "umami",
|
||||
"bindings": {
|
||||
"postgres-database": {
|
||||
"node": "novox",
|
||||
"module": "postgres"
|
||||
},
|
||||
"secret": {
|
||||
"node": "novox",
|
||||
"module": "mesh-vault"
|
||||
}
|
||||
},
|
||||
"layer": {
|
||||
"expose": {
|
||||
"3000": "mesh"
|
||||
},
|
||||
"ports": {
|
||||
"3000": 1212
|
||||
}
|
||||
},
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "website",
|
||||
"ports": {
|
||||
"8080": 20012
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "zsh",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "zsh-autosuggestions"
|
||||
},
|
||||
{
|
||||
"module": "zsh-syntax-highlighting"
|
||||
}
|
||||
],
|
||||
"holds": [
|
||||
{
|
||||
"claim": "git",
|
||||
"scope": "mesh",
|
||||
"module": "gitea",
|
||||
"site": "hosting"
|
||||
},
|
||||
{
|
||||
"claim": "mesh-artifact-store",
|
||||
"scope": "mesh",
|
||||
"module": "distribution",
|
||||
"site": "hosting"
|
||||
},
|
||||
{
|
||||
"claim": "mesh-broker",
|
||||
"scope": "mesh",
|
||||
"module": "nats",
|
||||
"site": "hosting"
|
||||
},
|
||||
{
|
||||
"claim": "mesh-catalog",
|
||||
"scope": "mesh",
|
||||
"module": "mesh-catalog",
|
||||
"site": "hosting"
|
||||
},
|
||||
{
|
||||
"claim": "mesh-config",
|
||||
"scope": "mesh",
|
||||
"module": "mesh-config",
|
||||
"site": "hosting"
|
||||
},
|
||||
{
|
||||
"claim": "mesh-controller",
|
||||
"scope": "mesh",
|
||||
"module": "mesh-controller",
|
||||
"site": "hosting"
|
||||
},
|
||||
{
|
||||
"claim": "mesh-delivery",
|
||||
"scope": "mesh",
|
||||
"module": "mesh-delivery-gate",
|
||||
"site": "hosting"
|
||||
},
|
||||
{
|
||||
"claim": "mesh-dns-resolver",
|
||||
"scope": "mesh",
|
||||
"module": "dnsmasq",
|
||||
"site": "hosting"
|
||||
},
|
||||
{
|
||||
"claim": "mesh-provisioner",
|
||||
"scope": "mesh",
|
||||
"module": "mesh-provisioner",
|
||||
"site": "hosting"
|
||||
},
|
||||
{
|
||||
"claim": "mesh-store",
|
||||
"scope": "mesh",
|
||||
"module": "postgres",
|
||||
"site": "hosting"
|
||||
},
|
||||
{
|
||||
"claim": "mesh-vault",
|
||||
"scope": "mesh",
|
||||
"module": "mesh-vault",
|
||||
"site": "hosting"
|
||||
},
|
||||
{
|
||||
"claim": "npm-package-registry",
|
||||
"scope": "mesh",
|
||||
"module": "gitea",
|
||||
"site": "hosting"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
{
|
||||
"v": 1,
|
||||
"node": "shanks",
|
||||
"decided": {
|
||||
"site": "home",
|
||||
"address": "10.10.0.3",
|
||||
"account": "jochen",
|
||||
"tunnel": {
|
||||
"interface": "wg0",
|
||||
"unit": "wg-quick@wg0",
|
||||
"config": "/etc/wireguard/wg0.conf",
|
||||
"port": 51820,
|
||||
"address": "10.10.0.3/24",
|
||||
"range": "10.10.0.0/24",
|
||||
"public-key": "XT1l25Z0/YhRNQ8FQM+9XsAp7chx5MI0eLOOg1SUEXA="
|
||||
}
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"module": "adwaita"
|
||||
},
|
||||
{
|
||||
"module": "avahi"
|
||||
},
|
||||
{
|
||||
"module": "blueman"
|
||||
},
|
||||
{
|
||||
"module": "bluetooth"
|
||||
},
|
||||
{
|
||||
"module": "brother",
|
||||
"bindings": {
|
||||
"s3-bucket": {
|
||||
"node": "novox",
|
||||
"module": "minio"
|
||||
}
|
||||
},
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "buildah"
|
||||
},
|
||||
{
|
||||
"module": "ca-trust"
|
||||
},
|
||||
{
|
||||
"module": "claude-code",
|
||||
"has-mesh-wide": true,
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "clipmenu"
|
||||
},
|
||||
{
|
||||
"module": "cups"
|
||||
},
|
||||
{
|
||||
"module": "dbus"
|
||||
},
|
||||
{
|
||||
"module": "dev-tools"
|
||||
},
|
||||
{
|
||||
"module": "disk-load"
|
||||
},
|
||||
{
|
||||
"module": "dmenu"
|
||||
},
|
||||
{
|
||||
"module": "docker"
|
||||
},
|
||||
{
|
||||
"module": "docker-compose"
|
||||
},
|
||||
{
|
||||
"module": "dunst"
|
||||
},
|
||||
{
|
||||
"module": "fail2ban"
|
||||
},
|
||||
{
|
||||
"module": "feh"
|
||||
},
|
||||
{
|
||||
"module": "flatpak"
|
||||
},
|
||||
{
|
||||
"module": "fonts"
|
||||
},
|
||||
{
|
||||
"module": "forticlient"
|
||||
},
|
||||
{
|
||||
"module": "gnome-keyring"
|
||||
},
|
||||
{
|
||||
"module": "go"
|
||||
},
|
||||
{
|
||||
"module": "hostname",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "i3"
|
||||
},
|
||||
{
|
||||
"module": "i3status-rust"
|
||||
},
|
||||
{
|
||||
"module": "kernel",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "lab",
|
||||
"bindings": {
|
||||
"npm-package-registry": {
|
||||
"node": "novox",
|
||||
"module": "gitea"
|
||||
}
|
||||
},
|
||||
"has-mesh-wide": true,
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "lemurs"
|
||||
},
|
||||
{
|
||||
"module": "localization"
|
||||
},
|
||||
{
|
||||
"module": "logrotate"
|
||||
},
|
||||
{
|
||||
"module": "mesh-build-go",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "mesh-build-image"
|
||||
},
|
||||
{
|
||||
"module": "mesh-build-node",
|
||||
"bindings": {
|
||||
"npm-package-registry": {
|
||||
"node": "novox",
|
||||
"module": "gitea"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"module": "mesh-cli"
|
||||
},
|
||||
{
|
||||
"module": "mesh-delivery-builder"
|
||||
},
|
||||
{
|
||||
"module": "mesh-delivery-view"
|
||||
},
|
||||
{
|
||||
"module": "mesh-files"
|
||||
},
|
||||
{
|
||||
"module": "mesh-host",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "mesh-mail-notifier"
|
||||
},
|
||||
{
|
||||
"module": "mesh-provisioner-client"
|
||||
},
|
||||
{
|
||||
"module": "mesh-reads"
|
||||
},
|
||||
{
|
||||
"module": "mesh-wireguard"
|
||||
},
|
||||
{
|
||||
"module": "mounts",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "netcheck"
|
||||
},
|
||||
{
|
||||
"module": "networkmanager"
|
||||
},
|
||||
{
|
||||
"module": "nextcloud-client"
|
||||
},
|
||||
{
|
||||
"module": "nftables"
|
||||
},
|
||||
{
|
||||
"module": "nm-applet"
|
||||
},
|
||||
{
|
||||
"module": "node-env"
|
||||
},
|
||||
{
|
||||
"module": "node-tools"
|
||||
},
|
||||
{
|
||||
"module": "nodejs"
|
||||
},
|
||||
{
|
||||
"module": "openrazer"
|
||||
},
|
||||
{
|
||||
"module": "pacman",
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "picom"
|
||||
},
|
||||
{
|
||||
"module": "polychromatic"
|
||||
},
|
||||
{
|
||||
"module": "power",
|
||||
"has-mesh-wide": true,
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "powerlevel10k"
|
||||
},
|
||||
{
|
||||
"module": "restic"
|
||||
},
|
||||
{
|
||||
"module": "rofi"
|
||||
},
|
||||
{
|
||||
"module": "screen-lock"
|
||||
},
|
||||
{
|
||||
"module": "slack"
|
||||
},
|
||||
{
|
||||
"module": "snapd"
|
||||
},
|
||||
{
|
||||
"module": "ssh-client"
|
||||
},
|
||||
{
|
||||
"module": "sshd"
|
||||
},
|
||||
{
|
||||
"module": "sudo"
|
||||
},
|
||||
{
|
||||
"module": "systemd"
|
||||
},
|
||||
{
|
||||
"module": "systemd-resolved"
|
||||
},
|
||||
{
|
||||
"module": "time-sync"
|
||||
},
|
||||
{
|
||||
"module": "xclip"
|
||||
},
|
||||
{
|
||||
"module": "xdg",
|
||||
"has-mesh-wide": true,
|
||||
"has-layer": true
|
||||
},
|
||||
{
|
||||
"module": "xorg"
|
||||
},
|
||||
{
|
||||
"module": "xterm"
|
||||
},
|
||||
{
|
||||
"module": "zsh"
|
||||
},
|
||||
{
|
||||
"module": "zsh-autosuggestions"
|
||||
},
|
||||
{
|
||||
"module": "zsh-syntax-highlighting"
|
||||
}
|
||||
],
|
||||
"pins": {
|
||||
"wildcard-resolution": {
|
||||
"node": "novox",
|
||||
"module": "dnsmasq"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user