Prefer blocklist/passlist over blacklist/whitelist

This commit is contained in:
Koen Kanters
2020-07-15 23:22:32 +02:00
parent ee9aedd0f3
commit 67f02adbd0
8 changed files with 156 additions and 69 deletions
+15 -10
View File
@@ -24,22 +24,27 @@ class Availability extends Extension {
this.timers = {};
this.state = {};
this.blacklist = settings.get().advanced.availability_blacklist.map((e) => settings.getEntity(e).ID);
this.whitelist = settings.get().advanced.availability_whitelist.map((e) => settings.getEntity(e).ID);
this.blocklist = settings.get().advanced.availability_blocklist
.concat(settings.get().advanced.availability_blacklist)
.map((e) => settings.getEntity(e).ID);
this.passlist = settings.get().advanced.availability_passlist
.concat(settings.get().advanced.availability_whitelist)
.map((e) => settings.getEntity(e).ID);
}
inWhitelistOrNotInBlacklist(device) {
inPasslistOrNotInBlocklist(device) {
const ieeeAddr = device.ieeeAddr;
const deviceSettings = settings.getDevice(ieeeAddr);
const name = deviceSettings.friendlyName;
// Whitelist is not empty and device is in it, enable availability
if (this.whitelist.length > 0) {
return this.whitelist.includes(ieeeAddr) || (name && this.whitelist.includes(name));
// Passlist is not empty and device is in it, enable availability
if (this.passlist.length > 0) {
return this.passlist.includes(ieeeAddr) || (name && this.passlist.includes(name));
}
// Device is on blacklist, disable availability
if (this.blacklist.includes(ieeeAddr) || (name && this.blacklist.includes(name))) {
// Device is on blocklist, disable availability
if (this.blocklist.includes(ieeeAddr) || (name && this.blocklist.includes(name))) {
return false;
}
@@ -61,7 +66,7 @@ class Availability extends Extension {
const ieeeAddr = device.ieeeAddr;
this.publishAvailability(device, this.state.hasOwnProperty(ieeeAddr) ? this.state[ieeeAddr] : true, true);
if (this.inWhitelistOrNotInBlacklist(device)) {
if (this.inPasslistOrNotInBlocklist(device)) {
if (this.isPingable(device)) {
this.setTimerPingable(device);
} else {
@@ -166,7 +171,7 @@ class Availability extends Extension {
return;
}
if (this.inWhitelistOrNotInBlacklist(device)) {
if (this.inPasslistOrNotInBlocklist(device)) {
this.publishAvailability(data.device, true);
if (this.isPingable(device)) {
+11 -11
View File
@@ -250,24 +250,24 @@ class Bridge extends Extension {
const ID = typeof message === 'object' ? message.id : message.trim();
const entity = this.getEntity(entityType, ID);
let ban = false;
let block = false;
let force = false;
let banForceLog = '';
let blockForceLog = '';
if (entityType === 'device' && typeof message === 'object') {
ban = !!message.ban;
block = !!message.block;
force = !!message.force;
banForceLog = ` (ban: ${ban}, force: ${force})`;
blockForceLog = ` (block: ${block}, force: ${force})`;
} else if (entityType === 'group' && typeof message === 'object') {
force = !!message.force;
banForceLog = ` (force: ${force})`;
blockForceLog = ` (force: ${force})`;
}
try {
logger.info(`Removing ${entity.type} '${entity.settings.friendlyName}'${banForceLog}`);
logger.info(`Removing ${entity.type} '${entity.settings.friendlyName}'${blockForceLog}`);
if (entity.type === 'device') {
if (ban) {
settings.banDevice(entity.settings.ID);
if (block) {
settings.blockDevice(entity.settings.ID);
}
if (force) {
@@ -298,18 +298,18 @@ class Bridge extends Extension {
// Remove from state
this.state.remove(entity.settings.ID);
logger.info(`Successfully removed ${entity.type} '${entity.settings.friendlyName}'${banForceLog}`);
logger.info(`Successfully removed ${entity.type} '${entity.settings.friendlyName}'${blockForceLog}`);
if (entity.type === 'device') {
this.publishDevices();
return utils.getResponse(message, {id: ID, ban: ban, force: force}, null);
return utils.getResponse(message, {id: ID, block, force}, null);
} else {
this.publishGroups();
return utils.getResponse(message, {id: ID, force: force}, null);
}
} catch (error) {
throw new Error(
`Failed to remove ${entity.type} '${entity.settings.friendlyName}'${banForceLog} (${error})`,
`Failed to remove ${entity.type} '${entity.settings.friendlyName}'${blockForceLog} (${error})`,
);
}
}
+27 -1
View File
@@ -8,6 +8,9 @@ const Ajv = require('ajv');
const ajv = new Ajv({allErrors: true});
const defaults = {
passlist: [],
blocklist: [],
// Deprecated: use block/passlist
whitelist: [],
ban: [],
permit_join: false,
@@ -59,6 +62,9 @@ const defaults = {
// Availability timeout in seconds, disabled by default.
availability_timeout: 0,
availability_blocklist: [],
availability_passlist: [],
// Deprecated, use block/passlist
availability_blacklist: [],
availability_whitelist: [],
@@ -157,8 +163,11 @@ const schema = {
adapter: {type: 'string', enum: ['deconz', 'zstack']},
},
},
ban: {type: 'array', items: {type: 'string'}},
blocklist: {type: 'array', items: {type: 'string'}},
passlist: {type: 'array', items: {type: 'string'}},
// Deprecated: use block/passlist
whitelist: {type: 'array', items: {type: 'string'}},
ban: {type: 'array', items: {type: 'string'}},
experimental: {
type: 'object',
properties: {
@@ -187,6 +196,9 @@ const schema = {
last_seen: {type: 'string', enum: ['disable', 'ISO_8601', 'ISO_8601_local', 'epoch']},
elapsed: {type: 'boolean'},
availability_timeout: {type: 'number', minimum: 0},
availability_blocklist: {type: 'array', items: {type: 'string'}},
availability_passlist: {type: 'array', items: {type: 'string'}},
// Deprecated, use block/passlist
availability_blacklist: {type: 'array', items: {type: 'string'}},
availability_whitelist: {type: 'array', items: {type: 'string'}},
report: {type: 'boolean'},
@@ -363,6 +375,8 @@ function validate() {
checkAvailabilityList(settingsWithDefaults.advanced.availability_blacklist, 'availability_blacklist');
checkAvailabilityList(settingsWithDefaults.advanced.availability_whitelist, 'availability_whitelist');
checkAvailabilityList(settingsWithDefaults.advanced.availability_blocklist, 'availability_blocklist');
checkAvailabilityList(settingsWithDefaults.advanced.availability_passlist, 'availability_passlist');
return !valid ? validate.errors.map((v) => `${v.dataPath.substring(1)} ${v.message}`) : null;
}
@@ -536,6 +550,7 @@ function addDevice(ID) {
return getDevice(ID);
}
// Legacy: can be removed after bridgeLegacy has been removed
function whitelistDevice(ID) {
const settings = get();
if (!settings.whitelist) {
@@ -550,6 +565,16 @@ function whitelistDevice(ID) {
write();
}
function blockDevice(ID) {
const settings = get();
if (!settings.blocklist) {
settings.blocklist = [];
}
settings.blocklist.push(ID);
write();
}
function banDevice(ID) {
const settings = get();
if (!settings.ban) {
@@ -687,6 +712,7 @@ module.exports = {
getEntity,
whitelistDevice,
banDevice,
blockDevice,
addDevice,
removeDevice,
addGroup,
+19 -15
View File
@@ -71,14 +71,16 @@ class Zigbee extends events.EventEmitter {
logger.debug(`Zigbee network parameters: ${JSON.stringify(await this.herdsman.getNetworkParameters())}`);
for (const device of this.getClients()) {
// If a whitelist is used, all other device will be removed from the network.
if (settings.get().whitelist.length > 0) {
if (!settings.get().whitelist.includes(device.ieeeAddr)) {
logger.warn(`Blacklisted device is connected (${device.ieeeAddr}), removing...`);
// If a passlist is used, all other device will be removed from the network.
const passlist = settings.get().passlist.concat(settings.get().whitelist);
const blocklist = settings.get().blocklist.concat(settings.get().ban);
if (passlist.length > 0) {
if (!passlist.includes(device.ieeeAddr)) {
logger.warn(`Device which is not on passlist connected (${device.ieeeAddr}), removing...`);
device.removeFromNetwork();
}
} else if (settings.get().ban.includes(device.ieeeAddr)) {
logger.warn(`Banned device is connected (${device.ieeeAddr}), removing...`);
} else if (blocklist.includes(device.ieeeAddr)) {
logger.warn(`Device on blocklist is connected (${device.ieeeAddr}), removing...`);
device.removeFromNetwork();
}
}
@@ -250,21 +252,23 @@ class Zigbee extends events.EventEmitter {
}
acceptJoiningDeviceHandler(ieeeAddr) {
// If set whitelist devices, all other device will be rejected to join the network
if (settings.get().whitelist.length > 0) {
if (settings.get().whitelist.includes(ieeeAddr)) {
logger.info(`Accepting joining whitelisted device '${ieeeAddr}'`);
// If passlist is set, all devices not on passlist will be rejected to join the network
const passlist = settings.get().passlist.concat(settings.get().whitelist);
const blocklist = settings.get().blocklist.concat(settings.get().ban);
if (passlist.length > 0) {
if (passlist.includes(ieeeAddr)) {
logger.info(`Accepting joining device which is on passlist '${ieeeAddr}'`);
return true;
} else {
logger.info(`Rejecting joining non-whitelisted device '${ieeeAddr}'`);
logger.info(`Rejecting joining not in passlist device '${ieeeAddr}'`);
return false;
}
} else if (settings.get().ban.length > 0) {
if (settings.get().ban.includes(ieeeAddr)) {
logger.info(`Rejecting joining banned device '${ieeeAddr}'`);
} else if (blocklist.length > 0) {
if (blocklist.includes(ieeeAddr)) {
logger.info(`Rejecting joining device which is on blocklist '${ieeeAddr}'`);
return false;
} else {
logger.info(`Accepting joining non-banned device '${ieeeAddr}'`);
logger.info(`Accepting joining not in blocklist device '${ieeeAddr}'`);
return true;
}
} else {
+36 -8
View File
@@ -197,7 +197,21 @@ describe('Availability', () => {
expect(MQTT.publish.mock.calls.find((c) => c[0].includes('availability'))).toBeUndefined();
});
it('Should not ping devices blacklisted by friendly name', async () => {
it('Should not ping devices on blocklist by friendly name', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
settings.set(['advanced', 'availability_blocklist'], ['bulb_color'])
await controller.stop();
await flushPromises();
controller = new Controller();
await controller.start();
await flushPromises();
device.ping.mockClear();
jest.advanceTimersByTime(11 * 1000);
await flushPromises();
expect(device.ping).toHaveBeenCalledTimes(0);
});
it('Should not ping devices on blacklist by friendly name', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
settings.set(['advanced', 'availability_blacklist'], ['bulb_color'])
await controller.stop();
@@ -211,9 +225,9 @@ describe('Availability', () => {
expect(device.ping).toHaveBeenCalledTimes(0);
});
it('Should not ping devices blacklisted by IEEE address', async () => {
it('Should not ping devices on blocklist by IEEE address', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
settings.set(['advanced', 'availability_blacklist'], [device.ieeeAddr]);
settings.set(['advanced', 'availability_blocklist'], [device.ieeeAddr]);
await controller.stop();
await flushPromises();
controller = new Controller();
@@ -238,7 +252,21 @@ describe('Availability', () => {
expect(device.ping).toHaveBeenCalledTimes(1);
});
it('Should ping devices whitelisted by friendly name if availability_whitelist is set', async () => {
it('Should ping devices on passlist by friendly name if availability_passlist is set', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
settings.set(['advanced', 'availability_passlist'], ['bulb_color']);
await controller.stop();
await flushPromises();
controller = new Controller();
await controller.start();
await flushPromises();
device.ping.mockClear();
jest.advanceTimersByTime(11 * 1000);
await flushPromises();
expect(device.ping).toHaveBeenCalledTimes(1);
});
it('Should ping devices on whitelist by friendly name if availability_whitelist is set', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
settings.set(['advanced', 'availability_whitelist'], ['bulb_color']);
await controller.stop();
@@ -252,9 +280,9 @@ describe('Availability', () => {
expect(device.ping).toHaveBeenCalledTimes(1);
});
it('Should ping devices whitelisted by IEEE address if availability_whitelist is set', async () => {
it('Should ping devices on passlist by IEEE address if availability_passlist is set', async () => {
const device = zigbeeHerdsman.devices.bulb_color;
settings.set(['advanced', 'availability_whitelist'], [device.ieeeAddr]);
settings.set(['advanced', 'availability_passlist'], [device.ieeeAddr]);
await controller.stop();
await flushPromises();
controller = new Controller();
@@ -266,10 +294,10 @@ describe('Availability', () => {
expect(device.ping).toHaveBeenCalledTimes(1);
});
it('Should not ping non-whitelisted devices if availability_whitelist is set', async () => {
it('Should not ping devices not in passlist if availability_passlist is set', async () => {
const device = zigbeeHerdsman.devices.bulb;
getExtension().state[device.ieeeAddr] = false;
settings.set(['advanced', 'availability_whitelist'], ['0x000b57fffec6a5b3'])
settings.set(['advanced', 'availability_passlist'], ['0x000b57fffec6a5b3'])
await controller.stop();
await flushPromises();
controller = new Controller();
+9 -9
View File
@@ -233,10 +233,10 @@ describe('Bridge', () => {
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/remove',
JSON.stringify({"data":{"id": "bulb","ban":false,"force":false},"status":"ok"}),
JSON.stringify({"data":{"id": "bulb","block":false,"force":false},"status":"ok"}),
{retain: false, qos: 0}, expect.any(Function)
);
expect(settings.get().ban).toStrictEqual([]);
expect(settings.get().blocklist).toStrictEqual([]);
});
it('Should allow to remove device by object ID', async () => {
@@ -250,7 +250,7 @@ describe('Bridge', () => {
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/remove',
JSON.stringify({"data":{"id": "bulb","ban":false,"force":false},"status":"ok"}),
JSON.stringify({"data":{"id": "bulb","block":false,"force":false},"status":"ok"}),
{retain: false, qos: 0}, expect.any(Function)
);
});
@@ -266,25 +266,25 @@ describe('Bridge', () => {
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/remove',
JSON.stringify({"data":{"id": "bulb","ban":false,"force":true},"status":"ok"}),
JSON.stringify({"data":{"id": "bulb","block":false,"force":true},"status":"ok"}),
{retain: false, qos: 0}, expect.any(Function)
);
});
it('Should allow to ban device', async () => {
it('Should allow to block device', async () => {
const device = zigbeeHerdsman.devices.bulb;
MQTT.publish.mockClear();
MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', JSON.stringify({id: "bulb", ban: true, force: true}));
MQTT.events.message('zigbee2mqtt/bridge/request/device/remove', JSON.stringify({id: "bulb", block: true, force: true}));
await flushPromises();
expect(device.removeFromDatabase).toHaveBeenCalledTimes(1);
expect(settings.getDevice('bulb')).toBeNull();
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), expect.any(Object), expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/remove',
JSON.stringify({"data":{"id": "bulb","ban":true,"force":true},"status":"ok"}),
JSON.stringify({"data":{"id": "bulb","block":true,"force":true},"status":"ok"}),
{retain: false, qos: 0}, expect.any(Function)
);
expect(settings.get().ban).toStrictEqual(["0x000b57fffec6a5b2"]);
expect(settings.get().blocklist).toStrictEqual(["0x000b57fffec6a5b2"]);
});
it('Should allow to remove group', async () => {
@@ -337,7 +337,7 @@ describe('Bridge', () => {
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/remove',
JSON.stringify({"data":{},"status":"error","error":"Failed to remove device 'bulb' (ban: false, force: false) (Error: device timeout)"}),
JSON.stringify({"data":{},"status":"error","error":"Failed to remove device 'bulb' (block: false, force: false) (Error: device timeout)"}),
{retain: false, qos: 0}, expect.any(Function)
);
});
+28 -12
View File
@@ -159,6 +159,14 @@ describe('Controller', () => {
expect(controller.state.state).toStrictEqual({});
});
it('Should remove device not on passlist on startup', async () => {
settings.set(['passlist'], [zigbeeHerdsman.devices.bulb_color.ieeeAddr]);
await controller.start();
await flushPromises();
expect(zigbeeHerdsman.devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(0);
expect(zigbeeHerdsman.devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(1);
});
it('Should remove non whitelisted devices on startup', async () => {
settings.set(['whitelist'], [zigbeeHerdsman.devices.bulb_color.ieeeAddr]);
await controller.start();
@@ -167,6 +175,14 @@ describe('Controller', () => {
expect(zigbeeHerdsman.devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(1);
});
it('Should remove device on blocklist on startup', async () => {
settings.set(['blocklist'], [zigbeeHerdsman.devices.bulb_color.ieeeAddr]);
await controller.start();
await flushPromises();
expect(zigbeeHerdsman.devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(1);
expect(zigbeeHerdsman.devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(0);
});
it('Should remove banned devices on startup', async () => {
settings.set(['ban'], [zigbeeHerdsman.devices.bulb_color.ieeeAddr]);
await controller.start();
@@ -292,48 +308,48 @@ describe('Controller', () => {
expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bridge/log", '{"type":"device_connected","message":{"friendly_name":"bulb"}}', {"retain": false, qos: 0}, expect.any(Function));
});
it('acceptJoiningDeviceHandler reject banned device', async () => {
it('acceptJoiningDeviceHandler reject device on blocklist', async () => {
await controller.start();
const device = zigbeeHerdsman.devices.bulb;
settings.set(['ban'], [device.ieeeAddr]);
settings.set(['blocklist'], [device.ieeeAddr]);
const handler = zigbeeHerdsman.constructor.mock.calls[0][0].acceptJoiningDeviceHandler;
expect(await handler(device.ieeeAddr)).toBe(false);
});
it('acceptJoiningDeviceHandler accept not banned device', async () => {
it('acceptJoiningDeviceHandler accept device not on blocklist', async () => {
await controller.start();
const device = zigbeeHerdsman.devices.bulb;
settings.set(['ban'], ['123']);
settings.set(['blocklist'], ['123']);
const handler = zigbeeHerdsman.constructor.mock.calls[0][0].acceptJoiningDeviceHandler;
expect(await handler(device.ieeeAddr)).toBe(true);
});
it('acceptJoiningDeviceHandler accept whitelisted device', async () => {
it('acceptJoiningDeviceHandler accept device on passlist', async () => {
await controller.start();
const device = zigbeeHerdsman.devices.bulb;
settings.set(['whitelist'], [device.ieeeAddr]);
settings.set(['passlist'], [device.ieeeAddr]);
const handler = zigbeeHerdsman.constructor.mock.calls[0][0].acceptJoiningDeviceHandler;
expect(await handler(device.ieeeAddr)).toBe(true);
});
it('acceptJoiningDeviceHandler reject non-whitelisted device', async () => {
it('acceptJoiningDeviceHandler reject device not in passlist', async () => {
await controller.start();
const device = zigbeeHerdsman.devices.bulb;
settings.set(['whitelist'], ['123']);
settings.set(['passlist'], ['123']);
const handler = zigbeeHerdsman.constructor.mock.calls[0][0].acceptJoiningDeviceHandler;
expect(await handler(device.ieeeAddr)).toBe(false);
});
it('acceptJoiningDeviceHandler should prefer whitelist above ban', async () => {
it('acceptJoiningDeviceHandler should prefer passlist above blocklist', async () => {
await controller.start();
const device = zigbeeHerdsman.devices.bulb;
settings.set(['whitelist'], [device.ieeeAddr]);
settings.set(['ban'], [device.ieeeAddr]);
settings.set(['passlist'], [device.ieeeAddr]);
settings.set(['blocklist'], [device.ieeeAddr]);
const handler = zigbeeHerdsman.constructor.mock.calls[0][0].acceptJoiningDeviceHandler;
expect(await handler(device.ieeeAddr)).toBe(true);
});
it('acceptJoiningDeviceHandler accept when no ban and whitelist', async () => {
it('acceptJoiningDeviceHandler accept when not on blocklist and passlist', async () => {
await controller.start();
const device = zigbeeHerdsman.devices.bulb;
const handler = zigbeeHerdsman.constructor.mock.calls[0][0].acceptJoiningDeviceHandler;
+11 -3
View File
@@ -528,17 +528,17 @@ describe('Settings', () => {
}).toThrowError('MQTT retention requires protocol version 5');
});
it('Should not allow non-existing entities in availability blacklist', () => {
it('Should not allow non-existing entities in availability_blocklist', () => {
write(configurationFile, {
devices: {'0x0017880104e45519': {friendly_name: 'tain'}},
advanced: {availability_blacklist: ['0x0017880104e45519', 'non_existing']},
advanced: {availability_blocklist: ['0x0017880104e45519', 'non_existing']},
});
settings._reRead();
expect(() => {
settings.validate();
}).toThrowError(`Non-existing entity 'non_existing' specified in 'availability_blacklist'`);
}).toThrowError(`Non-existing entity 'non_existing' specified in 'availability_blocklist'`);
});
it('Should ban devices', () => {
@@ -549,6 +549,14 @@ describe('Settings', () => {
expect(settings.get().ban).toStrictEqual(['0x123', '0x1234']);
});
it('Should add devices to blocklist', () => {
write(configurationFile, {});
settings.blockDevice('0x123');
expect(settings.get().blocklist).toStrictEqual(['0x123']);
settings.blockDevice('0x1234');
expect(settings.get().blocklist).toStrictEqual(['0x123', '0x1234']);
});
it('Should throw error when yaml file is invalid', () => {
fs.writeFileSync(configurationFile, `
good: 9