mesh-vault: make a new consumer's first credential whether or not it is handed
mesh/delivery merged: source.main moved to novox/mesh-catalog@c52afdf88b84
mesh/delivery-group group fix/1126-provision-credentials merged: every member is merged
mesh/check pass 019614aedc14 on fe7c5ccb64bf | unit 6028 run, 142 kept, 16 skipped | integration 6091 run, 174 kept, 25 skipped | over bound: unit, in…
mesh/delivery merged: source.main moved to novox/mesh-catalog@c52afdf88b84
mesh/delivery-group group fix/1126-provision-credentials merged: every member is merged
mesh/check pass 019614aedc14 on fe7c5ccb64bf | unit 6028 run, 142 kept, 16 skipped | integration 6091 run, 174 kept, 25 skipped | over bound: unit, in…
With the controller gone nothing made the pair row of a new consumer: the vault minted only for a provider handed to the provisioner (ADR 0349 clause 17's premise, that a holder filled the old way gets the controller's value). A node composing its own now asks the vault for its credentials (own, ADR 0547 §2, ADR 0550 §5), so the vault makes the first value of a shared or per-login provision for every holder, create-only (issue 1126; ADR 0570 §6). A remake or a rotation still waits while a holder is filled the old way. A credential asked of the vault in mesh-provisioner's provisions starts a pass at once, one pass at a time, and vault_status answers each provider and provision's last outcome as `credentials`. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -123,15 +123,15 @@ func TestAProviderWithRowsIsUnchangedByTheFormItsPlansName(t *testing.T) {
|
||||
}
|
||||
|
||||
// A form the vault does not make (issued by the provider's software, or given by a person), a form it does not know,
|
||||
// or plans that disagree: nothing is made, and the pass says why. The rule that no holder is left behind still holds.
|
||||
// or plans that disagree: nothing is made, and the pass says why. (A first value for a provider not handed is made since
|
||||
// issue 1126: TestAFirstValueIsMadeForEveryHolderAndARemakeWaitsForOneFilledTheOldWay.)
|
||||
func TestTheVaultMakesNothingForAFormItDoesNotMakeOrCannotTrust(t *testing.T) {
|
||||
for form, said := range map[string]string{
|
||||
"issued": "issued by the provider's software",
|
||||
"given": "given by a person",
|
||||
"one-for-all": "which is not a credential form",
|
||||
"shared|pair": "disagree",
|
||||
"shared|": "no plan says whether",
|
||||
"shared@unmoved": "stand-in on anchor",
|
||||
"issued": "issued by the provider's software",
|
||||
"given": "given by a person",
|
||||
"one-for-all": "which is not a credential form",
|
||||
"shared|pair": "disagree",
|
||||
"shared|": "no plan says whether",
|
||||
} {
|
||||
t.Run(form, func(t *testing.T) {
|
||||
provider := handedWith("anchor", "stand-in")
|
||||
@@ -139,9 +139,6 @@ func TestTheVaultMakesNothingForAFormItDoesNotMakeOrCannotTrust(t *testing.T) {
|
||||
if a, b, ok := strings.Cut(form, "|"); ok {
|
||||
first, second = a, b
|
||||
}
|
||||
if a, _, ok := strings.Cut(form, "@"); ok {
|
||||
first, second, provider = a, a, unmoved("anchor", "stand-in")
|
||||
}
|
||||
entry := func(f string) string {
|
||||
if f == "" {
|
||||
return "stand-in-api@anchor/stand-in"
|
||||
|
||||
@@ -208,6 +208,30 @@ func watchWants(env *Env) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// watchCredentialAsks starts a pass over the providers' credentials as soon as a credential is asked of the vault in
|
||||
// the seat mesh-provisioner's provisions (ADR 0570 §1, §6; issue 1126: a new consumer's login waited for nothing but
|
||||
// the minute): a watch, never a poll. Asks heard while a pass runs are one pass after it. It answers whether the watch
|
||||
// began; one that did not is begun again at the next minute pass, which makes the same values meanwhile.
|
||||
func watchCredentialAsks(env *Env, kick chan<- struct{}) bool {
|
||||
if env.Vault == nil {
|
||||
return true
|
||||
}
|
||||
err := stdio.State(provisions.ProvisionsRead).Watch("", func(c stdio.StateChange) error {
|
||||
if !c.Current && credentialAsked(c.Value) {
|
||||
select {
|
||||
case kick <- struct{}{}:
|
||||
default: // a pass is already asked: it reads every ask
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
say("the credentials asked of the vault are not watched, so each waits for the minute's pass; begun again then: %v", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// placement reads the seat mesh-placement'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() },
|
||||
@@ -293,7 +317,17 @@ func main() {
|
||||
watched, watching := watchHandings(env), watchAssignments()
|
||||
wants := watchWants(env)
|
||||
leavers := watchLeavers(env)
|
||||
kick := make(chan struct{}, 1)
|
||||
go func() {
|
||||
for range kick {
|
||||
env.pairPass()
|
||||
}
|
||||
}()
|
||||
asks := watchCredentialAsks(env, kick)
|
||||
for {
|
||||
if !asks {
|
||||
asks = watchCredentialAsks(env, kick)
|
||||
}
|
||||
if !leavers {
|
||||
leavers = watchLeavers(env)
|
||||
}
|
||||
|
||||
@@ -10,14 +10,18 @@ package main
|
||||
// client records its needs from. A consumer binds when such an entry appears and unbinds when it goes, or when its
|
||||
// assignment goes. The vault reads every plan on each pass (once a minute, and when it starts).
|
||||
//
|
||||
// **Every holder can get it, or nothing is made** (the lead's rule on issue 677, note 10). The holders are the
|
||||
// provider on its machine and every consumer bound to it. Only a module handed to the provisioner gets a value from
|
||||
// the vault; one still filled the old way gets the controller's (ADR 0349 clause 17). So the vault mints or remakes
|
||||
// a provider's value only when the provider's assignment is handed to the provisioner, and no consumer the vault
|
||||
// knows to hold it is still filled the old way: a consumer whose pair row the move brought in (V3) and whose
|
||||
// assignment is still there with no plan. Otherwise it makes nothing, and says why on each pass that changes what
|
||||
// it says. A consumer filled the old way that bound after the move names no provider on the bus at all; it is the
|
||||
// provider's move (design 57 B2, B3) that ends that case.
|
||||
// **A first value is made for every holder; a remake waits for one filled the old way** (the lead's rule on issue
|
||||
// 677, note 10, as issue 1126 left it). The holders are the provider on its machine and every consumer bound to it. A
|
||||
// module handed to the provisioner gets its value through its client (`get`); one that is not gets it through its
|
||||
// machine's node-engine, which composes its own and asks the vault for exactly the credentials its composition reads
|
||||
// (`own`, ADR 0547 §2, ADR 0550 §5), now that no controller makes them (ADR 0570 §6). So a provider or consumer the
|
||||
// vault holds no value for gets its first one made whether or not it is handed: a login of its own for each new
|
||||
// consumer of a per-login provider, or the provider's one shared value. That is create-only: a value made is never
|
||||
// made again by a bind. A remake (a bind or unbind changing who holds the shared value) and a rotation still wait
|
||||
// while the provider, or a consumer whose pair row the move brought in, is not handed: a node composing its own would
|
||||
// carry the old value until it composes again. The vault says why on each pass that changes what it says. A
|
||||
// credential asked of the vault in the seat mesh-provisioner's provisions (ADR 0570 §1, §6) starts a pass at once
|
||||
// (main.go watchCredentialAsks), not only at the minute's.
|
||||
//
|
||||
// **What the move brought in is adopted, and nothing is rotated early.** The move (V3) copied each pair row as
|
||||
// `pair/<provider>/<provision>/<consumer>/<module>[/<local>]`. Rows a provider shares (ADR 0158: its own secret and
|
||||
@@ -56,6 +60,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
)
|
||||
|
||||
@@ -333,12 +338,21 @@ func sharedHolders(k pairKey, b *binds) (string, []string) {
|
||||
return strings.Join(modules, ","), consumers
|
||||
}
|
||||
|
||||
// heldBack says why the vault may not make a value for k now, or "": a holder that would never get it.
|
||||
func heldBack(k pairKey, b *binds, rows []pairRow, seen bindings) string {
|
||||
// unnamedProvider says why the vault may not make a first value for k, or "": no plan names the provider's module.
|
||||
func unnamedProvider(k pairKey, b *binds) string {
|
||||
if b == nil || len(b.providerModules) == 0 {
|
||||
return fmt.Sprintf("no plan names the module serving %s on %s, so the vault cannot tell the provider gets "+
|
||||
"a value it makes", k.provision, k.provider)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// heldBack says why the vault may not remake or rotate a value for k now, or "": a holder filled the old way, which
|
||||
// would carry the old value until its node composes again.
|
||||
func heldBack(k pairKey, b *binds, rows []pairRow, seen bindings) string {
|
||||
if why := unnamedProvider(k, b); why != "" {
|
||||
return why
|
||||
}
|
||||
for m := range b.providerModules {
|
||||
if !seen.handed[k.provider+"."+m] {
|
||||
return fmt.Sprintf("the provider %s on %s is not handed to the provisioner, so it would never get a "+
|
||||
@@ -482,9 +496,7 @@ func (v *Vault) pairOne(k pairKey, b *binds, rows []pairRow, seen bindings) (Pai
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
if held := heldBack(k, b, rows, seen); held != "" {
|
||||
return PairOutcome{Name: name, Outcome: PairHeld, Why: held}, nil
|
||||
}
|
||||
// Each one's first login, create-only: whoever composes it, its node asks the vault for it.
|
||||
return v.firstLogins(k, missing)
|
||||
}
|
||||
}
|
||||
@@ -512,7 +524,8 @@ func (v *Vault) pairOne(k pairKey, b *binds, rows []pairRow, seen bindings) (Pai
|
||||
if len(consumers) == 0 {
|
||||
return PairOutcome{Name: name, Outcome: PairUnchanged}, nil
|
||||
}
|
||||
if why := heldBack(k, b, rows, seen); why != "" {
|
||||
// A first value, create-only: every holder gets it through its client or its node's own composition.
|
||||
if why := unnamedProvider(k, b); why != "" {
|
||||
return PairOutcome{Name: name, Outcome: PairHeld, Why: why}, nil
|
||||
}
|
||||
form, why := b.form(k)
|
||||
@@ -748,6 +761,9 @@ func (e *Env) pairPass() {
|
||||
if e.Vault == nil {
|
||||
return
|
||||
}
|
||||
// One pass at a time: the minute's and one an ask of the vault started (watchCredentialAsks) never race a mint.
|
||||
e.pairing.Lock()
|
||||
defer e.pairing.Unlock()
|
||||
outcomes, err := e.Vault.PairPass()
|
||||
if said, oerr := e.Vault.OutcomePass(); oerr != nil {
|
||||
say("the credentials' outcomes are not all written: %v", oerr)
|
||||
@@ -765,6 +781,7 @@ func (e *Env) pairPass() {
|
||||
if err != nil {
|
||||
e.lastPairFailure = err.Error()
|
||||
}
|
||||
e.pairOutcomes = outcomes
|
||||
if e.pairsSaid == nil {
|
||||
e.pairsSaid = map[string]string{}
|
||||
}
|
||||
@@ -836,3 +853,19 @@ func pairRowRefusal(read AssignmentReader, node string, parts []string) string {
|
||||
}
|
||||
return fmt.Sprintf("%s's plan on %s does not name %s from %s under that name", module, consumer, provision, provider)
|
||||
}
|
||||
|
||||
// credentialsState is what the last pass did, or did not do, for each provider and provision, and why: what
|
||||
// `vault_status` answers as `credentials`, so a provision whose credential is held back is read, not guessed.
|
||||
func (e *Env) credentialsState() []PairOutcome {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
return slices.Clone(e.pairOutcomes)
|
||||
}
|
||||
|
||||
// credentialAsked is whether a key of the seat mesh-provisioner's provisions asks the vault for a credential (ADR 0570
|
||||
// §1, §6): a provision `secret-<provision>` whose provider is the vault, in state asked.
|
||||
func credentialAsked(raw []byte) bool {
|
||||
p, err := provisions.Decode(raw)
|
||||
return err == nil && p.State == provisions.Asked && p.Provider.Module == provisions.VaultModule &&
|
||||
strings.HasPrefix(p.Provision, catalogue.CredentialProvisionPrefix)
|
||||
}
|
||||
|
||||
@@ -398,24 +398,72 @@ func TestAProviderTheVaultHoldsNoRowOfIsNotGuessed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The vault makes no value a holder would never get (the lead's rule on issue 677): a provider not handed to the
|
||||
// provisioner, or a consumer the move brought in that is still filled the old way, holds every make and remake
|
||||
// back, and a rotation too.
|
||||
func TestNoValueIsMadeThatAHolderFilledTheOldWayWouldNeverGet(t *testing.T) {
|
||||
t.Run("an unmoved provider", func(t *testing.T) {
|
||||
// keyedWith is a plan naming provisions alone: its module is not handed to the provisioner, and its node composes its
|
||||
// own and asks the vault for its credentials (`own`, ADR 0547 §2, ADR 0550 §5).
|
||||
func keyedWith(node, module string, provisions ...string) string {
|
||||
return strings.Replace(handedWith(node, module, provisions...), `"templates":{"source":"x","digest":"y"},`, "", 1)
|
||||
}
|
||||
|
||||
// After the controller, a node composing its own gets its credentials from the vault (`own`), so a first value is made
|
||||
// for a new consumer whether or not its ends are handed to the provisioner, and never remade (issue 1126, 2026-11-02:
|
||||
// nothing made the row of a new consumer once the controller's perform was gone; ADR 0570 §6). A remake, and a
|
||||
// rotation, still wait while a holder is filled the old way (the lead's rule on issue 677).
|
||||
func TestAFirstValueIsMadeForEveryHolderAndARemakeWaitsForOneFilledTheOldWay(t *testing.T) {
|
||||
t.Run("a first value for holders that compose their own", func(t *testing.T) {
|
||||
mirror := assignments{
|
||||
"anchor.stand-in": unmoved("anchor", "stand-in"),
|
||||
"g14.app": handedWith("g14", "app", "stand-in-api@anchor/stand-in"),
|
||||
"anchor.stand-in": keyedWith("anchor", "stand-in"),
|
||||
"ace.cli": keyedWith("ace", "cli", "stand-in-api@anchor/stand-in+pair"),
|
||||
}
|
||||
v := pairVault(t, mirror)
|
||||
if o := pass(t, v, standIn); o.Outcome != PairHeld || !strings.Contains(o.Why, "stand-in on anchor") {
|
||||
t.Fatalf("an unmoved provider: %+v", o)
|
||||
if o := pass(t, v, standIn); o.Outcome != PairMade {
|
||||
t.Fatalf("a new consumer's login: %+v", o)
|
||||
}
|
||||
if _, held, _ := v.Store.Read(standIn); held {
|
||||
t.Fatal("a value was made that the provider would never get")
|
||||
login := "pair/anchor/stand-in-api/ace/cli"
|
||||
first, held, _ := v.Store.Read(login)
|
||||
if !held {
|
||||
t.Fatal("no login was made for the new consumer")
|
||||
}
|
||||
if o := pass(t, v, standIn); o.Outcome != PairPerPair {
|
||||
t.Fatalf("the pass after: %+v", o)
|
||||
}
|
||||
if again, _, _ := v.Store.Read(login); again.Generation != first.Generation || again.AtRest != first.AtRest {
|
||||
t.Fatal("a login made once was made again")
|
||||
}
|
||||
env := &Env{Vault: v.Vault}
|
||||
env.pairPass()
|
||||
if got := env.credentialsState(); len(got) != 1 || got[0].Name != standIn || got[0].Outcome != PairPerPair {
|
||||
t.Fatalf("the credentials' state reads %+v", got)
|
||||
}
|
||||
|
||||
mirror = assignments{
|
||||
"anchor.stand-in": keyedWith("anchor", "stand-in"),
|
||||
"ace.cli": keyedWith("ace", "cli", "stand-in-api@anchor/stand-in+shared"),
|
||||
}
|
||||
v = pairVault(t, mirror)
|
||||
if o := pass(t, v, standIn); o.Outcome != PairMade {
|
||||
t.Fatalf("a new consumer's shared value: %+v", o)
|
||||
}
|
||||
mirror["g14.app"] = keyedWith("g14", "app", "stand-in-api@anchor/stand-in+shared")
|
||||
if o := pass(t, v, standIn); o.Outcome != PairHeld || !strings.Contains(o.Why, "stand-in on anchor") {
|
||||
t.Fatalf("a second consumer remakes the value its provider composes from: %+v", o)
|
||||
}
|
||||
if rec, _, _ := v.Store.Read(standIn); rec.Generation != 1 {
|
||||
t.Fatalf("the shared value was remade: generation %d", rec.Generation)
|
||||
}
|
||||
if _, err := v.Rotate(ControllerCaller, standIn, "person.jochen", "why not"); err == nil {
|
||||
t.Fatal("a rotation made a value the provider would never get")
|
||||
t.Fatal("a rotation remade a value a holder filled the old way holds")
|
||||
}
|
||||
})
|
||||
t.Run("an ask of the vault for a credential", func(t *testing.T) {
|
||||
for raw, want := range map[string]bool{
|
||||
`{"v":1,"provision":"secret-stand-in-api","consumer":{"module":"cli","node":"ace"},"provider":{"module":"mesh-vault","node":"novox"},"state":"asked"}`: true,
|
||||
`{"v":1,"provision":"secret-stand-in-api","consumer":{"module":"cli","node":"ace"},"provider":{"module":"mesh-vault","node":"novox"},"state":"provided"}`: false,
|
||||
`{"v":1,"provision":"stand-in-api","consumer":{"module":"cli","node":"ace"},"provider":{"module":"stand-in","node":"anchor"},"state":"asked"}`: false,
|
||||
`not a provision`: false,
|
||||
} {
|
||||
if credentialAsked([]byte(raw)) != want {
|
||||
t.Errorf("%s is an ask of the vault: %v", raw, !want)
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("an unmoved consumer", func(t *testing.T) {
|
||||
|
||||
@@ -43,6 +43,10 @@ type Env struct {
|
||||
// did not finish (pair.go): each said once, and again when it changes.
|
||||
pairsSaid map[string]string
|
||||
lastPairFailure string
|
||||
// pairOutcomes is the last pass's outcome for each provider and provision (vault_status `credentials`); pairing
|
||||
// keeps one pass at a time.
|
||||
pairOutcomes []PairOutcome
|
||||
pairing sync.Mutex
|
||||
}
|
||||
|
||||
func (e *Env) keyRefused() string {
|
||||
@@ -622,7 +626,9 @@ func definitions(env *Env) []toolDef {
|
||||
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 §9, issue 774): the vault mints every wanted bus account it holds no value for, " +
|
||||
"every wanted account it holds back with why (never one whose value it holds), and those it minted.",
|
||||
"every wanted account it holds back with why (never one whose value it holds), and those it minted; and " +
|
||||
"`credentials`: for each provider and provision, what the last pass did for its credential (made, held " +
|
||||
"and why, per pair) (issue 1126).",
|
||||
run: func(string, map[string]any) (any, error) {
|
||||
if env.Vault == nil {
|
||||
return map[string]any{"ready": false, "error": fmt.Sprint(env.Broken)}, nil
|
||||
@@ -634,7 +640,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(), "signing": env.Vault.SigningState(), "ok": left == 0 && env.keyRefused() == "" && key != "", "rotation": env.Vault.RotationState()}, nil
|
||||
"minting": env.Vault.MintState(), "credentials": env.credentialsState(), "retiring": env.Vault.RetireState(), "signing": env.Vault.SigningState(), "ok": left == 0 && env.keyRefused() == "" && key != "", "rotation": env.Vault.RotationState()}, nil
|
||||
}},
|
||||
}...)
|
||||
}
|
||||
|
||||
@@ -206,6 +206,8 @@ func TestNoBucketHoldsASecret(t *testing.T) {
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(BoundState)."+call, "", 1))
|
||||
}
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(provisions.ProvisionsRead).Get(", "", 1))
|
||||
// And one watch of them, for a credential asked of the vault (issue 1126).
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(provisions.ProvisionsRead).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)
|
||||
|
||||
Reference in New Issue
Block a user