/* 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'), settings = require('../settings.js'), superagent = require('superagent'), users = require('../users.js'), _ = require('underscore'); let gLdapServer; 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 }; // 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(); } 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 = _.extend({}, LDAP_CONFIG); delete conf.url; const [error] = await safe(settings.setExternalLdapConfig(conf)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling fails with empty url', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.url = ''; const [error] = await safe(settings.setExternalLdapConfig(conf)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling fails with missing baseDn', async function () { let conf = _.extend({}, LDAP_CONFIG); delete conf.baseDn; const [error] = await safe(settings.setExternalLdapConfig(conf)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling fails with empty baseDn', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.baseDn = ''; const [error] = await safe(settings.setExternalLdapConfig(conf)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling fails with missing filter', async function () { let conf = _.extend({}, LDAP_CONFIG); delete conf.filter; const [error] = await safe(settings.setExternalLdapConfig(conf)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling fails with empty filter', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.filter = ''; const [error] = await safe(settings.setExternalLdapConfig(conf)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling succeeds', async function () { await settings.setExternalLdapConfig(LDAP_CONFIG); }); it('disabling succeeds', async function () { await settings.setExternalLdapConfig({ provider: 'noop' }); }); // now test with groups it('enabling with groups fails with missing groupBaseDn', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.syncGroups = true; delete conf.groupBaseDn; const [error] = await safe(settings.setExternalLdapConfig(conf)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling with groups fails with empty groupBaseDn', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.syncGroups = true; conf.groupBaseDn = ''; const [error] = await safe(settings.setExternalLdapConfig(conf)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling with groups fails with missing groupFilter', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.syncGroups = true; delete conf.groupFilter; const [error] = await safe(settings.setExternalLdapConfig(conf)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling with groups fails with empty groupFilter', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.syncGroups = true; conf.groupFilter = ''; const [error] = await safe(settings.setExternalLdapConfig(conf)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling with groups fails with missing groupnameField', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.syncGroups = true; delete conf.groupnameField; const [error] = await safe(settings.setExternalLdapConfig(conf)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling with groups fails with empty groupnameField', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.syncGroups = true; conf.groupnameField = ''; const [error] = await safe(settings.setExternalLdapConfig(conf)); expect(error.reason).to.equal(BoxError.BAD_FIELD); }); it('enabling with groups succeeds', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.syncGroups = true; await settings.setExternalLdapConfig(conf); }); }); describe('sync', function () { it('disable sync', async function () { await settings.setExternalLdapConfig({ provider: 'noop' }); }); 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 settings.setExternalLdapConfig(LDAP_CONFIG); }); 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'; })).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'; })).to.be.ok(); }); it('ignores 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.not.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('enable with groupSync', async function () { let conf = _.extend({}, LDAP_CONFIG); conf.syncGroups = true; await settings.setExternalLdapConfig(conf); }); 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'; })).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'; })).to.be.ok(); }); it('does not create 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); }); 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); }); }); describe('user auto creation', function () { before(server.start); after(server.stop); it('enable', async function () { await settings.setExternalLdapConfig(LDAP_CONFIG); }); 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/cloudron/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 = _.extend({}, LDAP_CONFIG); conf.autoCreate = true; await settings.setExternalLdapConfig(conf); }); it('fails for unknown user', async function () { const response = await superagent.post(`${serverUrl}/api/v1/cloudron/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/cloudron/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 () { const newUser = { username: 'autologinuser2', displayName: 'Auto Login2', email: 'auto2@login.com', password: LDAP_SHARED_PASSWORD }; gLdapUsers.push(newUser); await users.add(newUser.email, newUser, auditSource); const response = await superagent.post(`${serverUrl}/api/v1/cloudron/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(); }); }); });