'use strict'; exports = module.exports = { verifyPassword: verifyPassword, testConfig: testConfig, startSyncer: startSyncer, injectPrivateFields: injectPrivateFields, removePrivateFields: removePrivateFields, sync: sync }; var assert = require('assert'), async = require('async'), auditSource = require('./auditsource.js'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), debug = require('debug')('box:externalldap'), ldap = require('ldapjs'), settings = require('./settings.js'), tasks = require('./tasks.js'), users = require('./users.js'); 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; } // performs service bind if required function getClient(externalLdapConfig, callback) { assert.strictEqual(typeof externalLdapConfig, 'object'); assert.strictEqual(typeof callback, 'function'); // 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')); } var client; try { client = ldap.createClient({ url: externalLdapConfig.url }); } 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 (!externalLdapConfig.bindDn) return callback(null, 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)); callback(null, client, externalLdapConfig); }); } 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); }); }); }); } function testConfig(config, callback) { assert.strictEqual(typeof config, 'object'); assert.strictEqual(typeof callback, 'function'); if (config.provider === 'noop') return callback(); 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')); if (!config.usernameField) config.usernameField = 'uid'; // bindDn may not be a dn! getClient(config, function (error, client) { if (error) return callback(error); var opts = { filter: config.filter, scope: 'sub' }; client.search(config.baseDn, opts, function (error, result) { if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error)); 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(); }); }); }); } 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) { if (error) return callback(error); if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled')); ldapSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` }, function (error, ldapUsers) { if (error) return callback(error); if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND)); if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT)); const userDn = ldapUsers[0].dn; let client = ldap.createClient({ url: externalLdapConfig.url }); client.bind(userDn, password, function (error) { if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error)); callback(); }); }); }); } function startSyncer(callback) { assert.strictEqual(typeof callback, 'function'); settings.getExternalLdapConfig(function (error, externalLdapConfig) { if (error) return callback(error); if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled')); tasks.add(tasks.TASK_SYNC_EXTERNAL_LDAP, [], function (error, taskId) { if (error) return callback(error); tasks.startTask(taskId, {}, function (error, result) { debug('sync: done', error, result); }); callback(null, taskId); }); }); } function sync(progressCallback, callback) { assert.strictEqual(typeof progressCallback, 'function'); assert.strictEqual(typeof callback, 'function'); debug('Start user syncing ...'); settings.getExternalLdapConfig(function (error, externalLdapConfig) { if (error) return callback(error); if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled')); ldapSearch(externalLdapConfig, {}, function (error, ldapUsers) { if (error) return callback(error); debug(`Found ${ldapUsers.length} users`); // 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 if (!username || !email || !displayName) { debug(`[empty username/email/displayName] username=${username} email=${email} displayName=${displayName} usernameField=${externalLdapConfig.usernameField}`); return iteratorCallback(); } 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(); } if (error) { debug(`[adding user] username=${username} email=${email} displayName=${displayName}`); 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}`); iteratorCallback(); } else if (result.email !== email || result.displayName !== displayName) { debug(`[updating user] username=${username} email=${email} displayName=${displayName}`); 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); iteratorCallback(); }); } else { // user known and up-to-date debug(`[up-to-date user] username=${username} email=${email} displayName=${displayName}`); iteratorCallback(); } }); }, function (error) { debug('sync: ldap sync is done', error); callback(error); }); }); }); }