diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/confirm.go b/modules/mesh-enrolment/cmd/mesh-enrolment/confirm.go new file mode 100644 index 0000000..ee8977a --- /dev/null +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/confirm.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "strings" +) + +// Confirm ends an enrolment that came through (ADR 0587 §6): the node, dialled as its own login node. after it +// redeemed, calls it, and the runtime's proof of that caller is the confirmation (as ADR 0583 §3). Only then does the holder +// say node-enrolled, with the key's whole content, and delete the key and its address key. The node's name is the caller's +// own; an argument naming another node is refused. A confirm called again after a stop between the event and the deletes +// finishes the deletes and says nothing twice (the bus keeps one message of one id). +func (h *Holder) Confirm(caller, node string) (map[string]any, error) { + own, ok := strings.CutPrefix(caller, nodePrefix) + if !ok || CheckNode(own) != nil || strings.Contains(own, "--") { + return nil, fmt.Errorf("only the node itself, dialled as node., confirms its enrolment, and the runtime proved the caller to be %q", caller) + } + if node != "" && node != own { + return nil, fmt.Errorf("%s confirms its own enrolment only, not that of %s", caller, node) + } + row, found, err := h.Store.Get(NodeKey(own)) + if err != nil { + return nil, fmt.Errorf("the enrolment of %s cannot be read: %w", own, err) + } + if !found { + return nil, fmt.Errorf("no enrolment of %s is open: nothing to confirm", own) + } + e, err := decodeEnrolment(row.Value) + if err != nil { + return nil, err + } + if e.Stage != StageRedeeming { + return nil, fmt.Errorf("the enrolment of %s is %s: it is confirmed after redeem", own, e.Stage) + } + if err := h.sayEvent(EventEnrolled, e, ""); err != nil { + return nil, fmt.Errorf("node-enrolled of %s cannot be said, so nothing is deleted (call confirm again): %w", own, err) + } + if err := h.release(e); err != nil { + return nil, fmt.Errorf("the enrolment of %s is confirmed and not closed (call confirm again): %w", own, err) + } + return map[string]any{"node": own, "address": e.Address, "says": "the node is enrolled; its enrolment is closed and the vault is asked to drop the token"}, nil +} diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/confirm_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/confirm_test.go new file mode 100644 index 0000000..b88928c --- /dev/null +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/confirm_test.go @@ -0,0 +1,44 @@ +package main + +import "testing" + +// 774/S57/AC1: a confirm from any caller but node. is refused; from 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) { + for _, c := range []struct{ name, caller, node, want string }{ + {"the enrolment user", "enrol.ace", "", "dialled as node."}, + {"another node", "node.g14", "ace", "its own enrolment only"}, + {"the second login", "node.ace--b", "", "dialled as node."}, + {"no one", "unproven", "ace", "dialled as node."}, + } { + r := newRig(t) + r.seed("ace", "10.9.0.2", StageRedeeming) + if _, err := r.h.Confirm(c.caller, c.node); err == nil { + t.Errorf("%s: confirmed", c.name) + } else { + mustContain(t, err.Error(), c.want) + } + if len(r.said) != 0 || len(r.keys()) != 2 { + t.Errorf("%s: said %v, keys %v", c.name, r.events(), r.keys()) + } + } + r := newRig(t) + r.seed("ace", "10.9.0.2", StageLive) + if _, err := r.h.Confirm("node.ace", ""); err == nil { + t.Error("confirmed before redeem") + } + r.seed("ace", "10.9.0.2", StageRedeeming) + r.st.failDelete[NodeKey("ace")] = true + if _, err := r.h.Confirm("node.ace", ""); err == nil { + t.Fatal("a delete that failed was taken for done") + } + if r.has(AddressKey("10.9.0.2")) || !r.has(NodeKey("ace")) || len(r.said) != 1 { + t.Fatalf("keys %v, said %v", r.keys(), r.events()) + } + if _, err := r.h.Confirm("node.ace", ""); err != nil || len(r.keys()) != 0 { + t.Fatalf("%v, keys %v", err, r.keys()) + } + if r.events()[0] != EventEnrolled || r.said[0].ID != r.said[1].ID { + t.Errorf("events %v ids %s %s", r.events(), r.said[0].ID, r.said[1].ID) + } +} diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/issue.go b/modules/mesh-enrolment/cmd/mesh-enrolment/issue.go index e6f6be2..e701e00 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/issue.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/issue.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "errors" "fmt" "strconv" @@ -119,21 +120,38 @@ func (h *Holder) Issue(caller, node, why string, lasts time.Duration) (IssueAnsw return IssueAnswer{}, fmt.Errorf("the enrolment of %s cannot be written: %w", node, err) } if err := h.raise(e); err != nil { - h.release(e) + _ = h.release(e) return IssueAnswer{}, err } return IssueAnswer{Node: node, Address: e.Address, Ask: e.Ask, Expires: e.Expires, Says: "the operator is asked on their channel; only their yes makes the token. mesh-enrolment.tokens shows where it stands"}, nil } -// release takes an enrolment's two keys away, the address key last (the node's key is what names the reservation's owner). -func (h *Holder) release(e Enrolment) { - if err := h.Store.Delete(NodeKey(e.Node)); err != nil { - h.logf("the key of %s could not be deleted and stays: %v", e.Node, err) +// release takes an enrolment's two keys away, the address key first and only while it still names this node (a key some +// other enrolment now holds is never deleted), then the node's key, whose presence is what lets a call made again finish a +// release that stopped. It answers what could not be deleted. +func (h *Holder) release(e Enrolment) error { + var errs []error + if row, found, err := h.Store.Get(AddressKey(e.Address)); err != nil { + errs = append(errs, fmt.Errorf("the address key %s cannot be read: %w", e.Address, err)) + } else if found { + var r Reservation + if json.Unmarshal(row.Value, &r) == nil && r.Node == e.Node { + if err := h.Store.Delete(AddressKey(e.Address)); err != nil { + errs = append(errs, fmt.Errorf("the address key %s could not be deleted: %w", e.Address, err)) + } + } } - if err := h.Store.Delete(AddressKey(e.Address)); err != nil { - h.logf("the address key %s could not be deleted and stays: %v", e.Address, err) + if len(errs) == 0 { + if err := h.Store.Delete(NodeKey(e.Node)); err != nil { + errs = append(errs, fmt.Errorf("the key of %s could not be deleted: %w", e.Node, err)) + } } + if err := errors.Join(errs...); err != nil { + h.logf("%v", err) + return err + } + return nil } // AskOf is the ask of an enrolment, in the words the operator reads: that a machine of this name may join until the @@ -235,8 +253,7 @@ func (h *Holder) refuse(e Enrolment, reason string) error { if err := h.sayEvent(EventRefused, e, reason); err != nil { return fmt.Errorf("enrolment-refused of %s cannot be said, so its keys stay: %w", e.Node, err) } - h.release(e) - return nil + return h.release(e) } // Said is the asker's note of an ask that ended with no yes or no (expired, or refused by the router): the enrolment ends. diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/tools.go b/modules/mesh-enrolment/cmd/mesh-enrolment/tools.go index 3777b71..e6f1ac3 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/tools.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/tools.go @@ -11,10 +11,11 @@ import ( // Tool names: the seat's verbs, as the manifest claims them (ADR 0587 §1, §8). const ( - IssueTool = Seat + ".issue" - AgainTool = Seat + ".again" - RedeemTool = Seat + ".redeem" - TokensTool = Seat + ".tokens" + IssueTool = Seat + ".issue" + AgainTool = Seat + ".again" + RedeemTool = Seat + ".redeem" + ConfirmTool = Seat + ".confirm" + TokensTool = Seat + ".tokens" ) func obj(props map[string]any, required ...string) map[string]any { @@ -87,6 +88,12 @@ func Tools(h *Holder) []stdio.Tool { } return h.Redeem(stdio.CallerOf(a), args) }}, + {Name: ConfirmTool, + Description: "The new machine's confirmation (novox/hq ADR 0587 §6), answered only for the node dialled as its own login " + + "node. after it redeemed: the runtime's proof of that caller is the confirmation. Says node-enrolled and closes " + + "the enrolment; the vault is then asked to drop the token.", + Input: obj(map[string]any{"node": str("the machine's own name (optional)")}), + Run: func(a map[string]any) (any, error) { return h.Confirm(stdio.CallerOf(a), arg(a, "node")) }}, {Name: AgainTool, Description: "Ask the operator again about an enrolment still at stage asked whose ask the router no longer holds open " + "(novox/hq ADR 0587 §8): a new ask for the same node, address and expiry. Nothing more: a redeem stopped midway is finished " +