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"` }