mesh_store_reader held pg_read_all_data, so every caller of mesh-store.query and postgres_query could read every column the store holds: the signing key nodes trust, sealed secrets, password hashes. The reader now holds no role; each minute, on every database, its memberships are revoked and its grants brought to what the consumer's contribution lists under readable, minus any column or table whose name says it holds a secret. Before a statement runs, the reader's own session is asked which secret columns it can read and the statement is refused if any; a statement naming a known secret table is refused before it is sent.
104 lines
3.7 KiB
Go
104 lines
3.7 KiB
Go
// postgres-provider: postgres's code, one binary the node's runtime launches and speaks MCP to over
|
|
// stdio through the Go SDK (novox/hq ADR 0188, 0193). It serves postgres's tools and the mesh-store
|
|
// seat's verbs and, beside them, runs long: the provisioner that makes postgres the provider of the
|
|
// mesh `postgres-database` interface, and an audit line for each database it provisions or withdraws.
|
|
//
|
|
// stdout is the MCP channel; everything this module says, it says on stderr.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
func say(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, "[postgres] "+format+"\n", args...)
|
|
}
|
|
|
|
func main() {
|
|
pg, err := ClientFromEnv(os.Getenv)
|
|
if err != nil {
|
|
// Without the server there is nothing to serve and nothing to provision; said, not fatal,
|
|
// so the runtime does not restart a process that cannot do better.
|
|
say("%v; serving no tools and provisioning nothing", err)
|
|
if err := stdio.Serve("", nil); err != nil {
|
|
say("%v", err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
var h *Harness
|
|
if receives := os.Getenv("MESH_RECEIVES"); receives == "" {
|
|
say("MESH_RECEIVES is not set — the provisioner cannot run without it")
|
|
} else {
|
|
h = &Harness{
|
|
Resource: "postgres-database",
|
|
Receives: receives,
|
|
Adapter: provisioner{pg: pg, announce: announce},
|
|
Log: func(format string, args ...any) { fmt.Fprintf(os.Stderr, format+"\n", args...) },
|
|
// A consumer failed for minutes is said on the bus, where the controller hears it and
|
|
// `status` names it (novox/hq ADR 0224).
|
|
Announce: func(event string, body map[string]any) {
|
|
if err := stdio.Emit(event, body); err != nil {
|
|
say("emit %s failed: %v", event, err)
|
|
}
|
|
},
|
|
Node: os.Getenv("MESH_NODE"),
|
|
}
|
|
go h.Run(context.Background())
|
|
}
|
|
go audit()
|
|
// The store's reader is narrowed to what modules declare readable, at once and every minute, whether
|
|
// or not the provisioner runs: without a contributions file it is given nothing (novox/hq issue 337).
|
|
go pg.RunReader(context.Background(), os.Getenv("MESH_RECEIVES"), time.Minute, say)
|
|
// The retirement tools beside postgres's own: what is retired here, approving or rejecting what waits
|
|
// for a person, and deleting a retired consumer (novox/hq ADR 0230).
|
|
if err := stdio.Serve("", append(Tools(pg), RetirementTools(h)...)); err != nil {
|
|
say("%v", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// announce emits a lifecycle event without letting a broker hiccup fail the provisioning itself.
|
|
func announce(event string, body map[string]string) {
|
|
if err := stdio.Emit(event, body); err != nil {
|
|
say("emit %s failed: %v", event, err)
|
|
}
|
|
}
|
|
|
|
// audit keeps a line for each database granted and withdrawn — observability the provider is best
|
|
// placed to log. The events are its own, read back from its consumer (`consumes`).
|
|
func audit() {
|
|
subscribe := func() error {
|
|
return stdio.Subscribe("postgres.database.*", func(e stdio.Envelope) error {
|
|
var body struct {
|
|
Consumer string `json:"consumer"`
|
|
Database string `json:"database"`
|
|
}
|
|
_ = json.Unmarshal(e.Body, &body)
|
|
switch e.Key {
|
|
case "postgres.database.provisioned":
|
|
say("database provisioned for %s (db %s)", body.Consumer, body.Database)
|
|
case "postgres.database.deprovisioned":
|
|
say("database retired, kept (db %s)", body.Database)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
// Asked again until the runtime takes it: the first ask may come before Serve is running.
|
|
for wait := time.Second; ; wait = min(wait*2, time.Minute) {
|
|
if err := subscribe(); err == nil {
|
|
say("auditing database lifecycle events")
|
|
return
|
|
} else if wait >= 8*time.Second {
|
|
say("subscribing to the lifecycle events failed, will retry: %v", err)
|
|
}
|
|
time.Sleep(wait)
|
|
}
|
|
}
|