Node-engine: logins, signers, enrolment tokens and redeems, and the grants part's sealed user list come from the vault and node-enroller clients and the grants state package; no file imports go/delivery (ADR 0634 §1, §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"github.com/novox/mesh-host/internal/buckets/bucketstest"
|
||||
"github.com/novox/mesh-host/internal/compositions"
|
||||
"github.com/novox/mesh-host/internal/judge"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -20,7 +21,6 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
const node = "n1"
|
||||
@@ -1692,7 +1692,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
|
||||
t.Errorf("the composition of db waiting for a yes reads %+v (%v)", view, err)
|
||||
}
|
||||
if view, err := b.ag.Composition(context.Background(), "db", ddig, false); err != nil || view.Composition.Digest != ddig ||
|
||||
view.Judged.Judgement != string(delivery.JudgedUnchanged) || view.Judged.WaitsForYes || view.Diff != nil {
|
||||
view.Judged.Judgement != judge.JudgedUnchanged || view.Judged.WaitsForYes || view.Diff != nil {
|
||||
t.Errorf("db's running revision, asked by its digest, reads %+v (%v)", view, err)
|
||||
}
|
||||
if _, err := b.ag.Composition(context.Background(), "db", "sha256:"+strings.Repeat("0", 64), false); err == nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ package buckets
|
||||
|
||||
import (
|
||||
"errors"
|
||||
vault "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
||||
|
||||
assigner "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
@@ -57,3 +58,15 @@ func CandidateKey(group string) (string, error) {
|
||||
}
|
||||
return catalog.CandidatesByGroupKey(keys.EscapeName(group)), nil
|
||||
}
|
||||
|
||||
// The states of the vault seat's holder that the engine reads, named by the holder's module (ADR 0634 §1, finding F4).
|
||||
const (
|
||||
// Signers holds the vault's public signing halves; NodeLogins each node's logins, `<node>`.
|
||||
Signers = vaultHolder + "_" + vault.StateSigners
|
||||
NodeLogins = vaultHolder + "_" + vault.StateNodeLogins
|
||||
// SignersRead and NodeLoginsRead are the states as a manifest's reads names them.
|
||||
SignersRead = vaultHolder + "." + vault.StateSigners
|
||||
NodeLoginsRead = vaultHolder + "." + vault.StateNodeLogins
|
||||
|
||||
vaultHolder = "mesh-vault"
|
||||
)
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
@@ -51,7 +52,6 @@ import (
|
||||
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"github.com/novox/mesh-host/internal/compose"
|
||||
"github.com/novox/mesh-host/internal/compose/resolution"
|
||||
"github.com/novox/mesh-host/internal/takein"
|
||||
@@ -93,11 +93,11 @@ type Reader interface {
|
||||
Served(ctx context.Context, node, module string, keys []string) (map[string]any, error)
|
||||
}
|
||||
|
||||
// SignersReader is a Reader that also reads the vault's public authority from its state `signers` (delivery.SignersBucket,
|
||||
// key delivery.SignersAuthorityKey; ADR 0550 §2). A Reader that has it composes the mesh's authority as a bundle, the
|
||||
// SignersReader is a Reader that also reads the vault's public authority from its state `signers` (buckets.Signers,
|
||||
// key meshvault.SignersByAuthorityKey(); ADR 0550 §2). A Reader that has it composes the mesh's authority as a bundle, the
|
||||
// controller's and the vault's; one that has not, or that answers none, composes the controller's alone, as before.
|
||||
type SignersReader interface {
|
||||
// VaultAuthority is the vault's authority certificate as PEM, checked (delivery.DecodeSignerAuthority); empty when the
|
||||
// VaultAuthority is the vault's authority certificate as PEM, checked (meshvaultclient.DecodeSignerAuthority); empty when the
|
||||
// vault has published none. A read that fails is said by the reader and answered empty: a node that may not read the
|
||||
// state yet composes as before rather than not at all.
|
||||
VaultAuthority(ctx context.Context) string
|
||||
@@ -345,7 +345,7 @@ func readBase(ctx context.Context, r Reader, mesh Mesh, s Scope) (base, error) {
|
||||
// against the bundle. The controller's stays while a certificate it issued is in a node's record.
|
||||
if sr, ok := r.(SignersReader); ok {
|
||||
if vault := sr.VaultAuthority(ctx); vault != "" {
|
||||
in.Authority = delivery.AuthorityBundle(in.Authority, vault)
|
||||
in.Authority = meshvaultclient.AuthorityBundle(in.Authority, vault)
|
||||
}
|
||||
}
|
||||
named := func(field *map[string]string, module, why string) {
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
package identity
|
||||
|
||||
import "git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
import (
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
)
|
||||
|
||||
// Token is what a person carries to a machine that is joining (novox/hq ADR 0004): where the broker is, what certificate to
|
||||
// expect there, whose signature to believe afterwards, and a one-time right to join. Its shape and its reading are mesh-sdk's
|
||||
// (delivery.Token), which the vault that makes it, mesh-cli that opens it and this engine that parses it share (novox/hq ADR
|
||||
// (meshnodeenrollerclient.Token), which the vault that makes it, mesh-cli that opens it and this engine that parses it share (novox/hq ADR
|
||||
// 0587 §3, issue 774); these are the names this package has always had for them.
|
||||
type Token = delivery.Token
|
||||
type Token = meshnodeenrollerclient.Token
|
||||
|
||||
// TokenTunnel is the joining machine's side of its first tunnel (delivery.TokenTunnel).
|
||||
type TokenTunnel = delivery.TokenTunnel
|
||||
// TokenTunnel is the joining machine's side of its first tunnel (meshnodeenrollerclient.TokenTunnel).
|
||||
type TokenTunnel = meshnodeenrollerclient.TokenTunnel
|
||||
|
||||
// ParseToken reads a token a person pasted (delivery.ParseToken).
|
||||
func ParseToken(encoded string) (Token, error) { return delivery.ParseToken(encoded) }
|
||||
// ParseToken reads a token a person pasted (meshnodeenrollerclient.ParseToken).
|
||||
func ParseToken(encoded string) (Token, error) { return meshnodeenrollerclient.ParseToken(encoded) }
|
||||
|
||||
+35
-33
@@ -7,12 +7,14 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
||||
"github.com/novox/mesh-host/internal/buckets"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// Joining a mesh through the seat mesh-enrolment (novox/hq ADR 0587 §6, issue 774): the node-engine dials the bus as enrol.<node>
|
||||
@@ -40,7 +42,7 @@ var ErrNoSeat = errors.New("the seat mesh-enrolment is not served to this machin
|
||||
// VerifyRedeemAnswer holds an answer to what it must be before anything is kept (ADR 0587 §6): it names the node asked for,
|
||||
// carries the signing key the token carries, and is signed by that key under the context of an enrolment's answer. An answer
|
||||
// signed by another key is refused, and so is one that names another signer.
|
||||
func VerifyRedeemAnswer(a delivery.RedeemAnswer, node string, signer ed25519.PublicKey) error {
|
||||
func VerifyRedeemAnswer(a meshnodeenrollerclient.RedeemAnswer, node string, signer ed25519.PublicKey) error {
|
||||
if a.Node != node {
|
||||
return fmt.Errorf("the answer is for %q, and this machine asked as %q", a.Node, node)
|
||||
}
|
||||
@@ -51,11 +53,11 @@ func VerifyRedeemAnswer(a delivery.RedeemAnswer, node string, signer ed25519.Pub
|
||||
if err != nil || len(sig) == 0 {
|
||||
return errors.New("the answer is not signed")
|
||||
}
|
||||
kindContext, err := delivery.SignContext(delivery.KindEnrolment)
|
||||
kindContext, err := meshvaultclient.SignContext(meshvaultclient.KindEnrolment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := delivery.VerifyBy([]delivery.Signer{{Name: "the token's", Key: signer}}, kindContext, a.SignedBytes(), sig); err != nil {
|
||||
if _, err := meshvaultclient.VerifyBy([]meshvaultclient.Signer{{Name: "the token's", Key: signer}}, kindContext, a.SignedBytes(), sig); err != nil {
|
||||
return fmt.Errorf("the answer is not signed by the key the token carries: %w", err)
|
||||
}
|
||||
if a.Login == "" || a.Account == "" {
|
||||
@@ -83,52 +85,52 @@ func seatAnswer(reply []byte) (json.RawMessage, error) {
|
||||
// asks again with the same keys while the holder says to (a stopped redeem finishes; the bus node loads the account), , for
|
||||
// EnrolPatience. An answer is returned as the holder gave it: the caller
|
||||
// verifies it (VerifyRedeemAnswer) before it keeps anything.
|
||||
func Redeem(ctx context.Context, to Approach, node, secret string, req delivery.Redeem, timeout time.Duration) (delivery.RedeemAnswer, error) {
|
||||
func Redeem(ctx context.Context, to Approach, node, secret string, req meshnodeenrollerclient.Redeem, timeout time.Duration) (meshnodeenrollerclient.RedeemAnswer, error) {
|
||||
if to.Transport != OnNATS {
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("this token is for the %q bus, and the mesh's bus is %s", to.Transport, OnNATS)
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("this token is for the %q bus, and the mesh's bus is %s", to.Transport, OnNATS)
|
||||
}
|
||||
config, err := PinnedConfig(to.Fingerprint)
|
||||
if err != nil {
|
||||
return delivery.RedeemAnswer{}, err
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, err
|
||||
}
|
||||
user := delivery.EnrolmentUser(node)
|
||||
user := meshnodeenrollerclient.User(node)
|
||||
conn, err := nats.Connect(natsURL(to.Address), nats.Secure(config), nats.CustomInboxPrefix("_INBOX."+user),
|
||||
nats.UserInfo(user, secret), nats.Name("mesh-host/enrol/"+node), nats.Timeout(timeout), nats.NoReconnect())
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrWrongCertificate) {
|
||||
return delivery.RedeemAnswer{}, err
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, err
|
||||
}
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("cannot reach the bus at %s as %s: %w", to.Address, user, err)
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("cannot reach the bus at %s as %s: %w", to.Address, user, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return delivery.RedeemAnswer{}, err
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, err
|
||||
}
|
||||
began := time.Now()
|
||||
for {
|
||||
msg, err := conn.RequestWithContext(ctx, CalledSeatSubject(EnrolmentSeat, EnrolmentRedeem, user), body)
|
||||
switch {
|
||||
case errors.Is(err, nats.ErrNoResponders) || (err != nil && strings.Contains(err.Error(), "permissions violation")):
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("%w: %v", ErrNoSeat, err)
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("%w: %v", ErrNoSeat, err)
|
||||
case err != nil:
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("the seat did not answer redeem: %w", err)
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("the seat did not answer redeem: %w", err)
|
||||
}
|
||||
result, aerr := seatAnswer(msg.Data)
|
||||
if aerr != nil {
|
||||
if strings.Contains(aerr.Error(), "call redeem again with the same keys") && time.Since(began) < EnrolPatience {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return delivery.RedeemAnswer{}, ctx.Err()
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, ctx.Err()
|
||||
case <-time.After(AskAgainAfter):
|
||||
continue
|
||||
}
|
||||
}
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("%w: %v", ErrRefused, aerr)
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("%w: %v", ErrRefused, aerr)
|
||||
}
|
||||
var answer delivery.RedeemAnswer
|
||||
var answer meshnodeenrollerclient.RedeemAnswer
|
||||
if err := json.Unmarshal(result, &answer); err != nil {
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("the seat's answer to redeem is not one: %w", err)
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("the seat's answer to redeem is not one: %w", err)
|
||||
}
|
||||
return answer, nil
|
||||
}
|
||||
@@ -137,7 +139,7 @@ func Redeem(ctx context.Context, to Approach, node, secret string, req delivery.
|
||||
// Confirming is this machine dialled as its own login after it redeemed: it reads its key of node-logins and calls confirm.
|
||||
type Confirming interface {
|
||||
// Login is this node's key of the vault's node-logins.
|
||||
Login(ctx context.Context) (delivery.NodeLogin, error)
|
||||
Login(ctx context.Context) (meshvault.NodeLoginsByNodeValue, error)
|
||||
// Confirm calls the seat's confirm; the runtime's proof of the caller as node.<node> is the confirmation (ADR 0583 §3).
|
||||
Confirm(ctx context.Context) error
|
||||
Close()
|
||||
@@ -149,29 +151,29 @@ type Joining struct {
|
||||
Signer ed25519.PublicKey
|
||||
// Redeem calls the seat; Open opens the account sealed to this node's sealing key; Keep writes the membership, the login
|
||||
// and the keys down; Dial dials the bus as login and answers it ready to read node-logins and confirm.
|
||||
Redeem func(ctx context.Context) (delivery.RedeemAnswer, error)
|
||||
Open func(a delivery.RedeemAnswer) (string, error)
|
||||
Keep func(a delivery.RedeemAnswer, password string) error
|
||||
Redeem func(ctx context.Context) (meshnodeenrollerclient.RedeemAnswer, error)
|
||||
Open func(a meshnodeenrollerclient.RedeemAnswer) (string, error)
|
||||
Keep func(a meshnodeenrollerclient.RedeemAnswer, password string) error
|
||||
Dial func(ctx context.Context, login, password string) (Confirming, error)
|
||||
}
|
||||
|
||||
// Run joins: the answer is verified before anything is kept (an answer not signed by the key the token carries keeps nothing),
|
||||
// kept, and then the machine dials as the login its key of node-logins names and confirms. Nothing is applied here: the engine
|
||||
// applies once its placement names a module.
|
||||
func (j Joining) Run(ctx context.Context) (delivery.RedeemAnswer, error) {
|
||||
func (j Joining) Run(ctx context.Context) (meshnodeenrollerclient.RedeemAnswer, error) {
|
||||
answer, err := j.Redeem(ctx)
|
||||
if err != nil {
|
||||
return delivery.RedeemAnswer{}, err
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, err
|
||||
}
|
||||
if err := VerifyRedeemAnswer(answer, j.Node, j.Signer); err != nil {
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("%w: %v; nothing was kept", ErrRefused, err)
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("%w: %v; nothing was kept", ErrRefused, err)
|
||||
}
|
||||
password, err := j.Open(answer)
|
||||
if err != nil {
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("the account in the answer cannot be opened: %w; nothing was kept", err)
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("the account in the answer cannot be opened: %w; nothing was kept", err)
|
||||
}
|
||||
if err := j.Keep(answer, password); err != nil {
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("the mesh accepted this node as %q and what it was given could not be kept: %w\n"+
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("the mesh accepted this node as %q and what it was given could not be kept: %w\n"+
|
||||
"That token is spent, so getting back needs a new one", answer.Node, err)
|
||||
}
|
||||
c, err := j.Dial(ctx, answer.Login, password)
|
||||
@@ -213,20 +215,20 @@ func DialConfirming(ctx context.Context, m Membership, login, password string, t
|
||||
return &confirmingNats{conn: conn, node: m.Node, user: login}, nil
|
||||
}
|
||||
|
||||
func (c *confirmingNats) Login(ctx context.Context) (delivery.NodeLogin, error) {
|
||||
func (c *confirmingNats) Login(ctx context.Context) (meshvault.NodeLoginsByNodeValue, error) {
|
||||
js, err := c.conn.JetStream()
|
||||
if err != nil {
|
||||
return delivery.NodeLogin{}, err
|
||||
return meshvault.NodeLoginsByNodeValue{}, err
|
||||
}
|
||||
kv, err := js.KeyValue(delivery.NodeLoginsBucket)
|
||||
kv, err := js.KeyValue(buckets.NodeLogins)
|
||||
if err != nil {
|
||||
return delivery.NodeLogin{}, err
|
||||
return meshvault.NodeLoginsByNodeValue{}, err
|
||||
}
|
||||
e, err := kv.Get(c.node)
|
||||
if err != nil {
|
||||
return delivery.NodeLogin{}, err
|
||||
return meshvault.NodeLoginsByNodeValue{}, err
|
||||
}
|
||||
return delivery.DecodeNodeLogin(e.Value())
|
||||
return meshvaultclient.DecodeNodeLogin(e.Value())
|
||||
}
|
||||
|
||||
func (c *confirmingNats) Confirm(ctx context.Context) error {
|
||||
|
||||
@@ -6,33 +6,34 @@ import (
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// signedBy is an answer for ace, signed under the enrolment context by the key seeded, naming that key as its signer.
|
||||
func signedBy(t *testing.T, seed byte) (delivery.RedeemAnswer, ed25519.PublicKey) {
|
||||
func signedBy(t *testing.T, seed byte) (meshnodeenrollerclient.RedeemAnswer, ed25519.PublicKey) {
|
||||
t.Helper()
|
||||
priv := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{seed}, ed25519.SeedSize))
|
||||
pub := priv.Public().(ed25519.PublicKey)
|
||||
a := delivery.RedeemAnswer{Node: "ace", Login: "node.ace", Account: `{"sealed":"box"}`, Certificate: "PEM", Signer: base64.StdEncoding.EncodeToString(pub)}
|
||||
ctx, err := delivery.SignContext(delivery.KindEnrolment)
|
||||
a := meshnodeenrollerclient.RedeemAnswer{Node: "ace", Login: "node.ace", Account: `{"sealed":"box"}`, Certificate: "PEM", Signer: base64.StdEncoding.EncodeToString(pub)}
|
||||
ctx, err := meshvaultclient.SignContext(meshvaultclient.KindEnrolment)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a.Signature = base64.StdEncoding.EncodeToString(delivery.SignMessage(priv, ctx, a.SignedBytes()))
|
||||
a.Signature = base64.StdEncoding.EncodeToString(meshvaultclient.SignMessage(priv, ctx, a.SignedBytes()))
|
||||
return a, pub
|
||||
}
|
||||
|
||||
type standInConfirming struct {
|
||||
steps *[]string
|
||||
nl delivery.NodeLogin
|
||||
nl meshvault.NodeLoginsByNodeValue
|
||||
}
|
||||
|
||||
func (c standInConfirming) Login(context.Context) (delivery.NodeLogin, error) {
|
||||
func (c standInConfirming) Login(context.Context) (meshvault.NodeLoginsByNodeValue, error) {
|
||||
*c.steps = append(*c.steps, "read node-logins")
|
||||
return c.nl, nil
|
||||
}
|
||||
@@ -52,18 +53,21 @@ func TestTheEngineKeepsOnlyAnAnswerSignedByTheKeyItsTokenCarries(t *testing.T) {
|
||||
badNode := good
|
||||
badNode.Node = "g14"
|
||||
for name, c := range map[string]struct {
|
||||
answer delivery.RedeemAnswer
|
||||
answer meshnodeenrollerclient.RedeemAnswer
|
||||
want string
|
||||
}{
|
||||
"signed by another key": {forged, "signing key other than the one the token carries"},
|
||||
"another node": {badNode, `is for "g14"`},
|
||||
"altered after signing": {func() delivery.RedeemAnswer { a := good; a.Account = "other"; return a }(), "not signed by the key the token carries"},
|
||||
"altered after signing": {func() meshnodeenrollerclient.RedeemAnswer { a := good; a.Account = "other"; return a }(), "not signed by the key the token carries"},
|
||||
} {
|
||||
var steps []string
|
||||
j := Joining{Node: "ace", Signer: pub,
|
||||
Redeem: func(context.Context) (delivery.RedeemAnswer, error) { return c.answer, nil },
|
||||
Open: func(delivery.RedeemAnswer) (string, error) { steps = append(steps, "open"); return "pw", nil },
|
||||
Keep: func(delivery.RedeemAnswer, string) error { steps = append(steps, "keep"); return nil },
|
||||
Redeem: func(context.Context) (meshnodeenrollerclient.RedeemAnswer, error) { return c.answer, nil },
|
||||
Open: func(meshnodeenrollerclient.RedeemAnswer) (string, error) {
|
||||
steps = append(steps, "open")
|
||||
return "pw", nil
|
||||
},
|
||||
Keep: func(meshnodeenrollerclient.RedeemAnswer, string) error { steps = append(steps, "keep"); return nil },
|
||||
Dial: func(context.Context, string, string) (Confirming, error) {
|
||||
steps = append(steps, "dial")
|
||||
return nil, errors.New("must not dial")
|
||||
@@ -75,18 +79,21 @@ func TestTheEngineKeepsOnlyAnAnswerSignedByTheKeyItsTokenCarries(t *testing.T) {
|
||||
var steps []string
|
||||
login := "node.ace"
|
||||
j := Joining{Node: "ace", Signer: pub,
|
||||
Redeem: func(context.Context) (delivery.RedeemAnswer, error) {
|
||||
Redeem: func(context.Context) (meshnodeenrollerclient.RedeemAnswer, error) {
|
||||
steps = append(steps, "redeem")
|
||||
return good, nil
|
||||
},
|
||||
Open: func(delivery.RedeemAnswer) (string, error) { steps = append(steps, "open"); return "pw", nil },
|
||||
Keep: func(a delivery.RedeemAnswer, pw string) error {
|
||||
Open: func(meshnodeenrollerclient.RedeemAnswer) (string, error) {
|
||||
steps = append(steps, "open")
|
||||
return "pw", nil
|
||||
},
|
||||
Keep: func(a meshnodeenrollerclient.RedeemAnswer, pw string) error {
|
||||
steps = append(steps, "keep "+a.Login+" "+pw)
|
||||
return nil
|
||||
},
|
||||
Dial: func(_ context.Context, l, pw string) (Confirming, error) {
|
||||
steps = append(steps, "dial "+l+" "+pw)
|
||||
return standInConfirming{steps: &steps, nl: delivery.NodeLogin{Node: "ace", Login: login}}, nil
|
||||
return standInConfirming{steps: &steps, nl: meshvault.NodeLoginsByNodeValue{Node: "ace", Login: login}}, nil
|
||||
}}
|
||||
if _, err := j.Run(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
@@ -114,7 +114,7 @@ func (b OverNATS) GrantsObject(ctx context.Context, name string, limit int64) ([
|
||||
}
|
||||
js = bounded
|
||||
}
|
||||
return objectByName(ctx, js, delivery.GrantsUserListObjects, delivery.GrantsUserListObjectStream, name, limit)
|
||||
return objectByName(ctx, js, grantsstate.GrantsUserListObjects, grantsstate.GrantsUserListObjectStream, name, limit)
|
||||
}
|
||||
|
||||
// objectByName is one object of bucket (its stream stream) by its name, no larger than limit, verified against its digest.
|
||||
|
||||
@@ -2,12 +2,12 @@ package link
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// Logins is which of its two logins this node-engine dials the bus as, and the password of each it holds (novox/hq ADR 0583 §1,
|
||||
@@ -42,12 +42,12 @@ func NewLogins(node, first string, saved LoginsState, save func(LoginsState) err
|
||||
l.values[login] = password
|
||||
}
|
||||
if first != "" {
|
||||
if _, ok := l.values[delivery.FirstLogin(node)]; !ok {
|
||||
l.values[delivery.FirstLogin(node)] = first
|
||||
if _, ok := l.values[meshvaultclient.FirstLogin(node)]; !ok {
|
||||
l.values[meshvaultclient.FirstLogin(node)] = first
|
||||
}
|
||||
}
|
||||
if l.current == "" {
|
||||
l.current = delivery.FirstLogin(node)
|
||||
l.current = meshvaultclient.FirstLogin(node)
|
||||
}
|
||||
return l
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func (l *Logins) Value(login string) (string, bool) {
|
||||
func (l *Logins) Hold(login, password string) error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if n, ok := delivery.LoginNode(login); !ok || n != l.node {
|
||||
if n, ok := meshvaultclient.LoginNode(login); !ok || n != l.node {
|
||||
return &loginError{login: login, node: l.node}
|
||||
}
|
||||
l.values[login] = password
|
||||
@@ -164,7 +164,7 @@ func (m Membership) User() string {
|
||||
// Heard takes this node's key of the vault's node-logins as read (a key read, or the empty value of a key gone): the login
|
||||
// in use becomes the one it names when its password is held. A key naming a login whose password is not held changes
|
||||
// nothing and says so; no key leaves the login as it is.
|
||||
func (l *Logins) Heard(nl delivery.NodeLogin) (said string) {
|
||||
func (l *Logins) Heard(nl meshvault.NodeLoginsByNodeValue) (said string) {
|
||||
if nl.Node != l.node || nl.Login == "" {
|
||||
return ""
|
||||
}
|
||||
@@ -182,7 +182,7 @@ func (l *Logins) Heard(nl delivery.NodeLogin) (said string) {
|
||||
}
|
||||
|
||||
// rotatingToUs says the key's rotating login is the one in use already: the engine switched ahead of the vault's own state.
|
||||
func rotatingToUs(nl delivery.NodeLogin, current string) bool {
|
||||
func rotatingToUs(nl meshvault.NodeLoginsByNodeValue, current string) bool {
|
||||
return nl.Rotating != "" && nl.Rotating == current
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package link
|
||||
|
||||
import (
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// 774/S61/AC1 and AC2 (ADR 0583 §1): with no node-logins key, or one that names the first login, the engine dials as node.<n>
|
||||
@@ -20,10 +19,10 @@ func TestTheLoginDialledIsTheOneNodeLoginsNames(t *testing.T) {
|
||||
if u, p := (Membership{Node: "ace", Password: "plain"}).Credential(); u != "node.ace" || p != "plain" {
|
||||
t.Fatalf("no logins at all: %s %s", u, p)
|
||||
}
|
||||
if said := l.Heard(delivery.NodeLogin{Node: "ace", Login: "node.ace"}); said != "" || m.User() != "node.ace" {
|
||||
if said := l.Heard(meshvault.NodeLoginsByNodeValue{Node: "ace", Login: "node.ace"}); said != "" || m.User() != "node.ace" {
|
||||
t.Errorf("the first login named: %q, %s", said, m.User())
|
||||
}
|
||||
if said := l.Heard(delivery.NodeLogin{Node: "ace", Login: "node.ace--b"}); !strings.Contains(said, "cannot dial as it") || m.User() != "node.ace" {
|
||||
if said := l.Heard(meshvault.NodeLoginsByNodeValue{Node: "ace", Login: "node.ace--b"}); !strings.Contains(said, "cannot dial as it") || m.User() != "node.ace" {
|
||||
t.Errorf("a login held no value for: %q, %s", said, m.User())
|
||||
}
|
||||
if err := l.Hold("node.ace--b", "second-secret"); err != nil {
|
||||
@@ -32,7 +31,7 @@ func TestTheLoginDialledIsTheOneNodeLoginsNames(t *testing.T) {
|
||||
if err := l.Hold("node.g14", "x"); err == nil {
|
||||
t.Error("another node's login was kept")
|
||||
}
|
||||
if said := l.Heard(delivery.NodeLogin{Node: "ace", Login: "node.ace--b"}); !strings.Contains(said, "dialled as it") {
|
||||
if said := l.Heard(meshvault.NodeLoginsByNodeValue{Node: "ace", Login: "node.ace--b"}); !strings.Contains(said, "dialled as it") {
|
||||
t.Errorf("said %q", said)
|
||||
}
|
||||
if u, p := m.Credential(); u != "node.ace--b" || p != "second-secret" || saved.Current != "node.ace--b" {
|
||||
|
||||
+12
-7
@@ -4,10 +4,11 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// Rotator is the engine's side of a node's account rotation (novox/hq ADR 0583 §3-§6, issue 774). The vault begins a rotation
|
||||
@@ -49,7 +50,7 @@ func (r *Rotator) say(f string, a ...any) {
|
||||
}
|
||||
|
||||
// Heard takes this node's key of node-logins as read.
|
||||
func (r *Rotator) Heard(ctx context.Context, nl delivery.NodeLogin) {
|
||||
func (r *Rotator) Heard(ctx context.Context, nl meshvault.NodeLoginsByNodeValue) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.fallen != nil && nl.Since.String() != r.fellBackSince {
|
||||
@@ -61,7 +62,7 @@ func (r *Rotator) Heard(ctx context.Context, nl delivery.NodeLogin) {
|
||||
// Done (the switch happened and the old login is retired), or no rotation at all: the other login's value goes (§4),
|
||||
// only now that the vault says so.
|
||||
r.old, r.failures = "", 0
|
||||
if other := delivery.OtherLogin(nl.Node, cur); other != "" {
|
||||
if other := meshvaultclient.OtherLogin(nl.Node, cur); other != "" {
|
||||
if _, held := r.Logins.Value(other); held {
|
||||
if err := r.Logins.Drop(other); err != nil {
|
||||
r.say("the old login %s's value is not dropped: %v", other, err)
|
||||
@@ -86,18 +87,18 @@ func (r *Rotator) Heard(ctx context.Context, nl delivery.NodeLogin) {
|
||||
if err := r.Logins.Drop(cur); err != nil {
|
||||
r.say("the abandoned login %s's value is not dropped: %v", cur, err)
|
||||
}
|
||||
case nl.Login == cur && nl.Stage != delivery.RotationBegun:
|
||||
case nl.Login == cur && nl.Stage != meshvaultclient.RotationBegun:
|
||||
if r.fellBackSince != "" && r.fellBackSince == nl.Since.String() {
|
||||
return // fallen back for this rotation: not tried again until the vault begins another
|
||||
}
|
||||
r.switchTo(ctx, nl)
|
||||
case nl.Rotating == cur && nl.Stage != delivery.RotationConfirmed:
|
||||
case nl.Rotating == cur && nl.Stage != meshvaultclient.RotationConfirmed:
|
||||
r.confirm(ctx, nl.Rotating)
|
||||
}
|
||||
}
|
||||
|
||||
// switchTo fetches the new value, test-dials it and, only if it authenticates, switches and confirms (§3).
|
||||
func (r *Rotator) switchTo(ctx context.Context, nl delivery.NodeLogin) {
|
||||
func (r *Rotator) switchTo(ctx context.Context, nl meshvault.NodeLoginsByNodeValue) {
|
||||
next := nl.Rotating
|
||||
password, held := r.Logins.Value(next)
|
||||
if !held {
|
||||
@@ -158,7 +159,7 @@ func (r *Rotator) AuthFailed(login string) {
|
||||
return
|
||||
}
|
||||
text := fmt.Sprintf("%s failed to authenticate %d times before the vault said rotation-done: this node dials as %s again", login, r.failures, r.old)
|
||||
r.say("%s: %s", delivery.NodeAccountFallbackFinding, text)
|
||||
r.say("%s: %s", NodeAccountFallbackFinding, text)
|
||||
now := time.Now
|
||||
if r.Now != nil {
|
||||
now = r.Now
|
||||
@@ -201,3 +202,7 @@ func (r *Rotator) Watch() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NodeAccountFallbackFinding is what the node-engine says when it fell back to its old login after repeated
|
||||
// authentication failures with the new one, before the rotation is done; the watchman raises it (ADR 0550).
|
||||
const NodeAccountFallbackFinding = "node-account-fallback"
|
||||
|
||||
@@ -5,14 +5,14 @@ package link
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
natsserver "github.com/nats-io/nats-server/v2/server"
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// 774/S65/AC1 (ADR 0583 §2-§6): the engine's rotation against a real bus whose list of users a stand-in vault reloads step by
|
||||
@@ -114,19 +114,19 @@ func TestARotationAgainstARealBusWhoseListIsReloadedStepByStep(t *testing.T) {
|
||||
}
|
||||
ctx := context.Background()
|
||||
since := time.Date(2026, 11, 2, 12, 0, 0, 0, time.UTC)
|
||||
key := func(login, rotating, stage string) delivery.NodeLogin {
|
||||
return delivery.NodeLogin{Node: "ace", Login: login, Rotating: rotating, Stage: stage, Since: since}
|
||||
key := func(login, rotating, stage string) meshvault.NodeLoginsByNodeValue {
|
||||
return meshvault.NodeLoginsByNodeValue{Node: "ace", Login: login, Rotating: rotating, Stage: stage, Since: since}
|
||||
}
|
||||
|
||||
// A whole rotation: begun, the list holds both logins, ready, the switch and the confirmation, the old login retired, done.
|
||||
srv, reload := start(t)
|
||||
r, l, confirmed := build(t, srv)
|
||||
oneAuthenticates(t, srv, "before the rotation")
|
||||
r.Heard(ctx, key(first, second, delivery.RotationBegun))
|
||||
r.Heard(ctx, key(first, second, meshvaultclient.RotationBegun))
|
||||
oneAuthenticates(t, srv, "begun")
|
||||
reload(first, second)
|
||||
oneAuthenticates(t, srv, "the list holds both")
|
||||
r.Heard(ctx, key(first, second, delivery.RotationReady))
|
||||
r.Heard(ctx, key(first, second, meshvaultclient.RotationReady))
|
||||
if cur, _ := l.Current(); cur != second {
|
||||
t.Fatalf("the engine did not switch once the list held both logins: %s", cur)
|
||||
}
|
||||
@@ -149,7 +149,7 @@ func TestARotationAgainstARealBusWhoseListIsReloadedStepByStep(t *testing.T) {
|
||||
// With the list never loaded the engine never switches: the test-dial is refused.
|
||||
srv, _ = start(t)
|
||||
r, l, _ = build(t, srv)
|
||||
r.Heard(ctx, key(first, second, delivery.RotationReady))
|
||||
r.Heard(ctx, key(first, second, meshvaultclient.RotationReady))
|
||||
if cur, _ := l.Current(); cur != first || !authenticates(srv, first) {
|
||||
t.Errorf("the list was never loaded and the engine is on %s", cur)
|
||||
}
|
||||
@@ -158,7 +158,7 @@ func TestARotationAgainstARealBusWhoseListIsReloadedStepByStep(t *testing.T) {
|
||||
srv, reload = start(t)
|
||||
r, l, _ = build(t, srv)
|
||||
reload(first, second)
|
||||
r.Heard(ctx, key(first, second, delivery.RotationReady))
|
||||
r.Heard(ctx, key(first, second, meshvaultclient.RotationReady))
|
||||
reload(first)
|
||||
r.Heard(ctx, key(first, "", ""))
|
||||
if cur, _ := l.Current(); cur != first || !authenticates(srv, first) {
|
||||
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
"errors"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// rotRig is a Rotator over stand-ins: what the vault hands, what a test-dial answers, and what it was asked, in order.
|
||||
@@ -53,8 +53,8 @@ func newRotRig() *rotRig {
|
||||
return g
|
||||
}
|
||||
|
||||
func key(login, rotating, stage string) delivery.NodeLogin {
|
||||
return delivery.NodeLogin{Node: "ace", Login: login, Rotating: rotating, Stage: stage, Since: time.Date(2026, 11, 2, 12, 0, 0, 0, time.UTC)}
|
||||
func key(login, rotating, stage string) meshvault.NodeLoginsByNodeValue {
|
||||
return meshvault.NodeLoginsByNodeValue{Node: "ace", Login: login, Rotating: rotating, Stage: stage, Since: time.Date(2026, 11, 2, 12, 0, 0, 0, time.UTC)}
|
||||
}
|
||||
|
||||
// 774/S62/AC1 and AC2 (ADR 0583 §3): at ready the engine fetches the new value and test-dials it; one that does not
|
||||
@@ -63,18 +63,18 @@ func key(login, rotating, stage string) delivery.NodeLogin {
|
||||
func TestTheEngineSwitchesOnlyAfterATestDialAndConfirmsAsTheNewLogin(t *testing.T) {
|
||||
g := newRotRig()
|
||||
ctx := context.Background()
|
||||
g.r.Heard(ctx, key("node.ace", "node.ace--b", delivery.RotationBegun))
|
||||
g.r.Heard(ctx, key("node.ace", "node.ace--b", meshvaultclient.RotationBegun))
|
||||
if len(g.steps) != 0 {
|
||||
t.Fatalf("before ready: %v", g.steps)
|
||||
}
|
||||
g.dialErr = errors.New("authorization violation")
|
||||
g.r.Heard(ctx, key("node.ace", "node.ace--b", delivery.RotationReady))
|
||||
g.r.Heard(ctx, key("node.ace", "node.ace--b", meshvaultclient.RotationReady))
|
||||
if cur, _ := g.logins.Current(); cur != "node.ace" || !strings.Contains(strings.Join(g.said, "|"), "stays on node.ace") ||
|
||||
slices.Contains(g.steps, "confirm as node.ace--b") {
|
||||
t.Fatalf("a test-dial that failed: %s, said %v, steps %v", cur, g.said, g.steps)
|
||||
}
|
||||
g.dialErr = nil
|
||||
g.r.Heard(ctx, key("node.ace", "node.ace--b", delivery.RotationReady))
|
||||
g.r.Heard(ctx, key("node.ace", "node.ace--b", meshvaultclient.RotationReady))
|
||||
cur, pw := g.logins.Current()
|
||||
if cur != "node.ace--b" || pw != "secret-of-node.ace--b" {
|
||||
t.Fatalf("after a good test-dial: %s %s", cur, pw)
|
||||
@@ -99,9 +99,9 @@ func TestTheOldValueGoesAtRotationDoneAbandonSwitchesBackAndFailuresFallBack(t *
|
||||
ctx := context.Background()
|
||||
for _, stage := range []string{"before confirming", "after confirming"} {
|
||||
g := newRotRig()
|
||||
g.r.Heard(ctx, key("node.ace", "node.ace--b", delivery.RotationReady))
|
||||
g.r.Heard(ctx, key("node.ace", "node.ace--b", meshvaultclient.RotationReady))
|
||||
if stage == "after confirming" {
|
||||
g.r.Heard(ctx, key("node.ace", "node.ace--b", delivery.RotationConfirmed))
|
||||
g.r.Heard(ctx, key("node.ace", "node.ace--b", meshvaultclient.RotationConfirmed))
|
||||
}
|
||||
if _, held := g.logins.Value("node.ace"); !held {
|
||||
t.Fatalf("%s: the old value was dropped before rotation-done", stage)
|
||||
@@ -121,7 +121,7 @@ func TestTheOldValueGoesAtRotationDoneAbandonSwitchesBackAndFailuresFallBack(t *
|
||||
}
|
||||
}
|
||||
g := newRotRig()
|
||||
g.r.Heard(ctx, key("node.ace", "node.ace--b", delivery.RotationReady))
|
||||
g.r.Heard(ctx, key("node.ace", "node.ace--b", meshvaultclient.RotationReady))
|
||||
g.r.Heard(ctx, key("node.ace--b", "", "")) // rotation-done: the vault retired the old login
|
||||
if _, held := g.logins.Value("node.ace"); held {
|
||||
t.Error("the old value is kept after rotation-done")
|
||||
@@ -131,7 +131,7 @@ func TestTheOldValueGoesAtRotationDoneAbandonSwitchesBackAndFailuresFallBack(t *
|
||||
}
|
||||
// Fallback: two failures with the new value (the bound) before rotation-done.
|
||||
f := newRotRig()
|
||||
f.r.Heard(ctx, key("node.ace", "node.ace--b", delivery.RotationReady))
|
||||
f.r.Heard(ctx, key("node.ace", "node.ace--b", meshvaultclient.RotationReady))
|
||||
f.logins.dialed("node.ace--b", errors.New("nats: authorization violation"))
|
||||
if cur, _ := f.logins.Current(); cur != "node.ace--b" {
|
||||
t.Fatalf("fell back after one failure: %s", cur)
|
||||
@@ -146,7 +146,7 @@ func TestTheOldValueGoesAtRotationDoneAbandonSwitchesBackAndFailuresFallBack(t *
|
||||
t.Errorf("the fall back is no health report's: %v", err)
|
||||
}
|
||||
steps := len(f.steps)
|
||||
f.r.Heard(ctx, key("node.ace", "node.ace--b", delivery.RotationReady))
|
||||
f.r.Heard(ctx, key("node.ace", "node.ace--b", meshvaultclient.RotationReady))
|
||||
if len(f.steps) != steps {
|
||||
t.Errorf("the rotation fallen back from was tried again: %v", f.steps[steps:])
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
|
||||
@@ -41,7 +42,6 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"github.com/novox/mesh-host/internal/compose"
|
||||
"github.com/novox/mesh-host/internal/compose/frombus"
|
||||
"github.com/novox/mesh-host/internal/compose/resolution"
|
||||
@@ -1360,7 +1360,7 @@ func (c *Composer) certifyIfWanted(ctx context.Context, r reader, p meshmoduleas
|
||||
return
|
||||
}
|
||||
trusted := []string{rec.Authority, r.VaultAuthority(ctx)}
|
||||
if rec.Certificate != "" && delivery.CheckNodeCertificate(rec.Certificate, trusted, overlay.InternalName(c.env.Node),
|
||||
if rec.Certificate != "" && meshvaultclient.CheckNodeCertificate(rec.Certificate, trusted, overlay.InternalName(c.env.Node),
|
||||
ed25519.PublicKey(key), time.Now()) == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -32,16 +32,15 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
"github.com/novox/mesh-host/internal/agent"
|
||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
"github.com/novox/mesh-host/internal/agent"
|
||||
)
|
||||
|
||||
// ResourceID is the id the composer gives the bus's user list (mesh-sdk catalogue.BusUsersID); in a composition it may
|
||||
@@ -66,7 +65,7 @@ type Env struct {
|
||||
Write func(ctx context.Context, module string, resource json.RawMessage, body []byte) (digest string, changed bool, err error)
|
||||
// Report says what was written, in the module's have.
|
||||
Report func(ctx context.Context, module string, w Written)
|
||||
// Grants watches the grants part's state (its key delivery.GrantsUserListKey); nil writes nothing.
|
||||
// Grants watches the grants part's state (its key grantsstate.GrantsUserListKey); nil writes nothing.
|
||||
Grants agent.Watcher
|
||||
// Fetch reads an object of the grants part's bucket by its name; Open opens a box sealed to this node's key; Key is
|
||||
// this node's sealing key's public half (SPKI PEM), said in the have so the grants part seals to it ("" unknown).
|
||||
@@ -103,7 +102,7 @@ type Follower struct {
|
||||
mod string
|
||||
said map[string]bool
|
||||
// grants is the grants part's key as last heard (nil when none).
|
||||
grants *delivery.GrantsUserList
|
||||
grants *grantsstate.GrantsUserList
|
||||
grantsRev uint64
|
||||
// unwatched is why the grants part's key is not watched now; empty while it is, or where it need not be.
|
||||
unwatched string
|
||||
@@ -124,7 +123,7 @@ func New(env Env) *Follower {
|
||||
said: map[string]bool{}}
|
||||
if env.Grants != nil && env.Kept != nil {
|
||||
if raw, rev := env.Kept(); raw != nil {
|
||||
if g, err := delivery.DecodeGrantsUserList(raw); err == nil {
|
||||
if g, err := grantsstate.DecodeGrantsUserList(raw); err == nil {
|
||||
f.grants, f.grantsRev = &g, rev
|
||||
}
|
||||
}
|
||||
@@ -180,7 +179,7 @@ func (f *Follower) Run(ctx context.Context) error {
|
||||
return
|
||||
}
|
||||
opened = true
|
||||
ch, err := f.env.Grants.Watch(ctx, delivery.GrantsUserListKey)
|
||||
ch, err := f.env.Grants.Watch(ctx, grantsstate.GrantsUserListKey)
|
||||
if err != nil {
|
||||
failed(err.Error())
|
||||
return
|
||||
@@ -268,7 +267,7 @@ func (f *Follower) takeGrants(u agent.Update) {
|
||||
f.sayOnceLocked("the grants part's user-list key is deleted, so the list loaded is kept")
|
||||
return
|
||||
}
|
||||
g, err := delivery.DecodeGrantsUserList(u.Value)
|
||||
g, err := grantsstate.DecodeGrantsUserList(u.Value)
|
||||
if err != nil {
|
||||
f.sayOnceLocked("the grants part's user-list key cannot be read, so the list loaded is kept: " + err.Error())
|
||||
return
|
||||
@@ -312,7 +311,7 @@ func reportable(prev, w Written) bool {
|
||||
// says why in the have.
|
||||
func (f *Follower) settleGrants(ctx context.Context, again bool) {
|
||||
f.mu.Lock()
|
||||
var g delivery.GrantsUserList
|
||||
var g grantsstate.GrantsUserList
|
||||
heard := f.grants != nil
|
||||
if heard {
|
||||
g = *f.grants
|
||||
@@ -343,8 +342,8 @@ func (f *Follower) settleGrants(ctx context.Context, again bool) {
|
||||
}
|
||||
f.mu.Unlock()
|
||||
|
||||
w := Written{Source: delivery.GrantsUserListSource, Revision: int64(rev), Wanted: g.Digest, At: f.env.Now().UTC(),
|
||||
Follows: delivery.FollowGrants, Key: f.key(), Unwatched: f.unwatchedNow()}
|
||||
w := Written{Source: grantsstate.GrantsUserListSource, Revision: int64(rev), Wanted: g.Digest, At: f.env.Now().UTC(),
|
||||
Follows: grantsstate.FollowGrants, Key: f.key(), Unwatched: f.unwatchedNow()}
|
||||
var err error
|
||||
var changed bool
|
||||
resource, rerr := mode0600(file.resource)
|
||||
@@ -357,14 +356,14 @@ func (f *Follower) settleGrants(ctx context.Context, again bool) {
|
||||
err = fmt.Errorf("the grants part's key names no list yet; the list on the machine is kept")
|
||||
case f.env.Fetch == nil || f.env.Open == nil:
|
||||
err = fmt.Errorf("this engine cannot read or open the grants part's list")
|
||||
case g.SealedTo != "" && w.Key != "" && g.SealedTo != delivery.SealingKeyID(w.Key):
|
||||
case g.SealedTo != "" && w.Key != "" && g.SealedTo != grantsstate.SealingKeyID(w.Key):
|
||||
err = fmt.Errorf("the grants part's list is sealed to another key than this node's (%s); the list on the machine is kept until it seals to this one", g.SealedTo)
|
||||
}
|
||||
var body []byte
|
||||
if err == nil {
|
||||
var object []byte
|
||||
if object, err = f.env.Fetch(ctx, g.Object); err == nil {
|
||||
body, err = delivery.OpenUserList(object, g.Digest, f.env.Open)
|
||||
body, err = grantsstate.OpenUserList(object, g.Digest, f.env.Open)
|
||||
}
|
||||
if err != nil {
|
||||
err = fmt.Errorf("the grants part's list %s could not be had: %w", short(g.Digest), err)
|
||||
@@ -395,7 +394,7 @@ func (f *Follower) settleGrants(ctx context.Context, again bool) {
|
||||
f.mod, f.state = file.module, w
|
||||
f.written = err == nil
|
||||
f.mu.Unlock()
|
||||
at := fmt.Sprintf("%s@%d", delivery.GrantsUserListSource, rev)
|
||||
at := fmt.Sprintf("%s@%d", grantsstate.GrantsUserListSource, rev)
|
||||
if err != nil {
|
||||
f.sayOnce(fmt.Sprintf("the bus's user list of %s could not be written: %v", at, err))
|
||||
} else if changed {
|
||||
|
||||
@@ -5,15 +5,14 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
"github.com/novox/mesh-host/internal/agent"
|
||||
engcomp "github.com/novox/mesh-host/internal/compositions"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
"github.com/novox/mesh-host/internal/agent"
|
||||
)
|
||||
|
||||
const node = "novox"
|
||||
@@ -126,50 +125,50 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) {
|
||||
// this node runs declares; the list a composition carries is never written, whatever becomes of the grants key.
|
||||
// B17: the file's place is read from what runs, as the agent holds it, never from a composition bucket.
|
||||
{name: "the grants part's list is written into the file the bus module it runs declares",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowGrants, content: three}},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants},
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: grantsstate.FollowGrants, content: three}},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: grantsstate.GrantsUserListSource, wantFollows: grantsstate.FollowGrants},
|
||||
{name: "a bus module it runs by digest still names the file",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two, byDigest: true}}, grants: []gput{{follow: delivery.FollowGrants, content: three}},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants},
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two, byDigest: true}}, grants: []gput{{follow: grantsstate.FollowGrants, content: three}},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: grantsstate.GrantsUserListSource, wantFollows: grantsstate.FollowGrants},
|
||||
{name: "a newer composition's list is not written",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}},
|
||||
after: []put{{module: "nats", fp: fpOf("b"), content: five}},
|
||||
grants: []gput{{follow: delivery.FollowGrants, content: three}},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants},
|
||||
grants: []gput{{follow: grantsstate.FollowGrants, content: three}},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: grantsstate.GrantsUserListSource, wantFollows: grantsstate.FollowGrants},
|
||||
{name: "a composition without the list is ignored", before: []put{{module: "web", fp: fpOf("a"), content: ""}},
|
||||
grants: []gput{{follow: delivery.FollowGrants, content: three}}},
|
||||
grants: []gput{{follow: grantsstate.FollowGrants, content: three}}},
|
||||
{name: "another node's composition declares no file",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two, asNode: "ace"}}, grants: []gput{{follow: delivery.FollowGrants, content: three}}},
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two, asNode: "ace"}}, grants: []gput{{follow: grantsstate.FollowGrants, content: three}}},
|
||||
{name: "an unreadable grants key with none heard before writes nothing, never the composition's list",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{unreadable: true}},
|
||||
onDisk: two, wantErr: "key is not heard", wantRefused: true, wantSource: delivery.GrantsUserListSource},
|
||||
onDisk: two, wantErr: "key is not heard", wantRefused: true, wantSource: grantsstate.GrantsUserListSource},
|
||||
{name: "a refused watch with no key kept writes nothing and says it is not watched",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grantsRefused: true, wantUnwatched: true,
|
||||
onDisk: two, wantErr: "key is not heard", wantRefused: true, wantSource: delivery.GrantsUserListSource},
|
||||
onDisk: two, wantErr: "key is not heard", wantRefused: true, wantSource: grantsstate.GrantsUserListSource},
|
||||
{name: "a refused watch keeps the key last heard",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grantsRefused: true, wantUnwatched: true,
|
||||
kept: &gput{follow: delivery.FollowGrants, content: three},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants},
|
||||
kept: &gput{follow: grantsstate.FollowGrants, content: three},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: grantsstate.GrantsUserListSource, wantFollows: grantsstate.FollowGrants},
|
||||
{name: "a grants key that turns unreadable keeps the loaded list",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}},
|
||||
grants: []gput{{follow: delivery.FollowGrants, content: three}, {unreadable: true, after: true}},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants},
|
||||
grants: []gput{{follow: grantsstate.FollowGrants, content: three}, {unreadable: true, after: true}},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: grantsstate.GrantsUserListSource, wantFollows: grantsstate.FollowGrants},
|
||||
{name: "a grants key that is deleted keeps the loaded list",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}},
|
||||
grants: []gput{{follow: delivery.FollowGrants, content: three}, {deleted: true, after: true}},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants},
|
||||
grants: []gput{{follow: grantsstate.FollowGrants, content: three}, {deleted: true, after: true}},
|
||||
wantWrites: []string{three}, wantUsers: 4, wantSource: grantsstate.GrantsUserListSource, wantFollows: grantsstate.FollowGrants},
|
||||
{name: "a grants list that drops more than half is refused and the file kept",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowGrants, content: one}},
|
||||
onDisk: five, wantUsers: 2, wantErr: "drops more than half", wantRefused: true, wantSource: delivery.GrantsUserListSource},
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: grantsstate.FollowGrants, content: one}},
|
||||
onDisk: five, wantUsers: 2, wantErr: "drops more than half", wantRefused: true, wantSource: grantsstate.GrantsUserListSource},
|
||||
{name: "a failed write is said and kept", before: []put{{module: "nats", fp: fpOf("a"), content: two}},
|
||||
grants: []gput{{follow: delivery.FollowGrants, content: three}}, failWrite: true,
|
||||
wantWrites: []string{three}, wantUsers: 4, wantErr: "disk full", wantSource: delivery.GrantsUserListSource},
|
||||
grants: []gput{{follow: grantsstate.FollowGrants, content: three}}, failWrite: true,
|
||||
wantWrites: []string{three}, wantUsers: 4, wantErr: "disk full", wantSource: grantsstate.GrantsUserListSource},
|
||||
{name: "a grants list that does not open to its digest is refused and the file kept",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowGrants, content: three, badDigest: true}},
|
||||
onDisk: two, wantErr: "could not be had", wantRefused: true, wantSource: delivery.GrantsUserListSource},
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: grantsstate.FollowGrants, content: three, badDigest: true}},
|
||||
onDisk: two, wantErr: "could not be had", wantRefused: true, wantSource: grantsstate.GrantsUserListSource},
|
||||
{name: "a grants list sealed to another key is refused and the file kept",
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: delivery.FollowGrants, content: three, otherKey: true}},
|
||||
onDisk: two, wantErr: "sealed to another key", wantRefused: true, wantSource: delivery.GrantsUserListSource},
|
||||
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: grantsstate.FollowGrants, content: three, otherKey: true}},
|
||||
onDisk: two, wantErr: "sealed to another key", wantRefused: true, wantSource: grantsstate.GrantsUserListSource},
|
||||
} {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
@@ -194,7 +193,7 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) {
|
||||
}
|
||||
for i, g := range gs {
|
||||
if g.unreadable || g.deleted {
|
||||
u := agent.Update{Key: delivery.GrantsUserListKey, Value: []byte("not a user-list key"), Revision: uint64(i + 1), Deleted: g.deleted}
|
||||
u := agent.Update{Key: grantsstate.GrantsUserListKey, Value: []byte("not a user-list key"), Revision: uint64(i + 1), Deleted: g.deleted}
|
||||
grantsAfter = append(grantsAfter, u)
|
||||
continue
|
||||
}
|
||||
@@ -203,21 +202,21 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) {
|
||||
other, _ := seal.GenerateKeyPair()
|
||||
to = other.PublicKey
|
||||
}
|
||||
digest := delivery.UserListDigest([]byte(g.content))
|
||||
digest := grantsstate.UserListDigest([]byte(g.content))
|
||||
if g.badDigest {
|
||||
digest = delivery.UserListDigest([]byte("another list"))
|
||||
digest = grantsstate.UserListDigest([]byte("another list"))
|
||||
}
|
||||
object, err := delivery.SealUserList([]byte(g.content), to)
|
||||
object, err := grantsstate.SealUserList([]byte(g.content), to)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sealed[delivery.UserListObjectName(digest)] = object
|
||||
raw, err := delivery.EncodeGrantsUserList(delivery.GrantsUserList{Follow: g.follow, Digest: digest,
|
||||
Object: delivery.UserListObjectName(digest), SealedTo: delivery.SealingKeyID(to), Node: node, Revision: uint64(i + 1)})
|
||||
sealed[grantsstate.UserListObjectName(digest)] = object
|
||||
raw, err := grantsstate.EncodeGrantsUserList(grantsstate.GrantsUserList{Follow: g.follow, Digest: digest,
|
||||
Object: grantsstate.UserListObjectName(digest), SealedTo: grantsstate.SealingKeyID(to), Node: node, Revision: uint64(i + 1)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
u := agent.Update{Key: delivery.GrantsUserListKey, Value: raw, Revision: uint64(i + 1)}
|
||||
u := agent.Update{Key: grantsstate.GrantsUserListKey, Value: raw, Revision: uint64(i + 1)}
|
||||
switch {
|
||||
case c.kept != nil && i == len(gs)-1:
|
||||
keptRaw = raw
|
||||
@@ -296,7 +295,7 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) {
|
||||
}
|
||||
if len(grantsAfter) > 0 {
|
||||
for until := time.Now().Add(5 * time.Second); ; time.Sleep(5 * time.Millisecond) {
|
||||
if _, st := fl.State(); st.Source == delivery.GrantsUserListSource || time.Now().After(until) {
|
||||
if _, st := fl.State(); st.Source == grantsstate.GrantsUserListSource || time.Now().After(until) {
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -355,11 +354,11 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) {
|
||||
t.Fatalf("an apply while following %s holds the composition's list: %v", c.wantFollows, hasList)
|
||||
}
|
||||
// A list on the machine that cannot be read stops the apply; it is never left out to be removed.
|
||||
if c.wantFollows == delivery.FollowGrants {
|
||||
if c.wantFollows == grantsstate.FollowGrants {
|
||||
readErr := New(Env{Node: node, OnDisk: func(json.RawMessage) ([]byte, error) {
|
||||
return nil, errors.New("permission denied")
|
||||
}})
|
||||
readErr.grants = &delivery.GrantsUserList{Follow: delivery.FollowGrants}
|
||||
readErr.grants = &grantsstate.GrantsUserList{Follow: grantsstate.FollowGrants}
|
||||
if _, err := readErr.Hold(cmp); err == nil || !strings.Contains(err.Error(), "could not be read") {
|
||||
t.Fatalf("an unreadable list on the machine was held as %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user