556 lines
19 KiB
JavaScript
556 lines
19 KiB
JavaScript
/* 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_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(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 } = 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));
|
|
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));
|
|
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));
|
|
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));
|
|
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));
|
|
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));
|
|
expect(error.reason).to.equal(BoxError.BAD_FIELD);
|
|
});
|
|
|
|
it('enabling succeeds', async function () {
|
|
await externalLdap.setConfig(LDAP_CONFIG);
|
|
});
|
|
|
|
it('disabling succeeds', async function () {
|
|
await externalLdap.setConfig({ provider: 'noop' });
|
|
});
|
|
|
|
// 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));
|
|
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));
|
|
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));
|
|
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));
|
|
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));
|
|
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));
|
|
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);
|
|
});
|
|
});
|
|
|
|
describe('sync', function () {
|
|
it('disable sync', async function () {
|
|
await externalLdap.setConfig({ 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 externalLdap.setConfig(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('mapps 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('enable with groupSync', async function () {
|
|
let conf = Object.assign({}, LDAP_CONFIG);
|
|
conf.syncGroups = true;
|
|
await externalLdap.setConfig(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 externalLdap.setConfig(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/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);
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|
|
});
|