From 8dd07495f50b756ccea83bd5bc5848c6265ddf41 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Sun, 21 Aug 2022 21:19:34 -0700 Subject: [PATCH] MeshCentral will now auto-create LDAP user groups and sync users to their membership groups when the login using LDAP. (#4415) --- meshuser.js | 27 +++++++-- views/default.handlebars | 30 +++++----- webserver.js | 115 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 144 insertions(+), 28 deletions(-) diff --git a/meshuser.js b/meshuser.js index d365164e..afcc566c 100644 --- a/meshuser.js +++ b/meshuser.js @@ -1514,11 +1514,11 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use db.Set(ugrp); if (db.changeStream == false) { parent.userGroups[ugrpid] = ugrp; } - // Event the device group creation + // Event the user group creation var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: ugrpid, name: ugrp.name, desc: ugrp.desc, action: 'createusergroup', links: ugrp.links, msgid: 69, msgArgv: [ugrp.name], msg: 'User group created: ' + ugrp.name, ugrpdomain: domain.id }; parent.parent.DispatchEvent(['*', ugrpid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon. - // Event any pending events, these must be sent out after the group creation event is displatched. + // Event any pending events, these must be sent out after the group creation event is dispatched. for (var i in pendingDispatchEvents) { var ev = pendingDispatchEvents[i]; parent.parent.DispatchEvent(ev[0], ev[1], ev[2]); } // Log in the auth log @@ -1561,6 +1561,13 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } var group = groups[0]; + // If this user group is an externally managed user group, it can't be deleted unless there are no users in it. + if (group.membershipType != null) { + var userCount = 0; + if (group.links != null) { for (var i in group.links) { if (i.startsWith('user/')) { userCount++; } } } + if (userCount > 0) return; + } + // Unlink any user and meshes that have a link to this group if (group.links) { for (var i in group.links) { @@ -1621,7 +1628,8 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use change = ''; var group = parent.userGroups[command.ugrpid]; if (group != null) { - 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 this user group is an externally managed user group, the name of the user group can't be edited + if ((group.membershipType == null) && (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 ((typeof command.consent == 'number') && (command.consent != group.consent)) { if (change != '') change += ' and consent changed'; else change += 'User group "' + group.name + '" consent changed'; group.consent = command.consent; } @@ -5770,6 +5778,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // Get the user group var group = parent.userGroups[command.ugrpid]; if (group != null) { + // If this user group is an externally managed user group, we can't add users to it. + if ((group != null) && (group.membershipType != null)) return; + if (group.links == null) { group.links = {}; } var unknownUsers = [], addedCount = 0, failCount = 0, knownUsers = []; @@ -5779,7 +5790,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use var chguser = parent.users[chguserid]; if (chguser == null) { chguserid = 'user/' + addUserDomain.id + '/' + command.usernames[i]; chguser = parent.users[chguserid]; } if (chguser != null) { - // Add mesh to user + // Add usr group to user if (chguser.links == null) { chguser.links = {}; } chguser.links[group._id] = { rights: 1 }; db.SetUser(chguser); @@ -6235,6 +6246,12 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use var chguser = parent.users[command.userid]; if (chguser != null) { + // Get the user group + var group = parent.userGroups[command.ugrpid]; + + // If this user group is an externally managed user group, we can't remove a user from it. + if ((group != null) && (group.membershipType != null)) return; + if ((chguser.links != null) && (chguser.links[command.ugrpid] != null)) { delete chguser.links[command.ugrpid]; @@ -6248,8 +6265,6 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use parent.parent.DispatchEvent([chguser._id], obj, 'resubscribe'); } - // Get the user group - var group = parent.userGroups[command.ugrpid]; if (group != null) { // Remove the user from the group if ((group.links != null) && (group.links[command.userid] != null)) { diff --git a/views/default.handlebars b/views/default.handlebars index 734ea0c8..016b8e63 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -7539,7 +7539,7 @@ if (usergroups != null) { var userGroupCount = 0, newUserGroup = false; for (var i in usergroups) { - if (usergroups[i]._id.split('/')[1] != nodeid.split('/')[1]) continue; + if ((usergroups[i].membershipType != null) || (usergroups[i]._id.split('/')[1] != nodeid.split('/')[1])) continue; userGroupCount++; if ((currentNode.links == null) || (currentNode.links[i] == null)) { newUserGroup = true; } } if ((userGroupCount > 0) && (newUserGroup)) { x += ' ' + "Add User Group" + ''; } @@ -12562,7 +12562,7 @@ if (usergroups != null) { var userGroupCount = 0, newUserGroup = false; for (var i in usergroups) { - if (usergroups[i]._id.split('/')[1] != currentMesh._id.split('/')[1]) continue; + if ((usergroups[i].membershipType != null) || (usergroups[i]._id.split('/')[1] != currentMesh._id.split('/')[1])) continue; userGroupCount++; if ((currentMesh.links == null) || (currentMesh.links[i] == null)) { newUserGroup = true; } } @@ -13035,7 +13035,7 @@ if (selected == null) { var ousergroups = getOrderedList(usergroups, 'name'); for (var i in ousergroups) { - if (currentNode._id.split('/')[1] != ousergroups[i]._id.split('/')[1]) continue; + if ((ousergroups[i].membershipType != null) || (currentNode._id.split('/')[1] != ousergroups[i]._id.split('/')[1])) continue; if ((currentNode.links == null) || (currentNode.links[ousergroups[i]._id] == null)) { y += ''; } } } else { @@ -15151,7 +15151,7 @@ // Add user group name var gname = EscapeHtml(group.name); if (gname.length == 0) { gname = '' + "None" + ''; } - if ((userinfo.siteadmin & 256) != 0) { gname = '' + gname + ' '; } + if ((currentUserGroup.membershipType == null) && ((userinfo.siteadmin & 256) != 0)) { gname = '' + gname + ' '; } QH('p51groupName', gname); var usercount = 0, meshcount = 0, devicecount = 0; @@ -15172,8 +15172,11 @@ x += addDeviceAttribute("Domain", (d != '')?EscapeHtml(d):('' + "Default" + '')); x += addDeviceAttribute("Group Identifier", EscapeHtml(group._id)); } + if (currentUserGroup.membershipType != null) { + x += addDeviceAttribute("Group Type", EscapeHtml(currentUserGroup.membershipType)); + } if ((userinfo.siteadmin & 256) != 0) { - x += addDeviceAttribute("Description", '' + desc + ' '); + x += addDeviceAttribute("Description", '' + desc + ' '); } else { x += addDeviceAttribute("Description", desc); } @@ -15217,7 +15220,7 @@ QH('p51group', x); x = '
'; - if ((userinfo.siteadmin & 256) != 0) { + if ((currentUserGroup.membershipType == null) && ((userinfo.siteadmin & 256) != 0)) { x += ' ' + "Add Users" + ''; } x += ''; @@ -15235,7 +15238,8 @@ // Display all users for this user group for (var i in sortedusers) { - var trash = ''; + var trash = ''; + if (currentUserGroup.membershipType == null) { trash = ''; } var username = EscapeHtml(decodeURIComponent(sortedusers[i].name)); if (users != null) { username = '' + username + ''; } x += ''; @@ -15294,7 +15298,7 @@ if (count == 1) { x += ''; } x += '
' + "Group Members" + '
 ' + username + '
' + trash + '
 ' + "No devices in common" + '
'; - if ((userinfo.siteadmin & 256) != 0) { + if (((currentUserGroup.membershipType == null) || (usercount == 0)) && ((userinfo.siteadmin & 256) != 0)) { x += '
' + "Delete User Group" + '
'; } @@ -15349,9 +15353,9 @@ meshserver.send({ action: 'removemeshuser', meshid: meshid, userid: currentUserGroup._id }); } - function p51editgroup(focus) { + function p51editgroup(focus, nameReadOnly) { if (xxdialogMode) return; - var x = addHtmlValue("Name", ''); + var x = addHtmlValue("Name", ''); x += addHtmlValue("Description", '
'); setDialogMode(2, "Edit User Group", 3, p51editgroupEx, x); Q('dp51name').value = currentUserGroup.name; @@ -15893,7 +15897,7 @@ if ((userinfo.siteadmin & 256) != 0) { var userGroupCount = 0, newUserGroup = false; for (var i in usergroups) { - if (usergroups[i]._id.split('/')[1] != currentUser._id.split('/')[1]) continue; + if ((usergroups[i].membershipType != null) || (usergroups[i]._id.split('/')[1] != currentUser._id.split('/')[1])) continue; userGroupCount++; if ((currentUser.links == null) || (currentUser.links[i] == null)) { newUserGroup = true; } } @@ -15911,7 +15915,7 @@ groupname = EscapeHtml(group.name); if (usergroups != null) { groupname = '' + groupname + ''; } } - if ((userinfo.siteadmin & 256) != 0) { trash = ''; } + if ((group.membershipType == null) && ((userinfo.siteadmin & 256) != 0)) { trash = ''; } x += '
 ' + groupname + '
' + trash + '
'; } } @@ -15986,7 +15990,7 @@ if (xxdialogMode || (usergroups == null)) return; var y = ''; for (var i in usergroups) { - if (usergroups[i]._id.split('/')[1] != currentUser._id.split('/')[1]) continue; + if ((usergroups[i].membershipType != null) || (usergroups[i]._id.split('/')[1] != currentUser._id.split('/')[1])) continue; if ((currentUser.links == null) || (currentUser.links[i] == null)) { y += ''; } } var x = addHtmlValue("User Group", '
'); diff --git a/webserver.js b/webserver.js index 079fe5bc..b40ce53e 100644 --- a/webserver.js +++ b/webserver.js @@ -472,21 +472,18 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF if (username == null) { username = shortname; } var userid = 'user/' + domain.id + '/' + shortname; + // Get the list of groups this user is a member of. + var userMemberships = xxuser[(typeof domain.ldapusergroups == 'string') ? domain.ldapusergroups : 'memberOf']; + if (typeof userMemberships == 'string') { userMemberships = [userMemberships]; } + if (Array.isArray(userMemberships) == false) { userMemberships = []; } + // See if the user is required to be part of an LDAP user group in order to log into this server. if (typeof domain.ldapuserrequiredgroupmembership == 'string') { domain.ldapuserrequiredgroupmembership = [domain.ldapuserrequiredgroupmembership]; } if (Array.isArray(domain.ldapuserrequiredgroupmembership) && (domain.ldapuserrequiredgroupmembership.length > 0)) { - // We must be part of a LDAP user group, lets get the list of groups this user is a member of. - const memberOfKey = (typeof domain.ldapusergroups == 'string') ? domain.ldapusergroups : 'memberOf'; - var userMemberships = xxuser[memberOfKey]; - if (typeof userMemberships == 'string') { userMemberships = [userMemberships]; } - if (Array.isArray(userMemberships) == false) { userMemberships = []; } - // Look for a matching LDAP user group var userMembershipMatch = false; for (var i in domain.ldapuserrequiredgroupmembership) { if (userMemberships.indexOf(domain.ldapuserrequiredgroupmembership[i]) >= 0) { userMembershipMatch = true; } } - - // If there is no match, deny the login - if (userMembershipMatch === false) { fn('denied'); return; } + if (userMembershipMatch === false) { fn('denied'); return; } // If there is no match, deny the login } // Get the email address for this LDAP user @@ -578,6 +575,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // Indicate that this user has a image if (userimage != null) { user.flags = 1; } + // Synd the user with LDAP matching user groups + if (syncExternalUserGroups(domain, user, userMemberships, 'ldap') == true) { userChanged = true; } + obj.users[user._id] = user; obj.db.SetUser(user); var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountcreate', msgid: 128, msgArgs: [user.name], msg: 'Account created, name is ' + user.name, domain: domain.id }; @@ -612,6 +612,9 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF if ((userimage != null) && ((user.flags == null) || ((user.flags & 1) == 0))) { if (user.flags == null) { user.flags = 1; } else { user.flags += 1; } userChanged = true; } if ((userimage == null) && (user.flags != null) && ((user.flags & 1) != 0)) { if (user.flags == 1) { delete user.flags; } else { user.flags -= 1; } userChanged = true; } + // Synd the user with LDAP matching user groups + if (syncExternalUserGroups(domain, user, userMemberships, 'ldap') == true) { userChanged = true; } + // If the user changed, save the changes to the database here if (userChanged) { obj.db.SetUser(user); @@ -8694,5 +8697,99 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF return r; } + // Sync an account with an external user group. + // Return true if the user was changed + function syncExternalUserGroups(domain, user, userMemberships, userMembershipType) { + var userChanged = false; + if (user.links == null) { user.links = {}; } + + // Create a user of memberships for this user that type + var existingUserMemberships = {}; + for (var i in user.links) { + if (i.startsWith('ugrp/') && (obj.userGroups[i] != null) && (obj.userGroups[i].membershipType == userMembershipType)) { existingUserMemberships[i] = obj.userGroups[i]; } + } + + // Go thru the list user memberships and create and add to any user groups as needed + for (var i in userMemberships) { + const membership = userMemberships[i]; + var ugrpid = 'ugrp/' + domain.id + '/' + obj.crypto.createHash('sha384').update(membership).digest('base64').replace(/\+/g, '@').replace(/\//g, '$'); + var ugrp = obj.userGroups[ugrpid]; + if (ugrp == null) { + // This user group does not exist, create it + ugrp = { type: 'ugrp', _id: ugrpid, name: membership, domain: domain.id, membershipType: userMembershipType, links: {} }; + + // Save the new group + db.Set(ugrp); + if (db.changeStream == false) { obj.userGroups[ugrpid] = ugrp; } + + // Event the user group creation + var event = { etype: 'ugrp', ugrpid: ugrpid, name: ugrp.name, action: 'createusergroup', links: ugrp.links, msgid: 69, msgArgv: [ugrp.name], msg: 'User group created: ' + ugrp.name, ugrpdomain: domain.id }; + parent.DispatchEvent(['*', ugrpid, user._id], obj, event); // Even if DB change stream is active, this event must be acted upon. + + // Log in the auth log + if (parent.authlog) { parent.authLog('https', 'Created ' + userMembershipType + ' user group ' + ugrp.name); } + } + + if (existingUserMemberships[ugrpid] == null) { + // This user is not part of the user group, add it. + if (user.links == null) { user.links = {}; } + user.links[ugrp._id] = { rights: 1 }; + userChanged = true; + db.SetUser(user); + parent.DispatchEvent([user._id], obj, 'resubscribe'); + + // Notify user change + var targets = ['*', 'server-users', user._id]; + var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msgid: 67, msgArgs: [user.name], msg: 'User group membership changed: ' + user.name, domain: domain.id }; + 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. + parent.DispatchEvent(targets, obj, event); + + // Add a user to the user group + ugrp.links[user._id] = { userid: user._id, name: user.name, rights: 1 }; + db.Set(ugrp); + + // Notify user group change + var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: ugrp._id, name: ugrp.name, desc: ugrp.desc, action: 'usergroupchange', links: ugrp.links, msgid: 71, msgArgs: [user.name, ugrp.name], msg: 'Added user(s) ' + user.name + ' to user group ' + ugrp.name, addUserDomain: domain.id }; + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come. + parent.DispatchEvent(['*', ugrp._id, user._id], obj, event); + } else { + // User is already part of this user group + delete existingUserMemberships[ugrpid]; + } + } + + // Remove the user from any memberships they don't belong to anymore + for (var ugrpid in existingUserMemberships) { + var ugrp = obj.userGroups[ugrpid]; + if ((user.links != null) && (user.links[ugrpid] != null)) { + delete user.links[ugrpid]; + + // Notify user change + var targets = ['*', 'server-users', user._id, user._id]; + var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountchange', msgid: 67, msgArgs: [user.name], msg: 'User group membership changed: ' + user.name, domain: domain.id }; + 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. + parent.DispatchEvent(targets, obj, event); + + db.SetUser(user); + parent.DispatchEvent([user._id], obj, 'resubscribe'); + } + + if (ugrp != null) { + // Remove the user from the group + if ((ugrp.links != null) && (ugrp.links[user._id] != null)) { + delete ugrp.links[user._id]; + db.Set(ugrp); + + // Notify user group change + var event = { etype: 'ugrp', userid: user._id, username: user.name, ugrpid: ugrp._id, name: ugrp.name, desc: ugrp.desc, action: 'usergroupchange', links: ugrp.links, msgid: 72, msgArgs: [user.name, ugrp.name], msg: 'Removed user ' + user.name + ' from user group ' + ugrp.name, domain: domain.id }; + if (db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to change the user group. Another event will come. + parent.DispatchEvent(['*', ugrp._id, user._id], obj, event); + } + } + } + + return userChanged; + } + return obj; };