MeshCtrl improvements.

Ylian Saint-Hilaire committed Jun 30, 2019 at 11:34 UTC e208046267b0e759b15fd45c5ccbfdc569ada420
2 files changed +279 -118
meshctrl.js
+114 -38
@@ -1,23 +1,28 @@
1 #!/usr/bin/env node
2
3 +const crypto = require('crypto');
4 var settings = {};
5 const args = require('minimist')(process.argv.slice(2));
5 -const possibleCommands = ['listusers', 'listgroups', 'serverinfo', 'userinfo','adduser','removeuser'];
6 +const possibleCommands = ['listusers', 'listdevicegroups', 'serverinfo', 'userinfo', 'adduser', 'removeuser', 'adddevicegroup', 'removedevicegroup', 'broadcast'];
7 //console.log(args);
8
8 -if ((args['_'].length != 1) && (args['_'][0].toLowerCase() != 'help')) {
9 - console.log("MeshCtrl perform command line actions on a MeshCentral server.");
9 +if (args['_'].length == 0) {
10 + console.log("MeshCtrl performs command line actions on a MeshCentral server.");
11 + console.log("Information at: https://meshcommander.com/meshcentral");
12 console.log("No action specified, use MeshCtrl like this:\r\n\r\n meshctrl [action] [arguments]\r\n");
13 console.log("Supported actions:");
14 console.log(" Help [action] - Get help on an action.");
15 console.log(" ServerInfo - Show server information.");
16 console.log(" UserInfo - Show user information.");
17 console.log(" ListUsers - List user accounts.");
16 - console.log(" ListGroups - List device groups.");
18 + console.log(" ListDeviceGroups - List device groups.");
19 console.log(" AddUser - Create a new user account.");
20 console.log(" RemoveUser - Delete a user account.");
21 + console.log(" AddDeviceGroup - Create a new device group.");
22 + console.log(" RemoveDeviceGroup - Delete a device group.");
23 + console.log(" Broadcast - Display a message to all online users.");
24 console.log("\r\nSupported login arguments:");
20 - console.log(" --url [wss://server] - Server url, wss://localhost:443 is default.");
25 + console.log(" --url [wss://server] - Server url, wss://localhost:443 is default.");
26 console.log(" --loginuser [username] - Login username, admin is default.");
27 console.log(" --loginpass [password] - Login password.");
28 console.log(" --token [number] - 2nd factor authentication token.");
@@ -36,9 +41,24 @@ if ((args['_'].length != 1) && (args['_'][0].toLowerCase() != 'help')) {
41 case 'userinfo': { ok = true; break; }
42 case 'listusers': { ok = true; break; }
43 case 'listgroups': { ok = true; break; }
44 + case 'adddevicegroup': {
45 + if (args.name == null) { console.log("Message group name, use --name [name]"); }
46 + else { ok = true; }
47 + break;
48 + }
49 + case 'removedevicegroup': {
50 + if (args.id == null) { console.log("Message group identifier, use --id [identifier]"); }
51 + else { ok = true; }
52 + break;
53 + }
54 + case 'broadcast': {
55 + if (args.msg == null) { console.log("Message missing, use --msg [message]"); }
56 + else { ok = true; }
57 + break;
58 + }
59 case 'adduser': {
60 if (args.user == null) { console.log("New account name missing, use --user [name]"); }
41 - else if (args.pass == null) { console.log("New account password missing, use --pass [password]"); }
61 + else if ((args.pass == null) && (args.randompass == null)) { console.log("New account password missing, use --pass [password] or --randompass"); }
62 else { ok = true; }
63 break;
64 }
@@ -79,10 +99,10 @@ if ((args['_'].length != 1) && (args['_'][0].toLowerCase() != 'help')) {
99 console.log(" --json - Show result as JSON.");
100 break;
101 }
82 - case 'listgroups': {
102 + case 'listdevicegroups': {
103 console.log("List the device groups for this account, Example usages:\r\n");
84 - console.log(" MeshCtrl ListGroups ");
85 - console.log(" MeshCtrl ListGroups --json");
104 + console.log(" MeshCtrl ListDeviceGroups ");
105 + console.log(" MeshCtrl ListDeviceGroups --json");
106 console.log("\r\nOptional arguments:\r\n");
107 console.log(" --idexists [id] - Return 1 if id exists, 0 if not.");
108 console.log(" --nameexists [name] - Return id if name exists.");
@@ -96,9 +116,17 @@ if ((args['_'].length != 1) && (args['_'][0].toLowerCase() != 'help')) {
116 console.log("\r\nRequired arguments:\r\n");
117 console.log(" --user [name] - New account name.");
118 console.log(" --pass [password] - New account password.");
119 + console.log(" --randompass - Create account with a random password.");
120 console.log("\r\nOptional arguments:\r\n");
121 console.log(" --email [email] - New account email address.");
122 console.log(" --resetpass - Request password reset on next login.");
123 + console.log(" --siteadmin - Create the account as full site administrator.");
124 + console.log(" --manageusers - Allow this account to manage server users.");
125 + console.log(" --fileaccess - Allow this account to store server files.");
126 + console.log(" --serverupdate - Allow this account to update the server.");
127 + console.log(" --locked - This account will be locked.");
128 + console.log(" --nonewgroups - Account will not be allowed to create device groups.");
129 + console.log(" --notools - Account not see MeshCMD download links.");
130 break;
131 }
132 case 'removeuser': {
@@ -108,6 +136,31 @@ if ((args['_'].length != 1) && (args['_'][0].toLowerCase() != 'help')) {
136 console.log(" --userid [id] - Account identifier.");
137 break;
138 }
139 + case 'adddevicegroup': {
140 + console.log("Add a device group, Example usages:\r\n");
141 + console.log(" MeshCtrl AddDeviceGroup --name newgroupname");
142 + console.log(" MeshCtrl AddDeviceGroup --name newgroupname --desc description --amtonly");
143 + console.log("\r\nRequired arguments:\r\n");
144 + console.log(" --name [name] - Name of the new group.");
145 + console.log("\r\nOptional arguments:\r\n");
146 + console.log(" --desc [description] - New group description.");
147 + console.log(" --amtonly - New group is agent-less, Intel AMT only.");
148 + break;
149 + }
150 + case 'removedevicegroup': {
151 + console.log("Remove a device group, Example usages:\r\n");
152 + console.log(" MeshCtrl RemoteDeviceGroup --id groupid");
153 + console.log("\r\nRequired arguments:\r\n");
154 + console.log(" --id [groupid] - The group identifier.");
155 + break;
156 + }
157 + case 'broadcast': {
158 + console.log("Display a message to all logged in users, Example usages:\r\n");
159 + console.log(" MeshCtrl Broadcast --msg \"This is a test\"");
160 + console.log("\r\nRequired arguments:\r\n");
161 + console.log(" --msg [message] - Message to display.");
162 + break;
163 + }
164 default: {
165 console.log("Get help on an action. Type:\r\n\r\n help [action]\r\n\r\nPossible actions are: " + possibleCommands.join(', ') + '.');
166 }
@@ -120,10 +173,11 @@ if ((args['_'].length != 1) && (args['_'][0].toLowerCase() != 'help')) {
173 if (ok) { serverConnect(); }
174 }
175
176 +function onVerifyServer(clientName, certs) { return null; }
177 +
178 function serverConnect() {
179 const WebSocket = require('ws');
180
126 - function onVerifyServer(clientName, certs) { console.log('onVerifyServer', clientName); }
181 var url = 'wss://localhost/control.ashx';
182 if (args.url) {
183 url = args.url;
@@ -133,6 +187,7 @@ function serverConnect() {
187 url += 'control.ashx';
188 }
189
190 + // TODO: checkServerIdentity does not work???
191 var options = { rejectUnauthorized: false, checkServerIdentity: onVerifyServer }
192
193 // Password authentication
@@ -177,16 +232,43 @@ function serverConnect() {
232 case 'serverinfo': { break; }
233 case 'userinfo': { break; }
234 case 'listusers': { ws.send(JSON.stringify({ action: 'users' })); break; }
180 - case 'listgroups': { ws.send(JSON.stringify({ action: 'meshes' })); break; }
235 + case 'listdevicegroups': { ws.send(JSON.stringify({ action: 'meshes' })); break; }
236 case 'adduser': {
182 - var op = { action: 'adduser', username: args.user, pass: args.pass };
237 + var siteadmin = 0;
238 + if (args.siteadmin) { siteadmin = 0xFFFFFFFF; }
239 + if (args.manageusers) { siteadmin |= 2; }
240 + if (args.fileaccess) { siteadmin |= 8; }
241 + if (args.serverupdate) { siteadmin |= 16; }
242 + if (args.locked) { siteadmin |= 32; }
243 + if (args.nonewgroups) { siteadmin |= 64; }
244 + if (args.notools) { siteadmin |= 128; }
245 + if (args.randompass) { args.pass = getRandomAmtPassword(); }
246 + var op = { action: 'adduser', username: args.user, pass: args.pass, responseid: 'meshctrl' };
247 if (args.email) { op.email = args.email; }
248 if (args.resetpass) { op.resetNextLogin = true; }
249 + if (siteadmin != 0) { op.siteadmin = siteadmin; }
250 ws.send(JSON.stringify(op));
251 break;
252 }
253 case 'removeuser': {
189 - var op = { action: 'deleteuser', userid: args.userid };
254 + var op = { action: 'deleteuser', userid: args.userid, responseid: 'meshctrl' };
255 + ws.send(JSON.stringify(op));
256 + break;
257 + }
258 + case 'adddevicegroup': {
259 + var op = { action: 'createmesh', meshname: args.name, meshtype: 2, responseid: 'meshctrl' };
260 + if (args.desc) { op.desc = args.desc; }
261 + if (args.amtonly) { op.meshtype = 1; }
262 + ws.send(JSON.stringify(op));
263 + break;
264 + }
265 + case 'removedevicegroup': {
266 + var op = { action: 'deletemesh', meshid: args.id, responseid: 'meshctrl' };
267 + ws.send(JSON.stringify(op));
268 + break;
269 + }
270 + case 'broadcast': {
271 + var op = { action: 'userbroadcast', msg: args.msg, responseid: 'meshctrl' };
272 ws.send(JSON.stringify(op));
273 break;
274 }
@@ -223,6 +305,19 @@ function serverConnect() {
305 }
306 break;
307 }
308 + case 'adduser': // ADDUSER
309 + case 'deleteuser': // REMOVEUSER
310 + case 'createmesh': // ADDDEVICEGROUP
311 + case 'deletemesh': // REMOVEDEVICEGROUP
312 + case 'userbroadcast': { // BROADCAST
313 + if (data.responseid == 'meshctrl') {
314 + if (data.meshid) { console.log(data.result, data.meshid); }
315 + else if (data.userid) { console.log(data.result, data.userid); }
316 + else console.log(data.result);
317 + process.exit();
318 + }
319 + break;
320 + }
321 case 'users': { // LISTUSERS
322 if (args.json) {
323 console.log(JSON.stringify(data.users, ' ', 2));
@@ -241,7 +336,7 @@ function serverConnect() {
336 process.exit();
337 break;
338 }
244 - case 'meshes': { // LISTGROUPS
339 + case 'meshes': { // LISTDEVICEGROUPS
340 if (args.json) {
341 console.log(JSON.stringify(data.meshes, ' ', 2));
342 } else {
@@ -269,29 +364,7 @@ function serverConnect() {
364 process.exit();
365 break;
366 }
272 - case 'event': {
273 - switch (data.event.action) {
274 - case 'accountcreate': {
275 - if ((settings.cmd == 'adduser') && (data.event.account.name == args.user)) {
276 - console.log('Account created, id: ' + data.event.account._id);
277 - process.exit();
278 - }
279 - break;
280 - }
281 - case 'accountremove': {
282 - if ((settings.cmd == 'removeuser') && (data.event.userid == args.userid)) {
283 - console.log('Account removed');
284 - process.exit();
285 - }
286 - break;
287 - }
288 - }
289 - break;
290 - }
291 - default: {
292 - console.log('Unknown action: ' + data.action);
293 - break;
294 - }
367 + default: { break; }
368 }
369 //console.log('Data', data);
370 //setTimeout(function timeout() { ws.send(Date.now()); }, 500);
@@ -300,7 +373,6 @@ function serverConnect() {
373
374 // Encode an object as a cookie using a key using AES-GCM. (key must be 32 bytes or more)
375 function encodeCookie(o, key) {
303 - var crypto = require('crypto');
376 try {
377 if (key == null) { return null; }
378 o.time = Math.floor(Date.now() / 1000); // Add the cookie creation time
@@ -309,3 +381,7 @@ function encodeCookie(o, key) {
381 return Buffer.concat([iv, cipher.getAuthTag(), crypted]).toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
382 } catch (e) { return null; }
383 }
384 +
385 +// Generate a random Intel AMT password
386 +function checkAmtPassword(p) { return (p.length > 7) && (/\d/.test(p)) && (/[a-z]/.test(p)) && (/[A-Z]/.test(p)) && (/\W/.test(p)); }
387 +function getRandomAmtPassword() { var p; do { p = Buffer.from(crypto.randomBytes(9), 'binary').toString('base64').split('/').join('@'); } while (checkAmtPassword(p) == false); return p; }
meshuser.js
+165 -80
@@ -40,9 +40,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
40 const SITERIGHT_SERVERRESTORE = 4;
41 const SITERIGHT_FILEACCESS = 8;
42 const SITERIGHT_SERVERUPDATE = 16;
43 - const SITERIGHT_LOCKED = 32;
44 - const SITERIGHT_NONEWGROUPS = 64;
45 - const SITERIGHT_NOMESHCMD = 128;
43 + const SITERIGHT_LOCKED = 32; // 0x00000020
44 + const SITERIGHT_NONEWGROUPS = 64; // 0x00000040
45 + const SITERIGHT_NOMESHCMD = 128; // 0x00000080
46
47 var obj = {};
48 obj.user = user;
@@ -932,12 +932,26 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
932 case 'deleteuser':
933 {
934 // Delete a user account
935 - if ((user.siteadmin & 2) == 0) break;
936 - if (common.validateString(command.userid, 1, 2048) == false) break;
937 - var delusersplit = command.userid.split('/'), deluserid = command.userid, deluser = parent.users[deluserid];
938 - if ((deluser == null) || (delusersplit.length != 3) || (delusersplit[1] != domain.id)) break; // Invalid domain, operation only valid for current domain
939 - if ((deluser.siteadmin != null) && (deluser.siteadmin > 0) && (user.siteadmin != 0xFFFFFFFF)) break; // Need full admin to remote another administrator
940 - if ((user.groups != null) && (user.groups.length > 0) && ((deluser.groups == null) || (findOne(deluser.groups, user.groups) == false))) break; // Can only perform this operation on other users of our group.
935 + var err = null, delusersplit, deluserid, deluser;
936 + try {
937 + if ((user.siteadmin & 2) == 0) { err = 'Permission denied'; }
938 + else if (common.validateString(command.userid, 1, 2048) == false) { err = 'Invalid userid'; }
939 + else {
940 + delusersplit = command.userid.split('/');
941 + deluserid = command.userid;
942 + deluser = parent.users[deluserid];
943 + if (deluser == null) { err = 'User does not exists'; }
944 + else if ((delusersplit.length != 3) || (delusersplit[1] != domain.id)) { err = 'Invalid domain'; } // Invalid domain, operation only valid for current domain
945 + else if ((deluser.siteadmin != null) && (deluser.siteadmin > 0) && (user.siteadmin != 0xFFFFFFFF)) { err = 'Permission denied'; } // Need full admin to remote another administrator
946 + else if ((user.groups != null) && (user.groups.length > 0) && ((deluser.groups == null) || (findOne(deluser.groups, user.groups) == false))) { err = 'Invalid user group'; } // Can only perform this operation on other users of our group.
947 + }
948 + } catch (ex) { err = 'Validation exception: ' + ex; }
949 +
950 + // Handle any errors
951 + if (err != null) {
952 + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deleteuser', responseid: command.responseid, result: err })); } catch (ex) { } }
953 + break;
954 + }
955
956 // Remove all the mesh links to this user
957 if (deluser.links != null) {
@@ -973,13 +987,24 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
987 parent.parent.DispatchEvent(targets, obj, { etype: 'user', userid: deluserid, username: deluser.name, action: 'accountremove', msg: 'Account removed', domain: domain.id });
988 parent.parent.DispatchEvent([deluserid], obj, 'close');
989
990 + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deleteuser', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
991 +
992 break;
993 }
994 case 'userbroadcast':
995 {
980 - // Broadcast a message to all currently connected users.
981 - if ((user.siteadmin & 2) == 0) break;
982 - if (common.validateString(command.msg, 1, 256) == false) break; // Notification message is between 1 and 256 characters
996 + var err = null;
997 + try {
998 + // Broadcast a message to all currently connected users.
999 + if ((user.siteadmin & 2) == 0) { err = 'Permission denied'; }
1000 + else if (common.validateString(command.msg, 1, 512) == false) { err = 'Message is too long'; } // Notification message is between 1 and 256 characters
1001 + } catch (ex) { err = 'Validation exception: ' + ex; }
1002 +
1003 + // Handle any errors
1004 + if (err != null) {
1005 + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'userbroadcast', responseid: command.responseid, result: err })); } catch (ex) { } }
1006 + break;
1007 + }
1008
1009 // Create the notification message
1010 var notification = { action: "msg", type: "notify", domain: domain.id, "value": command.msg, "title": user.name, icon: 0, tag: "broadcast" };
@@ -1004,6 +1029,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
1029
1030 // TODO: Notify all sessions on other peers.
1031
1032 + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'userbroadcast', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
1033 break;
1034 }
1035 case 'adduserbatch':
@@ -1074,32 +1100,53 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
1100 case 'adduser':
1101 {
1102 // Add a new user account
1077 - if ((user.siteadmin & 2) == 0) break;
1078 - if (common.validateUsername(command.username, 1, 64) == false) break; // Username is between 1 and 64 characters, no spaces
1079 - if (common.validateString(command.pass, 1, 256) == false) break; // Password is between 1 and 256 characters
1080 - if (command.username.indexOf('/') >= 0) break; // Usernames can't have '/'
1081 - if (common.checkPasswordRequirements(command.pass, domain.passwordrequirements) == false) break; // Password does not meet requirements
1082 - if ((command.email != null) && (common.validateEmail(command.email, 1, 256) == false)) break; // Check if this is a valid email address
1083 - var newusername = command.username, newuserid = 'user/' + domain.id + '/' + command.username.toLowerCase();
1084 - if (newusername == '~') break; // This is a reserved user name
1085 - if (parent.users[newuserid]) break; // Account already exists
1103 + var err = null, newusername, newuserid;
1104 + try {
1105 + if ((user.siteadmin & 2) == 0) { err = 'Permission denied'; }
1106 + else if (common.validateUsername(command.username, 1, 64) == false) { err = 'Invalid username'; } // Username is between 1 and 64 characters, no spaces
1107 + else if (common.validateString(command.pass, 1, 256) == false) { err = 'Invalid password'; } // Password is between 1 and 256 characters
1108 + else if (command.username.indexOf('/') >= 0) { err = 'Invalid username'; } // Usernames can't have '/'
1109 + else if (common.checkPasswordRequirements(command.pass, domain.passwordrequirements) == false) { err = 'Invalid password'; } // Password does not meet requirements
1110 + else if ((command.email != null) && (common.validateEmail(command.email, 1, 256) == false)) { err = 'Invalid email'; } // Check if this is a valid email address
1111 + else {
1112 + newusername = command.username;
1113 + newuserid = 'user/' + domain.id + '/' + command.username.toLowerCase();
1114 + if (newusername == '~') { err = 'Invalid username'; } // This is a reserved user name
1115 + else if (command.siteadmin != null) {
1116 + if ((typeof command.siteadmin != 'number') || (Number.isInteger(command.siteadmin) == false)) { err = 'Invalid site permissions'; } // Check permissions
1117 + else if ((user.siteadmin != 0xFFFFFFFF) && ((command.siteadmin & (0xFFFFFFFF - 224)) != 0)) { err = 'Invalid site permissions'; }
1118 + }
1119 + if (parent.users[newuserid]) { err = 'User already exists'; } // Account already exists
1120 + }
1121 + } catch (ex) { err = 'Validation exception'; }
1122 +
1123 + // Handle any errors
1124 + if (err != null) {
1125 + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'adduser', responseid: command.responseid, result: err })); } catch (ex) { } }
1126 + break;
1127 + }
1128
1129 // Check if we exceed the maximum number of user accounts
1130 db.isMaxType(domain.limits.maxuseraccounts, 'user', domain.id, function (maxExceed) {
1131 if (maxExceed) {
1132 // Account count exceed, do notification
1133 + if (command.responseid != null) {
1134 + // Respond privately if requested
1135 + try { ws.send(JSON.stringify({ action: 'adduser', responseid: command.responseid, result: 'maxUsersExceed' })); } catch (ex) { }
1136 + } else {
1137 + // Create the notification message
1138 + var notification = { action: "msg", type: "notify", value: "Account limit reached.", title: "Server Limit", userid: user._id, username: user.name, domain: domain.id };
1139
1092 - // Create the notification message
1093 - var notification = { action: "msg", type: "notify", value: "Account limit reached.", title: "Server Limit", userid: user._id, username: user.name, domain: domain.id };
1094 -
1095 - // Get the list of sessions for this user
1096 - var sessions = parent.wssessions[user._id];
1097 - if (sessions != null) { for (i in sessions) { try { if (sessions[i].domainid == domain.id) { sessions[i].send(JSON.stringify(notification)); } } catch (ex) { } } }
1098 - // TODO: Notify all sessions on other peers.
1140 + // Get the list of sessions for this user
1141 + var sessions = parent.wssessions[user._id];
1142 + if (sessions != null) { for (i in sessions) { try { if (sessions[i].domainid == domain.id) { sessions[i].send(JSON.stringify(notification)); } } catch (ex) { } } }
1143 + // TODO: Notify all sessions on other peers.
1144 + }
1145 } else {
1100 - // Check if this is an existing user
1146 + // Create a new user
1147 var newuser = { type: 'user', _id: newuserid, name: newusername, creation: Math.floor(Date.now() / 1000), domain: domain.id };
1102 - if (domain.newaccountsrights) { newuser.siteadmin = domain.newaccountsrights; }
1148 + if (command.siteadmin != null) { newuser.siteadmin = command.siteadmin; }
1149 + else if (domain.newaccountsrights) { newuser.siteadmin = domain.newaccountsrights; }
1150 if (command.email != null) { newuser.email = command.email; } // Email
1151 if (command.resetNextLogin === true) { newuser.passchange = -1; } else { newuser.passchange = Math.floor(Date.now() / 1000); }
1152 if ((user.groups != null) && (user.groups.length > 0)) { newuser.groups = user.groups; } // New account are automatically part of our groups.
@@ -1107,20 +1154,26 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
1154
1155 // Create a user, generate a salt and hash the password
1156 require('./pass').hash(command.pass, function (err, salt, hash, tag) {
1110 - if (err) throw err;
1111 - newuser.salt = salt;
1112 - newuser.hash = hash;
1113 - db.SetUser(newuser);
1157 + if (err == null) {
1158 + newuser.salt = salt;
1159 + newuser.hash = hash;
1160 + db.SetUser(newuser);
1161 +
1162 + var event, targets = ['*', 'server-users'];
1163 + if (newuser.groups) { for (var i in newuser.groups) { targets.push('server-users:' + i); } }
1164 + if (command.email == null) {
1165 + event = { etype: 'user', username: newusername, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msg: 'Account created, username is ' + command.username, domain: domain.id };
1166 + } else {
1167 + event = { etype: 'user', username: newusername, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msg: 'Account created, email is ' + command.email, domain: domain.id };
1168 + }
1169 + if (parent.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
1170 + parent.parent.DispatchEvent(targets, obj, event);
1171
1115 - var event, targets = ['*', 'server-users'];
1116 - if (newuser.groups) { for (var i in newuser.groups) { targets.push('server-users:' + i); } }
1117 - if (command.email == null) {
1118 - event = { etype: 'user', username: newusername, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msg: 'Account created, username is ' + command.username, domain: domain.id };
1172 + // OK Response
1173 + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'adduser', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
1174 } else {
1120 - event = { etype: 'user', username: newusername, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msg: 'Account created, email is ' + command.email, domain: domain.id };
1175 + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'adduser', responseid: command.responseid, result: 'passwordHashError' })); } catch (ex) { } }
1176 }
1122 - if (parent.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come.
1123 - parent.parent.DispatchEvent(targets, obj, event);
1177 }, 0);
1178 }
1179 });
@@ -1386,61 +1439,91 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
1439 }
1440 case 'createmesh':
1441 {
1389 - // Check if we have new group restriction
1390 - if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 64) != 0)) break;
1442 + var err = null;
1443 + try {
1444 + // Check if we have new group restriction
1445 + if ((user.siteadmin != 0xFFFFFFFF) && ((user.siteadmin & 64) != 0)) { err = 'Permission denied'; }
1446
1392 - // In some situations, we need a verified email address to create a device group.
1393 - if ((parent.parent.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (user.emailVerified !== true) && (user.siteadmin != 0xFFFFFFFF)) return; // User must verify it's email first.
1447 + // In some situations, we need a verified email address to create a device group.
1448 + else if ((parent.parent.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (user.emailVerified !== true) && (user.siteadmin != 0xFFFFFFFF)) { err = 'Email verification required'; } // User must verify it's email first.
1449
1395 - // Create mesh
1396 - if (common.validateString(command.meshname, 1, 64) == false) break; // Meshname is between 1 and 64 characters
1397 - if (common.validateString(command.desc, 0, 1024) == false) break; // Mesh description is between 0 and 1024 characters
1450 + // Create mesh
1451 + else if (common.validateString(command.meshname, 1, 64) == false) { err = 'Invalid group name'; } // Meshname is between 1 and 64 characters
1452 + else if ((command.desc != null) && (common.validateString(command.desc, 0, 1024) == false)) { err = 'Invalid group description'; } // Mesh description is between 0 and 1024 characters
1453 + else if ((command.meshtype != 1) && (command.meshtype != 2)) { err = 'Invalid group type'; }
1454 + } catch (ex) { err = 'Validation exception: ' + ex; }
1455 +
1456 + // Handle any errors
1457 + if (err != null) {
1458 + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createmesh', responseid: command.responseid, result: err })); } catch (ex) { } }
1459 + break;
1460 + }
1461
1462 // We only create Agent-less Intel AMT mesh (Type1), or Agent mesh (Type2)
1400 - if ((command.meshtype == 1) || (command.meshtype == 2)) {
1401 - parent.crypto.randomBytes(48, function (err, buf) {
1402 - // Create new device group identifier
1403 - meshid = 'mesh/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
1463 + parent.crypto.randomBytes(48, function (err, buf) {
1464 + // Create new device group identifier
1465 + meshid = 'mesh/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$');
1466 +
1467 + // Create the new device group
1468 + var links = {};
1469 + links[user._id] = { name: user.name, rights: 4294967295 };
1470 + mesh = { type: 'mesh', _id: meshid, name: command.meshname, mtype: command.meshtype, desc: command.desc, domain: domain.id, links: links };
1471 + db.Set(common.escapeLinksFieldName(mesh));
1472 + parent.meshes[meshid] = mesh;
1473 + parent.parent.AddEventDispatch([meshid], ws);
1474
1405 - // Create the new device group
1406 - var links = {};
1407 - links[user._id] = { name: user.name, rights: 4294967295 };
1408 - mesh = { type: 'mesh', _id: meshid, name: command.meshname, mtype: command.meshtype, desc: command.desc, domain: domain.id, links: links };
1409 - db.Set(common.escapeLinksFieldName(mesh));
1410 - parent.meshes[meshid] = mesh;
1411 - parent.parent.AddEventDispatch([meshid], ws);
1475 + // Change the user to make him administration of the new device group
1476 + if (user.links == null) user.links = {};
1477 + user.links[meshid] = { rights: 4294967295 };
1478 + user.subscriptions = parent.subscribe(user._id, ws);
1479 + db.SetUser(user);
1480
1413 - // Change the user to make him administration of the new device group
1414 - if (user.links == null) user.links = {};
1415 - user.links[meshid] = { rights: 4294967295 };
1416 - user.subscriptions = parent.subscribe(user._id, ws);
1417 - db.SetUser(user);
1481 + // Event the user change
1482 + var targets = ['*', 'server-users', user._id];
1483 + if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
1484 + var event = { etype: 'user', username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', domain: domain.id, nolog: 1 };
1485 + 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.
1486 + parent.parent.DispatchEvent(targets, obj, event);
1487
1419 - // Event the user change
1420 - var targets = ['*', 'server-users', user._id];
1421 - if (user.groups) { for (var i in user.groups) { targets.push('server-users:' + i); } }
1422 - var event = { etype: 'user', username: user.name, account: parent.CloneSafeUser(user), action: 'accountchange', domain: domain.id, nolog: 1 };
1423 - 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.
1424 - parent.parent.DispatchEvent(targets, obj, event);
1488 + // Event the device group creation
1489 + var event = { etype: 'mesh', username: user.name, meshid: meshid, name: command.meshname, mtype: command.meshtype, desc: command.desc, action: 'createmesh', links: links, msg: 'Mesh created: ' + command.meshname, domain: domain.id };
1490 + parent.parent.DispatchEvent(['*', meshid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon.
1491
1426 - // Event the device group creation
1427 - var event = { etype: 'mesh', username: user.name, meshid: meshid, name: command.meshname, mtype: command.meshtype, desc: command.desc, action: 'createmesh', links: links, msg: 'Mesh created: ' + command.meshname, domain: domain.id };
1428 - parent.parent.DispatchEvent(['*', meshid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon.
1429 - });
1430 - }
1492 + try { ws.send(JSON.stringify({ action: 'createmesh', responseid: command.responseid, result: 'ok', meshid: meshid })); } catch (ex) { }
1493 + });
1494 break;
1495 }
1496 case 'deletemesh':
1497 {
1435 - // Delete a mesh and all computers within it
1436 - if (common.validateString(command.meshid, 1, 1024) == false) break; // Check the meshid
1498 + var err = null;
1499 + try {
1500 + // Delete a mesh and all computers within it
1501 + if (common.validateString(command.meshid, 1, 1024) == false) { err = 'Invalid group identifier'; } // Check the meshid
1502 + } catch (ex) { err = 'Validation exception: ' + ex; }
1503 +
1504 + // Handle any errors
1505 + if (err != null) {
1506 + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deletemesh', responseid: command.responseid, result: err })); } catch (ex) { } }
1507 + break;
1508 + }
1509 +
1510 db.Get(command.meshid, function (err, meshes) {
1438 - if (meshes.length != 1) return;
1511 + if (meshes.length != 1) {
1512 + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deletemesh', responseid: command.responseid, result: 'Unknown device group' })); } catch (ex) { } }
1513 + return;
1514 + }
1515 var mesh = common.unEscapeLinksFieldName(meshes[0]);
1516
1517 // Check if this user has rights to do this
1442 - if (mesh.links[user._id] == null || mesh.links[user._id].rights != 0xFFFFFFFF) return;
1443 - if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) return; // Invalid domain, operation only valid for current domain
1518 + var err = null;
1519 + if (mesh.links[user._id] == null || mesh.links[user._id].rights != 0xFFFFFFFF) { err = 'Access denied'; }
1520 + if ((command.meshid.split('/').length != 3) || (command.meshid.split('/')[1] != domain.id)) { err = 'Invalid group'; } // Invalid domain, operation only valid for current domain
1521 +
1522 + // Handle any errors
1523 + if (err != null) {
1524 + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deletemesh', responseid: command.responseid, result: err })); } catch (ex) { } }
1525 + return;
1526 + }
1527
1528 // Fire the removal event first, because after this, the event will not route
1529 var event = { etype: 'mesh', username: user.name, meshid: command.meshid, name: command.meshname, action: 'deletemesh', msg: 'Mesh deleted: ' + command.meshname, domain: domain.id };
@@ -1475,6 +1558,8 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
1558
1559 // Delete all devices attached to this mesh in the database
1560 db.RemoveMeshDocuments(command.meshid);
1561 +
1562 + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'deletemesh', responseid: command.responseid, result: 'ok' })); } catch (ex) { } }
1563 });
1564 break;
1565 }