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
This commit is contained in:
2026-09-19 11:55:26 +05:30
parent 5f83a1077d
commit 4c750cb2ac
21 changed files with 1310 additions and 54 deletions

View File

@@ -1696,20 +1696,41 @@ The enrol response gained `client_slug` and `topic_prefix`, both **derived from
the broker username** rather than looked up separately, so the agent's topic
prefix and the broker's ACL are equal by construction.
### Still a command: creating the shop itself
### Opening a shop is an API call, and the broker learns of it in the same request
`provision site` prints a broker password that a human then has to add to
Mosquitto. So a tenant cannot open their second shop without us, and that is the
one remaining hole in self-service onboarding. Closing it needs a decision, not
code:
`POST /api/sites` (owner), and `provision site` behind the same code. This was
the last piece 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
— which turned out to be mounted read-only in the container, so the first
attempt failed silently and the password had to be re-rolled. No tenant could
open a second branch without us.
- **the server manages Mosquitto's `passwd`/`acl` and reloads it** — possible
because they are co-located, and it couples the API to the broker's
filesystem; or
- **one broker user per CLIENT rather than per site** — then adding a shop needs
no broker change at all. Cross-tenant isolation is unchanged; what is given up
is that one of a customer's own PCs could publish as another of their sites.
Every deployed site would need re-provisioning.
Neither option recorded here before was taken. The server does not write the
broker's files, and there is still one broker user per site. Mosquitto 2.0's
**dynamic-security plugin** takes the same operations as commands on
`$CONTROL/dynamic-security/v1`, from a client holding the `admin` role;
`server/internal/broker` drives it over the server's own broker login.
- **A role per site, with literal topics.** The 2.0 plugin does **not**
substitute `%u` in ACL topics (measured: the publish was denied), so
`site.<client>.<site>` is created with the client and deleted with it.
- **Idempotent.** Re-running `EnsureSite` on an existing login sets the password
to the one the database holds and confirms the role. `addClientRole` on a
client that already has the role answers "Internal error", so the role is
checked with `getClient` rather than inferred from prose.
- **The row and the login are created together, or not at all.** If the broker
refuses, the just-created row is removed and the caller gets 502. A shop that
exists in the database and not on the broker is one whose PC enrols fine and
never delivers a visit — the silent-failure class this whole endpoint ends.
- **Its own connection**, not the ingest client's: that one has
`SetOrderMatters` and blocking handlers, and a provisioning call must neither
wait behind a slow visit nor delay one.
- **Cutover keeps every password.** `behavision-server broker-init` converts the
passwd file into the plugin's store: `$7$` lines are PBKDF2-SHA512 with a
salt and iteration count, which is exactly what the plugin stores, so no shop
PC re-claims and no credential changes hands. Rehearsed locally against a
file `mosquitto_passwd` wrote; `run-local.sh` now brings the broker up the
same way as production.
## Running it against the real office camera: four dead wires