mesh/merge-gate head bff1ac14a198 merged into trunk 49c9002c04d3 beside mesh-controller#355@8c381172 — pass: builds mesh-host → ace, g14, novox, shanks; no…
mesh/repo-check head bff1ac14a198 merged into trunk 49c9002c04d3 beside mesh-controller#355@8c381172 — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group release/57-config-and-provisioning delivered: every member is delivered
142 lines
6.8 KiB
Go
142 lines
6.8 KiB
Go
package link
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
)
|
|
|
|
// The move's ask (design 57's V3, issue 676): a hand-over's fields and the row's own in one signed object, under a
|
|
// signing context of its own. The controller's side holds the same names and words.
|
|
func TestTheMovesAskKeepsItsFieldNamesAndContext(t *testing.T) {
|
|
body, _ := json.Marshal(MoveSecretAsk{Module: "m", Name: "n", Provider: "p", Provision: "q", Consumer: "c",
|
|
ConsumerModule: "cm", Local: "l", Origin: "made", Generation: "g", MadeAt: time.Unix(1, 0).UTC(),
|
|
Sealed: "s", SealedTo: "k"})
|
|
if got := keysOf(t, body); got != "consumer consumer_module generation local made_at module name origin provider "+
|
|
"provision sealed sealed_to" {
|
|
t.Fatalf("the move's fields are %q", got)
|
|
}
|
|
if AskMoveSecretSubject("ace") != "mesh.node.ace.ask.move-secret" {
|
|
t.Fatalf("the subject is %q", AskMoveSecretSubject("ace"))
|
|
}
|
|
if MoveSecretContext != "novox-mesh move-secret v1\n" {
|
|
t.Fatalf("the signing context is %q", MoveSecretContext)
|
|
}
|
|
answer, _ := json.Marshal(MoveSecretAnswer{Said: "s", Refused: "r", Secret: "own/a/b/c", Generation: 1, Unchanged: true})
|
|
if got := keysOf(t, answer); got != "generation refused said secret unchanged" {
|
|
t.Fatalf("the answer's fields are %q", got)
|
|
}
|
|
}
|
|
|
|
// signedMove is a move's ask as the controller makes it.
|
|
func signedMove(t *testing.T, key ed25519.PrivateKey, ask HandOverAsk, move MoveSecretAsk) []byte {
|
|
t.Helper()
|
|
raw, _ := json.Marshal(struct {
|
|
HandOverAsk
|
|
MoveSecretAsk
|
|
}{ask, move})
|
|
body, _ := json.Marshal(SignedHandOver{Ask: raw, Signature: ed25519.Sign(key, append([]byte(MoveSecretContext), raw...))})
|
|
return body
|
|
}
|
|
|
|
// A move is believed as every signed ask is, then names one row this machine holds — its own secret, or a pair
|
|
// credential with this machine at one end — with a value sealed to it and an origin the store has; only then is the
|
|
// handler given it. A hand-over's signature is not a move's.
|
|
func TestAMoveIsTakenOnlyForARowThisMachineHolds(t *testing.T) {
|
|
m, key := aMember(t)
|
|
now := time.Now()
|
|
var given []MoveSecret
|
|
handle := func(s MoveSecret) (MovedSecret, error) {
|
|
given = append(given, s)
|
|
return MovedSecret{Secret: "own/" + m.Node + "/postgres/superuser-password", Generation: 1}, nil
|
|
}
|
|
check := HandOverCheck{Signer: m.Signer, Node: m.Node, Nonces: &Nonces{}}
|
|
ask := func(nonce string) HandOverAsk {
|
|
return HandOverAsk{Node: m.Node, By: "the move", Expires: now.Add(HandOverGood), Nonce: nonce}
|
|
}
|
|
own := MoveSecretAsk{Module: "postgres", Name: "superuser-password", Origin: "made", Sealed: "c2VhbGVk"}
|
|
for why, body := range map[string][]byte{
|
|
"a hand-over's": signedAsk(t, key, ask(aNonce+"01")),
|
|
"no value": signedMove(t, key, ask(aNonce+"02"), MoveSecretAsk{Module: "postgres", Name: "x", Origin: "made"}),
|
|
"no origin": signedMove(t, key, ask(aNonce+"03"), MoveSecretAsk{Module: "postgres", Name: "x", Sealed: "c2Vh"}),
|
|
"both shapes": signedMove(t, key, ask(aNonce+"04"), MoveSecretAsk{Module: "postgres", Name: "x", Provider: m.Node,
|
|
Provision: "database", Consumer: "shanks", ConsumerModule: "gitea", Origin: "made", Sealed: "c2Vh"}),
|
|
"neither end": signedMove(t, key, ask(aNonce+"05"), MoveSecretAsk{Provider: "anchor", Provision: "database",
|
|
Consumer: "shanks", ConsumerModule: "gitea", Origin: "made", Sealed: "c2Vh"}),
|
|
"a path": signedMove(t, key, HandOverAsk{Node: m.Node, Path: "/etc", By: "x", Expires: now.Add(HandOverGood),
|
|
Nonce: aNonce + "06"}, own),
|
|
"two lines": signedMove(t, key, ask(aNonce+"07"), MoveSecretAsk{Module: "postgres", Name: "x\ny", Origin: "made",
|
|
Sealed: "c2Vh"}),
|
|
} {
|
|
if a := AnswerMoveSecret(check, handle, body); a.Refused == "" || !strings.HasSuffix(a.Refused, "nothing was moved") {
|
|
t.Errorf("%s: not refused, or refused without saying nothing was moved: %+v", why, a)
|
|
}
|
|
}
|
|
if len(given) != 0 {
|
|
t.Fatalf("a refused ask reached the move: %+v", given)
|
|
}
|
|
a := AnswerMoveSecret(check, handle, signedMove(t, key, ask(aNonce+"08"), own))
|
|
if a.Refused != "" || a.Secret != "own/"+m.Node+"/postgres/superuser-password" || a.Generation != 1 || a.Said == "" ||
|
|
len(given) != 1 || given[0].Name != "superuser-password" || given[0].By != "the move" {
|
|
t.Fatalf("a good ask: %+v %+v", a, given)
|
|
}
|
|
pair := MoveSecretAsk{Provider: "anchor", Provision: "database", Consumer: m.Node, ConsumerModule: "gitea",
|
|
Origin: "accepted", Sealed: "c2Vh"}
|
|
if a := AnswerMoveSecret(check, handle, signedMove(t, key, ask(aNonce+"09"), pair)); a.Refused != "" {
|
|
t.Fatalf("a pair this machine consumes was refused: %+v", a)
|
|
}
|
|
// What the move refuses is said, and nothing else.
|
|
refusing := func(MoveSecret) (MovedSecret, error) { return MovedSecret{}, errors.New("the vault said no") }
|
|
if a := AnswerMoveSecret(check, refusing, signedMove(t, key, ask(aNonce+"10"), own)); !strings.Contains(a.Refused,
|
|
"the vault said no") || a.Secret != "" {
|
|
t.Fatalf("a refused move: %+v", a)
|
|
}
|
|
}
|
|
|
|
// **Against a real server**: the move's signed ask on the node's own subject is answered with what the handler
|
|
// moved, and the engine asks the vault on the subject that names it, `node~<machine>` last.
|
|
func TestNatsAMoveIsAskedOfTheNodeAndTheVaultIsAskedInItsName(t *testing.T) {
|
|
conn, js := aBus(t)
|
|
m, key := aMember(t)
|
|
vault, err := conn.Subscribe("mesh.seat.mesh-vault.call.move-in.node~"+m.Node, func(msg *nats.Msg) {
|
|
_ = msg.Respond([]byte(`{"result":{"call":"c1","seal_to":"k"}}`))
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer vault.Unsubscribe()
|
|
l := &natsLink{conn: conn, js: js, node: m.Node, signer: m.Signer}
|
|
q := &Queue{Membership: m}
|
|
q.attach(t.Context(), l)
|
|
l.HearMoveSecrets(func(s MoveSecret) (MovedSecret, error) {
|
|
raw, err := q.AskVault(t.Context(), "move-in", map[string]string{"module": s.Module})
|
|
if err != nil || !strings.Contains(string(raw), `"call":"c1"`) {
|
|
return MovedSecret{}, fmt.Errorf("the vault answered %s, %v", raw, err)
|
|
}
|
|
return MovedSecret{Secret: "own/" + s.Node + "/" + s.Module + "/" + s.Name, Generation: 1}, nil
|
|
})
|
|
defer l.Close()
|
|
if err := conn.Flush(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ask := HandOverAsk{Node: m.Node, By: "the move", Expires: time.Now().Add(HandOverGood), Nonce: strings.Repeat("ab", 16)}
|
|
reply, err := conn.Request(AskMoveSecretSubject(m.Node), signedMove(t, key, ask,
|
|
MoveSecretAsk{Module: "postgres", Name: "superuser-password", Origin: "made", Sealed: "c2Vh"}), 5*time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var said MoveSecretAnswer
|
|
if err := json.Unmarshal(reply.Data, &said); err != nil || said.Secret != "own/"+m.Node+"/postgres/superuser-password" {
|
|
t.Fatalf("answered %s, %v", reply.Data, err)
|
|
}
|
|
if _, err := (&Queue{Membership: Membership{Node: "nowhere"}}).AskVault(t.Context(), "move-in", nil); !errors.Is(err, ErrNotAsked) {
|
|
t.Fatalf("a queue with no link asked the vault: %v", err)
|
|
}
|
|
}
|