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 de4a3cf9..dd866d7a 100644 Binary files a/public/images/images16.png and b/public/images/images16.png differ diff --git a/public/images/webp/group-256.webp b/public/images/webp/group-256.webp new file mode 100644 index 00000000..d16d79ce Binary files /dev/null and b/public/images/webp/group-256.webp differ 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 @@
+