Where the node-tools module is assigned, the machine's bus user list gains one principal of kind node-tools in place of that module's own: it may subscribe every carried module's tool namespace and every held seat's verbs on its node, read and follow every membership on its node, call any tool anywhere, answer what it is asked — and consume nothing, because tools are what it runs. Every other module keeps its own principal, so a module still serving tools from its container holds its own credential until it moves. Named exactly as the module it stands for, so `module issue` and `rollout mint` deliver its credential through the path a module's already takes, into node-tools' own `broker` secret. The runtime module's name is one constant in each of the broker and catalogue packages, held to one string by the agreement test, because a rule turns on it.
153 lines
6.0 KiB
Go
153 lines
6.0 KiB
Go
package broker
|
|
|
|
import (
|
|
"fmt"
|
|
"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
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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}}
|
|
|
|
for _, node := range sortedCopy(r.Nodes) {
|
|
out = append(out, Principal{Kind: KindNode, Node: node})
|
|
// **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
|
|
}
|
|
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,
|
|
})
|
|
}
|
|
if runtimeHere {
|
|
out = append(out, Principal{
|
|
Kind: KindNodeTools, Node: node, Module: RuntimeModule,
|
|
Carries: append([]Declared(nil), r.Assigned[node]...),
|
|
})
|
|
}
|
|
}
|
|
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
|
|
}
|