Merge pull request 'Ask a node's engine which of its directories the walk would refuse, read-only (issue 358)' (#258) from fix/358-judge-directories-read-only into main
This commit was merged in pull request #258.
This commit is contained in:
@@ -167,3 +167,54 @@ func TestASetuidSearchAsksOnlyAKnownNodeAndFailsOnARefusal(t *testing.T) {
|
|||||||
t.Fatalf("a start: %v, printed %q", err, out.String())
|
t.Fatalf("a start: %v, printed %q", err, out.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The directory judgement's line asks only a known node, one name and nothing else, fails on a refusal, and says it
|
||||||
|
// changed nothing (novox/hq issue 358).
|
||||||
|
func TestADirectoryJudgementAsksOnlyAKnownNodeAndSaysItIsReadOnly(t *testing.T) {
|
||||||
|
t.Setenv(link.CallerVar, "jo through mesh-cli on anchor")
|
||||||
|
asked := 0
|
||||||
|
ask := func(answer link.HandOverAnswer) func(node, by string) (link.HandOverAnswer, error) {
|
||||||
|
return func(node, by string) (link.HandOverAnswer, error) {
|
||||||
|
asked++
|
||||||
|
if node != "novox" || by != "jo through mesh-cli on anchor" {
|
||||||
|
t.Fatalf("asked %q %q", node, by)
|
||||||
|
}
|
||||||
|
return answer, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
known := func(string) error { return nil }
|
||||||
|
var out bytes.Buffer
|
||||||
|
for _, args := range [][]string{nil, {"novox", "extra"}, {"-x"}} {
|
||||||
|
if err := judgeDirectoriesAsked(args, known, ask(link.HandOverAnswer{Said: "x"}), &out); err == nil || asked != 0 {
|
||||||
|
t.Fatalf("%v was asked: %v", args, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := judgeDirectoriesAsked([]string{"novox"}, func(string) error { return errors.New("no node called novox") },
|
||||||
|
ask(link.HandOverAnswer{Said: "x"}), &out); err == nil || asked != 0 {
|
||||||
|
t.Fatalf("an unknown node: %v", err)
|
||||||
|
}
|
||||||
|
if err := judgeDirectoriesAsked([]string{"novox"}, known, ask(link.HandOverAnswer{Refused: "no; nothing was judged"}),
|
||||||
|
&out); err == nil || !strings.Contains(err.Error(), "novox refused") || out.Len() != 0 {
|
||||||
|
t.Fatalf("a refusal: %v, printed %q", err, out.String())
|
||||||
|
}
|
||||||
|
said := "2 directories declared: 1 placed, 1 would be refused\n/var/lib/a: placed\n/var/lib/b: would be refused: …"
|
||||||
|
if err := judgeDirectoriesAsked([]string{"novox"}, known, ask(link.HandOverAnswer{Said: said}), &out); err != nil ||
|
||||||
|
out.String() != "novox (read-only: nothing on novox was changed)\n"+said+"\n" {
|
||||||
|
t.Fatalf("a judgement: %v, printed %q", err, out.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The judgement is a seat verb an agent may call — it reads, and changes nothing on the node — and runs exactly
|
||||||
|
// `node judge-directories <node>` (novox/hq issue 358).
|
||||||
|
func TestTheDirectoryJudgementIsAVerbThatOnlyReads(t *testing.T) {
|
||||||
|
argv, err := argvFor("judge-directories", map[string]any{"node": "novox"})
|
||||||
|
if err != nil || strings.Join(argv, " ") != "node judge-directories novox" {
|
||||||
|
t.Fatalf("the verb runs %v, %v", argv, err)
|
||||||
|
}
|
||||||
|
if _, err := argvFor("judge-directories", map[string]any{}); err == nil {
|
||||||
|
t.Fatal("the verb ran with no node")
|
||||||
|
}
|
||||||
|
if !readingVerbs["judge-directories"] {
|
||||||
|
t.Fatal("the judgement is not counted a reading verb")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -127,9 +127,14 @@ func nodeCommand(ctx context.Context, args []string) error {
|
|||||||
// and whoever may call a verb includes agents.
|
// and whoever may call a verb includes agents.
|
||||||
return nodeSetuidSearch(ctx, open, args[1:])
|
return nodeSetuidSearch(ctx, open, args[1:])
|
||||||
|
|
||||||
|
case "judge-directories":
|
||||||
|
// Which of a node's declared directories the walk that follows no link another account could make would
|
||||||
|
// refuse (novox/hq issue 358). Read-only on the node and here, so a verb may run it too.
|
||||||
|
return nodeJudgeDirectories(ctx, open, args[1:])
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("node has no %q; it has add, list, show, public-domain, account, agent-account, hand-over "+
|
return fmt.Errorf("node has no %q; it has add, list, show, public-domain, account, agent-account, hand-over, "+
|
||||||
"and setuid-search", args[0])
|
"setuid-search and judge-directories", args[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -937,3 +942,57 @@ func printJSONTo(w io.Writer, v any) error {
|
|||||||
fmt.Fprintln(w, string(body))
|
fmt.Fprintln(w, string(body))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const judgeDirectoriesUsage = "node judge-directories <node> — read-only: which of the directories <node> is " +
|
||||||
|
"declared the node-engine's walk that follows no link another account could make would refuse, one line each " +
|
||||||
|
"(novox/hq issue 358). Nothing is made, chmodded or chowned"
|
||||||
|
|
||||||
|
// nodeJudgeDirectories asks the node's engine to judge its declared directories, signed with the mesh's key as a
|
||||||
|
// hand-over is. Read-only: the engine changes nothing and keeps nothing.
|
||||||
|
func nodeJudgeDirectories(ctx context.Context, open *stores, args []string) error {
|
||||||
|
known := func(node string) error {
|
||||||
|
_, err := open.inventory.NodeByName(ctx, node)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ask := func(node, by string) (link.HandOverAnswer, error) {
|
||||||
|
ident, err := open.Identity(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return link.HandOverAnswer{}, fmt.Errorf("the mesh's signing key cannot be read, so nothing was asked of %s: %w",
|
||||||
|
node, err)
|
||||||
|
}
|
||||||
|
address, err := broker.BusAddress()
|
||||||
|
if err != nil {
|
||||||
|
return link.HandOverAnswer{}, err
|
||||||
|
}
|
||||||
|
js, err := broker.Dial(address)
|
||||||
|
if err != nil {
|
||||||
|
return link.HandOverAnswer{}, fmt.Errorf("cannot reach the bus, so nothing was asked of %s: %w", node, err)
|
||||||
|
}
|
||||||
|
defer js.Close()
|
||||||
|
return link.AskJudgeDirectories(ctx, js.Conn(), ident, node, by, link.HandOverWithin)
|
||||||
|
}
|
||||||
|
return judgeDirectoriesAsked(args, known, ask, os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// judgeDirectoriesAsked is the line with its two acts given: whether the mesh knows the node, and the ask. The
|
||||||
|
// engine's refusal is this command's failure.
|
||||||
|
func judgeDirectoriesAsked(args []string, known func(node string) error,
|
||||||
|
ask func(node, by string) (link.HandOverAnswer, error), out io.Writer) error {
|
||||||
|
if len(args) != 1 || args[0] == "" || strings.HasPrefix(args[0], "-") {
|
||||||
|
return errors.New(judgeDirectoriesUsage)
|
||||||
|
}
|
||||||
|
node := args[0]
|
||||||
|
if err := known(node); err != nil {
|
||||||
|
return fmt.Errorf("nothing was asked: %w", err)
|
||||||
|
}
|
||||||
|
answer, err := ask(node, handOverBy())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if answer.Refused != "" {
|
||||||
|
return fmt.Errorf("%s refused: %s", node, answer.Refused)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(out, "%s (read-only: nothing on %s was changed)\n", node, node)
|
||||||
|
fmt.Fprintln(out, answer.Said)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -314,6 +314,11 @@ func (a *verbArguments) commandLine() ([]string, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return []string{"node", "show", str("node")}, nil
|
return []string{"node", "show", str("node")}, nil
|
||||||
|
case "judge-directories":
|
||||||
|
if err := need("node"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []string{"node", "judge-directories", str("node")}, nil
|
||||||
case "modules":
|
case "modules":
|
||||||
return []string{"module", "list", "--json"}, nil
|
return []string{"module", "list", "--json"}, nil
|
||||||
case "seats":
|
case "seats":
|
||||||
@@ -1479,7 +1484,9 @@ func seatAnnouncement(handlers map[string]link.ToolHandler) micro.Info {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// nodeReads are the `node` subcommands a verb may run: the ones that only read.
|
// nodeReads are the `node` subcommands a verb may run: the ones that only read.
|
||||||
var nodeReads = map[string]bool{"list": true, "show": true}
|
var nodeReads = map[string]bool{"list": true, "show": true,
|
||||||
|
// Read-only on the node too: the engine judges its directories and changes nothing (novox/hq issue 358).
|
||||||
|
"judge-directories": true}
|
||||||
|
|
||||||
// flagsOnly says a command line's rest names no subcommand: empty, or beginning with a flag. For a command
|
// flagsOnly says a command line's rest names no subcommand: empty, or beginning with a flag. For a command
|
||||||
// with no subcommands every word is a flag, its value or a name it reads.
|
// with no subcommands every word is a flag, its value or a name it reads.
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ func composeStatus(open *stores) func(context.Context) ([]byte, error) {
|
|||||||
var readingVerbs = map[string]bool{
|
var readingVerbs = map[string]bool{
|
||||||
"tools": true, "calls": true, "status": true, "nodes": true, "node": true, "modules": true,
|
"tools": true, "calls": true, "status": true, "nodes": true, "node": true, "modules": true,
|
||||||
"seats": true, "builds": true, "plan": true, "queue": true, "durations": true, "hand-acts": true,
|
"seats": true, "builds": true, "plan": true, "queue": true, "durations": true, "hand-acts": true,
|
||||||
"doctor": true, "conditions": true, "healers": true,
|
"doctor": true, "conditions": true, "healers": true, "judge-directories": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// nudgingListener is the enrolment, nudging the summary when a machine said something new.
|
// nudgingListener is the enrolment, nudging the summary when a machine said something new.
|
||||||
|
|||||||
+10
-1
@@ -336,6 +336,13 @@ func AskHandOverSubject(node string) string { return "mesh.node." + node + ".ask
|
|||||||
// a hand-over is (link.SetuidSearchContext), for the same reason.
|
// a hand-over is (link.SetuidSearchContext), for the same reason.
|
||||||
func AskSetuidSearchSubject(node string) string { return "mesh.node." + node + ".ask.setuid-search" }
|
func AskSetuidSearchSubject(node string) string { return "mesh.node." + node + ".ask.setuid-search" }
|
||||||
|
|
||||||
|
// AskJudgeDirectoriesSubject is where the controller asks one machine's node-engine to judge, read-only, which of
|
||||||
|
// its declared directories the walk that follows no link another account could make would refuse (novox/hq issue
|
||||||
|
// 358): a request answered once, signed as a hand-over is (link.JudgeDirectoriesContext), for the same reason.
|
||||||
|
func AskJudgeDirectoriesSubject(node string) string {
|
||||||
|
return "mesh.node." + node + ".ask.judge-directories"
|
||||||
|
}
|
||||||
|
|
||||||
// inbox is a principal's own reply space. No user is ever granted a bare `_INBOX.>` (design 25
|
// inbox is a principal's own reply space. No user is ever granted a bare `_INBOX.>` (design 25
|
||||||
// §4): with one account, inbox privacy is the permission list or it is nothing, so each user's
|
// §4): with one account, inbox privacy is the permission list or it is nothing, so each user's
|
||||||
// inbox is derived from its own identity and its permissions name that prefix and no other.
|
// inbox is derived from its own identity and its permissions name that prefix and no other.
|
||||||
@@ -614,7 +621,9 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
|||||||
// issue 356), which it answers on the request's reply: the one request a node is asked.
|
// issue 356), which it answers on the request's reply: the one request a node is asked.
|
||||||
AskHandOverSubject(p.Node),
|
AskHandOverSubject(p.Node),
|
||||||
// And asking it for a fresh search for setuid programs (novox/hq issue 361), answered the same way.
|
// And asking it for a fresh search for setuid programs (novox/hq issue 361), answered the same way.
|
||||||
AskSetuidSearchSubject(p.Node)}
|
AskSetuidSearchSubject(p.Node),
|
||||||
|
// And asking it to judge its declared directories, read-only (novox/hq issue 358), answered the same way.
|
||||||
|
AskJudgeDirectoriesSubject(p.Node)}
|
||||||
// The node-engine witnesses the core builds it places (novox/hq to-be 45 §8, ADR 0236; the
|
// The node-engine witnesses the core builds it places (novox/hq to-be 45 §8, ADR 0236; the
|
||||||
// contract is lease/witness.go): it asks its own machine's node tools PING, and, where the
|
// contract is lease/witness.go): it asks its own machine's node tools PING, and, where the
|
||||||
// machine runs the controller, reads the lease's one key — read, never written.
|
// machine runs the controller, reads the lease's one key — read, never written.
|
||||||
|
|||||||
@@ -114,7 +114,9 @@ func TestOnlyWhatCanBeAskedMayAnswer(t *testing.T) {
|
|||||||
node, _ := PermissionsFor(Principal{Kind: KindNode, Node: "one", PasswordHash: "x"})
|
node, _ := PermissionsFor(Principal{Kind: KindNode, Node: "one", PasswordHash: "x"})
|
||||||
if !node.AllowResponses || !slices.Contains(node.Subscribe, AskHandOverSubject("one")) ||
|
if !node.AllowResponses || !slices.Contains(node.Subscribe, AskHandOverSubject("one")) ||
|
||||||
!slices.Contains(node.Subscribe, AskSetuidSearchSubject("one")) ||
|
!slices.Contains(node.Subscribe, AskSetuidSearchSubject("one")) ||
|
||||||
slices.Contains(node.Subscribe, AskSetuidSearchSubject("two")) {
|
slices.Contains(node.Subscribe, AskSetuidSearchSubject("two")) ||
|
||||||
|
!slices.Contains(node.Subscribe, AskJudgeDirectoriesSubject("one")) ||
|
||||||
|
slices.Contains(node.Subscribe, AskJudgeDirectoriesSubject("two")) {
|
||||||
t.Fatalf("a node cannot answer the hand-over it is asked: %v %v", node.AllowResponses, node.Subscribe)
|
t.Fatalf("a node cannot answer the hand-over it is asked: %v %v", node.AllowResponses, node.Subscribe)
|
||||||
}
|
}
|
||||||
person, _ := PermissionsFor(Principal{Kind: KindPerson, Node: "one", Module: "jo", PasswordHash: "x"})
|
person, _ := PermissionsFor(Principal{Kind: KindPerson, Node: "one", Module: "jo", PasswordHash: "x"})
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ accounts {
|
|||||||
} }
|
} }
|
||||||
{ user: "node.one", password: "$2a$11$nnnnnnnnnnnnnnnnnnnnnn", permissions: {
|
{ user: "node.one", password: "$2a$11$nnnnnnnnnnnnnnnnnnnnnn", permissions: {
|
||||||
publish: { allow: ["$JS.ACK.NODES.one.>", "$JS.API.CONSUMER.INFO.NODES.one", "$SRV.PING.node-tools.one", "mesh.control.one.>"] }
|
publish: { allow: ["$JS.ACK.NODES.one.>", "$JS.API.CONSUMER.INFO.NODES.one", "$SRV.PING.node-tools.one", "mesh.control.one.>"] }
|
||||||
subscribe: { allow: ["_DELIVER.one", "_DELIVER.one.>", "_INBOX.node.one.>", "mesh.node.one.ask.hand-over", "mesh.node.one.ask.report", "mesh.node.one.ask.setuid-search", "mesh.node.one.declare"] }
|
subscribe: { allow: ["_DELIVER.one", "_DELIVER.one.>", "_INBOX.node.one.>", "mesh.node.one.ask.hand-over", "mesh.node.one.ask.judge-directories", "mesh.node.one.ask.report", "mesh.node.one.ask.setuid-search", "mesh.node.one.declare"] }
|
||||||
allow_responses: { max: 1, ttl: "1m" }
|
allow_responses: { max: 1, ttl: "1m" }
|
||||||
} }
|
} }
|
||||||
{ user: "one.nats", password: "$2a$11$bbbbbbbbbbbbbbbbbbbbbb", permissions: {
|
{ user: "one.nats", password: "$2a$11$bbbbbbbbbbbbbbbbbbbbbb", permissions: {
|
||||||
|
|||||||
@@ -96,6 +96,11 @@ var WritersTable = []WriterRow{
|
|||||||
{State: "a fresh setuid search asked of a machine", Writer: "controller, at its terminal",
|
{State: "a fresh setuid search asked of a machine", Writer: "controller, at its terminal",
|
||||||
KeptIn: "the machine, which throws its last search away", Others: "the engine verifies the mesh's signature and starts it, or refuses",
|
KeptIn: "the machine, which throws its last search away", Others: "the engine verifies the mesh's signature and starts it, or refuses",
|
||||||
Subjects: []string{"mesh.node.*.ask.setuid-search"}, Writes: isController},
|
Subjects: []string{"mesh.node.*.ask.setuid-search"}, Writes: isController},
|
||||||
|
// A read-only judgement of a machine's declared directories asked of its engine (novox/hq issue 358): signed as a
|
||||||
|
// hand-over is, under a signing context of its own. It changes nothing on the machine.
|
||||||
|
{State: "a judgement of a machine's directories asked of it", Writer: "controller",
|
||||||
|
KeptIn: "nowhere: the engine answers it and keeps nothing", Others: "the engine verifies the mesh's signature and answers, or refuses",
|
||||||
|
Subjects: []string{"mesh.node.*.ask.judge-directories"}, Writes: isController},
|
||||||
{State: "a machine's applied state and its report", Writer: "the node-engine's apply queue",
|
{State: "a machine's applied state and its report", Writer: "the node-engine's apply queue",
|
||||||
KeptIn: "the machine; the report on the bus", Others: "the reconcile and a delivery enqueue, never apply",
|
KeptIn: "the machine; the report on the bus", Others: "the reconcile and a delivery enqueue, never apply",
|
||||||
// And its health statement between reports (novox/hq ADR 0240): the same writer stating the same
|
// And its health statement between reports (novox/hq ADR 0240): the same writer stating the same
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ var designRows = []string{
|
|||||||
"a machine's declaration",
|
"a machine's declaration",
|
||||||
"a hand-over asked of a machine",
|
"a hand-over asked of a machine",
|
||||||
"a fresh setuid search asked of a machine",
|
"a fresh setuid search asked of a machine",
|
||||||
|
"a judgement of a machine's directories asked of it",
|
||||||
"a machine's applied state and its report",
|
"a machine's applied state and its report",
|
||||||
"the controller lease",
|
"the controller lease",
|
||||||
"plans and their tiers",
|
"plans and their tiers",
|
||||||
@@ -186,3 +187,21 @@ func TestASetuidSearchAskHasOnePublisher(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **A judgement of a machine's directories asked of it has one publisher, the controller** (novox/hq issue 358), as a
|
||||||
|
// hand-over has.
|
||||||
|
func TestADirectoryJudgementAskHasOnePublisher(t *testing.T) {
|
||||||
|
for _, p := range []Principal{
|
||||||
|
{Kind: KindNode, Node: "laptop"},
|
||||||
|
{Kind: KindNodeTools, Node: "laptop", Module: RuntimeModule},
|
||||||
|
{Kind: KindModule, Node: "laptop", Module: "notes"},
|
||||||
|
} {
|
||||||
|
err := CheckWriters(p, []string{AskJudgeDirectoriesSubject("laptop")})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "a judgement of a machine's directories asked of it") {
|
||||||
|
t.Errorf("%s may ask a judgement of directories: %v", p.Username(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := CheckWriters(Principal{Kind: KindController}, []string{AskJudgeDirectoriesSubject("laptop")}); err != nil {
|
||||||
|
t.Fatalf("the controller may not ask a judgement: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -490,6 +490,13 @@ var ControllerVerbs = []Verb{
|
|||||||
"why": "with consumer or older-than: why — required, and recorded in the hand-act log",
|
"why": "with consumer or older-than: why — required, and recorded in the hand-act log",
|
||||||
"cause": "with consumer or older-than: the cause in a word (cleanup-waiting when absent)",
|
"cause": "with consumer or older-than: the cause in a word (cleanup-waiting when absent)",
|
||||||
}, nil, "confirm")},
|
}, nil, "confirm")},
|
||||||
|
{Name: "judge-directories", Description: "Read-only: which of the directories one machine is declared its " +
|
||||||
|
"node-engine's walk that follows no link another account could make would refuse (novox/hq issue 358), one line " +
|
||||||
|
"each: placed, would be refused and why (the link and who could replace it), refused already by the placement " +
|
||||||
|
"guard, or below a home. The engine judges the declaration it keeps and makes, chmods and chowns nothing.",
|
||||||
|
Input: schema(map[string]string{
|
||||||
|
"node": "the machine's name",
|
||||||
|
}, []string{"node"})},
|
||||||
// What a consumer gave up on (novox/hq issue 330): kept in DEAD_LETTERS until a person acts on it.
|
// What a consumer gave up on (novox/hq issue 330): kept in DEAD_LETTERS until a person acts on it.
|
||||||
{Name: "root-free", Description: "Whether each machine named is root-free now (novox/hq ADR 0259 §8): no agent " +
|
{Name: "root-free", Description: "Whether each machine named is root-free now (novox/hq ADR 0259 §8): no agent " +
|
||||||
"there can become root without a person. Judged when asked, never from a condition: free only when the machine " +
|
"there can become root without a person. Judged when asked, never from a condition: free only when the machine " +
|
||||||
|
|||||||
@@ -148,6 +148,28 @@ func AskSetuidSearch(ctx context.Context, conn *nats.Conn, signer Signer, node,
|
|||||||
"there", record: "a start"})
|
"there", record: "a start"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JudgeDirectoriesContext is prefixed to a directory judgement ask's bytes before signing (novox/hq issue 358): never
|
||||||
|
// another ask's signature, nor a declaration's. The engine holds the same words.
|
||||||
|
const JudgeDirectoriesContext = "novox-mesh judge-directories v1\n"
|
||||||
|
|
||||||
|
// AskJudgeDirectories asks one machine's node-engine to judge, read-only, which of the directories it is declared
|
||||||
|
// the walk that follows no link another account could make would refuse (novox/hq issue 358): the ask a hand-over
|
||||||
|
// is, naming no path, signed under JudgeDirectoriesContext. The engine changes nothing and answers one line per
|
||||||
|
// directory.
|
||||||
|
func AskJudgeDirectories(ctx context.Context, conn *nats.Conn, signer Signer, node, by string,
|
||||||
|
timeout time.Duration) (HandOverAnswer, error) {
|
||||||
|
if conn == nil {
|
||||||
|
return HandOverAnswer{}, errors.New("this controller is not on the bus")
|
||||||
|
}
|
||||||
|
body, err := signAsk(ctx, signer, JudgeDirectoriesContext, HandOverAsk{Node: node, By: by})
|
||||||
|
if err != nil {
|
||||||
|
return HandOverAnswer{}, err
|
||||||
|
}
|
||||||
|
return askSigned(ctx, conn, broker.AskJudgeDirectoriesSubject(node), body, node, timeout, askWords{
|
||||||
|
what: "a judgement of its directories", nothing: "nothing was judged, and nothing is changed by one",
|
||||||
|
issue: "issue 358", unknown: "ask again: a judgement changes nothing", record: "a judgement"})
|
||||||
|
}
|
||||||
|
|
||||||
// SignHandOver fills the ask's expiry and nonce and signs it with the mesh's key, over HandOverContext and the
|
// SignHandOver fills the ask's expiry and nonce and signs it with the mesh's key, over HandOverContext and the
|
||||||
// ask's bytes exactly as they travel.
|
// ask's bytes exactly as they travel.
|
||||||
func SignHandOver(ctx context.Context, signer Signer, ask HandOverAsk) ([]byte, error) {
|
func SignHandOver(ctx context.Context, signer Signer, ask HandOverAsk) ([]byte, error) {
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package link
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/broker"
|
||||||
|
"github.com/novox/mesh-controller/internal/testbus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A judgement of a machine's directories is asked of the machine on its own subject, signed under its own context
|
||||||
|
// (never a hand-over's), naming no path, and the engine's answer comes back (novox/hq issue 358). The engine holds the
|
||||||
|
// same subject and context (mesh-host internal/link, TestTheDirectoryJudgementAskKeepsItsSubjectAndContext).
|
||||||
|
func TestADirectoryJudgementIsAskedOfTheMachineSignedUnderItsOwnContext(t *testing.T) {
|
||||||
|
if broker.AskJudgeDirectoriesSubject("laptop") != "mesh.node.laptop.ask.judge-directories" ||
|
||||||
|
JudgeDirectoriesContext != "novox-mesh judge-directories v1\n" {
|
||||||
|
t.Fatalf("the subject %q, the context %q", broker.AskJudgeDirectoriesSubject("laptop"), JudgeDirectoriesContext)
|
||||||
|
}
|
||||||
|
conn, err := nats.Connect(testbus.URL(t))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(conn.Close)
|
||||||
|
signer, public := testSigner(t)
|
||||||
|
var heard HandOverAsk
|
||||||
|
engine, err := conn.Subscribe(broker.AskJudgeDirectoriesSubject("laptop"), func(msg *nats.Msg) {
|
||||||
|
var signed SignedHandOver
|
||||||
|
_ = json.Unmarshal(msg.Data, &signed)
|
||||||
|
if ed25519.Verify(public, append([]byte(HandOverContext), signed.Ask...), signed.Signature) ||
|
||||||
|
ed25519.Verify(public, append([]byte(SetuidSearchContext), signed.Ask...), signed.Signature) ||
|
||||||
|
!ed25519.Verify(public, append([]byte(JudgeDirectoriesContext), signed.Ask...), signed.Signature) {
|
||||||
|
_ = msg.Respond([]byte(`{"refused":"not signed as a judgement"}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.Unmarshal(signed.Ask, &heard)
|
||||||
|
body, _ := json.Marshal(HandOverAnswer{Said: "3 directories declared, asked by " + heard.By})
|
||||||
|
_ = msg.Respond(body)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = engine.Unsubscribe() })
|
||||||
|
a, err := AskJudgeDirectories(context.Background(), conn, signer, "laptop", "jo", 5*time.Second)
|
||||||
|
if err != nil || a.Said != "3 directories declared, asked by jo" {
|
||||||
|
t.Fatalf("answered %+v, %v", a, err)
|
||||||
|
}
|
||||||
|
if heard.Node != "laptop" || heard.Path != "" || heard.Nonce == "" || heard.Expires.IsZero() {
|
||||||
|
t.Fatalf("the engine heard %+v", heard)
|
||||||
|
}
|
||||||
|
if _, err := AskJudgeDirectories(context.Background(), conn, signer, "anchor", "jo", 5*time.Second); err == nil ||
|
||||||
|
!strings.Contains(err.Error(), "nothing was judged") {
|
||||||
|
t.Fatalf("a machine with no engine listening: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -89,6 +89,7 @@
|
|||||||
"cleanup",
|
"cleanup",
|
||||||
"dead-letters",
|
"dead-letters",
|
||||||
"root-free",
|
"root-free",
|
||||||
|
"judge-directories",
|
||||||
"data",
|
"data",
|
||||||
"build",
|
"build",
|
||||||
"artifacts",
|
"artifacts",
|
||||||
|
|||||||
Reference in New Issue
Block a user