Review of L2: the vault's own fails closed: a pair credential's consumer is read standing, the shared value goes only to a holder its record names, and unread assignments refuse, never hand (C1, C2); the placement check pins mesh-sdk 3968849 (issue 1126, ADR 0550 §5)
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 1faf31706919 on 5b75233406fe | novox/mesh-catalog#505 in group feat/1126-nodes-compose
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 1faf31706919 on 5b75233406fe | novox/mesh-catalog#505 in group feat/1126-nodes-compose
This commit is contained in:
@@ -2,4 +2,4 @@ module checks/placement
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026183837-9713c3023a18
|
||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026190832-3968849b1b36
|
||||
|
||||
@@ -14,6 +14,8 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026182519-53c2def1a520 h1:twiziR88
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026182519-53c2def1a520/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026183837-9713c3023a18 h1:HWNPfnT9YT9O6xHPLy2y6Q+LYT8XRjvLnZw9O61NETk=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026183837-9713c3023a18/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026190832-3968849b1b36 h1:aBkmFUq/zAVdR3PzfgjjC9AxSymNl3eVC7UEZv9tZ0Q=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026190832-3968849b1b36/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
|
||||
|
||||
@@ -140,6 +140,7 @@ func TestOnARealBusANodeEngineStatesOnlyItsOwnKey(t *testing.T) {
|
||||
t.Cleanup(s.Shutdown)
|
||||
|
||||
v := newVault(t, nil)
|
||||
v.Askers = Declared{Read: assignments{"ace.web": `{"node":"ace","module":"web"}`}.read, Store: v.Store}
|
||||
tools := map[string]toolDef{"node-key": toolNamed(t, definitions(&Env{Vault: v.Vault}), Seat+".node-key"),
|
||||
"own": toolNamed(t, definitions(&Env{Vault: v.Vault}), Seat+".own")}
|
||||
vaultConn := connectAs(t, s.ClientURL(), "novox.mesh-vault")
|
||||
|
||||
@@ -9,6 +9,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -83,10 +84,17 @@ func (v *Vault) credentialRefusal(node string, c CredentialAsked) string {
|
||||
if c.Consumer != node {
|
||||
return fmt.Sprintf("%s asks as a consumer on %s, and is not that machine", node, c.Consumer)
|
||||
}
|
||||
if d, ok := v.Askers.(Declared); ok && d.Read != nil {
|
||||
if raw, err := standing(d.Read)(node + "." + c.ConsumerModule); err == nil && raw == nil {
|
||||
return fmt.Sprintf("%s is not on %s: neither assigned there nor required by a module there", c.ConsumerModule, node)
|
||||
}
|
||||
// The consuming module stands on the machine: read, never assumed (an assignment that cannot be read refuses).
|
||||
read, why := v.assignmentReader(node)
|
||||
if why != "" {
|
||||
return why
|
||||
}
|
||||
raw, err := read(node + "." + c.ConsumerModule)
|
||||
if err != nil {
|
||||
return fmt.Sprintf(notYetPrefix+"the assignment %s.%s could not be read: %v", node, c.ConsumerModule, err)
|
||||
}
|
||||
if raw == nil {
|
||||
return fmt.Sprintf("%s is not on %s: neither assigned there nor required by a module there", c.ConsumerModule, node)
|
||||
}
|
||||
return ""
|
||||
case "provider":
|
||||
@@ -98,6 +106,47 @@ func (v *Vault) credentialRefusal(node string, c CredentialAsked) string {
|
||||
return fmt.Sprintf("the role %q is not consumer or provider", c.Role)
|
||||
}
|
||||
|
||||
// assignmentReader is the vault's reader of the assignments, or why it has none: the rule `own` keeps reads them,
|
||||
// and without them it refuses rather than hands (a rule that cannot be read is no rule).
|
||||
func (v *Vault) assignmentReader(node string) (AssignmentReader, string) {
|
||||
d, ok := v.Askers.(Declared)
|
||||
if !ok || d.Read == nil {
|
||||
return nil, "the vault cannot read the assignments, so it cannot tell what " + node + "'s modules are; nothing is handed"
|
||||
}
|
||||
return standing(d.Read), ""
|
||||
}
|
||||
|
||||
// sharedRefusal says why node may not have the provision's one shared value `provision/<provider>/<provision>` for c,
|
||||
// or "": the vault's record of who holds it names the consumer's module among its consumers, or the machine as its
|
||||
// provider. Without that record nothing is handed: the shared value is not one any machine may have by naming a
|
||||
// provider (ADR 0550 §5).
|
||||
func (v *Vault) sharedRefusal(node string, c CredentialAsked) (string, error) {
|
||||
name := "provision/" + c.Provider + "/" + c.Provision
|
||||
sh, ok, err := v.Store.ReadShared(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Sprintf("the vault keeps no record of who holds %s", name), nil
|
||||
}
|
||||
switch c.Role {
|
||||
case "consumer":
|
||||
want := consumerOf(c.Consumer, c.ConsumerModule, c.Local)
|
||||
for _, held := range sh.Consumers {
|
||||
if held == want {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s is held by %s, not by %s", name, strings.Join(sh.Consumers, ", "), want), nil
|
||||
case "provider":
|
||||
if sh.Provider == node {
|
||||
return "", nil
|
||||
}
|
||||
return fmt.Sprintf("%s is provided from %s, not %s", name, sh.Provider, node), nil
|
||||
}
|
||||
return fmt.Sprintf("the role %q is not consumer or provider", c.Role), nil
|
||||
}
|
||||
|
||||
// composeOrigin is the vault's origin of a value as the composition reads it: made by the mesh, or given (accepted at a
|
||||
// terminal, typed at a desk, issued by a provider's software).
|
||||
func composeOrigin(origin string) string {
|
||||
@@ -138,7 +187,11 @@ func (v *Vault) OwnOfNode(caller string, asked ...CredentialAsked) (OwnAnswer, e
|
||||
if len(parts) != 4 || parts[0] != "own" || parts[1] != node {
|
||||
continue
|
||||
}
|
||||
if v.handed(node, parts[2]) {
|
||||
handed, err := v.handed(node, parts[2])
|
||||
if err != nil {
|
||||
return OwnAnswer{}, err
|
||||
}
|
||||
if handed {
|
||||
continue
|
||||
}
|
||||
value, err := seal.Open(r.AtRest, v.Own.PrivateKey)
|
||||
@@ -167,7 +220,8 @@ func (v *Vault) OwnOfNode(caller string, asked ...CredentialAsked) (OwnAnswer, e
|
||||
c.Provision, c.Provider, c.ConsumerModule, c.Consumer, why)
|
||||
}
|
||||
found := false
|
||||
for _, name := range credentialNames(c) {
|
||||
names := credentialNames(c)
|
||||
for i, name := range names {
|
||||
r, held, err := v.Store.Read(name)
|
||||
if err != nil {
|
||||
return OwnAnswer{}, err
|
||||
@@ -175,6 +229,17 @@ func (v *Vault) OwnOfNode(caller string, asked ...CredentialAsked) (OwnAnswer, e
|
||||
if !held {
|
||||
continue
|
||||
}
|
||||
if i == len(names)-1 {
|
||||
// The provision's one shared value: handed only to a machine the record of its holders names.
|
||||
why, err := v.sharedRefusal(node, c)
|
||||
if err != nil {
|
||||
return OwnAnswer{}, err
|
||||
}
|
||||
if why != "" {
|
||||
v.log("%s is not handed %s: %s", node, name, why)
|
||||
continue
|
||||
}
|
||||
}
|
||||
value, err := seal.Open(r.AtRest, v.Own.PrivateKey)
|
||||
if err != nil {
|
||||
return OwnAnswer{}, fmt.Errorf("%s does not open with the vault's key: %w", name, err)
|
||||
@@ -200,16 +265,24 @@ func (v *Vault) OwnOfNode(caller string, asked ...CredentialAsked) (OwnAnswer, e
|
||||
}
|
||||
|
||||
// handed says the plan of module on node hands it to the provisioner, so its secrets are filled from `get`, not composed.
|
||||
// Where the plans cannot be read, nothing counts as handed: the composer leaves out a handed module's secrets itself.
|
||||
func (v *Vault) handed(node, module string) bool {
|
||||
d, ok := v.Askers.(Declared)
|
||||
if !ok || d.Read == nil {
|
||||
return false
|
||||
// A plan that cannot be read is an error, never "not handed": `own` refuses rather than hands a secret the provisioner
|
||||
// holds.
|
||||
func (v *Vault) handed(node, module string) (bool, error) {
|
||||
read, why := v.assignmentReader(node)
|
||||
if why != "" {
|
||||
return false, errors.New(why)
|
||||
}
|
||||
raw, err := standing(d.Read)(node + "." + module)
|
||||
if err != nil || raw == nil {
|
||||
return false
|
||||
raw, err := read(node + "." + module)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf(notYetPrefix+"the assignment %s.%s could not be read, so whether it is handed to the "+
|
||||
"provisioner is not known: %w", node, module, err)
|
||||
}
|
||||
if raw == nil {
|
||||
return false, nil
|
||||
}
|
||||
var a plannedSecrets
|
||||
return json.Unmarshal(raw, &a) == nil && a.Provisioning != nil
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return false, fmt.Errorf("the assignment %s.%s is not the controller's: %w", node, module, err)
|
||||
}
|
||||
return a.Provisioning != nil, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -11,7 +12,9 @@ import (
|
||||
// the key that machine's engine stated, about that machine alone; another caller, another machine's secrets and a
|
||||
// machine with no stated key get nothing.
|
||||
func TestOwnAnswersAMachineItsModulesSecretsSealedToItsKey(t *testing.T) {
|
||||
plans := assignments{"anchor.web": `{"node":"anchor","module":"web"}`, "laptop.web": `{"node":"laptop","module":"web"}`}
|
||||
v := newVault(t, nil)
|
||||
v.Askers = Declared{Read: plans.read, Store: v.Store}
|
||||
anchorPub, anchorPriv := aSealingKey(t)
|
||||
laptopPub, _ := aSealingKey(t)
|
||||
for _, m := range []struct {
|
||||
@@ -89,4 +92,30 @@ func TestOwnAnswersAMachineItsModulesSecretsSealedToItsKey(t *testing.T) {
|
||||
t.Errorf("%s asking %+v: %v", who, c, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Review of L2 (C1, C2): `own` fails closed. A provision's one shared value is handed only to a consumer the record
|
||||
// of its holders names, never to a machine that names its provider; assignments that cannot be read, or a vault
|
||||
// that reads none, refuse rather than hand.
|
||||
if _, err := v.keep("provision/anchor/redis", "shared-redis", "test", "", OriginMade); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := v.Store.WriteShared(Shared{Name: "provision/anchor/redis", Provider: "anchor", Provision: "redis",
|
||||
Consumers: []string{"anchor/web"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, err := v.OwnOfNode("node.laptop", none); err != nil || len(got.Credentials) != 0 || len(got.Missing) != 1 {
|
||||
t.Errorf("laptop, whose web the shared value's record does not name, got %+v, %v", got, err)
|
||||
}
|
||||
anchorRedis := CredentialAsked{Role: "consumer", Provision: "redis", Consumer: "anchor", ConsumerModule: "web", Provider: "anchor"}
|
||||
if got, err := v.OwnOfNode("node.anchor", anchorRedis); err != nil || len(got.Credentials) != 1 {
|
||||
t.Errorf("anchor's web, a holder the record names, got %+v, %v", got, err)
|
||||
}
|
||||
v.Askers = Declared{Read: func(string) ([]byte, error) { return nil, errors.New("the bucket did not answer") }, Store: v.Store}
|
||||
if _, err := v.OwnOfNode("node.anchor"); err == nil || !strings.Contains(err.Error(), "could not be read") {
|
||||
t.Errorf("own with the assignments unread: %v", err)
|
||||
}
|
||||
v.Askers = nil
|
||||
if _, err := v.OwnOfNode("node.anchor", anchorRedis); err == nil || !strings.Contains(err.Error(), "cannot read the assignments") {
|
||||
t.Errorf("own with no reader of the assignments: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user