thelounge/src/plugins/auth/ldap.js
Jonathan Sambrook 878ac0d192 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.
2020-04-23 15:54:09 +01:00

235 lines
5.4 KiB
JavaScript

"use strict";
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;
const ldapclient = ldap.createClient({
url: config.ldap.url,
tlsOptions: config.ldap.tlsOptions,
});
ldapclient.on("error", function (err) {
log.error(`Unable to connect to LDAP server: ${err}`);
callback(false);
});
ldapclient.bind(bindDN, password, function (err) {
ldapclient.unbind();
if (err) {
log.error(`LDAP bind failed: ${err}`);
callback(false);
} else {
callback(true);
}
});
}
function simpleLdapAuth(user, password, callback) {
if (!user || !password) {
return callback(false);
}
const config = Helper.config;
const userDN = user.replace(/([,\\/#+<>;"= ])/g, "\\$1");
const bindDN = `${config.ldap.primaryKey}=${userDN},${config.ldap.baseDN}`;
log.info(`Auth against LDAP ${config.ldap.url} with provided bindDN ${bindDN}`);
ldapAuthCommon(user, bindDN, password, callback);
}
/**
* LDAP auth using initial DN search (see config comment for ldap.searchDN)
*/
function advancedLdapAuth(user, password, callback) {
if (!user || !password) {
return callback(false);
}
const config = Helper.config;
const userDN = user.replace(/([,\\/#+<>;"= ])/g, "\\$1");
const ldapclient = ldap.createClient({
url: config.ldap.url,
tlsOptions: config.ldap.tlsOptions,
});
const base = config.ldap.searchDN.base;
const searchOptions = {
scope: config.ldap.searchDN.scope,
filter: `(&(${config.ldap.primaryKey}=${userDN})${config.ldap.searchDN.filter})`,
attributes: ["dn"],
};
ldapclient.on("error", function (err) {
log.error(`Unable to connect to LDAP server: ${err}`);
callback(false);
});
ldapclient.bind(config.ldap.searchDN.rootDN, config.ldap.searchDN.rootPassword, function (err) {
if (err) {
log.error("Invalid LDAP root credentials");
ldapclient.unbind();
callback(false);
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);
}
});
});
});
}
function ldapAuth(manager, client, user, password, callback) {
// TODO: Enable the use of starttls() as an alternative to ldaps
// TODO: move this out of here and get rid of `manager` and `client` in
// auth plugin API
function callbackWrapper(valid) {
if (valid && !client) {
manager.addUser(user, null, true);
}
callback(valid);
}
let auth;
if ("baseDN" in Helper.config.ldap) {
auth = simpleLdapAuth;
} else {
auth = advancedLdapAuth;
}
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;
}
module.exports = {
moduleName: "ldap",
auth: ldapAuth,
isEnabled: isLdapEnabled,
loadUsers: ldapLoadUsers,
};