/* global it:false */ /* global describe:false */ /* global before:false */ /* global after:false */ 'use strict'; const async = require('async'), BoxError = require('../boxerror.js'), common = require('./common.js'), expect = require('expect.js'), externalLdap = require('../externalldap.js'), groups = require('../groups.js'), ldap = require('ldapjs'), safe = require('safetydance'), server = require('../server.js'), superagent = require('superagent'), users = require('../users.js'); let gLdapServer; const LDAP_SHARED_PASSWORD = 'validpassword'; const LDAP_SHARED_TOTP_TOKEN = '1234'; 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 }; // helper function to deal with pagination taken from ldap.js function finalSend(results, req, res, next) { let min = 0; const max = results.length; let cookie = null; let 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; let i; for (i = start; i < end; i++) { res.send(results[i]); } return i; } if (cookie && Buffer.isBuffer(cookie)) { // we have pagination let first = min; if (cookie.length !== 0) { first = parseInt(cookie.toString(), 10); } const last = sendPagedResults(first, first + pageSize); let 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) { const dn = ldap.parseDN(`cn=${entry.username},${LDAP_BASE_DN}`); const 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) { const dn = ldap.parseDN(`cn=${entry.groupname},${LDAP_GROUP_BASE_DN}`); const 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 const attributeName = Object.keys(req.dn.rdns[0].attrs)[0]; const commonName = req.dn.rdns[0].attrs[attributeName].value; if (!commonName) return next(new ldap.NoSuchObjectError('Missing CN')); // this code here is only for completeness. none of the apps send totptoken const TOTPTOKEN_ATTRIBUTE_NAME = 'totptoken'; const totpToken = TOTPTOKEN_ATTRIBUTE_NAME in req.dn.rdns[0].attrs ? req.dn.rdns[0].attrs[TOTPTOKEN_ATTRIBUTE_NAME].value : null; const skipTotpCheck = !(TOTPTOKEN_ATTRIBUTE_NAME in req.dn.rdns[0].attrs); const u = gLdapUsers.find(function (u) { return u.username === commonName; }); if (!u) return next(new ldap.NoSuchObjectError('No such user')); if (req.credentials !== LDAP_SHARED_PASSWORD) return next(new ldap.InvalidCredentialsError('Bad password')); if (!skipTotpCheck && u.has2fa) { if (!totpToken) return next(new ldap.InvalidCredentialsError('totpToken is required')); if (totpToken !== LDAP_SHARED_TOTP_TOKEN) return next(new ldap.InvalidCredentialsError('Invalid totpToken')); } res.end(); }); gLdapServer.listen(LDAP_PORT, callback); } function stopLdapServer(callback) { if (gLdapServer) gLdapServer.close(); callback(); } describe('External LDAP', function () { 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', async function () { let conf = Object.assign({}, LDAP_CONFIG); delete conf.url; const [error] = await safe(externalLdap.setConfig(conf, auditSource)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling fails with empty url', async function () { let conf = Object.assign({}, LDAP_CONFIG); conf.url = ''; const [error] = await safe(externalLdap.setConfig(conf, auditSource)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling fails with missing baseDn', async function () { let conf = Object.assign({}, LDAP_CONFIG); delete conf.baseDn; const [error] = await safe(externalLdap.setConfig(conf, auditSource)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling fails with empty baseDn', async function () { let conf = Object.assign({}, LDAP_CONFIG); conf.baseDn = ''; const [error] = await safe(externalLdap.setConfig(conf, auditSource)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling fails with missing filter', async function () { let conf = Object.assign({}, LDAP_CONFIG); delete conf.filter; const [error] = await safe(externalLdap.setConfig(conf, auditSource)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling fails with empty filter', async function () { let conf = Object.assign({}, LDAP_CONFIG); conf.filter = ''; const [error] = await safe(externalLdap.setConfig(conf, auditSource)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling succeeds', async function () { await externalLdap.setConfig(LDAP_CONFIG, auditSource); }); it('disabling succeeds', async function () { await externalLdap.setConfig({ provider: 'noop' }, auditSource); }); // now test with groups it('enabling with groups fails with missing groupBaseDn', async function () { let conf = Object.assign({}, LDAP_CONFIG); conf.syncGroups = true; delete conf.groupBaseDn; const [error] = await safe(externalLdap.setConfig(conf, auditSource)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling with groups fails with empty groupBaseDn', async function () { let conf = Object.assign({}, LDAP_CONFIG); conf.syncGroups = true; conf.groupBaseDn = ''; const [error] = await safe(externalLdap.setConfig(conf, auditSource)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling with groups fails with missing groupFilter', async function () { let conf = Object.assign({}, LDAP_CONFIG); conf.syncGroups = true; delete conf.groupFilter; const [error] = await safe(externalLdap.setConfig(conf, auditSource)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling with groups fails with empty groupFilter', async function () { let conf = Object.assign({}, LDAP_CONFIG); conf.syncGroups = true; conf.groupFilter = ''; const [error] = await safe(externalLdap.setConfig(conf, auditSource)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling with groups fails with missing groupnameField', async function () { let conf = Object.assign({}, LDAP_CONFIG); conf.syncGroups = true; delete conf.groupnameField; const [error] = await safe(externalLdap.setConfig(conf, auditSource)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling with groups fails with empty groupnameField', async function () { let conf = Object.assign({}, LDAP_CONFIG); conf.syncGroups = true; conf.groupnameField = ''; const [error] = await safe(externalLdap.setConfig(conf, auditSource)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling with groups succeeds', async function () { let conf = Object.assign({}, LDAP_CONFIG); conf.syncGroups = true; await externalLdap.setConfig(conf, auditSource); }); }); describe('verifyPassword', function () { before(async function () { await externalLdap.setConfig(LDAP_CONFIG, auditSource); gLdapUsers = [{ username: 'maximus', displayName: 'First User', email: 'first@user.com' }]; }); it('fails for unknown user', async function () { const [error] = await safe(externalLdap.verifyPassword('badusername', 'badpassword', {})); expect(error.reason).to.be(BoxError.NOT_FOUND); }); it('fails for bad password', async function () { const [error] = await safe(externalLdap.verifyPassword('maximus', 'badpassword', {})); expect(error.reason).to.be(BoxError.INVALID_CREDENTIALS); }); it('succeeds for valid password', async function () { const u = await externalLdap.verifyPassword('maximus', LDAP_SHARED_PASSWORD, {}); expect(u.username).to.be('maximus'); }); it('enable totp', () => gLdapUsers[0].has2fa = true); it('succeeds when totp required and skip totp', async function () { const u = await externalLdap.verifyPassword('maximus', LDAP_SHARED_PASSWORD, { skipTotpCheck: true }); expect(u.username).to.be('maximus'); }); it('fails when totp required and empty totp', async function () { const [error] = await safe(externalLdap.verifyPassword('maximus', LDAP_SHARED_PASSWORD, { skipTotpCheck: false, totpToken: '' })); expect(error.reason).to.be(BoxError.INVALID_CREDENTIALS); }); it('fails when totp required and bad totp', async function () { const [error] = await safe(externalLdap.verifyPassword('maximus', LDAP_SHARED_PASSWORD, { skipTotpCheck: false, totpToken: 'badtotp' })); expect(error.reason).to.be(BoxError.INVALID_CREDENTIALS); }); it('succeeds when totp required and good totp', async function () { const u = await externalLdap.verifyPassword('maximus', LDAP_SHARED_PASSWORD, { skipTotpCheck: false, totpToken: LDAP_SHARED_TOTP_TOKEN }); expect(u.username).to.be('maximus'); }); }); describe('sync', function () { before(function () { gLdapUsers = []; gLdapGroups = []; }); it('disable sync', async function () { await externalLdap.setConfig({ provider: 'noop' }, auditSource); }); it('fails if disabled', async function () { const [error] = await safe(externalLdap.sync(function progress() {})); expect(error.reason).to.equal(BoxError.BAD_STATE); }); it('enable', async function () { await externalLdap.setConfig(LDAP_CONFIG, auditSource); }); it('succeeds for new users', async function () { gLdapUsers.push({ username: 'firstuser', displayName: 'First User', email: 'first@user.com' }); 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' && u.source === 'ldap'; })).to.be.ok(); }); it('succeeds for updated users', async function () { gLdapUsers[0].displayName = 'User First'; gLdapUsers[0].email = 'first@changed.com'; 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' && u.source === 'ldap'; })).to.be.ok(); }); it('maps already existing users with same username', async function () { gLdapUsers.push({ username: admin.username, displayName: 'Something Else', email: 'foobar@bar.com' }); 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.be.ok(); }); it('does not sync group if group sync is disabled', async function () { gLdapGroups.push({ groupname: 'extGroup1' }); await externalLdap.sync(function progress() {}); const result = await groups.list(); expect(result.length).to.equal(0); }); it('can set groups of external user when group sync is disabled', async function () { const user = await users.getByUsername(gLdapUsers[0].username); await groups.setMembership(user, []); }); it('enable with groupSync', async function () { let conf = Object.assign({}, LDAP_CONFIG); conf.syncGroups = true; await externalLdap.setConfig(conf, auditSource); }); it('succeeds with groups enabled', async function () { gLdapGroups = []; await externalLdap.sync(function progress() {}); const result = await groups.list(); expect(result.length).to.equal(0); }); it('succeeds with groups enabled and new group', async function () { gLdapGroups.push({ groupname: 'extGroup1' }); await externalLdap.sync(function progress() {}); const result = await groups.list(); expect(result.find(function (g) { return g.name === 'extgroup1' && g.source === 'ldap'; })).to.be.ok(); }); it('succeeds with groups enabled and second new group', async function () { gLdapGroups.push({ groupname: 'extGroup2' }); 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' && g.source === 'ldap'; })).to.be.ok(); }); it('does not create or change already existing group', async function () { gLdapGroups.push({ groupname: 'INTERNALgroup' // also tests lowercasing }); await groups.add({ name: 'internalgroup' }); await externalLdap.sync(function progress() {}); const result = await groups.list(); expect(result.length).to.equal(3); expect(result.find(function (g) { return g.name === 'internalgroup' && g.source === 'ldap'; // source is updated to LDAP })).to.be.ok(); }); 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}`; }) }); await externalLdap.sync(function progress() {}); const result = await groups.getByName('nonemptygroup'); expect(result).to.be.ok(); const result2 = await groups.getMembers(result.id); expect(result2.length).to.equal(2); }); 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 }); await externalLdap.sync(function progress() {}); const result = await groups.getByName('nonemptygroup'); expect(result).to.be.ok(); const result2 = await groups.getMembers(result.id); expect(result2.length).to.equal(2); }); 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}` }); 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 u = await users.get(result2[0]); expect(u.username).to.equal(gLdapUsers[0].username); }); it('cannot set groups of external user when group sync is disabled', async function () { const user = await users.getByUsername(gLdapUsers[0].username); const [error] = await safe(groups.setMembership(user, [])); expect(error.reason).to.be(BoxError.BAD_STATE); }); it('cannot update profile of external user', async function () { const user = await users.getByUsername(gLdapUsers[0].username); const [error] = await safe(users.updateProfile(user, { displayName: 'another name'}, auditSource)); expect(error.reason).to.be(BoxError.BAD_STATE); }); }); describe('user auto creation', function () { before(server.start); after(server.stop); it('enable', async function () { await externalLdap.setConfig(LDAP_CONFIG, auditSource); }); it('fails if auto create is disabled', async function () { gLdapUsers.push({ username: 'autologinuser0', displayName: 'Auto Login0', email: 'auto0@login.com' }); const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: 'autologinuser0', password: LDAP_SHARED_PASSWORD }) .ok(() => true); expect(response.status).to.equal(401); const result = await users.list(); expect(result.find(function (u) { return u.username === 'autologinuser0'; })).to.not.be.ok(); }); it('enable auto create', async function () { let conf = Object.assign({}, LDAP_CONFIG); conf.autoCreate = true; await externalLdap.setConfig(conf, auditSource); }); it('fails for unknown user', async function () { const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: 'doesnotexist', password: LDAP_SHARED_PASSWORD }) .ok(() => true); expect(response.status).to.equal(401); 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', async function () { gLdapUsers.push({ username: 'autologinuser1', displayName: 'Auto Login1', email: 'auto1@login.com' }); const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: 'autologinuser1', password: 'wrongpassword' }) .ok(() => true); 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', async function () { gLdapUsers.push({ username: 'autologinuser2', displayName: 'Auto Login2', email: 'auto2@login.com', password: LDAP_SHARED_PASSWORD }); const response = await superagent.post(`${serverUrl}/api/v1/auth/login`) .send({ username: 'autologinuser2', password: LDAP_SHARED_PASSWORD }) .ok(() => true); expect(response.status).to.equal(200); const result = await users.list(); expect(result.find(function (u) { return u.username === 'autologinuser2'; })).to.be.ok(); }); }); });