Name a provision's credential as the vault's own names it, read each node's provisions by its engine, and give mesh-config's held values their layout in the sdk, so the vault, the engine and the composition checks read one form (issue 1954, issue 1953 D7)
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
|||||||
"git.novox.be/novox/mesh-sdk/go/announce"
|
"git.novox.be/novox/mesh-sdk/go/announce"
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||||
"github.com/nats-io/nats.go/jetstream"
|
"github.com/nats-io/nats.go/jetstream"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -77,7 +78,9 @@ func engineBeyond(user, g string) bool {
|
|||||||
strings.HasPrefix(g, "$SRV.") || strings.Contains(g, ".node-engine-"+node) ||
|
strings.HasPrefix(g, "$SRV.") || strings.Contains(g, ".node-engine-"+node) ||
|
||||||
strings.Contains(g, "KV_"+EngineActsBucket) || strings.HasPrefix(g, "$KV."+EngineActsBucket+"."+node+".") ||
|
strings.Contains(g, "KV_"+EngineActsBucket) || strings.HasPrefix(g, "$KV."+EngineActsBucket+"."+node+".") ||
|
||||||
// And its node's declared packages (novox/hq ADR 0565 clause 6).
|
// And its node's declared packages (novox/hq ADR 0565 clause 6).
|
||||||
strings.Contains(g, "KV_"+EnginePackagesBucket) || g == "$KV."+EnginePackagesBucket+"."+node
|
strings.Contains(g, "KV_"+EnginePackagesBucket) || g == "$KV."+EnginePackagesBucket+"."+node ||
|
||||||
|
// And the provisions of its node's modules, read (novox/hq ADR 0570 clause 6).
|
||||||
|
strings.Contains(g, "KV_"+provisions.ProvisionsBucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
// createsAnAccountsConsumer is the grants part's create and read of a durable consumer an account needs, by its stream
|
// createsAnAccountsConsumer is the grants part's create and read of a durable consumer an account needs, by its stream
|
||||||
@@ -652,6 +655,12 @@ func healthChecks(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// ADR 0570 clause 6: a node's engine reads its own node's provision keys, and no other node's, and writes none.
|
||||||
|
if u.Kind == KindNode && (!MayPublish(perms, "$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.a.keycloak.db") ||
|
||||||
|
MayPublish(perms, "$JS.API.DIRECT.GET.KV_mesh-provisioner_provisions.$KV.mesh-provisioner_provisions.b.keycloak.db") ||
|
||||||
|
MayPublish(perms, "$KV.mesh-provisioner_provisions.a.keycloak.db")) {
|
||||||
|
t.Errorf("node %s reads the provisions %v", u.Node, perms.Publish)
|
||||||
|
}
|
||||||
if u.Kind == KindNode && (!MayPublish(perms, "$KV.mesh-host_packages.a") || MayPublish(perms, "$KV.mesh-host_packages.b")) {
|
if u.Kind == KindNode && (!MayPublish(perms, "$KV.mesh-host_packages.a") || MayPublish(perms, "$KV.mesh-host_packages.b")) {
|
||||||
t.Errorf("node %s writes the packages keys %v", u.Node, perms.Publish)
|
t.Errorf("node %s writes the packages keys %v", u.Node, perms.Publish)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/provisions"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The node seat node-engine (novox/hq ADR 0563 §1): every node's engine holds it on its own bus account, and no module
|
// The node seat node-engine (novox/hq ADR 0563 §1): every node's engine holds it on its own bus account, and no module
|
||||||
@@ -52,6 +53,11 @@ func engineGrants(p Principal) (pub, sub []string) {
|
|||||||
if own, err := NodeKeysGrants(EngineActsBucket, p.Node); err == nil {
|
if own, err := NodeKeysGrants(EngineActsBucket, p.Node); err == nil {
|
||||||
pub = append(pub, own...)
|
pub = append(pub, own...)
|
||||||
}
|
}
|
||||||
|
// And the provisions its node's modules consume, its own node's keys alone, read (novox/hq ADR 0570 clause 6: the
|
||||||
|
// consuming engine checks the vault's name and generation of what it is handed).
|
||||||
|
if safeSubject.MatchString(p.Node) {
|
||||||
|
pub = append(pub, readGrants(provisions.ProvisionsBucket, p.Node+".>")...)
|
||||||
|
}
|
||||||
// And its node's declared packages, its own key alone (ADR 0565 clause 6).
|
// And its node's declared packages, its own key alone (ADR 0565 clause 6).
|
||||||
if safeSubject.MatchString(p.Node) {
|
if safeSubject.MatchString(p.Node) {
|
||||||
pub = append(pub, EnginePackagesGrants(p.Node)...)
|
pub = append(pub, EnginePackagesGrants(p.Node)...)
|
||||||
|
|||||||
@@ -91,5 +91,42 @@ type DeclaredPackages struct {
|
|||||||
Packages []PackageDeclared `json:"packages"`
|
Packages []PackageDeclared `json:"packages"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mesh-config's state of the approved values held for a pull request (novox/hq ADR 0565 clause 5): read by the checker
|
||||||
|
// and the assigner's composition check, which lay a pull request's own held values over its modules' values.
|
||||||
|
const (
|
||||||
|
MeshConfigKeptState = "kept"
|
||||||
|
MeshConfigKeptRead = "mesh-config." + MeshConfigKeptState
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeptValue is one approved held value, mesh-config's layout unchanged (modules/mesh-config pulls.go): kept apart from
|
||||||
|
// the values mesh-config resolves, laid only over the composition of its own pull request at its head.
|
||||||
|
type KeptValue struct {
|
||||||
|
Pull string `json:"pull"`
|
||||||
|
Head string `json:"head"`
|
||||||
|
Digest string `json:"declaration"`
|
||||||
|
Node string `json:"node"`
|
||||||
|
Module string `json:"module"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
// Value is the value held, as the operator approved it.
|
||||||
|
Value json.RawMessage `json:"value"`
|
||||||
|
By string `json:"by"`
|
||||||
|
Via string `json:"via"`
|
||||||
|
Why string `json:"why"`
|
||||||
|
// Approved is who approved it, as the proposal kept it.
|
||||||
|
Approved string `json:"approved"`
|
||||||
|
Proposal string `json:"proposal"`
|
||||||
|
At time.Time `json:"at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeldValueKey is a held value's key in `kept`: the pull request's key (ConfigCandidateKey's form), the node, the module and
|
||||||
|
// the config key, joined by dots, as mesh-config writes it.
|
||||||
|
func HeldValueKey(pull, node, module, key string) (string, error) {
|
||||||
|
p, err := ConfigCandidateKey(pull)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return p + "." + node + "." + module + "." + key, nil
|
||||||
|
}
|
||||||
|
|
||||||
// The assigner's state of a person's upgrade policy, one key per module (clause 7; catalogue.PersonPolicy).
|
// The assigner's state of a person's upgrade policy, one key per module (clause 7; catalogue.PersonPolicy).
|
||||||
const AssignerPoliciesState = "policies"
|
const AssignerPoliciesState = "policies"
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ func TestTheControlRecordsReadTheirWritersValues(t *testing.T) {
|
|||||||
`{"package":"nano","modules":["web"],"absent":true}]}`, &DeclaredPackages{}},
|
`{"package":"nano","modules":["web"],"absent":true}]}`, &DeclaredPackages{}},
|
||||||
{"a config candidate", `{"v":1,"pull":"novox/mesh-catalog#42","head":"abc","declares":{"mail":{"quota":{"kind":"config",` +
|
{"a config candidate", `{"v":1,"pull":"novox/mesh-catalog#42","head":"abc","declares":{"mail":{"quota":{"kind":"config",` +
|
||||||
`"default":10,"why":"a quota","type":"integer","minimum":1}}},"at":"2026-10-29T12:00:00Z"}`, &ConfigCandidate{}},
|
`"default":10,"why":"a quota","type":"integer","minimum":1}}},"at":"2026-10-29T12:00:00Z"}`, &ConfigCandidate{}},
|
||||||
|
{"a held value", `{"pull":"novox/mesh-catalog#42","head":"abc","declaration":"sha256:d","node":"novox","module":"mail",` +
|
||||||
|
`"key":"quota","value":20,"by":"person.jochen","via":"telegram","why":"more room","approved":"the operator",` +
|
||||||
|
`"proposal":"p-1","at":"2026-10-29T12:00:00Z"}`, &KeptValue{}},
|
||||||
{"the predefined list", `{"v":1,"commit":"abc","at":"2026-10-29T12:00:00Z","list":{"domain":{"type":"string"}}}`, &PredefinedKept{}},
|
{"the predefined list", `{"v":1,"commit":"abc","at":"2026-10-29T12:00:00Z","list":{"domain":{"type":"string"}}}`, &PredefinedKept{}},
|
||||||
} {
|
} {
|
||||||
if err := json.Unmarshal([]byte(c.raw), c.into); err != nil {
|
if err := json.Unmarshal([]byte(c.raw), c.into); err != nil {
|
||||||
@@ -32,6 +35,9 @@ func TestTheControlRecordsReadTheirWritersValues(t *testing.T) {
|
|||||||
t.Errorf("%s reads back as %s", c.name, again)
|
t.Errorf("%s reads back as %s", c.name, again)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if k, err := HeldValueKey("novox/mesh-catalog#42", "novox", "mail", "quota"); k != "novox.mesh-catalog.42.novox.mail.quota" || err != nil {
|
||||||
|
t.Errorf("a held value's key is %q, %v", k, err)
|
||||||
|
}
|
||||||
for pull, want := range map[string]string{"novox/mesh-catalog#42": "novox.mesh-catalog.42", "x#1": "", "a/b": "", "a/b.c#1": ""} {
|
for pull, want := range map[string]string{"novox/mesh-catalog#42": "novox.mesh-catalog.42", "x#1": "", "a/b": "", "a/b.c#1": ""} {
|
||||||
if got, err := ConfigCandidateKey(pull); got != want || (want == "") != (err != nil) {
|
if got, err := ConfigCandidateKey(pull); got != want || (want == "") != (err != nil) {
|
||||||
t.Errorf("%s keyed %q, %v", pull, got, err)
|
t.Errorf("%s keyed %q, %v", pull, got, err)
|
||||||
|
|||||||
@@ -92,6 +92,35 @@ func PlanKey(node, module string) (string, error) {
|
|||||||
return node + "." + module, nil
|
return node + "." + module, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The forms of a provision's credential, as a plan's entry names them (catalogue.PlanProvision.CredentialForm).
|
||||||
|
const (
|
||||||
|
FormPair = "pair"
|
||||||
|
FormShared = "shared"
|
||||||
|
FormIssued = "issued"
|
||||||
|
FormGiven = "given"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VaultName is the vault's name of a provision's credential in form, as its verb `own` names it and the provider asks
|
||||||
|
// it: `pair/<provider node>/<provision>/<consumer node>/<module>[/<local>]` for a login of the consumer's own (ADR 0158
|
||||||
|
// §11), `provision/<provider node>/<provision>` for the provider's one shared value, and `provision/<provider
|
||||||
|
// node>/<provision>/<consumer node>/<module>` for a holder's own issued or given value. Empty for a form with no
|
||||||
|
// credential.
|
||||||
|
func VaultName(form string, p Provision) string {
|
||||||
|
switch form {
|
||||||
|
case FormPair:
|
||||||
|
n := "pair/" + p.Provider.Node + "/" + p.Provision + "/" + p.Consumer.Node + "/" + p.Consumer.Module
|
||||||
|
if p.Local != "" {
|
||||||
|
n += "/" + p.Local
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
case FormShared:
|
||||||
|
return "provision/" + p.Provider.Node + "/" + p.Provision
|
||||||
|
case FormIssued, FormGiven:
|
||||||
|
return "provision/" + p.Provider.Node + "/" + p.Provision + "/" + p.Consumer.Node + "/" + p.Consumer.Module
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// End is one end of a provision: a module on a node.
|
// End is one end of a provision: a module on a node.
|
||||||
type End struct {
|
type End struct {
|
||||||
Module string `json:"module"`
|
Module string `json:"module"`
|
||||||
@@ -99,7 +128,9 @@ type End struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reference is a value the vault keeps, by its id and the generation of the value: never the value, sealed or not
|
// Reference is a value the vault keeps, by its id and the generation of the value: never the value, sealed or not
|
||||||
// (ADR 0349 §7). The holder writes the generation the vault says in its own entry (clause 6).
|
// (ADR 0349 §7). The holder writes the generation the vault says in its own entry (clause 6). ID is the vault's name of
|
||||||
|
// the credential exactly as its verb `own` names it (VaultName): the consuming node's engine checks both the name and
|
||||||
|
// the generation of what it is handed.
|
||||||
type Reference struct {
|
type Reference struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Generation int `json:"generation"`
|
Generation int `json:"generation"`
|
||||||
|
|||||||
@@ -72,6 +72,16 @@ func TestAProvisionIsAReferenceUnderItsConsumersKey(t *testing.T) {
|
|||||||
if k, err := PlanKey("novox", "restic"); k != "novox.restic" || err != nil {
|
if k, err := PlanKey("novox", "restic"); k != "novox.restic" || err != nil {
|
||||||
t.Errorf("a plan's key is %q, %v", k, err)
|
t.Errorf("a plan's key is %q, %v", k, err)
|
||||||
}
|
}
|
||||||
|
// A reference's id is the vault's name as its verb own names it, in each form (the host agent, 2026-10-29).
|
||||||
|
for form, want := range map[string]string{FormPair: "pair/novox/postgres-database/novox/keycloak", FormShared: "provision/novox/postgres-database",
|
||||||
|
FormIssued: "provision/novox/postgres-database/novox/keycloak", "none": ""} {
|
||||||
|
if got := VaultName(form, db); got != want {
|
||||||
|
t.Errorf("the %s name is %q", form, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := VaultName(FormPair, Provision{Provision: "p", Local: "r", Provider: End{Node: "a"}, Consumer: End{Node: "b", Module: "m"}}); got != "pair/a/p/b/m/r" {
|
||||||
|
t.Errorf("a local name's pair is %q", got)
|
||||||
|
}
|
||||||
if ProvisionsBucket != "mesh-provisioner_provisions" || PlansRead != "mesh-provisioner.plans" {
|
if ProvisionsBucket != "mesh-provisioner_provisions" || PlansRead != "mesh-provisioner.plans" {
|
||||||
t.Errorf("the states are %s and %s", ProvisionsBucket, PlansRead)
|
t.Errorf("the states are %s and %s", ProvisionsBucket, PlansRead)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user