158 lines
6.3 KiB
Go
158 lines
6.3 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
|
|
// State is the state it keeps, each a bucket its instances write (novox/hq ADR 0202).
|
|
State []Bucket
|
|
// Reads are other modules' state it reads, each `<module>.<name>` (novox/hq ADR 0202).
|
|
Reads []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,
|
|
State: stateNames(d.State), Reads: d.Reads,
|
|
})
|
|
}
|
|
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
|
|
}
|