From 0c445a473ecd6ea89065951dfdd76a7a2b82492b Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Mon, 1 Jul 2019 14:44:26 -0700 Subject: [PATCH] More improvements to MeshCtrl --- MeshCentralServer.njsproj | 1 + meshctrl.js | 111 ++++++++++++++++++++----- meshuser.js | 167 ++++++++++++++++++++++---------------- 3 files changed, 189 insertions(+), 90 deletions(-) diff --git a/MeshCentralServer.njsproj b/MeshCentralServer.njsproj index 989e10d6..c019d68c 100644 --- a/MeshCentralServer.njsproj +++ b/MeshCentralServer.njsproj @@ -96,6 +96,7 @@ + diff --git a/meshctrl.js b/meshctrl.js index 0d6d49fb..63d42387 100644 --- a/meshctrl.js +++ b/meshctrl.js @@ -3,7 +3,7 @@ const crypto = require('crypto'); var settings = {}; const args = require('minimist')(process.argv.slice(2)); -const possibleCommands = ['listusers', 'listdevicegroups', 'serverinfo', 'userinfo', 'adduser', 'removeuser', 'adddevicegroup', 'removedevicegroup', 'broadcast']; +const possibleCommands = ['listusers', 'listdevicegroups', 'serverinfo', 'userinfo', 'adduser', 'removeuser', 'adddevicegroup', 'removedevicegroup', 'broadcast', 'addusertodevicegroup', 'removeuserfromdevicegroup']; //console.log(args); if (args['_'].length == 0) { @@ -11,24 +11,26 @@ if (args['_'].length == 0) { console.log("Information at: https://meshcommander.com/meshcentral"); console.log("No action specified, use MeshCtrl like this:\r\n\r\n meshctrl [action] [arguments]\r\n"); console.log("Supported actions:"); - console.log(" Help [action] - Get help on an action."); - console.log(" ServerInfo - Show server information."); - console.log(" UserInfo - Show user information."); - console.log(" ListUsers - List user accounts."); - console.log(" ListDeviceGroups - List device groups."); - console.log(" AddUser - Create a new user account."); - console.log(" RemoveUser - Delete a user account."); - console.log(" AddDeviceGroup - Create a new device group."); - console.log(" RemoveDeviceGroup - Delete a device group."); - console.log(" Broadcast - Display a message to all online users."); + console.log(" Help [action] - Get help on an action."); + console.log(" ServerInfo - Show server information."); + console.log(" UserInfo - Show user information."); + console.log(" ListUsers - List user accounts."); + console.log(" ListDeviceGroups - List device groups."); + console.log(" AddUser - Create a new user account."); + console.log(" RemoveUser - Delete a user account."); + console.log(" AddDeviceGroup - Create a new device group."); + console.log(" RemoveDeviceGroup - Delete a device group."); + console.log(" AddUserToDeviceGroup - Add a user to a device group."); + console.log(" RemoveUserFromDeviceGroup - Remove a user from a device group."); + console.log(" Broadcast - Display a message to all online users."); console.log("\r\nSupported login arguments:"); - console.log(" --url [wss://server] - Server url, wss://localhost:443 is default."); - console.log(" --loginuser [username] - Login username, admin is default."); - console.log(" --loginpass [password] - Login password."); - console.log(" --token [number] - 2nd factor authentication token."); - console.log(" --loginkey [hex] - Server login key in hex."); - console.log(" --loginkeyfile [file] - File containing server login key in hex."); - console.log(" --domain [domainid] - Domain id, default is empty."); + console.log(" --url [wss://server] - Server url, wss://localhost:443 is default."); + console.log(" --loginuser [username] - Login username, admin is default."); + console.log(" --loginpass [password] - Login password."); + console.log(" --token [number] - 2nd factor authentication token."); + console.log(" --loginkey [hex] - Server login key in hex."); + console.log(" --loginkeyfile [file] - File containing server login key in hex."); + console.log(" --domain [domainid] - Domain id, default is empty."); return; } else { settings.cmd = args['_'][0].toLowerCase(); @@ -41,6 +43,18 @@ if (args['_'].length == 0) { case 'userinfo': { ok = true; break; } case 'listusers': { ok = true; break; } case 'listdevicegroups': { ok = true; break; } + case 'addusertodevicegroup': { + if (args.userid == null) { console.log("Add user to group missing useid, use --userid [userid]"); } + else if (args.id == null) { console.log("Add user to group missing group id, use --id [groupid]"); } + else { ok = true; } + break; + } + case 'removeuserfromdevicegroup': { + if (args.userid == null) { console.log("Remove user from group missing useid, use --userid [userid]"); } + else if (args.id == null) { console.log("Remove user from group missing group id, use --id [groupid]"); } + else { ok = true; } + break; + } case 'adddevicegroup': { if (args.name == null) { console.log("Message group name, use --name [name]"); } else { ok = true; } @@ -149,11 +163,43 @@ if (args['_'].length == 0) { } case 'removedevicegroup': { console.log("Remove a device group, Example usages:\r\n"); - console.log(" MeshCtrl RemoteDeviceGroup --id groupid"); + console.log(" MeshCtrl RemoveDeviceGroup --id groupid"); console.log("\r\nRequired arguments:\r\n"); console.log(" --id [groupid] - The group identifier."); break; } + case 'addusertodevicegroup': { + console.log("Add a user to a device group, Example usages:\r\n"); + console.log(" MeshCtrl AddUserToDeviceGroup --id groupid --userid userid --fullrights"); + console.log(" MeshCtrl AddUserToDeviceGroup --id groupid --userid userid --editgroup --manageusers"); + console.log("\r\nRequired arguments:\r\n"); + console.log(" --id [groupid] - The group identifier."); + console.log(" --userid [userid] - The user identifier."); + console.log("\r\nOptional arguments:\r\n"); + console.log(" --fullrights - Allow full rights over this device group."); + console.log(" --editgroup - Allow the user to edit group information."); + console.log(" --manageusers - Allow the user to add/remove users."); + console.log(" --managedevices - Allow the user to edit device information."); + console.log(" --remotecontrol - Allow device remote control operations."); + console.log(" --agentconsole - Allow agent console operations."); + console.log(" --serverfiles - Allow access to group server files."); + console.log(" --wakedevices - Allow device wake operation."); + console.log(" --notes - Allow editing of device notes."); + console.log(" --desktopviewonly - Restrict user to view-only remote desktop."); + console.log(" --limiteddesktop - Limit remote desktop keys."); + console.log(" --noterminal - Hide the terminal tab from this user."); + console.log(" --nofiles - Hide the files tab from this user."); + console.log(" --noamt - Hide the Intel AMT tab from this user."); + break; + } + case 'removeuserfromdevicegroup': { + console.log("Remove a user from a device group, Example usages:\r\n"); + console.log(" MeshCtrl RemoveuserFromDeviceGroup --id groupid --userid userid"); + console.log("\r\nRequired arguments:\r\n"); + console.log(" --id [groupid] - The group identifier."); + console.log(" --userid [userid] - The user identifier."); + break; + } case 'broadcast': { console.log("Display a message to all logged in users, Example usages:\r\n"); console.log(" MeshCtrl Broadcast --msg \"This is a test\""); @@ -267,6 +313,31 @@ function serverConnect() { ws.send(JSON.stringify(op)); break; } + case 'addusertodevicegroup': { + var meshrights = 0; + if (args.fullrights) { meshrights = 0xFFFFFFFF; } + if (args.editgroup) { meshrights |= 1; } + if (args.manageusers) { meshrights |= 2; } + if (args.managedevices) { meshrights |= 4; } + if (args.remotecontrol) { meshrights |= 8; } + if (args.agentconsole) { meshrights |= 16; } + if (args.serverfiles) { meshrights |= 32; } + if (args.wakedevices) { meshrights |= 64; } + if (args.notes) { meshrights |= 128; } + if (args.desktopviewonly) { meshrights |= 256; } + if (args.noterminal) { meshrights |= 512; } + if (args.nofiles) { meshrights |= 1024; } + if (args.noamt) { meshrights |= 2048; } + if (args.limiteddesktop) { meshrights |= 4096; } + var op = { action: 'addmeshuser', meshid: args.id, usernames: [args.userid], meshadmin: meshrights, responseid: 'meshctrl' }; + ws.send(JSON.stringify(op)); + break; + } + case 'removeuserfromdevicegroup': { + var op = { action: 'removemeshuser', meshid: args.id, userid: args.userid, responseid: 'meshctrl' }; + ws.send(JSON.stringify(op)); + break; + } case 'broadcast': { var op = { action: 'userbroadcast', msg: args.msg, responseid: 'meshctrl' }; ws.send(JSON.stringify(op)); @@ -309,6 +380,8 @@ function serverConnect() { case 'deleteuser': // REMOVEUSER case 'createmesh': // ADDDEVICEGROUP case 'deletemesh': // REMOVEDEVICEGROUP + case 'addmeshuser': // + case 'removemeshuser': // case 'userbroadcast': { // BROADCAST if (data.responseid == 'meshctrl') { if (data.meshid) { console.log(data.result, data.meshid); } diff --git a/meshuser.js b/meshuser.js index ca36eadd..ca2c6607 100644 --- a/meshuser.js +++ b/meshuser.js @@ -1590,88 +1590,113 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } case 'addmeshuser': { - if (common.validateString(command.meshid, 1, 1024) == false) break; // Check the meshid - if (common.validateInt(command.meshadmin) == false) break; // Mesh rights must be an integer - if (common.validateStrArray(command.usernames, 1, 64) == false) break; // Username is between 1 and 64 characters - - // Get the mesh - mesh = parent.meshes[command.meshid]; - if (mesh) { - // Check if this user has rights to do this - if (mesh.links[user._id] == null || ((mesh.links[user._id].rights & 2) == 0)) return; - if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain - - var unknownUsers = []; - for (var i in command.usernames) { - // Check if the user exists - var newuserid = 'user/' + domain.id + '/' + command.usernames[i].toLowerCase(), newuser = parent.users[newuserid]; - if (newuser != null) { - // Add mesh to user - if (newuser.links == null) newuser.links = {}; - newuser.links[command.meshid] = { rights: command.meshadmin }; - db.SetUser(newuser); - parent.parent.DispatchEvent([newuser._id], obj, 'resubscribe'); - - // Add a user to the mesh - mesh.links[newuserid] = { userid: newuser.id, name: newuser.name, rights: command.meshadmin }; - db.Set(common.escapeLinksFieldName(mesh)); - - // Notify mesh change - var event = { etype: 'mesh', username: newuser.name, userid: command.userid, meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msg: 'Added user ' + newuser.name + ' to mesh ' + mesh.name, domain: domain.id }; - if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come. - parent.parent.DispatchEvent(['*', mesh._id, user._id, newuserid], obj, event); - } else { - unknownUsers.push(command.usernames[i]); - } + var err = null; + try { + if (common.validateString(command.meshid, 1, 1024) == false) { err = 'Invalid groupid'; } // Check the meshid + else if (common.validateInt(command.meshadmin) == false) { err = 'Invalid group rights'; } // Mesh rights must be an integer + else if (common.validateStrArray(command.usernames, 1, 64) == false) { err = 'Invalid usernames'; } // Username is between 1 and 64 characters + else { + if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; } + mesh = parent.meshes[command.meshid]; + if (mesh == null) { err = 'Unknown group'; } + else if (mesh.links[user._id] == null || ((mesh.links[user._id].rights & 2) == 0)) { err = 'Permission denied'; } + else if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain } + } catch (ex) { err = 'Validation exception: ' + ex; } - if (unknownUsers.length > 0) { - // Send error back, user not found. - displayNotificationMessage('User' + ((unknownUsers.length > 1)?'s':'') + ' ' + EscapeHtml(unknownUsers.join(', ')) + ' not found.', 'Device Group', 'ServerNotify'); + // Handle any errors + if (err != null) { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'addmeshuser', responseid: command.responseid, result: err })); } catch (ex) { } } + break; + } + + var unknownUsers = [], removedCount = 0, failCount = 0; + for (var i in command.usernames) { + // Check if the user exists + var newuserid = 'user/' + domain.id + '/' + command.usernames[i].toLowerCase(), newuser = parent.users[newuserid]; + if (newuser != null) { + // Add mesh to user + if (newuser.links == null) newuser.links = {}; + newuser.links[command.meshid] = { rights: command.meshadmin }; + db.SetUser(newuser); + parent.parent.DispatchEvent([newuser._id], obj, 'resubscribe'); + + // Add a user to the mesh + mesh.links[newuserid] = { userid: newuser.id, name: newuser.name, rights: command.meshadmin }; + db.Set(common.escapeLinksFieldName(mesh)); + + // Notify mesh change + var event = { etype: 'mesh', username: newuser.name, userid: command.userid, meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msg: 'Added user ' + newuser.name + ' to mesh ' + mesh.name, domain: domain.id }; + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come. + parent.parent.DispatchEvent(['*', mesh._id, user._id, newuserid], obj, event); + removedCount++; + } else { + unknownUsers.push(command.usernames[i]); + failCount++; } } + + if (unknownUsers.length > 0) { + // Send error back, user not found. + displayNotificationMessage('User' + ((unknownUsers.length > 1)?'s':'') + ' ' + EscapeHtml(unknownUsers.join(', ')) + ' not found.', 'Device Group', 'ServerNotify'); + } + + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'addmeshuser', responseid: command.responseid, result: 'ok', removed: removedCount, failed: failCount })); } catch (ex) { } } break; } case 'removemeshuser': { - if (common.validateString(command.userid, 1, 1024) == false) break; // Check userid - if (common.validateString(command.meshid, 1, 1024) == false) break; // Check meshid - if ((command.userid.split('/').length != 3) || (command.userid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain + var err = null; + try { + if (common.validateString(command.userid, 1, 1024) == false) { err = 'Invalid userid'; } // Check userid + if (common.validateString(command.meshid, 1, 1024) == false) { err = 'Invalid groupid'; } // Check meshid + if (command.userid.indexOf('/') == -1) { command.userid = 'user/' + domain.id + '/' + command.userid; } + if ((command.userid.split('/').length != 3) || (command.userid.split('/')[1] != domain.id)) { err = 'Invalid userid'; } // Invalid domain, operation only valid for current domain + else { + if (command.meshid.indexOf('/') == -1) { command.meshid = 'mesh/' + domain.id + '/' + command.meshid; } + mesh = parent.meshes[command.meshid]; + if (mesh == null) { err = 'Unknown device group'; } + else if (mesh.links[user._id] == null || ((mesh.links[user._id].rights & 2) == 0)) { err = 'Permission denied'; } + else if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain + } + } catch (ex) { err = 'Validation exception: ' + ex; } - // Get the mesh - mesh = parent.meshes[command.meshid]; - if (mesh) { - // Check if this user has rights to do this - if (mesh.links[user._id] == null || ((mesh.links[user._id].rights & 2) == 0)) return; + // Handle any errors + if (err != null) { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'addmeshuser', responseid: command.responseid, result: err })); } catch (ex) { } } + break; + } - // Check if the user exists - Just in case we need to delete a mesh right for a non-existant user, we do it this way. Technically, it's not possible, but just in case. - var deluserid = command.userid, deluser = parent.users[deluserid]; + // Check if the user exists - Just in case we need to delete a mesh right for a non-existant user, we do it this way. Technically, it's not possible, but just in case. + var deluserid = command.userid, deluser = parent.users[deluserid]; + if (deluser != null) { + // Remove mesh from user + if (deluser.links != null && deluser.links[command.meshid] != null) { + var delmeshrights = deluser.links[command.meshid].rights; + if ((delmeshrights == 0xFFFFFFFF) && (mesh.links[deluserid].rights != 0xFFFFFFFF)) return; // A non-admin can't kick out an admin + delete deluser.links[command.meshid]; + db.Set(deluser); + parent.parent.DispatchEvent([deluser._id], obj, 'resubscribe'); + } + } + + // Remove user from the mesh + if (mesh.links[command.userid] != null) { + delete mesh.links[command.userid]; + db.Set(common.escapeLinksFieldName(mesh)); + + // Notify mesh change + var event; if (deluser != null) { - // Remove mesh from user - if (deluser.links != null && deluser.links[command.meshid] != null) { - var delmeshrights = deluser.links[command.meshid].rights; - if ((delmeshrights == 0xFFFFFFFF) && (mesh.links[deluserid].rights != 0xFFFFFFFF)) return; // A non-admin can't kick out an admin - delete deluser.links[command.meshid]; - db.Set(deluser); - parent.parent.DispatchEvent([deluser._id], obj, 'resubscribe'); - } - } - - // Remove user from the mesh - if (mesh.links[command.userid] != null) { - delete mesh.links[command.userid]; - db.Set(common.escapeLinksFieldName(mesh)); - - // Notify mesh change - var event; - if (deluser != null) { - event = { etype: 'mesh', username: user.name, userid: deluser.name, meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msg: 'Removed user ' + deluser.name + ' from group ' + mesh.name, domain: domain.id }; - } else { - event = { etype: 'mesh', username: user.name, userid: (deluserid.split('/')[2]), meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msg: 'Removed user ' + (deluserid.split('/')[2]) + ' from group ' + mesh.name, domain: domain.id }; - } - if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come. - parent.parent.DispatchEvent(['*', mesh._id, user._id, command.userid], obj, event); + event = { etype: 'mesh', username: user.name, userid: deluser.name, meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msg: 'Removed user ' + deluser.name + ' from group ' + mesh.name, domain: domain.id }; + } else { + event = { etype: 'mesh', username: user.name, userid: (deluserid.split('/')[2]), meshid: mesh._id, name: mesh.name, mtype: mesh.mtype, desc: mesh.desc, action: 'meshchange', links: mesh.links, msg: 'Removed user ' + (deluserid.split('/')[2]) + ' from group ' + mesh.name, domain: domain.id }; } + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the mesh. Another event will come. + parent.parent.DispatchEvent(['*', mesh._id, user._id, command.userid], obj, event); + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removemeshuser', responseid: command.responseid, result: 'ok' })); } catch (ex) { } } + } else { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'removemeshuser', responseid: command.responseid, result: 'User not in group' })); } catch (ex) { } } } break; } @@ -2621,7 +2646,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (common.validateString(command.meshid, 8, 128) == false) break; // Check the meshid if (common.validateInt(command.expire, 0, 99999) == false) break; // Check the expire time in hours if (common.validateInt(command.flags, 0, 256) == false) break; // Check the flags - var mesh = parent.meshes[command.meshid]; + mesh = parent.meshes[command.meshid]; if (mesh == null) break; const inviteCookie = parent.parent.encodeCookie({ a: 4, mid: command.meshid, f: command.flags, expire: command.expire * 60 }, parent.parent.invitationLinkEncryptionKey); if (inviteCookie == null) break;