2019-08-28 18:22:07 +02:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
exports = module.exports = {
|
2019-08-29 22:43:06 +02:00
|
|
|
verifyPassword: verifyPassword,
|
|
|
|
|
|
2019-08-28 18:22:07 +02:00
|
|
|
testConfig: testConfig,
|
2019-08-29 17:19:51 +02:00
|
|
|
startSyncer: startSyncer,
|
2019-08-28 18:22:07 +02:00
|
|
|
|
2019-10-25 15:58:11 -07:00
|
|
|
injectPrivateFields: injectPrivateFields,
|
|
|
|
|
removePrivateFields: removePrivateFields,
|
|
|
|
|
|
2019-08-28 18:22:07 +02:00
|
|
|
sync: sync
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var assert = require('assert'),
|
2019-08-29 22:56:48 +02:00
|
|
|
async = require('async'),
|
2019-10-30 11:02:21 -07:00
|
|
|
auditSource = require('./auditsource.js'),
|
2019-10-24 13:41:41 -07:00
|
|
|
BoxError = require('./boxerror.js'),
|
2019-10-25 15:58:11 -07:00
|
|
|
constants = require('./constants.js'),
|
2019-08-30 19:11:27 +02:00
|
|
|
debug = require('debug')('box:externalldap'),
|
2019-08-28 18:22:07 +02:00
|
|
|
ldap = require('ldapjs'),
|
|
|
|
|
settings = require('./settings.js'),
|
2019-08-29 17:19:51 +02:00
|
|
|
tasks = require('./tasks.js'),
|
2019-10-24 15:12:58 -07:00
|
|
|
users = require('./users.js');
|
2019-08-28 18:22:07 +02:00
|
|
|
|
2019-10-25 15:58:11 -07:00
|
|
|
function injectPrivateFields(newConfig, currentConfig) {
|
|
|
|
|
if (newConfig.bindPassword === constants.SECRET_PLACEHOLDER) newConfig.bindPassword = currentConfig.bindPassword;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removePrivateFields(ldapConfig) {
|
|
|
|
|
assert.strictEqual(typeof ldapConfig, 'object');
|
|
|
|
|
if (ldapConfig.bindPassword) ldapConfig.bindPassword = constants.SECRET_PLACEHOLDER;
|
|
|
|
|
return ldapConfig;
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-28 18:22:07 +02:00
|
|
|
// performs service bind if required
|
2019-08-29 17:19:51 +02:00
|
|
|
function getClient(externalLdapConfig, callback) {
|
2019-10-30 14:37:48 -07:00
|
|
|
assert.strictEqual(typeof externalLdapConfig, 'object');
|
2019-08-28 18:22:07 +02:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2019-08-29 22:51:27 +02:00
|
|
|
// basic validation to not crash
|
2019-10-24 13:41:41 -07:00
|
|
|
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')); }
|
2019-08-29 22:51:27 +02:00
|
|
|
|
2019-08-29 17:19:51 +02:00
|
|
|
var client;
|
|
|
|
|
try {
|
|
|
|
|
client = ldap.createClient({ url: externalLdapConfig.url });
|
|
|
|
|
} catch (e) {
|
2019-10-24 13:41:41 -07:00
|
|
|
if (e instanceof ldap.ProtocolError) return callback(new BoxError(BoxError.BAD_FIELD, 'url protocol is invalid'));
|
|
|
|
|
return callback(new BoxError(BoxError.INTERNAL_ERROR, e));
|
2019-08-29 17:19:51 +02:00
|
|
|
}
|
2019-08-28 18:22:07 +02:00
|
|
|
|
2019-08-29 17:19:51 +02:00
|
|
|
if (!externalLdapConfig.bindDn) return callback(null, client);
|
2019-08-28 18:22:07 +02:00
|
|
|
|
2019-08-29 17:19:51 +02:00
|
|
|
client.bind(externalLdapConfig.bindDn, externalLdapConfig.bindPassword, function (error) {
|
2019-10-24 13:41:41 -07:00
|
|
|
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
|
|
|
|
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
2019-08-28 18:22:07 +02:00
|
|
|
|
2019-08-29 17:19:51 +02:00
|
|
|
callback(null, client, externalLdapConfig);
|
2019-08-28 18:22:07 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-10-30 14:37:48 -07:00
|
|
|
function ldapSearch(externalLdapConfig, options, callback) {
|
|
|
|
|
assert.strictEqual(typeof externalLdapConfig, 'object');
|
|
|
|
|
assert.strictEqual(typeof options, 'object');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
getClient(externalLdapConfig, function (error, client) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
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()}`);
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-28 18:22:07 +02:00
|
|
|
function testConfig(config, callback) {
|
|
|
|
|
assert.strictEqual(typeof config, 'object');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2019-10-25 15:40:22 -07:00
|
|
|
if (config.provider === 'noop') return callback();
|
2019-08-29 12:28:41 +02:00
|
|
|
|
2019-10-24 13:41:41 -07:00
|
|
|
if (!config.url) return callback(new BoxError(BoxError.BAD_FIELD, 'url must not be empty'));
|
|
|
|
|
if (!config.baseDn) return callback(new BoxError(BoxError.BAD_FIELD, 'basedn must not be empty'));
|
|
|
|
|
if (!config.filter) return callback(new BoxError(BoxError.BAD_FIELD, 'filter must not be empty'));
|
2019-10-25 15:47:55 -07:00
|
|
|
if (!config.usernameField) config.usernameField = 'uid';
|
2019-10-30 09:35:30 -07:00
|
|
|
// bindDn may not be a dn!
|
2019-08-28 18:22:07 +02:00
|
|
|
|
2019-08-29 17:19:51 +02:00
|
|
|
getClient(config, function (error, client) {
|
2019-08-29 14:21:07 +02:00
|
|
|
if (error) return callback(error);
|
2019-08-28 18:22:07 +02:00
|
|
|
|
|
|
|
|
var opts = {
|
|
|
|
|
filter: config.filter,
|
|
|
|
|
scope: 'sub'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
client.search(config.baseDn, opts, function (error, result) {
|
2019-10-24 13:41:41 -07:00
|
|
|
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
2019-08-28 18:22:07 +02:00
|
|
|
|
2019-10-23 15:57:01 -07:00
|
|
|
result.on('searchEntry', function (/* entry */) {});
|
2019-10-25 16:58:15 -07:00
|
|
|
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(); });
|
2019-08-28 18:22:07 +02:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-29 22:43:06 +02:00
|
|
|
function verifyPassword(user, password, callback) {
|
|
|
|
|
assert.strictEqual(typeof user, 'object');
|
|
|
|
|
assert.strictEqual(typeof password, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
|
2019-10-24 18:32:33 -07:00
|
|
|
if (error) return callback(error);
|
2019-10-25 15:40:22 -07:00
|
|
|
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
2019-08-29 22:43:06 +02:00
|
|
|
|
2019-10-30 14:37:48 -07:00
|
|
|
ldapSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` }, function (error, ldapUsers) {
|
2019-08-29 22:43:06 +02:00
|
|
|
if (error) return callback(error);
|
2019-10-30 14:37:48 -07:00
|
|
|
if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
|
|
|
|
|
if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT));
|
2019-08-29 22:43:06 +02:00
|
|
|
|
2019-10-30 14:37:48 -07:00
|
|
|
const userDn = ldapUsers[0].dn;
|
|
|
|
|
let client = ldap.createClient({ url: externalLdapConfig.url });
|
2019-08-29 22:43:06 +02:00
|
|
|
|
2019-10-30 14:37:48 -07:00
|
|
|
client.bind(userDn, password, function (error) {
|
2019-10-24 13:41:41 -07:00
|
|
|
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
|
|
|
|
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
2019-08-29 22:43:06 +02:00
|
|
|
|
|
|
|
|
callback();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-29 17:19:51 +02:00
|
|
|
function startSyncer(callback) {
|
2019-08-28 18:22:07 +02:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2019-08-29 17:19:51 +02:00
|
|
|
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
|
2019-10-24 18:32:33 -07:00
|
|
|
if (error) return callback(error);
|
2019-10-25 15:40:22 -07:00
|
|
|
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
2019-08-28 18:22:07 +02:00
|
|
|
|
2019-08-29 17:19:51 +02:00
|
|
|
tasks.add(tasks.TASK_SYNC_EXTERNAL_LDAP, [], function (error, taskId) {
|
2019-10-24 18:32:33 -07:00
|
|
|
if (error) return callback(error);
|
2019-08-28 18:22:07 +02:00
|
|
|
|
2019-08-29 17:19:51 +02:00
|
|
|
tasks.startTask(taskId, {}, function (error, result) {
|
|
|
|
|
debug('sync: done', error, result);
|
2019-08-28 18:22:07 +02:00
|
|
|
});
|
2019-08-29 17:19:51 +02:00
|
|
|
|
|
|
|
|
callback(null, taskId);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sync(progressCallback, callback) {
|
|
|
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2019-08-30 19:11:27 +02:00
|
|
|
debug('Start user syncing ...');
|
|
|
|
|
|
2019-08-29 17:19:51 +02:00
|
|
|
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
|
2019-10-24 18:32:33 -07:00
|
|
|
if (error) return callback(error);
|
2019-10-25 15:40:22 -07:00
|
|
|
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
2019-08-29 17:19:51 +02:00
|
|
|
|
2019-10-30 14:37:48 -07:00
|
|
|
ldapSearch(externalLdapConfig, {}, function (error, ldapUsers) {
|
2019-08-29 17:19:51 +02:00
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
2019-10-30 14:37:48 -07:00
|
|
|
debug(`Found ${ldapUsers.length} users`);
|
2019-08-29 22:56:48 +02:00
|
|
|
|
2019-10-30 14:37:48 -07:00
|
|
|
// we ignore all errors here and just log them for now
|
|
|
|
|
async.eachSeries(ldapUsers, function (user, iteratorCallback) {
|
|
|
|
|
const username = user[externalLdapConfig.usernameField];
|
|
|
|
|
const email = user.mail;
|
|
|
|
|
const displayName = user.cn; // user.giveName + ' ' + user.sn
|
2019-10-25 15:47:55 -07:00
|
|
|
|
2019-10-30 14:37:48 -07:00
|
|
|
if (!username || !email || !displayName) {
|
|
|
|
|
debug(`[empty username/email/displayName] username=${username} email=${email} displayName=${displayName} usernameField=${externalLdapConfig.usernameField}`);
|
|
|
|
|
return iteratorCallback();
|
|
|
|
|
}
|
2019-10-25 16:58:15 -07:00
|
|
|
|
2019-10-30 14:37:48 -07:00
|
|
|
users.getByUsername(username, function (error, result) {
|
|
|
|
|
if (error && error.reason !== BoxError.NOT_FOUND) {
|
|
|
|
|
debug(`Could not find user with username ${username}: ${error.message}`);
|
|
|
|
|
return iteratorCallback();
|
|
|
|
|
}
|
2019-08-29 22:56:48 +02:00
|
|
|
|
2019-10-30 14:37:48 -07:00
|
|
|
if (error) {
|
|
|
|
|
debug(`[adding user] username=${username} email=${email} displayName=${displayName}`);
|
2019-08-29 23:13:23 +02:00
|
|
|
|
2019-10-30 14:37:48 -07:00
|
|
|
users.create(username, null /* password */, email, displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
|
|
|
|
|
if (error) console.error('Failed to create user', user, error);
|
|
|
|
|
iteratorCallback();
|
|
|
|
|
});
|
|
|
|
|
} else if (result.source !== 'ldap') {
|
|
|
|
|
debug(`[conflicting user] username=${username} email=${email} displayName=${displayName}`);
|
2019-08-29 23:13:23 +02:00
|
|
|
|
2019-10-30 14:37:48 -07:00
|
|
|
iteratorCallback();
|
|
|
|
|
} else if (result.email !== email || result.displayName !== displayName) {
|
|
|
|
|
debug(`[updating user] username=${username} email=${email} displayName=${displayName}`);
|
2019-10-25 16:13:41 -07:00
|
|
|
|
2019-10-30 14:37:48 -07:00
|
|
|
users.update(result.id, { email: email, fallbackEmail: email, displayName: displayName }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
|
|
|
|
|
if (error) debug('Failed to update user', user, error);
|
2019-10-25 16:13:41 -07:00
|
|
|
|
2019-10-30 14:37:48 -07:00
|
|
|
iteratorCallback();
|
2019-08-29 22:56:48 +02:00
|
|
|
});
|
2019-10-30 14:37:48 -07:00
|
|
|
} else {
|
|
|
|
|
// user known and up-to-date
|
|
|
|
|
debug(`[up-to-date user] username=${username} email=${email} displayName=${displayName}`);
|
|
|
|
|
|
|
|
|
|
iteratorCallback();
|
|
|
|
|
}
|
2019-08-29 17:19:51 +02:00
|
|
|
});
|
2019-10-30 14:37:48 -07:00
|
|
|
}, function (error) {
|
|
|
|
|
debug('sync: ldap sync is done', error);
|
|
|
|
|
callback(error);
|
2019-08-28 18:22:07 +02:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|