A module's containers that declare identity run as an identity the controller allocates once, mesh-wide, from 50000-59999; each access names its users and whether its folder is taken; an access wait is excused for a folder the mesh may not keep yet, and the operator is asked once per folder and machine, the folder taken only on the warrant. All of it is sent only to an engine at contract 5. A placement for an access a newer build brings is judged against that build (issue 575). mesh-host pinned to the ADR 0325 branch head.
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"})
|
|
}
|