diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index a5078e91..6abec2da 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -549,6 +549,8 @@ "ldapUserPhoneNumber": { "type": "string", "default": "telephoneNumber", "description": "The LDAP value to use for the user's phone number." }, "ldapUserImage": { "type": "string", "default": "thumbnailPhoto", "description": "The LDAP value to use for the user's image." }, "ldapSaveUserToFile": { "type": "string", "default": null, "description": "When set to a filename, for example c:\\temp\\ldapusers.txt, MeshCentral will save the LDAP user object to this file each time a user logs in. This is used for debugging LDAP issues." }, + "ldapUserGroups": { "type": "string", "default": "memberOf", "description": "The LDAP value to use for the user's group memberships." }, + "ldapUserRequiredGroupMembership": { "type": [ "string", "array" ], "default": null, "description": "A list of LDAP groups. Users must be part of at least one of these groups to allow login. If null, all users are allowed to login." }, "ldapOptions": { "type": "object", "description": "LDAP options passed to ldapauth-fork" }, "agentInviteCodes": { "type": "boolean", "default": false, "description": "Enabled a feature where you can set one or more invitation codes in a device group. You can then give a invitation link to users who can use it to download the agent." }, "agentNoProxy": { "type": "boolean", "default": false, "description": "When enabled, all newly installed MeshAgents will be instructed to no use a HTTP/HTTPS proxy even if one is configured on the remote system" }, diff --git a/sample-config-advanced.json b/sample-config-advanced.json index 79d05fa7..2403085c 100644 --- a/sample-config-advanced.json +++ b/sample-config-advanced.json @@ -533,6 +533,8 @@ "_LDAPUserName": "gecos", "_LDAPUserKey": "uid", "_LDAPUserEmail": "otherMail", + "_LDAPUserGroups": "memberOf", + "_LDAPUserRequiredGroupMembership": [ "CN=Domain Admins,CN=Users,DC=sample,DC=com" ], "_LDAPPptions": { "url": "test", "anne": { diff --git a/views/default.handlebars b/views/default.handlebars index 326106e6..734ea0c8 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -14205,7 +14205,8 @@ 151: "Started Web-VNC session \"{0}\".", // Not in use yet 152: "No longer a relay for \"{0}\".", 153: "Is a relay for \"{0}\".", - 154: "Account changed to sync with LDAP data." + 154: "Account changed to sync with LDAP data.", + 155: "Denied user login from {0}, {1}, {2}" }; var eventsShortMessageId = { diff --git a/webserver.js b/webserver.js index 03367399..079fe5bc 100644 --- a/webserver.js +++ b/webserver.js @@ -472,6 +472,23 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF if (username == null) { username = shortname; } var userid = 'user/' + domain.id + '/' + shortname; + // 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; } + } + // Get the email address for this LDAP user var email = null; if (domain.ldapuseremail) { email = xxuser[domain.ldapuseremail]; } else if (xxuser['mail']) { email = xxuser['mail']; } // Use given feild name or default @@ -1215,6 +1232,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF const ua = getUserAgentInfo(req); obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'User login attempt on locked account from ' + req.clientIp, msgid: 109, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] }); obj.setbadLogin(req); + } else if (err == 'denied') { + parent.debug('web', 'handleLoginRequest: login failed, access denied'); + req.session.messageid = 111; // Access denied. + const ua = getUserAgentInfo(req); + obj.parent.DispatchEvent(['*', 'server-users', xuserid], obj, { action: 'authfail', userid: xuserid, username: xusername, domain: domain.id, msg: 'Denied user login from ' + req.clientIp, msgid: 155, msgArgs: [req.clientIp, ua.browserStr, ua.osStr] }); + obj.setbadLogin(req); } else { parent.debug('web', 'handleLoginRequest: login failed, bad username and password'); req.session.messageid = 112; // Login failed, check username and password. @@ -2595,13 +2618,19 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF } else if (req.query.user && req.query.pass) { // User credentials are being passed in the URL. WARNING: Putting credentials in a URL is bad security... but people are requesting this option. obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) { - if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + userid + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); } - parent.debug('web', 'handleRootRequest: user/pass in URL auth ok.'); - req.session.userid = userid; - delete req.session.currentNode; - req.session.ip = req.clientIp; // Bind this session to the IP address of the request - setSessionRandom(req); - handleRootRequestEx(req, res, domain, direct); + if ((userid != null) && (err == null)) { + // Login success + if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + userid + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); } + parent.debug('web', 'handleRootRequest: user/pass in URL auth ok.'); + req.session.userid = userid; + delete req.session.currentNode; + req.session.ip = req.clientIp; // Bind this session to the IP address of the request + setSessionRandom(req); + handleRootRequestEx(req, res, domain, direct); + } else { + // Login failed + handleRootRequestEx(req, res, domain, direct); + } }); } else if ((req.session != null) && (typeof req.session.loginToken == 'string')) { // Check if the loginToken is still valid @@ -7101,7 +7130,6 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF if ((req.query.user != null) && (req.query.pass != null)) { // A user/pass is provided in URL arguments obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) { - var user = obj.users[userid]; // Check if user as the "notools" site right. If so, deny this connection as tools are not allowed to connect.