Filter user loading at startup for "advanced" LDAP

Users are loaded at startup. Currently when using "advanced" LDAP
authentication this is true even if they no longer have a
valid entry in the LDAP server.

This commit uses the existing LDAP filter (specified in config.js's searchDN
used by the "advanced" LDAP mechanism) to weed out any users that no
longer have the relevant LDAP entry.

Local and "simple" LDAP auth mechanisms continue to use the existing
load all users approach. In the "simple" LDAP case this is because we
only have access to the hashed password, and so can't bind to LDAP.
This commit is contained in:
Jonathan Sambrook 2020-04-22 14:33:09 +01:00
parent a0d10989ad
commit 878ac0d192
3 changed files with 132 additions and 33 deletions

View File

@ -6,6 +6,7 @@ const colors = require("chalk");
const crypto = require("crypto"); const crypto = require("crypto");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const Auth = require("./plugins/auth");
const Client = require("./client"); const Client = require("./client");
const Helper = require("./helper"); const Helper = require("./helper");
const WebPush = require("./plugins/webpush"); const WebPush = require("./plugins/webpush");
@ -45,7 +46,15 @@ ClientManager.prototype.loadUsers = function () {
); );
} }
// 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)); users.forEach((name) => this.loadUser(name));
}
}; };
ClientManager.prototype.autoloadUsers = function () { ClientManager.prototype.autoloadUsers = function () {

View File

@ -21,6 +21,12 @@ module.exports = {
// Must override: implements authentication mechanism // Must override: implements authentication mechanism
auth: () => unimplemented("auth"), 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 // local auth should always be enabled, but check here to verify

View File

@ -3,6 +3,7 @@
const log = require("../../log"); const log = require("../../log");
const Helper = require("../../helper"); const Helper = require("../../helper");
const ldap = require("ldapjs"); const ldap = require("ldapjs");
const colors = require("chalk");
function ldapAuthCommon(user, bindDN, password, callback) { function ldapAuthCommon(user, bindDN, password, callback) {
const config = Helper.config; const config = Helper.config;
@ -77,41 +78,42 @@ function advancedLdapAuth(user, password, callback) {
log.error("Invalid LDAP root credentials"); log.error("Invalid LDAP root credentials");
ldapclient.unbind(); ldapclient.unbind();
callback(false); callback(false);
} else { return;
}
ldapclient.search(base, searchOptions, function (err2, res) { ldapclient.search(base, searchOptions, function (err2, res) {
if (err2) { if (err2) {
log.warn(`LDAP User not found: ${userDN}`); log.warn(`LDAP User not found: ${userDN}`);
ldapclient.unbind(); ldapclient.unbind();
callback(false); callback(false);
} else { return;
}
let found = false; let found = false;
res.on("searchEntry", function (entry) { res.on("searchEntry", function (entry) {
found = true; found = true;
const bindDN = entry.objectName; const bindDN = entry.objectName;
log.info( log.info(`Auth against LDAP ${config.ldap.url} with found bindDN ${bindDN}`);
`Auth against LDAP ${config.ldap.url} with found bindDN ${bindDN}`
);
ldapclient.unbind(); ldapclient.unbind();
ldapAuthCommon(user, bindDN, password, callback); ldapAuthCommon(user, bindDN, password, callback);
}); });
res.on("error", function (err3) { res.on("error", function (err3) {
log.error(`LDAP error: ${err3}`); log.error(`LDAP error: ${err3}`);
callback(false); callback(false);
}); });
res.on("end", function (result) { res.on("end", function (result) {
ldapclient.unbind(); ldapclient.unbind();
if (!found) { if (!found) {
log.warn( log.warn(`LDAP Search did not find anything for: ${userDN} (${result.status})`);
`LDAP Search did not find anything for: ${userDN} (${result.status})`
);
callback(false); callback(false);
} }
}); });
}
}); });
}
}); });
} }
@ -139,6 +141,87 @@ function ldapAuth(manager, client, user, password, callback) {
return auth(user, password, callbackWrapper); 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() { function isLdapEnabled() {
return !Helper.config.public && Helper.config.ldap.enable; return !Helper.config.public && Helper.config.ldap.enable;
} }
@ -147,4 +230,5 @@ module.exports = {
moduleName: "ldap", moduleName: "ldap",
auth: ldapAuth, auth: ldapAuth,
isEnabled: isLdapEnabled, isEnabled: isLdapEnabled,
loadUsers: ldapLoadUsers,
}; };