Files
cloudron-box/src/test/directoryserver-test.js

355 lines
17 KiB
JavaScript
Raw Normal View History

2021-12-23 21:31:48 +01:00
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
const async = require('async'),
common = require('./common.js'),
constants = require('../constants.js'),
directoryServer = require('../directoryserver.js'),
2021-12-23 21:31:48 +01:00
expect = require('expect.js'),
groups = require('../groups.js'),
ldap = require('ldapjs'),
2024-01-06 13:25:12 +01:00
safe = require('safetydance'),
speakeasy = require('speakeasy'),
users = require('../users.js');
2021-12-23 21:31:48 +01:00
async function ldapBind(dn, password) {
return new Promise((resolve, reject) => {
2022-01-07 14:06:13 +01:00
const client = ldap.createClient({ url: 'ldaps://127.0.0.1:' + constants.USER_DIRECTORY_LDAPS_PORT, tlsOptions: { rejectUnauthorized: false }});
2021-12-23 21:31:48 +01:00
client.on('error', reject);
client.bind(dn, password, function (error) {
client.unbind();
if (error) reject(error);
resolve();
});
});
}
// ldapsearch -LLL -E pr=10/noprompt -x -h localhost -p 3002 -b cn=userName0@example.com,ou=mailboxes,dc=cloudron objectclass=mailbox
2022-01-07 09:57:02 +01:00
async function ldapSearch(dn, opts, auth) {
2021-12-23 21:31:48 +01:00
return new Promise((resolve, reject) => {
2022-01-07 14:06:13 +01:00
const client = ldap.createClient({ url: 'ldaps://127.0.0.1:' + constants.USER_DIRECTORY_LDAPS_PORT, tlsOptions: { rejectUnauthorized: false }});
2021-12-23 21:31:48 +01:00
2022-01-07 09:57:02 +01:00
function bindAuth(callback) {
if (!auth) return callback();
client.on('error', callback);
client.bind(auth.dn, auth.secret, callback);
}
2024-01-23 10:12:29 +01:00
function done(error, result) {
client.unbind();
if (error) reject(error); else resolve(result);
}
2022-01-07 09:57:02 +01:00
bindAuth(function (error) {
2024-01-23 10:12:29 +01:00
if (error) return done(error);
2021-12-23 21:31:48 +01:00
2022-01-07 09:57:02 +01:00
client.search(dn, opts, function (error, result) {
2024-01-23 10:12:29 +01:00
if (error) return done(error);
2021-12-23 21:31:48 +01:00
2022-01-07 09:57:02 +01:00
let entries = [];
2021-12-23 21:31:48 +01:00
2022-01-07 09:57:02 +01:00
result.on('searchEntry', function (entry) { entries.push(entry.object); });
2021-12-23 21:31:48 +01:00
2024-01-23 10:12:29 +01:00
result.on('error', done);
2022-01-07 09:57:02 +01:00
result.on('end', function (result) {
2024-01-23 10:12:29 +01:00
if (result.status !== 0) return done(new Error(`Unexpected status: ${result.status}`));
done(null, entries);
2022-01-07 09:57:02 +01:00
});
2021-12-23 21:31:48 +01:00
});
});
});
}
2023-10-01 14:33:19 +05:30
describe('Directory Server (LDAP)', function () {
2024-01-06 13:25:12 +01:00
const { setup, cleanup, admin, user, app, domain, auditSource } = common;
2021-12-23 21:31:48 +01:00
let group, group2;
const mockApp = Object.assign({}, app);
2022-01-07 09:57:02 +01:00
const auth = {
2022-01-07 14:06:13 +01:00
dn: constants.USER_DIRECTORY_LDAP_DN,
2022-01-07 09:57:02 +01:00
secret: 'ldapsecret'
};
2021-12-23 21:31:48 +01:00
before(function (done) {
async.series([
setup,
directoryServer.start.bind(null),
directoryServer.setConfig.bind(null, { enabled: true, secret: auth.secret, allowlist: '127.0.0.1' }, auditSource),
2021-12-23 21:31:48 +01:00
async () => {
2024-12-04 09:48:25 +01:00
group = await groups.add({ name: 'ldap-test-1' }, auditSource);
await groups.setMembers(group, [ admin.id, user.id ], {}, auditSource);
2021-12-23 21:31:48 +01:00
},
async () => {
2024-12-04 09:48:25 +01:00
group2 = await groups.add({ name: 'ldap-test-2' }, auditSource);
await groups.setMembers(group2, [ admin.id ], {}, auditSource);
2021-12-23 21:31:48 +01:00
}
], done);
});
after(function (done) {
async.series([
directoryServer.stop,
2021-12-23 21:31:48 +01:00
cleanup
], done);
});
describe('user bind', function () {
2024-01-06 13:25:12 +01:00
let twofa;
2021-12-23 21:31:48 +01:00
it('cn= fails for nonexisting user', async function () {
const [error] = await safe(ldapBind('cn=doesnotexist,ou=users,dc=cloudron', 'password'));
expect(error).to.be.a(ldap.NoSuchObjectError);
});
it('cn= fails with wrong password', async function () {
const [error] = await safe(ldapBind(`cn=${admin.id},ou=users,dc=cloudron`, 'wrongpassword'));
expect(error).to.be.a(ldap.InvalidCredentialsError);
});
it('cn= succeeds with id', async function () {
await ldapBind(`cn=${admin.id},ou=users,dc=cloudron`, admin.password);
});
it('cn= succeeds with username', async function () {
await ldapBind(`cn=${admin.username},ou=users,dc=cloudron`, admin.password);
});
it('cn= succeeds with email', async function () {
await ldapBind(`cn=${admin.email},ou=users,dc=cloudron`, admin.password);
});
it('mail= fails with bad email', async function () {
const [error] = await safe(ldapBind('mail=random,ou=users,dc=cloudron', admin.password));
expect(error).to.be.a(ldap.NoSuchObjectError);
});
it('mail= succeeds with email', async function () {
await ldapBind(`mail=${admin.email},ou=users,dc=cloudron`, admin.password);
});
2024-01-06 13:25:12 +01:00
// directory server cannot know the source of the requesting client.
// there are 3 sources - external app, cloudron app, cloudron dashboard.
// the 2fa is requested by client by passing `+totpToken=xxx` . totpToken
// is ignored if the user has no 2fa setup. If present, it is validated.
it('enable 2fa for admin', async function () {
twofa = await users.setTwoFactorAuthenticationSecret(admin, auditSource);
2024-01-06 13:25:12 +01:00
const totpToken = speakeasy.totp({ secret: twofa.secret, encoding: 'base32' });
await users.enableTwoFactorAuthentication(await users.get(admin.id), totpToken, auditSource);
2024-01-06 13:25:12 +01:00
});
it('totp check not requested - fails with bad password', async function () {
const [error] = await safe(ldapBind(`cn=${admin.id},ou=users,dc=cloudron`, admin.password + 'random'));
expect(error).to.be.a(ldap.InvalidCredentialsError);
2025-06-11 14:44:27 +02:00
expect(error.lde_message).to.be('Wrong password');
});
it('totp check not requested - succeeds with good password', async function () {
await ldapBind(`cn=${admin.id},ou=users,dc=cloudron`, admin.password);
});
it('totp check requested - fails with empty 2fa if user has 2fa', async function () {
const [error] = await safe(ldapBind(`cn=${admin.id}+totptoken=,ou=users,dc=cloudron`, admin.password));
2024-01-06 13:25:12 +01:00
expect(error).to.be.a(ldap.InvalidCredentialsError);
expect(error.lde_message).to.be('A totpToken must be provided');
});
it('totp check requested - succeeds with empty 2fa if user has no 2fa', async function () {
await ldapBind(`cn=${user.id}+totptoken=,ou=users,dc=cloudron`, user.password);
});
it('totp check requested - fails with invalid 2fa', async function () {
2024-01-06 13:25:12 +01:00
const [error] = await safe(ldapBind(`cn=${admin.id}+totptoken=schlecht,ou=users,dc=cloudron`, admin.password));
expect(error).to.be.a(ldap.InvalidCredentialsError);
expect(error.lde_message).to.be('Invalid totpToken');
});
it('totp check requested - succeeds with invalid 2fa if user has no 2fa', async function () {
await ldapBind(`cn=${user.id}+totptoken=schlecht,ou=users,dc=cloudron`, user.password);
});
it('totp check requested - fails with bad password if user has 2fa', async function () {
const totpToken = speakeasy.totp({ secret: twofa.secret, encoding: 'base32' });
const [error] = await safe(ldapBind(`cn=${admin.email}+totpToken=${totpToken},ou=users,dc=cloudron`, admin.password + 'random'));
2025-06-11 14:44:27 +02:00
expect(error.lde_message).to.be('Wrong password');
});
it('totp check requested - fails with bad password if user has no 2fa', async function () {
const totpToken = speakeasy.totp({ secret: twofa.secret, encoding: 'base32' });
const [error] = await safe(ldapBind(`cn=${admin.email}+totpToken=${totpToken},ou=users,dc=cloudron`, admin.password + 'random'));
2025-06-11 14:44:27 +02:00
expect(error.lde_message).to.be('Wrong password');
});
it('totp check requested - succeeds with valid 2fa', async function () {
2024-01-06 13:25:12 +01:00
const totpToken = speakeasy.totp({ secret: twofa.secret, encoding: 'base32' });
await ldapBind(`cn=${admin.email}+totpToken=${totpToken},ou=users,dc=cloudron`, admin.password);
});
2021-12-23 21:31:48 +01:00
});
describe('search users', function () {
2022-01-07 09:57:02 +01:00
it('fails without auth', async function () {
const [error] = await safe(ldapSearch('ou=users,dc=cloudron', { filter: 'objectcategory=person' }));
expect(error).to.be.a(ldap.InsufficientAccessRightsError);
});
it('fails with wrong auth DN', async function () {
const [error] = await safe(ldapSearch('ou=users,dc=cloudron', { filter: 'objectcategory=person' }, { dn: 'cn=doesnotexist,ou=system,dc=cloudron', secret: 'ldapsecret' }));
expect(error).to.be.a(ldap.InvalidCredentialsError);
});
it('fails with wrong auth secret', async function () {
const [error] = await safe(ldapSearch('ou=users,dc=cloudron', { filter: 'objectcategory=person' }, { dn: 'cn=admin,ou=system,dc=cloudron', secret: 'wrongldapsecret' }));
expect(error).to.be.a(ldap.InvalidCredentialsError);
});
2021-12-23 21:31:48 +01:00
it('fails for non existing tree', async function () {
2022-01-07 09:57:02 +01:00
const [error] = await safe(ldapSearch('o=example', { filter: '(&(l=Seattle)(email=*@' + domain.domain + '))' }, auth));
2021-12-23 21:31:48 +01:00
expect(error).to.be.a(ldap.NoSuchObjectError);
});
it('succeeds with basic filter', async function () {
2022-01-07 09:57:02 +01:00
const entries = await ldapSearch('ou=users,dc=cloudron', { filter: 'objectcategory=person' }, auth);
2021-12-23 21:31:48 +01:00
expect(entries.length).to.equal(2);
entries.sort(function (a, b) { return a.username > b.username; });
expect(entries[0].username).to.equal(admin.username.toLowerCase());
expect(entries[0].mail).to.equal(admin.email.toLowerCase());
expect(entries[1].username).to.equal(user.username.toLowerCase());
expect(entries[1].mail).to.equal(user.email.toLowerCase());
});
it('succeeds with pagination', async function () {
2022-01-07 09:57:02 +01:00
const entries = await ldapSearch('ou=users,dc=cloudron', { filter: 'objectcategory=person', paged: true }, auth);
2021-12-23 21:31:48 +01:00
expect(entries.length).to.equal(2);
entries.sort(function (a, b) { return a.username > b.username; });
expect(entries[0].username).to.equal(admin.username.toLowerCase());
expect(entries[0].mail).to.equal(admin.email.toLowerCase());
expect(entries[1].username).to.equal(user.username.toLowerCase());
expect(entries[1].mail).to.equal(user.email.toLowerCase());
});
it('succeeds with username wildcard filter', async function () {
2022-01-07 09:57:02 +01:00
const entries = await ldapSearch('ou=users,dc=cloudron', { filter: '&(objectcategory=person)(username=*)' }, auth);
2021-12-23 21:31:48 +01:00
expect(entries.length).to.equal(2);
entries.sort(function (a, b) { return a.username > b.username; });
expect(entries[0].username).to.equal(admin.username.toLowerCase());
expect(entries[1].username).to.equal(user.username.toLowerCase());
});
it('succeeds with username filter', async function () {
2022-01-07 09:57:02 +01:00
const entries = await ldapSearch('ou=users,dc=cloudron', { filter: '&(objectcategory=person)(username=' + admin.username + ')' }, auth);
2021-12-23 21:31:48 +01:00
expect(entries.length).to.equal(1);
expect(entries[0].username).to.equal(admin.username.toLowerCase());
expect(entries[0].memberof.length).to.equal(2);
});
});
describe('group search', function () {
2022-01-07 09:57:02 +01:00
it('fails without auth', async function () {
const [error] = await safe(ldapSearch('ou=groups,dc=cloudron', { filter: 'objectcategory=group' }));
expect(error).to.be.a(ldap.InsufficientAccessRightsError);
});
it('fails with wrong auth DN', async function () {
const [error] = await safe(ldapSearch('ou=groups,dc=cloudron', { filter: 'objectcategory=group' }, { dn: 'cn=doesnotexist,ou=system,dc=cloudron', secret: 'ldapsecret' }));
expect(error).to.be.a(ldap.InvalidCredentialsError);
});
it('fails with wrong auth secret', async function () {
const [error] = await safe(ldapSearch('ou=groups,dc=cloudron', { filter: 'objectcategory=group' }, { dn: 'cn=admin,ou=system,dc=cloudron', secret: 'wrongldapsecret' }));
expect(error).to.be.a(ldap.InvalidCredentialsError);
});
2021-12-23 21:31:48 +01:00
it('succeeds with basic filter', async function () {
mockApp.accessRestriction = null;
2022-01-07 09:57:02 +01:00
const entries = await ldapSearch('ou=groups,dc=cloudron', { filter: 'objectclass=group' }, auth);
2022-06-03 13:59:21 +02:00
expect(entries.length).to.equal(2);
2021-12-23 21:31:48 +01:00
// ensure order for testability
entries.sort(function (a, b) { return a.cn < b.cn; });
2022-06-03 13:59:21 +02:00
expect(entries[0].cn).to.equal('ldap-test-1');
2021-12-23 21:31:48 +01:00
expect(entries[0].memberuid.length).to.equal(2);
expect(entries[0].memberuid).to.contain(admin.id);
expect(entries[0].memberuid).to.contain(user.id);
expect(entries[0].member).to.contain(`cn=${user.id},ou=users,dc=cloudron`);
expect(entries[0].uniquemember).to.contain(`cn=${user.id},ou=users,dc=cloudron`);
2021-12-23 21:31:48 +01:00
2022-06-03 13:59:21 +02:00
expect(entries[1].cn).to.equal('ldap-test-2');
2021-12-23 21:31:48 +01:00
expect(entries[1].memberuid).to.equal(admin.id);
expect(entries[0].member).to.contain(`cn=${admin.id},ou=users,dc=cloudron`);
expect(entries[0].uniquemember).to.contain(`cn=${admin.id},ou=users,dc=cloudron`);
2021-12-23 21:31:48 +01:00
});
it ('succeeds with cn wildcard filter', async function () {
2022-01-07 09:57:02 +01:00
const entries = await ldapSearch('ou=groups,dc=cloudron', { filter: '&(objectclass=group)(cn=*)' }, auth);
2022-06-03 13:59:21 +02:00
expect(entries.length).to.equal(2);
2021-12-23 21:31:48 +01:00
// ensure order for testability
entries.sort(function (a, b) { return a.cn < b.cn; });
2022-06-03 13:59:21 +02:00
expect(entries[0].cn).to.equal('ldap-test-1');
2021-12-23 21:31:48 +01:00
expect(entries[0].memberuid.length).to.equal(2);
expect(entries[0].memberuid).to.contain(admin.id);
expect(entries[0].memberuid).to.contain(user.id);
expect(entries[0].member).to.contain(`cn=${user.id},ou=users,dc=cloudron`);
expect(entries[0].member).to.contain(`cn=${admin.id},ou=users,dc=cloudron`);
expect(entries[0].uniquemember).to.contain(`cn=${user.id},ou=users,dc=cloudron`);
expect(entries[0].uniquemember).to.contain(`cn=${admin.id},ou=users,dc=cloudron`);
2021-12-23 21:31:48 +01:00
2022-06-03 13:59:21 +02:00
expect(entries[1].cn).to.equal('ldap-test-2');
2021-12-23 21:31:48 +01:00
expect(entries[1].memberuid).to.equal(admin.id);
expect(entries[0].member).to.contain(`cn=${admin.id},ou=users,dc=cloudron`);
expect(entries[0].uniquemember).to.contain(`cn=${admin.id},ou=users,dc=cloudron`);
2021-12-23 21:31:48 +01:00
});
it('succeeds with memberuid filter', async function () {
2022-01-07 09:57:02 +01:00
const entries = await ldapSearch('ou=groups,dc=cloudron', { filter: '&(objectclass=group)(memberuid=' + user.id + ')' }, auth);
2022-06-03 13:59:21 +02:00
expect(entries.length).to.equal(1);
2021-12-23 21:31:48 +01:00
// ensure order for testability
entries.sort(function (a, b) { return a.cn < b.cn; });
2022-06-03 13:59:21 +02:00
expect(entries[0].cn).to.equal('ldap-test-1');
2021-12-23 21:31:48 +01:00
expect(entries[0].memberuid.length).to.equal(2);
2022-06-03 13:59:21 +02:00
expect(entries[0].memberuid).to.contain(admin.id);
expect(entries[0].memberuid).to.contain(user.id);
expect(entries[0].member).to.contain(`cn=${user.id},ou=users,dc=cloudron`);
expect(entries[0].member).to.contain(`cn=${admin.id},ou=users,dc=cloudron`);
expect(entries[0].uniquemember).to.contain(`cn=${user.id},ou=users,dc=cloudron`);
expect(entries[0].uniquemember).to.contain(`cn=${admin.id},ou=users,dc=cloudron`);
2021-12-23 21:31:48 +01:00
});
it ('succeeds with pagination', async function () {
mockApp.accessRestriction = null;
2022-01-07 09:57:02 +01:00
const entries = await ldapSearch('ou=groups,dc=cloudron', { filter: 'objectclass=group', paged: true }, auth);
2022-06-03 13:59:21 +02:00
expect(entries.length).to.equal(2);
2021-12-23 21:31:48 +01:00
// ensure order for testability
entries.sort(function (a, b) { return a.cn < b.cn; });
2022-06-03 13:59:21 +02:00
expect(entries[0].cn).to.equal('ldap-test-1');
2021-12-23 21:31:48 +01:00
expect(entries[0].memberuid.length).to.equal(2);
expect(entries[0].memberuid).to.contain(admin.id);
expect(entries[0].memberuid).to.contain(user.id);
expect(entries[0].member).to.contain(`cn=${user.id},ou=users,dc=cloudron`);
expect(entries[0].member).to.contain(`cn=${admin.id},ou=users,dc=cloudron`);
expect(entries[0].uniquemember).to.contain(`cn=${user.id},ou=users,dc=cloudron`);
expect(entries[0].uniquemember).to.contain(`cn=${admin.id},ou=users,dc=cloudron`);
2021-12-23 21:31:48 +01:00
2022-06-03 13:59:21 +02:00
expect(entries[1].cn).to.equal('ldap-test-2');
2021-12-23 21:31:48 +01:00
expect(entries[1].memberuid).to.equal(admin.id);
expect(entries[0].member).to.contain(`cn=${admin.id},ou=users,dc=cloudron`);
expect(entries[0].uniquemember).to.contain(`cn=${admin.id},ou=users,dc=cloudron`);
2021-12-23 21:31:48 +01:00
});
});
});