Files
Suriyakumarvijayanayagam 4c750cb2ac Opening a shop is an API call; the broker learns of it in the same request
The last step of onboarding that needed a shell: provision site printed
a broker password and a person typed it into Mosquitto's passwd file on
the host - mounted read-only in the container, so the first attempt
failed silently and the password was re-rolled. No tenant could open a
second branch without us.

The server now drives Mosquitto's dynamic-security plugin over its own
broker login: POST /api/sites (owner) writes the row and the sealed
password, registers the login and a per-site role with literal topics
(the 2.0 plugin does not substitute %u - measured), and removes the row
again if the broker refuses, so a shop cannot exist in the database and
not on the broker. provision site goes through the same path. The
head-office Shops screen gets 'Open a new shop'.

broker-init converts the existing passwd file into the plugin's store
with every hash intact - PBKDF2-SHA512 both sides - so the cutover
re-claims no shop PC. Rehearsed locally: old logins keep working,
isolation holds, the health probe works, and a PC claiming a shop opened
through the API connects as that shop. run-local.sh now brings the
broker up the same way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-19 11:55:26 +05:30

70 lines
2.2 KiB
Go

package main
import (
"errors"
"flag"
"fmt"
"os"
"github.com/loyaly/behavision-server/internal/broker"
)
// broker-init converts Mosquitto's passwd file into the dynamic-security
// plugin's store, once, at cutover. After it the broker is driven over MQTT
// and the passwd and acl files are no longer read.
func runBrokerInit(args []string) error {
fs := flag.NewFlagSet("broker-init", flag.ExitOnError)
passwd := fs.String("passwd", "", "path to the mosquitto passwd file to convert")
out := fs.String("out", "", "where to write dynamic-security.json (must be writable by the broker)")
backend := fs.String("backend-user", "behavision-backend", "the server's own broker username; becomes the plugin admin")
health := fs.String("health-user", "health", "the healthcheck username")
fs.Usage = func() {
fmt.Fprintf(os.Stderr, `usage: behavision-server broker-init -passwd FILE -out FILE
Converts a mosquitto_passwd file into the dynamic-security plugin's store,
keeping every password hash exactly as it is, so no shop PC has to be
re-claimed. Then in mosquitto.conf replace password_file/acl_file with:
per_listener_settings false
plugin /usr/lib/mosquitto_dynamic_security.so
plugin_opt_config_file /mosquitto/data/dynamic-security.json
and restart the broker. From then on 'provision site' and POST /api/sites
register a shop's login themselves.
`)
fs.PrintDefaults()
}
if err := fs.Parse(args); err != nil {
return err
}
if *passwd == "" || *out == "" {
fs.Usage()
return errors.New("-passwd and -out are required")
}
f, err := os.Open(*passwd)
if err != nil {
return err
}
defer f.Close()
st, err := broker.FromPasswd(f, *backend, *health)
if err != nil {
return err
}
if _, err := os.Stat(*out); err == nil {
return fmt.Errorf("%s already exists - refusing to overwrite a live store", *out)
}
w, err := os.OpenFile(*out, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
if err != nil {
return err
}
defer w.Close()
if err := st.Encode(w); err != nil {
return err
}
fmt.Printf("wrote %s: %d clients, %d roles\n", *out, len(st.Clients), len(st.Roles))
for _, c := range st.Clients {
fmt.Printf(" %-28s %s\n", c.Username, c.Roles[0].Rolename)
}
return nil
}