mesh-enrolment: confirm closes an enrolment on the node's own call
Only the node dialled as node.<node> confirms (the runtime's proof of the caller is the confirmation, ADR 0587 §6); the holder then says node-enrolled with the key's content and deletes the address key (while it still names the node) and the node's key. A stop between the event and the deletes is finished by calling confirm again; the event keeps one message. release now answers what it could not delete. Tests added: one table-style test. (issue 774, 774/S57) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -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.<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.<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
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
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) {
|
||||
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"},
|
||||
{"the second login", "node.ace--b", "", "dialled as node.<node>"},
|
||||
{"no one", "unproven", "ace", "dialled as node.<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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.<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 " +
|
||||
|
||||
Reference in New Issue
Block a user