Files
cloudron-box/src/ldap.js
T

665 lines
27 KiB
JavaScript
Raw Normal View History

'use strict';
exports = module.exports = {
2015-09-14 10:59:05 -07:00
start: start,
stop: stop
};
var assert = require('assert'),
2017-03-26 19:14:50 -07:00
appdb = require('./appdb.js'),
2016-02-18 16:04:53 +01:00
apps = require('./apps.js'),
async = require('async'),
config = require('./config.js'),
2016-09-21 15:34:58 -07:00
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:ldap'),
2016-04-30 23:16:37 -07:00
eventlog = require('./eventlog.js'),
2018-04-29 10:58:45 -07:00
users = require('./users.js'),
2018-04-29 17:37:53 -07:00
UsersError = users.UsersError,
2016-05-29 17:25:23 -07:00
ldap = require('ldapjs'),
mail = require('./mail.js'),
2018-01-29 13:14:08 +01:00
MailError = mail.MailError,
2016-09-21 15:34:58 -07:00
mailboxdb = require('./mailboxdb.js'),
2019-03-18 21:15:50 -07:00
path = require('path'),
paths = require('./paths.js'),
2017-03-10 14:51:12 -08:00
safe = require('safetydance');
var gServer = null;
var NOOP = function () {};
2015-08-12 15:31:44 +02:00
var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron';
2015-08-18 16:35:52 -07:00
var GROUP_ADMINS_DN = 'cn=admins,ou=groups,dc=cloudron';
2015-08-12 15:31:44 +02:00
// Will attach req.app if successful
function authenticateApp(req, res, next) {
2016-02-18 16:40:30 +01:00
var sourceIp = req.connection.ldap.id.split(':')[0];
if (sourceIp.split('.').length !== 4) return next(new ldap.InsufficientAccessRightsError('Missing source identifier'));
2016-02-18 16:04:53 +01:00
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'));
2016-06-17 10:08:41 -05:00
req.app = app;
2016-06-17 10:08:41 -05:00
next();
2016-02-18 16:04:53 +01:00
});
}
function getUsersWithAccessToApp(req, callback) {
assert.strictEqual(typeof req.app, 'object');
2017-03-13 11:10:08 +01:00
assert.strictEqual(typeof callback, 'function');
2019-01-14 16:39:20 +01:00
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);
});
});
2016-05-12 13:36:53 -07:00
}
2017-10-27 01:25:07 +02:00
// 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) {
2019-03-21 20:06:14 -07:00
resultCookie = Buffer.from(last.toString());
2017-10-27 01:25:07 +02:00
} else {
2019-03-21 20:06:14 -07:00
resultCookie = Buffer.from('');
2017-10-27 01:25:07 +02:00
}
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);
2017-10-27 01:25:07 +02:00
var results = [];
// send user objects
result.forEach(function (entry) {
// skip entries with empty username. Some apps like owncloud can't deal with this
if (!entry.username) return;
var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron');
var groups = [ GROUP_USERS_DN ];
2018-09-03 16:08:05 +02:00
if (entry.admin || req.app.ownerId === entry.id) groups.push(GROUP_ADMINS_DN);
var displayName = entry.displayName || entry.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'],
objectcategory: 'person',
cn: entry.id,
uid: entry.id,
mail: entry.email,
2018-01-21 14:50:24 +01:00
mailAlternateAddress: entry.fallbackEmail,
displayname: displayName,
givenName: firstName,
username: entry.username,
samaccountname: entry.username, // to support ActiveDirectory clients
2018-09-03 16:08:05 +02:00
isadmin: (entry.admin || req.app.ownerId === entry.id) ? 1 : 0,
memberof: groups
}
};
// 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)) {
2017-10-27 01:25:07 +02:00
results.push(obj);
}
});
2017-10-27 01:25:07 +02:00
finalSend(results, req, res, next);
});
}
2016-05-12 13:36:53 -07:00
function groupSearch(req, res, next) {
2016-05-16 14:14:58 -07:00
debug('group search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
2016-05-12 13:36:53 -07:00
getUsersWithAccessToApp(req, function (error, result) {
if (error) return next(error);
2016-05-12 13:36:53 -07:00
2017-10-27 01:25:07 +02:00
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');
2018-09-03 17:14:09 +02:00
var members = group.admin ? result.filter(function (entry) { return entry.admin || req.app.ownerId === entry.id; }) : 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)) {
2017-10-27 01:25:07 +02:00
results.push(obj);
}
});
2017-10-27 01:25:07 +02:00
finalSend(results, req, res, next);
});
2016-05-12 13:36:53 -07:00
}
2017-10-24 01:35:35 +02:00
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 found = result.find(function (u) { return u.id === req.value; });
2018-09-03 17:14:09 +02:00
if (found && (found.admin || req.app.ownerId == found.id)) return res.end(true);
2017-10-24 01:35:35 +02:00
}
res.end(false);
});
}
2016-09-26 10:18:58 -07:00
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);
2016-05-29 18:24:54 -07:00
// 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()));
2017-10-27 01:25:07 +02:00
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
2016-09-26 21:03:07 -07:00
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}`,
displayname: 'Max Mustermann',
givenName: 'Max',
username: 'mmustermann',
samaccountname: 'mmustermann'
}
};
2016-05-29 18:24:54 -07:00
// 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()));
2016-09-25 18:59:11 -07:00
if (lowerCaseFilter.matches(obj.attributes)) {
finalSend([ obj ], req, res, next);
} else {
res.end();
}
});
} else if (req.dn.rdns[0].attrs.domain) {
var domain = req.dn.rdns[0].attrs.domain.value.toLowerCase();
mailboxdb.listMailboxes(domain, function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
var results = [];
// send mailbox objects
result.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}`,
2018-12-06 20:25:24 -08:00
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);
}
});
2016-09-25 18:59:11 -07:00
finalSend(results, req, res, next);
});
} else {
return next(new ldap.NoSuchObjectError(req.dn.toString()));
}
2016-09-25 18:59:11 -07:00
}
2016-09-26 14:38:23 -07:00
function mailAliasSearch(req, res, next) {
2016-09-25 18:59:11 -07:00
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()));
2017-10-27 01:25:07 +02:00
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) {
2016-09-26 14:38:23 -07:00
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
2016-09-25 18:59:11 -07:00
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 = {
2016-09-27 11:58:02 -07:00
dn: req.dn.toString(),
attributes: {
objectclass: ['nisMailAlias'],
objectcategory: 'nisMailAlias',
cn: `${alias.name}@${alias.domain}`,
rfc822MailMember: `${alias.aliasTarget}@${alias.domain}`
}
};
2016-09-25 18:59:11 -07:00
// 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()));
2016-09-25 18:59:11 -07:00
2017-10-27 01:25:07 +02:00
if (lowerCaseFilter.matches(obj.attributes)) {
finalSend([ obj ], req, res, next);
} else {
res.end();
}
2016-09-25 18:59:11 -07:00
});
}
2016-09-27 12:20:20 -07:00
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);
2016-09-25 18:59:11 -07:00
2016-09-27 12:20:20 -07:00
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
2017-10-27 01:25:07 +02:00
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.getGroup(parts[0], parts[1], function (error, group) {
2016-09-27 12:20:20 -07:00
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
2016-09-25 18:59:11 -07:00
if (error) return next(new ldap.OperationsError(error.toString()));
2016-09-27 16:27:22 -07:00
// 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)
2016-09-27 12:20:20 -07:00
var obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['mailGroup'],
objectcategory: 'mailGroup',
cn: `${group.name}@${group.domain}`, // fully qualified
mail: `${group.name}@${group.domain}`,
mgrpRFC822MailMember: group.members.map(function (m) { return `${m}@${group.domain}`; })
2016-09-27 12:20:20 -07:00
}
};
2016-05-29 18:24:54 -07:00
2016-09-27 12:20:20 -07:00
// 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()));
2016-05-29 18:24:54 -07:00
2017-10-27 01:25:07 +02:00
if (lowerCaseFilter.matches(obj.attributes)) {
finalSend([ obj ], req, res, next);
} else {
res.end();
}
2016-05-29 18:24:54 -07:00
});
}
// Will attach req.user if successful
function authenticateUser(req, res, next) {
2016-05-16 14:14:58 -07:00
debug('user bind: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
2016-05-12 13:36:53 -07:00
// extract the common name which might have different attribute names
2016-09-26 09:08:04 -07:00
var attributeName = Object.keys(req.dn.rdns[0].attrs)[0];
var commonName = req.dn.rdns[0].attrs[attributeName].value;
2016-05-12 13:36:53 -07:00
if (!commonName) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var api;
2016-05-16 12:21:15 -07:00
if (attributeName === 'mail') {
2018-04-29 10:58:45 -07:00
api = users.verifyWithEmail;
2016-05-16 12:21:15 -07:00
} else if (commonName.indexOf('@') !== -1) { // if mail is specified, enforce mail check
2018-04-29 10:58:45 -07:00
api = users.verifyWithEmail;
2016-05-12 13:36:53 -07:00
} else if (commonName.indexOf('uid-') === 0) {
2018-04-29 10:58:45 -07:00
api = users.verify;
2016-05-12 13:36:53 -07:00
} else {
2018-04-29 10:58:45 -07:00
api = users.verifyWithUsername;
2016-05-12 13:36:53 -07:00
}
api(commonName, req.credentials || '', function (error, user) {
2018-04-29 17:37:53 -07:00
if (error && error.reason === UsersError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
2016-06-17 10:08:41 -05:00
if (error) return next(new ldap.OperationsError(error.message));
2016-05-12 13:36:53 -07:00
req.user = user;
2016-05-12 13:36:53 -07:00
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, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
2016-05-12 13:36:53 -07:00
// we return no such object, to avoid leakage of a users existence
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
2016-05-12 13:36:53 -07:00
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
2016-05-12 13:36:53 -07:00
res.end();
2016-05-12 13:36:53 -07:00
});
}
2018-12-16 18:04:30 -08:00
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 === MailError.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 === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
users.verify(mailbox.ownerId, req.credentials || '', function (error, result) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
res.end();
});
});
});
}
2019-03-18 21:15:50 -07:00
function authenticateProftpd(req, res, next) {
debug('proftpd addon auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
var sourceIp = req.connection.ldap.id.split(':')[0];
if (sourceIp.split('.').length !== 4) return next(new ldap.InsufficientAccessRightsError('Missing source identifier'));
if (sourceIp !== '127.0.0.1') return next(new ldap.InsufficientAccessRightsError('Source not authorized'));
2019-03-18 21:15:50 -07:00
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()));
// actual user bind
users.verifyWithUsername(parts[0], req.credentials, function (error) {
2019-03-18 21:15:50 -07:00
if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
debug('proftpd addon auth: success');
res.end();
});
}
function userSearchProftpd(req, res, next) {
debug('proftpd user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
var sourceIp = req.connection.ldap.id.split(':')[0];
if (sourceIp.split('.').length !== 4) return next(new ldap.InsufficientAccessRightsError('Missing source identifier'));
if (sourceIp !== '127.0.0.1') return next(new ldap.InsufficientAccessRightsError('Source not authorized'));
2019-03-18 21:15:50 -07:00
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];
2019-03-19 16:23:03 -07:00
var appFqdn = parts[1];
2019-03-18 21:15:50 -07:00
2019-03-19 16:23:03 -07:00
apps.getByFqdn(appFqdn, function (error, app) {
2019-03-18 21:15:50 -07:00
if (error) return next(new ldap.OperationsError(error.toString()));
// only allow apps which specify "ftp" support in the localstorage addon
if (!app.manifest.addons.localstorage || !app.manifest.addons.localstorage.ftp) return next(new ldap.UnavailableError('Not supported'));
var uidNumber = app.manifest.addons.localstorage.ftp;
if (!Number.isInteger(uidNumber)) {
console.error('addon localstorage ftp uid must be an integer', app);
return next(new ldap.UnavailableError('Not supported'));
}
2019-03-19 16:23:03 -07:00
users.getByUsername(username, function (error, user) {
2019-03-18 21:15:50 -07:00
if (error) return next(new ldap.OperationsError(error.toString()));
2019-03-19 16:23:03 -07:00
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'));
2019-03-18 21:15:50 -07:00
2019-03-19 16:23:03 -07:00
var obj = {
dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=proftpd,dc=cloudron`).toString(),
attributes: {
homeDirectory: path.join(paths.APPS_DATA_DIR, app.id, 'data'),
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
2019-03-19 16:23:03 -07:00
}
};
2019-03-18 21:15:50 -07:00
2019-03-19 16:23:03 -07:00
finalSend([ obj ], req, res, next);
});
2019-03-18 21:15:50 -07:00
});
});
}
2018-12-16 18:04:30 -08:00
function authenticateMailAddon(req, res, next) {
debug('mail addon auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
2018-02-08 18:49:27 -08:00
2016-09-26 11:50:32 -07:00
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()));
2016-09-26 11:50:32 -07:00
2018-12-06 21:08:19 -08:00
const addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
mail.getDomain(parts[1], function (error, domain) {
if (error && error.reason === MailError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
2016-09-26 11:50:32 -07:00
if (error) return next(new ldap.OperationsError(error.message));
2018-12-06 21:08:19 -08:00
if (addonId === 'recvmail' && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
2018-12-06 21:08:19 -08:00
let name;
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
2018-12-06 21:08:19 -08:00
// note: with sendmail addon, apps can send mail without a mailbox (unlike users)
appdb.getAppIdByAddonConfigValue(addonId, name, req.credentials || '', function (error, appId) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return next(new ldap.OperationsError(error.message));
if (appId) { // matched app password
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: email }, { appId: appId, addonId: addonId });
return res.end();
}
2018-12-06 21:08:19 -08:00
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
2018-04-29 10:58:45 -07:00
users.verify(mailbox.ownerId, req.credentials || '', function (error, result) {
2018-04-29 17:37:53 -07:00
if (error && error.reason === UsersError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === UsersError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
2018-04-29 10:58:45 -07:00
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
res.end();
});
2018-12-06 21:08:19 -08:00
});
});
2016-09-26 11:50:32 -07:00
});
2016-05-29 17:25:23 -07:00
}
2016-05-12 13:36:53 -07:00
function start(callback) {
assert.strictEqual(typeof callback, 'function');
2016-09-25 16:11:54 -07:00
var logger = {
trace: NOOP,
debug: NOOP,
info: debug,
warn: debug,
error: console.error,
fatal: console.error
};
gServer = ldap.createServer({ log: logger });
2016-05-12 13:36:53 -07:00
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);
2016-09-25 18:59:11 -07:00
// http://www.ietf.org/proceedings/43/I-D/draft-srivastava-ldap-mail-00.txt
2018-12-16 18:04:30 -08:00
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch); // haraka, dovecot
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
2016-09-25 18:59:11 -07:00
2018-12-16 18:04:30 -08:00
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailAddon); // dovecot
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailAddon); // haraka
2016-05-29 17:25:23 -07:00
2019-03-18 21:15:50 -07:00
gServer.bind('ou=proftpd,dc=cloudron', authenticateProftpd); // proftdp
gServer.search('ou=proftpd,dc=cloudron', userSearchProftpd);
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);
2017-10-24 01:35:35 +02:00
2016-05-12 13:20:57 -07:00
// this is the bind for addons (after bind, they might search and authenticate)
2017-11-15 18:07:10 -08:00
gServer.bind('ou=addons,dc=cloudron', function(req, res /*, next */) {
2016-05-12 13:20:57 -07:00
debug('addons bind: %s', req.dn.toString()); // note: cn can be email or id
2016-05-11 14:26:34 -07:00
res.end();
});
2016-05-12 13:20:57 -07:00
// this is the bind for apps (after bind, they might search and authenticate user)
2017-11-15 18:07:10 -08:00
gServer.bind('ou=apps,dc=cloudron', function(req, res /*, next */) {
2015-09-25 21:17:48 -07:00
// TODO: validate password
debug('application bind: %s', req.dn.toString());
2015-09-25 21:17:48 -07:00
res.end();
});
gServer.listen(config.get('ldapPort'), '0.0.0.0', callback);
}
2015-09-14 10:59:05 -07:00
function stop(callback) {
2015-09-14 11:09:37 -07:00
assert.strictEqual(typeof callback, 'function');
2015-09-14 10:59:05 -07:00
if (gServer) gServer.close();
2015-09-14 10:59:05 -07:00
callback();
}