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 } // 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 } // 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}) for _, d := range r.Assigned[node] { 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, }) } } 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 }