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
+20 -20
View File
@@ -7,6 +7,7 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"encoding/pem" "encoding/pem"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
engcomp "github.com/novox/mesh-host/internal/compositions" engcomp "github.com/novox/mesh-host/internal/compositions"
"os" "os"
"path/filepath" "path/filepath"
@@ -16,7 +17,6 @@ import (
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay" "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/identity" "github.com/novox/mesh-host/internal/identity"
) )
@@ -52,38 +52,38 @@ func TestTheAppliedAuthorityIsKeptForTheRecord(t *testing.T) {
told := 0 told := 0
trust.onChange(func() { told++ }) trust.onChange(func() { told++ })
keys := recordKeys(state, trust.Trusts) keys := recordKeys(state, trust.Trusts)
ctrlFP, vaultFP := delivery.SignerFingerprint(controllerPub), delivery.SignerFingerprint(vaultPub) ctrlFP, vaultFP := meshvaultclient.SignerFingerprint(controllerPub), meshvaultclient.SignerFingerprint(vaultPub)
enrol, _ := delivery.SignContext(delivery.KindEnrolment) enrol, _ := meshvaultclient.SignContext(meshvaultclient.KindEnrolment)
msg := []byte("a reply") msg := []byte("a reply")
if got := keys().Trusts; len(got) != 0 { if got := keys().Trusts; len(got) != 0 {
t.Errorf("before the signers are read the record trusts %v", got) t.Errorf("before the signers are read the record trusts %v", got)
} }
if _, err := trust.Verify(delivery.KindEnrolment, msg, delivery.SignMessage(controller, enrol, msg)); err == nil { if _, err := trust.Verify(meshvaultclient.KindEnrolment, msg, meshvaultclient.SignMessage(controller, enrol, msg)); err == nil {
t.Error("a controller-signed message is accepted") t.Error("a controller-signed message is accepted")
} }
if _, err := trust.Verify(delivery.KindEnrolment, msg, delivery.SignMessage(vault, enrol, msg)); err == nil { if _, err := trust.Verify(meshvaultclient.KindEnrolment, msg, meshvaultclient.SignMessage(vault, enrol, msg)); err == nil {
t.Error("a vault-signed message is accepted before the vault's key is read") t.Error("a vault-signed message is accepted before the vault's key is read")
} }
vaultCA := testAuthority(t, vaultPub, vault) vaultCA := testAuthority(t, vaultPub, vault)
keyValue, _ := json.Marshal(delivery.NewSignerKey(vaultPub, time.Now())) keyValue, _ := json.Marshal(meshvaultclient.NewSignerKey(vaultPub, time.Now()))
fp, _ := delivery.AuthorityFingerprint(vaultCA) fp, _ := meshvaultclient.AuthorityFingerprint(vaultCA)
authValue, _ := json.Marshal(delivery.SignerAuthority{Certificate: vaultCA, Fingerprint: fp, Made: time.Now()}) authValue, _ := json.Marshal(meshvault.SignersByAuthorityValue{Certificate: vaultCA, Fingerprint: fp, Made: time.Now()})
trust.heardSigner(delivery.SignersKeyKey, keyValue, false, func(string) {}) trust.heardSigner(meshvault.SignersByKeyKey(), keyValue, false, func(string) {})
trust.heardSigner(delivery.SignersAuthorityKey, authValue, false, func(string) {}) trust.heardSigner(meshvault.SignersByAuthorityKey(), authValue, false, func(string) {})
if s, err := trust.Verify(delivery.KindEnrolment, msg, delivery.SignMessage(vault, enrol, msg)); err != nil || s.Name != delivery.SignerVault { if s, err := trust.Verify(meshvaultclient.KindEnrolment, msg, meshvaultclient.SignMessage(vault, enrol, msg)); err != nil || s.Name != meshvaultclient.SignerVault {
t.Errorf("a vault-signed message on a node that read signers: %+v, %v", s, err) t.Errorf("a vault-signed message on a node that read signers: %+v, %v", s, err)
} }
if _, err := trust.Verify(delivery.KindEnrolment, msg, delivery.SignMessage(controller, enrol, msg)); err == nil { if _, err := trust.Verify(meshvaultclient.KindEnrolment, msg, meshvaultclient.SignMessage(controller, enrol, msg)); err == nil {
t.Error("a controller-signed message is accepted once the vault's key is read") t.Error("a controller-signed message is accepted once the vault's key is read")
} }
if got := keys().Trusts; strings.Contains(strings.Join(got, " "), ctrlFP) || !delivery.TrustsKey(got, delivery.SignerVault, vaultFP) || if got := keys().Trusts; strings.Contains(strings.Join(got, " "), ctrlFP) || !meshvaultclient.TrustsKey(got, meshvaultclient.SignerVault, vaultFP) ||
trust.VaultAuthority() != vaultCA || told != 2 { trust.VaultAuthority() != vaultCA || told != 2 {
t.Errorf("after the signers are read the record trusts %v (told %d)", got, told) t.Errorf("after the signers are read the record trusts %v (told %d)", got, told)
} }
// A signers' key naming another fingerprint is not trusted. // A signers' key naming another fingerprint is not trusted.
bad, _ := json.Marshal(delivery.SignerKey{Key: base64.StdEncoding.EncodeToString(controllerPub), Fingerprint: vaultFP}) bad, _ := json.Marshal(meshvault.SignersByKeyValue{Key: base64.StdEncoding.EncodeToString(controllerPub), Fingerprint: vaultFP})
trust.heardSigner(delivery.SignersKeyKey, bad, false, func(string) {}) trust.heardSigner(meshvault.SignersByKeyKey(), bad, false, func(string) {})
if !delivery.TrustsKey(keys().Trusts, delivery.SignerVault, vaultFP) { if !meshvaultclient.TrustsKey(keys().Trusts, meshvaultclient.SignerVault, vaultFP) {
t.Error("a signers' key that does not check replaced the vault's") t.Error("a signers' key that does not check replaced the vault's")
} }
@@ -98,8 +98,8 @@ func TestTheAppliedAuthorityIsKeptForTheRecord(t *testing.T) {
} }
servingPub, _ := base64.StdEncoding.DecodeString(serving.Public) servingPub, _ := base64.StdEncoding.DecodeString(serving.Public)
signedBy := func(ca string, key ed25519.PrivateKey) string { signedBy := func(ca string, key ed25519.PrivateKey) string {
parent, _ := delivery.ParseCertificate(ca) parent, _ := meshvaultclient.ParseCertificate(ca)
der, err := x509.CreateCertificate(nil, delivery.NodeCertificateTemplate(overlay.InternalName("n1"), servingPub, time.Now().Add(-time.Hour)), der, err := x509.CreateCertificate(nil, meshvaultclient.NodeCertificateTemplate(overlay.InternalName("n1"), servingPub, time.Now().Add(-time.Hour)),
parent, ed25519.PublicKey(servingPub), key) parent, ed25519.PublicKey(servingPub), key)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -110,7 +110,7 @@ func TestTheAppliedAuthorityIsKeptForTheRecord(t *testing.T) {
answer := signedBy(vaultCA, vault) answer := signedBy(vaultCA, vault)
ask := func(_ context.Context, verb string, args any) (json.RawMessage, error) { ask := func(_ context.Context, verb string, args any) (json.RawMessage, error) {
asked = append(asked, verb, args) asked = append(asked, verb, args)
return json.Marshal(meshvault.SignAnswer{Kind: delivery.KindNodeCertificate, Node: "n1", Certificate: answer}) return json.Marshal(meshvault.SignAnswer{Kind: meshvaultclient.KindNodeCertificate, Node: "n1", Certificate: answer})
} }
written := 0 written := 0
if err := certifyThisNode(context.Background(), state, "n1", trust, ask, func() { written++ }); err != nil { if err := certifyThisNode(context.Background(), state, "n1", trust, ask, func() { written++ }); err != nil {
@@ -160,7 +160,7 @@ func TestTheAppliedAuthorityIsKeptForTheRecord(t *testing.T) {
// testAuthority is a self-signed authority certificate for key, as the vault makes its own. // testAuthority is a self-signed authority certificate for key, as the vault makes its own.
func testAuthority(t *testing.T, pub ed25519.PublicKey, key ed25519.PrivateKey) string { func testAuthority(t *testing.T, pub ed25519.PublicKey, key ed25519.PrivateKey) string {
t.Helper() t.Helper()
tpl := delivery.AuthorityTemplate(pub, time.Now().Add(-time.Hour)) tpl := meshvaultclient.AuthorityTemplate(pub, time.Now().Add(-time.Hour))
der, err := x509.CreateCertificate(nil, tpl, tpl, pub, key) der, err := x509.CreateCertificate(nil, tpl, tpl, pub, key)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
+6 -7
View File
@@ -6,10 +6,9 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
"os" "os"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/identity" "github.com/novox/mesh-host/internal/identity"
"github.com/novox/mesh-host/internal/link" "github.com/novox/mesh-host/internal/link"
) )
@@ -21,7 +20,7 @@ import (
func enrolThroughSeat(ctx context.Context, opts options, token identity.Token, node string, mine identity.Identity, func enrolThroughSeat(ctx context.Context, opts options, token identity.Token, node string, mine identity.Identity,
sealing identity.SealingKey, serving identity.ServingKey, reported map[string]any, found *link.Tunnel, identityPath string) error { sealing identity.SealingKey, serving identity.ServingKey, reported map[string]any, found *link.Tunnel, identityPath string) error {
req := delivery.Redeem{Node: node, PublicKey: base64.StdEncoding.EncodeToString(mine.Public), OverlayKey: mine.Overlay.Public, req := meshnodeenrollerclient.Redeem{Node: node, PublicKey: base64.StdEncoding.EncodeToString(mine.Public), OverlayKey: mine.Overlay.Public,
SealingKey: sealing.Public, ServingKey: serving.Public, Profile: reported} SealingKey: sealing.Public, ServingKey: serving.Public, Profile: reported}
if found != nil { if found != nil {
var err error var err error
@@ -29,18 +28,18 @@ func enrolThroughSeat(ctx context.Context, opts options, token identity.Token, n
return err return err
} }
} }
req.Proof = base64.StdEncoding.EncodeToString(mine.Sign(delivery.RedeemProof(node, req.PublicKey, req.OverlayKey, req.SealingKey, req.ServingKey))) req.Proof = base64.StdEncoding.EncodeToString(mine.Sign(meshnodeenrollerclient.RedeemProof(node, req.PublicKey, req.OverlayKey, req.SealingKey, req.ServingKey)))
to := link.Approach{Address: token.Broker, Fingerprint: token.Fingerprint, Transport: link.OnNATS} to := link.Approach{Address: token.Broker, Fingerprint: token.Fingerprint, Transport: link.OnNATS}
membership := link.Membership{Node: node, Broker: token.Broker, Fingerprint: token.Fingerprint, Transport: link.OnNATS} membership := link.Membership{Node: node, Broker: token.Broker, Fingerprint: token.Fingerprint, Transport: link.OnNATS}
j := link.Joining{Node: node, Signer: token.SignerKey(), j := link.Joining{Node: node, Signer: token.SignerKey(),
Redeem: func(ctx context.Context) (delivery.RedeemAnswer, error) { Redeem: func(ctx context.Context) (meshnodeenrollerclient.RedeemAnswer, error) {
return link.Redeem(ctx, to, node, token.Secret, req, opts.timeout) return link.Redeem(ctx, to, node, token.Secret, req, opts.timeout)
}, },
Open: func(a delivery.RedeemAnswer) (string, error) { Open: func(a meshnodeenrollerclient.RedeemAnswer) (string, error) {
value, err := sealing.Open(a.Account) value, err := sealing.Open(a.Account)
return string(value), err return string(value), err
}, },
Keep: func(a delivery.RedeemAnswer, password string) error { Keep: func(a meshnodeenrollerclient.RedeemAnswer, password string) error {
// The mesh's name for this node wins over what the machine called itself, as on the controller's path. // The mesh's name for this node wins over what the machine called itself, as on the controller's path.
mine.Node = a.Node mine.Node = a.Node
mine.Membership = identity.Membership{Broker: token.Broker, Fingerprint: token.Fingerprint, Signer: token.Signer, mine.Membership = identity.Membership{Broker: token.Broker, Fingerprint: token.Fingerprint, Signer: token.Signer,
+7 -6
View File
@@ -4,12 +4,13 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"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"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/identity" "github.com/novox/mesh-host/internal/identity"
"github.com/novox/mesh-host/internal/link" "github.com/novox/mesh-host/internal/link"
) )
@@ -48,7 +49,7 @@ var loginsFollowAgain = 2 * time.Minute
// followLogins follows this node's key of the vault's node-logins on every link, as a watch (never a poll), for as long as ctx // followLogins follows this node's key of the vault's node-logins on every link, as a watch (never a poll), for as long as ctx
// lasts, and hands each value to hear. A watch that cannot be made is said once and tried again. // lasts, and hands each value to hear. A watch that cannot be made is said once and tried again.
func followLogins(ctx context.Context, session *link.Session, node string, hear func(delivery.NodeLogin), say link.Announce) { func followLogins(ctx context.Context, session *link.Session, node string, hear func(meshvault.NodeLoginsByNodeValue), say link.Announce) {
said := "" said := ""
for ctx.Err() == nil { for ctx.Err() == nil {
linked, cancel, err := session.WhenLinked(ctx) linked, cancel, err := session.WhenLinked(ctx)
@@ -56,7 +57,7 @@ func followLogins(ctx context.Context, session *link.Session, node string, hear
return return
} }
watching, stop := context.WithCancel(linked) watching, stop := context.WithCancel(linked)
updates, werr := session.KVWatch(watching, delivery.NodeLoginsBucket, node) updates, werr := session.KVWatch(watching, buckets.NodeLogins, node)
if werr != nil { if werr != nil {
stop() stop()
if line := "this node's login is not followed yet (" + werr.Error() + "): it dials as it last did, and looks again in " + loginsFollowAgain.String(); line != said { if line := "this node's login is not followed yet (" + werr.Error() + "): it dials as it last did, and looks again in " + loginsFollowAgain.String(); line != said {
@@ -69,9 +70,9 @@ func followLogins(ctx context.Context, session *link.Session, node string, hear
if u.Caught || u.Deleted || len(u.Value) == 0 { if u.Caught || u.Deleted || len(u.Value) == 0 {
continue continue
} }
nl, err := delivery.DecodeNodeLogin(u.Value) nl, err := meshvaultclient.DecodeNodeLogin(u.Value)
if err != nil { if err != nil {
say("the key " + node + " of " + delivery.NodeLoginsRead + " is not read: " + err.Error()) say("the key " + node + " of " + buckets.NodeLoginsRead + " is not read: " + err.Error())
continue continue
} }
hear(nl) hear(nl)
+2 -2
View File
@@ -16,6 +16,7 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
"io" "io"
"net" "net"
"os" "os"
@@ -30,7 +31,6 @@ import (
"time" "time"
"git.novox.be/novox/mesh-sdk/go/declaration" "git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/accounts" "github.com/novox/mesh-host/internal/accounts"
"github.com/novox/mesh-host/internal/agent" "github.com/novox/mesh-host/internal/agent"
"github.com/novox/mesh-host/internal/apply" "github.com/novox/mesh-host/internal/apply"
@@ -1075,7 +1075,7 @@ func runLink(ctx context.Context, opts options) error {
}, say) }, say)
// And the login it dials as, which the vault's node-logins names for this node (novox/hq ADR 0583 §1). // And the login it dials as, which the vault's node-logins names for this node (novox/hq ADR 0583 §1).
rotator := newRotator(queue, opts.state, opts.timeout, say) rotator := newRotator(queue, opts.state, opts.timeout, say)
go followLogins(aside, queue, mine.Node, func(nl delivery.NodeLogin) { go followLogins(aside, queue, mine.Node, func(nl meshvault.NodeLoginsByNodeValue) {
if line := membership.Logins.Heard(nl); line != "" { if line := membership.Logins.Heard(nl); line != "" {
say(line) say(line)
} }
+20 -20
View File
@@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "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/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
@@ -21,7 +22,6 @@ import (
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault" "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay" "git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/identity" "github.com/novox/mesh-host/internal/identity"
"github.com/novox/mesh-host/internal/link" "github.com/novox/mesh-host/internal/link"
@@ -51,23 +51,23 @@ func (t *meshTrust) onChange(f func()) {
} }
// Signers are the keys a mesh signature is verified against now: the vault's once read, and none before. // Signers are the keys a mesh signature is verified against now: the vault's once read, and none before.
func (t *meshTrust) Signers() []delivery.Signer { func (t *meshTrust) Signers() []meshvaultclient.Signer {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
var out []delivery.Signer var out []meshvaultclient.Signer
if t.vault != nil { if t.vault != nil {
out = append(out, delivery.Signer{Name: delivery.SignerVault, Key: t.vault}) out = append(out, meshvaultclient.Signer{Name: meshvaultclient.SignerVault, Key: t.vault})
} }
return out return out
} }
// Verify is the signer that signed msg as kind, or why none it trusts did. // Verify is the signer that signed msg as kind, or why none it trusts did.
func (t *meshTrust) Verify(kind string, msg, sig []byte) (delivery.Signer, error) { func (t *meshTrust) Verify(kind string, msg, sig []byte) (meshvaultclient.Signer, error) {
context, err := delivery.SignContext(kind) context, err := meshvaultclient.SignContext(kind)
if err != nil { if err != nil {
return delivery.Signer{}, err return meshvaultclient.Signer{}, err
} }
return delivery.VerifyBy(t.Signers(), context, msg, sig) return meshvaultclient.VerifyBy(t.Signers(), context, msg, sig)
} }
// VaultAuthority is the vault's authority certificate as read from `signers`, or "". // VaultAuthority is the vault's authority certificate as read from `signers`, or "".
@@ -87,9 +87,9 @@ func (t *meshTrust) Trusts() []string {
} }
authorities := map[string]string{} authorities := map[string]string{}
if a := t.VaultAuthority(); a != "" { if a := t.VaultAuthority(); a != "" {
authorities[delivery.SignerVault] = a authorities[meshvaultclient.SignerVault] = a
} }
return delivery.Trusts(t.Signers(), authorities) return meshvaultclient.Trusts(t.Signers(), authorities)
} }
// change applies f under the lock and tells who follows when Trusts differs after it. // change applies f under the lock and tells who follows when Trusts differs after it.
@@ -108,25 +108,25 @@ func (t *meshTrust) change(f func()) {
// check; a deleted key is no longer trusted. // check; a deleted key is no longer trusted.
func (t *meshTrust) heardSigner(key string, value []byte, deleted bool, say link.Announce) { func (t *meshTrust) heardSigner(key string, value []byte, deleted bool, say link.Announce) {
switch key { switch key {
case delivery.SignersKeyKey: case meshvault.SignersByKeyKey():
var pub ed25519.PublicKey var pub ed25519.PublicKey
if !deleted && len(value) > 0 { if !deleted && len(value) > 0 {
k, err := delivery.DecodeSignerKey(value) k, err := meshvaultclient.DecodeSignerKey(value)
if err == nil { if err == nil {
pub, err = k.PublicKey() pub, err = meshvaultclient.SignerPublicKey(k)
} }
if err != nil { if err != nil {
say("the vault's signing key in " + delivery.SignersRead + " is not trusted: " + err.Error()) say("the vault's signing key in " + buckets.SignersRead + " is not trusted: " + err.Error())
return return
} }
} }
t.change(func() { t.vault = pub }) t.change(func() { t.vault = pub })
case delivery.SignersAuthorityKey: case meshvault.SignersByAuthorityKey():
pem := "" pem := ""
if !deleted && len(value) > 0 { if !deleted && len(value) > 0 {
a, err := delivery.DecodeSignerAuthority(value) a, err := meshvaultclient.DecodeSignerAuthority(value)
if err != nil { if err != nil {
say("the vault's authority in " + delivery.SignersRead + " is not trusted: " + err.Error()) say("the vault's authority in " + buckets.SignersRead + " is not trusted: " + err.Error())
return return
} }
pem = a.Certificate pem = a.Certificate
@@ -157,7 +157,7 @@ func followTrust(ctx context.Context, session *link.Session, trust *meshTrust, s
return return
} }
watching, stop := context.WithCancel(linked) watching, stop := context.WithCancel(linked)
signers, serr := session.KVWatch(watching, delivery.SignersBucket, ">") signers, serr := session.KVWatch(watching, buckets.Signers, ">")
if serr != nil { if serr != nil {
stop() stop()
sayOnce(fmt.Sprintf("what this node trusts a mesh signature from is not followed yet (the vault's signers: %v): "+ sayOnce(fmt.Sprintf("what this node trusts a mesh signature from is not followed yet (the vault's signers: %v): "+
@@ -266,7 +266,7 @@ func certifyThisNode(ctx context.Context, state, node string, trust *meshTrust,
if err != nil || len(key) != ed25519.PublicKeySize { if err != nil || len(key) != ed25519.PublicKeySize {
return fmt.Errorf("this node's serving key is not an ed25519 key") return fmt.Errorf("this node's serving key is not an ed25519 key")
} }
raw, err := ask(ctx, meshvault.VerbSign, map[string]any{"kind": delivery.KindNodeCertificate, "node": node}) raw, err := ask(ctx, meshvault.VerbSign, map[string]any{"kind": meshvaultclient.KindNodeCertificate, "node": node})
if err != nil { if err != nil {
return err return err
} }
@@ -278,7 +278,7 @@ func certifyThisNode(ctx context.Context, state, node string, trust *meshTrust,
if trusted == "" { if trusted == "" {
return fmt.Errorf("this node has read no authority of the vault's to check the certificate against") return fmt.Errorf("this node has read no authority of the vault's to check the certificate against")
} }
if err := delivery.CheckNodeCertificate(a.Certificate, []string{trusted}, overlay.InternalName(node), if err := meshvaultclient.CheckNodeCertificate(a.Certificate, []string{trusted}, overlay.InternalName(node),
ed25519.PublicKey(key), time.Now()); err != nil { ed25519.PublicKey(key), time.Now()); err != nil {
return err return err
} }
+2 -2
View File
@@ -5,12 +5,12 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
engcomp "github.com/novox/mesh-host/internal/compositions" engcomp "github.com/novox/mesh-host/internal/compositions"
"os" "os"
"path/filepath" "path/filepath"
"git.novox.be/novox/mesh-sdk/go/declaration" "git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/seal" "git.novox.be/novox/mesh-sdk/go/seal"
"github.com/novox/mesh-host/internal/apply" "github.com/novox/mesh-host/internal/apply"
"github.com/novox/mesh-host/internal/identity" "github.com/novox/mesh-host/internal/identity"
@@ -44,7 +44,7 @@ func userListFollower(opts options, session *link.Session, running func() []engc
}, },
// From take-over, the grants part's list (ADR 0547 clauses 7, 8): its one key watched, its object read by name and // From take-over, the grants part's list (ADR 0547 clauses 7, 8): its one key watched, its object read by name and
// opened with this machine's sealing key, the key's public half said in the have so the grants part seals to it. // opened with this machine's sealing key, the key's public half said in the have so the grants part seals to it.
Grants: bucketWatch{session, delivery.GrantsUserListBucket}, Grants: bucketWatch{session, grantsstate.GrantsUserListBucket},
Fetch: func(ctx context.Context, name string) ([]byte, error) { Fetch: func(ctx context.Context, name string) ([]byte, error) {
return session.GrantsObject(ctx, name, grantsListLimit) return session.GrantsObject(ctx, name, grantsListLimit)
}, },
+3
View File
@@ -3,6 +3,9 @@ module github.com/novox/mesh-host
go 1.26.0 go 1.26.0
require ( require (
git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient v0.0.0-20261105000000-000000000000
git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller v0.0.0-20261105000000-000000000000
git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate v0.0.0-20261105000000-000000000000
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0 git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0
git.novox.be/novox/mesh-delivery/sdk v0.0.0-20261105000000-000000000000 git.novox.be/novox/mesh-delivery/sdk v0.0.0-20261105000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-20261105000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-20261105000000-000000000000
+2 -2
View File
@@ -10,6 +10,7 @@ import (
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"github.com/novox/mesh-host/internal/buckets/bucketstest" "github.com/novox/mesh-host/internal/buckets/bucketstest"
"github.com/novox/mesh-host/internal/compositions" "github.com/novox/mesh-host/internal/compositions"
"github.com/novox/mesh-host/internal/judge"
"maps" "maps"
"slices" "slices"
"strings" "strings"
@@ -20,7 +21,6 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/declaration" "git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
) )
const node = "n1" const node = "n1"
@@ -1692,7 +1692,7 @@ func TestTheNodesOwnCompositionIsAppliedWhenItDiffersAndARecordMoveWaitsForAYes(
t.Errorf("the composition of db waiting for a yes reads %+v (%v)", view, err) 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 || 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) 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 { 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 ( import (
"errors" "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" assigner "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
seat "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" 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 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" "errors"
"fmt" "fmt"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "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/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "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-networking/modules/mesh-wireguard/go/overlay"
"git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/declaration" "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"
"github.com/novox/mesh-host/internal/compose/resolution" "github.com/novox/mesh-host/internal/compose/resolution"
"github.com/novox/mesh-host/internal/takein" "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) 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, // SignersReader is a Reader that also reads the vault's public authority from its state `signers` (buckets.Signers,
// key delivery.SignersAuthorityKey; ADR 0550 §2). A Reader that has it composes the mesh's authority as a bundle, the // 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. // controller's and the vault's; one that has not, or that answers none, composes the controller's alone, as before.
type SignersReader interface { 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 // 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. // state yet composes as before rather than not at all.
VaultAuthority(ctx context.Context) string 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. // 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 sr, ok := r.(SignersReader); ok {
if vault := sr.VaultAuthority(ctx); vault != "" { 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) { named := func(field *map[string]string, module, why string) {
+9 -7
View File
@@ -1,15 +1,17 @@
package identity 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 // 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 // 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. // 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). // TokenTunnel is the joining machine's side of its first tunnel (meshnodeenrollerclient.TokenTunnel).
type TokenTunnel = delivery.TokenTunnel type TokenTunnel = meshnodeenrollerclient.TokenTunnel
// ParseToken reads a token a person pasted (delivery.ParseToken). // ParseToken reads a token a person pasted (meshnodeenrollerclient.ParseToken).
func ParseToken(encoded string) (Token, error) { return delivery.ParseToken(encoded) } func ParseToken(encoded string) (Token, error) { return meshnodeenrollerclient.ParseToken(encoded) }
+35 -33
View File
@@ -7,12 +7,14 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "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" "strings"
"time" "time"
"github.com/nats-io/nats.go" "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> // 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, // 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 // 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. // 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 { if a.Node != node {
return fmt.Errorf("the answer is for %q, and this machine asked as %q", 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 { if err != nil || len(sig) == 0 {
return errors.New("the answer is not signed") return errors.New("the answer is not signed")
} }
kindContext, err := delivery.SignContext(delivery.KindEnrolment) kindContext, err := meshvaultclient.SignContext(meshvaultclient.KindEnrolment)
if err != nil { if err != nil {
return err 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) return fmt.Errorf("the answer is not signed by the key the token carries: %w", err)
} }
if a.Login == "" || a.Account == "" { 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 // 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 // EnrolPatience. An answer is returned as the holder gave it: the caller
// verifies it (VerifyRedeemAnswer) before it keeps anything. // 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 { 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) config, err := PinnedConfig(to.Fingerprint)
if err != nil { 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), 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()) nats.UserInfo(user, secret), nats.Name("mesh-host/enrol/"+node), nats.Timeout(timeout), nats.NoReconnect())
if err != nil { if err != nil {
if errors.Is(err, ErrWrongCertificate) { 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() defer conn.Close()
body, err := json.Marshal(req) body, err := json.Marshal(req)
if err != nil { if err != nil {
return delivery.RedeemAnswer{}, err return meshnodeenrollerclient.RedeemAnswer{}, err
} }
began := time.Now() began := time.Now()
for { for {
msg, err := conn.RequestWithContext(ctx, CalledSeatSubject(EnrolmentSeat, EnrolmentRedeem, user), body) msg, err := conn.RequestWithContext(ctx, CalledSeatSubject(EnrolmentSeat, EnrolmentRedeem, user), body)
switch { switch {
case errors.Is(err, nats.ErrNoResponders) || (err != nil && strings.Contains(err.Error(), "permissions violation")): 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: 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) result, aerr := seatAnswer(msg.Data)
if aerr != nil { if aerr != nil {
if strings.Contains(aerr.Error(), "call redeem again with the same keys") && time.Since(began) < EnrolPatience { if strings.Contains(aerr.Error(), "call redeem again with the same keys") && time.Since(began) < EnrolPatience {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return delivery.RedeemAnswer{}, ctx.Err() return meshnodeenrollerclient.RedeemAnswer{}, ctx.Err()
case <-time.After(AskAgainAfter): case <-time.After(AskAgainAfter):
continue 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 { 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 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. // 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 { type Confirming interface {
// Login is this node's key of the vault's node-logins. // 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 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 Confirm(ctx context.Context) error
Close() Close()
@@ -149,29 +151,29 @@ type Joining struct {
Signer ed25519.PublicKey Signer ed25519.PublicKey
// Redeem calls the seat; Open opens the account sealed to this node's sealing key; Keep writes the membership, the login // 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. // 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) Redeem func(ctx context.Context) (meshnodeenrollerclient.RedeemAnswer, error)
Open func(a delivery.RedeemAnswer) (string, error) Open func(a meshnodeenrollerclient.RedeemAnswer) (string, error)
Keep func(a delivery.RedeemAnswer, password string) error Keep func(a meshnodeenrollerclient.RedeemAnswer, password string) error
Dial func(ctx context.Context, login, password string) (Confirming, 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), // 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 // 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. // 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) answer, err := j.Redeem(ctx)
if err != nil { if err != nil {
return delivery.RedeemAnswer{}, err return meshnodeenrollerclient.RedeemAnswer{}, err
} }
if err := VerifyRedeemAnswer(answer, j.Node, j.Signer); err != nil { 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) password, err := j.Open(answer)
if err != nil { 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 { 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) "That token is spent, so getting back needs a new one", answer.Node, err)
} }
c, err := j.Dial(ctx, answer.Login, password) 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 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() js, err := c.conn.JetStream()
if err != nil { 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 { if err != nil {
return delivery.NodeLogin{}, err return meshvault.NodeLoginsByNodeValue{}, err
} }
e, err := kv.Get(c.node) e, err := kv.Get(c.node)
if err != nil { 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 { func (c *confirmingNats) Confirm(ctx context.Context) error {
+24 -17
View File
@@ -6,33 +6,34 @@ import (
"crypto/ed25519" "crypto/ed25519"
"encoding/base64" "encoding/base64"
"errors" "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" "slices"
"strings" "strings"
"testing" "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. // 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() t.Helper()
priv := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{seed}, ed25519.SeedSize)) priv := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{seed}, ed25519.SeedSize))
pub := priv.Public().(ed25519.PublicKey) pub := priv.Public().(ed25519.PublicKey)
a := delivery.RedeemAnswer{Node: "ace", Login: "node.ace", Account: `{"sealed":"box"}`, Certificate: "PEM", Signer: base64.StdEncoding.EncodeToString(pub)} a := meshnodeenrollerclient.RedeemAnswer{Node: "ace", Login: "node.ace", Account: `{"sealed":"box"}`, Certificate: "PEM", Signer: base64.StdEncoding.EncodeToString(pub)}
ctx, err := delivery.SignContext(delivery.KindEnrolment) ctx, err := meshvaultclient.SignContext(meshvaultclient.KindEnrolment)
if err != nil { if err != nil {
t.Fatal(err) 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 return a, pub
} }
type standInConfirming struct { type standInConfirming struct {
steps *[]string 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") *c.steps = append(*c.steps, "read node-logins")
return c.nl, nil return c.nl, nil
} }
@@ -52,18 +53,21 @@ func TestTheEngineKeepsOnlyAnAnswerSignedByTheKeyItsTokenCarries(t *testing.T) {
badNode := good badNode := good
badNode.Node = "g14" badNode.Node = "g14"
for name, c := range map[string]struct { for name, c := range map[string]struct {
answer delivery.RedeemAnswer answer meshnodeenrollerclient.RedeemAnswer
want string want string
}{ }{
"signed by another key": {forged, "signing key other than the one the token carries"}, "signed by another key": {forged, "signing key other than the one the token carries"},
"another node": {badNode, `is for "g14"`}, "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 var steps []string
j := Joining{Node: "ace", Signer: pub, j := Joining{Node: "ace", Signer: pub,
Redeem: func(context.Context) (delivery.RedeemAnswer, error) { return c.answer, nil }, Redeem: func(context.Context) (meshnodeenrollerclient.RedeemAnswer, error) { return c.answer, nil },
Open: func(delivery.RedeemAnswer) (string, error) { steps = append(steps, "open"); return "pw", nil }, Open: func(meshnodeenrollerclient.RedeemAnswer) (string, error) {
Keep: func(delivery.RedeemAnswer, string) error { steps = append(steps, "keep"); return nil }, 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) { Dial: func(context.Context, string, string) (Confirming, error) {
steps = append(steps, "dial") steps = append(steps, "dial")
return nil, errors.New("must not dial") return nil, errors.New("must not dial")
@@ -75,18 +79,21 @@ func TestTheEngineKeepsOnlyAnAnswerSignedByTheKeyItsTokenCarries(t *testing.T) {
var steps []string var steps []string
login := "node.ace" login := "node.ace"
j := Joining{Node: "ace", Signer: pub, j := Joining{Node: "ace", Signer: pub,
Redeem: func(context.Context) (delivery.RedeemAnswer, error) { Redeem: func(context.Context) (meshnodeenrollerclient.RedeemAnswer, error) {
steps = append(steps, "redeem") steps = append(steps, "redeem")
return good, nil return good, nil
}, },
Open: func(delivery.RedeemAnswer) (string, error) { steps = append(steps, "open"); return "pw", nil }, Open: func(meshnodeenrollerclient.RedeemAnswer) (string, error) {
Keep: func(a delivery.RedeemAnswer, pw string) error { steps = append(steps, "open")
return "pw", nil
},
Keep: func(a meshnodeenrollerclient.RedeemAnswer, pw string) error {
steps = append(steps, "keep "+a.Login+" "+pw) steps = append(steps, "keep "+a.Login+" "+pw)
return nil return nil
}, },
Dial: func(_ context.Context, l, pw string) (Confirming, error) { Dial: func(_ context.Context, l, pw string) (Confirming, error) {
steps = append(steps, "dial "+l+" "+pw) 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 { if _, err := j.Run(ctx); err != nil {
t.Fatal(err) t.Fatal(err)
+2 -2
View File
@@ -7,10 +7,10 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
"time" "time"
"git.novox.be/novox/mesh-sdk/go/declaration" "git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
) )
@@ -114,7 +114,7 @@ func (b OverNATS) GrantsObject(ctx context.Context, name string, limit int64) ([
} }
js = bounded 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. // 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 ( import (
"errors" "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" "strings"
"sync" "sync"
"github.com/nats-io/nats.go" "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, // 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 l.values[login] = password
} }
if first != "" { if first != "" {
if _, ok := l.values[delivery.FirstLogin(node)]; !ok { if _, ok := l.values[meshvaultclient.FirstLogin(node)]; !ok {
l.values[delivery.FirstLogin(node)] = first l.values[meshvaultclient.FirstLogin(node)] = first
} }
} }
if l.current == "" { if l.current == "" {
l.current = delivery.FirstLogin(node) l.current = meshvaultclient.FirstLogin(node)
} }
return l return l
} }
@@ -71,7 +71,7 @@ func (l *Logins) Value(login string) (string, bool) {
func (l *Logins) Hold(login, password string) error { func (l *Logins) Hold(login, password string) error {
l.mu.Lock() l.mu.Lock()
defer l.mu.Unlock() 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} return &loginError{login: login, node: l.node}
} }
l.values[login] = password 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 // 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 // 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. // 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 == "" { if nl.Node != l.node || nl.Login == "" {
return "" 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. // 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 return nl.Rotating != "" && nl.Rotating == current
} }
+4 -5
View File
@@ -1,10 +1,9 @@
package link package link
import ( import (
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
"strings" "strings"
"testing" "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> // 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" { if u, p := (Membership{Node: "ace", Password: "plain"}).Credential(); u != "node.ace" || p != "plain" {
t.Fatalf("no logins at all: %s %s", u, p) 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()) 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()) t.Errorf("a login held no value for: %q, %s", said, m.User())
} }
if err := l.Hold("node.ace--b", "second-secret"); err != nil { 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 { if err := l.Hold("node.g14", "x"); err == nil {
t.Error("another node's login was kept") 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) t.Errorf("said %q", said)
} }
if u, p := m.Credential(); u != "node.ace--b" || p != "second-secret" || saved.Current != "node.ace--b" { 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" "context"
"fmt" "fmt"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "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" "sync"
"time" "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 // 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. // 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() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
if r.fallen != nil && nl.Since.String() != r.fellBackSince { 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), // 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. // only now that the vault says so.
r.old, r.failures = "", 0 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 _, held := r.Logins.Value(other); held {
if err := r.Logins.Drop(other); err != nil { if err := r.Logins.Drop(other); err != nil {
r.say("the old login %s's value is not dropped: %v", other, err) 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 { if err := r.Logins.Drop(cur); err != nil {
r.say("the abandoned login %s's value is not dropped: %v", cur, err) 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() { if r.fellBackSince != "" && r.fellBackSince == nl.Since.String() {
return // fallen back for this rotation: not tried again until the vault begins another return // fallen back for this rotation: not tried again until the vault begins another
} }
r.switchTo(ctx, nl) 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) r.confirm(ctx, nl.Rotating)
} }
} }
// switchTo fetches the new value, test-dials it and, only if it authenticates, switches and confirms (§3). // 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 next := nl.Rotating
password, held := r.Logins.Value(next) password, held := r.Logins.Value(next)
if !held { if !held {
@@ -158,7 +159,7 @@ func (r *Rotator) AuthFailed(login string) {
return 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) 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 now := time.Now
if r.Now != nil { if r.Now != nil {
now = r.Now 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 ( import (
"context" "context"
"errors" "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" "strings"
"testing" "testing"
"time" "time"
natsserver "github.com/nats-io/nats-server/v2/server" natsserver "github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go" "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 // 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() ctx := context.Background()
since := time.Date(2026, 11, 2, 12, 0, 0, 0, time.UTC) since := time.Date(2026, 11, 2, 12, 0, 0, 0, time.UTC)
key := func(login, rotating, stage string) delivery.NodeLogin { key := func(login, rotating, stage string) meshvault.NodeLoginsByNodeValue {
return delivery.NodeLogin{Node: "ace", Login: login, Rotating: rotating, Stage: stage, Since: since} 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. // A whole rotation: begun, the list holds both logins, ready, the switch and the confirmation, the old login retired, done.
srv, reload := start(t) srv, reload := start(t)
r, l, confirmed := build(t, srv) r, l, confirmed := build(t, srv)
oneAuthenticates(t, srv, "before the rotation") 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") oneAuthenticates(t, srv, "begun")
reload(first, second) reload(first, second)
oneAuthenticates(t, srv, "the list holds both") 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 { if cur, _ := l.Current(); cur != second {
t.Fatalf("the engine did not switch once the list held both logins: %s", cur) 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. // With the list never loaded the engine never switches: the test-dial is refused.
srv, _ = start(t) srv, _ = start(t)
r, l, _ = build(t, srv) 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) { if cur, _ := l.Current(); cur != first || !authenticates(srv, first) {
t.Errorf("the list was never loaded and the engine is on %s", cur) 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) srv, reload = start(t)
r, l, _ = build(t, srv) r, l, _ = build(t, srv)
reload(first, second) reload(first, second)
r.Heard(ctx, key(first, second, delivery.RotationReady)) r.Heard(ctx, key(first, second, meshvaultclient.RotationReady))
reload(first) reload(first)
r.Heard(ctx, key(first, "", "")) r.Heard(ctx, key(first, "", ""))
if cur, _ := l.Current(); cur != first || !authenticates(srv, first) { if cur, _ := l.Current(); cur != first || !authenticates(srv, first) {
+12 -12
View File
@@ -5,12 +5,12 @@ import (
"errors" "errors"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "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" "slices"
"strings" "strings"
"testing" "testing"
"time" "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. // 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 return g
} }
func key(login, rotating, stage string) delivery.NodeLogin { func key(login, rotating, stage string) meshvault.NodeLoginsByNodeValue {
return delivery.NodeLogin{Node: "ace", Login: login, Rotating: rotating, Stage: stage, Since: time.Date(2026, 11, 2, 12, 0, 0, 0, time.UTC)} 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 // 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) { func TestTheEngineSwitchesOnlyAfterATestDialAndConfirmsAsTheNewLogin(t *testing.T) {
g := newRotRig() g := newRotRig()
ctx := context.Background() 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 { if len(g.steps) != 0 {
t.Fatalf("before ready: %v", g.steps) t.Fatalf("before ready: %v", g.steps)
} }
g.dialErr = errors.New("authorization violation") 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") || 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") { 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) t.Fatalf("a test-dial that failed: %s, said %v, steps %v", cur, g.said, g.steps)
} }
g.dialErr = nil 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() cur, pw := g.logins.Current()
if cur != "node.ace--b" || pw != "secret-of-node.ace--b" { if cur != "node.ace--b" || pw != "secret-of-node.ace--b" {
t.Fatalf("after a good test-dial: %s %s", cur, pw) t.Fatalf("after a good test-dial: %s %s", cur, pw)
@@ -99,9 +99,9 @@ func TestTheOldValueGoesAtRotationDoneAbandonSwitchesBackAndFailuresFallBack(t *
ctx := context.Background() ctx := context.Background()
for _, stage := range []string{"before confirming", "after confirming"} { for _, stage := range []string{"before confirming", "after confirming"} {
g := newRotRig() 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" { 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 { if _, held := g.logins.Value("node.ace"); !held {
t.Fatalf("%s: the old value was dropped before rotation-done", stage) t.Fatalf("%s: the old value was dropped before rotation-done", stage)
@@ -121,7 +121,7 @@ func TestTheOldValueGoesAtRotationDoneAbandonSwitchesBackAndFailuresFallBack(t *
} }
} }
g := newRotRig() 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 g.r.Heard(ctx, key("node.ace--b", "", "")) // rotation-done: the vault retired the old login
if _, held := g.logins.Value("node.ace"); held { if _, held := g.logins.Value("node.ace"); held {
t.Error("the old value is kept after rotation-done") 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. // Fallback: two failures with the new value (the bound) before rotation-done.
f := newRotRig() 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")) f.logins.dialed("node.ace--b", errors.New("nats: authorization violation"))
if cur, _ := f.logins.Current(); cur != "node.ace--b" { if cur, _ := f.logins.Current(); cur != "node.ace--b" {
t.Fatalf("fell back after one failure: %s", cur) 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) t.Errorf("the fall back is no health report's: %v", err)
} }
steps := len(f.steps) 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 { if len(f.steps) != steps {
t.Errorf("the rotation fallen back from was tried again: %v", 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" "fmt"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "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/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" "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-networking/modules/mesh-wireguard/go/overlay"
"git.novox.be/novox/mesh-sdk/go/catalogue" "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"
"github.com/novox/mesh-host/internal/compose/frombus" "github.com/novox/mesh-host/internal/compose/frombus"
"github.com/novox/mesh-host/internal/compose/resolution" "github.com/novox/mesh-host/internal/compose/resolution"
@@ -1360,7 +1360,7 @@ func (c *Composer) certifyIfWanted(ctx context.Context, r reader, p meshmoduleas
return return
} }
trusted := []string{rec.Authority, r.VaultAuthority(ctx)} 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 { ed25519.PublicKey(key), time.Now()) == nil {
return return
} }
+14 -15
View File
@@ -32,16 +32,15 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "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" engcomp "github.com/novox/mesh-host/internal/compositions"
"regexp" "regexp"
"sort" "sort"
"strings" "strings"
"sync" "sync"
"time" "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 // 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) 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 says what was written, in the module's have.
Report func(ctx context.Context, module string, w Written) 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 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 // 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). // 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 mod string
said map[string]bool said map[string]bool
// grants is the grants part's key as last heard (nil when none). // grants is the grants part's key as last heard (nil when none).
grants *delivery.GrantsUserList grants *grantsstate.GrantsUserList
grantsRev uint64 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 is why the grants part's key is not watched now; empty while it is, or where it need not be.
unwatched string unwatched string
@@ -124,7 +123,7 @@ func New(env Env) *Follower {
said: map[string]bool{}} said: map[string]bool{}}
if env.Grants != nil && env.Kept != nil { if env.Grants != nil && env.Kept != nil {
if raw, rev := env.Kept(); raw != 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 f.grants, f.grantsRev = &g, rev
} }
} }
@@ -180,7 +179,7 @@ func (f *Follower) Run(ctx context.Context) error {
return return
} }
opened = true opened = true
ch, err := f.env.Grants.Watch(ctx, delivery.GrantsUserListKey) ch, err := f.env.Grants.Watch(ctx, grantsstate.GrantsUserListKey)
if err != nil { if err != nil {
failed(err.Error()) failed(err.Error())
return 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") f.sayOnceLocked("the grants part's user-list key is deleted, so the list loaded is kept")
return return
} }
g, err := delivery.DecodeGrantsUserList(u.Value) g, err := grantsstate.DecodeGrantsUserList(u.Value)
if err != nil { if err != nil {
f.sayOnceLocked("the grants part's user-list key cannot be read, so the list loaded is kept: " + err.Error()) f.sayOnceLocked("the grants part's user-list key cannot be read, so the list loaded is kept: " + err.Error())
return return
@@ -312,7 +311,7 @@ func reportable(prev, w Written) bool {
// says why in the have. // says why in the have.
func (f *Follower) settleGrants(ctx context.Context, again bool) { func (f *Follower) settleGrants(ctx context.Context, again bool) {
f.mu.Lock() f.mu.Lock()
var g delivery.GrantsUserList var g grantsstate.GrantsUserList
heard := f.grants != nil heard := f.grants != nil
if heard { if heard {
g = *f.grants g = *f.grants
@@ -343,8 +342,8 @@ func (f *Follower) settleGrants(ctx context.Context, again bool) {
} }
f.mu.Unlock() f.mu.Unlock()
w := Written{Source: delivery.GrantsUserListSource, Revision: int64(rev), Wanted: g.Digest, At: f.env.Now().UTC(), w := Written{Source: grantsstate.GrantsUserListSource, Revision: int64(rev), Wanted: g.Digest, At: f.env.Now().UTC(),
Follows: delivery.FollowGrants, Key: f.key(), Unwatched: f.unwatchedNow()} Follows: grantsstate.FollowGrants, Key: f.key(), Unwatched: f.unwatchedNow()}
var err error var err error
var changed bool var changed bool
resource, rerr := mode0600(file.resource) 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") 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: case f.env.Fetch == nil || f.env.Open == nil:
err = fmt.Errorf("this engine cannot read or open the grants part's list") 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) 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 var body []byte
if err == nil { if err == nil {
var object []byte var object []byte
if object, err = f.env.Fetch(ctx, g.Object); err == nil { 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 { if err != nil {
err = fmt.Errorf("the grants part's list %s could not be had: %w", short(g.Digest), err) 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.mod, f.state = file.module, w
f.written = err == nil f.written = err == nil
f.mu.Unlock() f.mu.Unlock()
at := fmt.Sprintf("%s@%d", delivery.GrantsUserListSource, rev) at := fmt.Sprintf("%s@%d", grantsstate.GrantsUserListSource, rev)
if err != nil { if err != nil {
f.sayOnce(fmt.Sprintf("the bus's user list of %s could not be written: %v", at, err)) f.sayOnce(fmt.Sprintf("the bus's user list of %s could not be written: %v", at, err))
} else if changed { } else if changed {
+38 -39
View File
@@ -5,15 +5,14 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "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" engcomp "github.com/novox/mesh-host/internal/compositions"
"strings" "strings"
"sync" "sync"
"testing" "testing"
"time" "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" 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. // 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. // 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", {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}}, before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grants: []gput{{follow: grantsstate.FollowGrants, content: three}},
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants}, wantWrites: []string{three}, wantUsers: 4, wantSource: grantsstate.GrantsUserListSource, wantFollows: grantsstate.FollowGrants},
{name: "a bus module it runs by digest still names the file", {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}}, before: []put{{module: "nats", fp: fpOf("a"), content: two, byDigest: true}}, grants: []gput{{follow: grantsstate.FollowGrants, content: three}},
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants}, wantWrites: []string{three}, wantUsers: 4, wantSource: grantsstate.GrantsUserListSource, wantFollows: grantsstate.FollowGrants},
{name: "a newer composition's list is not written", {name: "a newer composition's list is not written",
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, before: []put{{module: "nats", fp: fpOf("a"), content: two}},
after: []put{{module: "nats", fp: fpOf("b"), content: five}}, after: []put{{module: "nats", fp: fpOf("b"), content: five}},
grants: []gput{{follow: delivery.FollowGrants, content: three}}, grants: []gput{{follow: grantsstate.FollowGrants, content: three}},
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants}, 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: ""}}, {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", {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", {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}}, 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", {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, 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", {name: "a refused watch keeps the key last heard",
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grantsRefused: true, wantUnwatched: true, before: []put{{module: "nats", fp: fpOf("a"), content: two}}, grantsRefused: true, wantUnwatched: true,
kept: &gput{follow: delivery.FollowGrants, content: three}, kept: &gput{follow: grantsstate.FollowGrants, content: three},
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants}, wantWrites: []string{three}, wantUsers: 4, wantSource: grantsstate.GrantsUserListSource, wantFollows: grantsstate.FollowGrants},
{name: "a grants key that turns unreadable keeps the loaded list", {name: "a grants key that turns unreadable keeps the loaded list",
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, before: []put{{module: "nats", fp: fpOf("a"), content: two}},
grants: []gput{{follow: delivery.FollowGrants, content: three}, {unreadable: true, after: true}}, grants: []gput{{follow: grantsstate.FollowGrants, content: three}, {unreadable: true, after: true}},
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants}, wantWrites: []string{three}, wantUsers: 4, wantSource: grantsstate.GrantsUserListSource, wantFollows: grantsstate.FollowGrants},
{name: "a grants key that is deleted keeps the loaded list", {name: "a grants key that is deleted keeps the loaded list",
before: []put{{module: "nats", fp: fpOf("a"), content: two}}, before: []put{{module: "nats", fp: fpOf("a"), content: two}},
grants: []gput{{follow: delivery.FollowGrants, content: three}, {deleted: true, after: true}}, grants: []gput{{follow: grantsstate.FollowGrants, content: three}, {deleted: true, after: true}},
wantWrites: []string{three}, wantUsers: 4, wantSource: delivery.GrantsUserListSource, wantFollows: delivery.FollowGrants}, 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", {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}}, 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: delivery.GrantsUserListSource}, 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}}, {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, grants: []gput{{follow: grantsstate.FollowGrants, content: three}}, failWrite: true,
wantWrites: []string{three}, wantUsers: 4, wantErr: "disk full", wantSource: delivery.GrantsUserListSource}, 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", {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}}, 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: delivery.GrantsUserListSource}, 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", {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}}, 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: delivery.GrantsUserListSource}, onDisk: two, wantErr: "sealed to another key", wantRefused: true, wantSource: grantsstate.GrantsUserListSource},
} { } {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
var mu sync.Mutex var mu sync.Mutex
@@ -194,7 +193,7 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) {
} }
for i, g := range gs { for i, g := range gs {
if g.unreadable || g.deleted { 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) grantsAfter = append(grantsAfter, u)
continue continue
} }
@@ -203,21 +202,21 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) {
other, _ := seal.GenerateKeyPair() other, _ := seal.GenerateKeyPair()
to = other.PublicKey to = other.PublicKey
} }
digest := delivery.UserListDigest([]byte(g.content)) digest := grantsstate.UserListDigest([]byte(g.content))
if g.badDigest { 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
sealed[delivery.UserListObjectName(digest)] = object sealed[grantsstate.UserListObjectName(digest)] = object
raw, err := delivery.EncodeGrantsUserList(delivery.GrantsUserList{Follow: g.follow, Digest: digest, raw, err := grantsstate.EncodeGrantsUserList(grantsstate.GrantsUserList{Follow: g.follow, Digest: digest,
Object: delivery.UserListObjectName(digest), SealedTo: delivery.SealingKeyID(to), Node: node, Revision: uint64(i + 1)}) Object: grantsstate.UserListObjectName(digest), SealedTo: grantsstate.SealingKeyID(to), Node: node, Revision: uint64(i + 1)})
if err != nil { if err != nil {
t.Fatal(err) 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 { switch {
case c.kept != nil && i == len(gs)-1: case c.kept != nil && i == len(gs)-1:
keptRaw = raw keptRaw = raw
@@ -296,7 +295,7 @@ func TestTheBusNodeWritesTheGrantsPartsUserListAlone(t *testing.T) {
} }
if len(grantsAfter) > 0 { if len(grantsAfter) > 0 {
for until := time.Now().Add(5 * time.Second); ; time.Sleep(5 * time.Millisecond) { 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 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) 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. // 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) { readErr := New(Env{Node: node, OnDisk: func(json.RawMessage) ([]byte, error) {
return nil, errors.New("permission denied") 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") { 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) t.Fatalf("an unreadable list on the machine was held as %v", err)
} }