diff --git a/config/roles.json b/config/roles.json index 070281ee..433940f4 100644 --- a/config/roles.json +++ b/config/roles.json @@ -10,6 +10,13 @@ "authentication": { "create:any": ["*"], "read:any": ["*"] + }, + + "users": { + "create:any": ["*"], + "read:any": ["*"], + "update:any": ["*"], + "delete:any": ["*"] } }, diff --git a/lib/api/users.js b/lib/api/users.js index ee148409..76afc7b1 100644 --- a/lib/api/users.js +++ b/lib/api/users.js @@ -10,8 +10,16 @@ const openpgp = require('openpgp'); const addressparser = require('nodemailer/lib/addressparser'); const libmime = require('libmime'); const consts = require('../consts'); +const roles = require('../roles'); +const util = require('util'); module.exports = (db, server, userHandler) => { + const createUser = util.promisify(userHandler.create.bind(userHandler)); + const updateUser = util.promisify(userHandler.update.bind(userHandler)); + const logoutUser = util.promisify(userHandler.logout.bind(userHandler)); + const resetUser = util.promisify(userHandler.reset.bind(userHandler)); + const deleteUser = util.promisify(userHandler.delete.bind(userHandler)); + /** * @api {get} /users List registered Users * @apiName GetUsers @@ -138,6 +146,9 @@ module.exports = (db, server, userHandler) => { return next(); } + // permissions check + req.validate(roles.can(req.role).readAny('users')); + let query = result.value.query; let limit = result.value.limit; let page = result.value.page; @@ -361,230 +372,237 @@ module.exports = (db, server, userHandler) => { * "error": "This username already exists" * } */ - server.post('/users', (req, res, next) => { - res.charSet('utf-8'); + server.post( + '/users', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); - const schema = Joi.object().keys({ - username: Joi.string() - .lowercase() - // no spaces, printable range - .regex(/^[\x21-\x7e]{1,128}?$/, 'username') - .min(1) - .max(128) - .required(), - password: Joi.string() - .allow(false) - .max(256) - .required(), - - address: Joi.string().email(), - emptyAddress: Joi.boolean() - .truthy(['Y', 'true', 'yes', 'on', 1]) - .falsy(['N', 'false', 'no', 'off', 0, '']) - .default(false), - - language: Joi.string() - .min(2) - .max(20) - .lowercase(), - retention: Joi.number() - .min(0) - .default(0), - - name: Joi.string().max(256), - targets: Joi.array().items( - Joi.string().email(), - Joi.string().uri({ - scheme: [/smtps?/, /https?/], - allowRelative: false, - relativeOnly: false - }) - ), - - spamLevel: Joi.number() - .min(0) - .max(100) - .default(50), - - quota: Joi.number() - .min(0) - .default(0), - recipients: Joi.number() - .min(0) - .default(0), - forwards: Joi.number() - .min(0) - .default(0), - - requirePasswordChange: Joi.boolean() - .truthy(['Y', 'true', 'yes', 'on', 1]) - .falsy(['N', 'false', 'no', 'off', 0, '']) - .default(false), - - imapMaxUpload: Joi.number() - .min(0) - .default(0), - imapMaxDownload: Joi.number() - .min(0) - .default(0), - pop3MaxDownload: Joi.number() - .min(0) - .default(0), - receivedMax: Joi.number() - .min(0) - .default(0), - - tags: Joi.array().items( - Joi.string() - .trim() + const schema = Joi.object().keys({ + username: Joi.string() + .lowercase() + // no spaces, printable range + .regex(/^[\x21-\x7e]{1,128}?$/, 'username') + .min(1) .max(128) - ), - addTagsToAddress: Joi.boolean() - .truthy(['Y', 'true', 'yes', 'on', 1]) - .falsy(['N', 'false', 'no', 'off', 0, '']) - .default(false), + .required(), + password: Joi.string() + .allow(false) + .max(256) + .required(), - pubKey: Joi.string() - .empty('') - .trim() - .regex(/^-----BEGIN PGP PUBLIC KEY BLOCK-----/, 'PGP key format'), - encryptMessages: Joi.boolean() - .truthy(['Y', 'true', 'yes', 'on', 1]) - .falsy(['N', 'false', 'no', 'off', 0, '']) - .default(false), - encryptForwarded: Joi.boolean() - .truthy(['Y', 'true', 'yes', 'on', 1]) - .falsy(['N', 'false', 'no', 'off', 0, '']) - .default(false), - sess: Joi.string().max(255), - ip: Joi.string().ip({ - version: ['ipv4', 'ipv6'], - cidr: 'forbidden' - }) - }); + address: Joi.string().email(), + emptyAddress: Joi.boolean() + .truthy(['Y', 'true', 'yes', 'on', 1]) + .falsy(['N', 'false', 'no', 'off', 0, '']) + .default(false), - const result = Joi.validate(req.params, schema, { - abortEarly: false, - convert: true - }); + language: Joi.string() + .min(2) + .max(20) + .lowercase(), + retention: Joi.number() + .min(0) + .default(0), - if (result.error) { - res.json({ - error: result.error.message, - code: 'InputValidationError' + name: Joi.string().max(256), + targets: Joi.array().items( + Joi.string().email(), + Joi.string().uri({ + scheme: [/smtps?/, /https?/], + allowRelative: false, + relativeOnly: false + }) + ), + + spamLevel: Joi.number() + .min(0) + .max(100) + .default(50), + + quota: Joi.number() + .min(0) + .default(0), + recipients: Joi.number() + .min(0) + .default(0), + forwards: Joi.number() + .min(0) + .default(0), + + requirePasswordChange: Joi.boolean() + .truthy(['Y', 'true', 'yes', 'on', 1]) + .falsy(['N', 'false', 'no', 'off', 0, '']) + .default(false), + + imapMaxUpload: Joi.number() + .min(0) + .default(0), + imapMaxDownload: Joi.number() + .min(0) + .default(0), + pop3MaxDownload: Joi.number() + .min(0) + .default(0), + receivedMax: Joi.number() + .min(0) + .default(0), + + tags: Joi.array().items( + Joi.string() + .trim() + .max(128) + ), + addTagsToAddress: Joi.boolean() + .truthy(['Y', 'true', 'yes', 'on', 1]) + .falsy(['N', 'false', 'no', 'off', 0, '']) + .default(false), + + pubKey: Joi.string() + .empty('') + .trim() + .regex(/^-----BEGIN PGP PUBLIC KEY BLOCK-----/, 'PGP key format'), + encryptMessages: Joi.boolean() + .truthy(['Y', 'true', 'yes', 'on', 1]) + .falsy(['N', 'false', 'no', 'off', 0, '']) + .default(false), + encryptForwarded: Joi.boolean() + .truthy(['Y', 'true', 'yes', 'on', 1]) + .falsy(['N', 'false', 'no', 'off', 0, '']) + .default(false), + sess: Joi.string().max(255), + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) }); - return next(); - } - let targets = result.value.targets; + const result = Joi.validate(req.params, schema, { + abortEarly: false, + convert: true + }); - if (targets) { - for (let i = 0, len = targets.length; i < len; i++) { - let target = targets[i]; - if (!/^smtps?:/i.test(target) && !/^https?:/i.test(target) && target.indexOf('@') >= 0) { - // email - targets[i] = { - id: new ObjectID(), - type: 'mail', - value: target - }; - } else if (/^smtps?:/i.test(target)) { - targets[i] = { - id: new ObjectID(), - type: 'relay', - value: target - }; - } else if (/^https?:/i.test(target)) { - targets[i] = { - id: new ObjectID(), - type: 'http', - value: target - }; - } else { - res.json({ - error: 'Unknown target type "' + target + '"', - code: 'InputValidationError' - }); - return next(); - } + if (result.error) { + res.json({ + error: result.error.message, + code: 'InputValidationError' + }); + return next(); } - result.value.targets = targets; - } + // permissions check + req.validate(roles.can(req.role).createAny('users')); - if ('pubKey' in req.params && !result.value.pubKey) { - result.value.pubKey = ''; - } + let targets = result.value.targets; - if (result.value.tags) { - let tagSeen = new Set(); - let tags = result.value.tags - .map(tag => tag.trim()) - .filter(tag => { - if (tag && !tagSeen.has(tag.toLowerCase())) { - tagSeen.add(tag.toLowerCase()); - return true; - } - return false; - }) - .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); - - result.value.tags = tags; - result.value.tagsview = tags.map(tag => tag.toLowerCase()); - } - - if (result.value.username.indexOf('*') >= 0) { - res.json({ - error: 'Invalid character in username: *', - code: 'InputValidationError' - }); - return next(); - } - - if (/^\.|\.$|\.{2,}/g.test(result.value.username) || !/[^.]/.test(result.value.username)) { - res.json({ - error: 'Invalid dot symbols in username', - code: 'InputValidationError' - }); - return next(); - } - - if (result.value.address && result.value.address.indexOf('*') >= 0) { - res.json({ - error: 'Invalid character in email address: *', - code: 'InputValidationError' - }); - return next(); - } - - checkPubKey(result.value.pubKey) - .then(() => { - userHandler.create(result.value, (err, id) => { - if (err) { + if (targets) { + for (let i = 0, len = targets.length; i < len; i++) { + let target = targets[i]; + if (!/^smtps?:/i.test(target) && !/^https?:/i.test(target) && target.indexOf('@') >= 0) { + // email + targets[i] = { + id: new ObjectID(), + type: 'mail', + value: target + }; + } else if (/^smtps?:/i.test(target)) { + targets[i] = { + id: new ObjectID(), + type: 'relay', + value: target + }; + } else if (/^https?:/i.test(target)) { + targets[i] = { + id: new ObjectID(), + type: 'http', + value: target + }; + } else { res.json({ - error: err.message, - code: err.code, - username: result.value.username + error: 'Unknown target type "' + target + '"', + code: 'InputValidationError' }); return next(); } + } - res.json({ - success: !!id, - id - }); + result.value.targets = targets; + } - return next(); + if ('pubKey' in req.params && !result.value.pubKey) { + result.value.pubKey = ''; + } + + if (result.value.tags) { + let tagSeen = new Set(); + let tags = result.value.tags + .map(tag => tag.trim()) + .filter(tag => { + if (tag && !tagSeen.has(tag.toLowerCase())) { + tagSeen.add(tag.toLowerCase()); + return true; + } + return false; + }) + .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); + + result.value.tags = tags; + result.value.tagsview = tags.map(tag => tag.toLowerCase()); + } + + if (result.value.username.indexOf('*') >= 0) { + res.json({ + error: 'Invalid character in username: *', + code: 'InputValidationError' }); - }) - .catch(err => { + return next(); + } + + if (/^\.|\.$|\.{2,}/g.test(result.value.username) || !/[^.]/.test(result.value.username)) { + res.json({ + error: 'Invalid dot symbols in username', + code: 'InputValidationError' + }); + return next(); + } + + if (result.value.address && result.value.address.indexOf('*') >= 0) { + res.json({ + error: 'Invalid character in email address: *', + code: 'InputValidationError' + }); + return next(); + } + + try { + await checkPubKey(result.value.pubKey); + } catch (err) { res.json({ error: 'PGP key validation failed. ' + err.message, code: 'InputValidationError' }); return next(); + } + + let id; + try { + id = await createUser(result.value); + } catch (err) { + res.json({ + error: err.message, + code: err.code, + username: result.value.username + }); + return next(); + } + + res.json({ + success: !!id, + id }); - }); + + return next(); + }) + ); /** * @api {get} /users/resolve/:username Resolve ID for an username @@ -619,66 +637,74 @@ module.exports = (db, server, userHandler) => { * "error": "This user does not exist" * } */ - server.get('/users/resolve/:username', (req, res, next) => { - res.charSet('utf-8'); + server.get( + '/users/resolve/:username', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); - const schema = Joi.object().keys({ - username: Joi.string() - .lowercase() - .regex(/^[a-z0-9][a-z0-9.]+[a-z0-9]$/, 'username') - .min(3) - .max(32) - .required() - }); - - const result = Joi.validate(req.params, schema, { - abortEarly: false, - convert: true - }); - - if (result.error) { - res.json({ - error: result.error.message, - code: 'InputValidationError' + const schema = Joi.object().keys({ + username: Joi.string() + .lowercase() + .regex(/^[a-z0-9][a-z0-9.]+[a-z0-9]$/, 'username') + .min(3) + .max(32) + .required() }); - return next(); - } - let username = result.value.username; + const result = Joi.validate(req.params, schema, { + abortEarly: false, + convert: true + }); - db.users.collection('users').findOne( - { - unameview: username.replace(/\./g, '') - }, - { - projection: { - _id: true - } - }, - (err, userData) => { - if (err) { - res.json({ - error: 'MongoDB Error: ' + err.message, - code: 'InternalDatabaseError' - }); - return next(); - } - if (!userData) { - res.json({ - error: 'This user does not exist', - code: 'UserNotFound' - }); - return next(); - } + if (result.error) { res.json({ - success: true, - id: userData._id + error: result.error.message, + code: 'InputValidationError' }); - return next(); } - ); - }); + + // permissions check + req.validate(roles.can(req.role).readAny('users')); + + let username = result.value.username; + + let userData; + try { + userData = await db.users.collection('users').findOne( + { + unameview: username.replace(/\./g, '') + }, + { + projection: { + _id: true + } + } + ); + } catch (err) { + res.json({ + error: 'MongoDB Error: ' + err.message, + code: 'InternalDatabaseError' + }); + return next(); + } + + if (!userData) { + res.json({ + error: 'This user does not exist', + code: 'UserNotFound' + }); + return next(); + } + + res.json({ + success: true, + id: userData._id + }); + + return next(); + }) + ); /** * @api {get} /users/:id Request User information @@ -793,53 +819,66 @@ module.exports = (db, server, userHandler) => { * "error": "This user does not exist" * } */ - server.get('/users/:user', (req, res, next) => { - res.charSet('utf-8'); + server.get( + '/users/:user', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string() - .hex() - .lowercase() - .length(24) - .required() - }); - - const result = Joi.validate(req.params, schema, { - abortEarly: false, - convert: true - }); - - if (result.error) { - res.json({ - error: result.error.message, - code: 'InputValidationError' + const schema = Joi.object().keys({ + user: Joi.string() + .hex() + .lowercase() + .length(24) + .required() }); - return next(); - } - let user = new ObjectID(result.value.user); + const result = Joi.validate(req.params, schema, { + abortEarly: false, + convert: true + }); - db.users.collection('users').findOne( - { - _id: user - }, - (err, userData) => { - if (err) { - res.json({ - error: 'MongoDB Error: ' + err.message, - code: 'InternalDatabaseError' - }); - return next(); - } - if (!userData) { - res.json({ - error: 'This user does not exist', - code: 'UserNotFound' - }); - return next(); - } + if (result.error) { + res.json({ + error: result.error.message, + code: 'InputValidationError' + }); + return next(); + } - db.redis + // permissions check + if (req.user && req.user === result.value.user) { + req.validate(roles.can(req.role).readOwn('users')); + } else { + req.validate(roles.can(req.role).readAny('users')); + } + + let user = new ObjectID(result.value.user); + + let userData; + + try { + userData = await db.users.collection('users').findOne({ + _id: user + }); + } catch (err) { + res.json({ + error: 'MongoDB Error: ' + err.message, + code: 'InternalDatabaseError' + }); + return next(); + } + + if (!userData) { + res.json({ + error: 'This user does not exist', + code: 'UserNotFound' + }); + return next(); + } + + let response; + try { + response = await db.redis .multi() // sending counters are stored in Redis @@ -867,114 +906,114 @@ module.exports = (db, server, userHandler) => { .get('pdw:' + userData._id.toString()) .ttl('pdw:' + userData._id.toString()) - .exec((err, result) => { - if (err) { - // ignore - errors.notify(err, { userId: user }); - } - - let recipients = Number(userData.recipients) || config.maxRecipients || consts.MAX_RECIPIENTS; - let forwards = Number(userData.forwards) || config.maxForwards || consts.MAX_FORWARDS; - - let recipientsSent = Number(result && result[0] && result[0][1]) || 0; - let recipientsTtl = Number(result && result[1] && result[1][1]) || 0; - - let forwardsSent = Number(result && result[2] && result[2][1]) || 0; - let forwardsTtl = Number(result && result[3] && result[3][1]) || 0; - - let received = Number(result && result[4] && result[4][1]) || 0; - let receivedTtl = Number(result && result[5] && result[5][1]) || 0; - - let imapUpload = Number(result && result[6] && result[6][1]) || 0; - let imapUploadTtl = Number(result && result[7] && result[7][1]) || 0; - - let imapDownload = Number(result && result[8] && result[8][1]) || 0; - let imapDownloadTtl = Number(result && result[9] && result[9][1]) || 0; - - let pop3Download = Number(result && result[10] && result[10][1]) || 0; - let pop3DownloadTtl = Number(result && result[11] && result[11][1]) || 0; - - getKeyInfo(userData.pubKey).then(keyInfo => { - if (err) { - errors.notify(err, { userId: user, source: 'pgp' }); - } - res.json({ - success: true, - id: user, - - username: userData.username, - name: userData.name, - - address: userData.address, - - language: userData.language, - retention: userData.retention || false, - - enabled2fa: Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []), - - encryptMessages: userData.encryptMessages, - encryptForwarded: userData.encryptForwarded, - pubKey: userData.pubKey, - spamLevel: userData.spamLevel, - keyInfo, - - targets: [].concat(userData.targets || []), - - limits: { - quota: { - allowed: Number(userData.quota) || config.maxStorage * 1024 * 1024, - used: Math.max(Number(userData.storageUsed) || 0, 0) - }, - - recipients: { - allowed: recipients, - used: recipientsSent, - ttl: recipientsTtl >= 0 ? recipientsTtl : false - }, - - forwards: { - allowed: forwards, - used: forwardsSent, - ttl: forwardsTtl >= 0 ? forwardsTtl : false - }, - - received: { - allowed: Number(userData.receivedMax) || 150, - used: received, - ttl: receivedTtl >= 0 ? receivedTtl : false - }, - - imapUpload: { - allowed: Number(userData.imapMaxUpload) || (config.imap.maxUploadMB || 10) * 1024 * 1024, - used: imapUpload, - ttl: imapUploadTtl >= 0 ? imapUploadTtl : false - }, - - imapDownload: { - allowed: Number(userData.imapMaxDownload) || (config.imap.maxDownloadMB || 10) * 1024 * 1024, - used: imapDownload, - ttl: imapDownloadTtl >= 0 ? imapDownloadTtl : false - }, - - pop3Download: { - allowed: Number(userData.pop3MaxDownload) || (config.pop3.maxDownloadMB || 10) * 1024 * 1024, - used: pop3Download, - ttl: pop3DownloadTtl >= 0 ? pop3DownloadTtl : false - } - }, - - tags: userData.tags || [], - hasPasswordSet: !!userData.password || !!userData.tempPassword, - activated: userData.activated, - disabled: userData.disabled - }); - - return next(); - }); - }); + .exec(); + } catch (err) { + // ignore + errors.notify(err, { userId: user }); } - ); - }); + + let recipients = Number(userData.recipients) || config.maxRecipients || consts.MAX_RECIPIENTS; + let forwards = Number(userData.forwards) || config.maxForwards || consts.MAX_FORWARDS; + + let recipientsSent = Number(response && response[0] && response[0][1]) || 0; + let recipientsTtl = Number(response && response[1] && response[1][1]) || 0; + + let forwardsSent = Number(response && response[2] && response[2][1]) || 0; + let forwardsTtl = Number(response && response[3] && response[3][1]) || 0; + + let received = Number(response && response[4] && response[4][1]) || 0; + let receivedTtl = Number(response && response[5] && response[5][1]) || 0; + + let imapUpload = Number(response && response[6] && response[6][1]) || 0; + let imapUploadTtl = Number(response && response[7] && response[7][1]) || 0; + + let imapDownload = Number(response && response[8] && response[8][1]) || 0; + let imapDownloadTtl = Number(response && response[9] && response[9][1]) || 0; + + let pop3Download = Number(response && response[10] && response[10][1]) || 0; + let pop3DownloadTtl = Number(response && response[11] && response[11][1]) || 0; + + let keyInfo; + try { + keyInfo = await getKeyInfo(userData.pubKey); + } catch (err) { + errors.notify(err, { userId: user, source: 'pgp' }); + } + + res.json({ + success: true, + id: user, + + username: userData.username, + name: userData.name, + + address: userData.address, + + language: userData.language, + retention: userData.retention || false, + + enabled2fa: Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []), + + encryptMessages: userData.encryptMessages, + encryptForwarded: userData.encryptForwarded, + pubKey: userData.pubKey, + spamLevel: userData.spamLevel, + keyInfo, + + targets: [].concat(userData.targets || []), + + limits: { + quota: { + allowed: Number(userData.quota) || config.maxStorage * 1024 * 1024, + used: Math.max(Number(userData.storageUsed) || 0, 0) + }, + + recipients: { + allowed: recipients, + used: recipientsSent, + ttl: recipientsTtl >= 0 ? recipientsTtl : false + }, + + forwards: { + allowed: forwards, + used: forwardsSent, + ttl: forwardsTtl >= 0 ? forwardsTtl : false + }, + + received: { + allowed: Number(userData.receivedMax) || 150, + used: received, + ttl: receivedTtl >= 0 ? receivedTtl : false + }, + + imapUpload: { + allowed: Number(userData.imapMaxUpload) || (config.imap.maxUploadMB || 10) * 1024 * 1024, + used: imapUpload, + ttl: imapUploadTtl >= 0 ? imapUploadTtl : false + }, + + imapDownload: { + allowed: Number(userData.imapMaxDownload) || (config.imap.maxDownloadMB || 10) * 1024 * 1024, + used: imapDownload, + ttl: imapDownloadTtl >= 0 ? imapDownloadTtl : false + }, + + pop3Download: { + allowed: Number(userData.pop3MaxDownload) || (config.pop3.maxDownloadMB || 10) * 1024 * 1024, + used: pop3Download, + ttl: pop3DownloadTtl >= 0 ? pop3DownloadTtl : false + } + }, + + tags: userData.tags || [], + hasPasswordSet: !!userData.password || !!userData.tempPassword, + activated: userData.activated, + disabled: userData.disabled + }); + + return next(); + }) + ); /** * @api {put} /users/:id Update User information @@ -1033,189 +1072,201 @@ module.exports = (db, server, userHandler) => { * "error": "This user does not exist" * } */ - server.put('/users/:user', (req, res, next) => { - res.charSet('utf-8'); + server.put( + '/users/:user', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string() - .hex() - .lowercase() - .length(24) - .required(), + const schema = Joi.object().keys({ + user: Joi.string() + .hex() + .lowercase() + .length(24) + .required(), - existingPassword: Joi.string() - .empty('') - .min(1) - .max(256), - password: Joi.string() - .min(8) - .max(256) - .allow(false), + existingPassword: Joi.string() + .empty('') + .min(1) + .max(256), + password: Joi.string() + .min(8) + .max(256) + .allow(false), - language: Joi.string() - .min(2) - .max(20) - .lowercase(), + language: Joi.string() + .min(2) + .max(20) + .lowercase(), - name: Joi.string() - .empty('') - .max(256), - targets: Joi.array().items( - Joi.string().email(), - Joi.string().uri({ - scheme: [/smtps?/, /https?/], - allowRelative: false, - relativeOnly: false - }) - ), + name: Joi.string() + .empty('') + .max(256), + targets: Joi.array().items( + Joi.string().email(), + Joi.string().uri({ + scheme: [/smtps?/, /https?/], + allowRelative: false, + relativeOnly: false + }) + ), - spamLevel: Joi.number() - .min(0) - .max(100), + spamLevel: Joi.number() + .min(0) + .max(100), - pubKey: Joi.string() - .empty('') - .trim() - .regex(/^-----BEGIN PGP PUBLIC KEY BLOCK-----/, 'PGP key format'), - encryptMessages: Joi.boolean() - .empty('') - .truthy(['Y', 'true', 'yes', 'on', 1]) - .falsy(['N', 'false', 'no', 'off', 0, '']), - encryptForwarded: Joi.boolean() - .empty('') - .truthy(['Y', 'true', 'yes', 'on', 1]) - .falsy(['N', 'false', 'no', 'off', 0, '']), - retention: Joi.number().min(0), - quota: Joi.number().min(0), - recipients: Joi.number().min(0), - forwards: Joi.number().min(0), - - imapMaxUpload: Joi.number().min(0), - imapMaxDownload: Joi.number().min(0), - pop3MaxDownload: Joi.number().min(0), - receivedMax: Joi.number().min(0), - - disable2fa: Joi.boolean() - .empty('') - .truthy(['Y', 'true', 'yes', 'on', 1]) - .falsy(['N', 'false', 'no', 'off', 0, '']), - - tags: Joi.array().items( - Joi.string() + pubKey: Joi.string() + .empty('') .trim() - .max(128) - ), + .regex(/^-----BEGIN PGP PUBLIC KEY BLOCK-----/, 'PGP key format'), + encryptMessages: Joi.boolean() + .empty('') + .truthy(['Y', 'true', 'yes', 'on', 1]) + .falsy(['N', 'false', 'no', 'off', 0, '']), + encryptForwarded: Joi.boolean() + .empty('') + .truthy(['Y', 'true', 'yes', 'on', 1]) + .falsy(['N', 'false', 'no', 'off', 0, '']), + retention: Joi.number().min(0), + quota: Joi.number().min(0), + recipients: Joi.number().min(0), + forwards: Joi.number().min(0), - disabled: Joi.boolean() - .empty('') - .truthy(['Y', 'true', 'yes', 'on', 1]) - .falsy(['N', 'false', 'no', 'off', 0, '']), - sess: Joi.string().max(255), - ip: Joi.string().ip({ - version: ['ipv4', 'ipv6'], - cidr: 'forbidden' - }) - }); + imapMaxUpload: Joi.number().min(0), + imapMaxDownload: Joi.number().min(0), + pop3MaxDownload: Joi.number().min(0), + receivedMax: Joi.number().min(0), - const result = Joi.validate(req.params, schema, { - abortEarly: false, - convert: true - }); + disable2fa: Joi.boolean() + .empty('') + .truthy(['Y', 'true', 'yes', 'on', 1]) + .falsy(['N', 'false', 'no', 'off', 0, '']), - if (result.error) { - res.json({ - error: result.error.message, - code: 'InputValidationError' + tags: Joi.array().items( + Joi.string() + .trim() + .max(128) + ), + + disabled: Joi.boolean() + .empty('') + .truthy(['Y', 'true', 'yes', 'on', 1]) + .falsy(['N', 'false', 'no', 'off', 0, '']), + sess: Joi.string().max(255), + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) }); - return next(); - } - let user = new ObjectID(result.value.user); + const result = Joi.validate(req.params, schema, { + abortEarly: false, + convert: true + }); - let targets = result.value.targets; - - if (targets) { - for (let i = 0, len = targets.length; i < len; i++) { - let target = targets[i]; - if (!/^smtps?:/i.test(target) && !/^https?:/i.test(target) && target.indexOf('@') >= 0) { - // email - targets[i] = { - id: new ObjectID(), - type: 'mail', - value: target - }; - } else if (/^smtps?:/i.test(target)) { - targets[i] = { - id: new ObjectID(), - type: 'relay', - value: target - }; - } else if (/^https?:/i.test(target)) { - targets[i] = { - id: new ObjectID(), - type: 'http', - value: target - }; - } else { - res.json({ - error: 'Unknown target type "' + target + '"', - code: 'InputValidationError' - }); - return next(); - } + if (result.error) { + res.json({ + error: result.error.message, + code: 'InputValidationError' + }); + return next(); } - result.value.targets = targets; - } + // permissions check + if (req.user && req.user === result.value.user) { + req.validate(roles.can(req.role).updateOwn('users')); + } else { + req.validate(roles.can(req.role).updateAny('users')); + } - if (!result.value.name && 'name' in req.params) { - result.value.name = ''; - } + let user = new ObjectID(result.value.user); - if (!result.value.pubKey && 'pubKey' in req.params) { - result.value.pubKey = ''; - } + let targets = result.value.targets; - if (result.value.tags) { - let tagSeen = new Set(); - let tags = result.value.tags - .map(tag => tag.trim()) - .filter(tag => { - if (tag && !tagSeen.has(tag.toLowerCase())) { - tagSeen.add(tag.toLowerCase()); - return true; - } - return false; - }) - .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); - result.value.tags = tags; - result.value.tagsview = tags.map(tag => tag.toLowerCase()); - } - - checkPubKey(result.value.pubKey) - .then(() => { - userHandler.update(user, result.value, (err, success) => { - if (err) { + if (targets) { + for (let i = 0, len = targets.length; i < len; i++) { + let target = targets[i]; + if (!/^smtps?:/i.test(target) && !/^https?:/i.test(target) && target.indexOf('@') >= 0) { + // email + targets[i] = { + id: new ObjectID(), + type: 'mail', + value: target + }; + } else if (/^smtps?:/i.test(target)) { + targets[i] = { + id: new ObjectID(), + type: 'relay', + value: target + }; + } else if (/^https?:/i.test(target)) { + targets[i] = { + id: new ObjectID(), + type: 'http', + value: target + }; + } else { res.json({ - error: err.message, - code: err.code + error: 'Unknown target type "' + target + '"', + code: 'InputValidationError' }); return next(); } - res.json({ - success - }); - return next(); - }); - }) - .catch(err => { + } + + result.value.targets = targets; + } + + if (!result.value.name && 'name' in req.params) { + result.value.name = ''; + } + + if (!result.value.pubKey && 'pubKey' in req.params) { + result.value.pubKey = ''; + } + + if (result.value.tags) { + let tagSeen = new Set(); + let tags = result.value.tags + .map(tag => tag.trim()) + .filter(tag => { + if (tag && !tagSeen.has(tag.toLowerCase())) { + tagSeen.add(tag.toLowerCase()); + return true; + } + return false; + }) + .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); + result.value.tags = tags; + result.value.tagsview = tags.map(tag => tag.toLowerCase()); + } + + try { + await checkPubKey(result.value.pubKey); + } catch (err) { res.json({ error: 'PGP key validation failed. ' + err.message, code: 'InputValidationError' }); return next(); + } + + let success; + try { + success = await updateUser(user, result.value); + } catch (err) { + res.json({ + error: err.message, + code: err.code + }); + return next(); + } + + res.json({ + success }); - }); + return next(); + }) + ); /** * @api {put} /users/:id/logout Log out User @@ -1254,52 +1305,64 @@ module.exports = (db, server, userHandler) => { * "error": "This user does not exist" * } */ - server.put('/users/:user/logout', (req, res, next) => { - res.charSet('utf-8'); + server.put( + '/users/:user/logout', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string() - .hex() - .lowercase() - .length(24) - .required(), - reason: Joi.string() - .empty('') - .max(128), - sess: Joi.string().max(255), - ip: Joi.string().ip({ - version: ['ipv4', 'ipv6'], - cidr: 'forbidden' - }) - }); - - const result = Joi.validate(req.params, schema, { - abortEarly: false, - convert: true - }); - - if (result.error) { - res.json({ - error: result.error.message, - code: 'InputValidationError' + const schema = Joi.object().keys({ + user: Joi.string() + .hex() + .lowercase() + .length(24) + .required(), + reason: Joi.string() + .empty('') + .max(128), + sess: Joi.string().max(255), + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) }); - return next(); - } - userHandler.logout(result.value.user, result.value.reason || 'Logout requested from API', (err, success) => { - if (err) { + const result = Joi.validate(req.params, schema, { + abortEarly: false, + convert: true + }); + + if (result.error) { + res.json({ + error: result.error.message, + code: 'InputValidationError' + }); + return next(); + } + + // permissions check + if (req.user && req.user === result.value.user) { + req.validate(roles.can(req.role).readOwn('users')); + } else { + req.validate(roles.can(req.role).readAny('users')); + } + + let success; + try { + success = await logoutUser(result.value.user, result.value.reason || 'Logout requested from API'); + } catch (err) { res.json({ error: err.message, code: err.code }); return next(); } + res.json({ success }); return next(); - }); - }); + }) + ); /** * @api {post} /users/:id/quota/reset Recalculate User quota @@ -1339,66 +1402,79 @@ module.exports = (db, server, userHandler) => { * "error": "This user does not exist" * } */ - server.post('/users/:user/quota/reset', (req, res, next) => { - res.charSet('utf-8'); + server.post( + '/users/:user/quota/reset', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string() - .hex() - .lowercase() - .length(24) - .required(), - sess: Joi.string().max(255), - ip: Joi.string().ip({ - version: ['ipv4', 'ipv6'], - cidr: 'forbidden' - }) - }); - - const result = Joi.validate(req.params, schema, { - abortEarly: false, - convert: true - }); - - if (result.error) { - res.json({ - error: result.error.message, - code: 'InputValidationError' + const schema = Joi.object().keys({ + user: Joi.string() + .hex() + .lowercase() + .length(24) + .required(), + sess: Joi.string().max(255), + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) }); - return next(); - } - let user = new ObjectID(result.value.user); + const result = Joi.validate(req.params, schema, { + abortEarly: false, + convert: true + }); - db.users.collection('users').findOne( - { - _id: user - }, - { - projection: { - storageUsed: true - } - }, - (err, userData) => { - if (err) { - res.json({ - error: 'MongoDB Error: ' + err.message, - code: 'InternalDatabaseError' - }); - return next(); - } + if (result.error) { + res.json({ + error: result.error.message, + code: 'InputValidationError' + }); + return next(); + } - if (!userData) { - res.json({ - error: 'This user does not exist', - code: 'UserNotFound' - }); - return next(); - } + // permissions check + if (req.user && req.user === result.value.user) { + req.validate(roles.can(req.role).updateOwn('users')); + } else { + req.validate(roles.can(req.role).updateAny('users')); + } + let user = new ObjectID(result.value.user); + + let userData; + try { + userData = await db.users.collection('users').findOne( + { + _id: user + }, + { + projection: { + storageUsed: true + } + } + ); + } catch (err) { + res.json({ + error: 'MongoDB Error: ' + err.message, + code: 'InternalDatabaseError' + }); + return next(); + } + + if (!userData) { + res.json({ + error: 'This user does not exist', + code: 'UserNotFound' + }); + return next(); + } + + let storageData; + try { // calculate mailbox size by aggregating the size's of all messages // NB! Scattered query - db.database + storageData = db.database .collection('messages') .aggregate( [ @@ -1424,58 +1500,59 @@ module.exports = (db, server, userHandler) => { } } ) - .toArray((err, result) => { - if (err) { - res.json({ - error: 'MongoDB Error: ' + err.message, - code: 'InternalDatabaseError' - }); - return next(); - } - - let storageUsed = (result && result[0] && result[0].storageUsed) || 0; - - // update quota counter - db.users.collection('users').findOneAndUpdate( - { - _id: userData._id - }, - { - $set: { - storageUsed: Number(storageUsed) || 0 - } - }, - { - returnOriginal: false - }, - (err, result) => { - if (err) { - res.json({ - error: 'MongoDB Error: ' + err.message, - code: 'InternalDatabaseError' - }); - return next(); - } - - if (!result || !result.value) { - res.json({ - error: 'This user does not exist', - code: 'UserNotFound' - }); - return next(); - } - - res.json({ - success: true, - storageUsed: Number(result.value.storageUsed) || 0 - }); - return next(); - } - ); - }); + .toArray(); + } catch (err) { + res.json({ + error: 'MongoDB Error: ' + err.message, + code: 'InternalDatabaseError' + }); + return next(); } - ); - }); + + let storageUsed = (storageData && storageData[0] && storageData[0].storageUsed) || 0; + + let updateResponse; + try { + // update quota counter + updateResponse = await db.users.collection('users').findOneAndUpdate( + { + _id: userData._id + }, + { + $set: { + storageUsed: Number(storageUsed) || 0 + } + }, + { + returnOriginal: false, + projection: { + storageUsed: true + } + } + ); + } catch (err) { + res.json({ + error: 'MongoDB Error: ' + err.message, + code: 'InternalDatabaseError' + }); + return next(); + } + + if (!updateResponse || !updateResponse.value) { + res.json({ + error: 'This user does not exist', + code: 'UserNotFound' + }); + return next(); + } + + res.json({ + success: true, + storageUsed: Number(updateResponse.value.storageUsed) || 0 + }); + return next(); + }) + ); /** * @api {post} /users/:id/password/reset Reset password for an User @@ -1520,56 +1597,64 @@ module.exports = (db, server, userHandler) => { * "error": "This user does not exist" * } */ - server.post('/users/:user/password/reset', (req, res, next) => { - res.charSet('utf-8'); + server.post( + '/users/:user/password/reset', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string() - .hex() - .lowercase() - .length(24) - .required(), - validAfter: Joi.date() - .empty('') - .allow(false), - sess: Joi.string().max(255), - ip: Joi.string().ip({ - version: ['ipv4', 'ipv6'], - cidr: 'forbidden' - }) - }); - - const result = Joi.validate(req.params, schema, { - abortEarly: false, - convert: true - }); - - if (result.error) { - res.json({ - error: result.error.message, - code: 'InputValidationError' + const schema = Joi.object().keys({ + user: Joi.string() + .hex() + .lowercase() + .length(24) + .required(), + validAfter: Joi.date() + .empty('') + .allow(false), + sess: Joi.string().max(255), + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) }); - return next(); - } - let user = new ObjectID(result.value.user); + const result = Joi.validate(req.params, schema, { + abortEarly: false, + convert: true + }); - userHandler.reset(user, result.value, (err, password) => { - if (err) { + if (result.error) { + res.json({ + error: result.error.message, + code: 'InputValidationError' + }); + return next(); + } + + // permissions check + req.validate(roles.can(req.role).updateAny('users')); + + let user = new ObjectID(result.value.user); + + let password; + try { + password = await resetUser(user, result.value); + } catch (err) { res.json({ error: err.message, code: err.code }); return next(); } + res.json({ success: true, password, - validAfter: result.value || new Date() + validAfter: (result.value && result.value.validAfter) || new Date() }); return next(); - }); - }); + }) + ); /** * @api {delete} /users/:id Delete an User @@ -1604,46 +1689,54 @@ module.exports = (db, server, userHandler) => { * "error": "This user does not exist" * } */ - server.del('/users/:user', (req, res, next) => { - res.charSet('utf-8'); + server.del( + '/users/:user', + tools.asyncifyJson(async (req, res, next) => { + res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string() - .hex() - .lowercase() - .length(24) - .required(), - sess: Joi.string().max(255), - ip: Joi.string().ip({ - version: ['ipv4', 'ipv6'], - cidr: 'forbidden' - }) - }); - - if (req.query.sess) { - req.params.sess = req.query.sess; - } - - if (req.query.ip) { - req.params.ip = req.query.ip; - } - - const result = Joi.validate(req.params, schema, { - abortEarly: false, - convert: true - }); - - if (result.error) { - res.json({ - error: result.error.message, - code: 'InputValidationError' + const schema = Joi.object().keys({ + user: Joi.string() + .hex() + .lowercase() + .length(24) + .required(), + sess: Joi.string().max(255), + ip: Joi.string().ip({ + version: ['ipv4', 'ipv6'], + cidr: 'forbidden' + }) }); - return next(); - } - let user = new ObjectID(result.value.user); - userHandler.delete(user, {}, (err, status) => { - if (err) { + if (req.query.sess) { + req.params.sess = req.query.sess; + } + + if (req.query.ip) { + req.params.ip = req.query.ip; + } + + const result = Joi.validate(req.params, schema, { + abortEarly: false, + convert: true + }); + + if (result.error) { + res.json({ + error: result.error.message, + code: 'InputValidationError' + }); + return next(); + } + + // permissions check + req.validate(roles.can(req.role).deleteAny('users')); + + let user = new ObjectID(result.value.user); + + let status; + try { + status = await deleteUser(user, {}); + } catch (err) { res.json({ error: err.message, code: err.code @@ -1654,8 +1747,8 @@ module.exports = (db, server, userHandler) => { success: status }); return next(); - }); - }); + }) + ); }; async function getKeyInfo(pubKey) {