settings: move externaldap setting

This commit is contained in:
Girish Ramakrishnan
2023-08-03 02:06:07 +05:30
parent a19e502198
commit 4a34c390f8
9 changed files with 164 additions and 160 deletions

View File

@@ -1,13 +1,14 @@
'use strict';
exports = module.exports = {
getConfig,
setConfig,
verifyPassword,
maybeCreateUser,
testConfig,
startSyncer,
injectPrivateFields,
removePrivateFields,
sync
@@ -58,25 +59,49 @@ function validUserRequirements(user) {
}
}
async function getConfig() {
const value = await settings.get(settings.EXTERNAL_LDAP_KEY);
if (value === null) return { provider: 'noop', autoCreate: false };
const config = JSON.parse(value);
if (!config.autoCreate) config.autoCreate = false; // ensure new keys
return config;
}
async function setConfig(newConfig) {
assert.strictEqual(typeof newConfig, 'object');
if (settings.isDemo()) throw new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode');
const currentConfig = await getConfig();
injectPrivateFields(newConfig, currentConfig);
const error = await testConfig(newConfig);
if (error) throw error;
await settings.set(settings.EXTERNAL_LDAP_KEY, JSON.stringify(newConfig));
}
// performs service bind if required
async function getClient(externalLdapConfig, options) {
assert.strictEqual(typeof externalLdapConfig, 'object');
async function getClient(config, options) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof options, 'object');
// basic validation to not crash
try { ldap.parseDN(externalLdapConfig.baseDn); } catch (e) { throw new BoxError(BoxError.BAD_FIELD, 'invalid baseDn'); }
try { ldap.parseFilter(externalLdapConfig.filter); } catch (e) { throw new BoxError(BoxError.BAD_FIELD, 'invalid filter'); }
const config = {
url: externalLdapConfig.url,
tlsOptions: {
rejectUnauthorized: externalLdapConfig.acceptSelfSignedCerts ? false : true
}
};
try { ldap.parseDN(config.baseDn); } catch (e) { throw new BoxError(BoxError.BAD_FIELD, 'invalid baseDn'); }
try { ldap.parseFilter(config.filter); } catch (e) { throw new BoxError(BoxError.BAD_FIELD, 'invalid filter'); }
let client;
try {
client = ldap.createClient(config);
const ldapConfig = {
url: config.url,
tlsOptions: {
rejectUnauthorized: config.acceptSelfSignedCerts ? false : true
}
};
client = ldap.createClient(ldapConfig);
} catch (e) {
if (e instanceof ldap.ProtocolError) throw new BoxError(BoxError.BAD_FIELD, 'url protocol is invalid');
throw new BoxError(BoxError.INTERNAL_ERROR, e);
@@ -92,9 +117,9 @@ async function getClient(externalLdapConfig, options) {
});
// skip bind auth if none exist or if not wanted
if (!externalLdapConfig.bindDn || !options.bind) return resolve(client);
if (!config.bindDn || !options.bind) return resolve(client);
client.bind(externalLdapConfig.bindDn, externalLdapConfig.bindPassword, function (error) {
client.bind(config.bindDn, config.bindPassword, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return reject(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return reject(new BoxError(BoxError.EXTERNAL_ERROR, error));
@@ -130,8 +155,8 @@ async function clientSearch(client, dn, searchOptions) {
});
}
async function ldapGetByDN(externalLdapConfig, dn) {
assert.strictEqual(typeof externalLdapConfig, 'object');
async function ldapGetByDN(config, dn) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof dn, 'string');
const searchOptions = {
@@ -141,7 +166,7 @@ async function ldapGetByDN(externalLdapConfig, dn) {
debug(`ldapGetByDN: Get object at ${dn}`);
const client = await getClient(externalLdapConfig, { bind: true });
const client = await getClient(config, { bind: true });
const result = await clientSearch(client, dn, searchOptions);
client.unbind();
if (result.length === 0) throw new BoxError(BoxError.NOT_FOUND);
@@ -149,13 +174,13 @@ async function ldapGetByDN(externalLdapConfig, dn) {
}
// TODO support search by email
async function ldapUserSearch(externalLdapConfig, options) {
assert.strictEqual(typeof externalLdapConfig, 'object');
async function ldapUserSearch(config, options) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof options, 'object');
const searchOptions = {
paged: true,
filter: ldap.parseFilter(externalLdapConfig.filter),
filter: ldap.parseFilter(config.filter),
scope: 'sub' // We may have to make this configurable
};
@@ -164,14 +189,14 @@ async function ldapUserSearch(externalLdapConfig, options) {
searchOptions.filter = new ldap.AndFilter({ filters: [ extraFilter, searchOptions.filter ] });
}
const client = await getClient(externalLdapConfig, { bind: true });
const result = await clientSearch(client, externalLdapConfig.baseDn, searchOptions);
const client = await getClient(config, { bind: true });
const result = await clientSearch(client, config.baseDn, searchOptions);
client.unbind();
return result;
}
async function ldapGroupSearch(externalLdapConfig, options) {
assert.strictEqual(typeof externalLdapConfig, 'object');
async function ldapGroupSearch(config, options) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof options, 'object');
const searchOptions = {
@@ -179,15 +204,15 @@ async function ldapGroupSearch(externalLdapConfig, options) {
scope: 'sub' // We may have to make this configurable
};
if (externalLdapConfig.groupFilter) searchOptions.filter = ldap.parseFilter(externalLdapConfig.groupFilter);
if (config.groupFilter) searchOptions.filter = ldap.parseFilter(config.groupFilter);
if (options.filter) { // https://github.com/ldapjs/node-ldapjs/blob/master/docs/filters.md
const extraFilter = ldap.parseFilter(options.filter);
searchOptions.filter = new ldap.AndFilter({ filters: [ extraFilter, searchOptions.filter ] });
}
const client = await getClient(externalLdapConfig, { bind: true });
const result = await clientSearch(client, externalLdapConfig.groupBaseDn, searchOptions);
const client = await getClient(config, { bind: true });
const result = await clientSearch(client, config.groupBaseDn, searchOptions);
client.unbind();
return result;
}
@@ -239,15 +264,15 @@ async function testConfig(config) {
async function maybeCreateUser(identifier) {
assert.strictEqual(typeof identifier, 'string');
const externalLdapConfig = await settings.getExternalLdapConfig();
if (externalLdapConfig.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
if (!externalLdapConfig.autoCreate) throw new BoxError(BoxError.BAD_STATE, 'auto create not enabled');
const config = await getConfig();
if (config.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
if (!config.autoCreate) throw new BoxError(BoxError.BAD_STATE, 'auto create not enabled');
const ldapUsers = await ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` });
const ldapUsers = await ldapUserSearch(config, { filter: `${config.usernameField}=${identifier}` });
if (ldapUsers.length === 0) throw new BoxError(BoxError.NOT_FOUND);
if (ldapUsers.length > 1) throw new BoxError(BoxError.CONFLICT);
const user = translateUser(externalLdapConfig, ldapUsers[0]);
const user = translateUser(config, ldapUsers[0]);
if (!validUserRequirements(user)) throw new BoxError(BoxError.BAD_FIELD);
return await users.add(user.email, { username: user.username, password: null, displayName: user.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP_AUTO_CREATE);
@@ -258,14 +283,14 @@ async function verifyPassword(user, password, totpToken) {
assert.strictEqual(typeof password, 'string');
assert(totpToken === null || typeof totpToken === 'string');
const externalLdapConfig = await settings.getExternalLdapConfig();
if (externalLdapConfig.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
const config = await getConfig();
if (config.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
const ldapUsers = await ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` });
const ldapUsers = await ldapUserSearch(config, { filter: `${config.usernameField}=${user.username}` });
if (ldapUsers.length === 0) throw new BoxError(BoxError.NOT_FOUND);
if (ldapUsers.length > 1) throw new BoxError(BoxError.CONFLICT);
const client = await getClient(externalLdapConfig, { bind: false });
const client = await getClient(config, { bind: false });
let userAuthDn;
if (totpToken) {
@@ -281,12 +306,12 @@ async function verifyPassword(user, password, totpToken) {
if (error instanceof ldap.InvalidCredentialsError) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error);
return translateUser(externalLdapConfig, ldapUsers[0]);
return translateUser(config, ldapUsers[0]);
}
async function startSyncer() {
const externalLdapConfig = await settings.getExternalLdapConfig();
if (externalLdapConfig.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
const config = await getConfig();
if (config.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
const taskId = await tasks.add(tasks.TASK_SYNC_EXTERNAL_LDAP, []);
@@ -297,11 +322,11 @@ async function startSyncer() {
return taskId;
}
async function syncUsers(externalLdapConfig, progressCallback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
async function syncUsers(config, progressCallback) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const ldapUsers = await ldapUserSearch(externalLdapConfig, {});
const ldapUsers = await ldapUserSearch(config, {});
debug(`syncUsers: Found ${ldapUsers.length} users`);
@@ -310,7 +335,7 @@ async function syncUsers(externalLdapConfig, progressCallback) {
// we ignore all errors here and just log them for now
for (let i = 0; i < ldapUsers.length; i++) {
let ldapUser = translateUser(externalLdapConfig, ldapUsers[i]);
let ldapUser = translateUser(config, ldapUsers[i]);
if (!validUserRequirements(ldapUser)) continue;
percent += step;
@@ -342,17 +367,17 @@ async function syncUsers(externalLdapConfig, progressCallback) {
debug('syncUsers: done');
}
async function syncGroups(externalLdapConfig, progressCallback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
async function syncGroups(config, progressCallback) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof progressCallback, 'function');
if (!externalLdapConfig.syncGroups) {
if (!config.syncGroups) {
debug('syncGroups: Group sync is disabled');
progressCallback({ percent: 70, message: 'Skipping group sync...' });
return [];
}
const ldapGroups = await ldapGroupSearch(externalLdapConfig, {});
const ldapGroups = await ldapGroupSearch(config, {});
debug(`syncGroups: Found ${ldapGroups.length} groups`);
@@ -361,7 +386,7 @@ async function syncGroups(externalLdapConfig, progressCallback) {
// we ignore all non internal errors here and just log them for now
for (const ldapGroup of ldapGroups) {
let groupName = ldapGroup[externalLdapConfig.groupnameField];
let groupName = ldapGroup[config.groupnameField];
if (!groupName) return;
// some servers return empty array for unknown properties :-/
if (typeof groupName !== 'string') return;
@@ -387,11 +412,11 @@ async function syncGroups(externalLdapConfig, progressCallback) {
debug('syncGroups: sync done');
}
async function syncGroupUsers(externalLdapConfig, progressCallback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
async function syncGroupUsers(config, progressCallback) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof progressCallback, 'function');
if (!externalLdapConfig.syncGroups) {
if (!config.syncGroups) {
debug('syncGroupUsers: Group users sync is disabled');
progressCallback({ percent: 99, message: 'Skipping group users sync...' });
return [];
@@ -404,7 +429,7 @@ async function syncGroupUsers(externalLdapConfig, progressCallback) {
for (const group of ldapGroups) {
debug(`syncGroupUsers: Sync users for group ${group.name}`);
const result = await ldapGroupSearch(externalLdapConfig, {});
const result = await ldapGroupSearch(config, {});
if (!result || result.length === 0) {
debug(`syncGroupUsers: Unable to find group ${group.name} ignoring for now.`);
continue;
@@ -412,8 +437,8 @@ async function syncGroupUsers(externalLdapConfig, progressCallback) {
// since our group names are lowercase we cannot use potentially case matching ldap filters
let found = result.find(function (r) {
if (!r[externalLdapConfig.groupnameField]) return false;
return r[externalLdapConfig.groupnameField].toLowerCase() === group.name;
if (!r[config.groupnameField]) return false;
return r[config.groupnameField].toLowerCase() === group.name;
});
if (!found) {
@@ -429,7 +454,7 @@ async function syncGroupUsers(externalLdapConfig, progressCallback) {
debug(`syncGroupUsers: Group ${group.name} has ${ldapGroupMembers.length} members.`);
for (const memberDn of ldapGroupMembers) {
const [ldapError, result] = await safe(ldapGetByDN(externalLdapConfig, memberDn));
const [ldapError, result] = await safe(ldapGetByDN(config, memberDn));
if (ldapError) {
debug(`syncGroupUsers: Failed to get ${memberDn}: %o`, ldapError);
continue;
@@ -437,7 +462,7 @@ async function syncGroupUsers(externalLdapConfig, progressCallback) {
debug(`syncGroupUsers: Found member object at ${memberDn} adding to group ${group.name}`);
const username = result[externalLdapConfig.usernameField].toLowerCase();
const username = result[config.usernameField].toLowerCase();
if (!username) continue;
const [getError, userObject] = await safe(users.getByUsername(username));
@@ -459,12 +484,12 @@ async function sync(progressCallback) {
progressCallback({ percent: 10, message: 'Starting ldap user sync' });
const externalLdapConfig = await settings.getExternalLdapConfig();
if (externalLdapConfig.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
const config = await getConfig();
if (config.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
await syncUsers(externalLdapConfig, progressCallback);
await syncGroups(externalLdapConfig, progressCallback);
await syncGroupUsers(externalLdapConfig, progressCallback);
await syncUsers(config, progressCallback);
await syncGroups(config, progressCallback);
await syncGroupUsers(config, progressCallback);
progressCallback({ percent: 100, message: 'Done' });