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:
2026-11-05 15:47:59 +01:00
parent 4bfa94dfef
commit 13e2fe14aa
22 changed files with 248 additions and 219 deletions
+2 -2
View File
@@ -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 {
+13
View File
@@ -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"
)
+5 -5
View File
@@ -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) {
+9 -7
View File
@@ -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
View File
@@ -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 {
+24 -17
View File
@@ -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)
+2 -2
View File
@@ -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.
+8 -8
View File
@@ -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
}
+4 -5
View File
@@ -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
View File
@@ -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"
+8 -8
View File
@@ -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) {
+12 -12
View File
@@ -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:])
}
+2 -2
View File
@@ -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
}
+14 -15
View File
@@ -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 {
+38 -39
View File
@@ -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)
}