Files
cloudron-box/src/test/externalldap-test.js
2020-07-30 15:22:03 +02:00

792 lines
25 KiB
JavaScript

/* 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.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();
});
});
// 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}`; })
});
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(4);
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);
});
});