diff --git a/src/clientManager.js b/src/clientManager.js index 7aa8315d..ca1f55b6 100644 --- a/src/clientManager.js +++ b/src/clientManager.js @@ -6,6 +6,7 @@ const colors = require("chalk"); const crypto = require("crypto"); const fs = require("fs"); const path = require("path"); +const Auth = require("./plugins/auth"); const Client = require("./client"); const Helper = require("./helper"); const WebPush = require("./plugins/webpush"); @@ -45,7 +46,15 @@ ClientManager.prototype.loadUsers = function () { ); } - users.forEach((name) => this.loadUser(name)); + // This callback is used by Auth plugins to load users they deem acceptable + const callbackLoadUser = (user) => { + this.loadUser(user); + }; + + if (!Auth.loadUsers(users, callbackLoadUser)) { + // Fallback to loading all users + users.forEach((name) => this.loadUser(name)); + } }; ClientManager.prototype.autoloadUsers = function () { diff --git a/src/plugins/auth.js b/src/plugins/auth.js index e53fb3ee..6e951010 100644 --- a/src/plugins/auth.js +++ b/src/plugins/auth.js @@ -21,6 +21,12 @@ module.exports = { // Must override: implements authentication mechanism auth: () => unimplemented("auth"), + + // Optional to override: implements filter for loading users at start up + // This allows an auth plugin to check if a user is still acceptable, if the plugin + // can do so without access to the user's unhashed password. + // Returning 'false' triggers fallback to default behaviour of loading all users + loadUsers: () => false, }; // local auth should always be enabled, but check here to verify diff --git a/src/plugins/auth/ldap.js b/src/plugins/auth/ldap.js index 091c892e..1c93419f 100644 --- a/src/plugins/auth/ldap.js +++ b/src/plugins/auth/ldap.js @@ -3,6 +3,7 @@ const log = require("../../log"); const Helper = require("../../helper"); const ldap = require("ldapjs"); +const colors = require("chalk"); function ldapAuthCommon(user, bindDN, password, callback) { const config = Helper.config; @@ -77,41 +78,42 @@ function advancedLdapAuth(user, password, callback) { log.error("Invalid LDAP root credentials"); ldapclient.unbind(); callback(false); - } else { - ldapclient.search(base, searchOptions, function (err2, res) { - if (err2) { - log.warn(`LDAP User not found: ${userDN}`); - ldapclient.unbind(); + return; + } + + ldapclient.search(base, searchOptions, function (err2, res) { + if (err2) { + log.warn(`LDAP User not found: ${userDN}`); + ldapclient.unbind(); + callback(false); + return; + } + + let found = false; + + res.on("searchEntry", function (entry) { + found = true; + const bindDN = entry.objectName; + log.info(`Auth against LDAP ${config.ldap.url} with found bindDN ${bindDN}`); + ldapclient.unbind(); + + ldapAuthCommon(user, bindDN, password, callback); + }); + + res.on("error", function (err3) { + log.error(`LDAP error: ${err3}`); + callback(false); + }); + + res.on("end", function (result) { + ldapclient.unbind(); + + if (!found) { + log.warn(`LDAP Search did not find anything for: ${userDN} (${result.status})`); callback(false); - } else { - let found = false; - res.on("searchEntry", function (entry) { - found = true; - const bindDN = entry.objectName; - log.info( - `Auth against LDAP ${config.ldap.url} with found bindDN ${bindDN}` - ); - ldapclient.unbind(); - - ldapAuthCommon(user, bindDN, password, callback); - }); - res.on("error", function (err3) { - log.error(`LDAP error: ${err3}`); - callback(false); - }); - res.on("end", function (result) { - ldapclient.unbind(); - - if (!found) { - log.warn( - `LDAP Search did not find anything for: ${userDN} (${result.status})` - ); - callback(false); - } - }); } }); - } + }); }); } @@ -139,6 +141,87 @@ function ldapAuth(manager, client, user, password, callback) { return auth(user, password, callbackWrapper); } +/** + * Use the LDAP filter from config to check that users still exist before loading them + * via the supplied callback function. + */ + +function advancedLdapLoadUsers(users, callbackLoadUser) { + const config = Helper.config; + + const ldapclient = ldap.createClient({ + url: config.ldap.url, + tlsOptions: config.ldap.tlsOptions, + }); + + const base = config.ldap.searchDN.base; + + ldapclient.on("error", function (err) { + log.error(`Unable to connect to LDAP server: ${err}`); + }); + + ldapclient.bind(config.ldap.searchDN.rootDN, config.ldap.searchDN.rootPassword, function (err) { + if (err) { + log.error("Invalid LDAP root credentials"); + return true; + } + + const remainingUsers = new Set(users); + + const searchOptions = { + scope: config.ldap.searchDN.scope, + filter: `${config.ldap.searchDN.filter}`, + attributes: [config.ldap.primaryKey], + paged: true, + }; + + ldapclient.search(base, searchOptions, function (err2, res) { + if (err2) { + log.error(`LDAP search error: ${err2}`); + return true; + } + + res.on("searchEntry", function (entry) { + const user = entry.attributes[0]._vals[0].toString(); + + if (remainingUsers.has(user)) { + remainingUsers.delete(user); + callbackLoadUser(user); + } + }); + + res.on("error", function (err3) { + log.error(`LDAP error: ${err3}`); + }); + + res.on("end", function () { + remainingUsers.forEach((user) => { + log.warn( + `No account info in LDAP for ${colors.bold( + user + )} but user config file exists` + ); + }); + }); + }); + + ldapclient.unbind(); + }); + + return true; +} + +function ldapLoadUsers(users, callbackLoadUser) { + if ("baseDN" in Helper.config.ldap) { + // simple LDAP case can't test for user existence without access to the + // user's unhashed password, so indicate need to fallback to default + // loadUser behaviour by returning false + return false; + } + + return advancedLdapLoadUsers(users, callbackLoadUser); +} + function isLdapEnabled() { return !Helper.config.public && Helper.config.ldap.enable; } @@ -147,4 +230,5 @@ module.exports = { moduleName: "ldap", auth: ldapAuth, isEnabled: isLdapEnabled, + loadUsers: ldapLoadUsers, };