externalldap: async'ify
and make the tests work again
This commit is contained in:
+290
-385
@@ -1,7 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
search,
|
||||
verifyPassword,
|
||||
maybeCreateUser,
|
||||
|
||||
@@ -15,7 +14,6 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
const assert = require('assert'),
|
||||
async = require('async'),
|
||||
auditSource = require('./auditsource.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
constants = require('./constants.js'),
|
||||
@@ -29,8 +27,6 @@ const assert = require('assert'),
|
||||
users = require('./users.js'),
|
||||
util = require('util');
|
||||
|
||||
const settingsGetExternalLdapConfig = util.callbackify(settings.getExternalLdapConfig);
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.bindPassword === constants.SECRET_PLACEHOLDER) newConfig.bindPassword = currentConfig.bindPassword;
|
||||
}
|
||||
@@ -61,519 +57,428 @@ function validUserRequirements(user) {
|
||||
}
|
||||
|
||||
// performs service bind if required
|
||||
function getClient(externalLdapConfig, doBindAuth, callback) {
|
||||
async function getClient(externalLdapConfig, options) {
|
||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||
assert.strictEqual(typeof doBindAuth, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// ensure we only callback once since we also have to listen to client.error events
|
||||
callback = once(callback);
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
// basic validation to not crash
|
||||
try { ldap.parseDN(externalLdapConfig.baseDn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid baseDn')); }
|
||||
try { ldap.parseFilter(externalLdapConfig.filter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid filter')); }
|
||||
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'); }
|
||||
|
||||
var config = {
|
||||
const config = {
|
||||
url: externalLdapConfig.url,
|
||||
tlsOptions: {
|
||||
rejectUnauthorized: externalLdapConfig.acceptSelfSignedCerts ? false : true
|
||||
}
|
||||
};
|
||||
|
||||
var client;
|
||||
let client;
|
||||
try {
|
||||
client = ldap.createClient(config);
|
||||
} catch (e) {
|
||||
if (e instanceof ldap.ProtocolError) return callback(new BoxError(BoxError.BAD_FIELD, 'url protocol is invalid'));
|
||||
return callback(new BoxError(BoxError.INTERNAL_ERROR, e));
|
||||
if (e instanceof ldap.ProtocolError) throw new BoxError(BoxError.BAD_FIELD, 'url protocol is invalid');
|
||||
throw new BoxError(BoxError.INTERNAL_ERROR, e);
|
||||
}
|
||||
|
||||
// ensure we don't just crash
|
||||
client.on('error', function (error) {
|
||||
callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||
});
|
||||
|
||||
// skip bind auth if none exist or if not wanted
|
||||
if (!externalLdapConfig.bindDn || !doBindAuth) return callback(null, client);
|
||||
if (!externalLdapConfig.bindDn || !options.bind) return client;
|
||||
|
||||
client.bind(externalLdapConfig.bindDn, externalLdapConfig.bindPassword, function (error) {
|
||||
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||
return await new Promise((resolve, reject) => {
|
||||
reject = once(reject);
|
||||
|
||||
callback(null, client);
|
||||
// ensure we don't just crash
|
||||
client.on('error', function (error) {
|
||||
reject(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||
});
|
||||
|
||||
client.bind(externalLdapConfig.bindDn, externalLdapConfig.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));
|
||||
|
||||
resolve(client);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function ldapGetByDN(externalLdapConfig, dn, callback) {
|
||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||
async function clientSearch(client, dn, searchOptions) {
|
||||
assert.strictEqual(typeof client, 'object');
|
||||
assert.strictEqual(typeof dn, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
assert.strictEqual(typeof searchOptions, 'object');
|
||||
|
||||
getClient(externalLdapConfig, true, function (error, client) {
|
||||
if (error) return callback(error);
|
||||
debug(`clientSearch: Get objects at ${dn} with options ${JSON.stringify(searchOptions)}`);
|
||||
|
||||
let searchOptions = {
|
||||
paged: true,
|
||||
scope: 'sub' // We may have to make this configurable
|
||||
};
|
||||
|
||||
debug(`Get object at ${dn}`);
|
||||
|
||||
// basic validation to not crash
|
||||
try { ldap.parseDN(dn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid DN')); }
|
||||
// basic validation to not crash
|
||||
try { ldap.parseDN(dn); } catch (e) { throw new BoxError(BoxError.BAD_FIELD, 'invalid DN'); }
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
client.search(dn, searchOptions, function (error, result) {
|
||||
if (error instanceof ldap.NoSuchObjectError) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||
if (error instanceof ldap.NoSuchObjectError) return reject(new BoxError(BoxError.NOT_FOUND));
|
||||
if (error) return reject(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||
|
||||
let ldapObjects = [];
|
||||
|
||||
result.on('searchEntry', entry => ldapObjects.push(entry.object));
|
||||
result.on('error', error => callback(new BoxError(BoxError.EXTERNAL_ERROR, error)));
|
||||
result.on('error', error => reject(new BoxError(BoxError.EXTERNAL_ERROR, error)));
|
||||
|
||||
result.on('end', function (result) {
|
||||
client.unbind();
|
||||
if (result.status !== 0) return reject(new BoxError(BoxError.EXTERNAL_ERROR, 'Server returned status ' + result.status));
|
||||
|
||||
if (result.status !== 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Server returned status ' + result.status));
|
||||
if (ldapObjects.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
|
||||
callback(null, ldapObjects[0]);
|
||||
resolve(ldapObjects);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function ldapGetByDN(externalLdapConfig, dn) {
|
||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||
assert.strictEqual(typeof dn, 'string');
|
||||
|
||||
const searchOptions = {
|
||||
paged: true,
|
||||
scope: 'sub' // We may have to make this configurable
|
||||
};
|
||||
|
||||
debug(`ldapGetByDN: Get object at ${dn}`);
|
||||
|
||||
const client = await getClient(externalLdapConfig, { bind: true });
|
||||
const result = await clientSearch(client, dn, searchOptions);
|
||||
client.unbind();
|
||||
if (result.length === 0) throw new BoxError(BoxError.NOT_FOUND);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
// TODO support search by email
|
||||
function ldapUserSearch(externalLdapConfig, options, callback) {
|
||||
async function ldapUserSearch(externalLdapConfig, options) {
|
||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getClient(externalLdapConfig, true, function (error, client) {
|
||||
if (error) return callback(error);
|
||||
const searchOptions = {
|
||||
paged: true,
|
||||
filter: ldap.parseFilter(externalLdapConfig.filter),
|
||||
scope: 'sub' // We may have to make this configurable
|
||||
};
|
||||
|
||||
let searchOptions = {
|
||||
paged: true,
|
||||
filter: ldap.parseFilter(externalLdapConfig.filter),
|
||||
scope: 'sub' // We may have to make this configurable
|
||||
};
|
||||
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 ] });
|
||||
}
|
||||
|
||||
if (options.filter) { // https://github.com/ldapjs/node-ldapjs/blob/master/docs/filters.md
|
||||
let extraFilter = ldap.parseFilter(options.filter);
|
||||
searchOptions.filter = new ldap.AndFilter({ filters: [ extraFilter, searchOptions.filter ] });
|
||||
}
|
||||
debug(`Listing users at ${externalLdapConfig.baseDn} with filter ${searchOptions.filter.toString()}`);
|
||||
|
||||
debug(`Listing users at ${externalLdapConfig.baseDn} with filter ${searchOptions.filter.toString()}`);
|
||||
|
||||
client.search(externalLdapConfig.baseDn, searchOptions, function (error, result) {
|
||||
if (error instanceof ldap.NoSuchObjectError) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||
|
||||
let ldapUsers = [];
|
||||
|
||||
result.on('searchEntry', entry => ldapUsers.push(entry.object));
|
||||
result.on('error', error => callback(new BoxError(BoxError.EXTERNAL_ERROR, error)));
|
||||
|
||||
result.on('end', function (result) {
|
||||
client.unbind();
|
||||
|
||||
if (result.status !== 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Server returned status ' + result.status));
|
||||
|
||||
callback(null, ldapUsers);
|
||||
});
|
||||
});
|
||||
});
|
||||
const client = await getClient(externalLdapConfig, { bind: true });
|
||||
const result = await clientSearch(client, externalLdapConfig.baseDn, searchOptions);
|
||||
client.unbind();
|
||||
return result;
|
||||
}
|
||||
|
||||
function ldapGroupSearch(externalLdapConfig, options, callback) {
|
||||
async function ldapGroupSearch(externalLdapConfig, options) {
|
||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getClient(externalLdapConfig, true, function (error, client) {
|
||||
if (error) return callback(error);
|
||||
const searchOptions = {
|
||||
paged: true,
|
||||
scope: 'sub' // We may have to make this configurable
|
||||
};
|
||||
|
||||
let searchOptions = {
|
||||
paged: true,
|
||||
scope: 'sub' // We may have to make this configurable
|
||||
};
|
||||
if (externalLdapConfig.groupFilter) searchOptions.filter = ldap.parseFilter(externalLdapConfig.groupFilter);
|
||||
|
||||
if (externalLdapConfig.groupFilter) searchOptions.filter = ldap.parseFilter(externalLdapConfig.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 ] });
|
||||
}
|
||||
|
||||
if (options.filter) { // https://github.com/ldapjs/node-ldapjs/blob/master/docs/filters.md
|
||||
let extraFilter = ldap.parseFilter(options.filter);
|
||||
searchOptions.filter = new ldap.AndFilter({ filters: [ extraFilter, searchOptions.filter ] });
|
||||
}
|
||||
debug(`Listing groups at ${externalLdapConfig.groupBaseDn} with filter ${searchOptions.filter.toString()}`);
|
||||
|
||||
debug(`Listing groups at ${externalLdapConfig.groupBaseDn} with filter ${searchOptions.filter.toString()}`);
|
||||
|
||||
client.search(externalLdapConfig.groupBaseDn, searchOptions, function (error, result) {
|
||||
if (error instanceof ldap.NoSuchObjectError) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||
|
||||
let ldapGroups = [];
|
||||
|
||||
result.on('searchEntry', entry => ldapGroups.push(entry.object));
|
||||
result.on('error', error => callback(new BoxError(BoxError.EXTERNAL_ERROR, error)));
|
||||
|
||||
result.on('end', function (result) {
|
||||
client.unbind();
|
||||
|
||||
if (result.status !== 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Server returned status ' + result.status));
|
||||
|
||||
callback(null, ldapGroups);
|
||||
});
|
||||
});
|
||||
});
|
||||
const client = await getClient(externalLdapConfig, { bind: true });
|
||||
const result = await clientSearch(client, externalLdapConfig.groupBaseDn, searchOptions);
|
||||
client.unbind();
|
||||
return result;
|
||||
}
|
||||
|
||||
function testConfig(config, callback) {
|
||||
async function testConfig(config) {
|
||||
assert.strictEqual(typeof config, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (config.provider === 'noop') return callback();
|
||||
if (config.provider === 'noop') return null;
|
||||
|
||||
if (!config.url) return callback(new BoxError(BoxError.BAD_FIELD, 'url must not be empty'));
|
||||
if (!config.url.startsWith('ldap://') && !config.url.startsWith('ldaps://')) return callback(new BoxError(BoxError.BAD_FIELD, 'url is missing ldap:// or ldaps:// prefix'));
|
||||
if (!config.url) return new BoxError(BoxError.BAD_FIELD, 'url must not be empty');
|
||||
if (!config.url.startsWith('ldap://') && !config.url.startsWith('ldaps://')) return new BoxError(BoxError.BAD_FIELD, 'url is missing ldap:// or ldaps:// prefix');
|
||||
if (!config.usernameField) config.usernameField = 'uid';
|
||||
|
||||
// bindDn may not be a dn!
|
||||
if (!config.baseDn) return callback(new BoxError(BoxError.BAD_FIELD, 'basedn must not be empty'));
|
||||
try { ldap.parseDN(config.baseDn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid baseDn')); }
|
||||
if (!config.baseDn) return new BoxError(BoxError.BAD_FIELD, 'basedn must not be empty');
|
||||
try { ldap.parseDN(config.baseDn); } catch (e) { return new BoxError(BoxError.BAD_FIELD, 'invalid baseDn'); }
|
||||
|
||||
if (!config.filter) return callback(new BoxError(BoxError.BAD_FIELD, 'filter must not be empty'));
|
||||
try { ldap.parseFilter(config.filter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid filter')); }
|
||||
if (!config.filter) return new BoxError(BoxError.BAD_FIELD, 'filter must not be empty');
|
||||
try { ldap.parseFilter(config.filter); } catch (e) { return new BoxError(BoxError.BAD_FIELD, 'invalid filter'); }
|
||||
|
||||
if ('syncGroups' in config && typeof config.syncGroups !== 'boolean') return callback(new BoxError(BoxError.BAD_FIELD, 'syncGroups must be a boolean'));
|
||||
if ('acceptSelfSignedCerts' in config && typeof config.acceptSelfSignedCerts !== 'boolean') return callback(new BoxError(BoxError.BAD_FIELD, 'acceptSelfSignedCerts must be a boolean'));
|
||||
if ('syncGroups' in config && typeof config.syncGroups !== 'boolean') return new BoxError(BoxError.BAD_FIELD, 'syncGroups must be a boolean');
|
||||
if ('acceptSelfSignedCerts' in config && typeof config.acceptSelfSignedCerts !== 'boolean') return new BoxError(BoxError.BAD_FIELD, 'acceptSelfSignedCerts must be a boolean');
|
||||
|
||||
if (config.syncGroups) {
|
||||
if (!config.groupBaseDn) return callback(new BoxError(BoxError.BAD_FIELD, 'groupBaseDn must not be empty'));
|
||||
try { ldap.parseDN(config.groupBaseDn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid groupBaseDn')); }
|
||||
if (!config.groupBaseDn) return new BoxError(BoxError.BAD_FIELD, 'groupBaseDn must not be empty');
|
||||
try { ldap.parseDN(config.groupBaseDn); } catch (e) { return new BoxError(BoxError.BAD_FIELD, 'invalid groupBaseDn'); }
|
||||
|
||||
if (!config.groupFilter) return callback(new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty'));
|
||||
try { ldap.parseFilter(config.groupFilter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid groupFilter')); }
|
||||
if (!config.groupFilter) return new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty');
|
||||
try { ldap.parseFilter(config.groupFilter); } catch (e) { return new BoxError(BoxError.BAD_FIELD, 'invalid groupFilter'); }
|
||||
|
||||
if (!config.groupnameField || typeof config.groupnameField !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty'));
|
||||
if (!config.groupnameField || typeof config.groupnameField !== 'string') return new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty');
|
||||
}
|
||||
|
||||
getClient(config, true, function (error, client) {
|
||||
if (error) return callback(error);
|
||||
const [error, client] = await safe(getClient(config, { bind: true }));
|
||||
if (error) return error;
|
||||
|
||||
var opts = {
|
||||
filter: config.filter,
|
||||
scope: 'sub'
|
||||
};
|
||||
const opts = {
|
||||
filter: config.filter,
|
||||
scope: 'sub'
|
||||
};
|
||||
|
||||
client.search(config.baseDn, opts, function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||
const [searchError, ] = await safe(clientSearch(client, config.baseDn, opts));
|
||||
client.unbind();
|
||||
if (searchError) return searchError;
|
||||
|
||||
result.on('searchEntry', function (/* entry */) {});
|
||||
result.on('error', function (error) { client.unbind(); callback(new BoxError(BoxError.BAD_FIELD, `Unable to search directory: ${error.message}`)); });
|
||||
result.on('end', function (/* result */) { client.unbind(); callback(); });
|
||||
});
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
function search(identifier, callback) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
async function search(identifier) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsGetExternalLdapConfig(function (error, externalLdapConfig) {
|
||||
if (error) return callback(error);
|
||||
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
||||
const externalLdapConfig = await settings.getExternalLdapConfig();
|
||||
if (externalLdapConfig.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
|
||||
|
||||
ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) {
|
||||
if (error) return callback(error);
|
||||
const ldapUsers = await ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` });
|
||||
|
||||
// translate ldap properties to ours
|
||||
let users = ldapUsers.map(function (u) { return translateUser(externalLdapConfig, u); });
|
||||
// translate ldap properties to ours
|
||||
let users = ldapUsers.map(function (u) { return translateUser(externalLdapConfig, u); });
|
||||
|
||||
callback(null, users);
|
||||
});
|
||||
});
|
||||
return users;
|
||||
}
|
||||
|
||||
async function maybeCreateUser(identifier, password) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
settingsGetExternalLdapConfig(function (error, externalLdapConfig) {
|
||||
if (error) return reject(error);
|
||||
if (externalLdapConfig.provider === 'noop') return reject(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
||||
if (!externalLdapConfig.autoCreate) return reject(new BoxError(BoxError.BAD_STATE, 'auto create not enabled'));
|
||||
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');
|
||||
|
||||
ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, async function (error, ldapUsers) {
|
||||
if (error) return reject(error);
|
||||
if (ldapUsers.length === 0) return reject(new BoxError(BoxError.NOT_FOUND));
|
||||
if (ldapUsers.length > 1) return reject(new BoxError(BoxError.CONFLICT));
|
||||
const ldapUsers = await ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.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]);
|
||||
if (!validUserRequirements(user)) return reject(new BoxError(BoxError.BAD_FIELD));
|
||||
const user = translateUser(externalLdapConfig, ldapUsers[0]);
|
||||
if (!validUserRequirements(user)) throw new BoxError(BoxError.BAD_FIELD);
|
||||
|
||||
[error] = await safe(users.add(user.email, { username: user.username, password: null, displayName: user.displayName, source: 'ldap' }, auditSource.EXTERNAL_LDAP_AUTO_CREATE));
|
||||
if (error) {
|
||||
debug(`maybeCreateUser: failed to auto create user ${user.username}`, error);
|
||||
return reject(new BoxError(BoxError.INTERNAL_ERROR, error));
|
||||
}
|
||||
const [error] = await safe(users.add(user.email, { username: user.username, password: null, displayName: user.displayName, source: 'ldap' }, auditSource.EXTERNAL_LDAP_AUTO_CREATE));
|
||||
if (error) {
|
||||
debug(`maybeCreateUser: failed to auto create user ${user.username}`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
resolve(user);
|
||||
});
|
||||
});
|
||||
});
|
||||
return user;
|
||||
}
|
||||
|
||||
async function verifyPassword(user, password) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
settingsGetExternalLdapConfig(function (error, externalLdapConfig) {
|
||||
if (error) return reject(error);
|
||||
if (externalLdapConfig.provider === 'noop') return reject(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
||||
const externalLdapConfig = await settings.getExternalLdapConfig();
|
||||
if (externalLdapConfig.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
|
||||
|
||||
ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` }, function (error, ldapUsers) {
|
||||
if (error) return reject(error);
|
||||
if (ldapUsers.length === 0) return reject(new BoxError(BoxError.NOT_FOUND));
|
||||
if (ldapUsers.length > 1) return reject(new BoxError(BoxError.CONFLICT));
|
||||
const ldapUsers = await ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` });
|
||||
if (ldapUsers.length === 0) throw new BoxError(BoxError.NOT_FOUND);
|
||||
if (ldapUsers.length > 1) throw new BoxError(BoxError.CONFLICT);
|
||||
|
||||
getClient(externalLdapConfig, false, function (error, client) {
|
||||
if (error) return reject(error);
|
||||
const client = await getClient(externalLdapConfig, { bind: false });
|
||||
|
||||
client.bind(ldapUsers[0].dn, password, function (error) {
|
||||
if (error instanceof ldap.InvalidCredentialsError) return reject(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (error) return reject(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||
const [error] = await safe(util.promisify(client.bind)(ldapUsers[0].dn, password));
|
||||
client.unbind();
|
||||
if (error instanceof ldap.InvalidCredentialsError) throw new BoxError(BoxError.INVALID_CREDENTIALS);
|
||||
if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error);
|
||||
|
||||
resolve(translateUser(externalLdapConfig, ldapUsers[0]));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
return translateUser(externalLdapConfig, ldapUsers[0]);
|
||||
}
|
||||
|
||||
function startSyncer(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
async function startSyncer() {
|
||||
const externalLdapConfig = await settings.getExternalLdapConfig();
|
||||
if (externalLdapConfig.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
|
||||
|
||||
settingsGetExternalLdapConfig(async function (error, externalLdapConfig) {
|
||||
if (error) return callback(error);
|
||||
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
||||
const taskId = await tasks.add(tasks.TASK_SYNC_EXTERNAL_LDAP, []);
|
||||
|
||||
const [taskError, taskId] = await safe(tasks.add(tasks.TASK_SYNC_EXTERNAL_LDAP, []));
|
||||
if (taskError) return callback(taskError);
|
||||
|
||||
tasks.startTask(taskId, {}, function (error, result) {
|
||||
debug('sync: done', error, result);
|
||||
});
|
||||
|
||||
callback(null, taskId);
|
||||
tasks.startTask(taskId, {}, function (error, result) {
|
||||
debug('sync: done', error, result);
|
||||
});
|
||||
|
||||
return taskId;
|
||||
}
|
||||
|
||||
function syncUsers(externalLdapConfig, progressCallback, callback) {
|
||||
async function syncUsers(externalLdapConfig, progressCallback) {
|
||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
ldapUserSearch(externalLdapConfig, {}, async function (error, ldapUsers) {
|
||||
if (error) return callback(error);
|
||||
const ldapUsers = await ldapUserSearch(externalLdapConfig, {});
|
||||
|
||||
debug(`Found ${ldapUsers.length} users`);
|
||||
debug(`syncUsers: Found ${ldapUsers.length} users`);
|
||||
|
||||
let percent = 10;
|
||||
let step = 30/(ldapUsers.length+1); // ensure no divide by 0
|
||||
let percent = 10;
|
||||
let step = 30/(ldapUsers.length+1); // ensure no divide by 0
|
||||
|
||||
// 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]);
|
||||
if (!validUserRequirements(ldapUser)) continue;
|
||||
// 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]);
|
||||
if (!validUserRequirements(ldapUser)) continue;
|
||||
|
||||
percent += step;
|
||||
progressCallback({ percent, message: `Syncing... ${ldapUser.username}` });
|
||||
percent += step;
|
||||
progressCallback({ percent, message: `Syncing... ${ldapUser.username}` });
|
||||
|
||||
const [userGetError, user] = await safe(users.getByUsername(ldapUser.username));
|
||||
if (userGetError) {
|
||||
debug('syncUsers: Failed to get user by username', ldapUser, userGetError);
|
||||
break;
|
||||
}
|
||||
const user = await users.getByUsername(ldapUser.username);
|
||||
|
||||
if (!user) {
|
||||
debug(`[adding user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`);
|
||||
if (!user) {
|
||||
debug(`syncUsers: [adding user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`);
|
||||
|
||||
const [userAddError] = await safe(users.add(ldapUser.email, { username: ldapUser.username, password: null, displayName: ldapUser.displayName, source: 'ldap' }, auditSource.EXTERNAL_LDAP_TASK));
|
||||
if (userAddError) debug('syncUsers: Failed to create user', ldapUser, userAddError.message);
|
||||
} else if (user.source !== 'ldap') {
|
||||
debug(`[conflicting user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`);
|
||||
} else if (user.email !== ldapUser.email || user.displayName !== ldapUser.displayName) {
|
||||
debug(`[updating user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`);
|
||||
const [userAddError] = await safe(users.add(ldapUser.email, { username: ldapUser.username, password: null, displayName: ldapUser.displayName, source: 'ldap' }, auditSource.EXTERNAL_LDAP_TASK));
|
||||
if (userAddError) debug('syncUsers: Failed to create user', ldapUser, userAddError.message);
|
||||
} else if (user.source !== 'ldap') {
|
||||
debug(`syncUsers: [conflicting user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`);
|
||||
} else if (user.email !== ldapUser.email || user.displayName !== ldapUser.displayName) {
|
||||
debug(`syncUsers: [updating user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`);
|
||||
|
||||
const [userUpdateError] = await safe(users.update(user, { email: ldapUser.email, fallbackEmail: ldapUser.email, displayName: ldapUser.displayName }, auditSource.EXTERNAL_LDAP_TASK));
|
||||
if (userUpdateError) debug('Failed to update user', ldapUser, userUpdateError);
|
||||
} else {
|
||||
// user known and up-to-date
|
||||
debug(`[up-to-date user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`);
|
||||
}
|
||||
const [userUpdateError] = await safe(users.update(user, { email: ldapUser.email, fallbackEmail: ldapUser.email, displayName: ldapUser.displayName }, auditSource.EXTERNAL_LDAP_TASK));
|
||||
if (userUpdateError) debug('Failed to update user', ldapUser, userUpdateError);
|
||||
} else {
|
||||
// user known and up-to-date
|
||||
debug(`syncUsers: [up-to-date user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`);
|
||||
}
|
||||
}
|
||||
|
||||
debug('syncUsers: done');
|
||||
}
|
||||
|
||||
async function syncGroups(externalLdapConfig, progressCallback) {
|
||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
|
||||
if (!externalLdapConfig.syncGroups) {
|
||||
debug('syncGroups: Group sync is disabled');
|
||||
progressCallback({ percent: 70, message: 'Skipping group sync...' });
|
||||
return [];
|
||||
}
|
||||
|
||||
const ldapGroups = await ldapGroupSearch(externalLdapConfig, {});
|
||||
|
||||
debug(`syncGroups: Found ${ldapGroups.length} groups`);
|
||||
|
||||
let percent = 40;
|
||||
let step = 30/(ldapGroups.length+1); // ensure no divide by 0
|
||||
|
||||
// we ignore all non internal errors here and just log them for now
|
||||
for (const ldapGroup of ldapGroups) {
|
||||
let groupName = ldapGroup[externalLdapConfig.groupnameField];
|
||||
if (!groupName) return;
|
||||
// some servers return empty array for unknown properties :-/
|
||||
if (typeof groupName !== 'string') return;
|
||||
|
||||
// groups are lowercase
|
||||
groupName = groupName.toLowerCase();
|
||||
|
||||
percent += step;
|
||||
progressCallback({ percent, message: `Syncing... ${groupName}` });
|
||||
|
||||
const result = await groups.getByName(groupName);
|
||||
|
||||
if (!result) {
|
||||
debug(`syncGroups: [adding group] groupname=${groupName}`);
|
||||
|
||||
const [error] = await safe(groups.add({ name: groupName, source: 'ldap' }));
|
||||
if (error) debug('syncGroups: Failed to create group', groupName, error);
|
||||
} else {
|
||||
debug(`syncGroups: [up-to-date group] groupname=${groupName}`);
|
||||
}
|
||||
}
|
||||
|
||||
debug('syncGroups: sync done');
|
||||
}
|
||||
|
||||
async function syncGroupUsers(externalLdapConfig, progressCallback) {
|
||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
|
||||
if (!externalLdapConfig.syncGroups) {
|
||||
debug('syncGroupUsers: Group users sync is disabled');
|
||||
progressCallback({ percent: 99, message: 'Skipping group users sync...' });
|
||||
return [];
|
||||
}
|
||||
|
||||
const allGroups = await groups.list();
|
||||
const ldapGroups = allGroups.filter(function (g) { return g.source === 'ldap'; });
|
||||
debug(`syncGroupUsers: Found ${ldapGroups.length} groups to sync users`);
|
||||
|
||||
for (const group of ldapGroups) {
|
||||
debug(`syncGroupUsers: Sync users for group ${group.name}`);
|
||||
|
||||
const result = await ldapGroupSearch(externalLdapConfig, {});
|
||||
if (!result || result.length === 0) {
|
||||
debug(`syncGroupUsers: Unable to find group ${group.name} ignoring for now.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function syncGroups(externalLdapConfig, progressCallback, callback) {
|
||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!externalLdapConfig.syncGroups) {
|
||||
debug('Group sync is disabled');
|
||||
progressCallback({ percent: 70, message: 'Skipping group sync...' });
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
ldapGroupSearch(externalLdapConfig, {}, function (error, ldapGroups) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug(`Found ${ldapGroups.length} groups`);
|
||||
|
||||
let percent = 40;
|
||||
let step = 30/(ldapGroups.length+1); // ensure no divide by 0
|
||||
|
||||
// we ignore all non internal errors here and just log them for now
|
||||
async.eachSeries(ldapGroups, async function (ldapGroup) {
|
||||
var groupName = ldapGroup[externalLdapConfig.groupnameField];
|
||||
if (!groupName) return;
|
||||
// some servers return empty array for unknown properties :-/
|
||||
if (typeof groupName !== 'string') return;
|
||||
|
||||
// groups are lowercase
|
||||
groupName = groupName.toLowerCase();
|
||||
|
||||
percent += step;
|
||||
progressCallback({ percent, message: `Syncing... ${groupName}` });
|
||||
|
||||
let [error, result] = await safe(groups.getByName(groupName));
|
||||
if (error && error.reason !== BoxError.NOT_FOUND) throw error;
|
||||
|
||||
if (!result) {
|
||||
debug(`[adding group] groupname=${groupName}`);
|
||||
|
||||
[error] = await safe(groups.add({ name: groupName, source: 'ldap' }));
|
||||
if (error) debug('syncGroups: Failed to create group', groupName, error);
|
||||
} else {
|
||||
debug(`[up-to-date group] groupname=${groupName}`);
|
||||
}
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('sync: ldap sync is done', error);
|
||||
|
||||
callback(error);
|
||||
// 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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function syncGroupUsers(externalLdapConfig, progressCallback, callback) {
|
||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
if (!found) {
|
||||
debug(`syncGroupUsers: Unable to find group ${group.name} ignoring for now.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!externalLdapConfig.syncGroups) {
|
||||
debug('Group users sync is disabled');
|
||||
progressCallback({ percent: 99, message: 'Skipping group users sync...' });
|
||||
return callback(null, []);
|
||||
let ldapGroupMembers = found.member || found.uniqueMember || [];
|
||||
|
||||
// if only one entry is in the group ldap returns a string, not an array!
|
||||
if (typeof ldapGroupMembers === 'string') ldapGroupMembers = [ ldapGroupMembers ];
|
||||
|
||||
debug(`syncGroupUsers: Group ${group.name} has ${ldapGroupMembers.length} members.`);
|
||||
|
||||
for (const memberDn of ldapGroupMembers) {
|
||||
const [ldapError, result] = await safe(ldapGetByDN(externalLdapConfig, memberDn));
|
||||
if (ldapError) {
|
||||
debug(`syncGroupUsers: Failed to get ${memberDn}:`, ldapError);
|
||||
continue;
|
||||
}
|
||||
|
||||
debug(`syncGroupUsers: Found member object at ${memberDn} adding to group ${group.name}`);
|
||||
|
||||
const username = result[externalLdapConfig.usernameField];
|
||||
if (!username) continue;
|
||||
|
||||
const [getError, userObject] = await safe(users.getByUsername(username));
|
||||
if (getError || !userObject) {
|
||||
debug(`syncGroupUsers: Failed to get user by username ${username}`, getError ? getError : 'User not found');
|
||||
continue;
|
||||
}
|
||||
|
||||
const [addError] = await safe(groups.addMember(group.id, userObject.id));
|
||||
if (addError && addError.reason !== BoxError.ALREADY_EXISTS) debug('syncGroupUsers: Failed to add member', addError);
|
||||
}
|
||||
}
|
||||
|
||||
const listGroups = util.callbackify(groups.list);
|
||||
|
||||
listGroups(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const ldapGroups = result.filter(function (g) { return g.source === 'ldap'; });
|
||||
debug(`Found ${ldapGroups.length} groups to sync users`);
|
||||
|
||||
async.eachSeries(ldapGroups, function (group, iteratorDone) {
|
||||
debug(`Sync users for group ${group.name}`);
|
||||
|
||||
ldapGroupSearch(externalLdapConfig, {}, function (error, result) {
|
||||
if (error) return iteratorDone(error);
|
||||
if (!result || result.length === 0) {
|
||||
debug(`syncGroupUsers: Unable to find group ${group.name} ignoring for now.`);
|
||||
return iteratorDone();
|
||||
}
|
||||
|
||||
// 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 (!found) {
|
||||
debug(`syncGroupUsers: Unable to find group ${group.name} ignoring for now.`);
|
||||
return iteratorDone();
|
||||
}
|
||||
|
||||
var ldapGroupMembers = found.member || found.uniqueMember || [];
|
||||
|
||||
// if only one entry is in the group ldap returns a string, not an array!
|
||||
if (typeof ldapGroupMembers === 'string') ldapGroupMembers = [ ldapGroupMembers ];
|
||||
|
||||
debug(`Group ${group.name} has ${ldapGroupMembers.length} members.`);
|
||||
|
||||
async.eachSeries(ldapGroupMembers, function (memberDn, iteratorCallback) {
|
||||
ldapGetByDN(externalLdapConfig, memberDn, function (error, result) {
|
||||
if (error) {
|
||||
debug(`Failed to get ${memberDn}:`, error);
|
||||
return iteratorCallback();
|
||||
}
|
||||
|
||||
debug(`Found member object at ${memberDn} adding to group ${group.name}`);
|
||||
|
||||
const username = result[externalLdapConfig.usernameField];
|
||||
if (!username) return iteratorCallback();
|
||||
|
||||
users.getByUsername(username, async function (error, result) {
|
||||
if (error) {
|
||||
debug(`syncGroupUsers: Failed to get user by username ${username}`, error);
|
||||
return iteratorCallback();
|
||||
}
|
||||
|
||||
[error] = await safe(groups.addMember(group.id, result.id));
|
||||
if (error && error.reason !== BoxError.ALREADY_EXISTS) debug('syncGroupUsers: Failed to add member', error);
|
||||
iteratorCallback();
|
||||
});
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) debug('syncGroupUsers: ', error);
|
||||
iteratorDone();
|
||||
});
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
debug('syncGroupUsers: done');
|
||||
}
|
||||
|
||||
function sync(progressCallback, callback) {
|
||||
async function sync(progressCallback) {
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
progressCallback({ percent: 10, message: 'Starting ldap user sync' });
|
||||
|
||||
settingsGetExternalLdapConfig(function (error, externalLdapConfig) {
|
||||
if (error) return callback(error);
|
||||
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
||||
const externalLdapConfig = await settings.getExternalLdapConfig();
|
||||
if (externalLdapConfig.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
|
||||
|
||||
async.series([
|
||||
syncUsers.bind(null, externalLdapConfig, progressCallback),
|
||||
syncGroups.bind(null, externalLdapConfig, progressCallback),
|
||||
syncGroupUsers.bind(null, externalLdapConfig, progressCallback)
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
await syncUsers(externalLdapConfig, progressCallback);
|
||||
await syncGroups(externalLdapConfig, progressCallback);
|
||||
await syncGroupUsers(externalLdapConfig, progressCallback);
|
||||
|
||||
progressCallback({ percent: 100, message: 'Done' });
|
||||
progressCallback({ percent: 100, message: 'Done' });
|
||||
|
||||
debug('sync: ldap sync is done', error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
debug('sync: done');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user