mesh/merge-gate head 5a7ec3775b98 merged into trunk 53fd32618916 beside mesh-controller#346@c711e622 — pass: builds mesh-host → ace, g14, novox, shanks; no…
mesh/repo-check head 5a7ec3775b98 merged into trunk 53fd32618916 beside mesh-controller#346@c711e622 — pass: [release feat/57-config-and-provisioning, judg…
mesh/delivery stopped: its walk plan-1792114347281764422 was stopped by a person (mesh-controller): mesh-host#114 merged into the release branch feat/57-…
mesh/delivery-group group 57-config-and-provisioning stopped: a member was stopped
The node-engine renders the feature's templates, collections and moved providers' files, on the SDK the feature's modules build on (feature 57, config and provisioning).
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)
|
|
}
|
|
}
|