The answer names its proposal and the revision of the node's own candidate key; a node with no key in a proposal is not asked. The watch for its own placement key is tried again until the seat's state exists, so the node switches when its key is written.
118 lines
6.2 KiB
Go
118 lines
6.2 KiB
Go
package ownresync
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// Issue 1126, cause C4: after a shadow answer saying the vault holds another value of an own secret than runs, the node
|
|
// hands the vault what runs, in the verb's two steps, once per running value; and only while it applies the
|
|
// controller's compositions and runs the one its want names.
|
|
func TestTheNodeHandsTheVaultWhatRunsOfADriftedOwnSecret(t *testing.T) {
|
|
const running = "the-value-that-runs"
|
|
digest := "sha256:" + strings.Repeat("a", 64)
|
|
fp := "src2:" + strings.Repeat("b", 64)
|
|
sum := sha256.Sum256([]byte(running))
|
|
answers := map[string]delivery.CandidateAnswer{"g": {Proposal: "p-1", Revision: 1, Verdict: delivery.CandidateDiffers,
|
|
Differs: []delivery.ModuleDiffers{{Module: "only-office", Why: "the resource differs", Bodies: []delivery.ResourceBodies{
|
|
{ID: "only-office.needs-jwt", Composed: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`, Running: `{"sealed":"` + delivery.MaskedAnotherValue + `"}`},
|
|
{ID: "only-office.needs-salt", Composed: `{"sealed":"<masked: another seal of the same value>"}`}}}}}}
|
|
for name, row := range map[string]struct {
|
|
composes string
|
|
runs string // the have's composition digest
|
|
refuse string // the vault's refusal
|
|
asks int // the vault's verb asked, both passes
|
|
says string
|
|
// Issue 1126, 2026-10-28: a read or an ask the bus did not answer is no answer: taken again (Retry), never kept.
|
|
wantRead string // the want's read: "" answers it, "fails" the bus does not answer, "absent" no key
|
|
retry bool
|
|
}{
|
|
"applies the controller's, runs its want": {delivery.ComposesController, digest, "", 2, "generation 1 replaced by 2", "", false},
|
|
"composes its own": {delivery.ComposesOwn, digest, "", 0, "composes its own", "", false},
|
|
"not decided yet": {"", digest, "", 0, "", "", false},
|
|
"runs not yet what its want names": {delivery.ComposesController, "sha256:" + strings.Repeat("c", 64), "", 0, "not yet the composition", "", false},
|
|
"the vault refuses": {delivery.ComposesController, digest, "switched off", 1, "switched off", "", false},
|
|
"the vault does not answer": {delivery.ComposesController, digest, "nats: no responders available for request", 2, "asked again", "", true},
|
|
"the want's read does not answer": {delivery.ComposesController, digest, "", 0, "asked again", "fails", true},
|
|
"it has no want": {delivery.ComposesController, digest, "", 0, "it has no want", "absent", false},
|
|
} {
|
|
var asked []map[string]any
|
|
var said []string
|
|
want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Composed: digest})
|
|
have, _ := delivery.EncodeHave(delivery.Have{FP: fp, Composed: row.runs, Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
|
|
r := New(Env{Node: "novox", Composes: func() string { return row.composes },
|
|
Running: func() []delivery.Composed {
|
|
return []delivery.Composed{{Node: "novox", Module: "only-office", FP: fp, Resources: []json.RawMessage{
|
|
// As novox runs it (its shadow answer, 2026-10-28): the id carries the module's name.
|
|
json.RawMessage(`{"id":"only-office.needs-jwt","module":"only-office","type":"file",` +
|
|
`"path":"/var/lib/only-office/jwt.secret","sealed":"box-of-the-running-value"}`)}}}
|
|
},
|
|
Get: func(_ context.Context, bucket, key string) ([]byte, error) {
|
|
switch {
|
|
case bucket == delivery.AssignerWantsBucket && key == "want.novox.only-office" && row.wantRead == "fails":
|
|
return nil, errors.New("the bucket mesh-delivery-assigner_wants: nats: no responders available for request")
|
|
case bucket == delivery.AssignerWantsBucket && key == "want.novox.only-office" && row.wantRead == "absent":
|
|
return nil, nil
|
|
case bucket == delivery.AssignerWantsBucket && key == "want.novox.only-office":
|
|
return want, nil
|
|
case bucket == delivery.HaveBucket && key == "novox.only-office":
|
|
return have, nil
|
|
}
|
|
return nil, nil
|
|
},
|
|
Unseal: func(sealed string) ([]byte, error) {
|
|
if sealed != "box-of-the-running-value" {
|
|
return nil, errors.New("not this machine's")
|
|
}
|
|
return []byte(running), nil
|
|
},
|
|
Seal: func(to string, value []byte) (string, error) { return to + "(" + string(value) + ")", nil },
|
|
Ask: func(_ context.Context, verb string, args map[string]any) (json.RawMessage, error) {
|
|
asked = append(asked, args)
|
|
if verb != "move-in" || args["replace_running"] != true {
|
|
t.Errorf("%s: asked %s with %v, not move-in with replace_running", name, verb, args)
|
|
}
|
|
if row.refuse != "" {
|
|
return nil, errors.New(row.refuse)
|
|
}
|
|
if args["call"] == nil {
|
|
if args["module"] != "only-office" || args["name"] != "jwt" || args["node"] != nil {
|
|
t.Errorf("%s: the first step names %v", name, args)
|
|
}
|
|
return json.RawMessage(`{"call":"c1","seal_to":"key","secret":"own/novox/only-office/jwt"}`), nil
|
|
}
|
|
if args["sealed"] != "key("+running+")" {
|
|
t.Errorf("%s: handed %v, not what runs sealed to the call's key", name, args["sealed"])
|
|
}
|
|
return json.RawMessage(`{"secret":"own/novox/only-office/jwt","from_generation":1,"to_generation":2,` +
|
|
`"fingerprint":"sha256:` + hex.EncodeToString(sum[:]) + `"}`), nil
|
|
},
|
|
Say: func(s string) { said = append(said, s) }})
|
|
first := r.Take(context.Background(), answers)
|
|
again := r.Take(context.Background(), answers)
|
|
if len(asked) != row.asks {
|
|
t.Errorf("%s: the vault asked %d times, want %d: %v", name, len(asked), row.asks, asked)
|
|
}
|
|
if retry := len(first) == 1 && first[0].Retry; retry != row.retry {
|
|
t.Errorf("%s: taken again %v, want %v: %+v", name, retry, row.retry, first)
|
|
}
|
|
if row.retry && (len(again) != 1 || !again[0].Retry || again[0].Said != "") || !row.retry && len(again) != 0 {
|
|
t.Errorf("%s: the same answer did or said something again: %+v", name, again)
|
|
}
|
|
all := strings.Join(said, "\n")
|
|
if row.says == "" && (len(first) != 0 || all != "") || row.says != "" && (len(first) != 1 || !strings.Contains(all, row.says)) {
|
|
t.Errorf("%s: did %+v and said %q, want %q", name, first, all, row.says)
|
|
}
|
|
if strings.Contains(all, running) {
|
|
t.Errorf("%s: the value is in what the node said", name)
|
|
}
|
|
}
|
|
}
|