diff --git a/src/externalldap.js b/src/externalldap.js index 11c6120e9..6a8a7ff58 100644 --- a/src/externalldap.js +++ b/src/externalldap.js @@ -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'); } diff --git a/src/routes/cloudron.js b/src/routes/cloudron.js index aa950b254..989ca1c20 100644 --- a/src/routes/cloudron.js +++ b/src/routes/cloudron.js @@ -290,12 +290,11 @@ async function renewCerts(req, res, next) { next(new HttpSuccess(202, { taskId })); } -function syncExternalLdap(req, res, next) { - externalLdap.startSyncer(function (error, taskId) { - if (error) return next(new HttpError(500, error.message)); +async function syncExternalLdap(req, res, next) { + const [error, taskId] = await safe(externalLdap.startSyncer()); + if (error) return next(new HttpError(500, error.message)); - next(new HttpSuccess(202, { taskId })); - }); + next(new HttpSuccess(202, { taskId })); } function getServerIp(req, res, next) { diff --git a/src/settings.js b/src/settings.js index 20f59cbed..ab89dd79c 100644 --- a/src/settings.js +++ b/src/settings.js @@ -478,8 +478,8 @@ async function setExternalLdapConfig(externalLdapConfig) { externalLdap.injectPrivateFields(externalLdapConfig, currentConfig); - const externalLdapTestConfig = util.promisify(externalLdap.testConfig); - await externalLdapTestConfig(externalLdapConfig); + const error = await externalLdap.testConfig(externalLdapConfig); + if (error) throw error; await set(exports.EXTERNAL_LDAP_KEY, JSON.stringify(externalLdapConfig)); diff --git a/src/test/common.js b/src/test/common.js index 69b19ae19..0e6123bed 100644 --- a/src/test/common.js +++ b/src/test/common.js @@ -131,6 +131,8 @@ exports = module.exports = { manifest, user, appstoreToken: 'atoken', + + serverUrl: `http://localhost:${constants.PORT}`, }; function createTree(root, obj) { diff --git a/src/test/externalldap-test.js b/src/test/externalldap-test.js index 47df0d161..b3d87a3a8 100644 --- a/src/test/externalldap-test.js +++ b/src/test/externalldap-test.js @@ -7,41 +7,20 @@ const async = require('async'), BoxError = require('../boxerror.js'), - database = require('../database.js'), - constants = require('../constants.js'), + common = require('./common.js'), expect = require('expect.js'), - externalldap = require('../externalldap.js'), + externalLdap = require('../externalldap.js'), groups = require('../groups.js'), - domains = require('../domains.js'), ldap = require('ldapjs'), - mailer = require('../mailer.js'), + safe = require('safetydance'), server = require('../server.js'), settings = require('../settings.js'), superagent = require('superagent'), users = require('../users.js'), - util = require('util'), _ = require('underscore'); -var USERNAME = 'noBody'; -var EMAIL = 'else@no.body'; -var PASSWORD = 'sTrOnG#$34134'; -var DISPLAY_NAME = 'Nobody cares'; -var AUDIT_SOURCE = { ip: '1.2.3.4', userId: 'someuserid' }; - let gLdapServer; -const SERVER_URL = `http://localhost:${constants.PORT}`; - -const DOMAIN_0 = { - domain: 'example.com', - zoneName: 'example.com', - provider: 'manual', - config: {}, - fallbackCertificate: null, - tlsConfig: { provider: 'fallback' }, - wellKnown: null -}; - const LDAP_SHARED_PASSWORD = 'validpassword'; const LDAP_PORT = 4321; const LDAP_BASE_DN = 'ou=Users,dc=cloudron,dc=io'; @@ -59,23 +38,6 @@ const LDAP_CONFIG = { autoCreate: false }; -function cleanupUsers(done) { - mailer._mailQueue = []; - - async.series([ - database._clear, - ], done); -} - -function createOwner(done) { - users.createOwner(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) { - expect(error).to.not.be.ok(); - expect(result).to.be.ok(); - - done(); - }); -} - // helper function to deal with pagination taken from ldap.js function finalSend(results, req, res, next) { var min = 0; @@ -212,365 +174,243 @@ function stopLdapServer(callback) { callback(); } -function setup(done) { - mailer._mailQueue = []; - - async.series([ - startLdapServer, - server.start, - database._clear, - domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0, AUDIT_SOURCE), - cleanupUsers, - createOwner, - settings.setDashboardLocation.bind(null, DOMAIN_0.domain, 'my.' + DOMAIN_0.domain) - ], done); -} - -function cleanup(done) { - mailer._mailQueue = []; - - async.series([ - database._clear, - server.stop, - stopLdapServer - ], done); -} - -function enable(config, callback) { - if (typeof config === 'function') { - callback = config; - config = LDAP_CONFIG; - } - - settings.setExternalLdapConfig(config, callback); -} - -function disable(callback) { - const config = { - provider: 'noop' - }; - - settings.setExternalLdapConfig(config, callback); -} - describe('External LDAP', function () { - before(setup); - after(cleanup); + const { setup, cleanup, admin, serverUrl, auditSource } = common; + + before(function (done) { + async.series([ + startLdapServer, + setup + ], done); + }); + + after(function (done) { + async.series([ + stopLdapServer, + cleanup + ], done); + }); describe('settings', function () { - it('enabling fails with missing url', function (done) { + it('enabling fails with missing url', async function () { let conf = _.extend({}, LDAP_CONFIG); delete conf.url; - enable(conf, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.equal(BoxError.BAD_FIELD); - done(); - }); + const [error] = await safe(settings.setExternalLdapConfig(conf)); + expect(error.reason).to.equal(BoxError.BAD_FIELD); }); - it('enabling fails with empty url', function (done) { + it('enabling fails with empty url', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.url = ''; - enable(conf, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.equal(BoxError.BAD_FIELD); - done(); - }); + const [error] = await safe(settings.setExternalLdapConfig(conf)); + expect(error.reason).to.equal(BoxError.BAD_FIELD); }); - it('enabling fails with missing baseDn', function (done) { + it('enabling fails with missing baseDn', async function () { let conf = _.extend({}, LDAP_CONFIG); delete conf.baseDn; - enable(conf, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.equal(BoxError.BAD_FIELD); - done(); - }); + const [error] = await safe(settings.setExternalLdapConfig(conf)); + expect(error.reason).to.equal(BoxError.BAD_FIELD); }); - it('enabling fails with empty baseDn', function (done) { + it('enabling fails with empty baseDn', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.baseDn = ''; - enable(conf, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.equal(BoxError.BAD_FIELD); - done(); - }); + const [error] = await safe(settings.setExternalLdapConfig(conf)); + expect(error.reason).to.equal(BoxError.BAD_FIELD); }); - it('enabling fails with missing filter', function (done) { + it('enabling fails with missing filter', async function () { let conf = _.extend({}, LDAP_CONFIG); delete conf.filter; - enable(conf, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.equal(BoxError.BAD_FIELD); - done(); - }); + const [error] = await safe(settings.setExternalLdapConfig(conf)); + expect(error.reason).to.equal(BoxError.BAD_FIELD); }); - it('enabling fails with empty filter', function (done) { + it('enabling fails with empty filter', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.filter = ''; - enable(conf, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.equal(BoxError.BAD_FIELD); - done(); - }); + const [error] = await safe(settings.setExternalLdapConfig(conf)); + expect(error.reason).to.equal(BoxError.BAD_FIELD); }); - it('enabling succeeds', function (done) { - enable(function (error) { - expect(error).to.equal(null); - done(); - }); + it('enabling succeeds', async function () { + await settings.setExternalLdapConfig(LDAP_CONFIG); }); - it('disabling succeeds', function (done) { - disable(function (error) { - expect(error).to.equal(null); - done(); - }); + it('disabling succeeds', async function () { + await settings.setExternalLdapConfig({ provider: 'noop' }); }); // now test with groups - it('enabling with groups fails with missing groupBaseDn', function (done) { + it('enabling with groups fails with missing groupBaseDn', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.syncGroups = true; delete conf.groupBaseDn; - enable(conf, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.equal(BoxError.BAD_FIELD); - done(); - }); + const [error] = await safe(settings.setExternalLdapConfig(conf)); + expect(error.reason).to.equal(BoxError.BAD_FIELD); }); - it('enabling with groups fails with empty groupBaseDn', function (done) { + it('enabling with groups fails with empty groupBaseDn', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.syncGroups = true; conf.groupBaseDn = ''; - enable(conf, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.equal(BoxError.BAD_FIELD); - done(); - }); + const [error] = await safe(settings.setExternalLdapConfig(conf)); + expect(error.reason).to.equal(BoxError.BAD_FIELD); }); - it('enabling with groups fails with missing groupFilter', function (done) { + it('enabling with groups fails with missing groupFilter', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.syncGroups = true; delete conf.groupFilter; - enable(conf, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.equal(BoxError.BAD_FIELD); - done(); - }); + const [error] = await safe(settings.setExternalLdapConfig(conf)); + expect(error.reason).to.equal(BoxError.BAD_FIELD); }); - it('enabling with groups fails with empty groupFilter', function (done) { + it('enabling with groups fails with empty groupFilter', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.syncGroups = true; conf.groupFilter = ''; - enable(conf, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.equal(BoxError.BAD_FIELD); - done(); - }); + const [error] = await safe(settings.setExternalLdapConfig(conf)); + expect(error.reason).to.equal(BoxError.BAD_FIELD); }); - it('enabling with groups fails with missing groupnameField', function (done) { + it('enabling with groups fails with missing groupnameField', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.syncGroups = true; delete conf.groupnameField; - enable(conf, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.equal(BoxError.BAD_FIELD); - done(); - }); + const [error] = await safe(settings.setExternalLdapConfig(conf)); + expect(error.reason).to.equal(BoxError.BAD_FIELD); }); - it('enabling with groups fails with empty groupnameField', function (done) { + it('enabling with groups fails with empty groupnameField', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.syncGroups = true; conf.groupnameField = ''; - enable(conf, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.equal(BoxError.BAD_FIELD); - done(); - }); + const [error] = await safe(settings.setExternalLdapConfig(conf)); + expect(error.reason).to.equal(BoxError.BAD_FIELD); }); - it('enabling with groups succeeds', function (done) { + it('enabling with groups succeeds', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.syncGroups = true; - enable(function (error) { - expect(error).to.equal(null); - done(); - }); - }); - - it('disabling succeeds', function (done) { - disable(function (error) { - expect(error).to.equal(null); - done(); - }); + await settings.setExternalLdapConfig(conf); }); }); describe('sync', function () { - it('fails if disabled', function (done) { - externalldap.sync(function progress() {}, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.equal(BoxError.BAD_STATE); - - done(); - }); + it('disable sync', async function () { + await settings.setExternalLdapConfig({ provider: 'noop' }); }); - it('enable', enable); + it('fails if disabled', async function () { + const [error] = await safe(externalLdap.sync(function progress() {})); + expect(error.reason).to.equal(BoxError.BAD_STATE); + }); - it('succeeds for new users', function (done) { + it('enable', async function () { + await settings.setExternalLdapConfig(LDAP_CONFIG); + }); + + it('succeeds for new users', async function () { gLdapUsers.push({ username: 'firstuser', displayName: 'First User', email: 'first@user.com' }); - externalldap.sync(function progress() {}, function (error) { - expect(error).to.equal(null); - - users.list(function (error, result) { - expect(error).to.equal(null); - expect(result.length).to.equal(2); - expect(result.find(function (u) { - return u.username === 'firstuser' && u.email === 'first@user.com' && u.displayName === 'First User'; - })).to.be.ok(); - - done(); - }); - }); + await externalLdap.sync(function progress() {}); + const result = await users.list(); + expect(result.find(function (u) { + return u.username === 'firstuser' && u.email === 'first@user.com' && u.displayName === 'First User'; + })).to.be.ok(); }); - it('succeeds for updated users', function (done) { + it('succeeds for updated users', async function () { gLdapUsers[0].displayName = 'User First'; gLdapUsers[0].email = 'first@changed.com'; - externalldap.sync(function progress() {}, function (error) { - expect(error).to.equal(null); - - users.list(function (error, result) { - expect(error).to.equal(null); - expect(result.length).to.equal(2); - expect(result.find(function (u) { - return u.username === 'firstuser' && u.email === 'first@changed.com' && u.displayName === 'User First'; - })).to.be.ok(); - - done(); - }); - }); + await externalLdap.sync(function progress() {}); + const result = await users.list(); + expect(result.find(function (u) { + return u.username === 'firstuser' && u.email === 'first@changed.com' && u.displayName === 'User First'; + })).to.be.ok(); }); - it('ignores already existing users with same username', function (done) { + it('ignores already existing users with same username', async function () { gLdapUsers.push({ - username: USERNAME, + username: admin.username, displayName: 'Something Else', email: 'foobar@bar.com' }); - externalldap.sync(function progress() {}, function (error) { - expect(error).to.equal(null); - - users.list(function (error, result) { - expect(error).to.equal(null); - expect(result.length).to.equal(2); - expect(result.find(function (u) { - return u.email === 'foobar@bar.com' || u.displayName === 'Something Else'; - })).to.not.be.ok(); - - done(); - }); - }); + await externalLdap.sync(function progress() {}); + const result = await users.list(); + expect(result.find(function (u) { + return u.email === 'foobar@bar.com' || u.displayName === 'Something Else'; + })).to.not.be.ok(); }); - it('does not sync group if group sync is disabled', function (done) { + it('does not sync group if group sync is disabled', async function () { gLdapGroups.push({ groupname: 'extGroup1' }); - externalldap.sync(function progress() {}, async function (error) { - expect(error).to.equal(null); - - const result = await groups.list(); - expect(result.length).to.equal(0); - - done(); - }); + await externalLdap.sync(function progress() {}); + const result = await groups.list(); + expect(result.length).to.equal(0); }); - it('enable with groupSync', function (done) { - disable(function (error) { - expect(error).to.equal(null); - - let conf = _.extend({}, LDAP_CONFIG); - conf.syncGroups = true; - - enable(conf, done); - }); + it('enable with groupSync', async function () { + let conf = _.extend({}, LDAP_CONFIG); + conf.syncGroups = true; + await settings.setExternalLdapConfig(conf); }); - it('succeeds with groups enabled', function (done) { + it('succeeds with groups enabled', async function () { gLdapGroups = []; - externalldap.sync(function progress() {}, async function (error) { - expect(error).to.equal(null); - - const result = await groups.list(); - expect(result.length).to.equal(0); - - done(); - }); + await externalLdap.sync(function progress() {}); + const result = await groups.list(); + expect(result.length).to.equal(0); }); - it('succeeds with groups enabled and new group', function (done) { + it('succeeds with groups enabled and new group', async function () { gLdapGroups.push({ groupname: 'extGroup1' }); - externalldap.sync(function progress() {}, async function (error) { - expect(error).to.equal(null); - - const result = await groups.list(); - expect(result.length).to.equal(1); - - done(); - }); + await externalLdap.sync(function progress() {}); + const result = await groups.list(); + expect(result.find(function (g) { + return g.name === 'extgroup1'; + })).to.be.ok(); }); - it('succeeds with groups enabled and second new group', function (done) { + it('succeeds with groups enabled and second new group', async function () { gLdapGroups.push({ groupname: 'extGroup2' }); - externalldap.sync(function progress() {}, async function (error) { - expect(error).to.equal(null); - - const result = await groups.list(); - expect(result.length).to.equal(2); - - done(); - }); + await externalLdap.sync(function progress() {}); + const result = await groups.list(); + expect(result.length).to.be(2); + expect(result.find(function (g) { + return g.name === 'extgroup2'; + })).to.be.ok(); }); it('does not create already existing group', async function () { @@ -578,184 +418,144 @@ describe('External LDAP', function () { groupname: 'INTERNALgroup' // also tests lowercasing }); - const externalldapSync = util.promisify(externalldap.sync); - await groups.add({ name: 'internalgroup' }); - await externalldapSync(function progress() {}); + await externalLdap.sync(function progress() {}); const result = await groups.list(); expect(result.length).to.equal(3); }); - it('adds users of groups', function (done) { + it('adds users of groups', async function () { gLdapGroups.push({ groupname: 'nonEmptyGroup', member: gLdapUsers.slice(-2).map(function (u) { return `cn=${u.username},${LDAP_CONFIG.baseDn}`; }) }); - externalldap.sync(function progress() {}, async function (error) { - expect(error).to.equal(null); + await externalLdap.sync(function progress() {}); + const result = await groups.getByName('nonemptygroup'); + expect(result).to.be.ok(); - const result = await groups.getByName('nonemptygroup'); - expect(result).to.be.ok(); - - const result2 = await groups.getMembers(result.id); - expect(result2.length).to.equal(2); - done(); - }); + const result2 = await groups.getMembers(result.id); + expect(result2.length).to.equal(2); }); - it('adds new users of groups', function (done) { + it('adds new users of groups', async function () { gLdapGroups.push({ groupname: 'nonEmptyGroup', member: gLdapUsers.map(function (u) { return `cn=${u.username},${LDAP_CONFIG.baseDn}`; }) // has 2 entries }); - externalldap.sync(function progress() {}, async function (error) { - expect(error).to.equal(null); + await externalLdap.sync(function progress() {}); + const result = await groups.getByName('nonemptygroup'); + expect(result).to.be.ok(); - const result = await groups.getByName('nonemptygroup'); - expect(result).to.be.ok(); - - const result2 = await groups.getMembers(result.id); - expect(result2.length).to.equal(2); - - done(); - }); + const result2 = await groups.getMembers(result.id); + expect(result2.length).to.equal(2); }); - it('succeeds with only one group member (string instead of array)', function (done) { + it('succeeds with only one group member (string instead of array)', async function () { gLdapGroups.push({ groupname: 'onemembergroup', member: `cn=${gLdapUsers[0].username},${LDAP_CONFIG.baseDn}` }); - externalldap.sync(function progress() {}, async function (error) { - expect(error).to.equal(null); + await externalLdap.sync(function progress() {}); - const result = await groups.getByName('onemembergroup'); - const result2 = await groups.getMembers(result.id); - expect(result2.length).to.equal(1); + const result = await groups.getByName('onemembergroup'); + const result2 = await groups.getMembers(result.id); + expect(result2.length).to.equal(1); - users.get(result2[0], function (error, result) { - expect(error).to.equal(null); - expect(result.username).to.equal(gLdapUsers[0].username); - - done(); - }); - }); + const u = await users.get(result2[0]); + expect(u.username).to.equal(gLdapUsers[0].username); }); - - it('disable', disable); }); describe('user auto creation', function () { - it('fails if external ldap is disabled', function (done) { - settings.setExternalLdapConfig({ provider: 'noop' }, function (error) { - expect(error).to.equal(null); - done(); - }); + before(server.start); + after(server.stop); + + it('enable', async function () { + await settings.setExternalLdapConfig(LDAP_CONFIG); }); - it('enable', enable); - - it('fails if auto create is disabled', function (done) { + it('fails if auto create is disabled', async function () { gLdapUsers.push({ username: 'autologinuser0', displayName: 'Auto Login0', email: 'auto0@login.com' }); - superagent.post(SERVER_URL + '/api/v1/cloudron/login') + const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) .send({ username: 'autologinuser0', password: LDAP_SHARED_PASSWORD }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); + .ok(() => true); - users.list(function (error, result) { - expect(error).to.equal(null); - expect(result.length).to.equal(2); - expect(result.find(function (u) { - return u.username === 'autologinuser0'; - })).to.not.be.ok(); + expect(response.status).to.equal(401); - done(); - }); - }); + const result = await users.list(); + expect(result.find(function (u) { + return u.username === 'autologinuser0'; + })).to.not.be.ok(); }); - it('enable auto create', function (done) { + it('enable auto create', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.autoCreate = true; - - enable(conf, done); + await settings.setExternalLdapConfig(conf); }); - it('fails for unknown user', function (done) { - superagent.post(SERVER_URL + '/api/v1/cloudron/login') + it('fails for unknown user', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) .send({ username: 'doesnotexist', password: LDAP_SHARED_PASSWORD }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); + .ok(() => true); - users.list(function (error, result) { - expect(error).to.equal(null); - expect(result.length).to.equal(2); - expect(result.find(function (u) { - return u.username === 'doesnotexist'; - })).to.not.be.ok(); + expect(response.status).to.equal(401); - done(); - }); - }); + const result = await users.list(); + expect(result.find(function (u) { + return u.username === 'doesnotexist'; + })).to.not.be.ok(); }); - it('succeeds for known user with wrong password', function (done) { + it('succeeds for known user with wrong password', async function () { gLdapUsers.push({ username: 'autologinuser1', displayName: 'Auto Login1', email: 'auto1@login.com' }); - superagent.post(SERVER_URL + '/api/v1/cloudron/login') + const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) .send({ username: 'autologinuser1', password: 'wrongpassword' }) - .end(function (error, result) { - expect(result.statusCode).to.equal(401); + .ok(() => true); - users.list(function (error, result) { - expect(error).to.equal(null); - expect(result.length).to.equal(3); - expect(result.find(function (u) { - return u.username === 'autologinuser1'; - })).to.be.ok(); - - done(); - }); - }); + expect(response.status).to.equal(401); + const result = await users.list(); + expect(result.find(function (u) { + return u.username === 'autologinuser1'; + })).to.be.ok(); }); - it('succeeds for known user with correct password', function (done) { - gLdapUsers.push({ + it('succeeds for known user with correct password', async function () { + const newUser = { username: 'autologinuser2', displayName: 'Auto Login2', - email: 'auto2@login.com' - }); + email: 'auto2@login.com', + password: LDAP_SHARED_PASSWORD + }; - superagent.post(SERVER_URL + '/api/v1/cloudron/login') + gLdapUsers.push(newUser); + + await users.add(newUser.email, newUser, auditSource); + + const response = await superagent.post(`${serverUrl}/api/v1/cloudron/login`) .send({ username: 'autologinuser2', password: LDAP_SHARED_PASSWORD }) - .end(function (error, result) { - expect(result.statusCode).to.equal(200); + .ok(() => true); - users.list(function (error, result) { - expect(error).to.equal(null); - expect(result.length).to.equal(4); - expect(result.find(function (u) { - return u.username === 'autologinuser2'; - })).to.be.ok(); + expect(response.status).to.equal(200); - done(); - }); - }); + const result = await users.list(); + expect(result.find(function (u) { + return u.username === 'autologinuser2'; + })).to.be.ok(); }); - - it('disable', disable); }); });