You can now restrict what LDAP users can login based on LDAP membership groups (#4415)
Ylian Saint-Hilaire committed
Aug 21, 2022 at 14:05 UTC
daa4c60b771938db396475c02d08d0f20bb2a22d
4 files changed
+42
-9
meshcentral-config-schema.json
+2
@@ -549,6 +549,8 @@
549
"ldapUserPhoneNumber": { "type": "string", "default": "telephoneNumber", "description": "The LDAP value to use for the user's phone number." },
550
"ldapUserImage": { "type": "string", "default": "thumbnailPhoto", "description": "The LDAP value to use for the user's image." },
551
"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." },
552
+ "ldapUserGroups": { "type": "string", "default": "memberOf", "description": "The LDAP value to use for the user's group memberships." },
553
+ "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." },
554
"ldapOptions": { "type": "object", "description": "LDAP options passed to ldapauth-fork" },
555
"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." },
556
"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" },
sample-config-advanced.json
+2
@@ -533,6 +533,8 @@
533
"_LDAPUserName": "gecos",
534
"_LDAPUserKey": "uid",
535
"_LDAPUserEmail": "otherMail",
536
+ "_LDAPUserGroups": "memberOf",
537
+ "_LDAPUserRequiredGroupMembership": [ "CN=Domain Admins,CN=Users,DC=sample,DC=com" ],
538
"_LDAPPptions": {
539
"url": "test",
540
"anne": {
views/default.handlebars
+2
-1
@@ -14205,7 +14205,8 @@
14205
151: "Started Web-VNC session \"{0}\".", // Not in use yet
14206
152: "No longer a relay for \"{0}\".",
14207
153: "Is a relay for \"{0}\".",
14208
- 154: "Account changed to sync with LDAP data."
14208
+ 154: "Account changed to sync with LDAP data.",
14209
+ 155: "Denied user login from {0}, {1}, {2}"
14210
};
14211
14212
var eventsShortMessageId = {
webserver.js
+36
-8
@@ -472,6 +472,23 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
472
if (username == null) { username = shortname; }
473
var userid = 'user/' + domain.id + '/' + shortname;
474
475
+ // See if the user is required to be part of an LDAP user group in order to log into this server.
476
+ if (typeof domain.ldapuserrequiredgroupmembership == 'string') { domain.ldapuserrequiredgroupmembership = [domain.ldapuserrequiredgroupmembership]; }
477
+ if (Array.isArray(domain.ldapuserrequiredgroupmembership) && (domain.ldapuserrequiredgroupmembership.length > 0)) {
478
+ // We must be part of a LDAP user group, lets get the list of groups this user is a member of.
479
+ const memberOfKey = (typeof domain.ldapusergroups == 'string') ? domain.ldapusergroups : 'memberOf';
480
+ var userMemberships = xxuser[memberOfKey];
481
+ if (typeof userMemberships == 'string') { userMemberships = [userMemberships]; }
482
+ if (Array.isArray(userMemberships) == false) { userMemberships = []; }
483
+
484
+ // Look for a matching LDAP user group
485
+ var userMembershipMatch = false;
486
+ for (var i in domain.ldapuserrequiredgroupmembership) { if (userMemberships.indexOf(domain.ldapuserrequiredgroupmembership[i]) >= 0) { userMembershipMatch = true; } }
487
+
488
+ // If there is no match, deny the login
489
+ if (userMembershipMatch === false) { fn('denied'); return; }
490
+ }
491
+
492
// Get the email address for this LDAP user
493
var email = null;
494
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
1232
const ua = getUserAgentInfo(req);
1233
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] });
1234
obj.setbadLogin(req);
1235
+ } else if (err == 'denied') {
1236
+ parent.debug('web', 'handleLoginRequest: login failed, access denied');
1237
+ req.session.messageid = 111; // Access denied.
1238
+ const ua = getUserAgentInfo(req);
1239
+ 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] });
1240
+ obj.setbadLogin(req);
1241
} else {
1242
parent.debug('web', 'handleLoginRequest: login failed, bad username and password');
1243
req.session.messageid = 112; // Login failed, check username and password.
@@ -2595,13 +2618,19 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
2618
} else if (req.query.user && req.query.pass) {
2619
// User credentials are being passed in the URL. WARNING: Putting credentials in a URL is bad security... but people are requesting this option.
2620
obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) {
2598
- if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + userid + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
2599
- parent.debug('web', 'handleRootRequest: user/pass in URL auth ok.');
2600
- req.session.userid = userid;
2601
- delete req.session.currentNode;
2602
- req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2603
- setSessionRandom(req);
2604
- handleRootRequestEx(req, res, domain, direct);
2621
+ if ((userid != null) && (err == null)) {
2622
+ // Login success
2623
+ if (obj.parent.authlog) { obj.parent.authLog('https', 'Accepted password for ' + userid + ' from ' + req.clientIp + ' port ' + req.connection.remotePort); }
2624
+ parent.debug('web', 'handleRootRequest: user/pass in URL auth ok.');
2625
+ req.session.userid = userid;
2626
+ delete req.session.currentNode;
2627
+ req.session.ip = req.clientIp; // Bind this session to the IP address of the request
2628
+ setSessionRandom(req);
2629
+ handleRootRequestEx(req, res, domain, direct);
2630
+ } else {
2631
+ // Login failed
2632
+ handleRootRequestEx(req, res, domain, direct);
2633
+ }
2634
});
2635
} else if ((req.session != null) && (typeof req.session.loginToken == 'string')) {
2636
// Check if the loginToken is still valid
@@ -7101,7 +7130,6 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
7130
if ((req.query.user != null) && (req.query.pass != null)) {
7131
// A user/pass is provided in URL arguments
7132
obj.authenticate(req.query.user, req.query.pass, domain, function (err, userid, passhint, loginOptions) {
7104
-
7133
var user = obj.users[userid];
7134
7135
// Check if user as the "notools" site right. If so, deny this connection as tools are not allowed to connect.