Let a module declare a stream of its own and restrict who hears it by a trusted config value (hq issue 718, ADR 0363)
mesh-mail's events must live on a stream of their own and reach only the modules the operator granted an account (ADR 0359 §2, §4). The controller now creates a module's declared stream, publishes its events on mesh.stream.<module>.event.<event>, and composes a consumer's durable consumer, filters and bus grants on a restricted stream only where its consumes asks and the provider's trusted value names it; a consumer that loses its grant has its consumer removed on the next assertion. Nothing changes while no manifest declares a stream.
This commit is contained in:
@@ -61,12 +61,23 @@ func (i *Inventory) BusRecords(ctx context.Context) (broker.Records, error) {
|
||||
|
||||
out := broker.Records{Assigned: map[string][]broker.Declared{}, People: map[string][]string{},
|
||||
Interchangeable: map[string]bool{}}
|
||||
assigned := map[string][]string{}
|
||||
for _, n := range nodes {
|
||||
out.Nodes = append(out.Nodes, n.Name)
|
||||
modules, err := i.Assigned(ctx, n.Name)
|
||||
if err != nil {
|
||||
return broker.Records{}, fmt.Errorf("cannot read what %s runs: %w", n.Name, err)
|
||||
}
|
||||
assigned[n.Name] = modules
|
||||
}
|
||||
// What each restricting setting says on each machine running its module (novox/hq ADR 0363 §5): read
|
||||
// once, for every consumer composed below.
|
||||
grants, err := i.streamGrantValues(ctx, declared, nodes, assigned)
|
||||
if err != nil {
|
||||
return broker.Records{}, err
|
||||
}
|
||||
for _, n := range nodes {
|
||||
out.Nodes = append(out.Nodes, n.Name)
|
||||
modules := assigned[n.Name]
|
||||
for _, module := range modules {
|
||||
m, known := declared[module]
|
||||
if !known {
|
||||
@@ -89,6 +100,9 @@ func (i *Inventory) BusRecords(ctx context.Context) (broker.Records, error) {
|
||||
d.KeyedReads = append(d.KeyedReads, broker.KeyedRead{Read: sr.Read, Key: sr.Key})
|
||||
}
|
||||
d.Holds = heldHere(d.Holds, holdings, n.Name, module)
|
||||
// And the module streams it hears on this machine, as granted (novox/hq ADR 0363 §5): what a
|
||||
// stream carries whole leaves EVENTS' consumes.
|
||||
d.Hears, d.HearsAStream, d.Consumes = hearsOf(m, declared, n.Name, grants, d.Consumes)
|
||||
out.Assigned[n.Name] = append(out.Assigned[n.Name], d)
|
||||
if m.Instances == catalogue.InstancesInterchangeable {
|
||||
out.Interchangeable[m.Module] = true
|
||||
@@ -169,6 +183,8 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
|
||||
Reads: m.Reads,
|
||||
// And the logs it keeps (novox/hq ADR 0297).
|
||||
Logs: logsOf(m),
|
||||
// And the streams it owns (novox/hq ADR 0363).
|
||||
Streams: streamsOf(m),
|
||||
// And the tools its health asks (novox/hq ADR 0240): the machine's node-engine is granted them.
|
||||
Checks: catalogue.HealthChecks(m),
|
||||
// And whether it runs as an account of its own (novox/hq ADR 0259 §8).
|
||||
@@ -362,3 +378,101 @@ func (i *Inventory) DeclaredTrafficSeats(ctx context.Context) ([]broker.Seat, er
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// streamsOf is the streams a module owns, as the bus holds them (novox/hq ADR 0363).
|
||||
func streamsOf(m catalogue.Manifest) []broker.ModuleStream {
|
||||
var out []broker.ModuleStream
|
||||
for _, s := range m.Streams {
|
||||
events := make([]string, 0, len(s.Events))
|
||||
for _, e := range s.Events {
|
||||
events = append(events, broker.WirePattern(e))
|
||||
}
|
||||
out = append(out, broker.ModuleStream{Module: m.Module, Name: s.BusName(), Events: events,
|
||||
MaxAgeDays: s.Days(), MaxMiB: s.Cap(), Restricted: s.Grant != nil})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// DeclaredStreams is every module stream the catalogue declares, registered modules assigned or not: a
|
||||
// stream exists from registration, as a log does (novox/hq ADR 0363 §1).
|
||||
func (i *Inventory) DeclaredStreams(ctx context.Context) ([]broker.ModuleStream, error) {
|
||||
declared, err := i.Catalogue(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read the catalogue: %w", err)
|
||||
}
|
||||
var out []broker.ModuleStream
|
||||
for _, m := range declared {
|
||||
out = append(out, streamsOf(m)...)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// grantValues is each restricting setting's value on each machine running its module, by `<module>/<setting>`.
|
||||
type grantValues map[string][]any
|
||||
|
||||
// streamGrantValues reads, for every module owning a restricted stream, the setting that restricts it on every
|
||||
// machine the module runs on: the mesh's layer under that machine's, the machine's winning (novox/hq ADR 0363 §5).
|
||||
func (i *Inventory) streamGrantValues(ctx context.Context, declared map[string]catalogue.Manifest,
|
||||
nodes []Node, assigned map[string][]string) (grantValues, error) {
|
||||
out := grantValues{}
|
||||
for _, n := range nodes {
|
||||
for _, module := range assigned[n.Name] {
|
||||
m, known := declared[module]
|
||||
if !known {
|
||||
continue
|
||||
}
|
||||
var settings []string
|
||||
for _, s := range m.Streams {
|
||||
if s.Grant != nil && !slices.Contains(settings, s.Grant.Config) {
|
||||
settings = append(settings, s.Grant.Config)
|
||||
}
|
||||
}
|
||||
if len(settings) == 0 {
|
||||
continue
|
||||
}
|
||||
layers, err := i.SettingsFor(ctx, n.Name, module)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read %s's settings on %s, which grant who hears its stream: %w",
|
||||
module, n.Name, err)
|
||||
}
|
||||
for _, setting := range settings {
|
||||
if v, set := settingValue(layers, setting); set {
|
||||
out[module+"/"+setting] = append(out[module+"/"+setting], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// settingValue is one key's value over a machine's layers, the last that sets it winning. Pure.
|
||||
func settingValue(layers []catalogue.Layer, key string) (any, bool) {
|
||||
var value any
|
||||
set := false
|
||||
for _, l := range layers {
|
||||
if v, ok := l.Values[key]; ok {
|
||||
value, set = v, true
|
||||
}
|
||||
}
|
||||
return value, set
|
||||
}
|
||||
|
||||
// hearsOf is what a module hears of the module streams on one machine, and the consumes left for EVENTS
|
||||
// (novox/hq ADR 0363 §5). Pure.
|
||||
func hearsOf(m catalogue.Manifest, declared map[string]catalogue.Manifest, node string, grants grantValues,
|
||||
consumes []string) ([]broker.Heard, bool, []string) {
|
||||
reading := m
|
||||
reading.Consumes = consumes
|
||||
parts, touches, onEvents := catalogue.Hearing(reading, declared, node, func(provider, setting string) []any {
|
||||
return grants[provider+"/"+setting]
|
||||
})
|
||||
var out []broker.Heard
|
||||
for _, p := range parts {
|
||||
h := broker.Heard{Stream: p.Stream.BusName(), Emitter: p.Emitter}
|
||||
for _, pattern := range p.Patterns {
|
||||
h.Filters = append(h.Filters, broker.ModuleStreamSubject(p.Emitter, broker.WirePattern(pattern)))
|
||||
}
|
||||
out = append(out, h)
|
||||
}
|
||||
return out, touches, onEvents
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
|
||||
// A machine's layer wins over the mesh's for the setting that restricts a stream (novox/hq ADR 0363 §5).
|
||||
func TestARestrictingSettingIsReadMachineOverMesh(t *testing.T) {
|
||||
layers := []catalogue.Layer{
|
||||
{From: catalogue.MeshWideLayer, Values: map[string]any{"grants": "mesh", "other": 1}},
|
||||
{From: "novox", Values: map[string]any{"grants": "novox"}},
|
||||
}
|
||||
if v, set := settingValue(layers, "grants"); !set || v != "novox" {
|
||||
t.Fatalf("read %v, %v", v, set)
|
||||
}
|
||||
if _, set := settingValue(layers[:1], "absent"); set {
|
||||
t.Fatal("a key no layer sets read as set")
|
||||
}
|
||||
}
|
||||
|
||||
// What a consumer hears is composed into the bus's subjects, and what a stream carries leaves EVENTS.
|
||||
func TestWhatAModuleHearsIsComposedOntoTheStreamsSubjects(t *testing.T) {
|
||||
provider := catalogue.Manifest{Module: "mesh-mail", Streams: []catalogue.StreamDeclaration{{Name: "mail",
|
||||
Events: []string{"mail.*.message.added"}, Grant: &catalogue.StreamGrant{Config: "grants", Events: "mail.{key}.**"}}}}
|
||||
var value any
|
||||
_ = json.Unmarshal([]byte(`{"mesh":["reader"]}`), &value)
|
||||
grants := grantValues{"mesh-mail/grants": {value}}
|
||||
reader := catalogue.Manifest{Module: "reader"}
|
||||
hears, touches, onEvents := hearsOf(reader, map[string]catalogue.Manifest{"mesh-mail": provider}, "ace", grants,
|
||||
[]string{"mesh-mail.mail.**", "billing.paid"})
|
||||
if !touches || len(hears) != 1 || hears[0].Stream != "MAIL" ||
|
||||
!slices.Equal(hears[0].Filters, []string{"mesh.stream.mesh-mail.event.mail.mesh.message.added"}) {
|
||||
t.Fatalf("heard %+v", hears)
|
||||
}
|
||||
if !slices.Equal(onEvents, []string{"billing.paid"}) {
|
||||
t.Fatalf("left on EVENTS: %v", onEvents)
|
||||
}
|
||||
if got := streamsOf(provider); len(got) != 1 || got[0].Name != "MAIL" || got[0].MaxAgeDays != 7 || !got[0].Restricted {
|
||||
t.Fatalf("the provider's stream: %+v", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user