'use strict'; exports = module.exports = { ExternalLdapError: ExternalLdapError, verifyPassword: verifyPassword, testConfig: testConfig, startSyncer: startSyncer, sync: sync }; var assert = require('assert'), async = require('async'), auditsource = require('./auditsource.js'), debug = require('debug')('box:ldapclient'), ldap = require('ldapjs'), settings = require('./settings.js'), tasks = require('./tasks.js'), users = require('./users.js'), UserError = users.UsersError, util = require('util'); function ExternalLdapError(reason, errorOrMessage) { assert.strictEqual(typeof reason, 'string'); assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined'); Error.call(this); Error.captureStackTrace(this, this.constructor); this.name = this.constructor.name; this.reason = reason; if (typeof errorOrMessage === 'undefined') { this.message = reason; } else if (typeof errorOrMessage === 'string') { this.message = errorOrMessage; } else { this.message = 'Internal error'; this.nestedError = errorOrMessage; } } util.inherits(ExternalLdapError, Error); ExternalLdapError.EXTERNAL_ERROR = 'external error'; ExternalLdapError.INTERNAL_ERROR = 'internal error'; ExternalLdapError.INVALID_CREDENTIALS = 'invalid credentials'; ExternalLdapError.BAD_STATE = 'bad state'; ExternalLdapError.BAD_FIELD = 'bad field'; ExternalLdapError.NOT_FOUND = 'not found'; // performs service bind if required function getClient(externalLdapConfig, callback) { assert.strictEqual(typeof callback, 'function'); // basic validation to not crash try { ldap.parseDN(externalLdapConfig.baseDn); } catch (e) { return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'invalid baseDn')); } try { ldap.parseFilter(externalLdapConfig.filter); } catch (e) { return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'invalid filter')); } if (externalLdapConfig.bindDn) try { ldap.parseFilter(externalLdapConfig.bindDn); } catch (e) { return callback(new ExternalLdapError(ExternalLdapError.INVALID_CREDENTIALS)); } var client; try { client = ldap.createClient({ url: externalLdapConfig.url }); } catch (e) { if (e instanceof ldap.ProtocolError) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'url protocol is invalid')); return callback(new ExternalLdapError(ExternalLdapError.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 ExternalLdapError(ExternalLdapError.INVALID_CREDENTIALS)); if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error)); callback(null, client, externalLdapConfig); }); } function testConfig(config, callback) { assert.strictEqual(typeof config, 'object'); assert.strictEqual(typeof callback, 'function'); if (!config.enabled) return callback(); if (!config.url) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'url must not be empty')); if (!config.baseDn) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'basedn must not be empty')); if (!config.filter) return callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'filter must not be empty')); 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 ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error)); result.on('searchEntry', function (entry) {}); result.on('error', function (error) { callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'Unable to search directory')); }); result.on('end', function (result) { 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(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error)); if (!externalLdapConfig.enabled) return callback(new ExternalLdapError(ExternalLdapError.BAD_STATE, 'not enabled')); getClient(externalLdapConfig, function (error, client) { if (error) return callback(error); const dn = `uid=${user.username},${externalLdapConfig.baseDn}`; client.bind(dn, password, function (error) { if (error instanceof ldap.InvalidCredentialsError) return callback(new ExternalLdapError(ExternalLdapError.INVALID_CREDENTIALS)); if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error)); callback(); }); }); }); } function startSyncer(callback) { assert.strictEqual(typeof callback, 'function'); settings.getExternalLdapConfig(function (error, externalLdapConfig) { if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error)); if (!externalLdapConfig.enabled) return callback(new ExternalLdapError(ExternalLdapError.BAD_STATE, 'not enabled')); tasks.add(tasks.TASK_SYNC_EXTERNAL_LDAP, [], function (error, taskId) { if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, 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'); settings.getExternalLdapConfig(function (error, externalLdapConfig) { if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error)); if (!externalLdapConfig.enabled) return callback(new ExternalLdapError(ExternalLdapError.BAD_STATE, 'not enabled')); getClient(externalLdapConfig, function (error, client) { if (error) return callback(error); var opts = { filter: externalLdapConfig.filter, scope: 'sub' // We may have to make this configurable }; // TODO this needs pagination! client.search(externalLdapConfig.baseDn, opts, function (error, result) { if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error)); var ldapUsers = []; result.on('searchEntry', function (entry) { ldapUsers.push(entry.object); }); result.on('error', function (error) { callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error)); }); result.on('end', function (result) { console.log('status: ' + result.status); // we ignore all errors here and just log them for now async.eachSeries(ldapUsers, function (user, callback) { // ignore the bindDn user if any if (user.dn === externalLdapConfig.bindDn) return callback(); users.getByUsername(user.uid, function (error, result) { if (error && error.reason !== UserError.NOT_FOUND) { console.error(error); return callback(); } if (error) { users.create(user.uid, null, user.mail, user.cn, { source: 'ldap' }, auditsource.EXTERNAL_LDAP_TASK, function (error) { if (error) console.error('Failed to create user', user, error); callback(); }); } else if (result.email !== user.mail || result.displayName !== user.cn) { users.update(result.id, { email: user.mail, displayName: user.cn }, auditsource.EXTERNAL_LDAP_TASK, function (error) { if (error) console.error('Failed to update user', user, error); callback(); }); } else { // user known and up-to-date callback(); } }); }, callback); }); }); }); }); }