2019-08-28 18:22:07 +02:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
exports = module.exports = {
|
|
|
|
|
ExternalLdapError: ExternalLdapError,
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
sync: sync
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var assert = require('assert'),
|
2019-08-29 22:56:48 +02:00
|
|
|
async = require('async'),
|
2019-08-29 22:15:48 +02:00
|
|
|
auditsource = require('./auditsource.js'),
|
2019-08-28 18:22:07 +02:00
|
|
|
debug = require('debug')('box:ldapclient'),
|
|
|
|
|
ldap = require('ldapjs'),
|
|
|
|
|
settings = require('./settings.js'),
|
2019-08-29 17:19:51 +02:00
|
|
|
tasks = require('./tasks.js'),
|
2019-08-29 22:15:48 +02:00
|
|
|
users = require('./users.js'),
|
|
|
|
|
UserError = users.UsersError,
|
2019-08-28 18:22:07 +02:00
|
|
|
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
|
2019-08-29 17:19:51 +02:00
|
|
|
function getClient(externalLdapConfig, callback) {
|
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
|
|
|
|
|
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)); }
|
|
|
|
|
|
2019-08-29 17:19:51 +02:00
|
|
|
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));
|
|
|
|
|
}
|
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) {
|
|
|
|
|
if (error instanceof ldap.InvalidCredentialsError) return callback(new ExternalLdapError(ExternalLdapError.INVALID_CREDENTIALS));
|
|
|
|
|
if (error) return callback(new ExternalLdapError(ExternalLdapError.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
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function testConfig(config, callback) {
|
|
|
|
|
assert.strictEqual(typeof config, 'object');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2019-08-29 12:28:41 +02:00
|
|
|
if (!config.enabled) return callback();
|
|
|
|
|
|
2019-08-28 18:22:07 +02:00
|
|
|
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'));
|
|
|
|
|
|
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) {
|
|
|
|
|
if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
|
|
|
|
|
|
|
|
|
|
result.on('searchEntry', function (entry) {});
|
2019-08-29 15:14:12 +02:00
|
|
|
result.on('error', function (error) { callback(new ExternalLdapError(ExternalLdapError.BAD_FIELD, 'Unable to search directory')); });
|
2019-08-28 18:22:07 +02:00
|
|
|
result.on('end', function (result) { callback(); });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
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) {
|
|
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
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) {
|
|
|
|
|
if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, error));
|
|
|
|
|
if (!externalLdapConfig.enabled) return callback(new ExternalLdapError(ExternalLdapError.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) {
|
|
|
|
|
if (error) return callback(new ExternalLdapError(ExternalLdapError.INTERNAL_ERROR, 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');
|
|
|
|
|
|
|
|
|
|
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,
|
2019-08-29 22:51:27 +02:00
|
|
|
scope: 'sub' // We may have to make this configurable
|
2019-08-29 17:19:51 +02:00
|
|
|
};
|
|
|
|
|
|
2019-08-29 22:15:48 +02:00
|
|
|
// TODO this needs pagination!
|
2019-08-29 17:19:51 +02:00
|
|
|
client.search(externalLdapConfig.baseDn, opts, function (error, result) {
|
|
|
|
|
if (error) return callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
|
|
|
|
|
|
2019-08-29 22:56:48 +02:00
|
|
|
var ldapUsers = [];
|
|
|
|
|
|
2019-08-29 22:15:48 +02:00
|
|
|
result.on('searchEntry', function (entry) {
|
2019-08-29 22:56:48 +02:00
|
|
|
ldapUsers.push(entry.object);
|
2019-08-29 17:19:51 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
result.on('error', function (error) {
|
|
|
|
|
callback(new ExternalLdapError(ExternalLdapError.EXTERNAL_ERROR, error));
|
|
|
|
|
});
|
|
|
|
|
|
2019-08-29 22:15:48 +02:00
|
|
|
result.on('end', function (result) {
|
2019-08-29 17:19:51 +02:00
|
|
|
console.log('status: ' + result.status);
|
2019-08-29 22:56:48 +02:00
|
|
|
|
|
|
|
|
// 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);
|
2019-08-29 17:19:51 +02:00
|
|
|
});
|
2019-08-28 18:22:07 +02:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|