'use strict'; exports = module.exports = { start, stop }; const assert = require('assert'), appdb = require('./appdb.js'), apps = require('./apps.js'), async = require('async'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), debug = require('debug')('box:ldap'), eventlog = require('./eventlog.js'), groups = require('./groups.js'), ldap = require('ldapjs'), mail = require('./mail.js'), mailboxdb = require('./mailboxdb.js'), path = require('path'), safe = require('safetydance'), services = require('./services.js'), users = require('./users.js'); var gServer = null; var NOOP = function () {}; var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron'; var GROUP_ADMINS_DN = 'cn=admins,ou=groups,dc=cloudron'; // Will attach req.app if successful function authenticateApp(req, res, next) { var sourceIp = req.connection.ldap.id.split(':')[0]; if (sourceIp.split('.').length !== 4) return next(new ldap.InsufficientAccessRightsError('Missing source identifier')); apps.getByIpAddress(sourceIp, function (error, app) { if (error) return next(new ldap.OperationsError(error.message)); if (!app) return next(new ldap.OperationsError('Could not detect app source')); req.app = app; next(); }); } function getUsersWithAccessToApp(req, callback) { assert.strictEqual(typeof req.app, 'object'); assert.strictEqual(typeof callback, 'function'); users.getAll(function (error, result) { if (error) return callback(new ldap.OperationsError(error.toString())); async.filter(result, apps.hasAccessTo.bind(null, req.app), function (error, allowedUsers) { if (error) return callback(new ldap.OperationsError(error.toString())); callback(null, allowedUsers); }); }); } // helper function to deal with pagination 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(); } function userSearch(req, res, next) { debug('user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); getUsersWithAccessToApp(req, function (error, result) { if (error) return next(error); var results = []; // send user objects result.forEach(function (user) { // skip entries with empty username. Some apps like owncloud can't deal with this if (!user.username) return; var dn = ldap.parseDN('cn=' + user.id + ',ou=users,dc=cloudron'); var memberof = [ GROUP_USERS_DN ]; if (users.compareRoles(user.role, users.ROLE_ADMIN) >= 0) memberof.push(GROUP_ADMINS_DN); var displayName = user.displayName || user.username || ''; // displayName can be empty and username can be null var nameParts = displayName.split(' '); var firstName = nameParts[0]; var lastName = nameParts.length > 1 ? nameParts[nameParts.length - 1] : ''; // choose last part, if it exists var obj = { dn: dn.toString(), attributes: { objectclass: ['user', 'inetorgperson', 'person' ], objectcategory: 'person', cn: user.id, uid: user.id, entryuuid: user.id, // to support OpenLDAP clients mail: user.email, mailAlternateAddress: user.fallbackEmail, displayname: displayName, givenName: firstName, username: user.username, samaccountname: user.username, // to support ActiveDirectory clients memberof: memberof } }; // http://www.zytrax.com/books/ldap/ape/core-schema.html#sn has 'name' as SUP which is a DirectoryString // which is required to have atleast one character if present if (lastName.length !== 0) obj.attributes.sn = lastName; // ensure all filter values are also lowercase var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { results.push(obj); } }); finalSend(results, req, res, next); }); } function groupSearch(req, res, next) { debug('group search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); getUsersWithAccessToApp(req, function (error, result) { if (error) return next(error); var results = []; var groups = [{ name: 'users', admin: false }, { name: 'admins', admin: true }]; groups.forEach(function (group) { var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron'); var members = group.admin ? result.filter(function (user) { return users.compareRoles(user.role, users.ROLE_ADMIN) >= 0; }) : result; var obj = { dn: dn.toString(), attributes: { objectclass: ['group'], cn: group.name, memberuid: members.map(function(entry) { return entry.id; }) } }; // ensure all filter values are also lowercase var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { results.push(obj); } }); finalSend(results, req, res, next); }); } function groupUsersCompare(req, res, next) { debug('group users compare: dn %s, attribute %s, value %s (from %s)', req.dn.toString(), req.attribute, req.value, req.connection.ldap.id); getUsersWithAccessToApp(req, function (error, result) { if (error) return next(error); // we only support memberuid here, if we add new group attributes later add them here if (req.attribute === 'memberuid') { var found = result.find(function (u) { return u.id === req.value; }); if (found) return res.end(true); } res.end(false); }); } function groupAdminsCompare(req, res, next) { debug('group admins compare: dn %s, attribute %s, value %s (from %s)', req.dn.toString(), req.attribute, req.value, req.connection.ldap.id); getUsersWithAccessToApp(req, function (error, result) { if (error) return next(error); // we only support memberuid here, if we add new group attributes later add them here if (req.attribute === 'memberuid') { var user = result.find(function (u) { return u.id === req.value; }); if (user && users.compareRoles(user.role, users.ROLE_ADMIN) >= 0) return res.end(true); } res.end(false); }); } function mailboxSearch(req, res, next) { debug('mailbox search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); // if cn is set we only search for one mailbox specifically if (req.dn.rdns[0].attrs.cn) { var email = req.dn.rdns[0].attrs.cn.value.toLowerCase(); var parts = email.split('@'); if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString())); mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) { if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (error) return next(new ldap.OperationsError(error.toString())); var obj = { dn: req.dn.toString(), attributes: { objectclass: ['mailbox'], objectcategory: 'mailbox', cn: `${mailbox.name}@${mailbox.domain}`, uid: `${mailbox.name}@${mailbox.domain}`, mail: `${mailbox.name}@${mailbox.domain}` } }; // ensure all filter values are also lowercase var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); if (lowerCaseFilter.matches(obj.attributes)) { finalSend([ obj ], req, res, next); } else { res.end(); } }); } else if (req.dn.rdns[0].attrs.domain) { // legacy ldap mailbox search for old sogo var domain = req.dn.rdns[0].attrs.domain.value.toLowerCase(); mailboxdb.listMailboxes(domain, 1, 1000, function (error, mailboxes) { if (error) return next(new ldap.OperationsError(error.toString())); var results = []; // send mailbox objects mailboxes.forEach(function (mailbox) { var dn = ldap.parseDN(`cn=${mailbox.name}@${domain},domain=${domain},ou=mailboxes,dc=cloudron`); var obj = { dn: dn.toString(), attributes: { objectclass: ['mailbox'], objectcategory: 'mailbox', cn: `${mailbox.name}@${domain}`, uid: `${mailbox.name}@${domain}`, mail: `${mailbox.name}@${domain}` } }; // ensure all filter values are also lowercase var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { results.push(obj); } }); finalSend(results, req, res, next); }); } else { // new sogo mailboxdb.listAllMailboxes(1, 1000, function (error, mailboxes) { if (error) return next(new ldap.OperationsError(error.toString())); var results = []; // send mailbox objects async.eachSeries(mailboxes, function (mailbox, callback) { var dn = ldap.parseDN(`cn=${mailbox.name}@${mailbox.domain},ou=mailboxes,dc=cloudron`); let getFunc = mailbox.ownerType === mail.OWNERTYPE_USER ? users.get : groups.get; getFunc(mailbox.ownerId, function (error, ownerObject) { if (error) return callback(); // skip mailboxes with unknown owner var obj = { dn: dn.toString(), attributes: { objectclass: ['mailbox'], objectcategory: 'mailbox', displayname: mailbox.ownerType === mail.OWNERTYPE_USER ? ownerObject.displayName : ownerObject.name, cn: `${mailbox.name}@${mailbox.domain}`, uid: `${mailbox.name}@${mailbox.domain}`, mail: `${mailbox.name}@${mailbox.domain}` } }; mailbox.aliases.forEach(function (a, idx) { obj.attributes['mail' + idx] = `${a.name}@${a.domain}`; }); // ensure all filter values are also lowercase var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) { results.push(obj); } callback(); }); }, function (error) { if (error) return next(new ldap.OperationsError(error.toString())); finalSend(results, req, res, next); }); }); } } function mailAliasSearch(req, res, next) { debug('mail alias get: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString())); var email = req.dn.rdns[0].attrs.cn.value.toLowerCase(); var parts = email.split('@'); if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString())); mailboxdb.getAlias(parts[0], parts[1], function (error, alias) { if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (error) return next(new ldap.OperationsError(error.toString())); // https://wiki.debian.org/LDAP/MigrationTools/Examples // https://docs.oracle.com/cd/E19455-01/806-5580/6jej518pp/index.html // member is fully qualified - https://docs.oracle.com/cd/E19957-01/816-6082-10/chap4.doc.html#43314 var obj = { dn: req.dn.toString(), attributes: { objectclass: ['nisMailAlias'], objectcategory: 'nisMailAlias', cn: `${alias.name}@${alias.domain}`, rfc822MailMember: `${alias.aliasName}@${alias.aliasDomain}` } }; // ensure all filter values are also lowercase var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); if (lowerCaseFilter.matches(obj.attributes)) { finalSend([ obj ], req, res, next); } else { res.end(); } }); } function mailingListSearch(req, res, next) { debug('mailing list get: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString())); let email = req.dn.rdns[0].attrs.cn.value.toLowerCase(); let parts = email.split('@'); if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString())); const name = parts[0], domain = parts[1]; mail.resolveList(parts[0], parts[1], function (error, resolvedMembers, list) { if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (error) return next(new ldap.OperationsError(error.toString())); // http://ldapwiki.willeke.com/wiki/Original%20Mailgroup%20Schema%20From%20Netscape // members are fully qualified (https://docs.oracle.com/cd/E19444-01/816-6018-10/groups.htm#13356) var obj = { dn: req.dn.toString(), attributes: { objectclass: ['mailGroup'], objectcategory: 'mailGroup', cn: `${name}@${domain}`, // fully qualified mail: `${name}@${domain}`, membersOnly: list.membersOnly, // ldapjs only supports strings and string array. so this is not a bool! mgrpRFC822MailMember: resolvedMembers // fully qualified } }; // ensure all filter values are also lowercase var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null); if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString())); if (lowerCaseFilter.matches(obj.attributes)) { finalSend([ obj ], req, res, next); } else { res.end(); } }); } // Will attach req.user if successful function authenticateUser(req, res, next) { debug('user bind: %s (from %s)', req.dn.toString(), req.connection.ldap.id); // 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())); var api; if (attributeName === 'mail') { api = users.verifyWithEmail; } else if (commonName.indexOf('@') !== -1) { // if mail is specified, enforce mail check api = users.verifyWithEmail; } else if (commonName.indexOf('uid-') === 0) { api = users.verify; } else { api = users.verifyWithUsername; } api(commonName, req.credentials || '', req.app.id, function (error, user) { if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString())); if (error) return next(new ldap.OperationsError(error.message)); req.user = user; next(); }); } function authorizeUserForApp(req, res, next) { assert.strictEqual(typeof req.user, 'object'); assert.strictEqual(typeof req.app, 'object'); apps.hasAccessTo(req.app, req.user, function (error, hasAccess) { if (error) return next(new ldap.OperationsError(error.toString())); // we return no such object, to avoid leakage of a users existence if (!hasAccess) return next(new ldap.NoSuchObjectError(req.dn.toString())); eventlog.upsert(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id }, { userId: req.user.id, user: users.removePrivateFields(req.user) }); res.end(); }); } function verifyMailboxPassword(mailbox, password, callback) { assert.strictEqual(typeof mailbox, 'object'); assert.strictEqual(typeof password, 'string'); assert.strictEqual(typeof callback, 'function'); if (mailbox.ownerType === mail.OWNERTYPE_USER) return users.verify(mailbox.ownerId, password, users.AP_MAIL /* identifier */, callback); groups.getMembers(mailbox.ownerId, function (error, userIds) { if (error) return callback(error); let verifiedUser = null; async.someSeries(userIds, function iterator(userId, iteratorDone) { users.verify(userId, password, users.AP_MAIL /* identifier */, function (error, result) { if (error) return iteratorDone(null, false); verifiedUser = result; iteratorDone(null, true); }); }, function (error, result) { if (!result) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); callback(null, verifiedUser); }); }); } function authenticateUserMailbox(req, res, next) { debug('user mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id); if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString())); var email = req.dn.rdns[0].attrs.cn.value.toLowerCase(); var parts = email.split('@'); if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString())); mail.getDomain(parts[1], function (error, domain) { if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (error) return next(new ldap.OperationsError(error.message)); if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString())); mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) { if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (error) return next(new ldap.OperationsError(error.message)); verifyMailboxPassword(mailbox, req.credentials || '', function (error, result) { if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString())); if (error) return next(new ldap.OperationsError(error.message)); eventlog.upsert(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) }); res.end(); }); }); }); } function authenticateSftp(req, res, next) { debug('sftp auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id); if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString())); var email = req.dn.rdns[0].attrs.cn.value.toLowerCase(); var parts = email.split('@'); if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString())); apps.getByFqdn(parts[1], function (error, app) { if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString())); users.verifyWithUsername(parts[0], req.credentials, app.id, function (error) { if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString())); debug('sftp auth: success'); res.end(); }); }); } function loadSftpConfig(req, res, next) { services.getServiceConfig('sftp', function (error, serviceConfig) { if (error) return next(new ldap.OperationsError(error.toString())); req.requireAdmin = serviceConfig.requireAdmin; next(); }); } function userSearchSftp(req, res, next) { debug('sftp user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id); if (req.filter.attribute !== 'username' || !req.filter.value) return next(new ldap.NoSuchObjectError(req.dn.toString())); var parts = req.filter.value.split('@'); if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString())); var username = parts[0]; var appFqdn = parts[1]; apps.getByFqdn(appFqdn, function (error, app) { if (error) return next(new ldap.OperationsError(error.toString())); // only allow apps which specify "ftp" support in the localstorage addon if (!safe.query(app.manifest.addons, 'localstorage.ftp.uid')) return next(new ldap.UnavailableError('Not supported')); if (typeof app.manifest.addons.localstorage.ftp.uid !== 'number') return next(new ldap.UnavailableError('Bad uid, must be a number')); const uidNumber = app.manifest.addons.localstorage.ftp.uid; users.getByUsername(username, function (error, user) { if (error) return next(new ldap.OperationsError(error.toString())); if (req.requireAdmin && users.compareRoles(user.role, users.ROLE_ADMIN) < 0) return next(new ldap.InsufficientAccessRightsError('Insufficient previleges')); apps.hasAccessTo(app, user, function (error, hasAccess) { if (error) return next(new ldap.OperationsError(error.toString())); if (!hasAccess) return next(new ldap.InsufficientAccessRightsError('Not authorized')); var obj = { dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(), attributes: { homeDirectory: path.join('/app/data', app.id), objectclass: ['user'], objectcategory: 'person', cn: user.id, uid: `${username}@${appFqdn}`, // for bind after search uidNumber: uidNumber, // unix uid for ftp access gidNumber: uidNumber // unix gid for ftp access } }; finalSend([ obj ], req, res, next); }); }); }); } function verifyAppMailboxPassword(addonId, username, password, callback) { assert.strictEqual(typeof addonId, 'string'); assert.strictEqual(typeof username, 'string'); assert.strictEqual(typeof password, 'string'); assert.strictEqual(typeof callback, 'function'); const pattern = addonId === 'sendmail' ? 'MAIL_SMTP' : 'MAIL_IMAP'; appdb.getAppIdByAddonConfigValue(addonId, `%${pattern}_PASSWORD`, password, function (error, appId) { // search by password because this is unique for each app if (error) return callback(error); appdb.getAddonConfig(appId, addonId, function (error, result) { if (error) return callback(error); if (!result.some(r => r.name.endsWith(`${pattern}_USERNAME`) && r.value === username)) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); callback(null); }); }); } function authenticateMailAddon(req, res, next) { debug('mail addon auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id); if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString())); const email = req.dn.rdns[0].attrs.cn.value.toLowerCase(); const parts = email.split('@'); if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString())); const addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail' if (addonId !== 'sendmail' && addonId !== 'recvmail') return next(new ldap.OperationsError('Invalid DN')); mail.getDomain(parts[1], function (error, domain) { if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (error) return next(new ldap.OperationsError(error.message)); if (addonId === 'recvmail' && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString())); verifyAppMailboxPassword(addonId, email, req.credentials || '', function (error) { if (!error) return res.end(); // validated as app if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString())); if (error && error.reason !== BoxError.NOT_FOUND) return next(new ldap.OperationsError(error.message)); mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) { if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (error) return next(new ldap.OperationsError(error.message)); verifyMailboxPassword(mailbox, req.credentials || '', function (error, result) { if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString())); if (error) return next(new ldap.OperationsError(error.message)); eventlog.upsert(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) }); res.end(); }); }); }); }); } function start(callback) { assert.strictEqual(typeof callback, 'function'); var logger = { trace: NOOP, debug: NOOP, info: debug, warn: debug, error: debug, fatal: debug }; gServer = ldap.createServer({ log: logger }); gServer.on('error', function (error) { debug('start: server error ', error); }); gServer.search('ou=users,dc=cloudron', authenticateApp, userSearch); gServer.search('ou=groups,dc=cloudron', authenticateApp, groupSearch); gServer.bind('ou=users,dc=cloudron', authenticateApp, authenticateUser, authorizeUserForApp); // http://www.ietf.org/proceedings/43/I-D/draft-srivastava-ldap-mail-00.txt gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch); // haraka (address translation), dovecot (LMTP), sogo (mailbox search) gServer.bind('ou=mailboxes,dc=cloudron', authenticateUserMailbox); // apps like sogo can use domain=${domain} to authenticate a mailbox gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch); // haraka gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch); // haraka gServer.bind('ou=recvmail,dc=cloudron', authenticateMailAddon); // dovecot (IMAP auth) gServer.bind('ou=sendmail,dc=cloudron', authenticateMailAddon); // haraka (MSA auth) gServer.bind('ou=sftp,dc=cloudron', authenticateSftp); // sftp gServer.search('ou=sftp,dc=cloudron', loadSftpConfig, userSearchSftp); gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare); gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare); // this is the bind for addons (after bind, they might search and authenticate) gServer.bind('ou=addons,dc=cloudron', function(req, res /*, next */) { debug('addons bind: %s', req.dn.toString()); // note: cn can be email or id res.end(); }); // this is the bind for apps (after bind, they might search and authenticate user) gServer.bind('ou=apps,dc=cloudron', function(req, res /*, next */) { // TODO: validate password debug('application bind: %s', req.dn.toString()); res.end(); }); // just log that an attempt was made to unknown route, this helps a lot during app packaging gServer.use(function(req, res, next) { debug('not handled: dn %s, scope %s, filter %s (from %s)', req.dn ? req.dn.toString() : '-', req.scope, req.filter ? req.filter.toString() : '-', req.connection.ldap.id); return next(); }); gServer.listen(constants.LDAP_PORT, '0.0.0.0', callback); } function stop(callback) { assert.strictEqual(typeof callback, 'function'); if (gServer) gServer.close(); callback(); }