198 lines
8.8 KiB
Go
198 lines
8.8 KiB
Go
package link
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/asker"
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
)
|
|
|
|
// The take act's ask (novox/hq ADR 0325 §8): a hand-over's fields and the act's own in one signed object, under a
|
|
// signing context of its own. The controller's side holds the same names and words.
|
|
func TestTheTakeActsAskKeepsItsFieldNamesAndContext(t *testing.T) {
|
|
body, _ := json.Marshal(AccessActAsk{Act: ActTake, Access: "radarr.movies", Warrant: "w"})
|
|
if got := keysOf(t, body); got != "access act warrant" {
|
|
t.Fatalf("the act's fields are %q", got)
|
|
}
|
|
if AskAccessActSubject("ace") != "mesh.node.ace.ask.access-act" {
|
|
t.Fatalf("the subject is %q", AskAccessActSubject("ace"))
|
|
}
|
|
if AccessActContext != "novox-mesh access-act v2\n" {
|
|
t.Fatalf("the signing context is %q", AccessActContext)
|
|
}
|
|
}
|
|
|
|
// signedAct is an act's ask as the controller makes it: the hand-over's fields beside the act's, signed over the
|
|
// act's context and its bytes.
|
|
func signedAct(t *testing.T, key ed25519.PrivateKey, ask HandOverAsk, act AccessActAsk) []byte {
|
|
t.Helper()
|
|
raw, _ := json.Marshal(struct {
|
|
HandOverAsk
|
|
AccessActAsk
|
|
}{ask, act})
|
|
body, _ := json.Marshal(SignedHandOver{Ask: raw, Signature: ed25519.Sign(key, append([]byte(AccessActContext), raw...))})
|
|
return body
|
|
}
|
|
|
|
// **Nothing is done without a warrant**, nor for any act but take, nor signed as a hand-over (ADR 0325 §8).
|
|
func TestTheTakeActRunsOnlyOnAWarrant(t *testing.T) {
|
|
m, key := aMember(t)
|
|
now := time.Now()
|
|
var given []AccessAct
|
|
handle := func(a AccessAct) (string, error) { given = append(given, a); return "started", nil }
|
|
check := HandOverCheck{Signer: m.Signer, Node: m.Node, Nonces: &Nonces{}}
|
|
ask := func(nonce string) HandOverAsk {
|
|
return HandOverAsk{Node: m.Node, Path: "/srv/media/movies", By: "the operator", Expires: now.Add(HandOverGood),
|
|
Nonce: nonce}
|
|
}
|
|
for why, body := range map[string][]byte{
|
|
"no warrant": signedAct(t, key, ask(aNonce+"01"), AccessActAsk{Act: ActTake, Access: "radarr.movies"}),
|
|
"another act": signedAct(t, key, ask(aNonce+"02"), AccessActAsk{Act: "share", Access: "radarr.movies", Warrant: "w"}),
|
|
"no access": signedAct(t, key, ask(aNonce+"03"), AccessActAsk{Act: ActTake, Warrant: "w"}),
|
|
"a hand-over's": signedAsk(t, key, ask(aNonce+"04")),
|
|
"a slash in the warrant": signedAct(t, key, ask(aNonce+"05"), AccessActAsk{Act: ActTake, Access: "radarr.movies",
|
|
Warrant: "w/1"}),
|
|
"two lines as access": signedAct(t, key, ask(aNonce+"07"), AccessActAsk{Act: ActTake, Access: "radarr.\nmovies",
|
|
Warrant: "w"}),
|
|
} {
|
|
if a := AnswerAccessAct(check, handle, body); a.Refused == "" || !strings.HasSuffix(a.Refused, "nothing was changed") {
|
|
t.Errorf("%s: not refused, or refused without saying nothing was changed: %+v", why, a)
|
|
}
|
|
}
|
|
if len(given) != 0 {
|
|
t.Fatalf("a refused ask reached the act: %+v", given)
|
|
}
|
|
a := AnswerAccessAct(check, handle, signedAct(t, key, ask(aNonce+"06"), AccessActAsk{Act: ActTake,
|
|
Access: "radarr.movies", Warrant: "w-1"}))
|
|
if a.Said != "started" || len(given) != 1 || given[0].Warrant != "w-1" || given[0].Path != "/srv/media/movies" {
|
|
t.Fatalf("a warranted ask did not reach the act whole: %+v %+v", a, given)
|
|
}
|
|
// 774 (ADR 0563 §3): a hand-over asked through the node seat is the engine's own ask, performed only on the
|
|
// operator's warrant to it: never on the call, not on a warrant for another digest, once on a warrant heard twice,
|
|
// and not when the disk changed since it was asked.
|
|
t.Run("the node seat's hand-over acts only on the operator's warrant", func(t *testing.T) {
|
|
for _, row := range []struct {
|
|
name string
|
|
warrant func(asked asks.Ask, w asks.Warrant) asks.Warrant
|
|
diskAfter string
|
|
twice bool
|
|
done int
|
|
state string
|
|
}{
|
|
{name: "no warrant", done: 0, state: HandAsked},
|
|
{name: "the operator's yes", warrant: yes, done: 1, state: HandDone},
|
|
{name: "the yes heard twice", warrant: yes, twice: true, done: 1, state: HandDone},
|
|
{name: "a warrant for another digest", warrant: func(a asks.Ask, w asks.Warrant) asks.Warrant {
|
|
w = yes(a, w)
|
|
w.AskDigest = "sha256:another"
|
|
return w
|
|
}, done: 0, state: HandAsked},
|
|
{name: "a no", warrant: func(a asks.Ask, w asks.Warrant) asks.Warrant {
|
|
w = yes(a, w)
|
|
w.Option, w.Label, w.Level = "no", "No, leave it", asks.Acknowledge
|
|
return w
|
|
}, done: 0, state: HandRefused},
|
|
{name: "the disk changed", warrant: yes, diskAfter: "0:0 700 9:2", done: 0, state: HandRefused},
|
|
{name: "expired", warrant: func(a asks.Ask, w asks.Warrant) asks.Warrant {
|
|
return asks.Warrant{Ask: a.ID, Asker: w.Asker, Outcome: asks.OutcomeExpired, At: w.At}
|
|
}, done: 0, state: HandRefused},
|
|
} {
|
|
t.Run(row.name, func(t *testing.T) {
|
|
h, asked, done := anEngine(t)
|
|
disk := "1000:1000 755 9:1"
|
|
h.JudgeHandOver = func(path string) (Judged, error) {
|
|
return Judged{Subject: path, Path: path, Sets: "owner jo, group jo, mode 0750", Disk: disk,
|
|
Words: "A directory used as found."}, nil
|
|
}
|
|
if _, err := h.Call(EngineCall{Verb: ActHandOver, Caller: "shanks.node-tools",
|
|
Args: map[string]any{"path": "/srv/x", "why": "a test"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(*asked) != 1 || *done != 0 {
|
|
t.Fatalf("the call asked %d and performed %d", len(*asked), *done)
|
|
}
|
|
// The act is one key, and the open ask one key made by a create (ADR 0563 §3).
|
|
if acts, _ := h.State.Keys("act."); len(acts) != 1 || acts[0] != "act."+(*asked)[0].ID {
|
|
t.Fatalf("the act is kept as %v", acts)
|
|
}
|
|
if open, _ := h.State.Keys("open."); len(open) != 1 || !strings.HasPrefix(open[0], "open.hand-over.") {
|
|
t.Fatalf("the open ask is kept as %v", open)
|
|
}
|
|
if _, err := h.Call(EngineCall{Verb: ActHandOver, Caller: "x", Args: map[string]any{"path": "/srv/x"}}); err == nil {
|
|
t.Fatal("a second ask of the same path was asked while one is open")
|
|
}
|
|
if row.diskAfter != "" {
|
|
disk = row.diskAfter
|
|
}
|
|
if row.warrant != nil {
|
|
w := row.warrant((*asked)[0], asks.Warrant{Asker: asks.EngineAsker("n1"), At: time.Now()})
|
|
h.Heard(w)
|
|
if row.twice {
|
|
h.Heard(w)
|
|
}
|
|
}
|
|
got, _ := h.Acts(0)
|
|
acts := got.(map[string]any)["acts"].([]HandAct)
|
|
if *done != row.done || len(acts) != 1 || acts[0].State != row.state {
|
|
t.Fatalf("performed %d (want %d), acts %+v (want %s)", *done, row.done, acts, row.state)
|
|
}
|
|
if open, _ := h.State.Keys("open."); (len(open) == 1) != (row.state == HandAsked) {
|
|
t.Fatalf("an ask %s leaves its open key as %v", row.state, open)
|
|
}
|
|
if row.state == HandDone && (acts[0].By != "operator" || acts[0].Via == "" || acts[0].Ask == "") {
|
|
t.Fatalf("the act does not record the warrant: %+v", acts[0])
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// yes is the operator's yes to an ask, as the router writes it.
|
|
func yes(a asks.Ask, w asks.Warrant) asks.Warrant {
|
|
w.Ask, w.Outcome, w.Option, w.Label, w.Level = a.ID, asks.OutcomeChosen, "yes", "Yes", asks.Approve
|
|
w.By = &asks.Person{Who: asks.Operator, Kind: "telegram", Verified: "user id verified"}
|
|
w.AskDigest, w.Proofs = a.Digest(), []string{"P1"}
|
|
return w
|
|
}
|
|
|
|
// anEngine is a node seat with its own state in a temporary folder, a router that keeps what it was asked, and a
|
|
// hand-over that counts what it performed.
|
|
func anEngine(t *testing.T) (*HandActs, *[]asks.Ask, *int) {
|
|
t.Helper()
|
|
var asked []asks.Ask
|
|
done := 0
|
|
state := &MemState{}
|
|
h := &HandActs{Node: "n1", State: state,
|
|
HandOver: func(path, by string) (string, error) { done++; return path + " handed over by " + by, nil }}
|
|
h.Asker = &asker.Client{Module: asks.EngineAsker("n1"), State: state,
|
|
Publish: func(subject string, body any, id string) error {
|
|
if a, ok := body.(asks.Ask); ok && subject == asks.AskSubject(asks.EngineAsker("n1")) {
|
|
asked = append(asked, a)
|
|
}
|
|
return nil
|
|
},
|
|
Record: func(string) (*stdio.StateEntry, error) { return nil, nil }}
|
|
return h, &asked, &done
|
|
}
|
|
|
|
// The controller names an access by its declared resource id exactly, which may hold a `/`: it reaches the act,
|
|
// which matches it against what was declared.
|
|
func TestATakeActNamesItsAccessByItsDeclaredId(t *testing.T) {
|
|
m, key := aMember(t)
|
|
var given []AccessAct
|
|
handle := func(a AccessAct) (string, error) { given = append(given, a); return "started", nil }
|
|
check := HandOverCheck{Signer: m.Signer, Node: m.Node, Nonces: &Nonces{}}
|
|
ask := HandOverAsk{Node: m.Node, Path: "/services/media/movies", By: "the operator",
|
|
Expires: time.Now().Add(HandOverGood), Nonce: aNonce + "08"}
|
|
id := "radarr.access-services/media/movies"
|
|
a := AnswerAccessAct(check, handle, signedAct(t, key, ask, AccessActAsk{Act: ActTake, Access: id, Warrant: "w-1"}))
|
|
if a.Refused != "" || len(given) != 1 || given[0].Access != id {
|
|
t.Fatalf("an access id with a slash did not reach the act as declared: %+v %+v", a, given)
|
|
}
|
|
}
|