/* 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'), expect = require('expect.js'), groups = require('../groups.js'), ldap = require('ldapjs'), safe = require('safetydance'), settings = require('../settings.js'); async function ldapBind(dn, password) { return new Promise((resolve, reject) => { const client = ldap.createClient({ url: 'ldaps://127.0.0.1:' + constants.USER_DIRECTORY_LDAPS_PORT, tlsOptions: { rejectUnauthorized: false }}); 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 async function ldapSearch(dn, opts, auth) { return new Promise((resolve, reject) => { const client = ldap.createClient({ url: 'ldaps://127.0.0.1:' + constants.USER_DIRECTORY_LDAPS_PORT, tlsOptions: { rejectUnauthorized: false }}); function bindAuth(callback) { if (!auth) return callback(); client.on('error', callback); client.bind(auth.dn, auth.secret, callback); } bindAuth(function (error) { if (error) return reject(error); client.search(dn, opts, function (error, result) { if (error) return reject(error); let entries = []; result.on('searchEntry', function (entry) { entries.push(entry.object); }); result.on('error', function (error) { client.unbind(); reject(error); }); result.on('end', function (result) { if (result.status !== 0) return reject(new Error(`Unexpected status: ${result.status}`)); resolve(entries); }); }); }); }); } describe('User Directory Ldap', function () { const { setup, cleanup, admin, user, app, domain } = common; let group, group2; const mockApp = Object.assign({}, app); const auth = { dn: constants.USER_DIRECTORY_LDAP_DN, secret: 'ldapsecret' }; before(function (done) { async.series([ setup, directoryServer.start.bind(null), directoryServer.setConfig.bind(null, { enabled: true, secret: auth.secret, allowlist: '127.0.0.1' }), async () => { group = await groups.add({ name: 'ldap-test-1' }); await groups.setMembers(group.id, [ admin.id, user.id ]); }, async () => { group2 = await groups.add({ name: 'ldap-test-2' }); await groups.setMembers(group2.id, [ admin.id ]); } ], done); }); after(function (done) { async.series([ directoryServer.stop, cleanup ], done); }); describe('user bind', function () { 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); }); }); describe('search users', function () { 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); }); it('fails for non existing tree', async function () { const [error] = await safe(ldapSearch('o=example', { filter: '(&(l=Seattle)(email=*@' + domain.domain + '))' }, auth)); expect(error).to.be.a(ldap.NoSuchObjectError); }); it('succeeds with basic filter', async function () { const entries = await ldapSearch('ou=users,dc=cloudron', { filter: 'objectcategory=person' }, auth); 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 () { const entries = await ldapSearch('ou=users,dc=cloudron', { filter: 'objectcategory=person', paged: true }, auth); 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 () { const entries = await ldapSearch('ou=users,dc=cloudron', { filter: '&(objectcategory=person)(username=*)' }, auth); 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 () { const entries = await ldapSearch('ou=users,dc=cloudron', { filter: '&(objectcategory=person)(username=' + admin.username + ')' }, auth); 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 () { 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); }); it('succeeds with basic filter', async function () { mockApp.accessRestriction = null; const entries = await ldapSearch('ou=groups,dc=cloudron', { filter: 'objectclass=group' }, auth); expect(entries.length).to.equal(2); // ensure order for testability entries.sort(function (a, b) { return a.cn < b.cn; }); expect(entries[0].cn).to.equal('ldap-test-1'); 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`); expect(entries[1].cn).to.equal('ldap-test-2'); 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`); }); it ('succeeds with cn wildcard filter', async function () { const entries = await ldapSearch('ou=groups,dc=cloudron', { filter: '&(objectclass=group)(cn=*)' }, auth); expect(entries.length).to.equal(2); // ensure order for testability entries.sort(function (a, b) { return a.cn < b.cn; }); expect(entries[0].cn).to.equal('ldap-test-1'); 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`); expect(entries[1].cn).to.equal('ldap-test-2'); 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`); }); it('succeeds with memberuid filter', async function () { const entries = await ldapSearch('ou=groups,dc=cloudron', { filter: '&(objectclass=group)(memberuid=' + user.id + ')' }, auth); expect(entries.length).to.equal(1); // ensure order for testability entries.sort(function (a, b) { return a.cn < b.cn; }); expect(entries[0].cn).to.equal('ldap-test-1'); 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`); }); it ('succeeds with pagination', async function () { mockApp.accessRestriction = null; const entries = await ldapSearch('ou=groups,dc=cloudron', { filter: 'objectclass=group', paged: true }, auth); expect(entries.length).to.equal(2); // ensure order for testability entries.sort(function (a, b) { return a.cn < b.cn; }); expect(entries[0].cn).to.equal('ldap-test-1'); 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`); expect(entries[1].cn).to.equal('ldap-test-2'); 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`); }); }); });