/* 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'), mailboxdb = require('../mailboxdb.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 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' } }; const LDAP_SHARED_PASSWORD = 'validpassword'; const LDAP_PORT = 4321; const LDAP_BASE_DN = 'ou=Users,dc=cloudron,dc=io'; const LDAP_GROUP_BASE_DN = 'ou=Groups,dc=cloudron,dc=io'; const LDAP_CONFIG = { provider: 'testserver', url: `ldap://localhost:${LDAP_PORT}`, usernameField: 'customusernameprop', baseDn: LDAP_BASE_DN, filter: '(objectClass=inetOrgPerson)', syncGroups: false, groupBaseDn: LDAP_GROUP_BASE_DN, groupFilter: '(objectClass=groupOfNames)', groupnameField: 'customgroupnameprop', 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(); 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 = []; let gLdapGroups = []; function startLdapServer(callback) { gLdapServer = ldap.createServer(); gLdapServer.search(LDAP_BASE_DN, 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.search(LDAP_GROUP_BASE_DN, function (req, res, next) { let results = []; gLdapGroups.forEach(function (entry) { var dn = ldap.parseDN(`cn=${entry.groupname},${LDAP_GROUP_BASE_DN}`); var obj = { dn: dn.toString(), attributes: { objectclass: [ 'groupOfNames' ], cn: entry.groupname, member: entry.member || [] } }; obj.attributes[LDAP_CONFIG.groupnameField] = entry.groupname; 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_BASE_DN, 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.setAdminLocation.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(); }); }); // now test with groups it('enabling with groups fails with missing groupBaseDn', function (done) { 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(); }); }); it('enabling with groups fails with empty groupBaseDn', function (done) { 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(); }); }); it('enabling with groups fails with missing groupFilter', function (done) { 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(); }); }); it('enabling with groups fails with empty groupFilter', function (done) { 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(); }); }); it('enabling with groups fails with missing groupnameField', function (done) { 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(); }); }); it('enabling with groups fails with empty groupnameField', function (done) { 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(); }); }); it('enabling with groups succeeds', function (done) { 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(); }); }); }); 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('does not sync group if group sync is disabled', function (done) { gLdapGroups.push({ groupname: 'extGroup1' }); externalldap.sync(function progress() {}, function (error) { expect(error).to.equal(null); groups.getAll(function (error, result) { expect(error).to.equal(null); expect(result.length).to.equal(0); done(); }); }); }); 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('succeeds with groups enabled', function (done) { gLdapGroups = []; externalldap.sync(function progress() {}, function (error) { expect(error).to.equal(null); groups.getAll(function (error, result) { expect(error).to.equal(null); expect(result.length).to.equal(0); done(); }); }); }); it('succeeds with groups enabled and new group', function (done) { gLdapGroups.push({ groupname: 'extGroup1' }); externalldap.sync(function progress() {}, function (error) { expect(error).to.equal(null); groups.getAll(function (error, result) { expect(error).to.equal(null); expect(result.length).to.equal(1); done(); }); }); }); it('succeeds with groups enabled and second new group', function (done) { gLdapGroups.push({ groupname: 'extGroup2' }); externalldap.sync(function progress() {}, function (error) { expect(error).to.equal(null); groups.getAll(function (error, result) { expect(error).to.equal(null); expect(result.length).to.equal(2); done(); }); }); }); it('does not create already existing group', function (done) { gLdapGroups.push({ groupname: 'INTERNALgroup' // also tests lowercasing }); groups.create('internalgroup', '', function (error) { expect(error).to.equal(null); externalldap.sync(function progress() {}, function (error) { expect(error).to.equal(null); groups.getAll(function (error, result) { expect(error).to.equal(null); expect(result.length).to.equal(3); done(); }); }); }); }); it('adds users of groups', function (done) { gLdapGroups.push({ groupname: 'nonEmptyGroup', member: gLdapUsers.slice(-2).map(function (u) { return `cn=${u.username},${LDAP_CONFIG.baseDn}`; }) }); externalldap.sync(function progress() {}, function (error) { expect(error).to.equal(null); groups.getByName('nonemptygroup', function (error, result) { expect(error).to.equal(null); groups.getMembers(result.id, function (error, result) { expect(error).to.equal(null); expect(result.length).to.equal(2); done(); }); }); }); }); it('adds new users of groups', function (done) { gLdapGroups.push({ groupname: 'nonEmptyGroup', member: gLdapUsers.map(function (u) { return `cn=${u.username},${LDAP_CONFIG.baseDn}`; }) // has 2 entries }); externalldap.sync(function progress() {}, function (error) { expect(error).to.equal(null); groups.getByName('nonemptygroup', function (error, result) { expect(error).to.equal(null); groups.getMembers(result.id, function (error, result) { expect(error).to.equal(null); expect(result.length).to.equal(2); done(); }); }); }); }); it('succeeds with only one group member (string instead of array)', function (done) { gLdapGroups.push({ groupname: 'onemembergroup', member: `cn=${gLdapUsers[0].username},${LDAP_CONFIG.baseDn}` }); externalldap.sync(function progress() {}, function (error) { expect(error).to.equal(null); groups.getByName('onemembergroup', function (error, result) { expect(error).to.equal(null); groups.getMembers(result.id, function (error, result) { expect(error).to.equal(null); expect(result.length).to.equal(1); users.get(result[0], function (error, result) { expect(error).to.equal(null); expect(result.username).to.equal(gLdapUsers[0].username); 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); }); });