diff --git a/src/addons.js b/src/addons.js index a5b83b45c..371659e02 100644 --- a/src/addons.js +++ b/src/addons.js @@ -815,7 +815,8 @@ function setupEmail(app, options, callback) { { name: 'MAIL_SIEVE_SERVER', value: 'mail' }, { name: 'MAIL_SIEVE_PORT', value: '4190' }, { name: 'MAIL_DOMAIN', value: app.domain }, - { name: 'MAIL_DOMAINS', value: mailInDomains } + { name: 'MAIL_DOMAINS', value: mailInDomains }, + { name: 'LDAP_MAILBOXES_BASE_DN', value: 'ou=mailboxes,dc=cloudron' } ]; debugApp(app, 'Setting up Email'); diff --git a/src/ldap.js b/src/ldap.js index 98c0f05cf..210c6dce4 100644 --- a/src/ldap.js +++ b/src/ldap.js @@ -450,8 +450,39 @@ function authorizeUserForApp(req, res, next) { }); } -function authenticateMailbox(req, res, next) { - debug('mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id); +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(); + }); + }); + }); +} + +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())); @@ -516,12 +547,13 @@ function start(callback) { 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); - gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch); - gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch); + 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 - gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox); // dovecot - gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox); // haraka + gServer.bind('ou=recvmail,dc=cloudron', authenticateMailAddon); // dovecot + gServer.bind('ou=sendmail,dc=cloudron', authenticateMailAddon); // haraka gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare); gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare); diff --git a/src/test/ldap-test.js b/src/test/ldap-test.js index 884680cc0..3d061b205 100644 --- a/src/test/ldap-test.js +++ b/src/test/ldap-test.js @@ -110,6 +110,8 @@ function setup(done) { appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0, callback); }); }, + (done) => mailboxdb.addMailbox(USER_0.username.toLowerCase(), DOMAIN_0.domain, USER_0.id, done), + (done) => mailboxdb.setAliasesForName(USER_0.username.toLowerCase(), DOMAIN_0.domain, [ USER_0_ALIAS.toLocaleLowerCase() ], done), appdb.update.bind(null, APP_0.id, { containerId: APP_0.containerId }), appdb.setAddonConfig.bind(null, APP_0.id, 'sendmail', [{ name: 'MAIL_SMTP_PASSWORD', value : 'sendmailpassword' }]), appdb.setAddonConfig.bind(null, APP_0.id, 'recvmail', [{ name: 'MAIL_IMAP_PASSWORD', value : 'recvmailpassword' }]), @@ -812,10 +814,6 @@ describe('Ldap', function () { } describe('search mailbox', function () { - before(function (done) { - mailboxdb.addMailbox(USER_0.username.toLowerCase(), DOMAIN_0.domain, USER_0.id, done); - }); - it('get specific mailbox by email', function (done) { ldapSearch('cn=' + USER_0.username + '@example.com,ou=mailboxes,dc=cloudron', 'objectclass=mailbox', function (error, entries) { if (error) return done(error); @@ -848,10 +846,6 @@ describe('Ldap', function () { }); describe('search aliases', function () { - before(function (done) { - mailboxdb.setAliasesForName(USER_0.username.toLowerCase(), DOMAIN_0.domain, [ USER_0_ALIAS.toLocaleLowerCase() ], done); - }); - it('get specific alias', function (done) { ldapSearch('cn=' + USER_0_ALIAS + '@example.com,ou=mailaliases,dc=cloudron', 'objectclass=nismailalias', function (error, entries) { if (error) return done(error); @@ -919,6 +913,68 @@ describe('Ldap', function () { }); }); + describe('user mailbox bind', function () { + it('email disabled - cannot find domain email', function (done) { + var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') }); + + client.bind('cn=' + USER_0.username + '@example.com,domain=example.com,ou=mailboxes,dc=cloudron', USER_0.password + 'nope', function (error) { + expect(error).to.be.a(ldap.NoSuchObjectError); + client.unbind(done); + }); + }); + + it('email enabled - does not allow with invalid password', function (done) { + // use maildb to not trigger further events + maildb.update(DOMAIN_0.domain, { enabled: true }, function (error) { + expect(error).not.to.be.ok(); + + var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') }); + + client.bind('cn=' + USER_0.username + '@example.com,domain=example.com,ou=mailboxes,dc=cloudron', USER_0.password + 'nope', function (error) { + expect(error).to.be.a(ldap.InvalidCredentialsError); + + client.unbind(); + + maildb.update(DOMAIN_0.domain, { enabled: false }, done); + }); + }); + }); + + it('email enabled - allows with valid password', function (done) { + // use maildb to not trigger further events + maildb.update(DOMAIN_0.domain, { enabled: true }, function (error) { + expect(error).not.to.be.ok(); + + var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') }); + + client.bind('cn=' + USER_0.username + '@example.com,domain=example.com,ou=mailboxes,dc=cloudron', USER_0.password, function (error) { + expect(error).not.to.be.ok(); + + client.unbind(); + + maildb.update(DOMAIN_0.domain, { enabled: false }, done); + }); + }); + }); + + it('email enabled - cannot auth with alias', function (done) { + // use maildb to not trigger further events + maildb.update(DOMAIN_0.domain, { enabled: true }, function (error) { + expect(error).not.to.be.ok(); + + var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') }); + + client.bind('cn=' + USER_0_ALIAS + '@example.com,domain=example.com,ou=mailboxes,dc=cloudron', USER_0.password, function (error) { + expect(error).to.be.a(ldap.NoSuchObjectError); + + client.unbind(); + + maildb.update(DOMAIN_0.domain, { enabled: false }, done); + }); + }); + }); + }); + describe('user sendmail bind', function () { it('email disabled - cannot find domain email', function (done) { var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });