245 lines
11 KiB
Go
245 lines
11 KiB
Go
package buscompose
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// **The accounts the grants part wants, and the one shape a held account takes** (novox/hq ADR 0561 §1, §2; issue 774).
|
|
//
|
|
// The bus composer derives every user from placement, the manifests and the wants. For each account the mesh mints (a
|
|
// module's, a module's process's, a machine's provisioner client's), the grants part writes one key of its state
|
|
// `accounts`, keyed by the bus user, holding the account's fields that are not secret: what the controller's credential
|
|
// carries today beside the password. The vault watches that state, mints the user it holds no value for, and keeps the
|
|
// value as the module's own secret on that node, built by HeldCredential, which gives byte for byte the controller's
|
|
// shape, so a runtime reads it unchanged. The state is read by the vault alone (NamedReaders), as `bus-accounts` is read
|
|
// by the grants part alone.
|
|
//
|
|
// The node accounts, the controller's own, an enrolment user and a person are never wanted here: the vault mints none
|
|
// of them (ADR 0561 §4, §5).
|
|
|
|
// AccountsState is the grants part's state of wanted accounts, by bus user.
|
|
const AccountsState = "accounts"
|
|
|
|
// GrantsAccounts is that state as a reader names it (`<owner>.<name>`).
|
|
const GrantsAccounts = GrantsModule + "." + AccountsState
|
|
|
|
// AccountsBucket is the bucket the state lives in.
|
|
var AccountsBucket = BucketName(GrantsModule, AccountsState)
|
|
|
|
// AccountsSubject is the subject of one wanted account's key: `$KV.mesh-grants_accounts.<user>`.
|
|
func AccountsSubject(user string) string { return "$KV." + AccountsBucket + "." + user }
|
|
|
|
// AccountIssuedEvent is what the vault says when it minted an account: AccountIssued, never a value or a hash.
|
|
const AccountIssuedEvent = "account-issued"
|
|
|
|
// AccountIssuedSubject is where it says it, as the runtime addresses mesh-vault's events.
|
|
const AccountIssuedSubject = "mesh.mod." + VaultSeat + ".event." + AccountIssuedEvent
|
|
|
|
// ClaimedSeat is one seat a module claims, as its runtime needs it (the controller's seatClaimed): the name, the scope (a
|
|
// node-scoped seat's verb carries the machine) and the verbs the runtime serves for it.
|
|
type ClaimedSeat struct {
|
|
Seat string `json:"seat"`
|
|
Scope string `json:"scope"`
|
|
Serves []string `json:"serves,omitempty"`
|
|
}
|
|
|
|
// WantedAccount is one key of the grants part's state `accounts`: an account the mesh needs, without its password.
|
|
type WantedAccount struct {
|
|
User string `json:"user"`
|
|
// Kind is KindModule (KindNodeTools for the runtime, named as a module is), KindProcess or KindProvisioner.
|
|
Kind Kind `json:"kind"`
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
// Process is a KindProcess account's process id.
|
|
Process string `json:"process,omitempty"`
|
|
// URL is the bus's address as that node reaches it, `nats://<host>:<port>`; Fingerprint the bus certificate's.
|
|
URL string `json:"url"`
|
|
Fingerprint string `json:"fingerprint,omitempty"`
|
|
// Claims are a module's claims with the verbs each seat promises: none for a process or a provisioner client.
|
|
Claims []ClaimedSeat `json:"claims,omitempty"`
|
|
// At is when the grants part wrote the key: not compared (Same).
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
// mintedKinds are the kinds of account the vault mints (ADR 0561 §1).
|
|
var mintedKinds = []Kind{KindModule, KindNodeTools, KindProcess, KindProvisioner}
|
|
|
|
// principal is the account as the composer names it.
|
|
func (w WantedAccount) principal() Principal {
|
|
return Principal{Kind: w.Kind, Node: w.Node, Module: w.Module, Process: w.Process}
|
|
}
|
|
|
|
// Valid says why w is not an account the vault may mint, or nil: a kind it mints, every name a subject token, the user
|
|
// the one the composer derives from them, and an address to reach the bus at.
|
|
func (w WantedAccount) Valid() error {
|
|
if !slices.Contains(mintedKinds, w.Kind) {
|
|
return fmt.Errorf("%s is a %s account, which the vault never mints (ADR 0561 §4)", w.User, w.Kind)
|
|
}
|
|
for what, v := range map[string]string{"node": w.Node, "module": w.Module} {
|
|
if !safeSubject.MatchString(v) {
|
|
return fmt.Errorf("%s: the %s %q is not a name the mesh writes", w.User, what, v)
|
|
}
|
|
}
|
|
if w.Kind == KindProcess && !safeSubject.MatchString(w.Process) {
|
|
return fmt.Errorf("%s: the process %q is not a name the mesh writes", w.User, w.Process)
|
|
}
|
|
if w.Kind != KindProcess && w.Process != "" {
|
|
return fmt.Errorf("%s: only a process's account names a process", w.User)
|
|
}
|
|
if want := w.principal().Username(); w.User != want {
|
|
return fmt.Errorf("%s is not the user of a %s account of %s on %s (that is %s)", w.User, w.Kind, w.Module, w.Node, want)
|
|
}
|
|
if !strings.HasPrefix(w.URL, "nats://") || len(w.URL) == len("nats://") {
|
|
return fmt.Errorf("%s names no bus address (url, nats://<host>:<port>)", w.User)
|
|
}
|
|
if len(w.Claims) > 0 && w.Kind != KindModule && w.Kind != KindNodeTools {
|
|
return fmt.Errorf("%s: a %s account holds no seat, so it carries no claims", w.User, w.Kind)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Same says whether two wanted accounts want the same account: every field but when it was written.
|
|
func (w WantedAccount) Same(o WantedAccount) bool {
|
|
a, b := w, o
|
|
a.At, b.At = time.Time{}, time.Time{}
|
|
ra, _ := json.Marshal(a)
|
|
rb, _ := json.Marshal(b)
|
|
return string(ra) == string(rb)
|
|
}
|
|
|
|
// OwnSecret is the module's own secret the account is held in: `broker`, `broker-<process id>` or `provisioner-broker`
|
|
// (the controller's names, ADR 0436 §2, ADR 0349 §11).
|
|
func (w WantedAccount) OwnSecret() string {
|
|
switch w.Kind {
|
|
case KindProcess:
|
|
return "broker-" + w.Process
|
|
case KindProvisioner:
|
|
return "provisioner-broker"
|
|
}
|
|
return "broker"
|
|
}
|
|
|
|
// ReadWantedAccount reads the key user of the state: refused when it is not a valid wanted account of that user.
|
|
func ReadWantedAccount(user string, raw []byte) (WantedAccount, error) {
|
|
var w WantedAccount
|
|
if err := json.Unmarshal(raw, &w); err != nil {
|
|
return WantedAccount{}, fmt.Errorf("%s %s is not a wanted account: %w", GrantsAccounts, user, err)
|
|
}
|
|
if w.User != user {
|
|
return WantedAccount{}, fmt.Errorf("%s %s names the user %q", GrantsAccounts, user, w.User)
|
|
}
|
|
return w, w.Valid()
|
|
}
|
|
|
|
// AccountIssued is the vault's account-issued: which account it minted, and its generation. Never a value or a hash.
|
|
type AccountIssued struct {
|
|
User string `json:"user"`
|
|
Kind Kind `json:"kind"`
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
Process string `json:"process,omitempty"`
|
|
Generation int `json:"generation"`
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
// HeldCredential is the value the account's holder reads, exactly as the controller builds it today (issueWith,
|
|
// sealProcess and sealProvisionerClient): a module's `{url, fingerprint, node, module, user, password, claims}`, a
|
|
// process's without claims, a provisioner client's without module or claims; an empty fingerprint or claims left out.
|
|
func HeldCredential(w WantedAccount, password string) ([]byte, error) {
|
|
if err := w.Valid(); err != nil {
|
|
return nil, err
|
|
}
|
|
if password == "" {
|
|
return nil, fmt.Errorf("%s: an account is held with its password", w.User)
|
|
}
|
|
switch w.Kind {
|
|
case KindProcess:
|
|
return json.Marshal(struct {
|
|
URL string `json:"url"`
|
|
Fingerprint string `json:"fingerprint,omitempty"`
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
User string `json:"user"`
|
|
Password string `json:"password"`
|
|
}{URL: w.URL, Fingerprint: w.Fingerprint, Node: w.Node, Module: w.Module, User: w.User, Password: password})
|
|
case KindProvisioner:
|
|
return json.Marshal(struct {
|
|
URL string `json:"url"`
|
|
Fingerprint string `json:"fingerprint,omitempty"`
|
|
Node string `json:"node"`
|
|
User string `json:"user"`
|
|
Password string `json:"password"`
|
|
}{URL: w.URL, Fingerprint: w.Fingerprint, Node: w.Node, User: w.User, Password: password})
|
|
}
|
|
return json.Marshal(struct {
|
|
URL string `json:"url"`
|
|
Fingerprint string `json:"fingerprint,omitempty"`
|
|
Node string `json:"node"`
|
|
Module string `json:"module"`
|
|
User string `json:"user"`
|
|
Password string `json:"password"`
|
|
Claims []ClaimedSeat `json:"claims,omitempty"`
|
|
}{URL: w.URL, Fingerprint: w.Fingerprint, Node: w.Node, Module: w.Module, User: w.User, Password: password,
|
|
Claims: w.Claims})
|
|
}
|
|
|
|
// **One issuer at every moment: minting turns on at the lease write** (novox/hq ADR 0561 §3, §6). The controller's lease
|
|
// is the one key `holder` of its bucket (LeaseBucket). The operator's credential hands it away by writing a value that
|
|
// names its new holder in `handed-to`; while the key holds that, the vault mints and the grants part creates the durable
|
|
// consumers an account needs. A controller's value, a key deleted or expired, or one that cannot be read or parsed, turn
|
|
// both off: when in doubt nothing is minted, so the controller and the vault never both mint one account. No clock and
|
|
// no setting turns it.
|
|
|
|
// ControllerLease is the lease's bucket as a reader names it; the vault and the grants part alone read it
|
|
// (NamedReaders), the controller's announcement marking it readable by nobody.
|
|
const ControllerLease = ControllerSeat + ".lease"
|
|
|
|
// ControllerLeaseKey is the lease's one key.
|
|
const ControllerLeaseKey = leaseKey
|
|
|
|
// LeaseHolder is the lease key's value: a controller instance's (mesh-controller internal/lease Holder), or, once handed
|
|
// away, one naming its holder in HandedTo.
|
|
type LeaseHolder struct {
|
|
Instance string `json:"instance,omitempty"`
|
|
Host string `json:"host,omitempty"`
|
|
Epoch uint64 `json:"epoch,omitempty"`
|
|
Taken time.Time `json:"taken,omitzero"`
|
|
Renewed time.Time `json:"renewed,omitzero"`
|
|
// HandedTo is who holds the lease once the controller handed it away: never a controller instance.
|
|
HandedTo string `json:"handed-to,omitempty"`
|
|
// HandedBy is the credential that wrote it (the operator's), and Why its reason.
|
|
HandedBy string `json:"handed-by,omitempty"`
|
|
Why string `json:"why,omitempty"`
|
|
}
|
|
|
|
// LeaseHandedAway reads the lease key as a getter answered it (its value, whether it was there, the read's error) and
|
|
// says whether it is handed to a holder that is no controller, with why either way.
|
|
func LeaseHandedAway(raw []byte, found bool, readErr error) (bool, string) {
|
|
switch {
|
|
case readErr != nil:
|
|
return false, "the controller's lease cannot be read (" + readErr.Error() + "), so nothing is minted"
|
|
case !found || len(raw) == 0 || string(raw) == "null":
|
|
return false, "no lease is written (expired, deleted, or never taken): a controller may take it, so nothing is minted"
|
|
}
|
|
var h LeaseHolder
|
|
if err := json.Unmarshal(raw, &h); err != nil {
|
|
return false, "the controller's lease is not readable as a holder (" + err.Error() + "), so nothing is minted"
|
|
}
|
|
if strings.TrimSpace(h.HandedTo) == "" {
|
|
who := h.Instance
|
|
if who == "" {
|
|
who = "an instance that names itself not"
|
|
}
|
|
return false, "the controller holds the lease (" + who + "): it mints, so the vault mints nothing (ADR 0561 §3)"
|
|
}
|
|
if h.Instance != "" {
|
|
return false, "the lease names both a controller instance (" + h.Instance + ") and a holder it is handed to (" +
|
|
h.HandedTo + "), so nothing is minted"
|
|
}
|
|
return true, "the lease is handed to " + h.HandedTo + ": the vault mints every new account (ADR 0561 §1, §3)"
|
|
}
|