From fbbc6193647b4997a0cfe2aab63b9fe7b76924c8 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Sun, 29 Dec 2019 18:10:58 -0800 Subject: [PATCH] Lots of progress on security user group UI. --- meshcentral.js | 3 +- meshmail.js | 13 +- meshuser.js | 106 +++++++++++- public/images/images16.png | Bin 5801 -> 6503 bytes public/images/webp/group-256.webp | Bin 0 -> 7964 bytes public/scripts/common-0.0.1.js | 2 +- public/styles/style.css | 8 + views/default.handlebars | 269 +++++++++++++++++++++++++++++- webserver.js | 2 +- 9 files changed, 383 insertions(+), 20 deletions(-) create mode 100644 public/images/webp/group-256.webp diff --git a/meshcentral.js b/meshcentral.js index f79a3d29..7e6d8491 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -123,7 +123,7 @@ function CreateMeshCentralServer(config, args) { 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', 'rediraliasport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'shownodes', 'showmeshes', 'showevents', 'showsmbios', 'showpower', 'clearpower', 'showiplocations', 'help', 'exactports', 'xinstall', 'xuninstall', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbexportmin', 'dbimport', 'dbmerge', 'dbencryptkey', 'selfupdate', 'tlsoffload', 'userallowedip', 'userblockedip', 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport', 'logintoken', 'logintokenkey', 'logintokengen', 'logintokengen', 'mailtokengen', 'admin', 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore', 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'vaultpushconfigfiles', 'vaultpullconfigfiles', 'vaultdeleteconfigfiles', 'configkey', 'loadconfigfromdb', 'npmpath', 'memorytracking', 'serverid', 'recordencryptionrecode', 'vault', 'token', 'unsealkey', 'name', 'log', 'dbstats']; + var validArguments = ['_', 'notls', 'user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'rediraliasport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'showusergroups', 'shownodes', 'showmeshes', 'showevents', 'showsmbios', 'showpower', 'clearpower', 'showiplocations', 'help', 'exactports', 'xinstall', 'xuninstall', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbexportmin', 'dbimport', 'dbmerge', 'dbencryptkey', 'selfupdate', 'tlsoffload', 'userallowedip', 'userblockedip', 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport', 'logintoken', 'logintokenkey', 'logintokengen', 'logintokengen', 'mailtokengen', 'admin', 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore', 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'vaultpushconfigfiles', 'vaultpullconfigfiles', 'vaultdeleteconfigfiles', 'configkey', 'loadconfigfromdb', 'npmpath', 'memorytracking', 'serverid', 'recordencryptionrecode', 'vault', 'token', 'unsealkey', 'name', 'log', 'dbstats']; for (var arg in obj.args) { obj.args[arg.toLocaleLowerCase()] = obj.args[arg]; if (validArguments.indexOf(arg.toLocaleLowerCase()) == -1) { console.log('Invalid argument "' + arg + '", use --help.'); return; } } if (obj.args.mongodb == true) { console.log('Must specify: --mongodb [connectionstring] \r\nSee https://docs.mongodb.com/manual/reference/connection-string/ for MongoDB connection string.'); return; } for (i in obj.config.settings) { obj.args[i] = obj.config.settings[i]; } // Place all settings into arguments, arguments have already been placed into settings so arguments take precedence. @@ -437,6 +437,7 @@ function CreateMeshCentralServer(config, args) { if (obj.args.deletedefaultdomain) { obj.db.DeleteDomain('', function () { console.log('Deleted default domain.'); process.exit(); }); return; } if (obj.args.showall) { obj.db.GetAll(function (err, docs) { console.log(docs); process.exit(); }); return; } if (obj.args.showusers) { obj.db.GetAllType('user', function (err, docs) { console.log(docs); process.exit(); }); return; } + if (obj.args.showusergroups) { obj.db.GetAllType('ugrp', function (err, docs) { console.log(docs); process.exit(); }); return; } if (obj.args.shownodes) { obj.db.GetAllType('node', function (err, docs) { console.log(docs); process.exit(); }); return; } if (obj.args.showmeshes) { obj.db.GetAllType('mesh', function (err, docs) { console.log(docs); process.exit(); }); return; } if (obj.args.showevents) { obj.db.GetAllEvents(function (err, docs) { console.log(docs); process.exit(); }); return; } diff --git a/meshmail.js b/meshmail.js index 29275031..e00cdcd5 100644 --- a/meshmail.js +++ b/meshmail.js @@ -139,7 +139,7 @@ module.exports.CreateMeshMail = function (parent) { if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null) || (parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) return; // If the server name is not set, invitation not possible. // Set all the options. - var options = { username: username, accountname: accountname, email: email, servername: domain.title, password: password }; + var options = { username: username, accountname: accountname, email: email, servername: domain.title ? domain.title : 'MeshCentral', password: password }; // Send the email obj.pendingMails.push({ to: email, from: parent.config.smtp.from, subject: mailReplacements(template.htmlSubject, domain, options), text: mailReplacements(template.txt, domain, options), html: mailReplacements(template.html, domain, options) }); @@ -152,7 +152,7 @@ module.exports.CreateMeshMail = function (parent) { if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null) || (parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) return; // If the server name is not set, no reset possible. // Set all the options. - var options = { username: username, email: email, servername: domain.title }; + var options = { username: username, email: email, servername: domain.title ? domain.title : 'MeshCentral' }; options.cookie = obj.parent.encodeCookie({ u: domain.id + '/' + username.toLowerCase(), e: email, a: 1 }, obj.mailCookieEncryptionKey); // Send the email @@ -166,7 +166,7 @@ module.exports.CreateMeshMail = function (parent) { if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null) || (parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) return; // If the server name is not set, don't validate the email address. // Set all the options. - var options = { username: username, email: email, servername: domain.title }; + var options = { username: username, email: email, servername: domain.title ? domain.title : 'MeshCentral' }; options.cookie = obj.parent.encodeCookie({ u: domain.id + '/' + username, e: email, a: 2 }, obj.mailCookieEncryptionKey); // Send the email @@ -180,7 +180,7 @@ module.exports.CreateMeshMail = function (parent) { if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null) || (parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) return; // If the server name is not set, don't validate the email address. // Set all the template replacement options and generate the final email text (both in txt and html formats). - var options = { username: username, name: name, email: email, installflags: flags, msg: msg, meshid: meshid, meshidhex: meshid.split('/')[2], servername: domain.title }; + var options = { username: username, name: name, email: email, installflags: flags, msg: msg, meshid: meshid, meshidhex: meshid.split('/')[2], servername: domain.title ? domain.title : 'MeshCentral' }; options.windows = ((os == 0) || (os == 1)) ? 1 : 0; options.linux = ((os == 0) || (os == 2)) ? 1 : 0; options.osx = ((os == 0) || (os == 3)) ? 1 : 0; @@ -220,7 +220,10 @@ module.exports.CreateMeshMail = function (parent) { if (err == null) { console.log('SMTP mail server ' + parent.config.smtp.host + ' working as expected.'); } else { - console.log('SMTP mail server ' + parent.config.smtp.host + ' failed: ' + JSON.stringify(err)); + // Remove all non-object types from error to avoid a JSON stringify error. + var err2 = {}; + for (var i in err) { if (typeof (err[i]) != 'object') { err2[i] = err[i]; } } + console.log('SMTP mail server ' + parent.config.smtp.host + ' failed: ' + JSON.stringify(err2)); } }); }; diff --git a/meshuser.js b/meshuser.js index 20021f21..525484f4 100644 --- a/meshuser.js +++ b/meshuser.js @@ -1472,16 +1472,114 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } break; } + case 'usergroups': + { + if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { return; } + + // Request a list of all user groups this user as rights to + db.GetAllTypeNoTypeField('ugrp', domain.id, function (err, docs) { + try { ws.send(JSON.stringify({ action: 'usergroups', ugroups: docs, tag: command.tag })); } catch (ex) { } + }); + break; + } case 'createusergroup': { - // TODO - //console.log(command); + var err = null; + try { + // Check if we have new group restriction + if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { err = 'Permission denied'; } + + // In some situations, we need a verified email address to create a device group. + 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. + + // Create user group + else if (common.validateString(command.name, 1, 64) == false) { err = 'Invalid group name'; } // User group name is between 1 and 64 characters + else if ((command.desc != null) && (common.validateString(command.desc, 0, 1024) == false)) { err = 'Invalid group description'; } // User group description is between 0 and 1024 characters + } catch (ex) { err = 'Validation exception: ' + ex; } + + // Handle any errors + if (err != null) { + if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'createusergroup', responseid: command.responseid, result: err })); } catch (ex) { } } + break; + } + + // We only create Agent-less Intel AMT mesh (Type1), or Agent mesh (Type2) + parent.crypto.randomBytes(48, function (err, buf) { + // Create new device group identifier + var ugrpid = 'ugrp/' + domain.id + '/' + buf.toString('base64').replace(/\+/g, '@').replace(/\//g, '$'); + + // Create the new device group + var ugrp = { type: 'ugrp', _id: ugrpid, name: command.name, desc: command.desc, domain: domain.id, links: {} }; + db.Set(common.escapeLinksFieldName(ugrp)); + //parent.meshes[ugrpid] = ugrp; + + // Event the device group creation + var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: ugrpid, name: command.name, desc: command.desc, action: 'createusergroup', links: links, msg: 'User group created: ' + command.name, domain: domain.id }; + parent.parent.DispatchEvent(['*', ugrpid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon. + + try { ws.send(JSON.stringify({ action: 'createusergroup', responseid: command.responseid, result: 'ok', ugrpid: ugrpid, links: links })); } catch (ex) { } + }); break; } case 'deleteusergroup': { - // TODO - //console.log(command); + if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { return; } + + // Change the name or description of a user group + if (common.validateString(command.ugrpid, 1, 1024) == false) break; // Check the user group id + var ugroupidsplit = command.ugrpid.split('/'); + if ((ugroupidsplit.length != 3) || (ugroupidsplit[0] != 'ugrp') || (ugroupidsplit[1] != domain.id)) break; + + db.Get(command.ugrpid, function (err, groups) { + if ((err != null) || (groups.length != 1)) return; + var group = groups[0]; + db.Remove(group._id); + var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: group._id, action: 'deleteusergroup', msg: change, 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(['*', group._id, user._id], obj, event); + }); + break; + } + case 'editusergroup': + { + if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { return; } + + // Change the name or description of a user group + if (common.validateString(command.ugrpid, 1, 1024) == false) break; // Check the user group id + var ugroupidsplit = command.ugrpid.split('/'); + if ((ugroupidsplit.length != 3) || (ugroupidsplit[0] != 'ugrp') || (ugroupidsplit[1] != domain.id)) break; + + db.Get(command.ugrpid, function (err, groups) { + if ((err != null) || (groups.length != 1)) return; + var group = groups[0], change = ''; + + if ((common.validateString(command.name, 1, 64) == true) && (command.name != group.name)) { change = 'User group name changed from "' + group.name + '" to "' + command.name + '"'; group.name = command.name; } + if ((common.validateString(command.desc, 0, 1024) == true) && (command.desc != group.desc)) { if (change != '') change += ' and description changed'; else change += 'User group "' + group.name + '" description changed'; group.desc = command.desc; } + if (change != '') { + db.Set(common.escapeLinksFieldName(group)); + var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: group._id, name: group.name, desc: group.desc, action: 'usergroupchange', links: group.links, msg: change, 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(['*', group._id, user._id], obj, event); + } + }); + break; + } + case 'addusertousergroup': + { + if ((user.siteadmin & SITERIGHT_USERGROUPS) == 0) { return; } + + // Change the name or description of a user group + if (common.validateString(command.ugrpid, 1, 1024) == false) break; // Check the user group id + var ugroupidsplit = command.ugrpid.split('/'); + if ((ugroupidsplit.length != 3) || (ugroupidsplit[0] != 'ugrp') || (ugroupidsplit[1] != domain.id)) break; + + db.Get(command.ugrpid, function (err, groups) { + if ((err != null) || (groups.length != 1)) return; + var group = groups[0]; + + // TODO + console.log(command); + }); break; } case 'changemeshnotify': diff --git a/public/images/images16.png b/public/images/images16.png index de4a3cf9bbf52e839872ee5899a4391828d72836..dd866d7acdc2235fc582fe838d442e20035fd217 100644 GIT binary patch delta 3809 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 X@Vve53PCx#00000NkvXXu0mjfIiOti delta 3101 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 b36cNJM@at9?0?_?W1A-sXa3(j0rGzVhfiIp literal 0 HcmV?d00001 diff --git a/public/scripts/common-0.0.1.js b/public/scripts/common-0.0.1.js index 53799a5c..9793ca46 100644 --- a/public/scripts/common-0.0.1.js +++ b/public/scripts/common-0.0.1.js @@ -16,7 +16,7 @@ function QE(x, y) { try { Q(x).disabled = !y; } catch (x) { } } function QV(x, y) { try { QS(x).display = (y ? '' : 'none'); } catch (x) { } } // "Q" visible function QA(x, y) { Q(x).innerHTML += y; } // "Q" append function QH(x, y) { Q(x).innerHTML = y; } // "Q" html -function QC(x) { try { return Q(x).classList; } catch (x) { } } // "Q" class +function QC(x) { try { return Q(x).classList; } catch (x) { } } // "Q" class // Move cursor to end of input box function inputBoxFocus(x) { Q(x).focus(); var v = Q(x).value; Q(x).value = ''; Q(x).value = v; } diff --git a/public/styles/style.css b/public/styles/style.css index a0712e0b..f34b4ed2 100644 --- a/public/styles/style.css +++ b/public/styles/style.css @@ -1363,6 +1363,14 @@ a { float: left; } +.m4 { + background: url(../images/images16.png) -128px 0px; + height: 16px; + width: 16px; + border: none; + float: left; +} + .si1 { background: url(../images/icons16.png) 0px 0px; height: 16px; diff --git a/views/default.handlebars b/views/default.handlebars index 2799b916..249a9364 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -959,6 +959,29 @@
+