Merge pull request #3871 from ebardie/ebardie/dont_load_extinct_users

Filter user loading at startup for "advanced" LDAP
This commit is contained in:
Pavel Djundik 2020-04-24 10:21:42 +03:00 committed by GitHub
commit 4ac25d4bc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 180 additions and 49 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 () {
); );
} }
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 () { ClientManager.prototype.autoloadUsers = function () {

50
src/plugins/auth.js Normal file
View File

@ -0,0 +1,50 @@
"use strict";
const log = require("../log");
const colors = require("chalk");
// The order defines priority: the first available plugin is used.
// Always keep 'local' auth plugin at the end of the list; it should always be enabled.
const plugins = [require("./auth/ldap"), require("./auth/local")];
function unimplemented(funcName) {
log.debug(
`Auth module ${colors.bold(
module.exports.moduleName
)} doesn't implement function ${colors.bold(funcName)}`
);
}
// Default API implementations
module.exports = {
moduleName: "<module with no name>",
// 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
let somethingEnabled = false;
// Override default API stubs with exports from first enabled plugin found
for (const plugin of plugins) {
if (plugin.isEnabled()) {
somethingEnabled = true;
for (const name in plugin) {
module.exports[name] = plugin[name];
}
break;
}
}
if (!somethingEnabled) {
log.error("None of the auth plugins is enabled");
}

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) { }
if (err2) {
log.warn(`LDAP User not found: ${userDN}`); ldapclient.search(base, searchOptions, function (err2, res) {
ldapclient.unbind(); 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); 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,11 +141,94 @@ 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;
} }
module.exports = { module.exports = {
moduleName: "ldap",
auth: ldapAuth, auth: ldapAuth,
isEnabled: isLdapEnabled, isEnabled: isLdapEnabled,
loadUsers: ldapLoadUsers,
}; };

View File

@ -46,6 +46,7 @@ function localAuth(manager, client, user, password, callback) {
} }
module.exports = { module.exports = {
moduleName: "local",
auth: localAuth, auth: localAuth,
isEnabled: () => true, isEnabled: () => true,
}; };

View File

@ -17,16 +17,13 @@ const net = require("net");
const Identification = require("./identification"); const Identification = require("./identification");
const changelog = require("./plugins/changelog"); const changelog = require("./plugins/changelog");
const inputs = require("./plugins/inputs"); const inputs = require("./plugins/inputs");
const Auth = require("./plugins/auth");
const themes = require("./plugins/packages/themes"); const themes = require("./plugins/packages/themes");
themes.loadLocalThemes(); themes.loadLocalThemes();
const packages = require("./plugins/packages/index"); const packages = require("./plugins/packages/index");
// The order defined the priority: the first available plugin is used
// ALways keep local auth in the end, which should always be enabled.
const authPlugins = [require("./plugins/auth/ldap"), require("./plugins/auth/local")];
// A random number that will force clients to reload the page if it differs // A random number that will force clients to reload the page if it differs
const serverHash = Math.floor(Date.now() * Math.random()); const serverHash = Math.floor(Date.now() * Math.random());
@ -851,18 +848,7 @@ function performAuthentication(data) {
} }
// Perform password checking // Perform password checking
let auth = () => { Auth.auth(manager, client, data.user, data.password, authCallback);
log.error("None of the auth plugins is enabled");
};
for (let i = 0; i < authPlugins.length; ++i) {
if (authPlugins[i].isEnabled()) {
auth = authPlugins[i].auth;
break;
}
}
auth(manager, client, data.user, data.password, authCallback);
} }
function reverseDnsLookup(ip, callback) { function reverseDnsLookup(ip, callback) {