115 lines
5.3 KiB
Go
115 lines
5.3 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
)
|
|
|
|
// The take act: a shared folder that already holds data is taken by the mesh once, on the operator's word
|
|
// (novox/hq ADR 0325 §8).
|
|
//
|
|
// The controller asks the operator once for that folder on that machine (ADR 0259, at the level approve). On the
|
|
// warrant it asks the machine's node-engine, on the engine's own subject (broker.AskAccessActSubject), signed with
|
|
// the mesh's key as a hand-over is (SignedHandOver) under a signing context of its own (AccessActContext), to take
|
|
// the folder: the engine adds the entries of the users it was declared for that folder to everything under it —
|
|
// following no link, entering no other filesystem — answers at once that it started or why it refused, and states
|
|
// the walk on the access's verdict. **Nothing is taken without a warrant**: the ask names the warrant the operator
|
|
// gave, and the controller sends it only from the warrant (cmd/mesh-controller decideTake).
|
|
//
|
|
// The engine's own shape (mesh-host internal/link AccessActAsk, AnswerAccessAct): a hand-over's fields and
|
|
// {act: "take", access: <the access's resource id in the declaration>, warrant}.
|
|
|
|
// AccessActContext is prefixed to an act ask's bytes before signing: never another ask's signature, nor a
|
|
// declaration's. The engine holds the same words.
|
|
const AccessActContext = "novox-mesh access-act v2\n"
|
|
|
|
// ActTake is the one act the controller asks.
|
|
const ActTake = "take"
|
|
|
|
// AccessActAsk is what the controller asks: a hand-over's fields — the node it is for, the folder's absolute path as
|
|
// the declaration places it, who asked, when the ask stops being good and a nonce the engine takes once — and the
|
|
// act's own: which act, the access by its resource id, and the warrant the operator gave.
|
|
type AccessActAsk struct {
|
|
Node string `json:"node"`
|
|
Path string `json:"path"`
|
|
By string `json:"by"`
|
|
Expires time.Time `json:"expires"`
|
|
Nonce string `json:"nonce"`
|
|
Act string `json:"act"`
|
|
Access string `json:"access"`
|
|
Warrant string `json:"warrant"`
|
|
}
|
|
|
|
// MaxActField bounds an access id and a warrant, as the engine does.
|
|
const MaxActField = 200
|
|
|
|
// TakeWithin is how long the controller waits for the engine's answer: it starts the walk and answers, and the
|
|
// walk's end is stated on the access's verdict.
|
|
const TakeWithin = 30 * time.Second
|
|
|
|
// SignTake fills a take ask's act, expiry and nonce and signs it with the mesh's key over AccessActContext. It refuses
|
|
// an ask with no warrant, no path or no access — and one the engine would refuse for its shape: nothing is ever asked
|
|
// of an engine that it would have to refuse.
|
|
func SignTake(ctx context.Context, signer Signer, ask AccessActAsk) ([]byte, error) {
|
|
ask.Access, ask.Warrant = strings.TrimSpace(ask.Access), strings.TrimSpace(ask.Warrant)
|
|
switch {
|
|
case signer == nil:
|
|
return nil, errors.New("no signing key, so nothing can be asked of an engine")
|
|
case ask.Warrant == "":
|
|
return nil, errors.New("a take is asked only on the operator's warrant, and this one names none")
|
|
case ask.Node == "" || !strings.HasPrefix(ask.Path, "/"):
|
|
return nil, errors.New("a take names its machine and the folder's absolute path")
|
|
case ask.Access == "":
|
|
return nil, errors.New("a take names the access whose folder it takes")
|
|
}
|
|
// The warrant is one short name; the access is the resource id the declaration gave it, which may hold a path's
|
|
// slashes (catalogue.AccessID) — the engine judges it against what it was declared.
|
|
if len(ask.Warrant) > MaxActField || strings.ContainsAny(ask.Warrant, "\r\n\x00/") {
|
|
return nil, fmt.Errorf("the take's warrant %q is not one short name", ask.Warrant)
|
|
}
|
|
if len(ask.Access) > MaxActField || strings.ContainsAny(ask.Access, "\r\n\x00") {
|
|
return nil, fmt.Errorf("the take's access %q is not one line", ask.Access)
|
|
}
|
|
nonce := make([]byte, 16)
|
|
if _, err := rand.Read(nonce); err != nil {
|
|
return nil, err
|
|
}
|
|
ask.Act, ask.Nonce, ask.Expires = ActTake, hex.EncodeToString(nonce), time.Now().UTC().Add(HandOverGood)
|
|
raw, err := json.Marshal(ask)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
signature, err := signer.Sign(ctx, append([]byte(AccessActContext), raw...))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot sign the ask: %w", err)
|
|
}
|
|
return json.Marshal(SignedHandOver{Ask: raw, Signature: signature})
|
|
}
|
|
|
|
// AskTake asks one machine's node-engine to take a shared folder on the operator's warrant, and reads its answer. An
|
|
// error is the ask not reaching an engine, or an answer that is not one; a refusal is the engine's and comes back in
|
|
// the answer.
|
|
func AskTake(ctx context.Context, conn *nats.Conn, signer Signer, ask AccessActAsk, timeout time.Duration) (HandOverAnswer, error) {
|
|
if conn == nil {
|
|
return HandOverAnswer{}, errors.New("this controller is not on the bus")
|
|
}
|
|
body, err := SignTake(ctx, signer, ask)
|
|
if err != nil {
|
|
return HandOverAnswer{}, err
|
|
}
|
|
return askSigned(ctx, conn, broker.AskAccessActSubject(ask.Node), body, ask.Node, timeout, askWords{
|
|
what: "the take of a shared folder", nothing: "nothing was taken", issue: "ADR 0325",
|
|
unknown: "whether it started is not known — the access's verdict on the machine says whether it is kept",
|
|
record: "a start"})
|
|
}
|