mesh-enrolment: place before the vault is asked, read the grants pass whole, name the expired enrolment's issued event, carry the first tunnel
- redeem writes the node into placement before it asks the vault for the account and the certificate of the serving key presented (the vault reads the node's address from placement); a retry by the same keys still finishes (blocker 3). - loadedVia reads mesh-grants.user-list's pass as the part's whole Result, the list state under user_list; the fixture testdata/grants-pass.json was written by mesh-grants' own Result, not by hand (blocker 4). - enrolment-expired is delivery.EnrolmentExpiry with IssuedID, through its codec; a key that expired before its content was seen names no issued event, so nothing is said and its token is dropped at once (blocker 6). - issue builds the joining node's first tunnel from placement (the hub, its endpoint, the node's address) and the hub's record (its tunnel key) and passes it to enrolment-token; no hub, or a hub with no key or endpoint, is a finding of issue (ADR 0587 §3, insight of 2026-11-02). - The assigner takes an add-node only from mesh-enrolment's own bus user (stdio.Callers), never person.mesh-enrolment. - The watchman raises machine.<node>.node-account-fallback from a node's health report's fall back and clears it when the report no longer carries one (ADR 0583 §6, blocker 7). - mesh-enrolment's ten unit test functions are two table tests now (blocker 8): the repository is back at 816. (issue 774) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -262,10 +262,12 @@ func warrantOf(v any) (*delivery.AddNodeWarrant, error) {
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// moduleOfCaller is the module a caller is, as the runtime proves it: a module's bus user is `<node>.<module>`.
|
||||
// moduleOfCaller is the module a caller is, as the runtime proves it, for AddNodeRefusal: mesh-enrolment when the proven
|
||||
// caller is that module's (its bus user `<node>.mesh-enrolment`, never a person's `person.mesh-enrolment`), else the caller
|
||||
// as proven, which the refusal names.
|
||||
func moduleOfCaller(by string) string {
|
||||
if _, module, ok := strings.Cut(by, "."); ok {
|
||||
return module
|
||||
if (stdio.Callers{delivery.EnrolmentModule}).Allows(by) {
|
||||
return delivery.EnrolmentModule
|
||||
}
|
||||
return by
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ func TestTheSeatsVerbs(t *testing.T) {
|
||||
good := `{"node":"ace","address":"10.9.0.2","ask":"enrol-ace-1","warrant":"enrol-ace-1@2026-10-28T20:00:00Z"}`
|
||||
for _, c := range []struct{ by, warrant, want string }{
|
||||
{"novox.mesh-grants", good, "mesh-enrolment's alone"},
|
||||
{"person.mesh-enrolment", good, "mesh-enrolment's alone"},
|
||||
{"novox.mesh-enrolment", `{"node":"g14","address":"10.9.0.2","ask":"a","warrant":"a@t"}`, "carries the warrant of g14"},
|
||||
{"novox.mesh-enrolment", `{"node":"ace","address":"10.9.0.9","ask":"a","warrant":"a@t"}`, "carries the warrant of ace at 10.9.0.9"},
|
||||
{"novox.mesh-enrolment", "", "carries no warrant"},
|
||||
|
||||
@@ -246,7 +246,7 @@ func TestTheWatchmansFindings(t *testing.T) {
|
||||
w.Provision("ace.web.oidc", enc(provisions.Waits, "x"), Put)
|
||||
w.Provision("ace.web.oidc", nil, Delete)
|
||||
}, map[string]string{}},
|
||||
{"774: a node's own report: its network, the resolver file rewritten, failed units, a relogin, a directory used as found", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) {
|
||||
{"774: a node's own report: its network, the resolver file rewritten, failed units, a relogin, a directory used as found, a login fallen back", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) {
|
||||
report := func(h delivery.NodeHealth) []byte {
|
||||
b, err := delivery.EncodeNodeHealth(h)
|
||||
if err != nil {
|
||||
@@ -259,11 +259,13 @@ func TestTheWatchmansFindings(t *testing.T) {
|
||||
{Part: "resolv-conf", State: "unhealthy", Writer: "NetworkManager", Owner: "systemd-resolved", Said: "rewritten"},
|
||||
{Part: "names", State: "unhealthy", Reason: "names do not resolve.", Said: "x"},
|
||||
{Part: "route", State: "unhealthy", Reason: "no default route", Said: "y"}}},
|
||||
Units: &delivery.UnitsHealth{State: "degraded", Failed: []delivery.FailedUnit{{Unit: "cups.service", Scope: "system", Result: "exit-code"}}},
|
||||
Relogin: []delivery.NodeAccount{{Account: "jochen", Module: "docker", Reason: "a group"}},
|
||||
Found: []delivery.NodeDirectory{{Module: "photos", Resource: "data", Path: "/srv/photos", Reason: "owner differs"}}}), Put)
|
||||
Units: &delivery.UnitsHealth{State: "degraded", Failed: []delivery.FailedUnit{{Unit: "cups.service", Scope: "system", Result: "exit-code"}}},
|
||||
Relogin: []delivery.NodeAccount{{Account: "jochen", Module: "docker", Reason: "a group"}},
|
||||
Found: []delivery.NodeDirectory{{Module: "photos", Resource: "data", Path: "/srv/photos", Reason: "owner differs"}},
|
||||
Fallback: &delivery.LoginFallback{Node: "novox", From: "node.novox--b", Why: "refused twice", At: time.Date(2026, 11, 2, 12, 0, 0, 0, time.UTC)}}), Put)
|
||||
for key, sev := range map[string]string{"machine.novox.network": Urgent, "machine.novox.systemd-resolved.rewritten": Urgent,
|
||||
"machine.novox.units": Warning, "module.docker.novox.relogin-needed": Warning, "module.photos.novox.directory-used-as-found": Warning} {
|
||||
"machine.novox.units": Warning, "module.docker.novox.relogin-needed": Warning, "module.photos.novox.directory-used-as-found": Warning,
|
||||
"machine.novox.node-account-fallback": Warning} {
|
||||
if o.findings[key].Severity != sev {
|
||||
t.Errorf("%s = %+v", key, o.findings[key])
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ import (
|
||||
// said once there, never while its own network or its silence says it;
|
||||
// - machine-units `machine.<node>.units`: failed units no module places;
|
||||
// - relogin-needed `module.<module>.<node>.relogin-needed` and directory-used-as-found
|
||||
// `module.<module>.<node>.directory-used-as-found`.
|
||||
// `module.<module>.<node>.directory-used-as-found`;
|
||||
// - node-account-fallback `machine.<node>.node-account-fallback`: the engine fell back to its node's old login during a
|
||||
// rotation (novox/hq ADR 0583 §6), raised while its report carries the fall back and cleared once it no longer does.
|
||||
//
|
||||
// A failing part that points at other mesh machines (NetworkPart.Targets: the hub's node, each resolver's node) is held
|
||||
// there, as the controller holds it: when every machine it points at is down on the record (silent, its own network
|
||||
@@ -36,6 +38,7 @@ const (
|
||||
KindMachineUnits = "machine-units"
|
||||
KindReloginNeeded = "relogin-needed"
|
||||
KindUsedAsFound = "directory-used-as-found"
|
||||
KindAccountFallback = delivery.NodeAccountFallbackFinding
|
||||
)
|
||||
|
||||
// The parts and states a report names (mesh-control internal/link).
|
||||
@@ -101,6 +104,10 @@ func (w *Watchman) nodeFindings(node string) {
|
||||
want[f.Key()] = true
|
||||
w.find(f)
|
||||
}
|
||||
if f, raise := fallbackOf(node, h.Fallback); raise {
|
||||
want[f.Key()] = true
|
||||
w.find(f)
|
||||
}
|
||||
}
|
||||
for key := range w.findings {
|
||||
if !want[key] && w.ownsHealthKey(key, node) {
|
||||
@@ -249,6 +256,19 @@ func foundOf(node string, dirs []delivery.NodeDirectory) []Finding {
|
||||
return out
|
||||
}
|
||||
|
||||
// fallbackOf is a node's fall back to its old login during a rotation of its account (ADR 0583 §6), one finding.
|
||||
func fallbackOf(node string, f *delivery.LoginFallback) (Finding, bool) {
|
||||
if f == nil || f.Node != node {
|
||||
return Finding{}, false
|
||||
}
|
||||
said := fmt.Sprintf("%s fell back from its new login %s to its old one during a rotation of its bus account: %s", node, f.From, f.Why)
|
||||
return Finding{Scope: ScopeMachine, ID: node, Token: KindAccountFallback, Kind: KindAccountFallback, Machine: node, Severity: Warning,
|
||||
Source: "health-report", Since: f.At, Summary: said, Said: said,
|
||||
Headline: fmt.Sprintf("%s fell back to its old bus login", node),
|
||||
Needs: fmt.Sprintf("abandon the rotation of %s's account, or rotate it again once its new login authenticates.", node),
|
||||
Resolved: fmt.Sprintf("%s no longer falls back to its old bus login", node)}, true
|
||||
}
|
||||
|
||||
func orModule(m string) string {
|
||||
if m == "" {
|
||||
return "_"
|
||||
|
||||
@@ -4,7 +4,7 @@ import "testing"
|
||||
|
||||
// 774/S57/AC1: a confirm from any caller but node.<node> is refused; from node.<node> node-enrolled is said and both keys
|
||||
// are deleted; a stop between the event and the deletes is finished by calling confirm again.
|
||||
func TestConfirmIsTheNodesOwnAndEndsTheEnrolment(t *testing.T) {
|
||||
func confirmIsTheNodesOwnAndEndsTheEnrolment(t *testing.T) {
|
||||
for _, c := range []struct{ name, caller, node, want string }{
|
||||
{"the enrolment user", "enrol.ace", "", "dialled as node.<node>"},
|
||||
{"another node", "node.g14", "ace", "its own enrolment only"},
|
||||
|
||||
@@ -82,27 +82,27 @@ func (h *Holder) Heard(op, key string, value []byte) error {
|
||||
case stdio.OpExpire:
|
||||
e, known := h.seen[node]
|
||||
delete(h.seen, node)
|
||||
id := "enrolment-expired." + node
|
||||
if known {
|
||||
id = eventID(EventExpired, e)
|
||||
if !known {
|
||||
// The key expired before the watch handed its content over (a holder started after it was written, before its
|
||||
// catch-up): the enrolment-issued event cannot be named, and an expiry naming none is no expiry (the contract
|
||||
// refuses it). Nothing is said; the token is dropped at once, as an ending event would have it dropped.
|
||||
h.logf("the enrolment of %s expired before its content was seen, so enrolment-expired cannot name its issued event and is not said; its token is dropped", node)
|
||||
if err := h.Vault.Drop(node); err != nil {
|
||||
return fmt.Errorf("the token of %s, whose enrolment expired unseen, is not dropped (offered again): %w", node, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// The sequence of the enrolment-issued event on the events' stream is not given to a bundle that publishes (the runtime
|
||||
// answers no sequence), so issued-event is 0 and issued-id names the message id that carried the content.
|
||||
body := Expired{EnrolmentExpiry: delivery.EnrolmentExpiry{V: delivery.Version, Node: node}, IssuedID: eventID(EventIssued, e)}
|
||||
if err := h.say(EventExpired, body, id); err != nil {
|
||||
body, err := delivery.EncodeEnrolmentExpiry(delivery.EnrolmentExpiry{Node: node, IssuedID: eventID(EventIssued, e)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.say(EventExpired, json.RawMessage(body), eventID(EventExpired, e)); err != nil {
|
||||
return fmt.Errorf("enrolment-expired of %s cannot be said (offered again): %w", node, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Expired is what enrolment-expired carries (delivery.EnrolmentExpiry) and the message id of the enrolment-issued event that
|
||||
// carried the key's content, which the bus's sequence number cannot be known for.
|
||||
type Expired struct {
|
||||
delivery.EnrolmentExpiry
|
||||
IssuedID string `json:"issued-id,omitempty"`
|
||||
}
|
||||
|
||||
// Ended is the durable consumer's act on one of the holder's own ending events (ADR 0587 §5): the vault is asked to drop the
|
||||
// node's token and its hash, and the event is acknowledged only when the vault answers that both are gone, so a drop that
|
||||
// fails is delivered again by the bus. It never runs before an end: the consumer hears only the end events.
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -12,7 +13,7 @@ import (
|
||||
// 774/S58/AC1: a revoke at redeeming, whether the vault minted the account yet or not, takes both keys away, the vault retires
|
||||
// the account, the revoke is recorded and enrolment-revoked is said; one the vault failed to retire ends nothing and is
|
||||
// finished by calling it again. At asked it withdraws the ask; an expiry marker says enrolment-expired with the issued event.
|
||||
func TestRevokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) {
|
||||
func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) {
|
||||
r := newRig(t)
|
||||
r.seed("ace", "10.9.0.2", StageRedeeming)
|
||||
r.vault.fail["retire ace"] = true
|
||||
@@ -42,15 +43,20 @@ func TestRevokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
last := r.said[len(r.said)-1]
|
||||
mustContain(t, string(last.Body), `"issued-id":"enrolment-issued.novox.`)
|
||||
if last.Event != EventExpired {
|
||||
t.Errorf("said %s", last.Event)
|
||||
x, err := delivery.DecodeEnrolmentExpiry(last.Body)
|
||||
if err != nil || last.Event != EventExpired || x.Node != "novox" || !strings.HasPrefix(x.IssuedID, "enrolment-issued.novox.") {
|
||||
t.Errorf("said %s %s: %+v, %v", last.Event, last.Body, x, err)
|
||||
}
|
||||
// A key that expired before its content was seen names no issued event: nothing is said, and the token is dropped.
|
||||
said := len(r.said)
|
||||
if err := r.h.Heard(stdio.OpExpire, NodeKey("unseen"), nil); err != nil || len(r.said) != said || !slices.Contains(r.vault.calls, "drop unseen") {
|
||||
t.Errorf("an unseen expiry: %v, said %v, vault %v", err, r.events(), r.vault.calls)
|
||||
}
|
||||
}
|
||||
|
||||
// 774/S58/AC2 and AC3: the drop is asked of the vault only from an ending event, acknowledged only once the vault answers, and
|
||||
// asked again when its first answer fails; and neither an event nor the bucket ever holds the token, sealed or not.
|
||||
func TestTheDropFollowsOnlyAnEndAndNothingHoldsTheToken(t *testing.T) {
|
||||
func theDropFollowsOnlyAnEndAndNothingHoldsTheToken(t *testing.T) {
|
||||
r := newRig(t)
|
||||
a, _ := r.h.Issue("person.operator", "ace", "a new machine", 0)
|
||||
_ = r.h.Warranted(yes, askWarrant(a.Ask))
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The module's unit tests, as two tables (the repository's test budget): the operator's side (issue, the tokens verb, the
|
||||
// manifest, the read of the bus node's loaded list) and the joining node's side (redeem, confirm and every end).
|
||||
|
||||
// TestIssueTokensAndTheManifest is 774/S54 and S55, and the read of whether the bus node loaded a user.
|
||||
func TestIssueTokensAndTheManifest(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
run func(*testing.T)
|
||||
}{
|
||||
{"S55/AC1 issue refuses naming every finding", issueRefusesNamingEveryFinding},
|
||||
{"S55/AC2 two issues at once hold two addresses and a yes makes the token", twoIssuesAtOnceHoldTwoAddressesAndAYesMakesTheToken},
|
||||
{"S55/AC3 a declined or expired ask takes both keys; a vault failure keeps the enrolment for again", aDeclinedOrExpiredAskTakesBothKeysAndAVaultFailureKeepsTheEnrolmentForAgain},
|
||||
{"S54/AC1 tokens answers each stage and relays the sealed token of one node", tokensAnswersEachStageAndRelaysTheSealedTokenOfOneNode},
|
||||
{"S54/AC2 the manifest says what the code does", theManifestSaysWhatTheCodeDoes},
|
||||
{"S56 the bus node's loaded list is read from the grants part's pass", theLoadedListIsReadFromTheGrantsPass},
|
||||
} {
|
||||
t.Run(c.name, c.run)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRedeemConfirmAndEveryEnd is 774/S56, S57 and S58.
|
||||
func TestRedeemConfirmAndEveryEnd(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
run func(*testing.T)
|
||||
}{
|
||||
{"S56/AC1 redeem refuses what it must and spends the token once", redeemRefusesWhatItMustAndSpendsTheTokenOnce},
|
||||
{"S56/AC2 a redeem stopped midway is finished by the same keys again", aRedeemStoppedMidwayIsFinishedByTheSameKeysAgain},
|
||||
{"S57/AC1 confirm is the node's own and ends the enrolment", confirmIsTheNodesOwnAndEndsTheEnrolment},
|
||||
{"S58/AC1-AC2 revoke ends at any stage and an expiry marker says expired", revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired},
|
||||
{"S58/AC3 the drop follows only an end and nothing holds the token", theDropFollowsOnlyAnEndAndNothingHoldsTheToken},
|
||||
} {
|
||||
t.Run(c.name, c.run)
|
||||
}
|
||||
}
|
||||
|
||||
// The key pass of mesh-grants.user-list holds the part's whole pass result; testdata/grants-pass.json was written by
|
||||
// mesh-grants' own Result (json.Marshal of grants.Result with CredentialsOf), not written by hand: node.ace is in the shadow
|
||||
// list and loaded, node.g14 in the shadow list and not loaded yet.
|
||||
func theLoadedListIsReadFromTheGrantsPass(t *testing.T) {
|
||||
raw, err := os.ReadFile("testdata/grants-pass.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loaded := loadedVia(memRead{"mesh-grants.user-list": {"pass": string(raw)}})
|
||||
for user, want := range map[string]bool{"node.ace": true, "node.g14": false, "node.none": false} {
|
||||
if got, err := loaded(user); err != nil || got != want {
|
||||
t.Errorf("%s loaded = %v, %v; want %v", user, got, err, want)
|
||||
}
|
||||
}
|
||||
if _, err := loadedVia(memRead{"mesh-grants.user-list": {}})("node.ace"); err == nil {
|
||||
t.Error("no pass composed read as an answer")
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,8 @@ import (
|
||||
type Vault interface {
|
||||
// Token asks the vault to make the token and its hash for node, expiring at expires, and answers what the joining
|
||||
// machine must trust (the bus's address and fingerprint, the vault's signing key). No token is in the answer.
|
||||
Token(node string, expires time.Time) (delivery.EnrolmentTokenAnswer, error)
|
||||
// The token carries tunnel, the node's first tunnel built at issue (ADR 0587 §3, insight of 2026-11-02).
|
||||
Token(node string, expires time.Time, tunnel delivery.TokenTunnel) (delivery.EnrolmentTokenAnswer, error)
|
||||
// Sealed asks the vault for node's token sealed to the operator key, as the vault sealed it.
|
||||
Sealed(node string) (string, error)
|
||||
// Drop asks the vault to drop node's token and its hash; it answers only once both are gone.
|
||||
@@ -159,6 +160,46 @@ func (h *Holder) placements() (map[string]delivery.Placement, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// firstTunnel is the joining node's first tunnel as the token carries it (ADR 0587 §3, insight of 2026-11-02; as the
|
||||
// controller's `token issue` builds it): the hub's tunnel key from its record (the key its tunnel holds; on an adopted hub
|
||||
// that has not written one, the found tunnel's key kept in its placement), the hub's endpoint from its placement, and the
|
||||
// node's address on the private network. A mesh with no hub, or a hub with no key or no endpoint, has no tunnel to join
|
||||
// through, and that is said.
|
||||
func (h *Holder) firstTunnel(placements map[string]delivery.Placement, address string) (delivery.TokenTunnel, error) {
|
||||
var hub *delivery.Placement
|
||||
for _, p := range placements {
|
||||
if p.Decided.Hub {
|
||||
p := p
|
||||
hub = &p
|
||||
}
|
||||
}
|
||||
if hub == nil {
|
||||
return delivery.TokenTunnel{}, errors.New("placement names no hub, so there is no tunnel to join through")
|
||||
}
|
||||
key := ""
|
||||
if k, err := delivery.HaveNodeKey(hub.Node); err == nil {
|
||||
if raw, found, err := h.Read.Get("node-engine.have", k); err == nil && found {
|
||||
if hn, err := delivery.DecodeHaveNode(raw); err == nil && hn.Record != nil {
|
||||
key = hn.Record.TunnelKey
|
||||
}
|
||||
}
|
||||
}
|
||||
if key == "" && hub.Decided.Tunnel != nil {
|
||||
key = hub.Decided.Tunnel.PublicKey
|
||||
}
|
||||
var missing []string
|
||||
if key == "" {
|
||||
missing = append(missing, "no tunnel key in its record")
|
||||
}
|
||||
if hub.Decided.Endpoint == "" {
|
||||
missing = append(missing, "no endpoint in its placement")
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
return delivery.TokenTunnel{}, fmt.Errorf("the hub %s has %s, so there is no tunnel to join through", hub.Node, strings.Join(missing, " and "))
|
||||
}
|
||||
return delivery.TokenTunnel{HubKey: key, HubEndpoint: hub.Decided.Endpoint, Address: address + "/32"}, nil
|
||||
}
|
||||
|
||||
// hasRecord says the node already writes a record (a node of the mesh, ADR 0587 §4: "a node record ... holds it").
|
||||
func (h *Holder) hasRecord(node string) bool {
|
||||
keys, err := h.Read.Keys("node-engine.have")
|
||||
|
||||
@@ -143,7 +143,7 @@ type standInVault struct {
|
||||
ctx context.Context
|
||||
logins jetstream.KeyValue
|
||||
mu sync.Mutex
|
||||
tokens map[string]bool
|
||||
tokens map[string]delivery.Token
|
||||
calls []string
|
||||
}
|
||||
|
||||
@@ -153,9 +153,10 @@ func (v *standInVault) note(c string) {
|
||||
v.mu.Unlock()
|
||||
}
|
||||
|
||||
func (v *standInVault) Token(node string, expires time.Time) (delivery.EnrolmentTokenAnswer, error) {
|
||||
// Token makes the token as the vault does: the bus it states, its signing key, a secret, and the first tunnel it is handed.
|
||||
func (v *standInVault) Token(node string, expires time.Time, tunnel delivery.TokenTunnel) (delivery.EnrolmentTokenAnswer, error) {
|
||||
v.mu.Lock()
|
||||
v.tokens[node] = true
|
||||
v.tokens[node] = delivery.Token{Version: 1, Node: node, Broker: "bus:4222", Fingerprint: "f", Signer: make([]byte, 32), Secret: "secret-" + node, Tunnel: &tunnel}
|
||||
v.mu.Unlock()
|
||||
v.note("token " + node)
|
||||
return delivery.EnrolmentTokenAnswer{Node: node, Broker: "bus:4222", Fingerprint: "f", Signer: "s"}, nil
|
||||
@@ -219,13 +220,16 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
}
|
||||
enrolments := mk(delivery.EnrolmentsBucket, true)
|
||||
logins, lease, placement := mk(delivery.NodeLoginsBucket, false), mk("mesh-controller_lease", false), mk("mesh-delivery-assigner_placement", false)
|
||||
mk("node-engine_have", false)
|
||||
have := mk("node-engine_have", false)
|
||||
if _, err := lease.Put(ctx, "holder", []byte(`{"handed-to":"mesh-grants","handed-by":"operator"}`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := placement.Put(ctx, "hub.node", []byte(hubPlacement)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := have.Put(ctx, "hub.node", []byte(hubRecord)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The seats' events are kept on a stream of the events, as the bus keeps them, and heard on their real subjects.
|
||||
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "SEAT_EVENTS",
|
||||
Subjects: []string{"mesh.seat.mesh-enrolment.event.>", "mesh.seat.mesh-placement.event.>"}}); err != nil {
|
||||
@@ -245,7 +249,7 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
events := func() []string { heardMu.Lock(); defer heardMu.Unlock(); return slices.Clone(heard) }
|
||||
|
||||
store := &busStore{ctx: ctx, js: js, kv: enrolments}
|
||||
vault := &standInVault{ctx: ctx, logins: logins, tokens: map[string]bool{}}
|
||||
vault := &standInVault{ctx: ctx, logins: logins, tokens: map[string]delivery.Token{}}
|
||||
asker := &fakeAsker{}
|
||||
h := &Holder{Store: store, Read: busReader{ctx, js}, Vault: vault, Asker: asker, AskLasts: time.Hour,
|
||||
Say: func(event string, body any, id string) error {
|
||||
@@ -334,6 +338,18 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitFor("the user enrol.ace is composed", func() bool { return hasUser("enrol.ace") })
|
||||
// The token carries the first tunnel built at issue from the hub's placement and record (ADR 0587 §3, insight of
|
||||
// 2026-11-02). Everything mesh-enrolment builds is in it; the joining machine's own tunnel key and the private network's
|
||||
// range are not, because the seat's enrolment-token takes hub_key, hub_endpoint and address alone (reported in round 1).
|
||||
vault.mu.Lock()
|
||||
tok := vault.tokens["ace"]
|
||||
vault.mu.Unlock()
|
||||
if tok.Tunnel == nil || tok.Tunnel.HubKey != "hub-tunnel-key" || tok.Tunnel.HubEndpoint != "192.0.2.1:51820" || tok.Tunnel.Address != a.Address+"/32" {
|
||||
t.Fatalf("the token's tunnel %+v", tok.Tunnel)
|
||||
}
|
||||
if missing := tok.Missing(); !slices.Equal(missing, []string{"the tunnel key it was issued for", "the private network's range"}) {
|
||||
t.Errorf("the token misses %v", missing)
|
||||
}
|
||||
args := redeemArgs("ace", 1, "serving-ace")
|
||||
if _, err := h.Redeem("enrol.somebody", args); err == nil {
|
||||
t.Fatal("redeemed by another caller")
|
||||
@@ -361,7 +377,8 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
waitFor("the vault is asked to drop the token after the end", func() bool {
|
||||
vault.mu.Lock()
|
||||
defer vault.mu.Unlock()
|
||||
return slices.Contains(vault.calls, "drop ace") && !vault.tokens["ace"]
|
||||
_, held := vault.tokens["ace"]
|
||||
return slices.Contains(vault.calls, "drop ace") && !held
|
||||
})
|
||||
if keys, _ := store.Keys(); len(keys) != 0 {
|
||||
t.Errorf("an ended enrolment leaves keys %v", keys)
|
||||
|
||||
@@ -20,8 +20,6 @@ type Event struct {
|
||||
Node string `json:"node"`
|
||||
Enrolment Enrolment `json:"enrolment"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
// Issued names the enrolment-issued event that carried the content (for enrolment-expired, whose key is gone).
|
||||
Issued string `json:"issued-event,omitempty"`
|
||||
}
|
||||
|
||||
// eventID is the message id an event of e is said under: the bus keeps one message of it, so an event said again after a
|
||||
@@ -86,6 +84,11 @@ func (h *Holder) Issue(caller, node, why string, lasts time.Duration) (IssueAnsw
|
||||
findings = append(findings, "an enrolment of "+node+" is open already: revoke it first")
|
||||
}
|
||||
}
|
||||
if perr == nil {
|
||||
if _, err := h.firstTunnel(placements, "0.0.0.0"); err != nil {
|
||||
findings = append(findings, err.Error())
|
||||
}
|
||||
}
|
||||
if len(findings) > 0 {
|
||||
return IssueAnswer{}, fmt.Errorf("no enrolment of %q is issued: %s", node, strings.Join(findings, "; "))
|
||||
}
|
||||
@@ -235,7 +238,15 @@ func (h *Holder) Warranted(o asks.Option, w asks.Warrant) error {
|
||||
return fmt.Errorf("the warrant %s is not recorded in the hand-act log, so no token is made (mesh-enrolment.again asks again): %w", w.Ask, err)
|
||||
}
|
||||
}
|
||||
if _, err := h.Vault.Token(e.Node, e.Expires); err != nil {
|
||||
placements, err := h.placements()
|
||||
if err != nil {
|
||||
return fmt.Errorf("placement cannot be read, so the token's tunnel is not built (mesh-enrolment.again asks again): %w", err)
|
||||
}
|
||||
tunnel, err := h.firstTunnel(placements, e.Address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the token's tunnel is not built (mesh-enrolment.again asks again): %w", err)
|
||||
}
|
||||
if _, err := h.Vault.Token(e.Node, e.Expires, tunnel); err != nil {
|
||||
return fmt.Errorf("the vault did not make the token of %s (mesh-enrolment.again asks the operator again): %w", e.Node, err)
|
||||
}
|
||||
e.Stage = StageLive
|
||||
|
||||
@@ -8,13 +8,14 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asker"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
var yes = asks.Option{ID: "yes"}
|
||||
|
||||
// 774/S55/AC1: issue refuses, naming every finding, what it must: the lease with the controller, a bad name, a name a node
|
||||
// record, a placement key or an open enrolment holds, and an expiry past a day.
|
||||
func TestIssueRefusesNamingEveryFinding(t *testing.T) {
|
||||
func issueRefusesNamingEveryFinding(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
setup func(r *rig)
|
||||
@@ -30,6 +31,14 @@ func TestIssueRefusesNamingEveryFinding(t *testing.T) {
|
||||
r.rd["node-engine.have"]["ace"] = `{}`
|
||||
}, "ace", 0, []string{"ace has a placement key already"}},
|
||||
{"an open enrolment", func(r *rig) { r.seed("ace", "10.9.0.2", StageLive) }, "ace", 0, []string{"an enrolment of ace is open already"}},
|
||||
// The token carries the first tunnel (ADR 0587 §3, insight of 2026-11-02): no hub, or a hub with no key or endpoint.
|
||||
{"no hub", func(r *rig) {
|
||||
r.rd["mesh-delivery-assigner.placement"]["hub.node"] = strings.Replace(hubPlacement, `"hub":true,`, "", 1)
|
||||
}, "ace", 0, []string{"placement names no hub"}},
|
||||
{"a hub with no key and no endpoint", func(r *rig) {
|
||||
r.rd["mesh-delivery-assigner.placement"]["hub.node"] = strings.Replace(hubPlacement, `"endpoint":"192.0.2.1:51820",`, "", 1)
|
||||
delete(r.rd["node-engine.have"], "hub.node")
|
||||
}, "ace", 0, []string{"the hub hub has no tunnel key in its record and no endpoint in its placement"}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
r := newRig(t)
|
||||
@@ -51,7 +60,7 @@ func TestIssueRefusesNamingEveryFinding(t *testing.T) {
|
||||
|
||||
// 774/S55/AC2: two issues at once for two nodes hold two addresses; a yes makes the token, moves both keys to live with the
|
||||
// token's expiry, records the warrant, says enrolment-issued, and a second answer changes nothing.
|
||||
func TestTwoIssuesAtOnceHoldTwoAddressesAndAYesMakesTheToken(t *testing.T) {
|
||||
func twoIssuesAtOnceHoldTwoAddressesAndAYesMakesTheToken(t *testing.T) {
|
||||
r := newRig(t)
|
||||
var wg sync.WaitGroup
|
||||
addrs := make([]string, 2)
|
||||
@@ -86,6 +95,9 @@ func TestTwoIssuesAtOnceHoldTwoAddressesAndAYesMakesTheToken(t *testing.T) {
|
||||
len(r.warr) != 1 || r.warr[0] != e.Ask || r.events()[0] != EventIssued {
|
||||
t.Errorf("live %+v, ttl %v/%v, warrants %v, events %v", live, r.st.ttl[NodeKey("ace")], r.st.ttl[AddressKey(live.Address)], r.warr, r.events())
|
||||
}
|
||||
if got, want := r.vault.tunnels["ace"], (delivery.TokenTunnel{HubKey: "hub-tunnel-key", HubEndpoint: "192.0.2.1:51820", Address: live.Address + "/32"}); got != want {
|
||||
t.Errorf("the token's tunnel %+v, want %+v", got, want)
|
||||
}
|
||||
if strings.Contains(string(r.said[0].Body), "secret") || r.said[0].ID != eventID(EventIssued, live) {
|
||||
t.Errorf("the event %s carries a secret-like word or no stable id", r.said[0].Body)
|
||||
}
|
||||
@@ -96,7 +108,7 @@ func TestTwoIssuesAtOnceHoldTwoAddressesAndAYesMakesTheToken(t *testing.T) {
|
||||
|
||||
// 774/S55/AC3: a declined or expired ask, heard, takes both keys away, and enrolment-refused carries the key's content; a
|
||||
// vault that fails leaves the enrolment at asked, for `again` to ask once more.
|
||||
func TestADeclinedOrExpiredAskTakesBothKeysAndAVaultFailureKeepsTheEnrolmentForAgain(t *testing.T) {
|
||||
func aDeclinedOrExpiredAskTakesBothKeysAndAVaultFailureKeepsTheEnrolmentForAgain(t *testing.T) {
|
||||
r := newRig(t)
|
||||
a, _ := r.h.Issue("person.operator", "ace", "a new machine", 0)
|
||||
if err := r.h.Warranted(asks.Option{ID: "no"}, asks.Warrant{Ask: a.Ask}); err != nil {
|
||||
|
||||
@@ -114,8 +114,9 @@ func (r memRead) Get(state, key string) ([]byte, bool, error) {
|
||||
|
||||
// fakeVault records its calls in order and fails on the verbs named in fail, once each.
|
||||
type fakeVault struct {
|
||||
calls []string
|
||||
fail map[string]bool
|
||||
calls []string
|
||||
fail map[string]bool
|
||||
tunnels map[string]delivery.TokenTunnel
|
||||
}
|
||||
|
||||
func (v *fakeVault) do(what string) error {
|
||||
@@ -127,7 +128,11 @@ func (v *fakeVault) do(what string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *fakeVault) Token(node string, expires time.Time) (delivery.EnrolmentTokenAnswer, error) {
|
||||
func (v *fakeVault) Token(node string, expires time.Time, tunnel delivery.TokenTunnel) (delivery.EnrolmentTokenAnswer, error) {
|
||||
if v.tunnels == nil {
|
||||
v.tunnels = map[string]delivery.TokenTunnel{}
|
||||
}
|
||||
v.tunnels[node] = tunnel
|
||||
return delivery.EnrolmentTokenAnswer{Node: node, Broker: "b", Fingerprint: "f", Signer: "s"}, v.do("token " + node)
|
||||
}
|
||||
func (v *fakeVault) Sealed(node string) (string, error) {
|
||||
@@ -191,14 +196,17 @@ type rig struct {
|
||||
placeErr error
|
||||
}
|
||||
|
||||
const hubPlacement = `{"v":1,"node":"hub","decided":{"hub":true,"address":"10.9.0.1","tunnel":{"interface":"wg0","address":"10.9.0.1/24","range":"10.9.0.0/24"}},"modules":[]}`
|
||||
// hubRecord is the hub's own record in node-engine.have: its tunnel key, which the token's tunnel names.
|
||||
const hubRecord = `{"v":1,"record":{"v":1,"node":"hub","tunnel-key":"hub-tunnel-key"}}`
|
||||
|
||||
const hubPlacement = `{"v":1,"node":"hub","decided":{"hub":true,"endpoint":"192.0.2.1:51820","address":"10.9.0.1","tunnel":{"interface":"wg0","address":"10.9.0.1/24","range":"10.9.0.0/24"}},"modules":[]}`
|
||||
|
||||
func newRig(t *testing.T) *rig {
|
||||
r := &rig{t: t, st: newMem(), vault: &fakeVault{fail: map[string]bool{}}, asker: &fakeAsker{}, now: time.Date(2026, 11, 2, 10, 0, 0, 0, time.UTC), loaded: true}
|
||||
r.rd = memRead{
|
||||
"mesh-controller.lease": {"holder": `{"handed-to":"mesh-grants","handed-by":"operator"}`},
|
||||
"mesh-delivery-assigner.placement": {"hub.node": hubPlacement},
|
||||
"node-engine.have": {},
|
||||
"node-engine.have": {"hub.node": hubRecord},
|
||||
}
|
||||
r.h = &Holder{Store: r.st, Read: r.rd, Vault: r.vault, Asker: r.asker, AskLasts: time.Hour,
|
||||
Say: func(event string, body any, id string) error {
|
||||
@@ -210,6 +218,7 @@ func newRig(t *testing.T) *rig {
|
||||
},
|
||||
AddNode: func(a AddNode) error {
|
||||
r.added = append(r.added, a)
|
||||
r.vault.calls = append(r.vault.calls, "place "+a.Change.Node)
|
||||
if r.placeErr == nil { // placement writes the node as the assigner does on the warrant
|
||||
n := a.Change.Node
|
||||
r.rd["mesh-delivery-assigner.placement"][n+".node"] = `{"v":1,"node":"` + n + `","decided":{"address":"` + a.Change.Decided.Address + `"},"modules":[]}`
|
||||
|
||||
@@ -137,21 +137,25 @@ func loadedVia(rd Reader) func(string) (bool, error) {
|
||||
if !found {
|
||||
return false, errors.New("the grants part has composed no list yet")
|
||||
}
|
||||
var st struct {
|
||||
Compare struct {
|
||||
Unread string `json:"unread"`
|
||||
} `json:"compare"`
|
||||
Credentials struct {
|
||||
Users []struct {
|
||||
User string `json:"user"`
|
||||
InShadow bool `json:"in_shadow"`
|
||||
Loaded bool `json:"loaded"`
|
||||
} `json:"users"`
|
||||
} `json:"credentials"`
|
||||
// The key holds the part's whole pass result (mesh-grants Result): the list state sits under user_list.
|
||||
var pass struct {
|
||||
UserList struct {
|
||||
Compare struct {
|
||||
Unread string `json:"unread"`
|
||||
} `json:"compare"`
|
||||
Credentials struct {
|
||||
Users []struct {
|
||||
User string `json:"user"`
|
||||
InShadow bool `json:"in_shadow"`
|
||||
Loaded bool `json:"loaded"`
|
||||
} `json:"users"`
|
||||
} `json:"credentials"`
|
||||
} `json:"user_list"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &st); err != nil {
|
||||
if err := json.Unmarshal(raw, &pass); err != nil {
|
||||
return false, fmt.Errorf("the grants part's list state does not read: %w", err)
|
||||
}
|
||||
st := pass.UserList
|
||||
if st.Compare.Unread != "" {
|
||||
return false, errors.New("the bus node's loaded list cannot be read: " + st.Compare.Unread)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ import (
|
||||
// Redeem is the joining node-engine's one call (ADR 0587 §5, §6). It answers only a caller the runtime proves to be
|
||||
// enrol.<node>, for a key at stage live (or at redeeming with the same keys' digest), checks the proof against the identity key
|
||||
// presented, and then, each step read back and none repeated that is done: spends the token (the key to redeeming, by
|
||||
// compare-and-set, with no expiry), asks the vault for the node's account and the certificate, writes the node into placement,
|
||||
// compare-and-set, with no expiry), writes the node into placement (the vault reads the node's address from it), asks the vault
|
||||
// for the node's account and the certificate of the serving key presented,
|
||||
// waits for the bus node to load node.<node>, and answers the whole, signed by the vault. Called again with the same keys it
|
||||
// finishes what is not done and answers the same.
|
||||
func (h *Holder) Redeem(caller string, a delivery.Redeem) (delivery.RedeemAnswer, error) {
|
||||
@@ -37,6 +38,9 @@ func (h *Holder) Redeem(caller string, a delivery.Redeem) (delivery.RedeemAnswer
|
||||
if err != nil {
|
||||
return delivery.RedeemAnswer{}, err
|
||||
}
|
||||
if err := h.place(e, a); err != nil {
|
||||
return delivery.RedeemAnswer{}, err
|
||||
}
|
||||
account, err := h.Vault.Account(e.Node, a.SealingKey, digest)
|
||||
if err != nil {
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("the vault did not give the account of %s (call redeem again with the same keys): %w", e.Node, err)
|
||||
@@ -45,9 +49,6 @@ func (h *Holder) Redeem(caller string, a delivery.Redeem) (delivery.RedeemAnswer
|
||||
if err != nil {
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("the vault did not sign the certificate of %s (call redeem again with the same keys): %w", e.Node, err)
|
||||
}
|
||||
if err := h.place(e, a); err != nil {
|
||||
return delivery.RedeemAnswer{}, err
|
||||
}
|
||||
if h.Loaded != nil {
|
||||
loaded, err := h.Loaded(NodeUser(e.Node))
|
||||
if err != nil {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -22,7 +23,7 @@ func redeemArgs(node string, seed byte, serving string) delivery.Redeem {
|
||||
// 774/S56/AC1: a redeem from another caller, with a bad proof, after the expiry, or with other keys than the first, is
|
||||
// refused (one row each), and a good one spends the token, writes the node into placement on the issue's warrant and
|
||||
// answers once the bus node loaded the account.
|
||||
func TestRedeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) {
|
||||
func redeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) {
|
||||
good := redeemArgs("ace", 1, "serving-a")
|
||||
bad := good
|
||||
raw, _ := good.ProofBytes()
|
||||
@@ -81,6 +82,10 @@ func TestRedeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) {
|
||||
r.added[0].Change.Keys.Digest() != good.KeysDigest() || r.added[0].Change.Decided.Address != "10.9.0.2" || ans.Signature == "" || ans.Account != "account-ace" || ans.Login != "node.ace" || ans.Signer != "vault-signer" || ans.Certificate != "PEM-serving-a" {
|
||||
t.Errorf("placement %+v, answer %+v", r.added, ans)
|
||||
}
|
||||
// Placement is written before the vault is asked: the vault reads the node's address from it.
|
||||
if place, account := slices.Index(r.vault.calls, "place ace"), slices.Index(r.vault.calls, "account ace"); place < 0 || account < place {
|
||||
t.Errorf("the vault was asked before placement held the node: %v", r.vault.calls)
|
||||
}
|
||||
r.loaded = false
|
||||
if _, err := r.h.Redeem("enrol.ace", good); err == nil {
|
||||
t.Error("answered before the bus node loaded the account")
|
||||
@@ -89,7 +94,7 @@ func TestRedeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) {
|
||||
|
||||
// 774/S56/AC2: a redeem stopped after the vault's call, or after the placement call, called again with the same keys,
|
||||
// finishes and answers the same, and the vault minted once (it is asked for the same account).
|
||||
func TestARedeemStoppedMidwayIsFinishedByTheSameKeysAgain(t *testing.T) {
|
||||
func aRedeemStoppedMidwayIsFinishedByTheSameKeysAgain(t *testing.T) {
|
||||
good := redeemArgs("ace", 1, "serving-a")
|
||||
whole := newRig(t)
|
||||
whole.seed("ace", "10.9.0.2", StageLive)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"user_list": {
|
||||
"v": 1,
|
||||
"mode": "shadow",
|
||||
"inputs": "",
|
||||
"digest": "",
|
||||
"users": 0,
|
||||
"left_out": null,
|
||||
"compare": {
|
||||
"shadow": 0,
|
||||
"loaded": 0,
|
||||
"added": null,
|
||||
"dropped": null,
|
||||
"differ": null,
|
||||
"same": 0
|
||||
},
|
||||
"credentials": {
|
||||
"users": [
|
||||
{
|
||||
"user": "node.ace",
|
||||
"in_shadow": true,
|
||||
"loaded": true,
|
||||
"hash_held": true,
|
||||
"generation": 1,
|
||||
"would": "same"
|
||||
},
|
||||
{
|
||||
"user": "node.g14",
|
||||
"in_shadow": true,
|
||||
"loaded": false,
|
||||
"hash_held": true,
|
||||
"generation": 1,
|
||||
"would": "adds"
|
||||
}
|
||||
],
|
||||
"hashes_held": 2,
|
||||
"left_out": [],
|
||||
"loses_login": [],
|
||||
"adds": [
|
||||
"node.g14"
|
||||
]
|
||||
},
|
||||
"gaps": null,
|
||||
"at": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"revision": 0,
|
||||
"written": false,
|
||||
"memberships": 0,
|
||||
"memberships_written": 0,
|
||||
"assertions": 0,
|
||||
"assertions_written": 0,
|
||||
"published_or_asserted": ""
|
||||
}
|
||||
@@ -24,7 +24,7 @@ func (r *rig) seed(node, address string, stage string) {
|
||||
|
||||
// 774/S54/AC1 and AC2: tokens answers each stage with its expiry, ask and revision, the fill and whether issuing is on;
|
||||
// for one node at live it relays the vault's sealed token and keeps nothing; and the manifest says what the code does.
|
||||
func TestTokensAnswersEachStageAndRelaysTheSealedTokenOfOneNode(t *testing.T) {
|
||||
func tokensAnswersEachStageAndRelaysTheSealedTokenOfOneNode(t *testing.T) {
|
||||
r := newRig(t)
|
||||
r.seed("ace", "10.9.0.2", StageAsked)
|
||||
r.seed("g14", "10.9.0.3", StageLive)
|
||||
@@ -65,7 +65,7 @@ func TestTokensAnswersEachStageAndRelaysTheSealedTokenOfOneNode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
|
||||
func theManifestSaysWhatTheCodeDoes(t *testing.T) {
|
||||
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -39,8 +39,11 @@ func answer[T any](raw json.RawMessage, err error) (T, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (v vaultOver) Token(node string, expires time.Time) (delivery.EnrolmentTokenAnswer, error) {
|
||||
return answer[delivery.EnrolmentTokenAnswer](v.call(vaultToken, map[string]any{"node": node, "expires": expires.UTC().Format(time.RFC3339)}))
|
||||
func (v vaultOver) Token(node string, expires time.Time, t delivery.TokenTunnel) (delivery.EnrolmentTokenAnswer, error) {
|
||||
// The seat's tunnel takes these three fields and no other (catalogue's enrolment-token).
|
||||
tunnel := map[string]any{"hub_key": t.HubKey, "hub_endpoint": t.HubEndpoint, "address": t.Address}
|
||||
return answer[delivery.EnrolmentTokenAnswer](v.call(vaultToken, map[string]any{"node": node,
|
||||
"expires": expires.UTC().Format(time.RFC3339), "tunnel": tunnel}))
|
||||
}
|
||||
|
||||
func (v vaultOver) Sealed(node string) (string, error) {
|
||||
|
||||
Reference in New Issue
Block a user