This commit is contained in:
Andris Reinman 2017-07-24 20:32:22 +03:00
parent 9c6e378edb
commit 534e917b0f
3 changed files with 137 additions and 33 deletions

106
api.js
View file

@ -300,6 +300,10 @@ server.post('/users', (req, res, next) => {
language: Joi.string().min(2).max(20).lowercase(),
retention: Joi.number().min(0).default(0),
name: Joi.string().max(256),
forward: Joi.string().email(),
targetUrl: Joi.string().max(256),
quota: Joi.number().min(0).default(0),
recipients: Joi.number().min(0).default(0),
forwards: Joi.number().min(0).default(0),
@ -310,6 +314,13 @@ server.post('/users', (req, res, next) => {
})
});
let forward = req.params.forward ? tools.normalizeAddress(req.params.forward) : false;
if (forward && /[\u0080-\uFFFF]/.test(forward)) {
// replace unicode characters in email addresses before validation
req.params.forward = forward.replace(/[\u0080-\uFFFF]/g, 'x');
}
const result = Joi.validate(req.params, schema, {
abortEarly: false,
convert: true
@ -322,6 +333,10 @@ server.post('/users', (req, res, next) => {
return next();
}
if (forward) {
result.value.forward = forward;
}
userHandler.create(result.value, (err, id) => {
if (err) {
res.json({
@ -390,7 +405,7 @@ server.post('/authenticate', (req, res, next) => {
res.json({
success: true,
id: authData._id,
id: authData.user,
username: authData.username,
scope: authData.scope,
require2fa: authData.require2fa
@ -414,7 +429,6 @@ server.put('/users/:user', (req, res, next) => {
name: Joi.string().max(256),
forward: Joi.string().email(),
targetUrl: Joi.string().max(256),
autoreply: Joi.string().max(256),
retention: Joi.number().min(0),
quota: Joi.number().min(0),
@ -955,6 +969,8 @@ server.get('/users/:user', (req, res, next) => {
language: userData.language,
retention: userData.retention || false,
enabled2fa: userData.enabled2fa,
limits: {
quota: {
allowed: Number(userData.quota) || config.maxStorage * 1024 * 1024,
@ -3162,7 +3178,11 @@ server.post('/users/:user/asps', (req, res, next) => {
const schema = Joi.object().keys({
user: Joi.string().hex().lowercase().length(24).required(),
description: Joi.string().trim().max(255).required(),
scopes: Joi.array().items(Joi.string().valid('imap', 'pop3', 'smtp', '*').required()).unique()
scopes: Joi.array().items(Joi.string().valid('imap', 'pop3', 'smtp', '*').required()).unique(),
ip: Joi.string().ip({
version: ['ipv4', 'ipv6'],
cidr: 'forbidden'
})
});
const result = Joi.validate(req.params, schema, {
@ -3178,10 +3198,8 @@ server.post('/users/:user/asps', (req, res, next) => {
}
let user = new ObjectID(result.value.user);
let description = result.value.description;
let scopes = result.value.scopes;
userHandler.generateASP(user, description, scopes, (err, result) => {
userHandler.generateASP(user, result.value, (err, result) => {
if (err) {
res.json({
error: err.message
@ -3202,7 +3220,11 @@ server.del('/users/:user/asps/:asp', (req, res, next) => {
const schema = Joi.object().keys({
user: Joi.string().hex().lowercase().length(24).required(),
asp: Joi.string().hex().lowercase().length(24).required()
asp: Joi.string().hex().lowercase().length(24).required(),
ip: Joi.string().ip({
version: ['ipv4', 'ipv6'],
cidr: 'forbidden'
})
});
const result = Joi.validate(req.params, schema, {
@ -3220,24 +3242,13 @@ server.del('/users/:user/asps/:asp', (req, res, next) => {
let user = new ObjectID(result.value.user);
let asp = new ObjectID(result.value.asp);
db.users.collection('asps').deleteOne({
_id: asp,
user
}, (err, r) => {
userHandler.deleteASP(user, asp, result.value, err => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message
error: err.message
});
return next();
}
if (!r.deletedCount) {
res.json({
error: 'Application Specific Password was not found'
});
return next();
}
res.json({
success: true
});
@ -3288,6 +3299,57 @@ server.post('/users/:user/2fa', (req, res, next) => {
});
});
server.get('/users/:user/2fa', (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
user: Joi.string().hex().lowercase().length(24).required(),
token: Joi.string().length(6).required(),
ip: Joi.string().ip({
version: ['ipv4', 'ipv6'],
cidr: 'forbidden'
})
});
req.query.user = req.params.user;
const result = Joi.validate(req.query, schema, {
abortEarly: false,
convert: true
});
if (result.error) {
res.json({
error: result.error.message
});
return next();
}
let user = new ObjectID(result.value.user);
userHandler.check2fa(user, result.value, (err, result) => {
if (err) {
res.json({
error: err.message
});
return next();
}
if (!result) {
res.json({
error: 'Invalid authentication token'
});
return next();
}
res.json({
success: true
});
return next();
});
});
server.put('/users/:user/2fa', (req, res, next) => {
res.charSet('utf-8');
@ -3348,7 +3410,9 @@ server.del('/users/:user/2fa', (req, res, next) => {
})
});
const result = Joi.validate(req.params, schema, {
req.query.user = req.params.user;
const result = Joi.validate(req.query, schema, {
abortEarly: false,
convert: true
});

View file

@ -50,7 +50,7 @@ indexes:
name: user
key:
user: 1
created: -1
_id: -1
- collection: authlog
type: users # index applies to users database
index:

View file

@ -215,7 +215,7 @@ class UserHandler {
});
}
generateASP(user, description, scopes, callback) {
generateASP(user, data, callback) {
let password = generatePassword.generate({
length: 16,
uppercase: false,
@ -226,8 +226,9 @@ class UserHandler {
let allowedScopes = ['imap', 'pop3', 'smtp'];
let hasAllScopes = false;
let scopeSet = new Set();
let scopes = [].concat(data.scopes || []);
(scopes || []).forEach(scope => {
scopes.forEach(scope => {
scope = scope.toLowerCase().trim();
if (scope === '*') {
hasAllScopes = true;
@ -244,7 +245,7 @@ class UserHandler {
let passwordData = {
id: new ObjectID(),
user,
description,
description: data.description,
scopes,
password: bcrypt.hashSync(password, 11),
created: new Date()
@ -270,14 +271,50 @@ class UserHandler {
if (err) {
return callback(err);
}
callback(null, {
id: passwordData._id,
password
});
return this.logAuthEvent(
user,
{
action: 'create asp',
asp: passwordData._id,
result: 'success',
ip: data.ip
},
() =>
callback(null, {
id: passwordData._id,
password
})
);
});
});
}
deleteASP(user, asp, data, callback) {
this.users.collection('asps').deleteOne({
_id: asp,
user
}, (err, r) => {
if (err) {
return callback(err);
}
if (!r.deletedCount) {
return callback(new Error('Application Specific Password was not found'));
}
return this.logAuthEvent(
user,
{
action: 'delete asp',
asp,
result: 'success',
ip: data.ip
},
() => callback(null, true)
);
});
}
create(data, callback) {
this.users.collection('users').findOne({
username: data.username
@ -601,7 +638,8 @@ class UserHandler {
disable2fa(user, data, callback) {
return this.users.collection('users').findOneAndUpdate({
_id: user
_id: user,
enabled2fa: true
}, {
$set: {
enabled2fa: false,
@ -634,6 +672,7 @@ class UserHandler {
}, {
fields: {
username: true,
enabled2fa: true,
seed: true
}
}, (err, userData) => {
@ -642,13 +681,14 @@ class UserHandler {
return callback(new Error('Database Error, failed to find user'));
}
if (!userData) {
let err = new Error('This username does not exist');
let err = new Error('This user does not exist');
return callback(err);
}
if (!userData.seed) {
if (!userData.seed || !userData.enabled2fa) {
// 2fa not set up
return callback(null, true);
let err = new Error('2FA is not enabled for this user');
return callback(err);
}
let verified = speakeasy.totp.verify({