Production readiness hardening and ops tooling
This commit is contained in:
128
scripts/smoke-mqtt-acl.mjs
Normal file
128
scripts/smoke-mqtt-acl.mjs
Normal file
@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env node
|
||||
import mqtt from "mqtt";
|
||||
import "dotenv/config";
|
||||
|
||||
const brokerUrl = process.env.MQTT_BROKER_URL;
|
||||
const deviceA = {
|
||||
username: process.env.MQTT_TEST_DEVICE_A_USERNAME,
|
||||
password: process.env.MQTT_TEST_DEVICE_A_PASSWORD
|
||||
};
|
||||
const deviceB = {
|
||||
username: process.env.MQTT_TEST_DEVICE_B_USERNAME,
|
||||
password: process.env.MQTT_TEST_DEVICE_B_PASSWORD || process.env.MQTT_TEST_DEVICE_A_PASSWORD
|
||||
};
|
||||
const timeoutMs = Number(process.env.MQTT_ACL_SMOKE_TIMEOUT_MS || 8000);
|
||||
|
||||
function usage() {
|
||||
console.log(`Usage:
|
||||
MQTT_BROKER_URL=mqtts://broker:8883 \\
|
||||
MQTT_TEST_DEVICE_A_USERNAME=<device-a-id> \\
|
||||
MQTT_TEST_DEVICE_A_PASSWORD=<secret> \\
|
||||
MQTT_TEST_DEVICE_B_USERNAME=<device-b-id> \\
|
||||
npm run smoke:mqtt-acl
|
||||
|
||||
This validates Mosquitto ACL behavior from a device credential perspective:
|
||||
- device A can subscribe/read devices/A/downlink/#
|
||||
- device A can publish/write devices/A/uplink/#
|
||||
- device A is denied subscribe/read to devices/B/downlink/#
|
||||
`);
|
||||
}
|
||||
|
||||
if (!brokerUrl || !deviceA.username || !deviceA.password || !deviceB.username) {
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function connectDevice(device) {
|
||||
const client = mqtt.connect(brokerUrl, {
|
||||
username: device.username,
|
||||
password: device.password,
|
||||
clientId: `qris-acl-smoke-${device.username}-${Date.now()}`,
|
||||
reconnectPeriod: 0,
|
||||
connectTimeout: timeoutMs
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
client.end(true);
|
||||
reject(new Error(`connect timeout for ${device.username}`));
|
||||
}, timeoutMs);
|
||||
client.once("connect", () => {
|
||||
clearTimeout(timer);
|
||||
resolve(client);
|
||||
});
|
||||
client.once("error", (error) => {
|
||||
clearTimeout(timer);
|
||||
client.end(true);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function subscribe(client, topic) {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.subscribe(topic, { qos: 1 }, (error, grants) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(grants || []);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function publish(client, topic, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.publish(topic, payload, { qos: 1 }, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function granted(grants) {
|
||||
return grants.some((grant) => Number(grant.qos) !== 128);
|
||||
}
|
||||
|
||||
const client = await connectDevice(deviceA);
|
||||
try {
|
||||
const ownDownlink = `devices/${deviceA.username}/downlink/#`;
|
||||
const ownUplink = `devices/${deviceA.username}/uplink/acl-smoke`;
|
||||
const foreignDownlink = `devices/${deviceB.username}/downlink/#`;
|
||||
|
||||
const ownGrants = await subscribe(client, ownDownlink);
|
||||
if (!granted(ownGrants)) {
|
||||
throw new Error(`device ${deviceA.username} could not subscribe own downlink topic`);
|
||||
}
|
||||
|
||||
await publish(
|
||||
client,
|
||||
ownUplink,
|
||||
JSON.stringify({ type: "acl_smoke", device_id: deviceA.username, timestamp: new Date().toISOString() })
|
||||
);
|
||||
|
||||
const foreignGrants = await subscribe(client, foreignDownlink);
|
||||
if (granted(foreignGrants)) {
|
||||
throw new Error(`ACL allows ${deviceA.username} to subscribe foreign downlink ${foreignDownlink}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
broker_url: brokerUrl,
|
||||
device: deviceA.username,
|
||||
own_downlink: ownDownlink,
|
||||
own_uplink: ownUplink,
|
||||
foreign_downlink_denied: foreignDownlink
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
} finally {
|
||||
client.end(true);
|
||||
}
|
||||
Reference in New Issue
Block a user