From 27b9cfe8eab31f0c45673890aeedc5896673e16a Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Tue, 14 May 2019 14:39:26 -0700 Subject: [PATCH] Added batch account addition. --- meshcentral.js | 2 +- meshuser.js | 81 ++++++++++++++++++++++++++++++++--- package.json | 2 +- pass.js | 13 +++--- public/images/link6.png | Bin 0 -> 198 bytes views/default-min.handlebars | 2 +- views/default.handlebars | 31 ++++++++++++++ webserver.js | 30 ++++++------- 8 files changed, 130 insertions(+), 31 deletions(-) create mode 100644 public/images/link6.png diff --git a/meshcentral.js b/meshcentral.js index d9395cd1..2ecafbce 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -96,7 +96,7 @@ function CreateMeshCentralServer(config, args) { // Start the Meshcentral server obj.Start = function () { var i; - try { require('./pass').hash('test', function () { }); } catch (e) { console.log('Old version of node, must upgrade.'); return; } // TODO: Not sure if this test works or not. + try { require('./pass').hash('test', function () { }, 0); } catch (e) { console.log('Old version of node, must upgrade.'); return; } // TODO: Not sure if this test works or not. // Check for invalid arguments var validArguments = ['_', 'notls', 'user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'shownodes', 'showmeshes', 'showevents', 'showpower', 'clearpower', 'showiplocations', 'help', 'exactports', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpsdebug', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbexportmin', 'dbimport', 'dbmerge', 'dbencryptkey', 'selfupdate', 'tlsoffload', 'userallowedip', 'userblockedip', 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport', 'swarmdebug', 'logintoken', 'logintokenkey', 'logintokengen', 'logintokengen', 'mailtokengen', 'admin', 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore', 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'configkey', 'loadconfigfromdb', 'npmpath', 'memorytracking']; diff --git a/meshuser.js b/meshuser.js index c6e60029..ee53a82e 100644 --- a/meshuser.js +++ b/meshuser.js @@ -952,6 +952,68 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // TODO: Notify all sessions on other peers. + break; + } + case 'adduserbatch': + { + // Add many new user accounts + if ((user.siteadmin & 2) == 0) break; + if (!Array.isArray(command.users)) break; + var userCount = 0; + for (var i in command.users) { + if (common.validateUsername(command.users[i].user, 1, 64) == false) break; // Username is between 1 and 64 characters, no spaces + if ((command.users[i].user == '~') || (command.users[i].user.indexOf('/') >= 0)) break; // This is a reserved user name + if (common.validateString(command.users[i].pass, 1, 256) == false) break; // Password is between 1 and 256 characters + if (common.checkPasswordRequirements(command.users[i].pass, domain.passwordrequirements) == false) break; // Password does not meet requirements + if ((command.email != null) && (common.validateEmail(command.users[i].email, 1, 256) == false)) break; // Check if this is a valid email address + userCount++; + } + + // Check if we exceed the maximum number of user accounts + db.isMaxType(domain.limits.maxuseraccounts + userCount, 'user', domain.id, function (maxExceed) { + if (maxExceed) { + // Account count exceed, do notification + + // Create the notification message + var notification = { action: "msg", type: "notify", value: "Account limit reached.", title: "Server Limit", userid: user._id, username: user.name, domain: domain.id }; + + // Get the list of sessions for this user + var sessions = parent.wssessions[user._id]; + if (sessions != null) { for (i in sessions) { try { if (sessions[i].domainid == domain.id) { sessions[i].send(JSON.stringify(notification)); } } catch (ex) { } } } + // TODO: Notify all sessions on other peers. + } else { + for (var i in command.users) { + // Check if this is an existing user + var newuserid = 'user/' + domain.id + '/' + command.users[i].user.toLowerCase(); + var newuser = { type: 'user', _id: newuserid, name: command.users[i].user, creation: Math.floor(Date.now() / 1000), domain: domain.id }; + if (domain.newaccountsrights) { newuser.siteadmin = domain.newaccountsrights; } + if (command.users[i].email != null) { newuser.email = command.users[i].email; } // Email + if (command.users[i].resetNextLogin === true) { newuser.passchange = -1; } else { newuser.passchange = Math.floor(Date.now() / 1000); } + if ((command.users[i].groups != null) && (common.validateStrArray(command.users[i].groups, 1, 32))) { newuser.groups = command.users[i].groups; } // New account are automatically part of our groups. + + if (parent.users[newuserid] == null) { + parent.users[newuserid] = newuser; + + // Create a user, generate a salt and hash the password + require('./pass').hash(command.users[i].pass, function (err, salt, hash, newuser) { + if (err) throw err; + newuser.salt = salt; + newuser.hash = hash; + db.SetUser(newuser); + + var targets = ['*', 'server-users']; + if (newuser.groups) { for (var i in newuser.groups) { targets.push('server-users:' + i); } } + if (newuser.email == null) { + parent.parent.DispatchEvent(targets, obj, { etype: 'user', username: newuser.name, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msg: 'Account created, username is ' + newuser.name, domain: domain.id }); + } else { + parent.parent.DispatchEvent(targets, obj, { etype: 'user', username: newuser.name, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msg: 'Account created, email is ' + newuser.email, domain: domain.id }); + } + }, newuser); + } + } + } + }); + break; } case 'adduser': @@ -960,6 +1022,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if ((user.siteadmin & 2) == 0) break; if (common.validateUsername(command.username, 1, 64) == false) break; // Username is between 1 and 64 characters, no spaces if (common.validateString(command.pass, 1, 256) == false) break; // Password is between 1 and 256 characters + if (command.username.indexOf('/') >= 0) break; // Usernames can't have '/' if (common.checkPasswordRequirements(command.pass, domain.passwordrequirements) == false) break; // Password does not meet requirements if ((command.email != null) && (common.validateEmail(command.email, 1, 256) == false)) break; // Check if this is a valid email address var newusername = command.username, newuserid = 'user/' + domain.id + '/' + command.username.toLowerCase(); @@ -988,7 +1051,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use parent.users[newuserid] = newuser; // Create a user, generate a salt and hash the password - require('./pass').hash(command.pass, function (err, salt, hash) { + require('./pass').hash(command.pass, function (err, salt, hash, tag) { if (err) throw err; newuser.salt = salt; newuser.hash = hash; @@ -996,8 +1059,12 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use var targets = ['*', 'server-users']; if (newuser.groups) { for (var i in newuser.groups) { targets.push('server-users:' + i); } } - parent.parent.DispatchEvent(targets, obj, { etype: 'user', username: newusername, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msg: 'Account created, email is ' + command.email, domain: domain.id }); - }); + if (command.email == null) { + parent.parent.DispatchEvent(targets, obj, { etype: 'user', username: newusername, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msg: 'Account created, username is ' + command.user, domain: domain.id }); + } else { + parent.parent.DispatchEvent(targets, obj, { etype: 'user', username: newusername, account: parent.CloneSafeUser(newuser), action: 'accountcreate', msg: 'Account created, email is ' + command.email, domain: domain.id }); + } + }, 0); } }); break; @@ -1083,7 +1150,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use parent.checkUserPassword(domain, user, command.oldpass, function (result) { if (result == true) { // Update the password - require('./pass').hash(command.newpass, function (err, salt, hash) { + require('./pass').hash(command.newpass, function (err, salt, hash, tag) { if (err) { // Send user notification of error displayNotificationMessage('Error, password not changed.', 'Account Settings', 'ServerNotify'); @@ -1107,7 +1174,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // Send user notification of password change displayNotificationMessage('Password changed.', 'Account Settings', 'ServerNotify'); } - }); + }, 0); } else { // Send user notification of error displayNotificationMessage('Current password not correct.', 'Account Settings', 'ServerNotify'); @@ -1131,7 +1198,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if ((user.groups != null) && (user.groups.length > 0) && ((chguser.groups == null) || (findOne(chguser.groups, user.groups) == false))) break; // Compute the password hash & save it - require('./pass').hash(command.pass, function (err, salt, hash) { + require('./pass').hash(command.pass, function (err, salt, hash, tag) { if (!err) { chguser.salt = salt; chguser.hash = hash; @@ -1156,7 +1223,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // Report that the password change failed // TODO } - }); + }, 0); } break; } diff --git a/package.json b/package.json index 063dddf2..f59566e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meshcentral", - "version": "0.3.4-h", + "version": "0.3.4-i", "keywords": [ "Remote Management", "Intel AMT", diff --git a/pass.js b/pass.js index ce7e52d5..8c492af1 100644 --- a/pass.js +++ b/pass.js @@ -25,24 +25,25 @@ const iterations = 12000; * @param {Function} callback * @api public */ -exports.hash = function (pwd, salt, fn) { - if (3 == arguments.length) { +exports.hash = function (pwd, salt, fn, tag) { + if (4 == arguments.length) { try { - crypto.pbkdf2(pwd, salt, iterations, len, 'sha384', function (err, hash) { fn(err, hash.toString('base64')); }); + crypto.pbkdf2(pwd, salt, iterations, len, 'sha384', function (err, hash) { fn(err, hash.toString('base64'), tag); }); } catch (e) { // If this previous call fails, it's probably because older pbkdf2 did not specify the hashing function, just use the default. - crypto.pbkdf2(pwd, salt, iterations, len, function (err, hash) { fn(err, hash.toString('base64')); }); + crypto.pbkdf2(pwd, salt, iterations, len, function (err, hash) { fn(err, hash.toString('base64'), tag); }); } } else { + tag = fn; fn = salt; crypto.randomBytes(len, function (err, salt) { if (err) return fn(err); salt = salt.toString('base64'); try { - crypto.pbkdf2(pwd, salt, iterations, len, 'sha384', function (err, hash) { if (err) { return fn(err); } fn(null, salt, hash.toString('base64')); }); + crypto.pbkdf2(pwd, salt, iterations, len, 'sha384', function (err, hash) { if (err) { return fn(err); } fn(null, salt, hash.toString('base64'), tag); }); } catch (e) { // If this previous call fails, it's probably because older pbkdf2 did not specify the hashing function, just use the default. - crypto.pbkdf2(pwd, salt, iterations, len, function (err, hash) { if (err) { return fn(err); } fn(null, salt, hash.toString('base64')); }); + crypto.pbkdf2(pwd, salt, iterations, len, function (err, hash) { if (err) { return fn(err); } fn(null, salt, hash.toString('base64'), tag); }); } }); } diff --git a/public/images/link6.png b/public/images/link6.png new file mode 100644 index 0000000000000000000000000000000000000000..99e4331f4041739770315fcbcd01970d3f945e90 GIT binary patch literal 198 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4v7|ftIx;Y9?5p`5I2p+2Ebxdd zW?->OfTs&PI zLo|Zdb{*tpP~c#mc`ftq%0-+@R9~Dv&U$Meqd<<2{jFS+9U=ww+iE;sJy|W%F=L@% p(c}-VPZoB4DqgYW5jVp&2Cvub9BzC|wSnd_c)I$ztaD0e0s!neKZXDR literal 0 HcmV?d00001 diff --git a/views/default-min.handlebars b/views/default-min.handlebars index d8ccc2ae..cf054515 100644 --- a/views/default-min.handlebars +++ b/views/default-min.handlebars @@ -1 +1 @@ - {{{title}}}
{{{title}}}
{{{title2}}}

{{{logoutControl}}}

 

\ No newline at end of file + {{{title}}}
{{{title}}}
{{{title2}}}

{{{logoutControl}}}

 

\ No newline at end of file diff --git a/views/default.handlebars b/views/default.handlebars index 7237e190..c2c1363f 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -302,6 +302,7 @@
+
@@ -6846,6 +6847,36 @@ return false; } + function p4batchAccountCreate() { + if (xxdialogMode) return; + var x = 'Create many accounts at once by importing a JSON file with the following format:
[\r\n {"user":"x1","pass":"x","email":"x1@x"},\r\n {"user":"x2","pass":"x","resetNextLogin":true}\r\n]
'; + setDialogMode(2, "User Account Import", 3, p4batchAccountCreateEx, x); + QE('idx_dlgOkButton', false); + } + + function p4batchAccountCreateValidate() { + QE('idx_dlgOkButton', Q('d4importFile').value != null); + } + + function p4batchAccountCreateEx() { + var fr = new FileReader(); + fr.onload = function (r) { + var j = null; + try { j = JSON.parse(r.target.result); } catch (ex) { setDialogMode(2, "User Account Import", 1, null, "Invalid JSON file: " + ex + "."); return; } + if ((j != null) && (Array.isArray(j))) { + var ok = true; + for (var i in j) { + if ((typeof j[i].user != 'string') || (j[i].user.length < 1) || (j[i].user.length > 64)) { ok = false; } + if ((typeof j[i].pass != 'string') || (j[i].pass.length < 1) || (j[i].pass.length > 256)) { ok = false; } + if (checkPasswordRequirements(j[i].pass, passRequirements) == false) { ok = false; } + if ((j[i].email != null) && ((typeof j[i].email != 'string') || (j[i].email.length < 1) || (j[i].email.length > 128))) { ok = false; } + } + if (ok == false) { setDialogMode(2, "User Account Import", 1, null, "Invalid JSON file format."); } else { meshserver.send({ action: 'adduserbatch', users: j }); } + } else { setDialogMode(2, "User Account Import", 1, null, "Invalid JSON file format."); } + }; + fr.readAsText(Q('d4importFile').files[0]); + } + function p4downloadUserInfo() { if (xxdialogMode) return; var x = 'Download the list of users with one of the file formats below.

'; diff --git a/webserver.js b/webserver.js index 92ad6b79..e7e73754 100644 --- a/webserver.js +++ b/webserver.js @@ -388,7 +388,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if (err) return fn(err); if (hash == user.hash) { // Update the password to the stronger format. - require('./pass').hash(pass, function (err, salt, hash) { if (err) throw err; user.salt = salt; user.hash = hash; delete user.passtype; obj.db.SetUser(user); }); + require('./pass').hash(pass, function (err, salt, hash, tag) { if (err) throw err; user.salt = salt; user.hash = hash; delete user.passtype; obj.db.SetUser(user); }, 0); if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; } return fn(null, user._id); } @@ -396,14 +396,14 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { }); } else { // Default strong password hashing (pbkdf2 SHA384) - require('./pass').hash(pass, user.salt, function (err, hash) { + require('./pass').hash(pass, user.salt, function (err, hash, tag) { if (err) return fn(err); if (hash == user.hash) { if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { fn('locked'); return; } return fn(null, user._id); } fn(new Error('invalid password'), null, user.passhint); - }); + }, 0); } } } @@ -844,7 +844,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { req.session.userid = user._id; req.session.domainid = domain.id; // Create a user, generate a salt and hash the password - require('./pass').hash(req.body.password1, function (err, salt, hash) { + require('./pass').hash(req.body.password1, function (err, salt, hash, tag) { if (err) throw err; user.salt = salt; user.hash = hash; @@ -853,7 +853,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Send the verification email if ((obj.parent.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (obj.common.validateEmail(user.email, 1, 256) == true)) { obj.parent.mailserver.sendAccountCheckMail(domain, user.name, user.email); } - }); + }, 0); obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', username: user.name, account: obj.CloneSafeUser(user), action: 'accountcreate', msg: 'Account created, email is ' + req.body.email, domain: domain.id }); } res.redirect(domain.url); @@ -898,7 +898,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { } // Check if the password is the same as the previous one - require('./pass').hash(req.body.rpassword1, user.salt, function (err, hash) { + require('./pass').hash(req.body.rpassword1, user.salt, function (err, hash, tag) { if (user.hash == hash) { // This is the same password, request a password change again req.session.loginmode = '6'; @@ -906,7 +906,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { res.redirect(domain.url); } else { // Update the password, use a different salt. - require('./pass').hash(req.body.rpassword1, function (err, salt, hash) { + require('./pass').hash(req.body.rpassword1, function (err, salt, hash, tag) { if (err) throw err; user.salt = salt; user.hash = hash; @@ -920,9 +920,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { req.session.userid = userid; req.session.domainid = domain.id; completeLoginRequest(req, res, domain, obj.users[userid], userid, req.session.tokenusername, req.session.tokenpassword); - }); + }, 0); } - }); + }, 0); } else { // Failed, error out. delete req.session.loginmode; @@ -1057,7 +1057,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Set a temporary password obj.crypto.randomBytes(16, function (err, buf) { var newpass = buf.toString('base64').split('=').join('').split('/').join(''); - require('./pass').hash(newpass, function (err, salt, hash) { + require('./pass').hash(newpass, function (err, salt, hash, tag) { var userinfo = null; if (err) throw err; @@ -1076,7 +1076,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Send the new password res.render(obj.path.join(obj.parent.webViewsPath, 'message'), { title: domain.title, title2: domain.title2, title3: 'Account Verification', message: '
Password for account ' + EscapeHtml(user.name) + ' has been reset to:
' + EscapeHtml(newpass) + '
Login and go to the \"My Account\" tab to update your password. Go to login page.' }); - }); + }, 0); }); } } else { @@ -1151,14 +1151,14 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { }); } else { // Default strong password hashing (pbkdf2 SHA384) - require('./pass').hash(password, user.salt, function (err, hash) { + require('./pass').hash(password, user.salt, function (err, hash, tag) { if (err) return func(false); if (hash == user.hash) { if ((user.siteadmin) && (user.siteadmin != 0xFFFFFFFF) && (user.siteadmin & 32) != 0) { return func(false); } // Account is locked return func(true); // Allow password change } func(false); - }); + }, 0); } } @@ -1178,7 +1178,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { obj.checkUserPassword(domain, user, req.body.apassword0, function (result) { if (result == true) { // Update the password - require('./pass').hash(req.body.apassword1, function (err, salt, hash) { + require('./pass').hash(req.body.apassword1, function (err, salt, hash, tag) { if (err) throw err; user.salt = salt; user.hash = hash; @@ -1189,7 +1189,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { req.session.viewmode = 2; res.redirect(domain.url); obj.parent.DispatchEvent(['*', 'server-users'], obj, { etype: 'user', username: user.name, action: 'passchange', msg: 'Account password changed: ' + user.name, domain: domain.id }); - }); + }, 0); } }); }