A literal * in a published subject matches the grant's *, so in one shared bucket a node could read every node's files. Each node now has declaration-files-<node>, which its account reads by name and the controller writes; the sweep judges each bucket against that node's latest declaration, under the node's lock with the send, and an object the sweep marked is put again by the next send. Putting that fails send after send raises a condition.
409 lines
18 KiB
Go
409 lines
18 KiB
Go
package broker
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
)
|
|
|
|
// Every user the composed file should contain, derived from what the mesh knows.
|
|
//
|
|
// **The list is derived, never kept.** A stored user list would be a second account of who may
|
|
// reach the bus, able to disagree with the records it came from — and the disagreement would be
|
|
// invisible, because both would look internally consistent. So this is a pure function of the
|
|
// mesh's records, run again every time the file is written.
|
|
//
|
|
// Records are mirrored into this package's own types rather than imported from the catalogue, for
|
|
// the reason DeclaredSeat is: composing authority is a different job from parsing a manifest, and
|
|
// this package stays free of the other's types so a change to a manifest field cannot quietly widen
|
|
// a permission.
|
|
|
|
// Declared is one module on one node, as composing its authority needs it.
|
|
type Declared struct {
|
|
Module string
|
|
Emits []string
|
|
Consumes []string
|
|
Serves []string
|
|
// Holds are the seats this module claims, with the protocol each seat declares. A seat the
|
|
// mesh defines for itself declares no protocol, so holding one grants nothing on the bus —
|
|
// which is right: those seats are about who does a job, not about who may say what.
|
|
Holds []Seat
|
|
// Uses are the seats this module sends to.
|
|
Uses []Seat
|
|
// Issued are the tools that answer a credential its software issued, one per offer that says so (novox/hq
|
|
// ADR 0305): the controller is granted to ask each in its own name, and no other tool so.
|
|
Issued []string
|
|
// Watches are the seats whose events it consumes.
|
|
Watches []Seat
|
|
// Invokes are the tools it calls, `<module>.<tool>` or `*` (novox/hq ADR 0152).
|
|
Invokes []string
|
|
// Waits are the event families a session may wait on through the tool runner, the tool runner's module's
|
|
// alone (novox/hq ADR 0367 §2): its account subscribes them for a call, through no consumer.
|
|
Waits []Wait
|
|
// State is the state it keeps, each a bucket its instances write (novox/hq ADR 0201).
|
|
State []Bucket
|
|
// Logs are the logs it keeps, each a stream its instances append to (novox/hq ADR 0297).
|
|
Logs []Log
|
|
// Streams are the streams it owns, each its events' one place (novox/hq ADR 0363).
|
|
Streams []ModuleStream
|
|
// Queues are its own work-queue streams and the one process each is the consumer of (novox/hq ADR 0436
|
|
// §1, §2, §6): one durable consumer per entry, named `<node>_<module>_<process id>`.
|
|
Queues []QueueOwner
|
|
// Hears are the module streams it hears on this machine, each with the subjects it was granted
|
|
// (novox/hq ADR 0363 §5); HearsAStream says its `consumes` names a module that owns a stream, granted
|
|
// or not, so its membership is issued again when a grant changes.
|
|
Hears []Heard
|
|
HearsAStream bool
|
|
// Reads are other modules' state it reads, each `<module>.<name>` (novox/hq ADR 0201).
|
|
Reads []string
|
|
// KeyedReads are keys of other modules' state it reads, each one key alone: what a seat's holder is
|
|
// granted for the pieces the modules beside it offer (novox/hq ADR 0260).
|
|
KeyedReads []KeyedRead
|
|
// NoAccount says the module declares no own secret named broker, so no account could ever be
|
|
// delivered to it and nothing can connect as it (novox/hq issue 195). Said in the negative so a
|
|
// record that does not say is composed as it always was.
|
|
NoAccount bool
|
|
// SnapshotsTheBus says the module holds mesh-broker — it is the bus — and so is the one module
|
|
// granted the snapshot API, to copy the bus's streams for the night's backup (novox/hq ADR 0235).
|
|
SnapshotsTheBus bool
|
|
// Checks are the module's own tools its health asks, each `<module>.<tool>` (novox/hq ADR 0240, to-be
|
|
// 48 §3): the machine's node-engine asks them of its own node tools, and is granted that and no more.
|
|
Checks []string
|
|
// Loads says the module has a compiled bundle whose entrypoints the machine's tool runtime loads and runs
|
|
// (novox/hq ADR 0175; issue 1329): a helper that reaches the bus on the runner's account, so the runner
|
|
// must carry the module although it declares no tools.
|
|
Loads bool
|
|
// RunsAs is the account the module runs as in a runtime of its own (novox/hq ADR 0259 §8): it is never
|
|
// carried by the machine's runtime, and reaches the bus on its own account.
|
|
RunsAs string
|
|
// ProvisionsNode says the module runs the machine's provisioner client: it claims NodeProvisionerSeat and
|
|
// declares the own secret ProvisionerSecret its account is delivered in (novox/hq ADR 0349 clause 11, design 57
|
|
// P3). The client is given a bus account of its own, never the module's or the tool runner's.
|
|
ProvisionsNode bool
|
|
// Processes are this module's own processes that declare bus (novox/hq ADR 0436 §1), each getting a
|
|
// principal of its own: granted exactly what its bus says, and nothing of this module's other grants —
|
|
// composed whether or not the module itself has an account (NoAccount), because a process account is
|
|
// delivered in its own secret, broker-<process id>, never the module's `broker`.
|
|
Processes []ProcessPrincipal
|
|
}
|
|
|
|
// ProcessPrincipal is one process of a module that declares bus (novox/hq ADR 0436 §1, §2), as the broker
|
|
// composes its own account from: its own emits, its own work-queue consumers, its own state, whether it
|
|
// reads the router's records of its module's own asks, and the tools and seat verbs it calls.
|
|
type ProcessPrincipal struct {
|
|
// Process is the process resource's id, naming the account <node>.<module>.<process id>.
|
|
Process string
|
|
// Emits are the events this process may publish, named as the module's own Emits are.
|
|
Emits []string
|
|
// Queues are the module's own work-queue streams this process is the one consumer of.
|
|
Queues []QueueOwner
|
|
// State is the module's own state, by local name, this process reads and writes.
|
|
State []string
|
|
// Asks says whether this process reads the records of its module's own asks (novox/hq ADR 0259 §3), on
|
|
// whichever seats the module uses that keep them (the router's, operator-channel, among others).
|
|
Asks bool
|
|
// Invokes are the tools and seat verbs this process calls, each <module>.<tool> or a role's
|
|
// seat:<seat>.<verb>, or * for every tool on the mesh.
|
|
Invokes []string
|
|
}
|
|
|
|
// Records is what composing a user list needs to know about the mesh, and nothing more.
|
|
type Records struct {
|
|
// Nodes is every machine the mesh knows. Each gets a host user.
|
|
Nodes []string
|
|
// Assigned is the modules on each node, as they declare themselves.
|
|
Assigned map[string][]Declared
|
|
// Enrolling is every node with a live token — one enrolment user each, because the inbox an
|
|
// answer goes to is scoped to the token and a shared one is one machine reading another's
|
|
// sealed credentials (design 25 §6).
|
|
Enrolling []string
|
|
// People is each person's name against the tools they may invoke, `*` for an administrator.
|
|
People map[string][]string
|
|
// Interchangeable is each module whose definition says its instances are the same anywhere
|
|
// (ADR 0160), which decides whether the module's plain subject is issued to every instance.
|
|
Interchangeable map[string]bool
|
|
// RootFree is each machine judged root-free when this was composed (novox/hq ADR 0259 §8): it names an
|
|
// account agents run as, judged unable to become root by its node-engine, and serves no login shell
|
|
// execute. A machine absent is not free: no record of a pass is no pass.
|
|
RootFree map[string]bool
|
|
}
|
|
|
|
// Users is every user the composed file should contain, in the order it will be written.
|
|
//
|
|
// The controller is always first and always present: a mesh whose own controller is not in the file
|
|
// is a mesh that cannot be told anything, and there is no state of the records in which that is
|
|
// correct.
|
|
func Users(r Records) ([]Principal, error) {
|
|
out := []Principal{{Kind: KindController, Uses: asksSeatOf(r), AsksIssued: issuedOf(r), AsksTheVault: vaultVerbsOf(r),
|
|
AsksTheConfig: servedOf(r, VerbsTheControllerAsksTheConfig), FilesFor: sortedCopy(r.Nodes)}}
|
|
|
|
for _, node := range sortedCopy(r.Nodes) {
|
|
witness := false
|
|
var checks []string
|
|
for _, d := range r.Assigned[node] {
|
|
if d.Module == controllerModule {
|
|
witness = true
|
|
}
|
|
checks = append(checks, d.Checks...)
|
|
}
|
|
out = append(out, Principal{Kind: KindNode, Node: node, WitnessesController: witness, Checks: checks,
|
|
MovesIntoTheVault: servesMoveIn(r), StatesItsKeyToTheVault: servesVerb(r, VaultNodeKey)})
|
|
// **And the machine's provisioner client, on an account of its own** (novox/hq ADR 0349 clause 11): one
|
|
// per machine that runs it, delivered to the module running it.
|
|
for _, d := range r.Assigned[node] {
|
|
if d.ProvisionsNode {
|
|
out = append(out, Principal{Kind: KindProvisioner, Node: node, Module: d.Module,
|
|
BoundBuckets: boundBucketsOf(r)})
|
|
break
|
|
}
|
|
}
|
|
// **Where the runtime is assigned, the machine gets one runtime principal in place of the
|
|
// runtime module's own** (novox/hq ADR 0175, to-be 38). It carries every module on the
|
|
// node: its serving grants are the union of theirs. Every other module keeps its own
|
|
// principal — a module still serving tools from its own container holds its own
|
|
// credential until it moves, and the two serve side by side in the meantime.
|
|
runtimeHere := false
|
|
for _, d := range r.Assigned[node] {
|
|
if d.Module == RuntimeModule {
|
|
runtimeHere = true
|
|
}
|
|
}
|
|
for _, d := range r.Assigned[node] {
|
|
// **A process account is its own, delivered in its own secret** (broker-<process id>, novox/hq
|
|
// ADR 0436 §1, §2), never the module's `broker` — so it is composed whether or not the module
|
|
// itself has an account, and is never carried by the machine's runtime, which runs as the
|
|
// operator's account (ADR 0436 §5): added here, before either of those is judged.
|
|
for _, pr := range d.Processes {
|
|
out = append(out, Principal{
|
|
Kind: KindProcess, Node: node, Module: d.Module, Process: pr.Process,
|
|
Emits: pr.Emits, Queues: pr.Queues, State: pr.State, Uses: d.Uses, Asks: pr.Asks,
|
|
Invokes: pr.Invokes,
|
|
})
|
|
}
|
|
if runtimeHere && d.Module == RuntimeModule {
|
|
continue
|
|
}
|
|
// **A module with nowhere to read an account is no user** (novox/hq issue 195). `module
|
|
// issue` refuses it one (issue 078: an account nothing reads is an orphan), so its user
|
|
// could only ever be left out of the file for want of a password — and every such module
|
|
// was named, on every status and plan, as a credential the mesh had not minted. Where the
|
|
// runtime is, it speaks for the module; where it is not, the module cannot speak at all,
|
|
// and a user would not change that.
|
|
if d.NoAccount {
|
|
continue
|
|
}
|
|
out = append(out, Principal{
|
|
Kind: KindModule, Node: node, Module: d.Module,
|
|
Emits: d.Emits, Consumes: d.Consumes, Serves: d.Serves,
|
|
Holds: d.Holds, Uses: d.Uses, Watches: d.Watches, Invokes: d.Invokes,
|
|
State: stateNames(d.State), Reads: d.Reads, SnapshotsTheBus: d.SnapshotsTheBus,
|
|
PerMachine: perMachineNames(d.State), KeyedReads: d.KeyedReads,
|
|
Logs: logNames(d.Logs), Streams: d.Streams, Hears: ComposedHears(d), Queues: d.Queues,
|
|
})
|
|
// **The vault's holder asks the desks and the issuing providers itself** (issue 723), under the
|
|
// account its own runtime runs on (ADR 0259 §8's runs-as): the one module granted it.
|
|
if holdsSeat(d, VaultSeat) {
|
|
out[len(out)-1].AsksTheDesk = true
|
|
out[len(out)-1].AsksIssued = issuedOf(r)
|
|
}
|
|
}
|
|
if runtimeHere {
|
|
var carried []Declared
|
|
for _, d := range r.Assigned[node] {
|
|
if d.RunsAs == "" && runnerServes(d) {
|
|
// Never a restricted stream for what the tool runner carries (novox/hq ADR 0376 §1).
|
|
d.Hears = ComposedHears(d)
|
|
carried = append(carried, d)
|
|
}
|
|
}
|
|
out = append(out, Principal{Kind: KindNodeTools, Node: node, Module: RuntimeModule, Carries: carried})
|
|
}
|
|
}
|
|
for _, node := range sortedCopy(r.Enrolling) {
|
|
out = append(out, Principal{Kind: KindEnrolment, Node: node})
|
|
}
|
|
for _, person := range sortedNames(r.People) {
|
|
out = append(out, Principal{Kind: KindPerson, Module: person, Invokes: r.People[person]})
|
|
}
|
|
|
|
// Refused here rather than discovered by the server. Two users with one name is a file the
|
|
// server reads as one of them, and which one depends on the order — so a module assigned to a
|
|
// node twice, or a person named after nothing, is a composition that must not be written.
|
|
seen := map[string]string{}
|
|
for _, p := range out {
|
|
name := p.Username()
|
|
if name == "" || name == "." {
|
|
return nil, fmt.Errorf("a %s user has no name, so nothing could authenticate as it", p.Kind)
|
|
}
|
|
if first, already := seen[name]; already {
|
|
return nil, fmt.Errorf(
|
|
"two users would be called %q (a %s and a %s): the server would read the file as "+
|
|
"one of them, and which one depends on the order", name, first, p.Kind)
|
|
}
|
|
seen[name] = string(p.Kind)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// WithPasswords fills each user's hash from what the mesh minted, and says which users have none.
|
|
//
|
|
// **Separated from Users because they fail differently.** A user missing from the records is a bug
|
|
// in deriving them; a user with no password is a step that has not happened yet — a module assigned
|
|
// but never given a credential, a node enrolled before this existed. The second is ordinary and its
|
|
// remedy is to mint one, so it is named rather than returned as an error, and the caller decides
|
|
// whether a partial composition is worth writing.
|
|
func WithPasswords(principals []Principal, hashes map[string]string) (filled []Principal, missing []string) {
|
|
for _, p := range principals {
|
|
hash, ok := hashes[p.Username()]
|
|
if !ok || hash == "" {
|
|
missing = append(missing, p.Username())
|
|
continue
|
|
}
|
|
p.PasswordHash = hash
|
|
filled = append(filled, p)
|
|
}
|
|
return filled, missing
|
|
}
|
|
|
|
func sortedCopy(in []string) []string {
|
|
out := append([]string(nil), in...)
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func sortedNames(in map[string][]string) []string {
|
|
out := make([]string, 0, len(in))
|
|
for k := range in {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// controllerModule is the controller's module: the machine assigned it witnesses its upgrades.
|
|
const controllerModule = "mesh-controller"
|
|
|
|
// asksSeatOf is the seat an ask is made on, as its holder declares it (novox/hq ADR 0259): the controller
|
|
// asks the operator through it like any other user, and is granted what its declaration names for a caller.
|
|
// None while nothing holds it.
|
|
// issuedOf is every tool of a module assigned anywhere that answers a credential its software issued, as
|
|
// `<module>.<tool>` (novox/hq ADR 0305): what the controller asks in its own name, and nothing beside.
|
|
func issuedOf(r Records) []string {
|
|
seen := map[string]bool{}
|
|
var out []string
|
|
for _, node := range sortedCopy(r.Nodes) {
|
|
for _, d := range r.Assigned[node] {
|
|
for _, t := range d.Issued {
|
|
if key := d.Module + "." + t; !seen[key] {
|
|
seen[key] = true
|
|
out = append(out, key)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
func asksSeatOf(r Records) []Seat {
|
|
for _, node := range sortedCopy(r.Nodes) {
|
|
for _, d := range r.Assigned[node] {
|
|
for _, s := range d.Holds {
|
|
if s.Name == AsksSeat && namesVerb(s.ByCaller, "ask") {
|
|
seat := s
|
|
seat.Kind, seat.Capabilities = "", nil
|
|
return []Seat{seat}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// VaultSeat is the vault's seat: its holder alone asks the desks and the issuing providers in its own name.
|
|
const VaultSeat = "mesh-vault"
|
|
|
|
func holdsSeat(d Declared, seat string) bool {
|
|
for _, s := range d.Holds {
|
|
if s.Name == seat {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// vaultVerbsOf are the vault's intake verbs its holder serves, as the records say (issue 675): what the controller
|
|
// is granted to ask it in its own name. None while no holder serves them.
|
|
func vaultVerbsOf(r Records) []string { return servedOf(r, VerbsTheControllerAsksTheVault) }
|
|
|
|
// servedOf are the verbs of asked that a seat's holder serves, as the records say, by name.
|
|
func servedOf(r Records, asked []SeatVerb) []string {
|
|
seen := map[string]bool{}
|
|
var out []string
|
|
for _, node := range sortedCopy(r.Nodes) {
|
|
for _, d := range r.Assigned[node] {
|
|
for _, s := range d.Holds {
|
|
for _, v := range asked {
|
|
if s.Name == v.Seat && slices.Contains(s.Serves, v.Verb) && !seen[v.Verb] {
|
|
seen[v.Verb] = true
|
|
out = append(out, v.Verb)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// servesMoveIn says the vault's holder serves move-in, as the records say (issue 676): what lets each machine's
|
|
// node-engine ask it in its own name.
|
|
func servesMoveIn(r Records) bool { return servesVerb(r, VaultMoveIn) }
|
|
|
|
// servesVerb says a holder of verb's seat serves it, as the records say.
|
|
func servesVerb(r Records, verb SeatVerb) bool {
|
|
for _, node := range r.Nodes {
|
|
for _, d := range r.Assigned[node] {
|
|
for _, s := range d.Holds {
|
|
if s.Name == verb.Seat && slices.Contains(s.Serves, verb.Verb) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// boundBucketsOf is every provider's bucket of entries, `<module>_bound` (mesh-sdk go/provider, issue 678): the state
|
|
// named bound of every module placed anywhere, once each, in name order. A consumer on any machine may be bound to
|
|
// it, so each machine's provisioner client reads its own keys of each (issue 679).
|
|
func boundBucketsOf(r Records) []string {
|
|
seen := map[string]bool{}
|
|
for _, mods := range r.Assigned {
|
|
for _, d := range mods {
|
|
for _, b := range d.State {
|
|
if b.Name == BoundState {
|
|
seen[b.Bucket()] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
out := make([]string, 0, len(seen))
|
|
for b := range seen {
|
|
out = append(out, b)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// runnerServes says the machine's tool runner has something of this module to serve (novox/hq ADR 0436 §1,
|
|
// ADR 0437 §4, issue 1329): tools or loads it answers, a seat it holds, event families a session waits on,
|
|
// a bundle the runtime loads (Loads), or a check its health asks. A module with none of these is a process of its own on its own credential, and
|
|
// carrying it would hand every session on the machine the runner's account with that module's state, seat
|
|
// traffic and streams. A module with no account of its own (NoAccount) is spoken for by the runner, so it
|
|
// stays carried, and so does the runtime module itself. Whether the module has its own broker secret is not
|
|
// the test: a module with tools and its own credential is still carried (ADR 0175).
|
|
func runnerServes(d Declared) bool {
|
|
return d.Module == RuntimeModule || d.NoAccount || d.Loads || len(d.Serves) > 0 || len(d.Holds) > 0 || len(d.Waits) > 0 || len(d.Checks) > 0
|
|
}
|