diff --git a/src/routes/accesscontrol.js b/src/routes/accesscontrol.js index 40bd36d87..4d4059223 100644 --- a/src/routes/accesscontrol.js +++ b/src/routes/accesscontrol.js @@ -52,6 +52,7 @@ function initialize(callback) { if (error && error.reason === BoxError.BAD_STATE) return callback(null, false); if (error && error.reason === BoxError.BAD_FIELD) return callback(null, false); if (error && error.reason === BoxError.CONFLICT) return callback(null, false); + if (error && error.reason === BoxError.NOT_FOUND) return callback(null, false); if (error && error.reason === BoxError.INVALID_CREDENTIALS) return callback(null, false); if (error) return callback(error); diff --git a/src/test/externalldap-test.js b/src/test/externalldap-test.js new file mode 100644 index 000000000..e5f2d1233 --- /dev/null +++ b/src/test/externalldap-test.js @@ -0,0 +1,511 @@ +/* global it:false */ +/* global describe:false */ +/* global before:false */ +/* global after:false */ + +'use strict'; + +var async = require('async'), + BoxError = require('../boxerror.js'), + database = require('../database.js'), + constants = require('../constants.js'), + expect = require('expect.js'), + externalldap = require('../externalldap.js'), + groupdb = require('../groupdb.js'), + groups = require('../groups.js'), + domains = require('../domains.js'), + ldap = require('ldapjs'), + mail = require('../mail.js'), + mailboxdb = require('../mailboxdb.js'), + maildb = require('../maildb.js'), + mailer = require('../mailer.js'), + server = require('../server.js'), + settings = require('../settings.js'), + superagent = require('superagent'), + userdb = require('../userdb.js'), + users = require('../users.js'), + _ = require('underscore'); + +var USERNAME = 'noBody'; +var USERNAME_NEW = 'noBodyNew'; +var EMAIL = 'else@no.body'; +var EMAIL_NEW = 'noBodyNew@no.body'; +var PASSWORD = 'sTrOnG#$34134'; +var NEW_PASSWORD = 'oTHER@#$235'; +var DISPLAY_NAME = 'Nobody cares'; +var DISPLAY_NAME_NEW = 'Somone cares'; +var userObject = null; +var NON_ADMIN_GROUP = 'members'; +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' } +}; + +const LDAP_SHARED_PASSWORD = 'validpassword'; +const LDAP_PORT = 4321; +const LDAP_BASE_DN = 'ou=Users,dc=cloudron,dc=io'; +const LDAP_CONFIG = { + provider: 'testserver', + url: `ldap://localhost:${LDAP_PORT}`, + usernameField: 'customusernameprop', + baseDn: LDAP_BASE_DN, + filter: '(objectClass=inetOrgPerson)', + autoCreate: false +}; + +function cleanupUsers(done) { + mailer._mailQueue = []; + + async.series([ + groupdb._clear, + userdb._clear, + mailboxdb._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(); + + userObject = result; + + done(); + }); +} + +// helper function to deal with pagination taken from ldap.js +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 = Buffer.from(last.toString()); + } else { + resultCookie = Buffer.from(''); + } + + 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(); +} + +let gLdapUsers = []; + +function startLdapServer(callback) { + gLdapServer = ldap.createServer(); + + gLdapServer.search(LDAP_CONFIG.baseDn, function (req, res, next) { + let results = []; + + gLdapUsers.forEach(function (entry) { + var dn = ldap.parseDN(`cn=${entry.username},${LDAP_BASE_DN}`); + + var obj = { + dn: dn.toString(), + attributes: { + objectclass: [ 'inetOrgPerson' ], + mail: entry.email, + cn: entry.displayName + } + }; + + obj.attributes[LDAP_CONFIG.usernameField] = entry.username; + + if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(obj.attributes)) { + results.push(obj); + } + }); + + finalSend(results, req, res, next); + }); + + gLdapServer.bind(LDAP_CONFIG.baseDn, function (req, res, next) { + // extract the common name which might have different attribute names + var attributeName = Object.keys(req.dn.rdns[0].attrs)[0]; + var commonName = req.dn.rdns[0].attrs[attributeName].value; + if (!commonName) return next(new ldap.NoSuchObjectError(req.dn.toString())); + + if (!gLdapUsers.find(function (u) { return u.username === commonName; })) return next(new ldap.NoSuchObjectError(req.dn.toString())); + if (req.credentials !== LDAP_SHARED_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString())); + + res.end(); + }); + + gLdapServer.listen(LDAP_PORT, callback); +} + +function stopLdapServer(callback) { + if (gLdapServer) gLdapServer.close(); + 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.setAdmin.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); + + describe('settings', function () { + it('enabling fails with missing url', function (done) { + 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(); + }); + }); + + it('enabling fails with empty url', function (done) { + 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(); + }); + }); + + it('enabling fails with missing baseDn', function (done) { + 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(); + }); + }); + + it('enabling fails with empty baseDn', function (done) { + 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(); + }); + }); + + it('enabling fails with missing filter', function (done) { + 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(); + }); + }); + + it('enabling fails with empty filter', function (done) { + 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(); + }); + }); + + it('enabling succeeds', function (done) { + enable(function (error) { + expect(error).to.equal(null); + done(); + }); + }); + + it('disabling succeeds', function (done) { + disable(function (error) { + expect(error).to.equal(null); + done(); + }); + }); + }); + + 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('enable', enable); + + it('succeeds for new users', function (done) { + gLdapUsers.push({ + username: 'firstuser', + displayName: 'First User', + email: 'first@user.com' + }); + + externalldap.sync(function progress() {}, function (error) { + expect(error).to.equal(null); + + users.getAll(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(); + }); + }); + }); + + it('succeeds for updated users', function (done) { + gLdapUsers[0].displayName = 'User First'; + gLdapUsers[0].email = 'first@changed.com'; + + externalldap.sync(function progress() {}, function (error) { + expect(error).to.equal(null); + + users.getAll(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(); + }); + }); + }); + + it('ignores already existing users with same username', function (done) { + gLdapUsers.push({ + username: USERNAME, + displayName: 'Something Else', + email: 'foobar@bar.com' + }); + + externalldap.sync(function progress() {}, function (error) { + expect(error).to.equal(null); + + users.getAll(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(); + }); + }); + }); + + 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(); + }); + }); + + it('enable', enable); + + it('fails if auto create is disabled', function (done) { + gLdapUsers.push({ + username: 'autologinuser0', + displayName: 'Auto Login0', + email: 'auto0@login.com' + }); + + superagent.post(SERVER_URL + '/api/v1/developer/login') + .send({ username: 'autologinuser0', password: LDAP_SHARED_PASSWORD }) + .end(function (error, result) { + expect(result.statusCode).to.equal(401); + + users.getAll(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(); + + done(); + }); + }); + }); + + it('enable auto create', function (done) { + let conf = _.extend({}, LDAP_CONFIG); + conf.autoCreate = true; + + enable(conf, done); + }); + + it('fails for unknown user', function (done) { + superagent.post(SERVER_URL + '/api/v1/developer/login') + .send({ username: 'doesnotexist', password: LDAP_SHARED_PASSWORD }) + .end(function (error, result) { + expect(result.statusCode).to.equal(401); + + users.getAll(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(); + + done(); + }); + }); + }); + + it('succeeds for known user with wrong password', function (done) { + gLdapUsers.push({ + username: 'autologinuser1', + displayName: 'Auto Login1', + email: 'auto1@login.com' + }); + + superagent.post(SERVER_URL + '/api/v1/developer/login') + .send({ username: 'autologinuser1', password: 'wrongpassword' }) + .end(function (error, result) { + expect(result.statusCode).to.equal(401); + + users.getAll(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(); + }); + }); + }); + + it('succeeds for known user with correct password', function (done) { + gLdapUsers.push({ + username: 'autologinuser2', + displayName: 'Auto Login2', + email: 'auto2@login.com' + }); + + superagent.post(SERVER_URL + '/api/v1/developer/login') + .send({ username: 'autologinuser2', password: LDAP_SHARED_PASSWORD }) + .end(function (error, result) { + expect(result.statusCode).to.equal(200); + + users.getAll(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(); + + done(); + }); + }); + }); + + it('disable', disable); + }); +});