Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2136ec0c9 | ||
|
|
f0fd53281b | ||
|
|
fb039250f6 | ||
|
|
c51a46b802 | ||
|
|
15a5fe09b6 | ||
|
|
3ded9e7800 | ||
|
|
e7aaa8420e | ||
|
|
22381d7522 |
@@ -103,6 +103,33 @@ func TestUnitLevelNamesTheFailingTestAndFailsTheLevel(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 1921: a fixture with a failing test in each of two packages: one level's run names both, each with its own
|
||||
// cause, and counts every test, the passing one beside a failure too. go test runs every package to its end; nothing
|
||||
// stops at the first failure.
|
||||
func TestUnitLevelNamesEveryFailingTestOfEveryPackage(t *testing.T) {
|
||||
m, states, ws, calls := workspaceFor(t, "twofailing")
|
||||
res, st := runUnit(t, m, states, ws)
|
||||
if st.State != delivery.StepPassed || res.Passed {
|
||||
t.Fatalf("step %s (%s), result %+v: want the step done and the level failed", st.State, st.Reason, res)
|
||||
}
|
||||
var got []string
|
||||
for _, f := range res.Failed {
|
||||
got = append(got, f.Package+"."+f.Test+": "+f.Cause)
|
||||
}
|
||||
want := []string{"fixture/twofailing/one.TestOneIsTwo: One() = 1, want 2", "fixture/twofailing/two.TestTwoIsThree: Two() = 2, want 3"}
|
||||
if strings.Join(got, "\n") != strings.Join(want, "\n") {
|
||||
t.Errorf("failed\n%s\nwant\n%s", strings.Join(got, "\n"), strings.Join(want, "\n"))
|
||||
}
|
||||
if res.Tests != 3 {
|
||||
t.Errorf("%d tests counted, want 3", res.Tests)
|
||||
}
|
||||
for _, c := range *calls {
|
||||
if strings.Contains(c.line, " test ") && strings.Contains(c.line, "-failfast") {
|
||||
t.Errorf("go test ran with -failfast: %s", c.line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1290/AC2: a fixture whose tests take more than the bound (lowered by the module's option, not by waiting five
|
||||
// seconds): the level fails naming the bound and the slowest tests; under the real bound the same run passes.
|
||||
func TestUnitLevelOverItsBoundFailsNamingTheBoundAndTheSlowestTests(t *testing.T) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
Each file is the standard output of `GOTOOLCHAIN=go1.27.1 GOFLAGS=-mod=mod go test -count=1 -json ./...` (go1.27.1)
|
||||
run in the fixture module of the same name under ../repos, captured on 2026-10-20 for issue 1290. The unit
|
||||
tests feed them to the parser through a fake runner, so no test here runs `go test`.
|
||||
tests feed them to the parser through a fake runner, so no test here runs `go test`. twofailing.json was captured the
|
||||
same way on 2026-10-24 for issue 1921: two packages with a failing test each, and a passing test beside one of them.
|
||||
|
||||
golist-external.json is `GOTOOLCHAIN=go1.27.1 GOFLAGS=-mod=mod go list -json=ImportPath,Standard,DepOnly,Module
|
||||
git.novox.be/novox/mesh-sdk/go/buildstep` run in this module (go1.27.1, 2026-10-20 for issue 1291): a package of a module
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{"Time":"2026-10-24T23:45:37.721718705+02:00","Action":"start","Package":"fixture/twofailing/one"}
|
||||
{"Time":"2026-10-24T23:45:37.723685152+02:00","Action":"run","Package":"fixture/twofailing/one","Test":"TestOneIsTwo"}
|
||||
{"Time":"2026-10-24T23:45:37.723726472+02:00","Action":"output","Package":"fixture/twofailing/one","Test":"TestOneIsTwo","Output":"=== RUN TestOneIsTwo\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.723743682+02:00","Action":"output","Package":"fixture/twofailing/one","Test":"TestOneIsTwo","Output":" one_test.go:6: checking One\n"}
|
||||
{"Time":"2026-10-24T23:45:37.723750622+02:00","Action":"output","Package":"fixture/twofailing/one","Test":"TestOneIsTwo","Output":" one_test.go:8: One() = 1, want 2\n","OutputType":"error"}
|
||||
{"Time":"2026-10-24T23:45:37.723759302+02:00","Action":"output","Package":"fixture/twofailing/one","Test":"TestOneIsTwo","Output":"--- FAIL: TestOneIsTwo (0.00s)\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.723889303+02:00","Action":"start","Package":"fixture/twofailing/two"}
|
||||
{"Time":"2026-10-24T23:45:37.723762972+02:00","Action":"fail","Package":"fixture/twofailing/one","Test":"TestOneIsTwo","Elapsed":0}
|
||||
{"Time":"2026-10-24T23:45:37.723952274+02:00","Action":"run","Package":"fixture/twofailing/one","Test":"TestOneIsOne"}
|
||||
{"Time":"2026-10-24T23:45:37.723955484+02:00","Action":"output","Package":"fixture/twofailing/one","Test":"TestOneIsOne","Output":"=== RUN TestOneIsOne\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.723959304+02:00","Action":"output","Package":"fixture/twofailing/one","Test":"TestOneIsOne","Output":"--- PASS: TestOneIsOne (0.00s)\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.723964914+02:00","Action":"pass","Package":"fixture/twofailing/one","Test":"TestOneIsOne","Elapsed":0}
|
||||
{"Time":"2026-10-24T23:45:37.723967704+02:00","Action":"output","Package":"fixture/twofailing/one","Output":"FAIL\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.724225126+02:00","Action":"output","Package":"fixture/twofailing/one","Output":"FAIL\tfixture/twofailing/one\t0.002s\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.724231936+02:00","Action":"fail","Package":"fixture/twofailing/one","Elapsed":0.003}
|
||||
{"Time":"2026-10-24T23:45:37.725752949+02:00","Action":"run","Package":"fixture/twofailing/two","Test":"TestTwoIsThree"}
|
||||
{"Time":"2026-10-24T23:45:37.725796309+02:00","Action":"output","Package":"fixture/twofailing/two","Test":"TestTwoIsThree","Output":"=== RUN TestTwoIsThree\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.725808239+02:00","Action":"output","Package":"fixture/twofailing/two","Test":"TestTwoIsThree","Output":" two_test.go:7: Two() = 2, want 3\n","OutputType":"error"}
|
||||
{"Time":"2026-10-24T23:45:37.72581671+02:00","Action":"output","Package":"fixture/twofailing/two","Test":"TestTwoIsThree","Output":"--- FAIL: TestTwoIsThree (0.00s)\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.72582026+02:00","Action":"fail","Package":"fixture/twofailing/two","Test":"TestTwoIsThree","Elapsed":0}
|
||||
{"Time":"2026-10-24T23:45:37.72582549+02:00","Action":"output","Package":"fixture/twofailing/two","Output":"FAIL\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.726319344+02:00","Action":"output","Package":"fixture/twofailing/two","Output":"FAIL\tfixture/twofailing/two\t0.002s\n","OutputType":"frame"}
|
||||
{"Time":"2026-10-24T23:45:37.726362544+02:00","Action":"fail","Package":"fixture/twofailing/two","Elapsed":0.002}
|
||||
@@ -0,0 +1,3 @@
|
||||
module fixture/twofailing
|
||||
|
||||
go 1.25.0
|
||||
@@ -0,0 +1,3 @@
|
||||
package one
|
||||
|
||||
func One() int { return 1 }
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package one
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestOneIsTwo(t *testing.T) {
|
||||
t.Log("checking One")
|
||||
if One() != 2 {
|
||||
t.Fatalf("One() = %d, want 2", One())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOneIsOne(t *testing.T) {
|
||||
if One() != 1 {
|
||||
t.Fatal("one")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package two
|
||||
|
||||
func Two() int { return 2 }
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package two
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTwoIsThree(t *testing.T) {
|
||||
if Two() != 3 {
|
||||
t.Errorf("Two() = %d, want 3", Two())
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ module meshbuildgo
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025175617-518dae5ec9c8
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026095702-79649c3fa9f4
|
||||
github.com/distribution/distribution/v3 v3.1.2
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025175617-518dae5ec9c8 h1:x4mvY7yiGTP5lnzxbiwyj1o24p05hAmzZP7xHg3XX2M=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025175617-518dae5ec9c8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026095702-79649c3fa9f4 h1:05sVel/UOvz4/g0dxrDOeVUL7BGakVrxPBlqRq02ZhM=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026095702-79649c3fa9f4/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
|
||||
@@ -3,7 +3,7 @@ module meshbuildnode
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025175617-518dae5ec9c8
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026095702-79649c3fa9f4
|
||||
github.com/distribution/distribution/v3 v3.1.2
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025175617-518dae5ec9c8 h1:x4mvY7yiGTP5lnzxbiwyj1o24p05hAmzZP7xHg3XX2M=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261025175617-518dae5ec9c8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026095702-79649c3fa9f4 h1:05sVel/UOvz4/g0dxrDOeVUL7BGakVrxPBlqRq02ZhM=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026095702-79649c3fa9f4/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/mesh-vault
|
||||
@@ -14,8 +14,8 @@ package main
|
||||
// bus refused is kept as owed and tried again at the next minute pass, never lost silently.
|
||||
//
|
||||
// The hashes of accounts the vault holds no value for (enrolment users, persons, the controller's own) are adopted once,
|
||||
// before take-over, from the list the bus loaded (clause 9's adoption): such a key is marked adopted and is replaced by
|
||||
// the vault's own hash the moment the vault holds the account's value, never the other way round.
|
||||
// before take-over, from the list the bus loaded (clause 9's adoption, adopt.go): such a key is marked adopted and is
|
||||
// replaced by the vault's own hash the moment the vault holds the account's value, never the other way round.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -42,19 +42,25 @@ type AccountHash struct {
|
||||
User string `json:"user"`
|
||||
// Hash is the bcrypt hash of the account's password, at the composer's cost.
|
||||
Hash string `json:"hash"`
|
||||
// Secret, Generation and Fingerprint are the vault's own secret the hash is of: absent for an adopted one.
|
||||
Secret string `json:"secret,omitempty"`
|
||||
// Of, Generation and Fingerprint name the vault's own secret the hash is of: absent for an adopted one. The field
|
||||
// is not called "secret": the runtime refuses a state field whose name says it is a credential (ADR 0201).
|
||||
Of string `json:"of,omitempty"`
|
||||
Generation int `json:"generation,omitempty"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
At time.Time `json:"at"`
|
||||
// Adopted marks a hash taken from the list the bus loaded, for an account the vault holds no value for.
|
||||
Adopted bool `json:"adopted,omitempty"`
|
||||
// Adopted marks a hash taken from the list the bus loaded, for an account the vault holds no value for; From is
|
||||
// that list's digest and Node the node that loaded it and handed it (adopt.go).
|
||||
Adopted bool `json:"adopted,omitempty"`
|
||||
From string `json:"from,omitempty"`
|
||||
Node string `json:"node,omitempty"`
|
||||
}
|
||||
|
||||
// AccountStore is the vault's state of bus-account hashes: read and written by the vault's instances alone.
|
||||
type AccountStore interface {
|
||||
Get(user string) (*AccountHash, error)
|
||||
Put(a AccountHash) error
|
||||
// Users is every user with a key.
|
||||
Users() ([]string, error)
|
||||
}
|
||||
|
||||
// busAccounts is AccountStore over the vault's own state through its runtime.
|
||||
@@ -72,6 +78,8 @@ func (busAccounts) Get(user string) (*AccountHash, error) {
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
func (busAccounts) Users() ([]string, error) { return stdio.State(BusAccountsState).Keys() }
|
||||
|
||||
func (busAccounts) Put(a AccountHash) error {
|
||||
_, err := stdio.State(BusAccountsState).Put(a.User, a)
|
||||
return err
|
||||
@@ -149,7 +157,7 @@ func accountOf(secret, value string) (user, password string, err error) {
|
||||
|
||||
// Hashed is what one hashing did: the key written or found, never the hash.
|
||||
type Hashed struct {
|
||||
Secret string `json:"secret"`
|
||||
Of string `json:"of"`
|
||||
User string `json:"user"`
|
||||
Generation int `json:"generation"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
@@ -178,13 +186,13 @@ func (v *Vault) HashAccount(secret string, force bool) (Hashed, error) {
|
||||
return Hashed{}, err
|
||||
}
|
||||
fp := Fingerprint(value)
|
||||
h := Hashed{Secret: secret, User: user, Generation: rec.Generation, Fingerprint: fp}
|
||||
h := Hashed{Of: secret, User: user, Generation: rec.Generation, Fingerprint: fp}
|
||||
was, err := v.Accounts.Get(user)
|
||||
if err != nil {
|
||||
v.hashes.owe(secret, "its key could not be read: "+err.Error())
|
||||
return Hashed{}, fmt.Errorf("%s %s could not be read: %w", BusAccountsState, user, err)
|
||||
}
|
||||
if !force && was != nil && !was.Adopted && was.Secret == secret && was.Generation == rec.Generation && was.Fingerprint == fp {
|
||||
if !force && was != nil && !was.Adopted && was.Of == secret && was.Generation == rec.Generation && was.Fingerprint == fp {
|
||||
v.hashes.paid(secret)
|
||||
h.At = was.At
|
||||
return h, nil
|
||||
@@ -194,7 +202,7 @@ func (v *Vault) HashAccount(secret string, force bool) (Hashed, error) {
|
||||
return Hashed{}, err
|
||||
}
|
||||
h.At = v.Now().UTC()
|
||||
if err := v.Accounts.Put(AccountHash{User: user, Hash: string(hash), Secret: secret, Generation: rec.Generation,
|
||||
if err := v.Accounts.Put(AccountHash{User: user, Hash: string(hash), Of: secret, Generation: rec.Generation,
|
||||
Fingerprint: fp, At: h.At}); err != nil {
|
||||
v.hashes.owe(secret, "its key could not be written: "+err.Error())
|
||||
return Hashed{}, fmt.Errorf("the hash of %s (generation %d) could not be written to %s %s, tried again at the "+
|
||||
@@ -203,7 +211,7 @@ func (v *Vault) HashAccount(secret string, force bool) (Hashed, error) {
|
||||
v.hashes.paid(secret)
|
||||
h.Written = true
|
||||
if v.Emit != nil {
|
||||
if err := v.Emit(AccountHashedEvent, map[string]any{"user": user, "secret": secret, "generation": rec.Generation,
|
||||
if err := v.Emit(AccountHashedEvent, map[string]any{"user": user, "of": secret, "generation": rec.Generation,
|
||||
"fingerprint": fp, "at": h.At.Format(time.RFC3339Nano)}); err != nil {
|
||||
v.log("the hash of %s is written, but %s could not be said: %v", secret, AccountHashedEvent, err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
package main
|
||||
|
||||
// The vault adopts the hashes of the bus accounts it holds no value for (novox/hq ADR 0547 clause 9, issue 1945).
|
||||
//
|
||||
// The controller's own account, the node accounts and the enrolled devices were never handed to the vault, so it cannot
|
||||
// hash them, and the grants part's list would leave them out. The one part that has their hashes is the node-engine of
|
||||
// the node holding mesh-broker, in the list the bus loaded: it hands every loaded user's hash in the bus module's have,
|
||||
// sealed to this vault's key (mesh-sdk delivery.AccountsHanded), which it learns from the vault's answer to node-key.
|
||||
// The vault reads that have (its manifest `reads` node-engine.have), as a watch of the bus module's have and again by
|
||||
// hand with the tool `account_adopt`, and opens it with its own key.
|
||||
//
|
||||
// Of the users handed it takes only those it holds no value for: a user whose covered account it holds, or whose key in
|
||||
// bus-accounts is its own hash, is never touched (the hash it made from the value is the one that holds). Each other
|
||||
// user's hash is written to bus-accounts.<user> marked adopted, with the list it came from, saying account-hashed; an
|
||||
// adopted key is written again only when the loaded list carries another hash for the user. A handing from any node but
|
||||
// the one the assignments name as holding the bus module is refused: a node-engine writes only its own node's haves, so
|
||||
// that node is the one that loaded the list.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// HaveRead is the node-engines' have bucket as the vault reads it (its manifest's `reads`).
|
||||
const HaveRead = "node-engine.have"
|
||||
|
||||
// BusModule is the module holding mesh-broker, whose have on its node carries the handing.
|
||||
const BusModule = "nats"
|
||||
|
||||
// HaveReader reads one key of the have bucket: nil when there is none.
|
||||
type HaveReader func(key string) ([]byte, error)
|
||||
|
||||
// Adoption is what one adoption did: never a hash.
|
||||
type Adoption struct {
|
||||
Node string `json:"node"`
|
||||
// From is the digest of the loaded list the handing is of; Handed how many users it holds.
|
||||
From string `json:"from,omitempty"`
|
||||
Handed int `json:"handed"`
|
||||
// Adopted are the users whose hash was written now; Same those adopted before with the same hash; Own those the
|
||||
// vault holds a value for, never adopted.
|
||||
Adopted []string `json:"adopted"`
|
||||
Same int `json:"same"`
|
||||
Own int `json:"own"`
|
||||
Refused map[string]string `json:"refused,omitempty"`
|
||||
At time.Time `json:"at"`
|
||||
By string `json:"by"`
|
||||
}
|
||||
|
||||
// adoptionKept is what the vault keeps of its last adoption, for its tools and to skip a have that says the same.
|
||||
type adoptionKept struct {
|
||||
last *Adoption
|
||||
handed string // the From and To of the handing last taken in a complete pass
|
||||
failed string // why the last look found nothing to adopt, or ""
|
||||
}
|
||||
|
||||
// busNode is the node the assignments name as holding the bus module: exactly one.
|
||||
func (v *Vault) busNode() (string, error) {
|
||||
if v.Assignments.Keys == nil {
|
||||
return "", errors.New("the vault cannot read the assignments, so it cannot tell which node holds the bus")
|
||||
}
|
||||
keys, err := v.Assignments.Keys()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("the assignments' keys could not be read: %w", err)
|
||||
}
|
||||
var nodes []string
|
||||
for _, k := range keys {
|
||||
if node, module, ok := strings.Cut(k, "."); ok && module == BusModule {
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
}
|
||||
sort.Strings(nodes)
|
||||
switch len(nodes) {
|
||||
case 1:
|
||||
return nodes[0], nil
|
||||
case 0:
|
||||
return "", fmt.Errorf("no node is assigned the bus module %s", BusModule)
|
||||
}
|
||||
return "", fmt.Errorf("%d nodes are assigned the bus module %s (%s): the vault adopts from one alone", len(nodes),
|
||||
BusModule, strings.Join(nodes, ", "))
|
||||
}
|
||||
|
||||
// AdoptNow is the tool account_adopt: the bus node's have read now and adopted, though it says the same as before.
|
||||
func (v *Vault) AdoptNow(caller string) (Adoption, error) {
|
||||
if v.Haves == nil {
|
||||
return Adoption{}, errors.New("the vault cannot read the have bucket (" + HaveRead + ")")
|
||||
}
|
||||
node, err := v.busNode()
|
||||
if err != nil {
|
||||
return Adoption{}, err
|
||||
}
|
||||
raw, err := v.Haves(node + "." + BusModule)
|
||||
if err != nil {
|
||||
return Adoption{}, fmt.Errorf("%s %s.%s could not be read: %w", HaveRead, node, BusModule, err)
|
||||
}
|
||||
if raw == nil {
|
||||
return Adoption{}, fmt.Errorf("%s holds no have of %s on %s", HaveRead, BusModule, node)
|
||||
}
|
||||
return v.AdoptFrom(node, raw, someoneOr(caller, "a tool"), true)
|
||||
}
|
||||
|
||||
// AdoptFrom adopts from the have of the bus module on node: unless force, nothing when it hands what the last complete
|
||||
// adoption took.
|
||||
func (v *Vault) AdoptFrom(node string, raw []byte, by string, force bool) (Adoption, error) {
|
||||
a, err := v.adopt(node, raw, by, force)
|
||||
v.mu.Lock()
|
||||
switch {
|
||||
case err != nil:
|
||||
v.adoption.failed = err.Error()
|
||||
case a.At.IsZero():
|
||||
default:
|
||||
v.adoption.failed = ""
|
||||
v.adoption.last = &a
|
||||
}
|
||||
v.mu.Unlock()
|
||||
if err == nil && len(a.Adopted) > 0 {
|
||||
v.log("adopted the hashes of %d bus accounts it holds no value for from %s's loaded list %s (%s), asked by %s",
|
||||
len(a.Adopted), node, a.From, strings.Join(a.Adopted, ", "), by)
|
||||
}
|
||||
if len(a.Refused) > 0 {
|
||||
v.log("ADOPTION REFUSED %d of the users %s handed: %v", len(a.Refused), node, a.Refused)
|
||||
}
|
||||
return a, err
|
||||
}
|
||||
|
||||
func (v *Vault) adopt(node string, raw []byte, by string, force bool) (Adoption, error) {
|
||||
if v.Accounts == nil {
|
||||
return Adoption{}, errors.New("the vault has no state of bus accounts to write to")
|
||||
}
|
||||
bus, err := v.busNode()
|
||||
if err != nil {
|
||||
return Adoption{}, err
|
||||
}
|
||||
if node != bus {
|
||||
return Adoption{}, fmt.Errorf("%s does not hold the bus (the assignments name %s): its handing is not adopted",
|
||||
node, bus)
|
||||
}
|
||||
have, err := delivery.DecodeHave(raw)
|
||||
if err != nil {
|
||||
return Adoption{}, fmt.Errorf("the have of %s on %s is not a have: %w", BusModule, node, err)
|
||||
}
|
||||
if have.UserList == nil || have.UserList.Handed == nil {
|
||||
return Adoption{}, fmt.Errorf("the have of %s on %s hands no hashes yet (its node-engine hands them once it "+
|
||||
"heard the vault's key in its answer to node-key, and writes the list)", BusModule, node)
|
||||
}
|
||||
h := *have.UserList.Handed
|
||||
handed := h.From + " " + h.To
|
||||
v.mu.Lock()
|
||||
seen := v.adoption.handed == handed
|
||||
v.mu.Unlock()
|
||||
if seen && !force {
|
||||
return Adoption{}, nil
|
||||
}
|
||||
accounts, err := delivery.OpenHanded(h, v.Own.PublicKey, v.Own.PrivateKey)
|
||||
if err != nil {
|
||||
return Adoption{}, err
|
||||
}
|
||||
|
||||
v.hashMu.Lock()
|
||||
defer v.hashMu.Unlock()
|
||||
held, err := v.heldUsers()
|
||||
if err != nil {
|
||||
return Adoption{}, err
|
||||
}
|
||||
a := Adoption{Node: node, From: h.From, Handed: len(accounts), Adopted: []string{}, At: v.Now().UTC(), By: by}
|
||||
refuse := func(user, why string) {
|
||||
if a.Refused == nil {
|
||||
a.Refused = map[string]string{}
|
||||
}
|
||||
a.Refused[user] = why
|
||||
}
|
||||
for _, acc := range accounts {
|
||||
if !delivery.IsBcrypt(acc.Hash) || !namePart.MatchString(acc.User) {
|
||||
refuse(acc.User, "not a bus user with a bcrypt hash")
|
||||
continue
|
||||
}
|
||||
if held[acc.User] {
|
||||
a.Own++
|
||||
continue
|
||||
}
|
||||
was, err := v.Accounts.Get(acc.User)
|
||||
switch {
|
||||
case err != nil:
|
||||
refuse(acc.User, "its key could not be read: "+err.Error())
|
||||
continue
|
||||
case was != nil && !was.Adopted:
|
||||
a.Own++
|
||||
continue
|
||||
case was != nil && was.Hash == acc.Hash:
|
||||
a.Same++
|
||||
continue
|
||||
}
|
||||
if err := v.Accounts.Put(AccountHash{User: acc.User, Hash: acc.Hash, At: a.At, Adopted: true, From: h.From,
|
||||
Node: node}); err != nil {
|
||||
refuse(acc.User, "its key could not be written: "+err.Error())
|
||||
continue
|
||||
}
|
||||
a.Adopted = append(a.Adopted, acc.User)
|
||||
if v.Emit != nil {
|
||||
if err := v.Emit(AccountHashedEvent, map[string]any{"user": acc.User, "adopted": true, "from": h.From,
|
||||
"node": node, "at": a.At.Format(time.RFC3339Nano)}); err != nil {
|
||||
v.log("the adopted hash of %s is written, but %s could not be said: %v", acc.User, AccountHashedEvent, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// A complete pass, refusals and all: the same handing is not opened again at the next change of the have (a
|
||||
// refused user stays refused until the list changes, and the last adoption names it).
|
||||
v.mu.Lock()
|
||||
v.adoption.handed = handed
|
||||
v.mu.Unlock()
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// heldUsers is the bus user of every covered account the vault holds a value for.
|
||||
func (v *Vault) heldUsers() (map[string]bool, error) {
|
||||
records, err := v.Store.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]bool{}
|
||||
for _, r := range records {
|
||||
if !Covered(r.Name) {
|
||||
continue
|
||||
}
|
||||
value, err := openAtRest(r, v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the vault could not open its copy of %s, so it cannot tell whose value it holds: %w",
|
||||
r.Name, err)
|
||||
}
|
||||
if user, _, err := accountOf(r.Name, value); err == nil {
|
||||
out[user] = true
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// adoptedRows is every adopted key of bus-accounts, for secret_own: never a hash.
|
||||
func (v *Vault) adoptedRows() (rows []map[string]any, err error) {
|
||||
if v.Accounts == nil {
|
||||
return nil, nil
|
||||
}
|
||||
users, err := v.Accounts.Users()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sort.Strings(users)
|
||||
for _, u := range users {
|
||||
a, err := v.Accounts.Get(u)
|
||||
if err != nil || a == nil || !a.Adopted {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, map[string]any{"user": a.User, "adoptedAt": a.At, "from": a.From, "node": a.Node})
|
||||
if len(rows) >= ownRowsMax {
|
||||
break
|
||||
}
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// adoptionSaid is the last adoption and why the last look adopted nothing, for the tools.
|
||||
func (v *Vault) adoptionSaid() map[string]any {
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
out := map[string]any{"last": v.adoption.last}
|
||||
if v.adoption.last != nil && len(v.adoption.last.Refused) > 0 {
|
||||
out["refused"] = v.adoption.last.Refused
|
||||
}
|
||||
if v.adoption.failed != "" {
|
||||
out["why"] = v.adoption.failed
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// credentialEndings and credentialField mirror the runtime's refusal of a state or log value with a field whose name says
|
||||
// it is a credential (mesh-tools node-tools/internal/bus/state.go, credentialField, ADR 0201): the runtime's check is
|
||||
// unexported and in another repository, so this is a copy and must follow it. A fake state that skips it let a
|
||||
// field named "secret" pass every test and be refused on novox (issue 1945).
|
||||
var credentialEndings = []string{"password", "passwd", "secret", "token", "credential", "credentials",
|
||||
"authorization", "apikey", "privatekey", "accesskey", "cookie"}
|
||||
|
||||
func credentialField(value []byte) string {
|
||||
var v any
|
||||
if json.Unmarshal(value, &v) != nil {
|
||||
return ""
|
||||
}
|
||||
var walk func(any) string
|
||||
walk = func(v any) string {
|
||||
switch t := v.(type) {
|
||||
case map[string]any:
|
||||
for k, x := range t {
|
||||
norm := strings.NewReplacer("-", "", "_", "", " ", "").Replace(strings.ToLower(k))
|
||||
for _, end := range credentialEndings {
|
||||
if strings.HasSuffix(norm, end) {
|
||||
return k
|
||||
}
|
||||
}
|
||||
if f := walk(x); f != "" {
|
||||
return f
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
for _, x := range t {
|
||||
if f := walk(x); f != "" {
|
||||
return f
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return walk(v)
|
||||
}
|
||||
|
||||
// The vault's bus-accounts records pass the runtime's credential-field check; a record with a "secret" field does not.
|
||||
func TestStateRecordsPassTheRuntimesCredentialFieldCheck(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
value any
|
||||
field string
|
||||
}{
|
||||
{"a hash record of the vault's own secret", AccountHash{User: "ace.mesh-build-go", Hash: "$2a$10$h", Of: "own/ace/mesh-build-go/broker", Generation: 1, Fingerprint: "ab12"}, ""},
|
||||
{"an adopted hash record", AccountHash{User: "ace.x", Hash: "$2a$10$h", Adopted: true}, ""},
|
||||
{"a record with a secret field is refused", map[string]any{"user": "u", "secret": "own/x"}, "secret"},
|
||||
} {
|
||||
raw, err := json.Marshal(c.value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := credentialField(raw); got != c.field {
|
||||
t.Errorf("%s: credential field %q, want %q (%s)", c.name, got, c.field, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -36,11 +38,26 @@ func (f *fakeAccounts) Put(a AccountHash) error {
|
||||
if f.keys == nil {
|
||||
f.keys = map[string]AccountHash{}
|
||||
}
|
||||
// What the runtime does to every state value (credential_field_test.go): the vault's own record is checked as written.
|
||||
raw, _ := json.Marshal(a)
|
||||
if field := credentialField(raw); field != "" {
|
||||
return fmt.Errorf("%s carries a field %q, which names a credential: no secret is kept in state", a.User, field)
|
||||
}
|
||||
f.keys[a.User] = a
|
||||
f.puts++
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeAccounts) Users() ([]string, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
var out []string
|
||||
for u := range f.keys {
|
||||
out = append(out, u)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// hashOf says whether the key of user is the hash of password at generation, of the vault's own value.
|
||||
func (f *fakeAccounts) hashOf(user, password string, generation int) bool {
|
||||
f.mu.Lock()
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
@@ -50,10 +51,50 @@ 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{}}
|
||||
Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave}
|
||||
return env
|
||||
}
|
||||
|
||||
// readHave is one key of the node-engines' have bucket, read through the runtime under the vault's own account (its
|
||||
// manifest `reads` it), or nil when there is none.
|
||||
func readHave(key string) ([]byte, error) {
|
||||
e, err := stdio.State(HaveRead).Get(key)
|
||||
if err != nil || e == nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.Value, nil
|
||||
}
|
||||
|
||||
// watchHandings adopts from the bus module's have whenever it changes (adopt.go): a watch, never a poll. It answers
|
||||
// whether the watch began; one that did not is begun again at the next minute pass.
|
||||
func watchHandings(env *Env) bool {
|
||||
if env.Vault == nil {
|
||||
return true
|
||||
}
|
||||
// The have changes at every look of the bus module's health: why nothing was adopted is said once, and again when
|
||||
// it changes.
|
||||
var said string
|
||||
err := stdio.State(HaveRead).Watch("*."+BusModule, func(c stdio.StateChange) error {
|
||||
if c.Op == "delete" || len(c.Value) == 0 {
|
||||
return nil
|
||||
}
|
||||
node := strings.TrimSuffix(c.Key, "."+BusModule)
|
||||
_, err := env.Vault.AdoptFrom(node, c.Value, "the watch of "+HaveRead, false)
|
||||
if line := fmt.Sprint(err); err != nil && line != said {
|
||||
say("adopted nothing from %s: %v", c.Key, err)
|
||||
said = line
|
||||
} else if err == nil {
|
||||
said = ""
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
say("the bus module's have is not watched, so no hash is adopted; 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
|
||||
@@ -107,7 +148,11 @@ 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)
|
||||
}
|
||||
watched := watchHandings(env)
|
||||
for {
|
||||
if !watched {
|
||||
watched = watchHandings(env)
|
||||
}
|
||||
env.pass()
|
||||
if env.Vault != nil {
|
||||
env.Vault.HashOwed()
|
||||
|
||||
@@ -235,7 +235,8 @@ func definitions(env *Env) []toolDef {
|
||||
Description: "State this machine's sealing key to the vault, in its own name (ADR 0372, issue 786): from the " +
|
||||
"node-engine of the machine alone, at its start and whenever the key changes; the machine is the caller's, " +
|
||||
"as the bus proved it. A first or changed key is recorded and `secret-changed` is raised for the machine's " +
|
||||
"core secrets, so the controller asks again; the same key again changes nothing.",
|
||||
"core secrets, so the controller asks again; the same key again changes nothing. Answers the vault's own " +
|
||||
"public key (`vault_key`, SPKI PEM), which the bus node seals the loaded list's hashes to (ADR 0547 clause 9).",
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{
|
||||
"key": str("this machine's sealing key, a raw X25519 public key in base64, as move-in's sealing_key carries it")}},
|
||||
run: func(caller string, a map[string]any) (any, error) {
|
||||
@@ -246,7 +247,9 @@ func definitions(env *Env) []toolDef {
|
||||
if err := v.StateNodeKey(caller, arg(a, "key")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{"recorded": true}, nil
|
||||
// And the vault's own public key, which the bus node seals the loaded list's hashes to (ADR 0547
|
||||
// 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 + ".move-asks",
|
||||
Description: "Copy rows of the controller's record of asks at a desk into the vault's, as they are and once " +
|
||||
@@ -347,14 +350,40 @@ func definitions(env *Env) []toolDef {
|
||||
"(sha256:<hex>), when it was made and last changed, by whom, where the value came from, and whether it is a " +
|
||||
"covered bus account; for a bus account, its user and whether the hash of this generation is written in " +
|
||||
"`bus-accounts` (hashed, hashedAt, or why not). Narrow by node, module or name prefix. At most 1000 rows; " +
|
||||
"says when cut. Never a value or a hash.",
|
||||
"says when cut. Unnarrowed by module, also `adopted`: every account whose hash the vault adopted from the " +
|
||||
"bus's loaded list because it holds no value for it (user, adoptedAt, the list's digest, the node), and " +
|
||||
"`adoption`: the last adoption and why the last look adopted nothing. Never a value or a hash.",
|
||||
Input: map[string]any{"node": str("only this machine's secrets"), "module": str("only this module's secrets"),
|
||||
"name": str("only secrets whose name (the last part) starts with this, e.g. broker")},
|
||||
run: func(_ string, a map[string]any) (any, error) {
|
||||
if env.Vault == nil {
|
||||
return nil, errors.New("the vault is not ready")
|
||||
}
|
||||
return ownAnswer(env.Vault, arg(a, "node"), arg(a, "module"), arg(a, "name"))
|
||||
out, err := ownAnswer(env.Vault, arg(a, "node"), arg(a, "module"), arg(a, "name"))
|
||||
if err != nil || arg(a, "module") != "" {
|
||||
return out, err
|
||||
}
|
||||
adopted, err := env.Vault.adoptedRows()
|
||||
if err != nil {
|
||||
out["adoptedWhy"] = "bus-accounts could not be read: " + err.Error()
|
||||
}
|
||||
if adopted == nil {
|
||||
adopted = []map[string]any{}
|
||||
}
|
||||
out["adopted"], out["adoption"] = adopted, env.Vault.adoptionSaid()
|
||||
return out, nil
|
||||
}},
|
||||
{Name: "account_adopt",
|
||||
Description: "Adopt now the hashes of the bus accounts the vault holds no value for (ADR 0547 clause 9): read " +
|
||||
"the have of the bus module on the node holding it, open the loaded list's hashes its node-engine handed " +
|
||||
"sealed to the vault's key, and write each user's into `bus-accounts` marked adopted, saying " +
|
||||
"`account-hashed`. Never a user whose value the vault holds or whose key is its own hash. As the watch of " +
|
||||
"that have does, though the handing is the one adopted before. Answers the users adopted and counted, never a hash.",
|
||||
run: func(caller string, _ map[string]any) (any, error) {
|
||||
if env.Vault == nil {
|
||||
return nil, errors.New("the vault is not ready")
|
||||
}
|
||||
return env.Vault.AdoptNow(caller)
|
||||
}},
|
||||
{Name: "account_hash",
|
||||
Description: "Hash one covered bus account again now (ADR 0547 clause 9): the vault writes the bcrypt hash of " +
|
||||
@@ -761,8 +790,8 @@ func hashedRow(v *Vault, r Record, fp string, row map[string]any) {
|
||||
row["hashed"], row["hashWhy"] = false, "no key"
|
||||
case h.Adopted:
|
||||
row["hashed"], row["hashedAt"], row["hashWhy"] = false, h.At, "the key is adopted from the loaded list, not of this value"
|
||||
case h.Secret != r.Name || h.Generation != r.Generation || h.Fingerprint != fp:
|
||||
row["hashed"], row["hashedAt"], row["hashWhy"] = false, h.At, fmt.Sprintf("the key is of %s generation %d", h.Secret, h.Generation)
|
||||
case h.Of != r.Name || h.Generation != r.Generation || h.Fingerprint != fp:
|
||||
row["hashed"], row["hashedAt"], row["hashWhy"] = false, h.At, fmt.Sprintf("the key is of %s generation %d", h.Of, h.Generation)
|
||||
default:
|
||||
row["hashed"], row["hashedAt"] = true, h.At
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ package main
|
||||
// secret_verify — none returns a value. And the seat's verb `get`, reached the way the runtime calls it.
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
)
|
||||
|
||||
@@ -218,4 +220,113 @@ func TestSecretOwnListsFingerprintsOfOwnSecretsOnly(t *testing.T) {
|
||||
if _, err := hash.Run(map[string]any{"user": "ace.mesh-build-go"}); err == nil {
|
||||
t.Error("account_hash hashed a value that is no account")
|
||||
}
|
||||
|
||||
// ADR 0547 clause 9: the hashes of the accounts the vault holds no value for are adopted from the bus node's
|
||||
// handing, sealed to the vault's key (which node-key answers), never over a user whose value it holds, again only
|
||||
// when the loaded list carries another hash, and only from the node the assignments name as holding the bus.
|
||||
defs := definitions(&Env{Vault: v.Vault})
|
||||
answer, err := toolNamed(t, defs, Seat+".node-key").runAs("node.novox",
|
||||
map[string]any{"key": base64.StdEncoding.EncodeToString(make([]byte, 32))})
|
||||
if err != nil || answer.(map[string]any)["vault_key"] != v.Own.PublicKey {
|
||||
t.Fatalf("node-key answered %v, %v: no vault key", answer, err)
|
||||
}
|
||||
h := func(c string) string { return "$2a$11$" + strings.Repeat(c, 53) }
|
||||
handing := map[string]string{"controller": h("c"), "node.ace": h("n"), "g14.dunst": h("d")}
|
||||
sealedTo := v.Own.PublicKey
|
||||
have := func() []byte {
|
||||
var accounts []delivery.HandedAccount
|
||||
for _, u := range []string{"controller", "node.ace", "g14.dunst", "bad.user"} {
|
||||
if hash, ok := handing[u]; ok {
|
||||
accounts = append(accounts, delivery.HandedAccount{User: u, Hash: hash})
|
||||
}
|
||||
}
|
||||
// The list's digest, as the node says it: another list (another hash or user) is another digest.
|
||||
list, _ := json.Marshal(accounts)
|
||||
handed, err := delivery.HandAccounts(accounts, sealedTo, Fingerprint(string(list)), time.Now())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, _ := delivery.EncodeHave(delivery.Have{Phase: delivery.PhaseRunning, Health: delivery.HealthOK,
|
||||
UserList: &delivery.UserListWritten{Source: "k", Users: 3, Handed: &handed}})
|
||||
return raw
|
||||
}
|
||||
v.Assignments = Mirror{Keys: func() ([]string, error) { return []string{"novox.nats", "novox.mesh-vault", "ace.dunst"}, nil }}
|
||||
v.Haves = func(key string) ([]byte, error) {
|
||||
if key != "novox.nats" {
|
||||
return nil, nil
|
||||
}
|
||||
return have(), nil
|
||||
}
|
||||
dunst := acc.keys["g14.dunst"]
|
||||
adopt := toolNamed(t, defs, "account_adopt")
|
||||
for _, step := range []struct {
|
||||
name string
|
||||
change func()
|
||||
adopted []string
|
||||
same, own int
|
||||
wantErr, said string
|
||||
}{
|
||||
{name: "the users it holds no value for are adopted", adopted: []string{"controller", "node.ace"}, own: 1},
|
||||
{name: "again: the same hashes are kept", adopted: []string{}, same: 2, own: 1},
|
||||
{name: "another hash in the loaded list is adopted again", change: func() { handing["node.ace"] = h("m") },
|
||||
adopted: []string{"node.ace"}, same: 1, own: 1},
|
||||
{name: "a handing sealed to another key is refused", change: func() {
|
||||
other, _ := seal.GenerateKeyPair()
|
||||
sealedTo = other.PublicKey
|
||||
}, wantErr: "not to this one"},
|
||||
} {
|
||||
if step.change != nil {
|
||||
step.change()
|
||||
}
|
||||
before := len(v.bus.events)
|
||||
got, err := adopt.Run(map[string]any{})
|
||||
if step.wantErr != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), step.wantErr) {
|
||||
t.Fatalf("%s: %v %+v", step.name, err, got)
|
||||
}
|
||||
continue
|
||||
}
|
||||
a, _ := got.(Adoption)
|
||||
if err != nil || strings.Join(a.Adopted, ",") != strings.Join(step.adopted, ",") || a.Same != step.same ||
|
||||
a.Own != step.own || a.Handed != 3 || eventsNamed(v, before, AccountHashedEvent) != len(step.adopted) {
|
||||
t.Fatalf("%s: %+v, %v", step.name, a, err)
|
||||
}
|
||||
raw, _ := json.Marshal(got)
|
||||
for _, hash := range handing {
|
||||
if strings.Contains(string(raw), hash) {
|
||||
t.Fatalf("%s: account_adopt said a hash: %s", step.name, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
if k := acc.keys["controller"]; !k.Adopted || k.Hash != h("c") || k.Node != "novox" || acc.keys["node.ace"].Hash != h("m") {
|
||||
t.Errorf("adopted keys %+v %+v", k, acc.keys["node.ace"])
|
||||
}
|
||||
if acc.keys["g14.dunst"] != dunst {
|
||||
t.Errorf("a user whose value the vault holds was adopted over: %+v", acc.keys["g14.dunst"])
|
||||
}
|
||||
// A refused user is named in the last adoption, and the same handing is not opened again at the next change.
|
||||
sealedTo, handing["bad.user"] = v.Own.PublicKey, "not-bcrypt"
|
||||
if a, err := v.AdoptFrom("novox", have(), "the watch", false); err != nil || a.Refused["bad.user"] == "" || a.Handed != 4 {
|
||||
t.Fatalf("a refused user: %+v, %v", a, err)
|
||||
}
|
||||
if a, err := v.AdoptFrom("novox", have(), "the watch", false); err != nil || !a.At.IsZero() {
|
||||
t.Fatalf("the same handing was opened again after a refusal: %+v, %v", a, err)
|
||||
}
|
||||
if said := v.adoptionSaid(); said["refused"] == nil {
|
||||
t.Errorf("the refusal is not said: %v", said)
|
||||
}
|
||||
if _, err := v.AdoptFrom("ace", have(), "the test", true); err == nil || !strings.Contains(err.Error(), "does not hold the bus") {
|
||||
t.Errorf("a handing from a node not holding the bus: %v", err)
|
||||
}
|
||||
listed, err := own.Run(map[string]any{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rows := listed.(map[string]any)["adopted"].([]map[string]any)
|
||||
if len(rows) != 2 || rows[0]["user"] != "controller" || rows[1]["user"] != "node.ace" || rows[0]["node"] != "novox" {
|
||||
t.Errorf("secret_own's adopted rows %v", rows)
|
||||
}
|
||||
if narrowed, _ := own.Run(map[string]any{"module": "dunst"}); narrowed.(map[string]any)["adopted"] != nil {
|
||||
t.Errorf("secret_own narrowed to a module lists adopted accounts: %v", narrowed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,11 +46,16 @@ type Vault struct {
|
||||
Accounts AccountStore
|
||||
// HashCost is the bcrypt cost of those hashes: zero is the controller's, bcrypt's default.
|
||||
HashCost int
|
||||
// Haves reads the node-engines' have bucket, where the bus node hands the loaded list's hashes (adopt.go): nil
|
||||
// adopts nothing.
|
||||
Haves HaveReader
|
||||
|
||||
mu sync.Mutex
|
||||
// hashMu keeps one hashing at a time, and hashes what is owed (accounts.go).
|
||||
hashMu sync.Mutex
|
||||
hashes hashing
|
||||
// adoption is the last adoption (adopt.go), under mu.
|
||||
adoption adoptionKept
|
||||
// callsMu guards pending: the keys of the calls open now (intake.go), never written anywhere.
|
||||
callsMu sync.Mutex
|
||||
pending map[string]*callKey
|
||||
|
||||
@@ -131,8 +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 one bucket of another's, and only reads it: the
|
||||
// controller's assignments, for what each node's plan names (askers.go, design 57 V4).
|
||||
// (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 {
|
||||
@@ -146,8 +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"]` {
|
||||
t.Errorf("the manifest reads %s: the vault reads the controller's assignments 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`)
|
||||
@@ -164,6 +165,12 @@ func TestNoBucketHoldsASecret(t *testing.T) {
|
||||
if f == "accounts.go" {
|
||||
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))
|
||||
}
|
||||
// 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))
|
||||
}
|
||||
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.20261021100209-cb54756c3865
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026102838-174789941936
|
||||
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,7 @@
|
||||
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=
|
||||
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=
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"account-hashed"
|
||||
],
|
||||
"reads": [
|
||||
"mesh-controller.assignments"
|
||||
"mesh-controller.assignments",
|
||||
"node-engine.have"
|
||||
],
|
||||
"state": [
|
||||
{
|
||||
@@ -154,6 +155,7 @@
|
||||
"secret_handed",
|
||||
"secret_own",
|
||||
"account_hash",
|
||||
"account_adopt",
|
||||
"secret_asks",
|
||||
"secret_vault_export",
|
||||
"provisioner_retirement",
|
||||
|
||||
Reference in New Issue
Block a user