diff --git a/src/ldap.js b/src/ldap.js index 1f596a77f..22b876024 100644 --- a/src/ldap.js +++ b/src/ldap.js @@ -61,12 +61,74 @@ function getUsersWithAccessToApp(req, callback) { }); } +// helper function to deal with pagination +function finalSend(results, req, res, next) { + var min = 0; + var max = results.length; + var cookie = null; + var pageSize = 0; + + // check if this is a paging request, if so get the cookie for session info + req.controls.forEach(function (control) { + if (control.type === ldap.PagedResultsControl.OID) { + pageSize = control.value.size; + cookie = control.value.cookie; + } + }); + + function sendPagedResults(start, end) { + start = (start < min) ? min : start; + end = (end > max || end < min) ? max : end; + var i; + + for (i = start; i < end; i++) { + res.send(results[i]); + } + + return i; + } + + if (cookie && Buffer.isBuffer(cookie)) { + // we have pagination + var first = min; + if (cookie.length !== 0) { + first = parseInt(cookie.toString(), 10); + } + var last = sendPagedResults(first, first + pageSize); + + var resultCookie; + if (last < max) { + resultCookie = new Buffer(last.toString()); + } else { + resultCookie = new Buffer(''); + } + + res.controls.push(new ldap.PagedResultsControl({ + value: { + size: pageSize, // correctness not required here + cookie: resultCookie + } + })); + } else { + // no pagination simply send all + results.forEach(function (result) { + res.send(result); + }); + } + + // all done + res.end(); + next(); +} + function userSearch(req, res, next) { debug('user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); getUsersWithAccessToApp(req, function (error, result) { if (error) return next(error); + var results = []; + // send user objects result.forEach(function (entry) { // skip entries with empty username. Some apps like owncloud can't deal with this @@ -109,11 +171,11 @@ function userSearch(req, res, next) { if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { - res.send(obj); + results.push(obj); } }); - res.end(); + finalSend(results, req, res, next); }); } @@ -123,6 +185,8 @@ function groupSearch(req, res, next) { getUsersWithAccessToApp(req, function (error, result) { if (error) return next(error); + var results = []; + var groups = [{ name: 'users', admin: false @@ -149,11 +213,11 @@ function groupSearch(req, res, next) { if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { - res.send(obj); + results.push(obj); } }); - res.end(); + finalSend(results, req, res, next); }); } @@ -193,6 +257,7 @@ function mailboxSearch(req, res, next) { debug('mailbox search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString())); + var name = req.dn.rdns[0].attrs.cn.value.toLowerCase(); // allow login via email var parts = name.split('@'); @@ -220,9 +285,11 @@ function mailboxSearch(req, res, next) { var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); - if (lowerCaseFilter.matches(obj.attributes)) res.send(obj); - - res.end(); + if (lowerCaseFilter.matches(obj.attributes)) { + finalSend([ obj ], req, res, next); + } else { + res.end(); + } }); } @@ -230,6 +297,7 @@ function mailAliasSearch(req, res, next) { debug('mail alias get: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString())); + mailboxdb.getAlias(req.dn.rdns[0].attrs.cn.value.toLowerCase(), function (error, alias) { if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (error) return next(new ldap.OperationsError(error.toString())); @@ -250,9 +318,11 @@ function mailAliasSearch(req, res, next) { var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); - if (lowerCaseFilter.matches(obj.attributes)) res.send(obj); - - res.end(); + if (lowerCaseFilter.matches(obj.attributes)) { + finalSend([ obj ], req, res, next); + } else { + res.end(); + } }); } @@ -260,6 +330,7 @@ function mailingListSearch(req, res, next) { debug('mailing list get: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString())); + mailboxdb.getGroup(req.dn.rdns[0].attrs.cn.value.toLowerCase(), function (error, group) { if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (error) return next(new ldap.OperationsError(error.toString())); @@ -280,9 +351,11 @@ function mailingListSearch(req, res, next) { var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); - if (lowerCaseFilter.matches(obj.attributes)) res.send(obj); - - res.end(); + if (lowerCaseFilter.matches(obj.attributes)) { + finalSend([ obj ], req, res, next); + } else { + res.end(); + } }); } diff --git a/src/test/ldap-test.js b/src/test/ldap-test.js index fc2923535..f6f2fd489 100644 --- a/src/test/ldap-test.js +++ b/src/test/ldap-test.js @@ -342,6 +342,35 @@ describe('Ldap', function () { }); }); + it ('succeeds with pagination', function (done) { + var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') }); + + var opts = { + filter: 'objectcategory=person', + paged: true + }; + + client.search('ou=users,dc=cloudron', opts, function (error, result) { + expect(error).to.be(null); + expect(result).to.be.an(EventEmitter); + + var entries = []; + + result.on('searchEntry', function (entry) { entries.push(entry.object); }); + result.on('error', done); + result.on('end', function (result) { + expect(result.status).to.equal(0); + expect(entries.length).to.equal(2); + entries.sort(function (a, b) { return a.username > b.username; }); + expect(entries[0].username).to.equal(USER_0.username.toLowerCase()); + expect(entries[0].mail).to.equal(USER_0.email.toLowerCase()); + expect(entries[1].username).to.equal(USER_1.username.toLowerCase()); + expect(entries[1].mail).to.equal(USER_1.email.toLowerCase()); + done(); + }); + }); + }); + it ('succeeds with basic filter and email enabled', function (done) { // user settingsdb instead of settings, to not trigger further events settingsdb.set(settings.MAIL_CONFIG_KEY, JSON.stringify({ enabled: true }), function (error) { @@ -617,13 +646,50 @@ describe('Ldap', function () { }); }); }); + + it ('succeeds with pagination', function (done) { + var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') }); + + var opts = { + filter: 'objectclass=group', + paged: true + }; + + client.search('ou=groups,dc=cloudron', opts, function (error, result) { + expect(error).to.be(null); + expect(result).to.be.an(EventEmitter); + + var entries = []; + + result.on('searchEntry', function (entry) { entries.push(entry.object); }); + result.on('error', done); + result.on('end', function (result) { + expect(result.status).to.equal(0); + expect(entries.length).to.equal(2); + + // ensure order for testability + entries.sort(function (a, b) { return a.username < b.username; }); + + expect(entries[0].cn).to.equal('users'); + expect(entries[0].memberuid.length).to.equal(3); + expect(entries[0].memberuid[0]).to.equal(USER_0.id); + expect(entries[0].memberuid[1]).to.equal(USER_1.id); + expect(entries[0].memberuid[2]).to.equal(USER_2.id); + expect(entries[1].cn).to.equal('admins'); + // if only one entry, the array becomes a string :-/ + expect(entries[1].memberuid).to.equal(USER_0.id); + done(); + }); + }); + }); }); function ldapSearch(dn, filter, callback) { var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') }); var opts = { - filter: filter + filter: filter, + paged: true }; client.search(dn, opts, function (error, result) {