From 2281a61e30fb8f58d2fabb0d721277286bc80252 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Wed, 4 Aug 2021 10:55:33 -0700 Subject: [PATCH] Completed device inactive auto-remove feature. --- db.js | 105 +++++++++++++++++++++++++++++++++ meshcentral-config-schema.json | 2 +- meshcentral.js | 4 +- meshuser.js | 6 ++ 4 files changed, 114 insertions(+), 3 deletions(-) diff --git a/db.js b/db.js index a42c1ec5..bee66b06 100644 --- a/db.js +++ b/db.js @@ -112,6 +112,111 @@ module.exports.CreateDB = function (parent, func) { } } + // Remove inactive devices + obj.removeInactiveDevices = function () { + // Get a list of domains and what their inactive device removal setting is + var removeInactiveDevicesPerDomain = {}, minRemoveInactiveDevicesPerDomain = {}, minRemoveInactiveDevice = 9999; + for (var i in parent.config.domains) { + if (typeof parent.config.domains[i].autoremoveinactivedevices == 'number') { + var v = parent.config.domains[i].autoremoveinactivedevices; + if ((v > 1) && (v <= 2000)) { + if (v < minRemoveInactiveDevice) { minRemoveInactiveDevice = v; } + removeInactiveDevicesPerDomain[i] = v; + minRemoveInactiveDevicesPerDomain[i] = v; + } + } + } + + // Check if any device groups have a inactive device removal setting + for (var i in parent.webserver.meshes) { + if (typeof parent.webserver.meshes[i].expireDevs == 'number') { + var v = parent.webserver.meshes[i].expireDevs; + if ((v > 1) && (v <= 2000)) { + if (v < minRemoveInactiveDevice) { minRemoveInactiveDevice = v; } + if ((minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] == null) || (minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] > v)) { + minRemoveInactiveDevicesPerDomain[parent.webserver.meshes[i].domain] = v; + } + } else { + delete parent.webserver.meshes[i].expireDevs; + } + } + } + + // If there are no such settings for any domain, we can exit now. + if (minRemoveInactiveDevice == 9999) return; + const now = Date.now(); + + // For each domain with a inactive device removal setting, get a list of last device connections + for (var domainid in minRemoveInactiveDevicesPerDomain) { + obj.GetAllTypeNoTypeField('lastconnect', domainid, function (err, docs) { + if ((err != null) || (docs == null)) return; + for (var j in docs) { + const days = (now - docs[j].time) / 86400000; // Calculate the number of inactive days + var remove = false; + if (removeInactiveDevicesPerDomain[docs[j].domain] && (removeInactiveDevicesPerDomain[docs[j].domain] < days)) { remove = true; } + else { const mesh = parent.webserver.meshes[docs[j].meshid]; if (mesh && (typeof mesh.expireDevs == 'number') && (mesh.expireDevs < days)) { remove = true; } } + if (remove) { + // Check if this device is connected right now + const nodeid = docs[j]._id.substring(2); + const conn = parent.GetConnectivityState(nodeid); + if (conn == null) { + // Remove the device + obj.Get(nodeid, function (err, docs) { + if ((err != null) || (docs == null) || (docs.length != 1)) return; + const node = docs[0]; + + // Delete this node including network interface information, events and timeline + obj.Remove(node._id); // Remove node with that id + obj.Remove('if' + node._id); // Remove interface information + obj.Remove('nt' + node._id); // Remove notes + obj.Remove('lc' + node._id); // Remove last connect time + obj.Remove('si' + node._id); // Remove system information + obj.Remove('al' + node._id); // Remove error log last time + if (obj.RemoveSMBIOS) { obj.RemoveSMBIOS(node._id); } // Remove SMBios data + obj.RemoveAllNodeEvents(node._id); // Remove all events for this node + obj.removeAllPowerEventsForNode(node._id); // Remove all power events for this node + if (typeof node.pmt == 'string') { obj.Remove('pmt_' + node.pmt); } // Remove Push Messaging Token + obj.Get('ra' + node._id, function (err, nodes) { + if ((nodes != null) && (nodes.length == 1)) { obj.Remove('da' + nodes[0].daid); } // Remove diagnostic agent to real agent link + obj.Remove('ra' + node._id); // Remove real agent to diagnostic agent link + }); + + // Remove any user node links + if (node.links != null) { + for (var i in node.links) { + if (i.startsWith('user/')) { + var cuser = parent.webserver.users[i]; + if ((cuser != null) && (cuser.links != null) && (cuser.links[node._id] != null)) { + // Remove the user link & save the user + delete cuser.links[node._id]; + if (Object.keys(cuser.links).length == 0) { delete cuser.links; } + obj.SetUser(cuser); + + // Notify user change + var targets = ['*', 'server-users', cuser._id]; + var event = { etype: 'user', userid: cuser._id, username: cuser.name, action: 'accountchange', msgid: 86, msgArgs: [cuser.name], msg: 'Removed user device rights for ' + cuser.name, domain: node.domain, account: parent.webserver.CloneSafeUser(cuser) }; + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user. Another event will come. + parent.DispatchEvent(targets, obj, event); + } + } + } + } + + // Event node deletion + var meshname = '(unknown)'; + if ((parent.webserver.meshes[node.meshid] != null) && (parent.webserver.meshes[node.meshid].name != null)) { meshname = parent.webserver.meshes[node.meshid].name; } + var event = { etype: 'node', action: 'removenode', nodeid: node._id, msgid: 87, msgArgs: [node.name, meshname], msg: 'Removed device ' + node.name + ' from device group ' + meshname, domain: node.domain }; + // TODO: We can't use the changeStream for node delete because we will not know the meshid the device was in. + //if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to remove the node. Another event will come. + parent.DispatchEvent(parent.webserver.CreateNodeDispatchTargets(node.meshid, node._id), obj, event); + }); + } + } + } + }); + } + } + obj.cleanup = function (func) { // TODO: Remove all mesh links to invalid users // TODO: Remove all meshes that dont have any links diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index e13ff842..63680046 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -295,7 +295,7 @@ "footer": { "type": "string", "default": null, "description": "This is a HTML string displayed at the bottom of the web page when a user is logged in." }, "loginfooter": { "type": "string", "default": null, "description": "This is a HTML string displayed at the bottom of the web page when a user is not logged in." }, "guestDeviceSharing": { "type": "boolean", "default": true, "description": "When set to false, the desktop/terminal sharing link feature is not available." }, - "autoRemoveInactiveDevices": { "type": "integer", "default": 0, "description": "Number of days a device can be inactive before it's removed. 0 disables this feature. Device group setting will override this value." }, + "autoRemoveInactiveDevices": { "type": "integer", "default": 0, "minimum": 0, "maximum": 2000, "description": "Number of days a device can be inactive before it's removed. 0 disables this feature. Device group setting will override this value." }, "altMessenging": { "type": "object", "properties": { diff --git a/meshcentral.js b/meshcentral.js index 8c3e1f75..888d735f 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -2129,7 +2129,7 @@ function CreateMeshCentralServer(config, args) { obj.DispatchEvent(obj.webserver.CreateNodeDispatchTargets(meshid, nodeid), obj, { action: 'nodeconnect', meshid: meshid, nodeid: nodeid, domain: nodeid.split('/')[1], conn: state.connectivity, pwr: state.powerState, ct: connectTime, nolog: 1, nopeers: 1 }); // Save indication of node connection change - const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 1, connectType: connectType, serverid: obj.serverId }; + const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 1, connectType: connectType }; if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; } obj.db.Set(lc); } @@ -2200,7 +2200,7 @@ function CreateMeshCentralServer(config, args) { state.connectivity -= connectType; // Save indication of node connection change - const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 0, connectType: connectType, serverid: obj.serverId }; + const lc = { _id: 'lc' + nodeid, type: 'lastconnect', domain: nodeid.split('/')[1], meshid: meshid, time: Date.now(), cause: 0, connectType: connectType }; if (extraInfo && extraInfo.remoteaddrport) { lc.addr = extraInfo.remoteaddrport; } obj.db.Set(lc); diff --git a/meshuser.js b/meshuser.js index 21585771..e1178ed0 100644 --- a/meshuser.js +++ b/meshuser.js @@ -5508,6 +5508,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use 'nodeconfig': [serverUserCommandNodeConfig, ""], 'print': [serverUserCommandPrint, ""], 'relays': [serverUserCommandRelays, ""], + 'removeinactivedevices': [serverUserCommandRemoveInactiveDevices, ""], 'resetserver': [serverUserCommandResetServer, "Causes the server to reset, this is sometimes useful is the config.json file was changed."], 'serverupdate': [serverUserCommandServerUpdate, "Updates server to latest version. Optional version argument to install specific version. Example: serverupdate 0.8.49"], 'setmaxtasks': [serverUserCommandSetMaxTasks, ""], @@ -6231,6 +6232,11 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (cmdData.result == '') { cmdData.result = 'No relays.'; } } + function serverUserCommandRemoveInactiveDevices(cmdData) { + parent.db.removeInactiveDevices(); + cmdData.result = 'Ok'; + } + function serverUserCommandAutoBackup(cmdData) { var backupResult = parent.db.performBackup(function (msg) { try { ws.send(JSON.stringify({ action: 'serverconsole', value: msg, tag: cmdData.command.tag })); } catch (ex) { }