diff --git a/src/accesscontrol.js b/src/accesscontrol.js index 39b888c15..97be93b8a 100644 --- a/src/accesscontrol.js +++ b/src/accesscontrol.js @@ -8,10 +8,7 @@ const assert = require('assert'), BoxError = require('./boxerror.js'), safe = require('safetydance'), tokens = require('./tokens.js'), - users = require('./users.js'), - util = require('util'); - -const userGet = util.promisify(users.get); + users = require('./users.js'); async function verifyToken(accessToken) { assert.strictEqual(typeof accessToken, 'string'); @@ -19,10 +16,8 @@ async function verifyToken(accessToken) { const token = await tokens.getByAccessToken(accessToken); if (!token) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'No such token'); - const [error, user] = await safe(userGet(token.identifier)); - if (error && error.reason === BoxError.NOT_FOUND) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'User not found'); - if (error) throw error; - + const user = await users.get(token.identifier); + if (!user) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'User not found'); if (!user.active) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'User not active'); await safe(tokens.update(token.id, { lastUsedTime: new Date() })); // ignore any error diff --git a/src/apppasswords.js b/src/apppasswords.js index 909f7cacd..2facd71d7 100644 --- a/src/apppasswords.js +++ b/src/apppasswords.js @@ -15,6 +15,7 @@ const assert = require('assert'), database = require('./database.js'), hat = require('./hat.js'), safe = require('safetydance'), + uuid = require('uuid'), _ = require('underscore'); diff --git a/src/cloudron.js b/src/cloudron.js index 9a489ebd8..20c187562 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -50,7 +50,8 @@ const apps = require('./apps.js'), split = require('split'), sysinfo = require('./sysinfo.js'), tasks = require('./tasks.js'), - users = require('./users.js'); + users = require('./users.js'), + util = require('util'); const REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'); @@ -125,7 +126,7 @@ function runStartupTasks() { // check activation state and start the platform function (callback) { - users.isActivated(function (error, activated) { + util.callbackify(users.isActivated)(function (error, activated) { if (error) return callback(error); // configure nginx to be reachable by IP when not activated. for the moment, the IP based redirect exists even after domain is setup diff --git a/src/externalldap.js b/src/externalldap.js index f39febbdf..8288eb6af 100644 --- a/src/externalldap.js +++ b/src/externalldap.js @@ -3,7 +3,7 @@ exports = module.exports = { search, verifyPassword, - createAndVerifyUserIfNotExist, + maybeCreateUser, testConfig, startSyncer, @@ -292,61 +292,59 @@ function search(identifier, callback) { }); } -function createAndVerifyUserIfNotExist(identifier, password, callback) { +async function maybeCreateUser(identifier, password) { assert.strictEqual(typeof identifier, 'string'); assert.strictEqual(typeof password, 'string'); - assert.strictEqual(typeof callback, 'function'); - settings.getExternalLdapConfig(function (error, externalLdapConfig) { - if (error) return callback(error); - if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled')); - if (!externalLdapConfig.autoCreate) return callback(new BoxError(BoxError.BAD_STATE, 'auto create not enabled')); + return new Promise((resolve, reject) => { + settings.getExternalLdapConfig(function (error, externalLdapConfig) { + if (error) return reject(error); + if (externalLdapConfig.provider === 'noop') return reject(new BoxError(BoxError.BAD_STATE, 'not enabled')); + if (!externalLdapConfig.autoCreate) return reject(new BoxError(BoxError.BAD_STATE, 'auto create not enabled')); - ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) { - if (error) return callback(error); - if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND)); - if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT)); + ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, async function (error, ldapUsers) { + if (error) return reject(error); + if (ldapUsers.length === 0) return reject(new BoxError(BoxError.NOT_FOUND)); + if (ldapUsers.length > 1) return reject(new BoxError(BoxError.CONFLICT)); - let user = translateUser(externalLdapConfig, ldapUsers[0]); - if (!validUserRequirements(user)) return callback(new BoxError(BoxError.BAD_FIELD)); + const user = translateUser(externalLdapConfig, ldapUsers[0]); + if (!validUserRequirements(user)) return reject(new BoxError(BoxError.BAD_FIELD)); - users.create(user.username, null /* password */, user.email, user.displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_AUTO_CREATE, function (error, user) { + [error] = await safe(users.add(user.email, { username: user.username, password: null, displayName: user.displayName, source: 'ldap' }, auditSource.EXTERNAL_LDAP_AUTO_CREATE)); if (error) { - debug(`createAndVerifyUserIfNotExist: Failed to auto create user ${user.username}`, error); - return callback(new BoxError(BoxError.INTERNAL_ERROR)); + debug(`maybeCreateUser: failed to auto create user ${user.username}`, error); + return reject(new BoxError(BoxError.INTERNAL_ERROR, error)); } - verifyPassword(user, password, function (error) { - if (error) return callback(error); - callback(null, user); - }); + resolve(user); }); }); }); } -function verifyPassword(user, password, callback) { +async function verifyPassword(user, password) { assert.strictEqual(typeof user, 'object'); assert.strictEqual(typeof password, 'string'); - assert.strictEqual(typeof callback, 'function'); - settings.getExternalLdapConfig(function (error, externalLdapConfig) { - if (error) return callback(error); - if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled')); + return new Promise((resolve, reject) => { + settings.getExternalLdapConfig(function (error, externalLdapConfig) { + if (error) return reject(error); + if (externalLdapConfig.provider === 'noop') return reject(new BoxError(BoxError.BAD_STATE, 'not enabled')); - ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` }, function (error, ldapUsers) { - if (error) return callback(error); - if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND)); - if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT)); + ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` }, function (error, ldapUsers) { + if (error) return reject(error); + if (ldapUsers.length === 0) return reject(new BoxError(BoxError.NOT_FOUND)); + if (ldapUsers.length > 1) return reject(new BoxError(BoxError.CONFLICT)); - getClient(externalLdapConfig, false, function (error, client) { - if (error) return callback(error); + getClient(externalLdapConfig, false, function (error, client) { + if (error) return reject(error); - client.bind(ldapUsers[0].dn, password, function (error) { - if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); - if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error)); + client.bind(ldapUsers[0].dn, password, function (error) { + if (error instanceof ldap.InvalidCredentialsError) return reject(new BoxError(BoxError.INVALID_CREDENTIALS)); + if (error) return reject(new BoxError(BoxError.EXTERNAL_ERROR, error)); - callback(null, translateUser(externalLdapConfig, ldapUsers[0])); + resolve(translateUser(externalLdapConfig, ldapUsers[0])); + }); }); }); }); diff --git a/src/ldap.js b/src/ldap.js index 6a1b7e9bb..6df226e1f 100644 --- a/src/ldap.js +++ b/src/ldap.js @@ -56,7 +56,9 @@ function getUsersWithAccessToApp(req, callback) { assert.strictEqual(typeof req.app, 'object'); assert.strictEqual(typeof callback, 'function'); - users.getAll(function (error, result) { + const getAllUsers = util.callbackify(users.getAll); + + getAllUsers(function (error, result) { if (error) return callback(new ldap.OperationsError(error.toString())); async.filter(result, apps.hasAccessTo.bind(null, req.app), function (error, allowedUsers) { @@ -331,53 +333,45 @@ function mailboxSearch(req, res, next) { finalSend(results, req, res, next); }); } else { // new sogo - mailboxdb.listAllMailboxes(1, 1000, function (error, mailboxes) { + mailboxdb.listAllMailboxes(1, 1000, async function (error, mailboxes) { if (error) return next(new ldap.OperationsError(error.toString())); mailboxes = mailboxes.filter(m => m.active); let results = []; - // send mailbox objects - async.eachSeries(mailboxes, function (mailbox, callback) { - var dn = ldap.parseDN(`cn=${mailbox.name}@${mailbox.domain},ou=mailboxes,dc=cloudron`); + for (const mailbox of mailboxes) { + const dn = ldap.parseDN(`cn=${mailbox.name}@${mailbox.domain},ou=mailboxes,dc=cloudron`); - let getFunc = mailbox.ownerType === mail.OWNERTYPE_USER ? users.get : groups.get; + const [error, ownerObject] = await safe(mailbox.ownerType === mail.OWNERTYPE_USER ? users.get(mailbox.ownerId) : groups.get(mailbox.ownerId)); + if (error || !ownerObject) continue; // skip mailboxes with unknown user - 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); + const 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}` } + }; - callback(); + mailbox.aliases.forEach(function (a, idx) { + obj.attributes['mail' + idx] = `${a.name}@${a.domain}`; }); - }, function (error) { - if (error) return next(new ldap.OperationsError(error.toString())); - finalSend(results, req, res, next); - }); + // ensure all filter values are also lowercase + const 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); }); } } @@ -465,34 +459,33 @@ function mailingListSearch(req, res, next) { } // Will attach req.user if successful -function authenticateUser(req, res, next) { +async 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; + const attributeName = Object.keys(req.dn.rdns[0].attrs)[0]; + const commonName = req.dn.rdns[0].attrs[attributeName].value; if (!commonName) return next(new ldap.NoSuchObjectError(req.dn.toString())); - var api; + let verifyFunc; if (attributeName === 'mail') { - api = users.verifyWithEmail; + verifyFunc = users.verifyWithEmail; } else if (commonName.indexOf('@') !== -1) { // if mail is specified, enforce mail check - api = users.verifyWithEmail; + verifyFunc = users.verifyWithEmail; } else if (commonName.indexOf('uid-') === 0) { - api = users.verify; + verifyFunc = users.verify; } else { - api = users.verifyWithUsername; + verifyFunc = 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)); + const [error, user] = await safe(verifyFunc(commonName, req.credentials || '', req.app.id)); + 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; + req.user = user; - next(); - }); + next(); } function authorizeUserForApp(req, res, next) { @@ -511,27 +504,24 @@ function authorizeUserForApp(req, res, next) { }); } -async function verifyMailboxPassword(mailbox, password, callback) { +async function verifyMailboxPassword(mailbox, password) { 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); + if (mailbox.ownerType === mail.OWNERTYPE_USER) return await users.verify(mailbox.ownerId, password, users.AP_MAIL /* identifier */); - const [error, userIds] = await safe(groups.getMembers(mailbox.ownerId)); - if (error) return callback(error); + const userIds = await groups.getMembers(mailbox.ownerId); 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); - }); + for (const userId of userIds) { + const [error, result] = await safe(users.verify(userId, password, users.AP_MAIL /* identifier */)); + if (error) continue; // try the next user + verifiedUser = result; + break; // found a matching validated user + } + + if (!verifiedUser) throw new BoxError(BoxError.INVALID_CREDENTIALS); + return verifiedUser; } function authenticateUserMailbox(req, res, next) { @@ -551,21 +541,20 @@ function authenticateUserMailbox(req, res, next) { if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString())); - mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) { + mailboxdb.getMailbox(parts[0], parts[1], async 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)); if (!mailbox.active) return next(new ldap.NoSuchObjectError(req.dn.toString())); - verifyMailboxPassword(mailbox, req.credentials || '', async 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)); + const [verifyError, result] = await safe(verifyMailboxPassword(mailbox, req.credentials || '')); + if (verifyError && verifyError.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); + if (verifyError && verifyError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString())); + if (verifyError) return next(new ldap.OperationsError(verifyError.message)); - eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) }); + eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) }); - res.end(); - }); + res.end(); }); }); } @@ -575,20 +564,19 @@ function authenticateSftp(req, res, next) { 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('@'); + 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())); - apps.getByFqdn(parts[1], function (error, app) { + apps.getByFqdn(parts[1], async 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())); + [error] = await safe(users.verifyWithUsername(parts[0], req.credentials, app.id)); + if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString())); - debug('sftp auth: success'); + debug('sftp auth: success'); - res.end(); - }); + res.end(); }); } @@ -613,7 +601,7 @@ function userSearchSftp(req, res, next) { var username = parts[0]; var appFqdn = parts[1]; - apps.getByFqdn(appFqdn, function (error, app) { + apps.getByFqdn(appFqdn, async function (error, app) { if (error) return next(new ldap.OperationsError(error.toString())); // only allow apps which specify "ftp" support in the localstorage addon @@ -622,30 +610,30 @@ function userSearchSftp(req, res, next) { const uidNumber = app.manifest.addons.localstorage.ftp.uid; - users.getByUsername(username, function (error, user) { + const [userGetError, user] = await users.getByUsername(username); + if (userGetError) return next(new ldap.OperationsError(userGetError.toString())); + if (!user) return next(new ldap.OperationsError('Invalid username')); + + 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')); - if (req.requireAdmin && users.compareRoles(user.role, users.ROLE_ADMIN) < 0) return next(new ldap.InsufficientAccessRightsError('Insufficient previleges')); + var obj = { + dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(), + attributes: { + homeDirectory: app.dataDir ? `/mnt/${app.id}` : `/mnt/appsdata/${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 + } + }; - 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: app.dataDir ? `/mnt/${app.id}` : `/mnt/appsdata/${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 - } - }; - - finalSend([ obj ], req, res, next); - }); + finalSend([ obj ], req, res, next); }); }); } @@ -696,21 +684,20 @@ function authenticateMailAddon(req, res, next) { 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) { + mailboxdb.getMailbox(parts[0], parts[1], async 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)); if (!mailbox.active) return next(new ldap.NoSuchObjectError(req.dn.toString())); - verifyMailboxPassword(mailbox, req.credentials || '', async 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)); + const [verifyError, result] = await safe(verifyMailboxPassword(mailbox, req.credentials || '')); + if (verifyError && verifyError.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString())); + if (verifyError && verifyError.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString())); + if (verifyError) return next(new ldap.OperationsError(verifyError.message)); - eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) }); + eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) }); - res.end(); - }); + res.end(); }); }); }); diff --git a/src/mail.js b/src/mail.js index ed759ccb5..6d4a23331 100644 --- a/src/mail.js +++ b/src/mail.js @@ -750,26 +750,20 @@ function restartMail(callback) { }); } -function restartMailIfActivated(callback) { - assert.strictEqual(typeof callback, 'function'); +async function restartMailIfActivated() { + const activated = await users.isActivated(); - users.isActivated(function (error, activated) { - if (error) return callback(error); + if (!activated) { + debug('restartMailIfActivated: skipping restart of mail container since Cloudron is not activated yet'); + return; // not provisioned yet, do not restart container after dns setup + } - if (!activated) { - debug('restartMailIfActivated: skipping restart of mail container since Cloudron is not activated yet'); - return callback(); // not provisioned yet, do not restart container after dns setup - } - - restartMail(callback); - }); + await util.promisify(restartMail)(); } -function handleCertChanged(callback) { - assert.strictEqual(typeof callback, 'function'); - +async function handleCertChanged() { debug('handleCertChanged: will restart if activated'); - restartMailIfActivated(callback); + await restartMailIfActivated(); } async function getDomain(domain) { @@ -998,12 +992,13 @@ function changeLocation(auditSource, progressCallback, callback) { progressCallback({ percent: progress, message: `Updated DNS of ${domainObject.domain}: ${error ? error.message : 'success'}` }); iteratorDone(); }); - }, function (error) { + }, async function (error) { if (error) return callback(error); progressCallback({ percent: 90, message: 'Restarting mail server' }); - restartMailIfActivated(callback); + [error] = await safe(restartMailIfActivated()); + callback(error); }); }); }); diff --git a/src/mailer.js b/src/mailer.js index 669d18a48..c53a99681 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -215,7 +215,7 @@ function backupFailed(mailTo, errorMessage, logUrl, callback) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof errorMessage, 'string'); assert.strictEqual(typeof logUrl, 'string'); - assert.strictEqual(typeof callback, 'function'); + assert(typeof callback === 'undefined' || typeof callback ==='function'); getMailConfig(function (error, mailConfig) { if (error) return debug('Error getting mail details:', error); @@ -235,7 +235,7 @@ function certificateRenewalError(mailTo, domain, message, callback) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof domain, 'string'); assert.strictEqual(typeof message, 'string'); - assert.strictEqual(typeof callback, 'function'); + assert(typeof callback === 'undefined' || typeof callback ==='function'); getMailConfig(function (error, mailConfig) { if (error) return debug('Error getting mail details:', error); diff --git a/src/notifications.js b/src/notifications.js index 7709828af..e0eb63466 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -22,7 +22,6 @@ exports = module.exports = { }; const assert = require('assert'), - async = require('async'), auditSource = require('./auditsource.js'), BoxError = require('./boxerror.js'), changelog = require('./changelog.js'), @@ -178,21 +177,12 @@ async function certificateRenewalError(eventId, vhost, errorMessage) { assert.strictEqual(typeof vhost, 'string'); assert.strictEqual(typeof errorMessage, 'string'); - return new Promise((resolve, reject) => { - users.getAdmins(function (error, admins) { - if (error) return reject(error); + await add(eventId, `Certificate renewal of ${vhost} failed`, `Failed to renew certs of ${vhost}: ${errorMessage}. Renewal will be retried in 12 hours.`); - async.eachSeries(admins, function (admin, iteratorDone) { - mailer.certificateRenewalError(admin.email, vhost, errorMessage, iteratorDone); - }, async function (error) { - if (error) return reject(error); - - await add(eventId, `Certificate renewal of ${vhost} failed`, `Failed to renew certs of ${vhost}: ${errorMessage}. Renewal will be retried in 12 hours.`); - - resolve(); - }); - }); - }); + const admins = await users.getAdmins(); + for (const admin of admins) { + mailer.certificateRenewalError(admin.email, vhost, errorMessage); + } } async function backupFailed(eventId, taskId, errorMessage) { @@ -211,19 +201,10 @@ async function backupFailed(eventId, taskId, errorMessage) { } if (count !== 3) return; // less than 3 failures - return new Promise((resolve, reject) => { - users.getSuperadmins(function (error, superadmins) { - if (error) return reject(error); - - async.eachSeries(superadmins, function (superadmin, iteratorDone) { - mailer.backupFailed(superadmin.email, errorMessage, `${settings.dashboardOrigin()}/logs.html?taskId=${taskId}`, iteratorDone); - }, function (error) { - if (error) return reject(error); - - resolve(); - }); - }); - }); + const superadmins = await users.getSuperadmins(); + for (const superadmin of superadmins) { + mailer.backupFailed(superadmin.email, errorMessage, `${settings.dashboardOrigin()}/logs.html?taskId=${taskId}`); + } } // id is unused but nice to search code diff --git a/src/provision.js b/src/provision.js index d52be282b..b3fa07e1c 100644 --- a/src/provision.js +++ b/src/provision.js @@ -28,6 +28,7 @@ const assert = require('assert'), users = require('./users.js'), tld = require('tldjs'), tokens = require('./tokens.js'), + util = require('util'), _ = require('underscore'); const NOOP_CALLBACK = function (error) { if (error) debug(error); }; @@ -82,7 +83,7 @@ function setup(dnsConfig, sysinfoConfig, auditSource, callback) { callback(error); } - users.isActivated(function (error, activated) { + util.callbackify(users.isActivated)(function (error, activated) { if (error) return done(error); if (activated) return done(new BoxError(BoxError.CONFLICT, 'Already activated', { activate: true })); @@ -127,36 +128,32 @@ function setup(dnsConfig, sysinfoConfig, auditSource, callback) { }); } -function activate(username, password, email, displayName, ip, auditSource, callback) { +async function activate(username, password, email, displayName, ip, auditSource) { assert.strictEqual(typeof username, 'string'); assert.strictEqual(typeof password, 'string'); assert.strictEqual(typeof email, 'string'); assert.strictEqual(typeof displayName, 'string'); assert.strictEqual(typeof ip, 'string'); assert.strictEqual(typeof auditSource, 'object'); - assert.strictEqual(typeof callback, 'function'); - debug('activating user:%s email:%s', username, email); + debug(`activate: user: ${username} email:${email}`); - users.createOwner(username, password, email, displayName, auditSource, async function (error, userObject) { - if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(new BoxError(BoxError.CONFLICT, 'Already activated')); - if (error) return callback(error); + const [error, userObject] = await safe(users.createOwner(email, username, password, displayName, auditSource)); + if (error && error.reason === BoxError.ALREADY_EXISTS) throw new BoxError(BoxError.CONFLICT, 'Already activated'); + if (error) throw error; - const token = { clientId: tokens.ID_WEBADMIN, identifier: userObject.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS }; - let result; - [error, result] = await safe(tokens.add(token)); - if (error) return callback(error); + const token = { clientId: tokens.ID_WEBADMIN, identifier: userObject.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS }; + const result = await tokens.add(token); - eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { }); + eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, {}); - callback(null, { - userId: userObject.id, - token: result.accessToken, - expires: result.expires - }); + setImmediate(cloudron.onActivated.bind(null, {}, NOOP_CALLBACK)); - setImmediate(cloudron.onActivated.bind(null, {}, NOOP_CALLBACK)); // hack for now to not block the above http response - }); + return { + userId: userObject.id, + token: result.accessToken, + expires: result.expires + }; } function restore(backupConfig, backupId, version, sysinfoConfig, options, auditSource, callback) { @@ -181,7 +178,7 @@ function restore(backupConfig, backupId, version, sysinfoConfig, options, auditS callback(error); } - users.isActivated(async function (error, activated) { + util.callbackify(users.isActivated)(async function (error, activated) { if (error) return done(error); if (activated) return done(new BoxError(BoxError.CONFLICT, 'Already activated. Restore with a fresh Cloudron installation.')); @@ -248,7 +245,7 @@ function restore(backupConfig, backupId, version, sysinfoConfig, options, auditS function getStatus(callback) { assert.strictEqual(typeof callback, 'function'); - users.isActivated(function (error, activated) { + util.callbackify(users.isActivated)(function (error, activated) { if (error) return callback(error); settings.getAll(function (error, allSettings) { diff --git a/src/proxyauth.js b/src/proxyauth.js index 4b3a89197..12046603a 100644 --- a/src/proxyauth.js +++ b/src/proxyauth.js @@ -53,19 +53,17 @@ function basicAuthVerify(req, res, next) { const credentials = basicAuth(req); if (!appId || !credentials) return next(); - const api = credentials.name.indexOf('@') !== -1 ? users.verifyWithEmail : users.verifyWithUsername; - - apps.get(appId, function (error, app) { + apps.get(appId, async function (error, app) { if (error) return next(new HttpError(503, error.message)); if (!app.manifest.addons.proxyAuth.basicAuth) return next(); - api(credentials.name, credentials.pass, appId, function (error, user) { - if (error) return next(new HttpError(403, 'Invalid username or password' )); + const verifyFunc = credentials.name.indexOf('@') !== -1 ? users.verifyWithEmail : users.verifyWithUsername; + const [verifyError, user] = await safe(verifyFunc(credentials.name, credentials.pass, appId)); + if (verifyError) return next(new HttpError(403, 'Invalid username or password' )); - req.user = user; - next(); - }); + req.user = user; + next(); }); } @@ -136,7 +134,7 @@ function auth(req, res, next) { } // endpoint called by login page, username and password posted as JSON body -function passwordAuth(req, res, next) { +async function passwordAuth(req, res, next) { assert.strictEqual(typeof req.body, 'object'); const appId = req.headers['x-app-id'] || ''; @@ -148,21 +146,19 @@ function passwordAuth(req, res, next) { const { username, password, totpToken } = req.body; - const api = username.indexOf('@') !== -1 ? users.verifyWithEmail : users.verifyWithUsername; + const verifyFunc = username.indexOf('@') !== -1 ? users.verifyWithEmail : users.verifyWithUsername; + const [error, user] = await safe(verifyFunc(username, password, appId)); + if (error) return next(new HttpError(403, 'Invalid username or password' )); - api(username, password, appId, function (error, user) { - if (error) return next(new HttpError(403, 'Invalid username or password' )); + if (!user.ghost && !user.appPassword && user.twoFactorAuthenticationEnabled) { + if (!totpToken) return next(new HttpError(403, 'A totpToken must be provided')); - if (!user.ghost && !user.appPassword && user.twoFactorAuthenticationEnabled) { - if (!totpToken) return next(new HttpError(403, 'A totpToken must be provided')); + let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 }); + if (!verified) return next(new HttpError(403, 'Invalid totpToken')); + } - let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 }); - if (!verified) return next(new HttpError(403, 'Invalid totpToken')); - } - - req.user = user; - next(); - }); + req.user = user; + next(); } function authorize(req, res, next) { diff --git a/src/reverseproxy.js b/src/reverseproxy.js index 7754ebc77..1da2e60e3 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -66,34 +66,29 @@ function nginxLocation(s) { return `~ ^(?!(${re.slice(1)}))`; // negative regex assertion - https://stackoverflow.com/questions/16302897/nginx-location-not-equal-to-regex } -function getAcmeApi(domainObject, callback) { +async function getAcmeApi(domainObject) { assert.strictEqual(typeof domainObject, 'object'); - assert.strictEqual(typeof callback, 'function'); - const api = acme2; + const acmeApi = acme2; - let options = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' }; - options.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod' - options.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null; - options.wildcard = !!domainObject.tlsConfig.wildcard; + let apiOptions = { prod: false, performHttpAuthorization: false, wildcard: false, email: '' }; + apiOptions.prod = domainObject.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod' + apiOptions.performHttpAuthorization = domainObject.provider.match(/noop|manual|wildcard/) !== null; + apiOptions.wildcard = !!domainObject.tlsConfig.wildcard; // registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197) // we cannot use admin@fqdn because the user might not have set it up. // we simply update the account with the latest email we have each time when getting letsencrypt certs // https://github.com/ietf-wg-acme/acme/issues/30 - users.getOwner(function (error, owner) { - options.email = error ? 'webmaster@cloudron.io' : owner.email; // can error if not activated yet + const [error, owner] = await safe(users.getOwner()); + apiOptions.email = error ? 'webmaster@cloudron.io' : owner.email; // can error if not activated yet - const blobGet = util.callbackify(blobs.get); - blobGet(blobs.ACME_ACCOUNT_KEY, function (error, accountKeyPem) { - if (error) return callback(error); - if (!accountKeyPem) return callback(new BoxError(BoxError.NOT_FOUND, 'acme account key not found')); + const accountKeyPem = await blobs.get(blobs.ACME_ACCOUNT_KEY); + if (!accountKeyPem) throw new BoxError(BoxError.NOT_FOUND, 'acme account key not found'); - options.accountKeyPem = accountKeyPem; + apiOptions.accountKeyPem = accountKeyPem; - callback(null, api, options); - }); - }); + return { acmeApi, apiOptions }; } function getExpiryDate(certFilePath) { @@ -417,43 +412,41 @@ function ensureCertificate(vhost, domain, auditSource, callback) { return callback(null, getFallbackCertificatePathSync(domain), { renewed: false }); } - getAcmeApi(domainObject, async function (error, acmeApi, apiOptions) { - if (error) return callback(error); - let notAfter = null; + const { acmeApi, apiOptions } = getAcmeApi(domainObject); + let notAfter = null; - const [, currentBundle] = await safe(checkAcmeCertificate(vhost, domainObject)); - if (currentBundle) { - debug(`ensureCertificate: ${vhost} certificate already exists at ${currentBundle.keyFilePath}`); - notAfter = getExpiryDate(currentBundle.certFilePath); - const isExpiring = (notAfter - new Date()) <= (30 * 24 * 60 * 60 * 1000); // expiring in a month - if (!isExpiring && providerMatchesSync(domainObject, currentBundle.certFilePath, apiOptions)) return callback(null, currentBundle, { renewed: false }); - debug(`ensureCertificate: ${vhost} cert requires renewal`); - } else { - debug(`ensureCertificate: ${vhost} cert does not exist`); + const [, currentBundle] = await safe(checkAcmeCertificate(vhost, domainObject)); + if (currentBundle) { + debug(`ensureCertificate: ${vhost} certificate already exists at ${currentBundle.keyFilePath}`); + notAfter = getExpiryDate(currentBundle.certFilePath); + const isExpiring = (notAfter - new Date()) <= (30 * 24 * 60 * 60 * 1000); // expiring in a month + if (!isExpiring && providerMatchesSync(domainObject, currentBundle.certFilePath, apiOptions)) return callback(null, currentBundle, { renewed: false }); + debug(`ensureCertificate: ${vhost} cert requires renewal`); + } else { + debug(`ensureCertificate: ${vhost} cert does not exist`); + } + + debug('ensureCertificate: getting certificate for %s with options %j', vhost, _.omit(apiOptions, 'accountKeyPem')); + + const acmePaths = getAcmeCertificatePathSync(vhost, domainObject); + acmeApi.getCertificate(vhost, domain, acmePaths, apiOptions, async function (error) { + debug(`ensureCertificate: error: ${error ? error.message : 'null'} cert: ${acmePaths.certFilePath || 'null'}`); + + await safe(eventlog.add(currentBundle ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: vhost, errorMessage: error ? error.message : '', notAfter })); + + if (error && currentBundle && (notAfter - new Date() > 0)) { // still some life left in this certificate + debug('ensureCertificate: continue using existing bundle since renewal failed'); + return callback(null, currentBundle, { renewed: false }); } - debug('ensureCertificate: getting certificate for %s with options %j', vhost, _.omit(apiOptions, 'accountKeyPem')); + if (!error) { + [error] = await safe(updateCertBlobs(vhost, domainObject)); + if (!error) return callback(null, { certFilePath: acmePaths.certFilePath, keyFilePath: acmePaths.keyFilePath }, { renewed: true }); + } - const acmePaths = getAcmeCertificatePathSync(vhost, domainObject); - acmeApi.getCertificate(vhost, domain, acmePaths, apiOptions, async function (error) { - debug(`ensureCertificate: error: ${error ? error.message : 'null'} cert: ${acmePaths.certFilePath || 'null'}`); + debug(`ensureCertificate: renewal of ${vhost} failed. using fallback certificates for ${domain}`); - await safe(eventlog.add(currentBundle ? eventlog.ACTION_CERTIFICATE_RENEWAL : eventlog.ACTION_CERTIFICATE_NEW, auditSource, { domain: vhost, errorMessage: error ? error.message : '', notAfter })); - - if (error && currentBundle && (notAfter - new Date() > 0)) { // still some life left in this certificate - debug('ensureCertificate: continue using existing bundle since renewal failed'); - return callback(null, currentBundle, { renewed: false }); - } - - if (!error) { - [error] = await safe(updateCertBlobs(vhost, domainObject)); - if (!error) return callback(null, { certFilePath: acmePaths.certFilePath, keyFilePath: acmePaths.keyFilePath }, { renewed: true }); - } - - debug(`ensureCertificate: renewal of ${vhost} failed. using fallback certificates for ${domain}`); - - callback(null, getFallbackCertificatePathSync(domain), { renewed: false }); - }); + callback(null, getFallbackCertificatePathSync(domain), { renewed: false }); }); }); } @@ -737,7 +730,7 @@ function renewCerts(options, auditSource, progressCallback, callback) { if (renewed.length === 0) return callback(null); async.series([ - (next) => { if (renewed.includes(settings.mailFqdn())) mail.handleCertChanged(next); else next(); }, // mail cert renewed + async () => { if (renewed.includes(settings.mailFqdn())) await mail.handleCertChanged(); }, // mail cert renewed reload, // reload nginx if any certs were updated but the config was not rewritten (next) => { // restart tls apps on cert change const tlsApps = allApps.filter(app => app.manifest.addons && app.manifest.addons.tls && renewed.includes(app.fqdn)); diff --git a/src/routes/accesscontrol.js b/src/routes/accesscontrol.js index 7aac11e8d..79fcd5565 100644 --- a/src/routes/accesscontrol.js +++ b/src/routes/accesscontrol.js @@ -17,7 +17,7 @@ const accesscontrol = require('../accesscontrol.js'), speakeasy = require('speakeasy'), users = require('../users.js'); -function passwordAuth(req, res, next) { +async function passwordAuth(req, res, next) { assert.strictEqual(typeof req.body, 'object'); if (!req.body.username || typeof req.body.username !== 'string') return next(new HttpError(400, 'A username must be non-empty string')); @@ -26,56 +26,29 @@ function passwordAuth(req, res, next) { const { username, password, totpToken } = req.body; - function check2FA(user) { - assert.strictEqual(typeof user, 'object'); + const verifyFunc = username.indexOf('@') === -1 ? users.verifyWithUsername : users.verifyWithEmail; - if (!user.ghost && !user.appPassword && user.twoFactorAuthenticationEnabled) { - if (!totpToken) return next(new HttpError(401, 'A totpToken must be provided')); + let [error, user] = await safe(verifyFunc(username, password, users.AP_WEBADMIN)); + if (error && error.reason === BoxError.NOT_FOUND) { + [error, user] = await safe(externalLdap.maybeCreateUser(username.toLowerCase(), password)); + if (error) return next(new HttpError(401, 'Unauthorized')); + [error] = await safe(externalLdap.verifyPassword(user)); + if (error) return next(new HttpError(401, 'Unauthorized')); + } + if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized')); + if (error) return next(new HttpError(500, error)); + if (!user) return next(new HttpError(401, 'Unauthorized')); - let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: totpToken, window: 2 }); - if (!verified) return next(new HttpError(401, 'Invalid totpToken')); - } + if (!user.ghost && !user.appPassword && user.twoFactorAuthenticationEnabled) { + if (!totpToken) return next(new HttpError(401, 'A totpToken must be provided')); - req.user = user; - - next(); + const verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: totpToken, window: 2 }); + if (!verified) return next(new HttpError(401, 'Invalid totpToken')); } - function createAndVerifyUserIfNotExist(identifier, password) { - assert.strictEqual(typeof identifier, 'string'); - assert.strictEqual(typeof password, 'string'); + req.user = user; - externalLdap.createAndVerifyUserIfNotExist(identifier.toLowerCase(), password, function (error, result) { - if (error && error.reason === BoxError.BAD_STATE) return next(new HttpError(401, 'Unauthorized')); - if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(401, 'Unauthorized')); - if (error && error.reason === BoxError.CONFLICT) return next(new HttpError(401, 'Unauthorized')); - if (error && error.reason === BoxError.NOT_FOUND) return next(new HttpError(401, 'Unauthorized')); - if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized')); - if (error) return next(new HttpError(500, error)); - - check2FA(result); - }); - } - - if (username.indexOf('@') === -1) { - users.verifyWithUsername(username, password, users.AP_WEBADMIN, function (error, result) { - if (error && error.reason === BoxError.NOT_FOUND) return createAndVerifyUserIfNotExist(username, password); - if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized')); - if (error) return next(new HttpError(500, error)); - if (!result) return next(new HttpError(401, 'Unauthorized')); - - check2FA(result); - }); - } else { - users.verifyWithEmail(username, password, users.AP_WEBADMIN, function (error, result) { - if (error && error.reason === BoxError.NOT_FOUND) return createAndVerifyUserIfNotExist(username, password); - if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized')); - if (error) return next(new HttpError(500, error)); - if (!result) return next(new HttpError(401, 'Unauthorized')); - - check2FA(result); - }); - } + next(); } async function tokenAuth(req, res, next) { diff --git a/src/routes/cloudron.js b/src/routes/cloudron.js index cdf8047d8..8c91a11c8 100644 --- a/src/routes/cloudron.js +++ b/src/routes/cloudron.js @@ -51,7 +51,7 @@ async function login(req, res, next) { const type = req.body.type || tokens.ID_WEBADMIN; const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null; const userAgent = req.headers['user-agent'] || ''; - const auditSource = { authType: 'basic', ip: ip }; + const auditSource = { authType: 'basic', ip }; let error = tokens.validateTokenType(type); if (error) return next(new HttpError(400, error.message)); @@ -62,7 +62,7 @@ async function login(req, res, next) { eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user) }); - users.checkLoginLocation(req.user, ip, userAgent); + await safe(users.notifyLoginLocation(req.user, ip, userAgent, auditSource)); next(new HttpSuccess(200, token)); } @@ -76,44 +76,42 @@ async function logout(req, res) { res.redirect('/login.html'); } -function passwordResetRequest(req, res, next) { +async function passwordResetRequest(req, res, next) { if (!req.body.identifier || typeof req.body.identifier !== 'string') return next(new HttpError(401, 'A identifier must be non-empty string')); - users.sendPasswordResetByIdentifier(req.body.identifier, function (error) { - if (error && error.reason !== BoxError.NOT_FOUND) return next(BoxError.toHttpError(error)); + const [error] = await safe(users.sendPasswordResetByIdentifier(req.body.identifier, auditSource.fromRequest(req))); + if (error && error.reason !== BoxError.NOT_FOUND) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, {})); - }); + next(new HttpSuccess(202, {})); } -function passwordReset(req, res, next) { +async function passwordReset(req, res, next) { assert.strictEqual(typeof req.body, 'object'); if (typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'Missing resetToken')); if (typeof req.body.password !== 'string') return next(new HttpError(400, 'Missing password')); - users.getByResetToken(req.body.resetToken, function (error, userObject) { - if (error) return next(new HttpError(401, 'Invalid resetToken')); + let [error, userObject] = await safe(users.getByResetToken(req.body.resetToken)); + if (error) return next(new HttpError(401, 'Invalid resetToken')); + if (!userObject) return next(new HttpError(401, 'Invalid resetToken')); - // if you fix the duration here, the emails and UI have to be fixed as well - if (Date.now() - userObject.resetTokenCreationTime > 7 * 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired')); - if (!userObject.username) return next(new HttpError(409, 'No username set')); + // if you fix the duration here, the emails and UI have to be fixed as well + if (Date.now() - userObject.resetTokenCreationTime > 7 * 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired')); + if (!userObject.username) return next(new HttpError(409, 'No username set')); - // setPassword clears the resetToken - users.setPassword(userObject, req.body.password, async function (error) { - if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message)); - if (error) return next(BoxError.toHttpError(error)); + // setPassword clears the resetToken + [error] = await safe(users.setPassword(userObject, req.body.password, auditSource.fromRequest(req))); + if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message)); + if (error) return next(BoxError.toHttpError(error)); - let result; - [error, result] = await safe(tokens.add({ clientId: tokens.ID_WEBADMIN, identifier: userObject.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS })); - if (error) return next(BoxError.toHttpError(error)); + let result; + [error, result] = await safe(tokens.add({ clientId: tokens.ID_WEBADMIN, identifier: userObject.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS })); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, { accessToken: result.accessToken })); - }); - }); + next(new HttpSuccess(202, { accessToken: result.accessToken })); } -function setupAccount(req, res, next) { +async function setupAccount(req, res, next) { assert.strictEqual(typeof req.body, 'object'); if (!req.body.resetToken || typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'resetToken must be a non-empty string')); @@ -123,18 +121,17 @@ function setupAccount(req, res, next) { if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a non-empty string')); if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be a non-empty string')); - users.getByResetToken(req.body.resetToken, function (error, userObject) { - if (error) return next(new HttpError(401, 'Invalid Reset Token')); + const [error, userObject] = await safe(users.getByResetToken(req.body.resetToken)); + if (error) return next(new HttpError(401, 'Invalid resetToken')); + if (!userObject) return next(new HttpError(401, 'Invalid resetToken')); - // if you fix the duration here, the emails and UI have to be fixed as well - if (Date.now() - userObject.resetTokenCreationTime > 7 * 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired')); + // if you fix the duration here, the emails and UI have to be fixed as well + if (Date.now() - userObject.resetTokenCreationTime > 7 * 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired')); - users.setupAccount(userObject, req.body, auditSource.fromRequest(req), function (error, accessToken) { - if (error) return next(BoxError.toHttpError(error)); + const [setupAccountError, accessToken] = await safe(users.setupAccount(userObject, req.body, auditSource.fromRequest(req))); + if (setupAccountError) return next(BoxError.toHttpError(setupAccountError)); - next(new HttpSuccess(201, { accessToken })); - }); - }); + next(new HttpSuccess(201, { accessToken })); } async function reboot(req, res, next) { diff --git a/src/routes/profile.js b/src/routes/profile.js index e848d4ccc..96b0c1171 100644 --- a/src/routes/profile.js +++ b/src/routes/profile.js @@ -6,7 +6,7 @@ exports = module.exports = { update, getAvatar, setAvatar, - changePassword, + setPassword, setTwoFactorAuthenticationSecret, enableTwoFactorAuthentication, disableTwoFactorAuthentication, @@ -55,7 +55,7 @@ async function get(req, res, next) { })); } -function update(req, res, next) { +async function update(req, res, next) { assert.strictEqual(typeof req.user, 'object'); assert.strictEqual(typeof req.body, 'object'); @@ -63,13 +63,12 @@ function update(req, res, next) { if ('fallbackEmail' in req.body && typeof req.body.fallbackEmail !== 'string') return next(new HttpError(400, 'fallbackEmail must be string')); if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string')); - var data = _.pick(req.body, 'email', 'fallbackEmail', 'displayName'); + const data = _.pick(req.body, 'email', 'fallbackEmail', 'displayName'); - users.update(req.user, data, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(users.update(req.user, data, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(204)); - }); + next(new HttpSuccess(204)); } async function setAvatar(req, res, next) { @@ -99,48 +98,44 @@ async function getAvatar(req, res, next) { res.send(avatar); } -function changePassword(req, res, next) { +async function setPassword(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.user, 'object'); if (typeof req.body.newPassword !== 'string') return next(new HttpError(400, 'newPassword must be a string')); - users.setPassword(req.user, req.body.newPassword, function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(users.setPassword(req.user, req.body.newPassword, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(204)); - }); + next(new HttpSuccess(204)); } -function setTwoFactorAuthenticationSecret(req, res, next) { +async function setTwoFactorAuthenticationSecret(req, res, next) { assert.strictEqual(typeof req.user, 'object'); - users.setTwoFactorAuthenticationSecret(req.user.id, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(users.setTwoFactorAuthenticationSecret(req.user.id, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(201, { secret: result.secret, qrcode: result.qrcode })); - }); + next(new HttpSuccess(201, { secret: result.secret, qrcode: result.qrcode })); } -function enableTwoFactorAuthentication(req, res, next) { +async function enableTwoFactorAuthentication(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.user, 'object'); if (!req.body.totpToken || typeof req.body.totpToken !== 'string') return next(new HttpError(400, 'totpToken must be a nonempty string')); - users.enableTwoFactorAuthentication(req.user.id, req.body.totpToken, function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(users.enableTwoFactorAuthentication(req.user.id, req.body.totpToken, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, {})); - }); + next(new HttpSuccess(202, {})); } -function disableTwoFactorAuthentication(req, res, next) { +async function disableTwoFactorAuthentication(req, res, next) { assert.strictEqual(typeof req.user, 'object'); - users.disableTwoFactorAuthentication(req.user.id, function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(users.disableTwoFactorAuthentication(req.user.id, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(202, {})); - }); + next(new HttpSuccess(202, {})); } diff --git a/src/routes/provision.js b/src/routes/provision.js index 636c4dd55..2c2de5d98 100644 --- a/src/routes/provision.js +++ b/src/routes/provision.js @@ -12,7 +12,6 @@ exports = module.exports = { const assert = require('assert'), auditSource = require('../auditsource.js'), BoxError = require('../boxerror.js'), - debug = require('debug')('box:routes/setup'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, paths = require('../paths.js'), @@ -80,7 +79,7 @@ function setup(req, res, next) { }); } -function activate(req, res, next) { +async function activate(req, res, next) { assert.strictEqual(typeof req.body, 'object'); if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string')); @@ -88,19 +87,15 @@ function activate(req, res, next) { if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string')); if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string')); - var username = req.body.username; - var password = req.body.password; - var email = req.body.email; - var displayName = req.body.displayName || ''; + const { username, password, email } = req.body; + const displayName = req.body.displayName || ''; - var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; - debug('activate: username:%s ip:%s', username, ip); + const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; - provision.activate(username, password, email, displayName, ip, auditSource.fromRequest(req), function (error, info) { - if (error) return next(BoxError.toHttpError(error)); + const [error, info] = await safe(provision.activate(username, password, email, displayName, ip, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(201, info)); - }); + next(new HttpSuccess(201, info)); } function restore(req, res, next) { diff --git a/src/routes/test/profile-test.js b/src/routes/test/profile-test.js index b67da5f24..586182bbb 100644 --- a/src/routes/test/profile-test.js +++ b/src/routes/test/profile-test.js @@ -202,14 +202,11 @@ describe('Profile API', function () { }); it('can enable 2fa', async function () { - const totpToken = speakeasy.totp({ - secret: secret, - encoding: 'base32' - }); + const totpToken = speakeasy.totp({ secret, encoding: 'base32' }); await superagent.post(`${serverUrl}/api/v1/profile/twofactorauthentication_enable`) .query({ access_token: user.token }) - .send({ totpToken: totpToken }); + .send({ totpToken }); }); it('fails due to missing token', async function () { diff --git a/src/routes/test/users-test.js b/src/routes/test/users-test.js index 6c923f8ba..771e47e59 100644 --- a/src/routes/test/users-test.js +++ b/src/routes/test/users-test.js @@ -159,11 +159,8 @@ describe('Users API', function () { userWithPassword.id = response.body.id; }); - it('did set password of created user', function (done) { - users.verify(userWithPassword.id, userWithPassword.password, users.AP_WEBADMIN, function (error) { - expect(error).to.be(null); - done(); - }); + it('did set password of created user', async function () { + await users.verify(userWithPassword.id, userWithPassword.password, users.AP_WEBADMIN); }); }); @@ -398,11 +395,8 @@ describe('Users API', function () { expect(response.statusCode).to.equal(204); }); - it('did change the user password', function (done) { - users.verify(user.id, 'bigenough', users.AP_WEBADMIN, function (error) { - expect(error).to.be(null); - done(); - }); + it('did change the user password', async function () { + await users.verify(user.id, 'bigenough', users.AP_WEBADMIN); }); }); diff --git a/src/routes/users.js b/src/routes/users.js index 12cf21909..b0db67201 100644 --- a/src/routes/users.js +++ b/src/routes/users.js @@ -4,9 +4,9 @@ exports = module.exports = { get, update, list, - create, + add, del, - changePassword, + setPassword, verifyPassword, createInvite, sendInvite, @@ -21,24 +21,24 @@ exports = module.exports = { const assert = require('assert'), auditSource = require('../auditsource.js'), BoxError = require('../boxerror.js'), + groups = require('../groups.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, safe = require('safetydance'), users = require('../users.js'); -function load(req, res, next) { +async function load(req, res, next) { assert.strictEqual(typeof req.params.userId, 'string'); - users.get(req.params.userId, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(users.get(req.params.userId)); + if (error) return next(BoxError.toHttpError(error)); + if (!result) return next(new HttpError(404, 'User not found')); + req.resource = result; - req.resource = result; - - next(); - }); + next(); } -function create(req, res, next) { +async function add(req, res, next) { assert.strictEqual(typeof req.body, 'object'); if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string')); @@ -50,19 +50,18 @@ function create(req, res, next) { if (users.compareRoles(req.user.role, req.body.role) < 0) return next(new HttpError(403, `role '${req.body.role}' is required but you are only '${req.user.role}'`)); } - var password = req.body.password || null; - var email = req.body.email; - var username = 'username' in req.body ? req.body.username : null; - var displayName = req.body.displayName || ''; + const password = req.body.password || null; + const email = req.body.email; + const username = 'username' in req.body ? req.body.username : null; + const displayName = req.body.displayName || ''; - users.create(username, password, email, displayName, { invitor: req.user, role: req.body.role || users.ROLE_USER }, auditSource.fromRequest(req), function (error, user) { - if (error) return next(BoxError.toHttpError(error)); + const [error, user] = await safe(users.add(email, { username, password, displayName, invitor: req.user, role: req.body.role || users.ROLE_USER }, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(201, users.removePrivateFields(user))); - }); + next(new HttpSuccess(201, users.removePrivateFields(user))); } -function update(req, res, next) { +async function update(req, res, next) { assert.strictEqual(typeof req.resource, 'object'); assert.strictEqual(typeof req.user, 'object'); assert.strictEqual(typeof req.body, 'object'); @@ -86,29 +85,27 @@ function update(req, res, next) { if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but you are only '${req.user.role}'`)); - users.update(req.resource, req.body, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(users.update(req.resource, req.body, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(204)); - }); + next(new HttpSuccess(204)); } -function list(req, res, next) { - var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1; +async function list(req, res, next) { + const page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1; if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number')); - var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25; + const perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25; if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number')); if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string')); - users.getAllPaged(req.query.search || null, page, perPage, function (error, results) { - if (error) return next(BoxError.toHttpError(error)); + let [error, results] = await safe(users.getAllPaged(req.query.search || null, page, perPage)); + if (error) return next(BoxError.toHttpError(error)); - results = results.map(users.removeRestrictedFields); + results = results.map(users.removeRestrictedFields); - next(new HttpSuccess(200, { users: results })); - }); + next(new HttpSuccess(200, { users: results })); } function get(req, res, next) { @@ -130,30 +127,28 @@ async function del(req, res, next) { next(new HttpSuccess(204)); } -function verifyPassword(req, res, next) { +async function verifyPassword(req, res, next) { assert.strictEqual(typeof req.body, 'object'); if (typeof req.body.password !== 'string') return next(new HttpError(400, 'API call requires user password')); - users.verifyWithUsername(req.user.username, req.body.password, users.AP_WEBADMIN, function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(users.verifyWithUsername(req.user.username, req.body.password, users.AP_WEBADMIN)); + if (error) return next(BoxError.toHttpError(error)); - req.body.password = ''; // this will prevent logs from displaying plain text password + req.body.password = ''; // this will prevent logs from displaying plain text password - next(); - }); + next(); } -function createInvite(req, res, next) { +async function createInvite(req, res, next) { assert.strictEqual(typeof req.resource, 'object'); if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but user has only '${req.user.role}'`)); - users.createInvite(req.resource, function (error, result) { - if (error) return next(BoxError.toHttpError(error)); + const [error, result] = await safe(users.createInvite(req.resource, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, result)); - }); + next(new HttpSuccess(200, result)); } function disableTwoFactorAuthentication(req, res, next) { @@ -168,16 +163,15 @@ function disableTwoFactorAuthentication(req, res, next) { }); } -function sendInvite(req, res, next) { +async function sendInvite(req, res, next) { assert.strictEqual(typeof req.resource, 'object'); if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but user has only '${req.user.role}'`)); - users.sendInvite(req.resource, { invitor: req.user }, function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(users.sendInvite(req.resource, { invitor: req.user })); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(200, { })); - }); + next(new HttpSuccess(200, { })); } async function setGroups(req, res, next) { @@ -187,38 +181,35 @@ async function setGroups(req, res, next) { if (!Array.isArray(req.body.groupIds)) return next(new HttpError(400, 'API call requires a groups array.')); if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but user has only '${req.user.role}'`)); - const [error] = await safe(users.setMembership(req.resource, req.body.groupIds)); + const [error] = await safe(groups.setMembership(req.resource.id, req.body.groupIds)); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(204)); } -function changePassword(req, res, next) { +async function setPassword(req, res, next) { assert.strictEqual(typeof req.body, 'object'); assert.strictEqual(typeof req.resource, 'object'); if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string')); if (users.compareRoles(req.user.role, req.resource.role) < 0) return next(new HttpError(403, `role '${req.resource.role}' is required but user has only '${req.user.role}'`)); - users.setPassword(req.resource, req.body.password, function (error) { - if (error) return next(BoxError.toHttpError(error)); + const [error] = await safe(users.setPassword(req.resource, req.body.password, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(204)); - }); + next(new HttpSuccess(204)); } // This route transfers ownership from token user to user specified in path param -function makeOwner(req, res, next) { +async function makeOwner(req, res, next) { assert.strictEqual(typeof req.resource, 'object'); - // first make new one owner, then devote current one - users.update(req.resource, { role: users.ROLE_OWNER }, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + // first make new one owner, then demote current one + let [error] = await safe(users.update(req.resource, { role: users.ROLE_OWNER }, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - users.update(req.user, { role: users.ROLE_USER }, auditSource.fromRequest(req), function (error) { - if (error) return next(BoxError.toHttpError(error)); + [error] = await safe(users.update(req.user, { role: users.ROLE_USER }, auditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); - next(new HttpSuccess(204)); - }); - }); + next(new HttpSuccess(204)); } diff --git a/src/server.js b/src/server.js index fdc2e3be7..ca32efdca 100644 --- a/src/server.js +++ b/src/server.js @@ -149,7 +149,7 @@ function initializeExpressSync() { router.post('/api/v1/profile', json, token, routes.profile.authorize, routes.profile.update); router.get ('/api/v1/profile/avatar/:identifier', routes.profile.getAvatar); // this is not scoped so it can used directly in img tag router.post('/api/v1/profile/avatar', json, token, (req, res, next) => { return typeof req.body.avatar === 'string' ? next() : multipart(req, res, next); }, routes.profile.setAvatar); // avatar is not exposed in LDAP. so it's personal and not locked - router.post('/api/v1/profile/password', json, token, routes.users.verifyPassword, routes.profile.changePassword); + router.post('/api/v1/profile/password', json, token, routes.users.verifyPassword, routes.profile.setPassword); router.post('/api/v1/profile/twofactorauthentication_secret', json, token, routes.profile.setTwoFactorAuthenticationSecret); router.post('/api/v1/profile/twofactorauthentication_enable', json, token, routes.profile.enableTwoFactorAuthentication); router.post('/api/v1/profile/twofactorauthentication_disable', json, token, routes.users.verifyPassword, routes.profile.disableTwoFactorAuthentication); @@ -168,11 +168,11 @@ function initializeExpressSync() { // user routes router.get ('/api/v1/users', token, authorizeUserManager, routes.users.list); - router.post('/api/v1/users', json, token, authorizeUserManager, routes.users.create); + router.post('/api/v1/users', json, token, authorizeUserManager, routes.users.add); router.get ('/api/v1/users/:userId', token, authorizeUserManager, routes.users.load, routes.users.get); // this is manage scope because it returns non-restricted fields router.del ('/api/v1/users/:userId', token, authorizeUserManager, routes.users.load, routes.users.del); router.post('/api/v1/users/:userId', json, token, authorizeUserManager, routes.users.load, routes.users.update); - router.post('/api/v1/users/:userId/password', json, token, authorizeUserManager, routes.users.load, routes.users.changePassword); + router.post('/api/v1/users/:userId/password', json, token, authorizeUserManager, routes.users.load, routes.users.setPassword); router.put ('/api/v1/users/:userId/groups', json, token, authorizeUserManager, routes.users.load, routes.users.setGroups); router.post('/api/v1/users/:userId/make_owner', json, token, authorizeOwner, routes.users.load, routes.users.makeOwner); router.post('/api/v1/users/:userId/send_invite', json, token, authorizeUserManager, routes.users.load, routes.users.sendInvite); diff --git a/src/tasks.js b/src/tasks.js index 8cca89aed..c8de5c7e2 100644 --- a/src/tasks.js +++ b/src/tasks.js @@ -228,10 +228,11 @@ async function stopTask(id) { } async function stopAllTasks() { - debug('stopTask: stopping all tasks'); + debug('stopAllTasks: stopping all tasks'); gTasks = {}; // this signals startTask() to not set completion status as "crashed" - safe(shell.promises.sudo('stopTask', [ STOP_TASK_CMD, 'all' ], { cwd: paths.baseDir() })); // stop in background, do not wait + const [error] = await safe(shell.promises.sudo('stopTask', [ STOP_TASK_CMD, 'all' ], { cwd: paths.baseDir() })); + if (error) debug(`stopAllTasks: error stopping stasks: ${error.message}`); } async function listByTypePaged(type, page, perPage) { diff --git a/src/test/apppasswords-test.js b/src/test/apppasswords-test.js index 53b574a9c..83e12e2cc 100644 --- a/src/test/apppasswords-test.js +++ b/src/test/apppasswords-test.js @@ -53,56 +53,37 @@ describe('App passwords', function () { expect(results[0].identifier).to.be('appid'); }); - it('can verify app password', function (done) { - users.verify(ADMIN.id, password, 'appid', function (error, result) { - expect(error).to.not.be.ok(); - expect(result).to.be.ok(); - expect(result.appPassword).to.be(true); - - done(); - }); + it('can verify app password', async function () { + const result = await users.verify(ADMIN.id, password, 'appid'); + expect(result).to.be.ok(); + expect(result.appPassword).to.be(true); }); - it('can verify non-app password', function (done) { - users.verify(ADMIN.id, ADMIN.password, 'appid', function (error, result) { - expect(error).to.not.be.ok(); - expect(result).to.be.ok(); - expect(result.appPassword).to.be(undefined); - - done(); - }); + it('can verify non-app password', async function () { + const result = await users.verify(ADMIN.id, ADMIN.password, 'appid'); + expect(result).to.be.ok(); + expect(result.appPassword).to.be(undefined); }); - it('cannot verify bad password', function (done) { - users.verify(ADMIN.id, 'bad', 'appid', function (error, result) { - expect(error).to.be.ok(); - expect(result).to.not.be.ok(); - expect(error.reason).to.be(BoxError.INVALID_CREDENTIALS); - - done(); - }); + it('cannot verify bad password', async function () { + const [error, result] = await safe(users.verify(ADMIN.id, 'bad', 'appid')); + expect(result).to.not.be.ok(); + expect(error.reason).to.be(BoxError.INVALID_CREDENTIALS); }); - it('cannot verify password for another app', function (done) { - users.verify(ADMIN.id, password, 'appid2', function (error, result) { - expect(error).to.be.ok(); - expect(result).to.not.be.ok(); - expect(error.reason).to.be(BoxError.INVALID_CREDENTIALS); - - done(); - }); + it('cannot verify password for another app', async function () { + const [error, result] = await safe(users.verify(ADMIN.id, password, 'appid2')); + expect(result).to.not.be.ok(); + expect(error.reason).to.be(BoxError.INVALID_CREDENTIALS); }); it('can del app password', async function () { await appPasswords.del(id); }); - it('cannot verify deleted app password', function (done) { - users.verify(ADMIN.id, password, 'appid', function (error) { - expect(error).to.be.ok(); - - done(); - }); + it('cannot verify deleted app password', async function () { + const [error] = await safe(users.verify(ADMIN.id, password, 'appid')); + expect(error.reason).to.be(BoxError.INVALID_CREDENTIALS); }); it('cannot del random app password', async function () { diff --git a/src/test/apps-test.js b/src/test/apps-test.js index ce9d4db10..2be3c59a9 100644 --- a/src/test/apps-test.js +++ b/src/test/apps-test.js @@ -13,8 +13,7 @@ const appdb = require('../appdb.js'), common = require('./common.js'), domains = require('../domains.js'), expect = require('expect.js'), - hat = require('../hat.js'), - userdb = require('../userdb.js'); + hat = require('../hat.js'); let AUDIT_SOURCE = { ip: '1.2.3.4' }; diff --git a/src/test/common.js b/src/test/common.js index 96069a824..161877578 100644 --- a/src/test/common.js +++ b/src/test/common.js @@ -18,7 +18,6 @@ const appdb = require('../appdb.js'), settings = require('../settings.js'), settingsdb = require('../settingsdb.js'), tasks = require('../tasks.js'), - userdb = require('../userdb.js'), users = require('../users.js'); const MANIFEST = { @@ -168,18 +167,16 @@ function setup(done) { settings.initCache, blobs.initSecrets, domains.add.bind(null, DOMAIN.domain, DOMAIN, AUDIT_SOURCE), - function createOwner(done) { - users.createOwner(ADMIN.username, ADMIN.password, ADMIN.email, ADMIN.displayName, AUDIT_SOURCE, function (error, result) { - if (error) return done(error); - ADMIN.id = result.id; - done(); - }); + async function createOwner() { + const result = await users.createOwner(ADMIN.email, ADMIN.username, ADMIN.password, ADMIN.displayName, AUDIT_SOURCE); + ADMIN.id = result.id; }, appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.domain, APP.portBindings, APP), settingsdb.set.bind(null, settings.CLOUDRON_TOKEN_KEY, exports.APPSTORE_TOKEN), // appstore token - userdb.add.bind(null, USER.id, USER), - users.setPassword.bind(null, USER, USER.password), - + async function createUser() { + const result = await users.add(USER.email, USER, AUDIT_SOURCE); + USER.id = result.id; + }, (done) => mailboxdb.addMailbox(exports.MAILBOX_NAME, DOMAIN.domain, { ownerId: USER.id, ownerType: mail.OWNERTYPE_USER, active: true }, done), (done) => mailboxdb.setAliasesForName(exports.MAILBOX_NAME, DOMAIN.domain, [ { name: exports.ALIAS_NAME, domain: DOMAIN.domain} ], done), diff --git a/src/test/database-test.js b/src/test/database-test.js index 629255237..8c661fa73 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -9,71 +9,14 @@ const appdb = require('../appdb.js'), apps = require('../apps.js'), async = require('async'), BoxError = require('../boxerror.js'), - constants = require('../constants.js'), database = require('../database'), domaindb = require('../domaindb'), expect = require('expect.js'), - hat = require('../hat.js'), mailboxdb = require('../mailboxdb.js'), reverseProxy = require('../reverseproxy.js'), settingsdb = require('../settingsdb.js'), - userdb = require('../userdb.js'), _ = require('underscore'); -var USER_0 = { - id: 'uuid0', - username: 'uuid0', - password: 'secret', - email: 'safe@me.com', - fallbackEmail: 'safer@me.com', - salt: 'morton', - resetToken: hat(256), - displayName: '', - twoFactorAuthenticationEnabled: false, - twoFactorAuthenticationSecret: '', - role: 'user', - active: true, - source: '', - loginLocations: [], - avatar: constants.AVATAR_GRAVATAR -}; - -var USER_1 = { - id: 'uuid1', - username: 'uuid1', - password: 'secret', - email: 'safe2@me.com', - fallbackEmail: 'safer2@me.com', - salt: 'tata', - resetToken: '', - displayName: 'Herbert 1', - twoFactorAuthenticationEnabled: false, - twoFactorAuthenticationSecret: '', - role: 'user', - active: true, - source: '', - loginLocations: [], - avatar: constants.AVATAR_GRAVATAR -}; - -var USER_2 = { - id: 'uuid2', - username: 'uuid2', - password: 'secret', - email: 'safe3@me.com', - fallbackEmail: 'safer3@me.com', - salt: 'tata', - resetToken: '', - displayName: 'Herbert 2', - twoFactorAuthenticationEnabled: false, - twoFactorAuthenticationSecret: '', - role: 'user', - active: true, - source: '', - loginLocations: [], - avatar: constants.AVATAR_NONE -}; - const DOMAIN_0 = { domain: 'foobar.com', zoneName: 'foobar.com', @@ -110,10 +53,6 @@ describe('database', function () { }); describe('domains', function () { - before(function (done) { - userdb.add(USER_0.id, USER_0, done); - }); - after(function (done) { database._clear(done); }); @@ -265,239 +204,6 @@ describe('database', function () { }); }); - describe('user', function () { - function validateUser(a, b) { - expect(a.creationTime).to.be.a(Date); - expect(a.resetTokenCreationTime).to.be.a(Date); - expect(_.omit(b, ['avatar'])).to.be.eql(_.omit(a, ['creationTime', 'resetTokenCreationTime'])); - } - - it('can add user', function (done) { - userdb.add(USER_0.id, USER_0, done); - }); - - it('can add another user', function (done) { - userdb.add(USER_1.id, USER_1, done); - }); - - it('can add another user with empty username', function (done) { - userdb.add(USER_2.id, USER_2, done); - }); - - it('cannot add user with same email again', function (done) { - var tmp = JSON.parse(JSON.stringify(USER_0)); - tmp.id = 'somethingelse'; - tmp.username = 'somethingelse'; - tmp.avatar = constants.AVATAR_GRAVATAR; - - userdb.add(tmp.id, tmp, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.be(BoxError.ALREADY_EXISTS); - expect(error.message).to.equal('email already exists'); - done(); - }); - }); - - it('cannot add user with same username again', function (done) { - var tmp = JSON.parse(JSON.stringify(USER_0)); - tmp.id = 'somethingelse'; - tmp.email = 'somethingelse@not.taken'; - tmp.avatar = constants.AVATAR_GRAVATAR; - - userdb.add(tmp.id, tmp, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.be(BoxError.ALREADY_EXISTS); - expect(error.message).to.equal('username already exists'); - done(); - }); - }); - - it('can get by user id', function (done) { - userdb.get(USER_0.id, function (error, user) { - expect(error).to.not.be.ok(); - - validateUser(user, USER_0); - - done(); - }); - }); - - it('can get by user name', function (done) { - userdb.getByUsername(USER_0.username, function (error, user) { - expect(error).to.not.be.ok(); - - validateUser(user, USER_0); - - done(); - }); - }); - - it('can get by email', function (done) { - userdb.getByEmail(USER_0.email, function (error, user) { - expect(error).to.not.be.ok(); - - validateUser(user, USER_0); - - done(); - }); - }); - - it('getByResetToken fails for empty resetToken', function (done) { - userdb.getByResetToken('', function (error, user) { - expect(error).to.be.ok(); - expect(error.reason).to.be(BoxError.NOT_FOUND); - expect(user).to.not.be.ok(); - done(); - }); - }); - - it('getByResetToken fails for invalid resetToken', function (done) { - userdb.getByResetToken('invalid', function (error, user) { - expect(error).to.be.ok(); - expect(error.reason).to.be(BoxError.NOT_FOUND); - expect(user).to.not.be.ok(); - done(); - }); - }); - - it('can get by resetToken', function (done) { - userdb.getByResetToken(USER_0.resetToken, function (error, user) { - expect(error).to.not.be.ok(); - - validateUser(user, USER_0); - - done(); - }); - }); - - it('can get all with group ids', function (done) { - userdb.getAllWithGroupIds(function (error, all) { - expect(error).to.not.be.ok(); - expect(all.length).to.equal(3); - - var userCopy; - - userCopy = _.extend({}, USER_0); - userCopy.groupIds = [ ]; - validateUser(all[0], userCopy); - - userCopy = _.extend({}, USER_1); - userCopy.groupIds = [ ]; - validateUser(all[1], userCopy); - - userCopy = _.extend({}, USER_2); - userCopy.groupIds = [ ]; - validateUser(all[2], userCopy); - - done(); - }); - }); - - it('can get all with group ids paged', function (done) { - userdb.getAllWithGroupIdsPaged(null, 1, 2, function (error, all) { - expect(error).to.not.be.ok(); - expect(all.length).to.equal(2); - - var userCopy; - - userCopy = _.extend({}, USER_0); - userCopy.groupIds = []; - validateUser(all[0], userCopy); - - userCopy = _.extend({}, USER_1); - userCopy.groupIds = []; - validateUser(all[1], userCopy); - - userdb.getAllWithGroupIdsPaged(null, 2, 2, function (error, all) { - expect(error).to.not.be.ok(); - expect(all.length).to.equal(1); - - var userCopy; - - userCopy = _.extend({}, USER_2); - userCopy.groupIds = []; - validateUser(all[0], userCopy); - - done(); - }); - }); - }); - - it('can get all with group ids paged and search', function (done) { - userdb.getAllWithGroupIdsPaged('id1', 1, 2, function (error, all) { - expect(error).to.not.be.ok(); - expect(all.length).to.equal(1); - - var userCopy; - - userCopy = _.extend({}, USER_1); - userCopy.groupIds = []; - validateUser(all[0], userCopy); - - done(); - }); - }); - - it('can get all admins', function (done) { - userdb.getByRole('owner', function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.be(BoxError.NOT_FOUND); - done(); - }); - }); - - it('counts the users', function (done) { - userdb.count(function (error, count) { - expect(error).to.not.be.ok(); - expect(count).to.equal(3); - done(); - }); - }); - - it('can get all users', function (done) { - userdb.getByRole('user', function (error, all) { - expect(error).to.not.be.ok(); - expect(all.length).to.equal(3); - done(); - }); - }); - - it('can update the user', function (done) { - userdb.update(USER_0.id, { email: 'some@thing.com', displayName: 'Heiter' }, function (error) { - expect(error).to.not.be.ok(); - userdb.get(USER_0.id, function (error, user) { - expect(user.email).to.equal('some@thing.com'); - expect(user.displayName).to.equal('Heiter'); - done(); - }); - }); - }); - - it('can update the user with already existing email', function (done) { - userdb.update(USER_0.id, { email: USER_2.email }, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.be(BoxError.ALREADY_EXISTS); - expect(error.message).to.equal('email already exists'); - done(); - }); - }); - - it('can update the user with already existing username', function (done) { - userdb.update(USER_0.id, { username: USER_2.username }, function (error) { - expect(error).to.be.ok(); - expect(error.reason).to.be(BoxError.ALREADY_EXISTS); - expect(error.message).to.equal('username already exists'); - done(); - }); - }); - - it('cannot update with null field', function () { - expect(function () { - userdb.update(USER_0.id, { email: null }, function () {}); - }).to.throwError(); - }); - }); - describe('apps', function () { var APP_0 = { id: 'appid-0', @@ -580,7 +286,6 @@ describe('database', function () { before(function (done) { async.series([ database._clear, - userdb.add.bind(null, USER_0.id, USER_0), domaindb.add.bind(null, DOMAIN_0.domain, DOMAIN_0) ], done); }); diff --git a/src/test/users-test.js b/src/test/users-test.js index dad9e8e29..cd3ae449e 100644 --- a/src/test/users-test.js +++ b/src/test/users-test.js @@ -30,6 +30,42 @@ var DISPLAY_NAME_NEW = 'Somone cares'; var userObject = null; var AUDIT_SOURCE = { ip: '1.2.3.4', userId: 'someuserid' }; +var USER_1 = { + id: 'uuid1', + username: 'uuid1', + password: 'secret', + email: 'safe2@me.com', + fallbackEmail: 'safer2@me.com', + salt: 'tata', + resetToken: '', + displayName: 'Herbert 1', + twoFactorAuthenticationEnabled: false, + twoFactorAuthenticationSecret: '', + role: 'user', + active: true, + source: '', + loginLocations: [], + avatar: constants.AVATAR_GRAVATAR +}; + +var USER_2 = { + id: 'uuid2', + username: 'uuid2', + password: 'secret', + email: 'safe3@me.com', + fallbackEmail: 'safer3@me.com', + salt: 'tata', + resetToken: '', + displayName: 'Herbert 2', + twoFactorAuthenticationEnabled: false, + twoFactorAuthenticationSecret: '', + role: 'user', + active: true, + source: '', + loginLocations: [], + avatar: constants.AVATAR_NONE +}; + const DOMAIN_0 = { domain: 'example.com', zoneName: 'example.com', @@ -101,6 +137,239 @@ describe('User', function () { before(setup); after(cleanup); + describe('user', function () { + function validateUser(a, b) { + expect(a.creationTime).to.be.a(Date); + expect(a.resetTokenCreationTime).to.be.a(Date); + expect(_.omit(b, ['avatar'])).to.be.eql(_.omit(a, ['creationTime', 'resetTokenCreationTime'])); + } + + it('can add user', function (done) { + userdb.add(USER_0.id, USER_0, done); + }); + + it('can add another user', function (done) { + userdb.add(USER_1.id, USER_1, done); + }); + + it('can add another user with empty username', function (done) { + userdb.add(USER_2.id, USER_2, done); + }); + + it('cannot add user with same email again', function (done) { + var tmp = JSON.parse(JSON.stringify(USER_0)); + tmp.id = 'somethingelse'; + tmp.username = 'somethingelse'; + tmp.avatar = constants.AVATAR_GRAVATAR; + + userdb.add(tmp.id, tmp, function (error) { + expect(error).to.be.ok(); + expect(error.reason).to.be(BoxError.ALREADY_EXISTS); + expect(error.message).to.equal('email already exists'); + done(); + }); + }); + + it('cannot add user with same username again', function (done) { + var tmp = JSON.parse(JSON.stringify(USER_0)); + tmp.id = 'somethingelse'; + tmp.email = 'somethingelse@not.taken'; + tmp.avatar = constants.AVATAR_GRAVATAR; + + userdb.add(tmp.id, tmp, function (error) { + expect(error).to.be.ok(); + expect(error.reason).to.be(BoxError.ALREADY_EXISTS); + expect(error.message).to.equal('username already exists'); + done(); + }); + }); + + it('can get by user id', function (done) { + userdb.get(USER_0.id, function (error, user) { + expect(error).to.not.be.ok(); + + validateUser(user, USER_0); + + done(); + }); + }); + + it('can get by user name', function (done) { + userdb.getByUsername(USER_0.username, function (error, user) { + expect(error).to.not.be.ok(); + + validateUser(user, USER_0); + + done(); + }); + }); + + it('can get by email', function (done) { + userdb.getByEmail(USER_0.email, function (error, user) { + expect(error).to.not.be.ok(); + + validateUser(user, USER_0); + + done(); + }); + }); + + it('getByResetToken fails for empty resetToken', function (done) { + userdb.getByResetToken('', function (error, user) { + expect(error).to.be.ok(); + expect(error.reason).to.be(BoxError.NOT_FOUND); + expect(user).to.not.be.ok(); + done(); + }); + }); + + it('getByResetToken fails for invalid resetToken', function (done) { + userdb.getByResetToken('invalid', function (error, user) { + expect(error).to.be.ok(); + expect(error.reason).to.be(BoxError.NOT_FOUND); + expect(user).to.not.be.ok(); + done(); + }); + }); + + it('can get by resetToken', function (done) { + userdb.getByResetToken(USER_0.resetToken, function (error, user) { + expect(error).to.not.be.ok(); + + validateUser(user, USER_0); + + done(); + }); + }); + + it('can get all with group ids', function (done) { + userdb.getAllWithGroupIds(function (error, all) { + expect(error).to.not.be.ok(); + expect(all.length).to.equal(3); + + var userCopy; + + userCopy = _.extend({}, USER_0); + userCopy.groupIds = [ ]; + validateUser(all[0], userCopy); + + userCopy = _.extend({}, USER_1); + userCopy.groupIds = [ ]; + validateUser(all[1], userCopy); + + userCopy = _.extend({}, USER_2); + userCopy.groupIds = [ ]; + validateUser(all[2], userCopy); + + done(); + }); + }); + + it('can get all with group ids paged', function (done) { + userdb.getAllWithGroupIdsPaged(null, 1, 2, function (error, all) { + expect(error).to.not.be.ok(); + expect(all.length).to.equal(2); + + var userCopy; + + userCopy = _.extend({}, USER_0); + userCopy.groupIds = []; + validateUser(all[0], userCopy); + + userCopy = _.extend({}, USER_1); + userCopy.groupIds = []; + validateUser(all[1], userCopy); + + userdb.getAllWithGroupIdsPaged(null, 2, 2, function (error, all) { + expect(error).to.not.be.ok(); + expect(all.length).to.equal(1); + + var userCopy; + + userCopy = _.extend({}, USER_2); + userCopy.groupIds = []; + validateUser(all[0], userCopy); + + done(); + }); + }); + }); + + it('can get all with group ids paged and search', function (done) { + userdb.getAllWithGroupIdsPaged('id1', 1, 2, function (error, all) { + expect(error).to.not.be.ok(); + expect(all.length).to.equal(1); + + var userCopy; + + userCopy = _.extend({}, USER_1); + userCopy.groupIds = []; + validateUser(all[0], userCopy); + + done(); + }); + }); + + it('can get all admins', function (done) { + userdb.getByRole('owner', function (error) { + expect(error).to.be.ok(); + expect(error.reason).to.be(BoxError.NOT_FOUND); + done(); + }); + }); + + it('counts the users', function (done) { + userdb.count(function (error, count) { + expect(error).to.not.be.ok(); + expect(count).to.equal(3); + done(); + }); + }); + + it('can get all users', function (done) { + userdb.getByRole('user', function (error, all) { + expect(error).to.not.be.ok(); + expect(all.length).to.equal(3); + done(); + }); + }); + + it('can update the user', function (done) { + userdb.update(USER_0.id, { email: 'some@thing.com', displayName: 'Heiter' }, function (error) { + expect(error).to.not.be.ok(); + userdb.get(USER_0.id, function (error, user) { + expect(user.email).to.equal('some@thing.com'); + expect(user.displayName).to.equal('Heiter'); + done(); + }); + }); + }); + + it('can update the user with already existing email', function (done) { + userdb.update(USER_0.id, { email: USER_2.email }, function (error) { + expect(error).to.be.ok(); + expect(error.reason).to.be(BoxError.ALREADY_EXISTS); + expect(error.message).to.equal('email already exists'); + done(); + }); + }); + + it('can update the user with already existing username', function (done) { + userdb.update(USER_0.id, { username: USER_2.username }, function (error) { + expect(error).to.be.ok(); + expect(error.reason).to.be(BoxError.ALREADY_EXISTS); + expect(error.message).to.equal('username already exists'); + done(); + }); + }); + + it('cannot update with null field', function () { + expect(function () { + userdb.update(USER_0.id, { email: null }, function () {}); + }).to.throwError(); + }); + }); + describe('create', function() { before(cleanupUsers); after(cleanupUsers); diff --git a/src/userdb.js b/src/userdb.js deleted file mode 100644 index b049459b1..000000000 --- a/src/userdb.js +++ /dev/null @@ -1,253 +0,0 @@ -'use strict'; - -exports = module.exports = { - get, - getByUsername, - getByEmail, - getByAccessToken, - getByResetToken, - getAllWithGroupIds, - getAllWithGroupIdsPaged, - getByRole, - add, - update, - count, - - _clear: clear -}; - -// the avatar field is special and not added here to reduce response sizes -const USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'creationTime', 'resetToken', 'displayName', - 'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime', 'loginLocationsJson' ].join(','); - -const assert = require('assert'), - BoxError = require('./boxerror.js'), - database = require('./database.js'), - mysql = require('mysql'), - safe = require('safetydance'); - -function postProcess(result) { - assert.strictEqual(typeof result, 'object'); - - result.twoFactorAuthenticationEnabled = !!result.twoFactorAuthenticationEnabled; - result.active = !!result.active; - - result.loginLocations = safe.JSON.parse(result.loginLocationsJson) || []; - if (!Array.isArray(result.loginLocations)) result.loginLocations = []; - delete result.loginLocationsJson; - - return result; -} - -function get(userId, callback) { - assert.strictEqual(typeof userId, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE id = ?', [ userId ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); - - callback(null, postProcess(result[0])); - }); -} - -function getByUsername(username, callback) { - assert.strictEqual(typeof username, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE username = ?', [ username ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); - - callback(null, postProcess(result[0])); - }); -} - -function getByEmail(email, callback) { - assert.strictEqual(typeof email, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE email = ?', [ email ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); - - callback(null, postProcess(result[0])); - }); -} - -function getByResetToken(resetToken, callback) { - assert.strictEqual(typeof resetToken, 'string'); - assert.strictEqual(typeof callback, 'function'); - - // empty reset tokens means it does not exist - if (!resetToken) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); - - database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE resetToken=?', [ resetToken ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); - - callback(null, postProcess(result[0])); - }); -} - -function getAllWithGroupIds(callback) { - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + USERS_FIELDS + ',GROUP_CONCAT(groupMembers.groupId) AS groupIds ' + - ' FROM users LEFT OUTER JOIN groupMembers ON users.id = groupMembers.userId ' + - ' GROUP BY users.id ORDER BY users.username', function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - results.forEach(function (result) { - result.groupIds = result.groupIds ? result.groupIds.split(',') : [ ]; - }); - - results.forEach(postProcess); - - callback(null, results); - }); -} - -function getAllWithGroupIdsPaged(search, page, perPage, callback) { - assert(typeof search === 'string' || search === null); - assert.strictEqual(typeof page, 'number'); - assert.strictEqual(typeof perPage, 'number'); - assert.strictEqual(typeof callback, 'function'); - - var query = `SELECT ${USERS_FIELDS},GROUP_CONCAT(groupMembers.groupId) AS groupIds FROM users LEFT OUTER JOIN groupMembers ON users.id = groupMembers.userId `; - - if (search) { - query += ' WHERE '; - query += '(LOWER(users.username) LIKE ' + mysql.escape(`%${search.toLowerCase()}%`) + ')'; - query += ' OR '; - query += '(LOWER(users.email) LIKE ' + mysql.escape(`%${search.toLowerCase()}%`) + ')'; - query += ' OR '; - query += '(LOWER(users.displayName) LIKE ' + mysql.escape(`%${search.toLowerCase()}%`) + ')'; - } - - query += ` GROUP BY users.id ORDER BY users.username ASC LIMIT ${(page-1)*perPage},${perPage} `; - - database.query(query, function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - results.forEach(function (result) { - result.groupIds = result.groupIds ? result.groupIds.split(',') : [ ]; - }); - - results.forEach(postProcess); - - callback(null, results); - }); -} - -function getByRole(role, callback) { - assert.strictEqual(typeof role, 'string'); - assert.strictEqual(typeof callback, 'function'); - - // the mailer code relies on the first object being the 'owner' (thus the ORDER) - database.query('SELECT ' + USERS_FIELDS + ' FROM users WHERE role=? ORDER BY creationTime', [ role ], function (error, results) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); - - results.forEach(postProcess); - - callback(null, results); - }); -} - -function add(userId, user, callback) { - assert.strictEqual(typeof userId, 'string'); - assert(user.username === null || typeof user.username === 'string'); - assert.strictEqual(typeof user.password, 'string'); - assert.strictEqual(typeof user.email, 'string'); - assert.strictEqual(typeof user.fallbackEmail, 'string'); - assert.strictEqual(typeof user.salt, 'string'); - assert.strictEqual(typeof user.resetToken, 'string'); - assert.strictEqual(typeof user.displayName, 'string'); - assert.strictEqual(typeof user.source, 'string'); - assert.strictEqual(typeof user.role, 'string'); - assert(Buffer.isBuffer(user.avatar)); - assert.strictEqual(typeof callback, 'function'); - - const query = 'INSERT INTO users (id, username, password, email, fallbackEmail, salt, resetToken, displayName, source, role, avatar) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; - const args = [ userId, user.username, user.password, user.email, user.fallbackEmail, user.salt, user.resetToken, user.displayName, user.source, user.role, user.avatar ]; - - database.query(query, args, function (error) { - if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('users_email') !== -1) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'email already exists')); - if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('users_username') !== -1) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'username already exists')); - if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('PRIMARY') !== -1) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'id already exists')); - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(null); - }); -} - -function getByAccessToken(accessToken, callback) { - assert.strictEqual(typeof accessToken, 'string'); - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT ' + USERS_FIELDS + ' FROM users, tokens WHERE tokens.accessToken = ?', [ accessToken ], function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); - - callback(null, postProcess(result[0])); - }); -} - -function clear(callback) { - database.query('DELETE FROM users', function (error) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - callback(error); - }); -} - -function update(userId, user, callback) { - assert.strictEqual(typeof userId, 'string'); - assert.strictEqual(typeof user, 'object'); - assert.strictEqual(typeof callback, 'function'); - - assert(!('username' in user) || (user.username === null || typeof user.username === 'string')); - assert(!('email' in user) || (typeof user.email === 'string')); - assert(!('fallbackEmail' in user) || (typeof user.fallbackEmail === 'string')); - assert(!('twoFactorAuthenticationEnabled' in user) || (typeof user.twoFactorAuthenticationEnabled === 'boolean')); - assert(!('role' in user) || (typeof user.role === 'string')); - assert(!('active' in user) || (typeof user.active === 'boolean')); - assert(!('loginLocations' in user) || (Array.isArray(user.loginLocations))); - - var args = [ ]; - var fields = [ ]; - for (var k in user) { - if (k === 'twoFactorAuthenticationEnabled' || k === 'active') { - fields.push(k + ' = ?'); - args.push(user[k] ? 1 : 0); - } else if (k === 'loginLocations') { - fields.push('loginLocationsJson = ?'); - args.push(JSON.stringify(user[k])); - } else { - fields.push(k + ' = ?'); - args.push(user[k]); - } - } - args.push(userId); - - database.query('UPDATE users SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) { - if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('users_email') !== -1) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'email already exists')); - if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('users_username') !== -1) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'username already exists')); - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'User not found')); - - return callback(null); - }); -} - -function count(callback) { - assert.strictEqual(typeof callback, 'function'); - - database.query('SELECT COUNT(*) AS total FROM users', function (error, result) { - if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error)); - - return callback(null, result[0].total); - }); -} - diff --git a/src/users.js b/src/users.js index 873dfc2f0..22ed8b94d 100644 --- a/src/users.js +++ b/src/users.js @@ -4,41 +4,45 @@ exports = module.exports = { removePrivateFields, removeRestrictedFields, + add, + createOwner, + isActivated, + getAll, getAllPaged, - create, - isActivated, - verify, - verifyWithUsername, - verifyWithEmail, - del, get, getByResetToken, getByUsername, + getOwner, getAdmins, getSuperadmins, + + verify, + verifyWithUsername, + verifyWithEmail, + setPassword, update, - createOwner, - getOwner, + + del, + createInvite, sendInvite, - setMembership, + setTwoFactorAuthenticationSecret, enableTwoFactorAuthentication, disableTwoFactorAuthentication, sendPasswordResetByIdentifier, - checkLoginLocation, + notifyLoginLocation, setupAccount, + getAvatarUrl, setAvatar, getAvatar, - count, - AP_MAIL: 'mail', AP_WEBADMIN: 'webadmin', @@ -51,6 +55,10 @@ exports = module.exports = { const ORDERED_ROLES = [ exports.ROLE_USER, exports.ROLE_USER_MANAGER, exports.ROLE_ADMIN, exports.ROLE_OWNER ]; +// the avatar field is special and not added here to reduce response sizes +const USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'creationTime', 'resetToken', 'displayName', + 'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime', 'loginLocationsJson' ].join(','); + const appPasswords = require('./apppasswords.js'), assert = require('assert'), BoxError = require('./boxerror.js'), @@ -60,19 +68,19 @@ const appPasswords = require('./apppasswords.js'), debug = require('debug')('box:user'), eventlog = require('./eventlog.js'), externalLdap = require('./externalldap.js'), - groups = require('./groups.js'), hat = require('./hat.js'), mailer = require('./mailer.js'), + mysql = require('mysql'), paths = require('./paths.js'), qrcode = require('qrcode'), safe = require('safetydance'), settings = require('./settings.js'), speakeasy = require('speakeasy'), tokens = require('./tokens.js'), - userdb = require('./userdb.js'), uuid = require('uuid'), uaParser = require('ua-parser-js'), superagent = require('superagent'), + util = require('util'), validator = require('validator'), _ = require('underscore'); @@ -81,6 +89,22 @@ const CRYPTO_ITERATIONS = 10000; // iterations const CRYPTO_KEY_LENGTH = 512; // bits const CRYPTO_DIGEST = 'sha1'; // used to be the default in node 4.1.1 cannot change since it will affect existing db records +const pbkdf2Async = util.promisify(crypto.pbkdf2); +const getDirectoryConfigAsync = util.promisify(settings.getDirectoryConfig); + +function postProcess(result) { + assert.strictEqual(typeof result, 'object'); + + result.twoFactorAuthenticationEnabled = !!result.twoFactorAuthenticationEnabled; + result.active = !!result.active; + + result.loginLocations = safe.JSON.parse(result.loginLocationsJson) || []; + if (!Array.isArray(result.loginLocations)) result.loginLocations = []; + delete result.loginLocationsJson; + + return result; +} + // keep this in sync with validateGroupname and validateAlias function validateUsername(username) { assert.strictEqual(typeof username, 'string'); @@ -107,7 +131,7 @@ function validateEmail(email) { return null; } -function validateToken(token) { +function validateResetToken(token) { assert.strictEqual(typeof token, 'string'); if (token.length !== 64) return new BoxError(BoxError.BAD_FIELD, 'Invalid token'); // 256-bit hex coded token @@ -140,75 +164,83 @@ function removeRestrictedFields(user) { return _.pick(user, 'id', 'username', 'email', 'displayName', 'active'); } -function create(username, password, email, displayName, options, auditSource, callback) { - assert(username === null || typeof username === 'string'); - assert(password === null || typeof password === 'string'); +async function add(email, data, auditSource) { assert.strictEqual(typeof email, 'string'); - assert.strictEqual(typeof displayName, 'string'); - assert(options && typeof options === 'object'); + assert(data && typeof data === 'object'); assert(auditSource && typeof auditSource === 'object'); - const source = options.source || ''; // empty is local user - const role = options.role || exports.ROLE_USER; + assert(data.username === null || typeof data.username === 'string'); + assert(data.password === null || typeof data.password === 'string'); + assert.strictEqual(typeof data.displayName, 'string'); + + let { username, password, displayName } = data; + const source = data.source || ''; // empty is local user + const role = data.role || exports.ROLE_USER; let error; if (username !== null) { username = username.toLowerCase(); error = validateUsername(username); - if (error) return callback(error); + if (error) throw error; } if (password !== null) { error = validatePassword(password); - if (error) return callback(error); + if (error) throw error; } else { password = hat(8 * 8); } email = email.toLowerCase(); error = validateEmail(email); - if (error) return callback(error); + if (error) throw error; error = validateDisplayName(displayName); - if (error) return callback(error); + if (error) throw error; error = validateRole(role); - if (error) return callback(error); + if (error) throw error; - crypto.randomBytes(CRYPTO_SALT_SIZE, function (error, salt) { - if (error) return callback(new BoxError(BoxError.CRYPTO_ERROR, error)); + const randomBytes = util.promisify(crypto.randomBytes); + let salt, derivedKey; - crypto.pbkdf2(password, salt, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST, function (error, derivedKey) { - if (error) return callback(new BoxError(BoxError.CRYPTO_ERROR, error)); + [error, salt] = await safe(randomBytes(CRYPTO_SALT_SIZE)); + if (error) throw new BoxError(BoxError.CRYPTO_ERROR, error); - const user = { - id: 'uid-' + uuid.v4(), - username: username, - email: email, - fallbackEmail: email, - password: Buffer.from(derivedKey, 'binary').toString('hex'), - salt: salt.toString('hex'), - resetToken: '', - displayName: displayName, - source: source, - role: role, - avatar: constants.AVATAR_NONE - }; + [error, derivedKey] = await safe(pbkdf2Async(password, salt, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST)); + if (error) throw new BoxError(BoxError.CRYPTO_ERROR, error); - userdb.add(user.id, user, function (error) { - if (error) return callback(error); + const user = { + id: 'uid-' + uuid.v4(), + username: username, + email: email, + fallbackEmail: email, + password: Buffer.from(derivedKey, 'binary').toString('hex'), + salt: salt.toString('hex'), + resetToken: '', + displayName: displayName, + source: source, + role: role, + avatar: constants.AVATAR_NONE + }; - // when this is used to create the owner, then we have to patch the auditSource to contain himself - if (!auditSource.userId) auditSource.userId = user.id; - if (!auditSource.username) auditSource.username= user.username; + const query = 'INSERT INTO users (id, username, password, email, fallbackEmail, salt, resetToken, displayName, source, role, avatar) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + const args = [ user.id, user.username, user.password, user.email, user.fallbackEmail, user.salt, user.resetToken, user.displayName, user.source, user.role, user.avatar ]; - eventlog.add(eventlog.ACTION_USER_ADD, auditSource, { userId: user.id, email: user.email, user: removePrivateFields(user) }); + [error] = await safe(database.query(query, args)); + if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('users_email') !== -1) throw new BoxError(BoxError.ALREADY_EXISTS, 'email already exists'); + if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('users_username') !== -1) throw new BoxError(BoxError.ALREADY_EXISTS, 'username already exists'); + if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('PRIMARY') !== -1) throw new BoxError(BoxError.ALREADY_EXISTS, 'id already exists'); + if (error) throw error; - callback(null, user); - }); - }); - }); + // when this is used to create the owner, then we have to patch the auditSource to contain himself + if (!auditSource.userId) auditSource.userId = user.id; + if (!auditSource.username) auditSource.username= user.username; + + eventlog.add(eventlog.ACTION_USER_ADD, auditSource, { userId: user.id, email: user.email, user: removePrivateFields(user) }); + + return user; } // returns true if ghost user was matched @@ -228,91 +260,76 @@ function verifyGhost(username, password) { return false; } -async function verifyAppPassword(userId, password, identifier, callback) { +async function verifyAppPassword(userId, password, identifier) { assert.strictEqual(typeof userId, 'string'); assert.strictEqual(typeof password, 'string'); assert.strictEqual(typeof identifier, 'string'); - assert.strictEqual(typeof callback, 'function'); - const [error, results] = await safe(appPasswords.list(userId)); - if (error) return callback(error); + const results = await appPasswords.list(userId); const hashedPasswords = results.filter(r => r.identifier === identifier).map(r => r.hashedPassword); let hash = crypto.createHash('sha256').update(password).digest('base64'); - if (hashedPasswords.includes(hash)) return callback(null); + if (hashedPasswords.includes(hash)) return; - return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); + throw new BoxError(BoxError.INVALID_CREDENTIALS); } -function verify(userId, password, identifier, callback) { +async function verify(userId, password, identifier) { assert.strictEqual(typeof userId, 'string'); assert.strictEqual(typeof password, 'string'); assert.strictEqual(typeof identifier, 'string'); - assert.strictEqual(typeof callback, 'function'); - get(userId, function (error, user) { - if (error) return callback(error); + const user = await get(userId); + if (!user) throw new BoxError(BoxError.NOT_FOUND, 'User not found'); + if (!user.active) throw new BoxError(BoxError.NOT_FOUND, 'User not active'); - if (!user.active) return callback(new BoxError(BoxError.NOT_FOUND)); + // for just invited users the username may be still null + if (user.username && verifyGhost(user.username, password)) { + user.ghost = true; + return user; + } - // for just invited users the username may be still null - if (user.username && verifyGhost(user.username, password)) { - user.ghost = true; - return callback(null, user); - } + const [error] = await safe(verifyAppPassword(user.id, password, identifier)); + if (!error) { // matched app password + user.appPassword = true; + return user; + } - verifyAppPassword(user.id, password, identifier, function (error) { - if (!error) { - user.appPassword = true; - return callback(null, user); - } + if (user.source === 'ldap') { + await externalLdap.verifyPassword(user, password); + } else { + const saltBinary = Buffer.from(user.salt, 'hex'); + const [error, derivedKey] = await safe(pbkdf2Async(password, saltBinary, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST)); + if (error) throw new BoxError(BoxError.CRYPTO_ERROR, error); - if (user.source === 'ldap') { - externalLdap.verifyPassword(user, password, function (error) { - if (error) return callback(error); + const derivedKeyHex = Buffer.from(derivedKey, 'binary').toString('hex'); + if (derivedKeyHex !== user.password) throw new BoxError(BoxError.INVALID_CREDENTIALS); + } - callback(null, user); - }); - } else { - var saltBinary = Buffer.from(user.salt, 'hex'); - crypto.pbkdf2(password, saltBinary, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST, function (error, derivedKey) { - if (error) return callback(new BoxError(BoxError.CRYPTO_ERROR, error)); - - var derivedKeyHex = Buffer.from(derivedKey, 'binary').toString('hex'); - if (derivedKeyHex !== user.password) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); - - callback(null, user); - }); - } - }); - }); + return user; } -function verifyWithUsername(username, password, identifier, callback) { +async function verifyWithUsername(username, password, identifier) { assert.strictEqual(typeof username, 'string'); assert.strictEqual(typeof password, 'string'); assert.strictEqual(typeof identifier, 'string'); - assert.strictEqual(typeof callback, 'function'); - userdb.getByUsername(username.toLowerCase(), function (error, user) { - if (error) return callback(error); + const user = await getByUsername(username.toLowerCase()); + if (!user) throw new BoxError(BoxError.NOT_FOUND, 'User not found'); - verify(user.id, password, identifier, callback); - }); + return await verify(user.id, password, identifier); } -function verifyWithEmail(email, password, identifier, callback) { +async function verifyWithEmail(email, password, identifier) { assert.strictEqual(typeof email, 'string'); assert.strictEqual(typeof password, 'string'); assert.strictEqual(typeof identifier, 'string'); - assert.strictEqual(typeof callback, 'function'); - userdb.getByEmail(email.toLowerCase(), function (error, user) { - if (error) return callback(error); + const user = await getByEmail(email.toLowerCase()); + if (!user) throw new BoxError(BoxError.NOT_FOUND, 'User not found'); - verify(user.id, password, identifier, callback); - }); + return await verify(user.id, password, identifier); } async function del(user, auditSource) { @@ -335,295 +352,292 @@ async function del(user, auditSource) { await safe(eventlog.add(eventlog.ACTION_USER_REMOVE, auditSource, { userId: user.id, user: removePrivateFields(user) })); } -function getAll(callback) { - assert.strictEqual(typeof callback, 'function'); +async function getAll() { + const results = await database.query(`SELECT ${USERS_FIELDS},GROUP_CONCAT(groupMembers.groupId) AS groupIds ` + + ' FROM users LEFT OUTER JOIN groupMembers ON users.id = groupMembers.userId ' + + ' GROUP BY users.id ORDER BY users.username'); - userdb.getAllWithGroupIds(function (error, results) { - if (error) return callback(error); - - return callback(null, results); + results.forEach(function (result) { + result.groupIds = result.groupIds ? result.groupIds.split(',') : [ ]; }); + + results.forEach(postProcess); + + return results; } -function getAllPaged(search, page, perPage, callback) { +async function getAllPaged(search, page, perPage) { assert(typeof search === 'string' || search === null); assert.strictEqual(typeof page, 'number'); assert.strictEqual(typeof perPage, 'number'); - assert.strictEqual(typeof callback, 'function'); - userdb.getAllWithGroupIdsPaged(search, page, perPage, function (error, results) { - if (error) return callback(error); + let query = `SELECT ${USERS_FIELDS},GROUP_CONCAT(groupMembers.groupId) AS groupIds FROM users LEFT OUTER JOIN groupMembers ON users.id = groupMembers.userId `; - return callback(null, results); + if (search) { + query += ' WHERE '; + query += '(LOWER(users.username) LIKE ' + mysql.escape(`%${search.toLowerCase()}%`) + ')'; + query += ' OR '; + query += '(LOWER(users.email) LIKE ' + mysql.escape(`%${search.toLowerCase()}%`) + ')'; + query += ' OR '; + query += '(LOWER(users.displayName) LIKE ' + mysql.escape(`%${search.toLowerCase()}%`) + ')'; + } + + query += ` GROUP BY users.id ORDER BY users.username ASC LIMIT ${(page-1)*perPage},${perPage} `; + + const results = await database.query(query); + + results.forEach(function (result) { + result.groupIds = result.groupIds ? result.groupIds.split(',') : [ ]; }); + + results.forEach(postProcess); + + return results; } -function count(callback) { - assert.strictEqual(typeof callback, 'function'); +async function isActivated() { + const result = await database.query('SELECT COUNT(*) AS total FROM users'); - userdb.count(function (error, count) { - if (error) return callback(error); - - callback(null, count); - }); + return result[0].total !== 0; } -function isActivated(callback) { - assert.strictEqual(typeof callback, 'function'); - - count(function (error, count) { - if (error) return callback(error); - - callback(null, count !== 0); - }); -} - -function get(userId, callback) { +async function get(userId) { assert.strictEqual(typeof userId, 'string'); - assert.strictEqual(typeof callback, 'function'); - userdb.get(userId, async function (error, result) { - if (error) return callback(error); + const results = await database.query(`SELECT ${USERS_FIELDS},GROUP_CONCAT(groupMembers.groupId) AS groupIds ` + + ' FROM users LEFT OUTER JOIN groupMembers ON users.id = groupMembers.userId ' + + ' GROUP BY users.id HAVING users.id = ?', [ userId ]); - let groupIds; - [error, groupIds] = await safe(groups.getMembership(userId)); - if (error) return callback(error); + if (results.length === 0) return null; - result.groupIds = groupIds; + results[0].groupIds = results[0].groupIds ? results[0].groupIds.split(',') : [ ]; - return callback(null, result); - }); + return postProcess(results[0]); } -function getByResetToken(resetToken, callback) { +async function getByEmail(email) { + assert.strictEqual(typeof email, 'string'); + + const result = await database.query(`SELECT ${USERS_FIELDS} FROM users WHERE email = ?`, [ email ]); + if (result.length === 0) return null; + + return postProcess(result[0]); +} + +async function getByRole(role) { + assert.strictEqual(typeof role, 'string'); + + // the mailer code relies on the first object being the 'owner' (thus the ORDER) + const results = await database.query(`SELECT ${USERS_FIELDS} FROM users WHERE role=? ORDER BY creationTime`, [ role ]); + + results.forEach(postProcess); + + return results; +} + +async function getByResetToken(resetToken) { assert.strictEqual(typeof resetToken, 'string'); - assert.strictEqual(typeof callback, 'function'); - var error = validateToken(resetToken); - if (error) return callback(error); + let error = validateResetToken(resetToken); + if (error) throw error; - userdb.getByResetToken(resetToken, function (error, result) { - if (error) return callback(error); + const result = await database.query(`SELECT ${USERS_FIELDS} FROM users WHERE resetToken=?`, [ resetToken ]); + if (result.length === 0) return null; - callback(null, result); - }); + return postProcess(result[0]); } -function getByUsername(username, callback) { +async function getByUsername(username) { assert.strictEqual(typeof username, 'string'); - assert.strictEqual(typeof callback, 'function'); - userdb.getByUsername(username.toLowerCase(), function (error, result) { - if (error) return callback(error); + const result = await database.query(`SELECT ${USERS_FIELDS} FROM users WHERE username = ?`, [ username ]); + if (result.length === 0) return null; - get(result.id, callback); - }); + return postProcess(result[0]); } -function update(user, data, auditSource, callback) { +async function update(user, data, auditSource) { assert.strictEqual(typeof user, 'object'); assert.strictEqual(typeof data, 'object'); assert(auditSource && typeof auditSource === 'object'); - assert.strictEqual(typeof callback, 'function'); - if (settings.isDemo() && user.username === constants.DEMO_USERNAME) return callback(new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode')); + assert(!('twoFactorAuthenticationEnabled' in data) || (typeof data.twoFactorAuthenticationEnabled === 'boolean')); + assert(!('active' in data) || (typeof data.active === 'boolean')); + assert(!('loginLocations' in data) || (Array.isArray(data.loginLocations))); - var error; - data = _.pick(data, 'email', 'fallbackEmail', 'displayName', 'username', 'active', 'role'); + if (settings.isDemo() && user.username === constants.DEMO_USERNAME) throw new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'); - if (_.isEmpty(data)) return callback(); + let error, result; + + if (_.isEmpty(data)) return; if (data.username) { data.username = data.username.toLowerCase(); error = validateUsername(data.username); - if (error) return callback(error); + if (error) throw error; } if (data.email) { data.email = data.email.toLowerCase(); error = validateEmail(data.email); - if (error) return callback(error); + if (error) throw error; } if (data.fallbackEmail) { data.fallbackEmail = data.fallbackEmail.toLowerCase(); error = validateEmail(data.fallbackEmail); - if (error) return callback(error); + if (error) throw error; } if (data.role) { error = validateRole(data.role); - if (error) return callback(error); + if (error) throw error; } - userdb.update(user.id, data, function (error) { - if (error) return callback(error); + let args = [ ]; + let fields = [ ]; + for (const k in data) { + if (k === 'twoFactorAuthenticationEnabled' || k === 'active') { + fields.push(k + ' = ?'); + args.push(data[k] ? 1 : 0); + } else if (k === 'loginLocations') { + fields.push('loginLocationsJson = ?'); + args.push(JSON.stringify(data[k])); + } else { + fields.push(k + ' = ?'); + args.push(data[k]); + } + } + args.push(user.id); - const newUser = _.extend({}, user, data); + [error, result] = await safe(database.query('UPDATE users SET ' + fields.join(', ') + ' WHERE id = ?', args)); + if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('users_email') !== -1) throw new BoxError(BoxError.ALREADY_EXISTS, 'email already exists'); + if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('users_username') !== -1) throw new BoxError(BoxError.ALREADY_EXISTS, 'username already exists'); + if (error) throw new BoxError(BoxError.DATABASE_ERROR, error); + if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'User not found'); - eventlog.add(eventlog.ACTION_USER_UPDATE, auditSource, { - userId: user.id, - user: removePrivateFields(newUser), - roleChanged: newUser.role !== user.role, - activeStatusChanged: ((newUser.active && !user.active) || (!newUser.active && user.active)) - }); + const newUser = _.extend({}, user, data); - callback(null); + eventlog.add(eventlog.ACTION_USER_UPDATE, auditSource, { + userId: user.id, + user: removePrivateFields(newUser), + roleChanged: newUser.role !== user.role, + activeStatusChanged: ((newUser.active && !user.active) || (!newUser.active && user.active)) }); } -async function setMembership(user, groupIds) { - assert.strictEqual(typeof user, 'object'); - assert(Array.isArray(groupIds)); - - await groups.setMembership(user.id, groupIds); +async function getOwner() { + const owners = await getByRole(exports.ROLE_OWNER); + if (owners.length === 0) return null; + return owners[0]; } -function getAdmins(callback) { - assert.strictEqual(typeof callback, 'function'); +async function getAdmins() { + const owners = await getByRole(exports.ROLE_OWNER); + const admins = await getByRole(exports.ROLE_ADMIN); - userdb.getByRole(exports.ROLE_OWNER, function (error, owners) { - if (error) return callback(error); - - userdb.getByRole(exports.ROLE_ADMIN, function (error, admins) { - if (error && error.reason === BoxError.NOT_FOUND) return callback(null, owners); - if (error) return callback(error); - - callback(null, owners.concat(admins)); - }); - }); + return owners.concat(admins); } -function getSuperadmins(callback) { - assert.strictEqual(typeof callback, 'function'); - - userdb.getByRole(exports.ROLE_OWNER, function (error, owners) { - if (error) return callback(error); - - callback(null, owners); - }); +async function getSuperadmins() { + return await getByRole(exports.ROLE_OWNER); } -function sendPasswordResetByIdentifier(identifier, callback) { +async function sendPasswordResetByIdentifier(identifier, auditSource) { assert.strictEqual(typeof identifier, 'string'); - assert.strictEqual(typeof callback, 'function'); + assert.strictEqual(typeof auditSource, 'object'); - const getter = identifier.indexOf('@') === -1 ? userdb.getByUsername : userdb.getByEmail; + const user = identifier.indexOf('@') === -1 ? await getByUsername(identifier.toLowerCase()) : await getByEmail(identifier.toLowerCase()); + if (!user) throw new BoxError(BoxError.NOT_FOUND, 'User not found'); - getter(identifier.toLowerCase(), function (error, result) { - if (error) return callback(error); + let resetToken = hat(256), resetTokenCreationTime = new Date(); + user.resetToken = resetToken; + user.resetTokenCreationTime = resetTokenCreationTime; - let resetToken = hat(256), resetTokenCreationTime = new Date(); - result.resetToken = resetToken; - result.resetTokenCreationTime = resetTokenCreationTime; + await update(user, { resetToken, resetTokenCreationTime }, auditSource); - userdb.update(result.id, { resetToken, resetTokenCreationTime }, function (error) { - if (error) return callback(error); - - mailer.passwordReset(result); - - callback(null); - }); - }); + mailer.passwordReset(user); } -function checkLoginLocation(user, ip, userAgent) { +async function notifyLoginLocation(user, ip, userAgent, auditSource) { assert.strictEqual(typeof user, 'object'); assert.strictEqual(typeof ip, 'string'); assert.strictEqual(typeof userAgent, 'string'); + assert.strictEqual(typeof auditSource, 'object'); - debug(`checkLoginLocation: ${user.id} ${ip} ${userAgent}`); + debug(`notifyLoginLocation: ${user.id} ${ip} ${userAgent}`); - superagent.get('https://geolocation.cloudron.io/json').query({ ip }).end(function (error, result) { - if (error) return console.error('Failed to get geoip info:', error); + if (constants.TEST && ip === '127.0.0.1') return; - const country = safe.query(result.body, 'country.names.en', ''); - const city = safe.query(result.body, 'city.names.en', ''); + const response = await superagent.get('https://geolocation.cloudron.io/json').query({ ip }).ok(() => true).end(); + if (response.statusCode !== 200) return console.error(`Failed to get geoip info. statusCode: ${response.statusCode}`); - if (!city || !country) return; + const country = safe.query(response.body, 'country.names.en', ''); + const city = safe.query(response.body, 'city.names.en', ''); - const ua = uaParser(userAgent); - const simplifiedUserAgent = ua.browser.name ? `${ua.browser.name} - ${ua.os.name}` : userAgent; + if (!city || !country) return; - const knownLogin = user.loginLocations.find(function (l) { - return l.userAgent === simplifiedUserAgent && l.country === country && l.city === city; - }); + const ua = uaParser(userAgent); + const simplifiedUserAgent = ua.browser.name ? `${ua.browser.name} - ${ua.os.name}` : userAgent; - if (knownLogin) return; - - // purge potentially old locations where ts > now() - 6 months - const sixMonthsBack = Date.now() - 6 * 30 * 24 * 60 * 60 * 1000; - const newLoginLocation = { ts: Date.now(), ip, userAgent: simplifiedUserAgent, country, city }; - let loginLocations = user.loginLocations.filter(function (l) { return l.ts > sixMonthsBack; }); - - // only stash if we have a real useragent, otherwise warn the user every time - if (simplifiedUserAgent) loginLocations.push(newLoginLocation); - - userdb.update(user.id, { loginLocations }, function (error) { - if (error) console.error('checkLoginLocation: Failed to update user location.', error); - - mailer.sendNewLoginLocation(user, newLoginLocation); - }); + const knownLogin = user.loginLocations.find(function (l) { + return l.userAgent === simplifiedUserAgent && l.country === country && l.city === city; }); + + if (knownLogin) return; + + // purge potentially old locations where ts > now() - 6 months + const sixMonthsBack = Date.now() - 6 * 30 * 24 * 60 * 60 * 1000; + const newLoginLocation = { ts: Date.now(), ip, userAgent: simplifiedUserAgent, country, city }; + let loginLocations = user.loginLocations.filter(function (l) { return l.ts > sixMonthsBack; }); + + // only stash if we have a real useragent, otherwise warn the user every time + if (simplifiedUserAgent) loginLocations.push(newLoginLocation); + + await update(user, { loginLocations }, auditSource); + + mailer.sendNewLoginLocation(user, newLoginLocation); } -function setPassword(user, newPassword, callback) { +async function setPassword(user, newPassword, auditSource) { assert.strictEqual(typeof user, 'object'); assert.strictEqual(typeof newPassword, 'string'); - assert.strictEqual(typeof callback, 'function'); + assert.strictEqual(typeof auditSource, 'object'); - var error = validatePassword(newPassword); - if (error) return callback(error); + let error = validatePassword(newPassword); + if (error) throw error; - if (settings.isDemo() && user.username === constants.DEMO_USERNAME) return callback(new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode')); - if (user.source) return callback(new BoxError(BoxError.CONFLICT, 'User is from an external directory')); + if (settings.isDemo() && user.username === constants.DEMO_USERNAME) throw new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'); + if (user.source) throw new BoxError(BoxError.CONFLICT, 'User is from an external directory'); - var saltBuffer = Buffer.from(user.salt, 'hex'); - crypto.pbkdf2(newPassword, saltBuffer, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST, function (error, derivedKey) { - if (error) return callback(new BoxError(BoxError.CRYPTO_ERROR, error)); + const saltBuffer = Buffer.from(user.salt, 'hex'); - let data = { - password: Buffer.from(derivedKey, 'binary').toString('hex'), - resetToken: '' - }; + let derivedKey; + [error, derivedKey] = await safe(pbkdf2Async(newPassword, saltBuffer, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST)); + if (error) throw new BoxError(BoxError.CRYPTO_ERROR, error); - userdb.update(user.id, data, function (error) { - if (error) return callback(error); + const data = { + password: Buffer.from(derivedKey, 'binary').toString('hex'), + resetToken: '' + }; - callback(); - }); - }); + await update(user, data, auditSource); } -function createOwner(username, password, email, displayName, auditSource, callback) { +async function createOwner(email, username, password, displayName, auditSource) { + assert.strictEqual(typeof email, 'string'); assert.strictEqual(typeof username, 'string'); assert.strictEqual(typeof password, 'string'); - assert.strictEqual(typeof email, 'string'); assert.strictEqual(typeof displayName, 'string'); assert(auditSource && typeof auditSource === 'object'); - assert.strictEqual(typeof callback, 'function'); - // This is only not allowed for the owner - if (username === '') return callback(new BoxError(BoxError.BAD_FIELD, 'Username cannot be empty')); + // This is only not allowed for the owner. reset of username validation happens in add() + if (username === '') throw new BoxError(BoxError.BAD_FIELD, 'Username cannot be empty'); - isActivated(function (error, activated) { - if (error) return callback(error); - if (activated) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'Cloudron already activated')); + const activated = await isActivated(); + if (activated) throw new BoxError(BoxError.ALREADY_EXISTS, 'Cloudron already activated'); - create(username, password, email, displayName, { role: exports.ROLE_OWNER }, auditSource, function (error, user) { - if (error) return callback(error); - - callback(null, user); - }); - }); -} - -function getOwner(callback) { - userdb.getByRole(exports.ROLE_OWNER, function (error, results) { - if (error) return callback(error); - - return callback(null, results[0]); - }); + return await add(email, { username, password, displayName, role: exports.ROLE_OWNER }, auditSource); } function inviteLink(user, directoryConfig) { @@ -636,130 +650,96 @@ function inviteLink(user, directoryConfig) { return link; } -function createInvite(user, callback) { +async function createInvite(user, auditSource) { assert.strictEqual(typeof user, 'object'); - assert.strictEqual(typeof callback, 'function'); + assert.strictEqual(typeof auditSource, 'object'); - if (user.source) return callback(new BoxError(BoxError.CONFLICT, 'User is from an external directory')); + if (user.source) throw new BoxError(BoxError.CONFLICT, 'User is from an external directory'); const resetToken = hat(256), resetTokenCreationTime = new Date(); - settings.getDirectoryConfig(function (error, directoryConfig) { - if (error) return callback(error); + const directoryConfig = await getDirectoryConfigAsync(); - userdb.update(user.id, { resetToken, resetTokenCreationTime }, function (error) { - if (error) return callback(error); + await update(user, { resetToken, resetTokenCreationTime }, auditSource); - user.resetToken = resetToken; + user.resetToken = resetToken; - callback(null, { resetToken, inviteLink: inviteLink(user, directoryConfig) }); - }); - }); + return { resetToken, inviteLink: inviteLink(user, directoryConfig) }; } -function sendInvite(user, options, callback) { +async function sendInvite(user, options) { assert.strictEqual(typeof user, 'object'); assert.strictEqual(typeof options, 'object'); - assert.strictEqual(typeof callback, 'function'); - if (user.source) return callback(new BoxError(BoxError.CONFLICT, 'User is from an external directory')); - if (!user.resetToken) return callback(new BoxError(BoxError.CONFLICT, 'Must generate resetToken to send invitation')); + if (user.source) throw new BoxError(BoxError.CONFLICT, 'User is from an external directory'); + if (!user.resetToken) throw new BoxError(BoxError.CONFLICT, 'Must generate resetToken to send invitation'); - settings.getDirectoryConfig(function (error, directoryConfig) { - if (error) return callback(error); + const directoryConfig = await getDirectoryConfigAsync(); - mailer.sendInvite(user, options.invitor || null, inviteLink(user, directoryConfig)); - - callback(null); - }); + mailer.sendInvite(user, options.invitor || null, inviteLink(user, directoryConfig)); } -function setupAccount(user, data, auditSource, callback) { +async function setupAccount(user, data, auditSource) { assert.strictEqual(typeof user, 'object'); assert.strictEqual(typeof data, 'object'); assert(auditSource && typeof auditSource === 'object'); - assert.strictEqual(typeof callback, 'function'); - settings.getDirectoryConfig(function (error, directoryConfig) { - if (error) return callback(error); + const directoryConfig = await getDirectoryConfigAsync(); + if (directoryConfig.lockUserProfiles) return; - const updateFunc = (done) => { - if (directoryConfig.lockUserProfiles) return done(); - update(user, _.pick(data, 'username', 'displayName'), auditSource, done); - }; + await update(user, _.pick(data, 'username', 'displayName'), auditSource); - updateFunc(function (error) { - if (error) return callback(error); + await setPassword(user, data.password); // setPassword clears the resetToken - setPassword(user, data.password, async function (error) { // setPassword clears the resetToken - if (error) return callback(error); - - const token = { clientId: tokens.ID_WEBADMIN, identifier: user.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS }; - let result; - [error, result] = await safe(tokens.add(token)); - if (error) return callback(error); - - callback(null, result.accessToken); - }); - }); - }); + const token = { clientId: tokens.ID_WEBADMIN, identifier: user.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS }; + const result = await tokens.add(token); + return result.accessToken; } -function setTwoFactorAuthenticationSecret(userId, callback) { +async function setTwoFactorAuthenticationSecret(userId, auditSource) { assert.strictEqual(typeof userId, 'string'); - assert.strictEqual(typeof callback, 'function'); + assert(auditSource && typeof auditSource === 'object'); - userdb.get(userId, function (error, result) { - if (error) return callback(error); + const user = await get(userId); + if (!user) throw new BoxError(BoxError.NOT_FOUND, 'User not found'); - if (settings.isDemo() && result.username === constants.DEMO_USERNAME) return callback(new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode')); + if (settings.isDemo() && user.username === constants.DEMO_USERNAME) throw new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'); - if (result.twoFactorAuthenticationEnabled) return callback(new BoxError(BoxError.ALREADY_EXISTS)); + if (user.twoFactorAuthenticationEnabled) throw new BoxError(BoxError.ALREADY_EXISTS); - var secret = speakeasy.generateSecret({ name: `Cloudron ${settings.dashboardFqdn()} (${result.username})` }); + const secret = speakeasy.generateSecret({ name: `Cloudron ${settings.dashboardFqdn()} (${user.username})` }); - userdb.update(userId, { twoFactorAuthenticationSecret: secret.base32, twoFactorAuthenticationEnabled: false }, function (error) { - if (error) return callback(error); + await update(user, { twoFactorAuthenticationSecret: secret.base32, twoFactorAuthenticationEnabled: false }, auditSource); - qrcode.toDataURL(secret.otpauth_url, function (error, dataUrl) { - if (error) return callback(new BoxError(BoxError.INTERNAL_ERROR, error)); + const [error, dataUrl] = await safe(qrcode.toDataURL(secret.otpauth_url)); + if (error) throw new BoxError(BoxError.INTERNAL_ERROR, error); - callback(null, { secret: secret.base32, qrcode: dataUrl }); - }); - }); - }); + return { secret: secret.base32, qrcode: dataUrl }; } -function enableTwoFactorAuthentication(userId, totpToken, callback) { +async function enableTwoFactorAuthentication(userId, totpToken, auditSource) { assert.strictEqual(typeof userId, 'string'); assert.strictEqual(typeof totpToken, 'string'); - assert.strictEqual(typeof callback, 'function'); + assert(auditSource && typeof auditSource === 'object'); - userdb.get(userId, function (error, result) { - if (error) return callback(error); + const user = await get(userId); + if (!user) throw new BoxError(BoxError.NOT_FOUND, 'User not found'); - var verified = speakeasy.totp.verify({ secret: result.twoFactorAuthenticationSecret, encoding: 'base32', token: totpToken, window: 2 }); - if (!verified) return callback(new BoxError(BoxError.INVALID_CREDENTIALS)); + const verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: totpToken, window: 2 }); + if (!verified) throw new BoxError(BoxError.INVALID_CREDENTIALS); - if (result.twoFactorAuthenticationEnabled) return callback(new BoxError(BoxError.ALREADY_EXISTS)); + if (user.twoFactorAuthenticationEnabled) throw new BoxError(BoxError.ALREADY_EXISTS); - userdb.update(userId, { twoFactorAuthenticationEnabled: true }, function (error) { - if (error) return callback(error); - - callback(null); - }); - }); + await update(user, { twoFactorAuthenticationEnabled: true }, auditSource); } -function disableTwoFactorAuthentication(userId, callback) { +async function disableTwoFactorAuthentication(userId, auditSource) { assert.strictEqual(typeof userId, 'string'); - assert.strictEqual(typeof callback, 'function'); + assert(auditSource && typeof auditSource === 'object'); - userdb.update(userId, { twoFactorAuthenticationEnabled: false, twoFactorAuthenticationSecret: '' }, function (error) { - if (error) return callback(error); - - callback(null); - }); + const user = await get(userId); + if (!user) throw new BoxError(BoxError.NOT_FOUND, 'User not found'); + await update(user, { twoFactorAuthenticationEnabled: false, twoFactorAuthenticationSecret: '' }, auditSource); } function validateRole(role) {