Merge pull request #3871 from ebardie/ebardie/dont_load_extinct_users
Filter user loading at startup for "advanced" LDAP
This commit is contained in:
commit
4ac25d4bc5
@ -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 () {
|
||||||
|
50
src/plugins/auth.js
Normal file
50
src/plugins/auth.js
Normal 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");
|
||||||
|
}
|
@ -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,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,
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user