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 (`.`). 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.`. 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://:`; 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://:)", 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-` 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)" }