mqtt-topic served nothing: with two listens the mesh could not say which port a consumer dials, so a
consumer had to type 1883 into its config. It now serves the MQTT listener's port (the machine's,
once assigned) and the scheme, so `${bound:mqtt-topic:port}` fills.
The provisioner confined every consumer to `<as>/#`, which leaves nothing for the consumers the
broker exists for: Home Assistant discovers under homeassistant/# and tasmota/discovery/#, and
Node-RED's flows follow the devices' own topics. A consumer now contributes `topics` (MQTT topic
filters) to its mqtt-topic requirement and is granted exactly those; with none, its own subtree as
before. Settings merge into contributions, so an operator narrows a grant per assignment. The role
is brought to exactly the wanted ACLs (stale ones removed), `holds` checks the ACLs too, and an
invalid list is refused, never quietly narrowed. Only the role named for the consumer is touched:
a client carried from the predecessor's password file keeps its own.
73 lines
3.4 KiB
TypeScript
73 lines
3.4 KiB
TypeScript
// What a consumer of mqtt-topic is granted (topics.ts): its own subtree unless it contributed
|
|
// `topics`; a contributed list is granted exactly, refused whole when it is not topic filters; and
|
|
// the role is brought to exactly the wanted ACLs — missing ones added, stale ones removed — read from
|
|
// `mosquitto_ctrl dynsec getRole` as eclipse-mosquitto 2.1.2 prints it.
|
|
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { filterProblem, missingAcls, parseRoleAcls, staleAcls, topicFilters, wantedAcls } from "../topics.ts";
|
|
|
|
test("a consumer that contributed nothing gets its own subtree", () => {
|
|
assert.deepEqual(topicFilters({}, "mesh_ace_hass"), { ok: true, filters: ["mesh_ace_hass/#"], own: true });
|
|
assert.deepEqual(topicFilters(undefined, "x"), { ok: true, filters: ["x/#"], own: true });
|
|
// Settings merge into every contribution: keys that are not `topics` change nothing.
|
|
assert.deepEqual(topicFilters({ endpoints: { web: {} } }, "x"), { ok: true, filters: ["x/#"], own: true });
|
|
});
|
|
|
|
test("a contributed list is granted exactly, duplicates once", () => {
|
|
assert.deepEqual(topicFilters({ topics: ["#"] }, "x"), { ok: true, filters: ["#"], own: false });
|
|
assert.deepEqual(topicFilters({ topics: ["stat/+/POWER", "tele/#", "tele/#", "/octoprint/x"] }, "x"), {
|
|
ok: true,
|
|
filters: ["stat/+/POWER", "tele/#", "/octoprint/x"],
|
|
own: false,
|
|
});
|
|
});
|
|
|
|
test("a list that is not topic filters is refused whole", () => {
|
|
for (const topics of [[], "#", [""], ["a/#/b"], ["a#"], ["a/b+"], [42], ["a\u0000b"], {}]) {
|
|
const out = topicFilters({ topics } as Record<string, unknown>, "x");
|
|
assert.equal(out.ok, false, JSON.stringify(topics));
|
|
}
|
|
assert.equal(filterProblem("+/+/#"), undefined);
|
|
assert.equal(filterProblem("#"), undefined);
|
|
});
|
|
|
|
const GET_ROLE = `Warning: You are running mosquitto_ctrl without encryption.
|
|
This means all of the configuration changes you are making are visible on the network, including passwords.
|
|
|
|
Rolename: u1
|
|
ACLs: publishClientSend : allow : # (priority: 0)
|
|
subscribePattern : allow : u1/# (priority: 0)
|
|
publishClientReceive : deny : secret topic/with space (priority: -1)
|
|
`;
|
|
|
|
test("getRole's ACL lines are read, the warning and headings are not", () => {
|
|
assert.deepEqual(parseRoleAcls(GET_ROLE), [
|
|
{ type: "publishClientSend", allow: true, topic: "#" },
|
|
{ type: "subscribePattern", allow: true, topic: "u1/#" },
|
|
{ type: "publishClientReceive", allow: false, topic: "secret topic/with space" },
|
|
]);
|
|
assert.deepEqual(parseRoleAcls("Rolename: empty\nACLs:\n"), []);
|
|
});
|
|
|
|
test("the role is brought to exactly the wanted ACLs", () => {
|
|
const current = parseRoleAcls(GET_ROLE);
|
|
const wanted = wantedAcls(["u1/#"]);
|
|
assert.deepEqual(wanted, [
|
|
{ type: "publishClientSend", allow: true, topic: "u1/#" },
|
|
{ type: "publishClientReceive", allow: true, topic: "u1/#" },
|
|
{ type: "subscribePattern", allow: true, topic: "u1/#" },
|
|
]);
|
|
assert.deepEqual(missingAcls(current, wanted), [
|
|
{ type: "publishClientSend", allow: true, topic: "u1/#" },
|
|
{ type: "publishClientReceive", allow: true, topic: "u1/#" },
|
|
]);
|
|
assert.deepEqual(staleAcls(current, wanted), [
|
|
{ type: "publishClientSend", allow: true, topic: "#" },
|
|
{ type: "publishClientReceive", allow: false, topic: "secret topic/with space" },
|
|
]);
|
|
assert.deepEqual(staleAcls(wanted, wanted), []);
|
|
assert.deepEqual(missingAcls(wanted, wanted), []);
|
|
});
|