The vault reads each plan from mesh-provisioner, says each credential's outcome in its own bound state, mints nothing for form none, and answers rotate and issued for any proven caller (issue 1954)
Each outcome names the value by the vault's own name and generation, never the value; held is provided, issued or given waits, an entry goes only on a retiring key's Approve. The vault no longer reads the controller's assignments, so it leaves the list of their readers. Pins mesh-sdk 6b8c694.
This commit is contained in:
@@ -4,6 +4,5 @@
|
||||
# controller (novox/hq ADR 0549 §3, §6; issue 774).
|
||||
gitea the provisioner's plan of a module (go/provider) rides only in an assignment's value; its home after the lease is issue 774's provisioner-plan subtask
|
||||
keycloak the provisioner's plan of a module (go/provider), as gitea
|
||||
mesh-vault the provisioner's plan, which only an assignment's value carries; its home is issue 774's provisioner-plan subtask
|
||||
postgres the provisioner's plan of a module (go/provider), as gitea
|
||||
route-adapter the provisioner's plan of a module (go/provider), as gitea
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -72,8 +74,23 @@ func (l AllowList) May(caller, name string) (bool, string) {
|
||||
// internal/broker, KindProvisioner).
|
||||
const ProvisionerPrefix = "provisioner."
|
||||
|
||||
// AssignmentsRead is the controller's assignments mirror as this module reads it (its manifest's `reads`).
|
||||
const AssignmentsRead = "mesh-controller.assignments"
|
||||
// AssignmentsRead is where the vault reads each module's provisioning plan on each node (its manifest's `reads`): the
|
||||
// seat mesh-provisioner's state `plans`, `<node>.<module>` (novox/hq ADR 0570 clause 2), no longer the controller's
|
||||
// assignments. A plan is read as the assignment's value carried it (asAssignment), so the rules that read a plan are
|
||||
// unchanged.
|
||||
const AssignmentsRead = provisions.PlansRead
|
||||
|
||||
// asAssignment is a plan's key in the form the vault's rules read: `{node, module, provisioning: plan}`, nil for none.
|
||||
func asAssignment(key string, raw []byte, err error) ([]byte, error) {
|
||||
if err != nil || raw == nil {
|
||||
return raw, err
|
||||
}
|
||||
p, err := provisions.DecodePlan(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the plan %s: %w", key, err)
|
||||
}
|
||||
return json.Marshal(map[string]any{"node": p.Node, "module": p.Module, "provisioning": p.Plan})
|
||||
}
|
||||
|
||||
// AssignmentReader reads one value of the assignments mirror, `<node>.<module>`: nil when there is none.
|
||||
type AssignmentReader func(key string) ([]byte, error)
|
||||
|
||||
@@ -120,6 +120,15 @@ func fromTheController(caller, verb string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// fromAGrantedCaller refuses a caller the runtime did not prove: `rotate` and `issued` answer every caller whose
|
||||
// manifest invokes them, which the bus grants it (novox/hq ADR 0570, Consequences), no longer the controller alone.
|
||||
func fromAGrantedCaller(caller, verb string) error {
|
||||
if strings.TrimSpace(caller) == "" || caller == stdio.Unproven {
|
||||
return fmt.Errorf("%s answers a caller the runtime proves, and it proved none", verb)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// openCall makes a key for one call and keeps its private half here until the call is taken or two minutes
|
||||
// have passed.
|
||||
func (v *Vault) openCall(verb, secret, by string, ask *AskRecord) (string, string, error) {
|
||||
@@ -495,7 +504,7 @@ func askedByName(caller string) string {
|
||||
// 10, ADR 0305 with the vault in the controller's place): the controller asks the provider's tool, on the
|
||||
// provider's machine, with the call's key, and passes on what it answered.
|
||||
func (v *Vault) IssuedOpen(caller, provision, provider, module string) (Opened, error) {
|
||||
if err := fromTheController(caller, "issued"); err != nil {
|
||||
if err := fromAGrantedCaller(caller, "issued"); err != nil {
|
||||
return Opened{}, err
|
||||
}
|
||||
return v.openIssued(provision, provider, module)
|
||||
@@ -520,7 +529,7 @@ func (v *Vault) openIssued(provision, provider, module string) (Opened, error) {
|
||||
// IssuedTake opens what the provider's tool sealed to the call's key and keeps it as the provision's one
|
||||
// credential. The same value again writes nothing (ADR 0305 §5): every holder that has it is left as it is.
|
||||
func (v *Vault) IssuedTake(caller, call, sealed string) (Changed, error) {
|
||||
if err := fromTheController(caller, "issued"); err != nil {
|
||||
if err := fromAGrantedCaller(caller, "issued"); err != nil {
|
||||
return Changed{}, err
|
||||
}
|
||||
c, err := v.takeCall("issued", call)
|
||||
@@ -643,7 +652,7 @@ func (v *Vault) TakeIssued(caller, provision, provider, module, tool string) (Is
|
||||
// credential, as before) or one per holder (`holders`: node, module and sealed each, kept as that holder's own and
|
||||
// handed to it alone, perholder.go). Never both.
|
||||
func (v *Vault) TakeIssuedFor(caller, provision, provider, module, tool string, holders []Holder) (Issued, error) {
|
||||
if err := fromTheController(caller, "issued"); err != nil {
|
||||
if err := fromAGrantedCaller(caller, "issued"); err != nil {
|
||||
return Issued{}, err
|
||||
}
|
||||
return v.takeIssuedFor(caller, provision, provider, module, tool, holders)
|
||||
@@ -747,9 +756,11 @@ func (n Narrow) String() string {
|
||||
// value shared by every holder (ADR 0158) is one value, so it is never rotated for one consumer alone: refused, and
|
||||
// nothing changes. A module's own secret names no consumer.
|
||||
func (v *Vault) RotateFor(caller, secret string, one Narrow, by, why string) (Changed, error) {
|
||||
if err := fromAGrantedCaller(caller, "rotate"); err != nil {
|
||||
return Changed{}, err
|
||||
}
|
||||
if caller != ControllerCaller {
|
||||
return Changed{}, fmt.Errorf("the vault rotates for the controller alone, which knows whether the module's "+
|
||||
"secrets are filled from the vault yet; %s is not the controller: `secret rotate` at the controller", caller)
|
||||
by = "" // only the controller relays who asked at its terminal; any other caller is who asked
|
||||
}
|
||||
if strings.TrimSpace(why) == "" {
|
||||
return Changed{}, errors.New("not rotated: a rotation says why it was asked (`why`)")
|
||||
|
||||
@@ -307,9 +307,12 @@ func TestAnIssuingProviderAnswersOnlyTheVaultsCallSealedToTheVault(t *testing.T)
|
||||
t.Errorf("the provider's tool answered %q", caller)
|
||||
}
|
||||
}
|
||||
// The vault takes the steps from the controller alone.
|
||||
if _, err := v.IssuedOpen("anchor.node-tools", "plex-api", "anchor", "plex"); err == nil {
|
||||
t.Error("the vault opened an issued call for another caller")
|
||||
// The vault takes the steps from a caller the runtime proved, whose manifest the bus granted the verb (ADR 0570),
|
||||
// and from no unproven one.
|
||||
for _, caller := range []string{"unproven", ""} {
|
||||
if _, err := v.IssuedOpen(caller, "plex-api", "anchor", "plex"); err == nil {
|
||||
t.Errorf("the vault opened an issued call for %q", caller)
|
||||
}
|
||||
}
|
||||
|
||||
changed, err := issue(t, v, provider)
|
||||
@@ -437,17 +440,17 @@ func TestRotateKeepsWhoAskedAndWhoCarriedIt(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate is the controller's alone, and says why (the review of issue 675): only the controller knows whether a
|
||||
// module is filled from the vault yet, so a rotation asked by any other caller — a proven one included — would
|
||||
// drift from the value the controller holds; and a rotation without a reason is refused. Neither changes anything.
|
||||
func TestRotateIsTheControllersAloneAndSaysWhy(t *testing.T) {
|
||||
// Rotate answers a caller the runtime proved, whose manifest the bus granted it (novox/hq ADR 0570 Consequences), and
|
||||
// says why: an unproven caller, and a rotation without a reason, are refused and change nothing; a proven caller
|
||||
// other than the controller is who asked, whatever `by` it says.
|
||||
func TestRotateIsAProvenCallersAndSaysWhy(t *testing.T) {
|
||||
v := newVault(t, nil)
|
||||
if _, err := v.Mint("own/g14/dunst/token", "the test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, caller := range []string{"g14.claude-code", "g14.node-provisioner", "unproven", ""} {
|
||||
for _, caller := range []string{"unproven", ""} {
|
||||
if _, err := v.Rotate(caller, "own/g14/dunst/token", "jochen", "leaked"); err == nil ||
|
||||
!strings.Contains(err.Error(), "controller alone") {
|
||||
!strings.Contains(err.Error(), "proves") {
|
||||
t.Errorf("%q rotated: %v", caller, err)
|
||||
}
|
||||
}
|
||||
@@ -460,6 +463,12 @@ func TestRotateIsTheControllersAloneAndSaysWhy(t *testing.T) {
|
||||
if rec, _, _ := v.Store.Read("own/g14/dunst/token"); rec.Generation != 1 {
|
||||
t.Fatalf("a refused rotation changed the secret: generation %d", rec.Generation)
|
||||
}
|
||||
if _, err := v.Rotate("g14.claude-code", "own/g14/dunst/token", "jochen", "leaked in a log"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec, _, _ := v.Store.Read("own/g14/dunst/token"); rec.Generation != 2 || rec.ChangedBy != "g14.claude-code" {
|
||||
t.Errorf("a session's rotation is recorded by %q at generation %d", rec.ChangedBy, rec.Generation)
|
||||
}
|
||||
}
|
||||
|
||||
// The names the vault keeps them under are names, never paths.
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -50,12 +54,35 @@ func build() *Env {
|
||||
// Who may ask (askers.go): a node's provisioner client, for what its module's plan there names.
|
||||
// Where it sees a consumer bind a provider (pair.go): the same mirror, every key.
|
||||
// The assignments as the vault's watch keeps them (watchAssignments), read over the bus until it has caught up.
|
||||
assigned = &WatchedAssignments{Fallback: readAssignment}
|
||||
env.Vault = &Vault{Store: store, Own: own, Askers: Declared{Read: assigned.Read, Confirm: readAssignment, Store: store},
|
||||
assigned = &WatchedAssignments{Fallback: readPlanRaw}
|
||||
watchedPlan := func(key string) ([]byte, error) { raw, err := assigned.Read(key); return asAssignment(key, raw, err) }
|
||||
env.Vault = &Vault{Store: store, Own: own, Askers: Declared{Read: watchedPlan, Confirm: 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,
|
||||
HaveKeys: func() ([]string, error) { return stdio.State(HaveRead).Keys() }, Placement: placement, Signers: busSigners{},
|
||||
AskCallers: askCallers(os.Getenv("MESH_VAULT_SECRET_ASK_CALLERS"))}
|
||||
AskCallers: askCallers(os.Getenv("MESH_VAULT_SECRET_ASK_CALLERS")),
|
||||
Bound: Bound{
|
||||
Get: func(key string) ([]byte, error) {
|
||||
e, err := stdio.State(BoundState).Get(key)
|
||||
if err != nil || e == nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.Value, nil
|
||||
},
|
||||
Put: func(key string, value []byte) error {
|
||||
_, err := stdio.State(BoundState).Put(key, json.RawMessage(value))
|
||||
return err
|
||||
},
|
||||
Delete: func(key string) error { return stdio.State(BoundState).Delete(key) },
|
||||
Keys: func() ([]string, error) { return stdio.State(BoundState).Keys() },
|
||||
Provision: func(key string) ([]byte, error) {
|
||||
e, err := stdio.State(provisions.ProvisionsRead).Get(key)
|
||||
if err != nil || e == nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.Value, nil
|
||||
},
|
||||
}}
|
||||
return env
|
||||
}
|
||||
|
||||
@@ -222,6 +249,12 @@ func adoptOperatorKey(env *Env) error {
|
||||
// readAssignment is one value of the controller's assignments mirror, read through the runtime under the vault's
|
||||
// own account (its manifest `reads` it), or nil when there is none.
|
||||
func readAssignment(key string) ([]byte, error) {
|
||||
raw, err := readPlanRaw(key)
|
||||
return asAssignment(key, raw, err)
|
||||
}
|
||||
|
||||
// readPlanRaw is one key of the seat mesh-provisioner's plans as it is kept, nil when there is none.
|
||||
func readPlanRaw(key string) ([]byte, error) {
|
||||
e, err := stdio.State(AssignmentsRead).Get(key)
|
||||
if err != nil || e == nil {
|
||||
return nil, err
|
||||
|
||||
@@ -317,7 +317,7 @@ func TestTheVaultWholeMintsOnTheBusOnlyFromTheLeaseWrite(t *testing.T) {
|
||||
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-assigner_placement"} {
|
||||
"mesh-provisioner_plans", "node-engine_have", "mesh-delivery-assigner_placement"} {
|
||||
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b, History: 2}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package main
|
||||
|
||||
// The vault's outcome for each credential it provides (novox/hq ADR 0570 clauses 3, 6, 7): an entry in its own state
|
||||
// `bound`, one per credential provision `<node>.<consumer>.secret-<provision>[.<local>]`, which the holder of
|
||||
// mesh-provisioner reads to set that key's state. The entry names the value by the vault's own name for it
|
||||
// (provisions.VaultName, as its verb `own` names it) and its generation, never the value. A value the vault holds is
|
||||
// provided; one only the provider's software issues, or only a person gives, waits for them; one the vault has not
|
||||
// made yet has no entry, and the key stays asked. An offer whose form is none has no credential and no entry. An entry
|
||||
// is let go only once its key is retiring with the operator's warrant, or gone.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||
)
|
||||
|
||||
// BoundState is the vault's state of outcome entries.
|
||||
const BoundState = "bound"
|
||||
|
||||
// Bound is the vault's own state `bound`, and what it reads of the seat's provisions.
|
||||
type Bound struct {
|
||||
Get func(key string) ([]byte, error)
|
||||
Put func(key string, value []byte) error
|
||||
Delete func(key string) error
|
||||
Keys func() ([]string, error)
|
||||
// Provision is one key of mesh-provisioner's provisions, nil for none.
|
||||
Provision func(key string) ([]byte, error)
|
||||
}
|
||||
|
||||
// BoundOutcome is one entry of `bound`, in the provider helper's entry layout with the generation beside it.
|
||||
type BoundOutcome struct {
|
||||
Provider string `json:"provider"`
|
||||
ProviderModule string `json:"provider-module"`
|
||||
Provision string `json:"provision"`
|
||||
Consumer string `json:"consumer"`
|
||||
Module string `json:"module"`
|
||||
Local string `json:"local,omitempty"`
|
||||
VaultName string `json:"vault-name,omitempty"`
|
||||
State string `json:"state"`
|
||||
Why string `json:"why,omitempty"`
|
||||
Generation int `json:"generation,omitempty"`
|
||||
}
|
||||
|
||||
// OutcomePass writes each credential's outcome that differs and lets go each entry no plan names whose key is retiring
|
||||
// with the operator's warrant or gone. It answers what it wrote and removed.
|
||||
func (v *Vault) OutcomePass() ([]string, error) {
|
||||
if v.Bound.Get == nil || v.Assignments.Keys == nil {
|
||||
return nil, nil
|
||||
}
|
||||
want, err := v.outcomes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var said []string
|
||||
for _, key := range sortedOutcomeKeys(want) {
|
||||
raw, _ := json.Marshal(want[key])
|
||||
if have, err := v.Bound.Get(key); err == nil && bytes.Equal(have, raw) {
|
||||
continue
|
||||
}
|
||||
if err := v.Bound.Put(key, raw); err != nil {
|
||||
return said, fmt.Errorf("the outcome %s: %w", key, err)
|
||||
}
|
||||
said = append(said, key+" "+want[key].State)
|
||||
}
|
||||
keys, err := v.Bound.Keys()
|
||||
if err != nil {
|
||||
return said, err
|
||||
}
|
||||
for _, key := range keys {
|
||||
if _, still := want[key]; still {
|
||||
continue
|
||||
}
|
||||
if !v.letGo(key) {
|
||||
continue
|
||||
}
|
||||
if err := v.Bound.Delete(key); err != nil {
|
||||
return said, err
|
||||
}
|
||||
said = append(said, key+" let go")
|
||||
}
|
||||
return said, nil
|
||||
}
|
||||
|
||||
// letGo says an entry no plan names may go: its key is gone, or retiring with the operator's Approve.
|
||||
func (v *Vault) letGo(key string) bool {
|
||||
if v.Bound.Provision == nil {
|
||||
return false
|
||||
}
|
||||
raw, err := v.Bound.Provision(key)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if raw == nil {
|
||||
return true
|
||||
}
|
||||
p, err := provisions.Decode(raw)
|
||||
return err == nil && p.State == provisions.Retiring && p.Warrant != nil && p.Warrant.Option == "approve"
|
||||
}
|
||||
|
||||
// outcomes is every credential's outcome the plans call for, by its key.
|
||||
func (v *Vault) outcomes() (map[string]BoundOutcome, error) {
|
||||
keys, err := v.Assignments.Keys()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the plans' keys: %w", err)
|
||||
}
|
||||
out := map[string]BoundOutcome{}
|
||||
for _, k := range keys {
|
||||
raw, err := v.Assignments.Read(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the plan %s: %w", k, err)
|
||||
}
|
||||
if raw == nil {
|
||||
continue
|
||||
}
|
||||
var a plannedProvisions
|
||||
if err := json.Unmarshal(raw, &a); err != nil || a.Provisioning == nil {
|
||||
continue
|
||||
}
|
||||
consumer := provisions.End{Module: a.Module, Node: a.Node}
|
||||
for _, p := range a.Provisioning.Provisions {
|
||||
provider := provisions.End{Module: p.ProviderModule, Node: p.Provider}
|
||||
if p.ProviderModule == provisions.VaultModule && p.Provision == "secret" {
|
||||
// The vault's own offer: a value it hands the consumer's client on request (`get`), which it serves as
|
||||
// soon as it runs.
|
||||
key, err := provisions.Key(a.Node, a.Module, p.Provision, p.Local)
|
||||
if err == nil {
|
||||
out[key] = BoundOutcome{Provider: p.Provider, ProviderModule: provisions.VaultModule, Provision: p.Provision,
|
||||
Consumer: a.Node, Module: a.Module, Local: p.Local, State: provisions.Provided,
|
||||
Why: "the vault hands it to the consumer's client on request"}
|
||||
}
|
||||
continue
|
||||
}
|
||||
ck, ok := provisions.CredentialKey(p.CredentialForm, consumer, provider, p.Provision, p.Local)
|
||||
if !ok {
|
||||
continue // form none: no credential, nothing minted
|
||||
}
|
||||
parent := provisions.Provision{Provision: p.Provision, Local: p.Local, Consumer: consumer, Provider: provider}
|
||||
name := provisions.VaultName(p.CredentialForm, parent)
|
||||
cn, cm, cp, cl, err := provisions.ParseKey(ck)
|
||||
if err != nil || name == "" {
|
||||
continue
|
||||
}
|
||||
o := BoundOutcome{Provider: p.Provider, ProviderModule: provisions.VaultModule, Provision: cp, Consumer: cn,
|
||||
Module: cm, Local: cl, VaultName: name}
|
||||
rec, held, err := v.Store.Read(name)
|
||||
switch {
|
||||
case err != nil:
|
||||
o.State, o.Why = provisions.Failed, "the vault cannot read "+name+": "+err.Error()
|
||||
case held:
|
||||
o.State, o.Generation = provisions.Provided, rec.Generation
|
||||
case p.CredentialForm == FormIssued:
|
||||
o.State, o.Why = provisions.Waits, "the provider's software issues it (ADR 0305): `issued` brings it in"
|
||||
case p.CredentialForm == FormGiven:
|
||||
o.State, o.Why = provisions.Waits, "a person gives it (ADR 0337): `accept` brings it in"
|
||||
default:
|
||||
continue // not made yet: the key stays asked until the pair pass makes it
|
||||
}
|
||||
out[ck] = o
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func sortedOutcomeKeys(m map[string]BoundOutcome) []string {
|
||||
out := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The vault says each credential's outcome in its own state `bound`, by its own name for the value and its generation,
|
||||
// never the value: held is provided, an issued or given value not yet held waits, one not made yet has no entry, an
|
||||
// offer of form none has none, the vault's own offer is provided; an entry no plan names goes only once its key is gone
|
||||
// or retiring with the operator's Approve (novox/hq ADR 0570 clauses 3, 5-7; issue 1954 V1).
|
||||
func TestTheVaultSaysEachCredentialsOutcome(t *testing.T) {
|
||||
v := newVault(t, NoAsker{})
|
||||
plan := func(node, module string, provisions ...map[string]any) []byte {
|
||||
raw, _ := json.Marshal(map[string]any{"node": node, "module": module, "provisioning": map[string]any{"provisions": provisions}})
|
||||
return raw
|
||||
}
|
||||
entry := func(provision, provider, module, form string) map[string]any {
|
||||
return map[string]any{"provision": provision, "provider": provider, "provider-module": module, "credential-form": form}
|
||||
}
|
||||
plans := map[string][]byte{
|
||||
"g14.gitea": plan("g14", "gitea",
|
||||
entry("postgres-database", "ace", "postgres", "pair"),
|
||||
entry("oidc-client", "novox", "keycloak", "shared"),
|
||||
entry("media-token", "ace", "jellyfin", "issued"),
|
||||
entry("bot-token", "ace", "telegram", "given"),
|
||||
entry("route", "novox", "route-adapter", "none"),
|
||||
entry("secret", "novox", "mesh-vault", "pair")),
|
||||
}
|
||||
v.Assignments = Mirror{
|
||||
Keys: func() ([]string, error) {
|
||||
var k []string
|
||||
for key := range plans {
|
||||
k = append(k, key)
|
||||
}
|
||||
return k, nil
|
||||
},
|
||||
Read: func(key string) ([]byte, error) { return plans[key], nil }}
|
||||
bound := map[string][]byte{}
|
||||
keys := map[string]string{} // the seat's provisions, by key: a state, or a raw value
|
||||
v.Bound = Bound{
|
||||
Get: func(key string) ([]byte, error) { return bound[key], nil },
|
||||
Put: func(key string, value []byte) error { bound[key] = value; return nil },
|
||||
Delete: func(key string) error { delete(bound, key); return nil },
|
||||
Keys: func() ([]string, error) {
|
||||
var out []string
|
||||
for k := range bound {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out, nil
|
||||
},
|
||||
Provision: func(key string) ([]byte, error) {
|
||||
if raw, ok := keys[key]; ok {
|
||||
return []byte(raw), nil
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
if _, err := v.Put("pair/ace/postgres-database/g14/gitea", "pw", "the test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := v.OutcomePass(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
read := func(key string) BoundOutcome {
|
||||
var o BoundOutcome
|
||||
_ = json.Unmarshal(bound[key], &o)
|
||||
return o
|
||||
}
|
||||
for _, row := range []struct {
|
||||
key, state, name string
|
||||
generation int
|
||||
}{
|
||||
{"g14.gitea.secret-postgres-database", "provided", "pair/ace/postgres-database/g14/gitea", 1},
|
||||
{"g14.gitea.secret-media-token", "waits", "provision/ace/media-token/g14/gitea", 0},
|
||||
{"g14.gitea.secret-bot-token", "waits", "provision/ace/bot-token/g14/gitea", 0},
|
||||
{"g14.gitea.secret", "provided", "", 0},
|
||||
} {
|
||||
o := read(row.key)
|
||||
if o.State != row.state || o.VaultName != row.name || o.Generation != row.generation || o.ProviderModule != "mesh-vault" {
|
||||
t.Errorf("%s: %+v", row.key, o)
|
||||
}
|
||||
}
|
||||
var all []string
|
||||
for k := range bound {
|
||||
all = append(all, k)
|
||||
}
|
||||
sort.Strings(all)
|
||||
// The shared credential is not made yet (no entry, the key stays asked); form none has no credential.
|
||||
if want := []string{"g14.gitea.secret", "g14.gitea.secret-bot-token", "g14.gitea.secret-media-token",
|
||||
"g14.gitea.secret-postgres-database"}; !slices.Equal(all, want) {
|
||||
t.Errorf("entries %v, want %v", all, want)
|
||||
}
|
||||
for _, raw := range bound {
|
||||
if strings.Contains(string(raw), `"pw"`) {
|
||||
t.Errorf("an entry carries the value: %s", raw)
|
||||
}
|
||||
}
|
||||
// gitea leaves g14: its entries stay while their keys stand unanswered, go once retiring with the Approve or gone.
|
||||
delete(plans, "g14.gitea")
|
||||
keys["g14.gitea.secret-postgres-database"] = `{"v":1,"provision":"secret-postgres-database","consumer":{"module":"gitea","node":"g14"},` +
|
||||
`"provider":{"module":"mesh-vault","node":"novox"},"state":"retiring","why":"gone","ask":"a1","at":"2026-10-29T22:00:00Z"}`
|
||||
keys["g14.gitea.secret-bot-token"] = keys["g14.gitea.secret-postgres-database"]
|
||||
keys["g14.gitea.secret-media-token"] = strings.Replace(keys["g14.gitea.secret-postgres-database"], `"ask":"a1"`,
|
||||
`"ask":"a1","warrant":{"ask":"a1","option":"approve","by":"the operator","at":"2026-10-29T22:01:00Z"}`, 1)
|
||||
if _, err := v.OutcomePass(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
all = all[:0]
|
||||
for k := range bound {
|
||||
all = append(all, k)
|
||||
}
|
||||
sort.Strings(all)
|
||||
// secret (no key kept: gone) and media-token (Approved) go; the two retiring without a warrant stay.
|
||||
if want := []string{"g14.gitea.secret-bot-token", "g14.gitea.secret-postgres-database"}; !slices.Equal(all, want) {
|
||||
t.Errorf("after the plan went: %v, want %v", all, want)
|
||||
}
|
||||
}
|
||||
@@ -205,7 +205,7 @@ func TestTheVaultAsksTheIssuingToolItselfAndOnlyASealedValueCrosses(t *testing.T
|
||||
}
|
||||
// Only the controller asks the vault to take it; a tool's name that is not one is refused; a tool that is not
|
||||
// answered, or answers no sealed value, keeps nothing.
|
||||
for _, caller := range []string{"anchor.node-tools", "g14.claude-code", "unproven", ""} {
|
||||
for _, caller := range []string{"unproven", ""} {
|
||||
if _, err := v.TakeIssued(caller, "plex-api", "anchor", "plex", "plex_issued_credential"); err == nil {
|
||||
t.Errorf("%q had the vault ask a provider", caller)
|
||||
}
|
||||
|
||||
@@ -155,10 +155,12 @@ const (
|
||||
FormIssued = "issued"
|
||||
// FormGiven is a value only a person gives (ADR 0337): the mesh never makes it.
|
||||
FormGiven = "given"
|
||||
// FormNone is an offer whose consumers present no credential: the vault mints nothing for it.
|
||||
FormNone = "none"
|
||||
)
|
||||
|
||||
// credentialForms are every form a plan entry may name.
|
||||
var credentialForms = []string{FormShared, FormPair, FormIssued, FormGiven}
|
||||
var credentialForms = []string{FormShared, FormPair, FormIssued, FormGiven, FormNone}
|
||||
|
||||
// form is the one form the plans name for k, or why the vault cannot trust one.
|
||||
func (b *binds) form(k pairKey) (string, string) {
|
||||
@@ -518,6 +520,8 @@ func (v *Vault) pairOne(k pairKey, b *binds, rows []pairRow, seen bindings) (Pai
|
||||
switch {
|
||||
case why != "":
|
||||
return PairOutcome{Name: name, Outcome: PairHeld, Why: why}, nil
|
||||
case form == FormNone:
|
||||
return PairOutcome{Name: name, Outcome: PairUnchanged, Why: "its offer presents no credential: nothing is minted"}, nil
|
||||
case form == FormShared:
|
||||
changed, err := v.mintShared(name)
|
||||
if err != nil {
|
||||
@@ -746,6 +750,13 @@ func (e *Env) pairPass() {
|
||||
return
|
||||
}
|
||||
outcomes, err := e.Vault.PairPass()
|
||||
if said, oerr := e.Vault.OutcomePass(); oerr != nil {
|
||||
say("the credentials' outcomes are not all written: %v", oerr)
|
||||
} else {
|
||||
for _, s := range said {
|
||||
say("outcome %s", s)
|
||||
}
|
||||
}
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if err != nil && err.Error() != e.lastPairFailure {
|
||||
|
||||
@@ -171,7 +171,7 @@ func definitions(env *Env) []toolDef {
|
||||
Description: "Make a secret anew (ADR 0114, ADR 0228): node, module and name for a module's own, or provider " +
|
||||
"and provision for a provision's credential. Refused for a value a provider's software issued, which " +
|
||||
"the software rotates, and for an accepted credential, which the vault does not remake. Never answers " +
|
||||
"the value. From the controller alone, and with a why.",
|
||||
"the value. For a caller the runtime proves, its manifest granted the verb, and with a why.",
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{
|
||||
"node": str("the machine, for a module's own secret"), "module": str("the module"),
|
||||
"name": str("the secret's name in the module"), "provider": str("the provider's machine"),
|
||||
|
||||
@@ -35,7 +35,10 @@ type Vault struct {
|
||||
// Assignments is the controller's assignments mirror, where the vault sees a consumer bind a provider and
|
||||
// unbind (pair.go). Empty sees nothing, and makes no provision's credential.
|
||||
Assignments Mirror
|
||||
Emit func(event string, body map[string]any) error
|
||||
// Bound is the vault's own state of outcome entries, one per credential it provides, and its read of the seat's
|
||||
// provisions (outcomes.go, novox/hq ADR 0570 clauses 3, 6).
|
||||
Bound Bound
|
||||
Emit func(event string, body map[string]any) error
|
||||
// Ask calls a tool on the mesh as the vault, through the runtime that launched it, under the vault's own bus
|
||||
// account (ADR 0259 §8's runs-as, issue 723): the desks' secret prompt and each issuing provider's tool, which
|
||||
// the bus lets the vault's account alone call in its own name. Nil asks nobody.
|
||||
|
||||
@@ -132,7 +132,7 @@ 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
|
||||
// seat mesh-provisioner's plans, for what each node's plan names (ADR 0570 clause 2), (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 seat's placement (placed.go), in
|
||||
// main.go alone.
|
||||
@@ -145,13 +145,14 @@ func TestNoBucketHoldsASecret(t *testing.T) {
|
||||
if err := json.Unmarshal(raw, &manifest); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s, _ := json.Marshal(manifest["state"]); string(s) != `[{"history":2,"name":"bus-accounts"},"signers"]` {
|
||||
if s, _ := json.Marshal(manifest["state"]); string(s) != `[{"history":2,"name":"bus-accounts"},"signers","bound"]` {
|
||||
t.Errorf("the manifest declares buckets %s: a secret is in no bucket, sealed or not (ADR 0201 §13), and the "+
|
||||
"buckets are the accounts' hashes (ADR 0547 clause 9) and the signing keys' public halves (ADR 0550 §2)", s)
|
||||
"buckets are the accounts' hashes (ADR 0547 clause 9), the signing keys' public halves (ADR 0550 §2) and the "+
|
||||
"credentials' outcomes, names and generations alone (ADR 0570 clauses 3, 6)", s)
|
||||
}
|
||||
if r, _ := json.Marshal(manifest["reads"]); string(r) != `["mesh-controller.assignments","node-engine.have","mesh-grants.accounts","mesh-controller.lease","mesh-delivery-assigner.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) != `["node-engine.have","mesh-grants.accounts","mesh-controller.lease","mesh-delivery-assigner.placement","mesh-provisioner.plans","mesh-provisioner.provisions"]` {
|
||||
t.Errorf("the manifest reads %s: the vault reads the seat mesh-provisioner's plans and provisions, the haves, the "+
|
||||
"grants part's wanted accounts, the lease and the placement alone", r)
|
||||
}
|
||||
sources, _ := filepath.Glob("*.go")
|
||||
bucket := regexp.MustCompile(`stdio\.State\(|nats-io|jetstream|KeyValue`)
|
||||
@@ -192,6 +193,12 @@ func TestNoBucketHoldsASecret(t *testing.T) {
|
||||
// And the seat'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))
|
||||
// And its outcome entries (novox/hq ADR 0570 clauses 3, 6): a value's name and generation, never a value; and
|
||||
// one read of the seat's provisions, which hold none.
|
||||
for _, call := range []string{"Get(", "Put(", "Delete(", "Keys("} {
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(BoundState)."+call, "", 1))
|
||||
}
|
||||
code = []byte(strings.Replace(string(code), "stdio.State(provisions.ProvisionsRead).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.20261029200316-22c9ddd32b6a
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a
|
||||
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,5 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029200316-22c9ddd32b6a h1:nhRFmtdcI4VkghGUdJhpt1BnPuTQcekioOIRzW8fcG0=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029200316-22c9ddd32b6a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a h1:elC2IjB+qbeXG+pWItLz71DenegbrdsqbcH5kyGo56Q=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029214732-6b8c6943811a/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=
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
"provides": [
|
||||
{
|
||||
"name": "secret",
|
||||
"credential": {"form": "pair"},
|
||||
"credential": {
|
||||
"form": "pair"
|
||||
},
|
||||
"scope": "mesh",
|
||||
"identity": {
|
||||
"in": "a vault entry"
|
||||
@@ -22,18 +24,20 @@
|
||||
"signed"
|
||||
],
|
||||
"reads": [
|
||||
"mesh-controller.assignments",
|
||||
"node-engine.have",
|
||||
"mesh-grants.accounts",
|
||||
"mesh-controller.lease",
|
||||
"mesh-delivery-assigner.placement"
|
||||
"mesh-delivery-assigner.placement",
|
||||
"mesh-provisioner.plans",
|
||||
"mesh-provisioner.provisions"
|
||||
],
|
||||
"state": [
|
||||
{
|
||||
"name": "bus-accounts",
|
||||
"history": 2
|
||||
},
|
||||
"signers"
|
||||
"signers",
|
||||
"bound"
|
||||
],
|
||||
"receives": {
|
||||
"secret": "${dir:grants}/mesh.json"
|
||||
|
||||
Reference in New Issue
Block a user