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