The vault takes a value only from the controller, which it knows from the subject that names its caller. The grant is composed from the records, as the issued tools' is, so the installer's first user list is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
267 lines
11 KiB
Go
267 lines
11 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
|
|
// 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
|
|
// 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
|
|
// 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
|
|
}
|
|
|
|
// 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)}}
|
|
|
|
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})
|
|
// **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] {
|
|
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),
|
|
})
|
|
}
|
|
if runtimeHere {
|
|
var carried []Declared
|
|
for _, d := range r.Assigned[node] {
|
|
if d.RunsAs == "" {
|
|
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
|
|
}
|
|
|
|
// 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 {
|
|
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 VerbsTheControllerAsksTheVault {
|
|
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
|
|
}
|