use new permissions system for users api

This commit is contained in:
Andris Reinman 2018-08-29 15:27:57 +03:00
parent fffc259eb0
commit 3b645d39f3
2 changed files with 865 additions and 765 deletions

View file

@ -10,6 +10,13 @@
"authentication": { "authentication": {
"create:any": ["*"], "create:any": ["*"],
"read:any": ["*"] "read:any": ["*"]
},
"users": {
"create:any": ["*"],
"read:any": ["*"],
"update:any": ["*"],
"delete:any": ["*"]
} }
}, },

View file

@ -10,8 +10,16 @@ const openpgp = require('openpgp');
const addressparser = require('nodemailer/lib/addressparser'); const addressparser = require('nodemailer/lib/addressparser');
const libmime = require('libmime'); const libmime = require('libmime');
const consts = require('../consts'); const consts = require('../consts');
const roles = require('../roles');
const util = require('util');
module.exports = (db, server, userHandler) => { 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 * @api {get} /users List registered Users
* @apiName GetUsers * @apiName GetUsers
@ -138,6 +146,9 @@ module.exports = (db, server, userHandler) => {
return next(); return next();
} }
// permissions check
req.validate(roles.can(req.role).readAny('users'));
let query = result.value.query; let query = result.value.query;
let limit = result.value.limit; let limit = result.value.limit;
let page = result.value.page; let page = result.value.page;
@ -361,7 +372,9 @@ module.exports = (db, server, userHandler) => {
* "error": "This username already exists" * "error": "This username already exists"
* } * }
*/ */
server.post('/users', (req, res, next) => { server.post(
'/users',
tools.asyncifyJson(async (req, res, next) => {
res.charSet('utf-8'); res.charSet('utf-8');
const schema = Joi.object().keys({ const schema = Joi.object().keys({
@ -476,6 +489,9 @@ module.exports = (db, server, userHandler) => {
return next(); return next();
} }
// permissions check
req.validate(roles.can(req.role).createAny('users'));
let targets = result.value.targets; let targets = result.value.targets;
if (targets) { if (targets) {
@ -557,10 +573,20 @@ module.exports = (db, server, userHandler) => {
return next(); return next();
} }
checkPubKey(result.value.pubKey) try {
.then(() => { await checkPubKey(result.value.pubKey);
userHandler.create(result.value, (err, id) => { } catch (err) {
if (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({ res.json({
error: err.message, error: err.message,
code: err.code, code: err.code,
@ -575,16 +601,8 @@ module.exports = (db, server, userHandler) => {
}); });
return next(); return next();
});
}) })
.catch(err => { );
res.json({
error: 'PGP key validation failed. ' + err.message,
code: 'InputValidationError'
});
return next();
});
});
/** /**
* @api {get} /users/resolve/:username Resolve ID for an username * @api {get} /users/resolve/:username Resolve ID for an username
@ -619,7 +637,9 @@ module.exports = (db, server, userHandler) => {
* "error": "This user does not exist" * "error": "This user does not exist"
* } * }
*/ */
server.get('/users/resolve/:username', (req, res, next) => { server.get(
'/users/resolve/:username',
tools.asyncifyJson(async (req, res, next) => {
res.charSet('utf-8'); res.charSet('utf-8');
const schema = Joi.object().keys({ const schema = Joi.object().keys({
@ -644,9 +664,14 @@ module.exports = (db, server, userHandler) => {
return next(); return next();
} }
// permissions check
req.validate(roles.can(req.role).readAny('users'));
let username = result.value.username; let username = result.value.username;
db.users.collection('users').findOne( let userData;
try {
userData = await db.users.collection('users').findOne(
{ {
unameview: username.replace(/\./g, '') unameview: username.replace(/\./g, '')
}, },
@ -654,15 +679,16 @@ module.exports = (db, server, userHandler) => {
projection: { projection: {
_id: true _id: true
} }
}, }
(err, userData) => { );
if (err) { } catch (err) {
res.json({ res.json({
error: 'MongoDB Error: ' + err.message, error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError' code: 'InternalDatabaseError'
}); });
return next(); return next();
} }
if (!userData) { if (!userData) {
res.json({ res.json({
error: 'This user does not exist', error: 'This user does not exist',
@ -670,15 +696,15 @@ module.exports = (db, server, userHandler) => {
}); });
return next(); return next();
} }
res.json({ res.json({
success: true, success: true,
id: userData._id id: userData._id
}); });
return next(); return next();
} })
); );
});
/** /**
* @api {get} /users/:id Request User information * @api {get} /users/:id Request User information
@ -793,7 +819,9 @@ module.exports = (db, server, userHandler) => {
* "error": "This user does not exist" * "error": "This user does not exist"
* } * }
*/ */
server.get('/users/:user', (req, res, next) => { server.get(
'/users/:user',
tools.asyncifyJson(async (req, res, next) => {
res.charSet('utf-8'); res.charSet('utf-8');
const schema = Joi.object().keys({ const schema = Joi.object().keys({
@ -817,20 +845,29 @@ module.exports = (db, server, userHandler) => {
return next(); 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 user = new ObjectID(result.value.user); let user = new ObjectID(result.value.user);
db.users.collection('users').findOne( let userData;
{
try {
userData = await db.users.collection('users').findOne({
_id: user _id: user
}, });
(err, userData) => { } catch (err) {
if (err) {
res.json({ res.json({
error: 'MongoDB Error: ' + err.message, error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError' code: 'InternalDatabaseError'
}); });
return next(); return next();
} }
if (!userData) { if (!userData) {
res.json({ res.json({
error: 'This user does not exist', error: 'This user does not exist',
@ -839,7 +876,9 @@ module.exports = (db, server, userHandler) => {
return next(); return next();
} }
db.redis let response;
try {
response = await db.redis
.multi() .multi()
// sending counters are stored in Redis // sending counters are stored in Redis
@ -867,8 +906,8 @@ module.exports = (db, server, userHandler) => {
.get('pdw:' + userData._id.toString()) .get('pdw:' + userData._id.toString())
.ttl('pdw:' + userData._id.toString()) .ttl('pdw:' + userData._id.toString())
.exec((err, result) => { .exec();
if (err) { } catch (err) {
// ignore // ignore
errors.notify(err, { userId: user }); errors.notify(err, { userId: user });
} }
@ -876,28 +915,31 @@ module.exports = (db, server, userHandler) => {
let recipients = Number(userData.recipients) || config.maxRecipients || consts.MAX_RECIPIENTS; let recipients = Number(userData.recipients) || config.maxRecipients || consts.MAX_RECIPIENTS;
let forwards = Number(userData.forwards) || config.maxForwards || consts.MAX_FORWARDS; let forwards = Number(userData.forwards) || config.maxForwards || consts.MAX_FORWARDS;
let recipientsSent = Number(result && result[0] && result[0][1]) || 0; let recipientsSent = Number(response && response[0] && response[0][1]) || 0;
let recipientsTtl = Number(result && result[1] && result[1][1]) || 0; let recipientsTtl = Number(response && response[1] && response[1][1]) || 0;
let forwardsSent = Number(result && result[2] && result[2][1]) || 0; let forwardsSent = Number(response && response[2] && response[2][1]) || 0;
let forwardsTtl = Number(result && result[3] && result[3][1]) || 0; let forwardsTtl = Number(response && response[3] && response[3][1]) || 0;
let received = Number(result && result[4] && result[4][1]) || 0; let received = Number(response && response[4] && response[4][1]) || 0;
let receivedTtl = Number(result && result[5] && result[5][1]) || 0; let receivedTtl = Number(response && response[5] && response[5][1]) || 0;
let imapUpload = Number(result && result[6] && result[6][1]) || 0; let imapUpload = Number(response && response[6] && response[6][1]) || 0;
let imapUploadTtl = Number(result && result[7] && result[7][1]) || 0; let imapUploadTtl = Number(response && response[7] && response[7][1]) || 0;
let imapDownload = Number(result && result[8] && result[8][1]) || 0; let imapDownload = Number(response && response[8] && response[8][1]) || 0;
let imapDownloadTtl = Number(result && result[9] && result[9][1]) || 0; let imapDownloadTtl = Number(response && response[9] && response[9][1]) || 0;
let pop3Download = Number(result && result[10] && result[10][1]) || 0; let pop3Download = Number(response && response[10] && response[10][1]) || 0;
let pop3DownloadTtl = Number(result && result[11] && result[11][1]) || 0; let pop3DownloadTtl = Number(response && response[11] && response[11][1]) || 0;
getKeyInfo(userData.pubKey).then(keyInfo => { let keyInfo;
if (err) { try {
keyInfo = await getKeyInfo(userData.pubKey);
} catch (err) {
errors.notify(err, { userId: user, source: 'pgp' }); errors.notify(err, { userId: user, source: 'pgp' });
} }
res.json({ res.json({
success: true, success: true,
id: user, id: user,
@ -970,11 +1012,8 @@ module.exports = (db, server, userHandler) => {
}); });
return next(); return next();
}); })
});
}
); );
});
/** /**
* @api {put} /users/:id Update User information * @api {put} /users/:id Update User information
@ -1033,7 +1072,9 @@ module.exports = (db, server, userHandler) => {
* "error": "This user does not exist" * "error": "This user does not exist"
* } * }
*/ */
server.put('/users/:user', (req, res, next) => { server.put(
'/users/:user',
tools.asyncifyJson(async (req, res, next) => {
res.charSet('utf-8'); res.charSet('utf-8');
const schema = Joi.object().keys({ const schema = Joi.object().keys({
@ -1130,6 +1171,13 @@ module.exports = (db, server, userHandler) => {
return next(); 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 user = new ObjectID(result.value.user);
let targets = result.value.targets; let targets = result.value.targets;
@ -1192,30 +1240,33 @@ module.exports = (db, server, userHandler) => {
result.value.tagsview = tags.map(tag => tag.toLowerCase()); result.value.tagsview = tags.map(tag => tag.toLowerCase());
} }
checkPubKey(result.value.pubKey) try {
.then(() => { await checkPubKey(result.value.pubKey);
userHandler.update(user, result.value, (err, success) => { } catch (err) {
if (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({ res.json({
error: err.message, error: err.message,
code: err.code code: err.code
}); });
return next(); return next();
} }
res.json({ res.json({
success success
}); });
return next(); return next();
});
}) })
.catch(err => { );
res.json({
error: 'PGP key validation failed. ' + err.message,
code: 'InputValidationError'
});
return next();
});
});
/** /**
* @api {put} /users/:id/logout Log out User * @api {put} /users/:id/logout Log out User
@ -1254,7 +1305,9 @@ module.exports = (db, server, userHandler) => {
* "error": "This user does not exist" * "error": "This user does not exist"
* } * }
*/ */
server.put('/users/:user/logout', (req, res, next) => { server.put(
'/users/:user/logout',
tools.asyncifyJson(async (req, res, next) => {
res.charSet('utf-8'); res.charSet('utf-8');
const schema = Joi.object().keys({ const schema = Joi.object().keys({
@ -1286,20 +1339,30 @@ module.exports = (db, server, userHandler) => {
return next(); return next();
} }
userHandler.logout(result.value.user, result.value.reason || 'Logout requested from API', (err, success) => { // permissions check
if (err) { 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({ res.json({
error: err.message, error: err.message,
code: err.code code: err.code
}); });
return next(); return next();
} }
res.json({ res.json({
success success
}); });
return next(); return next();
}); })
}); );
/** /**
* @api {post} /users/:id/quota/reset Recalculate User quota * @api {post} /users/:id/quota/reset Recalculate User quota
@ -1339,7 +1402,9 @@ module.exports = (db, server, userHandler) => {
* "error": "This user does not exist" * "error": "This user does not exist"
* } * }
*/ */
server.post('/users/:user/quota/reset', (req, res, next) => { server.post(
'/users/:user/quota/reset',
tools.asyncifyJson(async (req, res, next) => {
res.charSet('utf-8'); res.charSet('utf-8');
const schema = Joi.object().keys({ const schema = Joi.object().keys({
@ -1368,9 +1433,18 @@ module.exports = (db, server, userHandler) => {
return next(); 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 user = new ObjectID(result.value.user);
db.users.collection('users').findOne( let userData;
try {
userData = await db.users.collection('users').findOne(
{ {
_id: user _id: user
}, },
@ -1378,9 +1452,9 @@ module.exports = (db, server, userHandler) => {
projection: { projection: {
storageUsed: true storageUsed: true
} }
}, }
(err, userData) => { );
if (err) { } catch (err) {
res.json({ res.json({
error: 'MongoDB Error: ' + err.message, error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError' code: 'InternalDatabaseError'
@ -1396,9 +1470,11 @@ module.exports = (db, server, userHandler) => {
return next(); return next();
} }
let storageData;
try {
// calculate mailbox size by aggregating the size's of all messages // calculate mailbox size by aggregating the size's of all messages
// NB! Scattered query // NB! Scattered query
db.database storageData = db.database
.collection('messages') .collection('messages')
.aggregate( .aggregate(
[ [
@ -1424,8 +1500,8 @@ module.exports = (db, server, userHandler) => {
} }
} }
) )
.toArray((err, result) => { .toArray();
if (err) { } catch (err) {
res.json({ res.json({
error: 'MongoDB Error: ' + err.message, error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError' code: 'InternalDatabaseError'
@ -1433,10 +1509,12 @@ module.exports = (db, server, userHandler) => {
return next(); return next();
} }
let storageUsed = (result && result[0] && result[0].storageUsed) || 0; let storageUsed = (storageData && storageData[0] && storageData[0].storageUsed) || 0;
let updateResponse;
try {
// update quota counter // update quota counter
db.users.collection('users').findOneAndUpdate( updateResponse = await db.users.collection('users').findOneAndUpdate(
{ {
_id: userData._id _id: userData._id
}, },
@ -1446,10 +1524,13 @@ module.exports = (db, server, userHandler) => {
} }
}, },
{ {
returnOriginal: false returnOriginal: false,
}, projection: {
(err, result) => { storageUsed: true
if (err) { }
}
);
} catch (err) {
res.json({ res.json({
error: 'MongoDB Error: ' + err.message, error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError' code: 'InternalDatabaseError'
@ -1457,7 +1538,7 @@ module.exports = (db, server, userHandler) => {
return next(); return next();
} }
if (!result || !result.value) { if (!updateResponse || !updateResponse.value) {
res.json({ res.json({
error: 'This user does not exist', error: 'This user does not exist',
code: 'UserNotFound' code: 'UserNotFound'
@ -1467,15 +1548,11 @@ module.exports = (db, server, userHandler) => {
res.json({ res.json({
success: true, success: true,
storageUsed: Number(result.value.storageUsed) || 0 storageUsed: Number(updateResponse.value.storageUsed) || 0
}); });
return next(); return next();
} })
); );
});
}
);
});
/** /**
* @api {post} /users/:id/password/reset Reset password for an User * @api {post} /users/:id/password/reset Reset password for an User
@ -1520,7 +1597,9 @@ module.exports = (db, server, userHandler) => {
* "error": "This user does not exist" * "error": "This user does not exist"
* } * }
*/ */
server.post('/users/:user/password/reset', (req, res, next) => { server.post(
'/users/:user/password/reset',
tools.asyncifyJson(async (req, res, next) => {
res.charSet('utf-8'); res.charSet('utf-8');
const schema = Joi.object().keys({ const schema = Joi.object().keys({
@ -1552,24 +1631,30 @@ module.exports = (db, server, userHandler) => {
return next(); return next();
} }
// permissions check
req.validate(roles.can(req.role).updateAny('users'));
let user = new ObjectID(result.value.user); let user = new ObjectID(result.value.user);
userHandler.reset(user, result.value, (err, password) => { let password;
if (err) { try {
password = await resetUser(user, result.value);
} catch (err) {
res.json({ res.json({
error: err.message, error: err.message,
code: err.code code: err.code
}); });
return next(); return next();
} }
res.json({ res.json({
success: true, success: true,
password, password,
validAfter: result.value || new Date() validAfter: (result.value && result.value.validAfter) || new Date()
}); });
return next(); return next();
}); })
}); );
/** /**
* @api {delete} /users/:id Delete an User * @api {delete} /users/:id Delete an User
@ -1604,7 +1689,9 @@ module.exports = (db, server, userHandler) => {
* "error": "This user does not exist" * "error": "This user does not exist"
* } * }
*/ */
server.del('/users/:user', (req, res, next) => { server.del(
'/users/:user',
tools.asyncifyJson(async (req, res, next) => {
res.charSet('utf-8'); res.charSet('utf-8');
const schema = Joi.object().keys({ const schema = Joi.object().keys({
@ -1641,9 +1728,15 @@ module.exports = (db, server, userHandler) => {
return next(); return next();
} }
// permissions check
req.validate(roles.can(req.role).deleteAny('users'));
let user = new ObjectID(result.value.user); let user = new ObjectID(result.value.user);
userHandler.delete(user, {}, (err, status) => {
if (err) { let status;
try {
status = await deleteUser(user, {});
} catch (err) {
res.json({ res.json({
error: err.message, error: err.message,
code: err.code code: err.code
@ -1654,8 +1747,8 @@ module.exports = (db, server, userHandler) => {
success: status success: status
}); });
return next(); return next();
}); })
}); );
}; };
async function getKeyInfo(pubKey) { async function getKeyInfo(pubKey) {