diff --git a/src/externalldap.js b/src/externalldap.js index 5363141bd..bf5d755a8 100644 --- a/src/externalldap.js +++ b/src/externalldap.js @@ -83,7 +83,7 @@ function getClient(externalLdapConfig, callback) { // TODO support search by email -function ldapSearch(externalLdapConfig, options, callback) { +function ldapUserSearch(externalLdapConfig, options, callback) { assert.strictEqual(typeof externalLdapConfig, 'object'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); @@ -124,6 +124,50 @@ function ldapSearch(externalLdapConfig, options, callback) { }); } +function ldapGroupSearch(externalLdapConfig, options, callback) { + assert.strictEqual(typeof externalLdapConfig, 'object'); + assert.strictEqual(typeof options, 'object'); + assert.strictEqual(typeof callback, 'function'); + + if (!externalLdapConfig.groupBaseDn) return callback(null, []); + + getClient(externalLdapConfig, function (error, client) { + if (error) return callback(error); + + let searchOptions = { + paged: true, + scope: 'sub' // We may have to make this configurable + }; + + if (externalLdapConfig.groupFilter) searchOptions.filter = ldap.parseFilter(externalLdapConfig.groupFilter); + + // 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()}`); + + 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); + }); + }); + }); +} + function testConfig(config, callback) { assert.strictEqual(typeof config, 'object'); assert.strictEqual(typeof callback, 'function'); @@ -167,7 +211,7 @@ function search(identifier, callback) { if (error) return callback(error); if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled')); - ldapSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) { + ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) { if (error) return callback(error); // translate ldap properties to ours @@ -188,7 +232,7 @@ function createAndVerifyUserIfNotExist(identifier, password, callback) { if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled')); if (!externalLdapConfig.autoCreate) return callback(new BoxError(BoxError.BAD_STATE, 'auto create not enabled')); - ldapSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) { + ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, 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)); @@ -220,7 +264,7 @@ function verifyPassword(user, password, callback) { 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) { + ldapUserSearch(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)); @@ -265,58 +309,65 @@ function sync(progressCallback, callback) { 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); + ldapGroupSearch(externalLdapConfig, {}, function (error, ldapGroups) { + if (error) callback(error); - debug(`Found ${ldapUsers.length} users`); - let percent = 10; - let step = 90/(ldapUsers.length+1); // ensure no divide by 0 + debug(`Found ${ldapGroups.length} groups`); - // we ignore all errors here and just log them for now - async.eachSeries(ldapUsers, function (user, iteratorCallback) { - user = translateUser(externalLdapConfig, user); + ldapUserSearch(externalLdapConfig, {}, function (error, ldapUsers) { + if (error) return callback(error); - if (!validUserRequirements(user)) return iteratorCallback(); + debug(`Found ${ldapUsers.length} users`); + let percent = 10; + let step = 90/(ldapUsers.length+1); // ensure no divide by 0 - percent += step; - progressCallback({ percent, message: `Syncing... ${user.username}` }); + // we ignore all errors here and just log them for now + async.eachSeries(ldapUsers, function (user, iteratorCallback) { + user = translateUser(externalLdapConfig, user); - users.getByUsername(user.username, function (error, result) { - if (error && error.reason !== BoxError.NOT_FOUND) { - debug(`Could not find user with username ${user.username}: ${error.message}`); - return iteratorCallback(); - } + if (!validUserRequirements(user)) return iteratorCallback(); - if (error) { - debug(`[adding user] username=${user.username} email=${user.email} displayName=${user.displayName}`); + percent += step; + progressCallback({ percent, message: `Syncing... ${user.username}` }); - users.create(user.username, null /* password */, user.email, user.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=${user.username} email=${user.email} displayName=${user.displayName}`); + users.getByUsername(user.username, function (error, result) { + if (error && error.reason !== BoxError.NOT_FOUND) { + debug(`Could not find user with username ${user.username}: ${error.message}`); + return iteratorCallback(); + } - iteratorCallback(); - } else if (result.email !== user.email || result.displayName !== user.displayName) { - debug(`[updating user] username=${user.username} email=${user.email} displayName=${user.displayName}`); + if (error) { + debug(`[adding user] username=${user.username} email=${user.email} displayName=${user.displayName}`); - users.update(result, { email: user.email, fallbackEmail: user.email, displayName: user.displayName }, auditSource.EXTERNAL_LDAP_TASK, function (error) { - if (error) debug('Failed to update user', user, error); + users.create(user.username, null /* password */, user.email, user.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=${user.username} email=${user.email} displayName=${user.displayName}`); iteratorCallback(); - }); - } else { - // user known and up-to-date - debug(`[up-to-date user] username=${user.username} email=${user.email} displayName=${user.displayName}`); + } else if (result.email !== user.email || result.displayName !== user.displayName) { + debug(`[updating user] username=${user.username} email=${user.email} displayName=${user.displayName}`); - iteratorCallback(); - } + users.update(result, { email: user.email, fallbackEmail: user.email, displayName: user.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=${user.username} email=${user.email} displayName=${user.displayName}`); + + iteratorCallback(); + } + }); + }, function (error) { + debug('sync: ldap sync is done', error); + callback(error); }); - }, function (error) { - debug('sync: ldap sync is done', error); - callback(error); }); }); + }); } diff --git a/src/routes/settings.js b/src/routes/settings.js index 85bce2826..1dc1c80c2 100644 --- a/src/routes/settings.js +++ b/src/routes/settings.js @@ -160,6 +160,7 @@ function setExternalLdapConfig(req, res, next) { if ('baseDn' in req.body && typeof req.body.baseDn !== 'string') return next(new HttpError(400, 'baseDn must be a string')); if ('usernameField' in req.body && typeof req.body.usernameField !== 'string') return next(new HttpError(400, 'usernameField must be a string')); if ('filter' in req.body && typeof req.body.filter !== 'string') return next(new HttpError(400, 'filter must be a string')); + if ('groupBaseDn' in req.body && typeof req.body.groupBaseDn !== 'string') return next(new HttpError(400, 'groupBaseDn must be a string')); if ('bindDn' in req.body && typeof req.body.bindDn !== 'string') return next(new HttpError(400, 'bindDn must be a non empty string')); if ('bindPassword' in req.body && typeof req.body.bindPassword !== 'string') return next(new HttpError(400, 'bindPassword must be a string'));