2015-07-20 00:09:47 -07:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
exports = module.exports = {
|
2021-04-14 15:54:09 -07:00
|
|
|
removePrivateFields,
|
|
|
|
|
removeRestrictedFields,
|
|
|
|
|
|
|
|
|
|
getAll,
|
|
|
|
|
getAllPaged,
|
|
|
|
|
create,
|
|
|
|
|
isActivated,
|
|
|
|
|
verify,
|
|
|
|
|
verifyWithUsername,
|
|
|
|
|
verifyWithEmail,
|
|
|
|
|
remove,
|
|
|
|
|
get,
|
|
|
|
|
getByResetToken,
|
|
|
|
|
getByUsername,
|
|
|
|
|
getAdmins,
|
2021-04-19 20:52:10 -07:00
|
|
|
getSuperadmins,
|
2021-04-14 15:54:09 -07:00
|
|
|
setPassword,
|
|
|
|
|
update,
|
|
|
|
|
createOwner,
|
|
|
|
|
getOwner,
|
|
|
|
|
createInvite,
|
|
|
|
|
sendInvite,
|
|
|
|
|
setMembership,
|
|
|
|
|
setTwoFactorAuthenticationSecret,
|
|
|
|
|
enableTwoFactorAuthentication,
|
|
|
|
|
disableTwoFactorAuthentication,
|
|
|
|
|
|
|
|
|
|
sendPasswordResetByIdentifier,
|
2020-07-09 14:42:39 -07:00
|
|
|
|
2021-04-30 13:21:50 +02:00
|
|
|
checkLoginLocation,
|
|
|
|
|
|
2020-07-09 16:39:29 -07:00
|
|
|
setupAccount,
|
2021-04-29 12:49:48 -07:00
|
|
|
getAvatarUrl,
|
2020-07-09 22:33:36 -07:00
|
|
|
setAvatar,
|
2021-04-29 12:49:48 -07:00
|
|
|
getAvatar,
|
2020-07-09 16:39:29 -07:00
|
|
|
|
2021-04-14 15:54:09 -07:00
|
|
|
count,
|
2020-01-31 15:28:42 -08:00
|
|
|
|
|
|
|
|
AP_MAIL: 'mail',
|
|
|
|
|
AP_WEBADMIN: 'webadmin',
|
|
|
|
|
|
2020-02-21 12:17:06 -08:00
|
|
|
ROLE_ADMIN: 'admin',
|
|
|
|
|
ROLE_USER: 'user',
|
|
|
|
|
ROLE_USER_MANAGER: 'usermanager',
|
|
|
|
|
ROLE_OWNER: 'owner',
|
2021-04-14 15:54:09 -07:00
|
|
|
compareRoles,
|
2020-02-21 12:17:06 -08:00
|
|
|
|
2021-04-14 15:54:09 -07:00
|
|
|
getAppPasswords,
|
|
|
|
|
getAppPassword,
|
|
|
|
|
addAppPassword,
|
|
|
|
|
delAppPassword
|
2015-07-20 00:09:47 -07:00
|
|
|
};
|
|
|
|
|
|
2020-02-21 12:17:06 -08:00
|
|
|
const ORDERED_ROLES = [ exports.ROLE_USER, exports.ROLE_USER_MANAGER, exports.ROLE_ADMIN, exports.ROLE_OWNER ];
|
|
|
|
|
|
|
|
|
|
let assert = require('assert'),
|
2019-10-22 16:34:17 -07:00
|
|
|
BoxError = require('./boxerror.js'),
|
2015-07-20 00:09:47 -07:00
|
|
|
crypto = require('crypto'),
|
2016-07-12 10:07:55 -07:00
|
|
|
constants = require('./constants.js'),
|
2016-05-29 23:15:55 -07:00
|
|
|
debug = require('debug')('box:user'),
|
2016-05-01 20:01:34 -07:00
|
|
|
eventlog = require('./eventlog.js'),
|
2019-10-25 15:58:11 -07:00
|
|
|
externalLdap = require('./externalldap.js'),
|
2016-02-08 15:16:59 -08:00
|
|
|
groups = require('./groups.js'),
|
2018-06-11 12:38:15 -07:00
|
|
|
hat = require('./hat.js'),
|
2016-02-08 15:15:42 -08:00
|
|
|
mailer = require('./mailer.js'),
|
2020-03-15 11:41:39 -07:00
|
|
|
paths = require('./paths.js'),
|
2018-04-25 19:08:15 +02:00
|
|
|
qrcode = require('qrcode'),
|
2016-07-12 10:07:55 -07:00
|
|
|
safe = require('safetydance'),
|
2019-07-26 10:49:29 -07:00
|
|
|
settings = require('./settings.js'),
|
2018-04-25 19:08:15 +02:00
|
|
|
speakeasy = require('speakeasy'),
|
2020-07-09 16:39:29 -07:00
|
|
|
tokens = require('./tokens.js'),
|
2016-02-08 15:15:42 -08:00
|
|
|
userdb = require('./userdb.js'),
|
2017-08-13 17:44:31 -07:00
|
|
|
uuid = require('uuid'),
|
2021-04-30 13:21:50 +02:00
|
|
|
superagent = require('superagent'),
|
2015-07-20 00:09:47 -07:00
|
|
|
validator = require('validator'),
|
|
|
|
|
_ = require('underscore');
|
|
|
|
|
|
|
|
|
|
var CRYPTO_SALT_SIZE = 64; // 512-bit salt
|
|
|
|
|
var CRYPTO_ITERATIONS = 10000; // iterations
|
|
|
|
|
var CRYPTO_KEY_LENGTH = 512; // bits
|
2017-01-04 14:53:21 +01:00
|
|
|
var CRYPTO_DIGEST = 'sha1'; // used to be the default in node 4.1.1 cannot change since it will affect existing db records
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2018-01-25 18:03:26 +01:00
|
|
|
// keep this in sync with validateGroupname and validateAlias
|
2015-07-20 00:09:47 -07:00
|
|
|
function validateUsername(username) {
|
|
|
|
|
assert.strictEqual(typeof username, 'string');
|
2016-04-01 16:48:34 +02:00
|
|
|
|
2019-10-24 14:40:26 -07:00
|
|
|
if (username.length < 1) return new BoxError(BoxError.BAD_FIELD, 'Username must be atleast 1 char');
|
|
|
|
|
if (username.length >= 200) return new BoxError(BoxError.BAD_FIELD, 'Username too long');
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2019-10-24 14:40:26 -07:00
|
|
|
if (constants.RESERVED_NAMES.indexOf(username) !== -1) return new BoxError(BoxError.BAD_FIELD, 'Username is reserved');
|
2016-04-13 16:50:20 -07:00
|
|
|
|
2020-07-23 16:29:54 -07:00
|
|
|
// also need to consider valid LDAP characters here (e.g '+' is reserved). apps like openvpn require _ to not be used
|
2019-10-24 14:40:26 -07:00
|
|
|
if (/[^a-zA-Z0-9.-]/.test(username)) return new BoxError(BoxError.BAD_FIELD, 'Username can only contain alphanumerals, dot and -');
|
2016-05-25 21:36:20 -07:00
|
|
|
|
2016-05-30 01:32:18 -07:00
|
|
|
// app emails are sent using the .app suffix
|
2019-10-24 14:40:26 -07:00
|
|
|
if (username.indexOf('.app') !== -1) return new BoxError(BoxError.BAD_FIELD, 'Username pattern is reserved for apps');
|
2016-05-18 21:45:02 -07:00
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateEmail(email) {
|
|
|
|
|
assert.strictEqual(typeof email, 'string');
|
|
|
|
|
|
2019-10-24 14:40:26 -07:00
|
|
|
if (!validator.isEmail(email)) return new BoxError(BoxError.BAD_FIELD, 'Invalid email');
|
2015-07-20 00:09:47 -07:00
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateToken(token) {
|
|
|
|
|
assert.strictEqual(typeof token, 'string');
|
|
|
|
|
|
2019-10-24 14:40:26 -07:00
|
|
|
if (token.length !== 64) return new BoxError(BoxError.BAD_FIELD, 'Invalid token'); // 256-bit hex coded token
|
2015-07-20 00:09:47 -07:00
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2016-01-19 23:34:49 -08:00
|
|
|
function validateDisplayName(name) {
|
|
|
|
|
assert.strictEqual(typeof name, 'string');
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-11 12:55:24 -07:00
|
|
|
function validatePassword(password) {
|
|
|
|
|
assert.strictEqual(typeof password, 'string');
|
|
|
|
|
|
2019-10-24 14:40:26 -07:00
|
|
|
if (password.length < 8) return new BoxError(BoxError.BAD_FIELD, 'Password must be atleast 8 characters');
|
|
|
|
|
if (password.length > 256) return new BoxError(BoxError.BAD_FIELD, 'Password cannot be more than 256 characters');
|
2018-06-11 12:55:24 -07:00
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-25 15:54:24 -07:00
|
|
|
// remove all fields that should never be sent out via REST API
|
2018-03-02 11:24:06 +01:00
|
|
|
function removePrivateFields(user) {
|
2021-04-14 21:46:35 -07:00
|
|
|
return _.pick(user, 'id', 'username', 'email', 'fallbackEmail', 'displayName', 'groupIds', 'active', 'source', 'role', 'createdAt', 'twoFactorAuthenticationEnabled');
|
2018-03-02 11:24:06 +01:00
|
|
|
}
|
|
|
|
|
|
2018-06-25 15:54:24 -07:00
|
|
|
// remove all fields that Non-privileged users must not see
|
|
|
|
|
function removeRestrictedFields(user) {
|
2019-08-08 07:19:50 -07:00
|
|
|
return _.pick(user, 'id', 'username', 'email', 'displayName', 'active');
|
2018-06-25 15:54:24 -07:00
|
|
|
}
|
|
|
|
|
|
2018-06-11 14:47:24 -07:00
|
|
|
function create(username, password, email, displayName, options, auditSource, callback) {
|
2017-02-02 00:23:11 -08:00
|
|
|
assert(username === null || typeof username === 'string');
|
2018-06-11 12:59:52 -07:00
|
|
|
assert(password === null || typeof password === 'string');
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof email, 'string');
|
2016-01-19 23:34:49 -08:00
|
|
|
assert.strictEqual(typeof displayName, 'string');
|
2018-06-11 14:47:24 -07:00
|
|
|
assert(options && typeof options === 'object');
|
2019-01-23 11:18:31 +01:00
|
|
|
assert(auditSource && typeof auditSource === 'object');
|
2016-02-08 21:05:02 -08:00
|
|
|
|
2019-08-29 22:15:48 +02:00
|
|
|
const source = options.source || ''; // empty is local user
|
2020-02-21 12:17:06 -08:00
|
|
|
const role = options.role || exports.ROLE_USER;
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2017-02-02 00:23:11 -08:00
|
|
|
var error;
|
2016-04-14 16:25:46 +02:00
|
|
|
|
2017-02-02 00:23:11 -08:00
|
|
|
if (username !== null) {
|
|
|
|
|
username = username.toLowerCase();
|
|
|
|
|
error = validateUsername(username);
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
}
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2018-06-11 12:59:52 -07:00
|
|
|
if (password !== null) {
|
|
|
|
|
error = validatePassword(password);
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2018-06-11 12:59:52 -07:00
|
|
|
} else {
|
|
|
|
|
password = hat(8 * 8);
|
|
|
|
|
}
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2017-02-02 00:23:11 -08:00
|
|
|
email = email.toLowerCase();
|
2015-07-20 00:09:47 -07:00
|
|
|
error = validateEmail(email);
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
2016-01-19 23:34:49 -08:00
|
|
|
error = validateDisplayName(displayName);
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
2020-02-21 12:17:06 -08:00
|
|
|
error = validateRole(role);
|
2020-02-13 22:06:54 -08:00
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
2015-07-20 00:09:47 -07:00
|
|
|
crypto.randomBytes(CRYPTO_SALT_SIZE, function (error, salt) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(new BoxError(BoxError.CRYPTO_ERROR, error));
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2017-01-04 14:53:21 +01:00
|
|
|
crypto.pbkdf2(password, salt, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST, function (error, derivedKey) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(new BoxError(BoxError.CRYPTO_ERROR, error));
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2015-09-17 13:50:20 -07:00
|
|
|
var now = (new Date()).toISOString();
|
2015-07-20 00:09:47 -07:00
|
|
|
var user = {
|
2016-04-01 16:48:34 +02:00
|
|
|
id: 'uid-' + uuid.v4(),
|
2015-07-20 00:09:47 -07:00
|
|
|
username: username,
|
|
|
|
|
email: email,
|
2020-03-20 13:39:04 -07:00
|
|
|
fallbackEmail: email,
|
2019-03-21 20:06:14 -07:00
|
|
|
password: Buffer.from(derivedKey, 'binary').toString('hex'),
|
2015-07-20 00:09:47 -07:00
|
|
|
salt: salt.toString('hex'),
|
|
|
|
|
createdAt: now,
|
2018-08-17 09:49:58 -07:00
|
|
|
resetToken: '',
|
2018-07-26 17:17:52 -07:00
|
|
|
displayName: displayName,
|
2020-02-13 22:06:54 -08:00
|
|
|
source: source,
|
2020-02-21 12:17:06 -08:00
|
|
|
role: role
|
2015-07-20 00:09:47 -07:00
|
|
|
};
|
|
|
|
|
|
2017-02-14 10:42:32 -08:00
|
|
|
userdb.add(user.id, user, function (error) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2019-01-19 14:34:49 +01:00
|
|
|
// when this is used to create the owner, then we have to patch the auditSource to contain himself
|
2020-02-21 12:17:06 -08:00
|
|
|
if (!auditSource.userId) auditSource.userId = user.id;
|
|
|
|
|
if (!auditSource.username) auditSource.username= user.username;
|
2019-01-19 14:34:49 +01:00
|
|
|
|
2020-02-21 12:17:06 -08:00
|
|
|
eventlog.add(eventlog.ACTION_USER_ADD, auditSource, { userId: user.id, email: user.email, user: removePrivateFields(user) });
|
2016-09-22 15:55:18 -07:00
|
|
|
|
2019-01-17 13:12:26 +01:00
|
|
|
callback(null, user);
|
2015-07-20 00:09:47 -07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-12 10:07:55 -07:00
|
|
|
// returns true if ghost user was matched
|
|
|
|
|
function verifyGhost(username, password) {
|
|
|
|
|
assert.strictEqual(typeof username, 'string');
|
|
|
|
|
assert.strictEqual(typeof password, 'string');
|
|
|
|
|
|
2020-03-15 11:41:39 -07:00
|
|
|
var ghostData = safe.JSON.parse(safe.fs.readFileSync(paths.GHOST_USER_FILE, 'utf8'));
|
2016-07-12 10:07:55 -07:00
|
|
|
if (!ghostData) return false;
|
|
|
|
|
|
|
|
|
|
if (username in ghostData && ghostData[username] === password) {
|
|
|
|
|
debug('verifyGhost: matched ghost user');
|
2020-09-17 10:46:15 -07:00
|
|
|
safe.fs.unlinkSync(paths.GHOST_USER_FILE);
|
2016-07-12 10:07:55 -07:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-31 15:28:42 -08:00
|
|
|
function verifyAppPassword(userId, password, identifier, callback) {
|
|
|
|
|
assert.strictEqual(typeof userId, 'string');
|
|
|
|
|
assert.strictEqual(typeof password, 'string');
|
|
|
|
|
assert.strictEqual(typeof identifier, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
userdb.getAppPasswords(userId, function (error, results) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function verify(userId, password, identifier, callback) {
|
2016-04-05 16:27:04 +02:00
|
|
|
assert.strictEqual(typeof userId, 'string');
|
|
|
|
|
assert.strictEqual(typeof password, 'string');
|
2020-01-31 15:28:42 -08:00
|
|
|
assert.strictEqual(typeof identifier, 'string');
|
2016-04-05 16:27:04 +02:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2018-06-11 11:39:49 -07:00
|
|
|
get(userId, function (error, user) {
|
2016-09-26 16:28:52 +02:00
|
|
|
if (error) return callback(error);
|
2016-04-05 16:27:04 +02:00
|
|
|
|
2019-10-24 14:40:26 -07:00
|
|
|
if (!user.active) return callback(new BoxError(BoxError.NOT_FOUND));
|
2019-08-08 05:45:56 -07:00
|
|
|
|
2017-02-20 21:48:31 +01:00
|
|
|
// for just invited users the username may be still null
|
2018-05-14 14:49:31 -07:00
|
|
|
if (user.username && verifyGhost(user.username, password)) {
|
|
|
|
|
user.ghost = true;
|
|
|
|
|
return callback(null, user);
|
|
|
|
|
}
|
2016-07-12 10:07:55 -07:00
|
|
|
|
2020-01-31 15:28:42 -08:00
|
|
|
verifyAppPassword(user.id, password, identifier, function (error) {
|
|
|
|
|
if (!error) {
|
|
|
|
|
user.appPassword = true;
|
|
|
|
|
return callback(null, user);
|
|
|
|
|
}
|
2019-08-29 22:43:06 +02:00
|
|
|
|
2020-01-31 15:28:42 -08:00
|
|
|
if (user.source === 'ldap') {
|
|
|
|
|
externalLdap.verifyPassword(user, password, function (error) {
|
|
|
|
|
if (error) return callback(error);
|
2016-04-05 16:27:04 +02:00
|
|
|
|
2020-01-31 15:28:42 -08:00
|
|
|
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));
|
2016-04-05 16:27:04 +02:00
|
|
|
|
2020-01-31 15:28:42 -08:00
|
|
|
var derivedKeyHex = Buffer.from(derivedKey, 'binary').toString('hex');
|
|
|
|
|
if (derivedKeyHex !== user.password) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
|
|
|
|
|
|
|
|
|
callback(null, user);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
2016-04-05 16:27:04 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-31 15:28:42 -08:00
|
|
|
function verifyWithUsername(username, password, identifier, callback) {
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof username, 'string');
|
|
|
|
|
assert.strictEqual(typeof password, 'string');
|
2020-01-31 15:28:42 -08:00
|
|
|
assert.strictEqual(typeof identifier, 'string');
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2016-04-14 16:25:46 +02:00
|
|
|
userdb.getByUsername(username.toLowerCase(), function (error, user) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2020-01-31 15:28:42 -08:00
|
|
|
verify(user.id, password, identifier, callback);
|
2015-07-20 00:09:47 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-31 15:28:42 -08:00
|
|
|
function verifyWithEmail(email, password, identifier, callback) {
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof email, 'string');
|
|
|
|
|
assert.strictEqual(typeof password, 'string');
|
2020-01-31 15:28:42 -08:00
|
|
|
assert.strictEqual(typeof identifier, 'string');
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2018-01-22 16:12:13 +01:00
|
|
|
userdb.getByEmail(email.toLowerCase(), function (error, user) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2016-09-27 15:41:34 +02:00
|
|
|
|
2020-01-31 15:28:42 -08:00
|
|
|
verify(user.id, password, identifier, callback);
|
2015-07-20 00:09:47 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-14 15:54:09 -07:00
|
|
|
function remove(user, auditSource, callback) {
|
2020-02-13 20:45:00 -08:00
|
|
|
assert.strictEqual(typeof user, 'object');
|
2019-01-23 11:18:31 +01:00
|
|
|
assert(auditSource && typeof auditSource === 'object');
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2020-02-13 20:45:00 -08:00
|
|
|
if (settings.isDemo() && user.username === constants.DEMO_USERNAME) return callback(new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'));
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2020-02-13 20:45:00 -08:00
|
|
|
userdb.del(user.id, function (error) {
|
|
|
|
|
if (error) return callback(error);
|
2016-05-01 20:09:31 -07:00
|
|
|
|
2020-02-13 20:45:00 -08:00
|
|
|
eventlog.add(eventlog.ACTION_USER_REMOVE, auditSource, { userId: user.id, user: removePrivateFields(user) }, callback);
|
2015-07-20 00:09:47 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-14 16:39:20 +01:00
|
|
|
function getAll(callback) {
|
2016-02-09 09:25:17 -08:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2016-06-03 00:04:17 -07:00
|
|
|
userdb.getAllWithGroupIds(function (error, results) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2016-02-09 09:25:17 -08:00
|
|
|
|
2018-01-21 14:50:24 +01:00
|
|
|
return callback(null, results);
|
2016-02-09 09:25:17 -08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-15 17:21:40 +01:00
|
|
|
function getAllPaged(search, page, perPage, callback) {
|
|
|
|
|
assert(typeof search === 'string' || search === null);
|
2019-01-14 16:39:20 +01:00
|
|
|
assert.strictEqual(typeof page, 'number');
|
|
|
|
|
assert.strictEqual(typeof perPage, 'number');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2019-01-15 17:21:40 +01:00
|
|
|
userdb.getAllWithGroupIdsPaged(search, page, perPage, function (error, results) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2019-01-14 16:39:20 +01:00
|
|
|
|
|
|
|
|
return callback(null, results);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-06-07 09:59:29 -07:00
|
|
|
function count(callback) {
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
userdb.count(function (error, count) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2016-06-07 09:59:29 -07:00
|
|
|
|
|
|
|
|
callback(null, count);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-10 18:08:08 -08:00
|
|
|
function isActivated(callback) {
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
count(function (error, count) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
callback(null, count !== 0);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-11 11:39:49 -07:00
|
|
|
function get(userId, callback) {
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof userId, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
userdb.get(userId, function (error, result) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2018-06-18 13:57:17 -07:00
|
|
|
groups.getMembership(userId, function (error, groupIds) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2016-02-08 20:38:50 -08:00
|
|
|
|
|
|
|
|
result.groupIds = groupIds;
|
|
|
|
|
|
2018-01-21 14:50:24 +01:00
|
|
|
return callback(null, result);
|
2016-02-08 20:38:50 -08:00
|
|
|
});
|
2015-07-20 00:09:47 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-04 17:05:08 +01:00
|
|
|
function getByResetToken(resetToken, callback) {
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof resetToken, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2020-02-04 18:05:12 +01:00
|
|
|
var error = validateToken(resetToken);
|
2015-07-20 00:09:47 -07:00
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
2020-02-04 17:05:08 +01:00
|
|
|
userdb.getByResetToken(resetToken, function (error, result) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2018-06-15 16:22:28 -07:00
|
|
|
callback(null, result);
|
2015-07-20 00:09:47 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-18 21:15:50 -07:00
|
|
|
function getByUsername(username, callback) {
|
|
|
|
|
assert.strictEqual(typeof username, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
userdb.getByUsername(username.toLowerCase(), function (error, result) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2019-03-18 21:15:50 -07:00
|
|
|
|
|
|
|
|
get(result.id, callback);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-09 15:41:02 -07:00
|
|
|
function update(user, data, auditSource, callback) {
|
2020-02-13 20:45:00 -08:00
|
|
|
assert.strictEqual(typeof user, 'object');
|
2016-06-02 23:53:06 -07:00
|
|
|
assert.strictEqual(typeof data, 'object');
|
2019-01-23 11:18:31 +01:00
|
|
|
assert(auditSource && typeof auditSource === 'object');
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2020-10-23 11:41:39 -07:00
|
|
|
if (settings.isDemo() && user.username === constants.DEMO_USERNAME) return callback(new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'));
|
|
|
|
|
|
2016-06-02 23:53:06 -07:00
|
|
|
var error;
|
2020-02-21 12:17:06 -08:00
|
|
|
data = _.pick(data, 'email', 'fallbackEmail', 'displayName', 'username', 'active', 'role');
|
2016-04-14 16:25:46 +02:00
|
|
|
|
2016-06-02 23:53:06 -07:00
|
|
|
if (_.isEmpty(data)) return callback();
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2016-06-02 23:53:06 -07:00
|
|
|
if (data.username) {
|
|
|
|
|
data.username = data.username.toLowerCase();
|
|
|
|
|
error = validateUsername(data.username);
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.email) {
|
|
|
|
|
data.email = data.email.toLowerCase();
|
|
|
|
|
error = validateEmail(data.email);
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
}
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2018-01-21 14:25:39 +01:00
|
|
|
if (data.fallbackEmail) {
|
|
|
|
|
data.fallbackEmail = data.fallbackEmail.toLowerCase();
|
|
|
|
|
error = validateEmail(data.fallbackEmail);
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-21 12:17:06 -08:00
|
|
|
if (data.role) {
|
|
|
|
|
error = validateRole(data.role);
|
2020-02-13 22:06:54 -08:00
|
|
|
if (error) return callback(error);
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-13 20:45:00 -08:00
|
|
|
userdb.update(user.id, data, function (error) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2016-05-01 20:09:31 -07:00
|
|
|
|
2020-02-14 13:01:51 -08:00
|
|
|
const newUser = _.extend({}, user, data);
|
2020-02-13 20:45:00 -08:00
|
|
|
|
2020-02-14 13:01:51 -08:00
|
|
|
eventlog.add(eventlog.ACTION_USER_UPDATE, auditSource, {
|
|
|
|
|
userId: user.id,
|
|
|
|
|
user: removePrivateFields(newUser),
|
2020-02-21 12:17:06 -08:00
|
|
|
roleChanged: newUser.role !== user.role,
|
2020-02-14 13:01:51 -08:00
|
|
|
activeStatusChanged: ((newUser.active && !user.active) || (!newUser.active && user.active))
|
2017-02-15 23:19:54 -08:00
|
|
|
});
|
2020-02-14 13:01:51 -08:00
|
|
|
|
|
|
|
|
callback(null);
|
2016-09-21 15:34:58 -07:00
|
|
|
});
|
2015-07-20 00:09:47 -07:00
|
|
|
}
|
|
|
|
|
|
2020-02-13 20:45:00 -08:00
|
|
|
function setMembership(user, groupIds, callback) {
|
|
|
|
|
assert.strictEqual(typeof user, 'object');
|
2016-02-09 15:47:02 -08:00
|
|
|
assert(Array.isArray(groupIds));
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2020-02-13 20:45:00 -08:00
|
|
|
groups.setMembership(user.id, groupIds, function (error) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2016-05-04 13:54:32 +02:00
|
|
|
|
2018-07-26 17:17:52 -07:00
|
|
|
callback(null);
|
2016-02-09 15:47:02 -08:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-21 12:17:06 -08:00
|
|
|
function getAdmins(callback) {
|
2016-01-15 16:04:33 +01:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2020-02-21 12:17:06 -08:00
|
|
|
userdb.getByRole(exports.ROLE_OWNER, function (error, owners) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2016-09-27 14:52:15 +02:00
|
|
|
|
2020-02-21 12:17:06 -08:00
|
|
|
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));
|
|
|
|
|
});
|
2016-01-15 16:04:33 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-19 20:52:10 -07:00
|
|
|
function getSuperadmins(callback) {
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
userdb.getByRole(exports.ROLE_OWNER, function (error, owners) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
callback(null, owners);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-09 14:42:39 -07:00
|
|
|
function sendPasswordResetByIdentifier(identifier, callback) {
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof identifier, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2020-07-09 14:42:39 -07:00
|
|
|
const getter = identifier.indexOf('@') === -1 ? userdb.getByUsername : userdb.getByEmail;
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2016-04-14 16:25:46 +02:00
|
|
|
getter(identifier.toLowerCase(), function (error, result) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2020-03-30 16:47:18 -07:00
|
|
|
let resetToken = hat(256), resetTokenCreationTime = new Date();
|
2020-02-13 22:45:07 -08:00
|
|
|
result.resetToken = resetToken;
|
2020-03-30 16:47:18 -07:00
|
|
|
result.resetTokenCreationTime = resetTokenCreationTime;
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2020-03-30 16:47:18 -07:00
|
|
|
userdb.update(result.id, { resetToken, resetTokenCreationTime }, function (error) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2015-07-20 00:09:47 -07:00
|
|
|
|
|
|
|
|
mailer.passwordReset(result);
|
|
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-30 13:21:50 +02:00
|
|
|
function checkLoginLocation(user, ip, userAgent) {
|
|
|
|
|
assert.strictEqual(typeof user, 'object');
|
|
|
|
|
assert.strictEqual(typeof ip, 'string');
|
|
|
|
|
assert.strictEqual(typeof userAgent, 'string');
|
|
|
|
|
|
|
|
|
|
debug(`checkLoginLocation: ${user.id} ${ip} ${userAgent}`);
|
|
|
|
|
|
|
|
|
|
superagent.get('https://geolocation.cloudron.io/json').query({ ip: ip }).end(function (error, result) {
|
|
|
|
|
if (error) return console.error('Failed to get geoip info:', error);
|
|
|
|
|
|
|
|
|
|
const country = result.body.country.names.en;
|
|
|
|
|
const city = result.body.city.names.en;
|
|
|
|
|
|
|
|
|
|
const knownLogin = user.locations.find(function (l) {
|
|
|
|
|
return l.userAgent === userAgent && 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;
|
|
|
|
|
var locations = user.locations.filter(function (l) { return l.ts > sixMonthsBack; });
|
|
|
|
|
|
|
|
|
|
locations.push({ ts: Date.now(), ip, userAgent, country, city });
|
|
|
|
|
userdb.update(user.id, { locations }, function (error) {
|
|
|
|
|
if (error) console.error('checkLoginLocation: Failed to update user location.', error);
|
|
|
|
|
|
|
|
|
|
mailer.sendNewLoginLocation(user, ip, userAgent, country, city);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-13 20:45:00 -08:00
|
|
|
function setPassword(user, newPassword, callback) {
|
|
|
|
|
assert.strictEqual(typeof user, 'object');
|
2015-07-20 00:09:47 -07:00
|
|
|
assert.strictEqual(typeof newPassword, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
var error = validatePassword(newPassword);
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2020-02-13 20:45:00 -08:00
|
|
|
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'));
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2020-02-13 20:45:00 -08:00
|
|
|
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));
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2020-02-13 20:45:00 -08:00
|
|
|
let data = {
|
|
|
|
|
password: Buffer.from(derivedKey, 'binary').toString('hex'),
|
|
|
|
|
resetToken: ''
|
|
|
|
|
};
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2020-02-13 20:45:00 -08:00
|
|
|
userdb.update(user.id, data, function (error) {
|
|
|
|
|
if (error) return callback(error);
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2020-02-13 20:45:00 -08:00
|
|
|
callback();
|
2015-07-20 00:09:47 -07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-01 20:01:34 -07:00
|
|
|
function createOwner(username, password, email, displayName, auditSource, callback) {
|
2016-04-04 15:14:00 +02:00
|
|
|
assert.strictEqual(typeof username, 'string');
|
|
|
|
|
assert.strictEqual(typeof password, 'string');
|
|
|
|
|
assert.strictEqual(typeof email, 'string');
|
|
|
|
|
assert.strictEqual(typeof displayName, 'string');
|
2019-01-23 11:18:31 +01:00
|
|
|
assert(auditSource && typeof auditSource === 'object');
|
2016-04-04 15:14:00 +02:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2016-04-14 16:25:46 +02:00
|
|
|
// This is only not allowed for the owner
|
2019-10-24 14:40:26 -07:00
|
|
|
if (username === '') return callback(new BoxError(BoxError.BAD_FIELD, 'Username cannot be empty'));
|
2016-04-04 15:16:16 +02:00
|
|
|
|
2020-02-21 12:17:06 -08:00
|
|
|
isActivated(function (error, activated) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2020-02-21 12:17:06 -08:00
|
|
|
if (activated) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'Cloudron already activated'));
|
2015-07-20 00:09:47 -07:00
|
|
|
|
2020-02-21 12:17:06 -08:00
|
|
|
create(username, password, email, displayName, { role: exports.ROLE_OWNER }, auditSource, function (error, user) {
|
2018-07-26 17:17:52 -07:00
|
|
|
if (error) return callback(error);
|
2016-02-08 15:16:59 -08:00
|
|
|
|
2018-07-26 17:17:52 -07:00
|
|
|
callback(null, user);
|
2016-02-08 15:16:59 -08:00
|
|
|
});
|
2015-07-20 00:09:47 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2016-01-13 12:28:38 -08:00
|
|
|
function getOwner(callback) {
|
2020-02-21 12:17:06 -08:00
|
|
|
userdb.getByRole(exports.ROLE_OWNER, function (error, results) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2016-01-13 12:28:38 -08:00
|
|
|
|
2020-02-21 12:17:06 -08:00
|
|
|
return callback(null, results[0]);
|
2016-01-13 12:28:38 -08:00
|
|
|
});
|
|
|
|
|
}
|
2016-01-18 15:16:18 +01:00
|
|
|
|
2020-07-09 16:39:29 -07:00
|
|
|
function inviteLink(user, directoryConfig) {
|
2020-02-05 16:34:34 +01:00
|
|
|
let link = `${settings.adminOrigin()}/setupaccount.html?resetToken=${user.resetToken}&email=${encodeURIComponent(user.email)}`;
|
|
|
|
|
|
|
|
|
|
if (user.username) link += `&username=${encodeURIComponent(user.username)}`;
|
|
|
|
|
if (user.displayName) link += `&displayName=${encodeURIComponent(user.displayName)}`;
|
2020-07-09 16:39:29 -07:00
|
|
|
if (directoryConfig.lockUserProfiles) link += '&profileLocked=true';
|
2020-02-05 16:34:34 +01:00
|
|
|
|
|
|
|
|
return link;
|
2020-02-05 15:53:05 +01:00
|
|
|
}
|
|
|
|
|
|
2020-02-13 20:45:00 -08:00
|
|
|
function createInvite(user, callback) {
|
|
|
|
|
assert.strictEqual(typeof user, 'object');
|
2016-01-18 15:16:18 +01:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2020-02-13 20:45:00 -08:00
|
|
|
if (user.source) return callback(new BoxError(BoxError.CONFLICT, 'User is from an external directory'));
|
2019-10-29 11:03:28 -07:00
|
|
|
|
2020-04-08 13:11:24 -07:00
|
|
|
const resetToken = hat(256), resetTokenCreationTime = new Date();
|
2016-01-18 15:16:18 +01:00
|
|
|
|
2020-07-09 16:39:29 -07:00
|
|
|
settings.getDirectoryConfig(function (error, directoryConfig) {
|
2020-02-13 20:45:00 -08:00
|
|
|
if (error) return callback(error);
|
2016-01-18 15:16:18 +01:00
|
|
|
|
2020-07-09 16:39:29 -07:00
|
|
|
userdb.update(user.id, { resetToken, resetTokenCreationTime }, function (error) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
user.resetToken = resetToken;
|
|
|
|
|
|
|
|
|
|
callback(null, { resetToken, inviteLink: inviteLink(user, directoryConfig) });
|
|
|
|
|
});
|
2016-01-18 15:16:18 +01:00
|
|
|
});
|
|
|
|
|
}
|
2016-05-06 13:56:26 +02:00
|
|
|
|
2020-02-13 20:45:00 -08:00
|
|
|
function sendInvite(user, options, callback) {
|
|
|
|
|
assert.strictEqual(typeof user, 'object');
|
2018-08-17 09:49:58 -07:00
|
|
|
assert.strictEqual(typeof options, 'object');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2020-02-13 20:45:00 -08:00
|
|
|
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'));
|
2018-08-17 09:49:58 -07:00
|
|
|
|
2020-07-09 16:39:29 -07:00
|
|
|
settings.getDirectoryConfig(function (error, directoryConfig) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
mailer.sendInvite(user, options.invitor || null, inviteLink(user, directoryConfig));
|
|
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setupAccount(user, data, auditSource, callback) {
|
|
|
|
|
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 updateFunc = (done) => {
|
|
|
|
|
if (directoryConfig.lockUserProfiles) return done();
|
|
|
|
|
update(user, _.pick(data, 'username', 'displayName'), auditSource, done);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
updateFunc(function (error) {
|
|
|
|
|
if (error) return callback(error);
|
2018-08-17 09:49:58 -07:00
|
|
|
|
2020-07-09 16:39:29 -07:00
|
|
|
setPassword(user, data.password, function (error) { // setPassword clears the resetToken
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
tokens.add(tokens.ID_WEBADMIN, user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
callback(null, result.accessToken);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
2018-08-17 09:49:58 -07:00
|
|
|
}
|
|
|
|
|
|
2018-04-25 19:08:15 +02:00
|
|
|
function setTwoFactorAuthenticationSecret(userId, callback) {
|
|
|
|
|
assert.strictEqual(typeof userId, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
userdb.get(userId, function (error, result) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2018-04-25 19:08:15 +02:00
|
|
|
|
2020-07-22 16:18:22 -07:00
|
|
|
if (settings.isDemo() && result.username === constants.DEMO_USERNAME) return callback(new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'));
|
|
|
|
|
|
2019-10-24 14:40:26 -07:00
|
|
|
if (result.twoFactorAuthenticationEnabled) return callback(new BoxError(BoxError.ALREADY_EXISTS));
|
2018-04-25 19:08:15 +02:00
|
|
|
|
2019-07-26 10:49:29 -07:00
|
|
|
var secret = speakeasy.generateSecret({ name: `Cloudron ${settings.adminFqdn()} (${result.username})` });
|
2018-04-25 19:08:15 +02:00
|
|
|
|
|
|
|
|
userdb.update(userId, { twoFactorAuthenticationSecret: secret.base32, twoFactorAuthenticationEnabled: false }, function (error) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2018-04-25 19:08:15 +02:00
|
|
|
|
|
|
|
|
qrcode.toDataURL(secret.otpauth_url, function (error, dataUrl) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(new BoxError(BoxError.INTERNAL_ERROR, error));
|
2018-04-25 19:08:15 +02:00
|
|
|
|
|
|
|
|
callback(null, { secret: secret.base32, qrcode: dataUrl });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function enableTwoFactorAuthentication(userId, totpToken, callback) {
|
|
|
|
|
assert.strictEqual(typeof userId, 'string');
|
|
|
|
|
assert.strictEqual(typeof totpToken, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
userdb.get(userId, function (error, result) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2018-04-25 19:08:15 +02:00
|
|
|
|
2019-07-23 14:42:03 -07:00
|
|
|
var verified = speakeasy.totp.verify({ secret: result.twoFactorAuthenticationSecret, encoding: 'base32', token: totpToken, window: 2 });
|
2019-10-24 14:40:26 -07:00
|
|
|
if (!verified) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
2018-04-25 19:08:15 +02:00
|
|
|
|
2019-10-24 14:40:26 -07:00
|
|
|
if (result.twoFactorAuthenticationEnabled) return callback(new BoxError(BoxError.ALREADY_EXISTS));
|
2018-04-25 19:08:15 +02:00
|
|
|
|
|
|
|
|
userdb.update(userId, { twoFactorAuthenticationEnabled: true }, function (error) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2018-04-25 19:08:15 +02:00
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function disableTwoFactorAuthentication(userId, callback) {
|
|
|
|
|
assert.strictEqual(typeof userId, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
userdb.update(userId, { twoFactorAuthenticationEnabled: false, twoFactorAuthenticationSecret: '' }, function (error) {
|
2019-10-24 14:40:26 -07:00
|
|
|
if (error) return callback(error);
|
2019-08-08 05:45:56 -07:00
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
}
|
2020-01-31 15:28:42 -08:00
|
|
|
|
2020-02-21 12:17:06 -08:00
|
|
|
function validateRole(role) {
|
|
|
|
|
assert.strictEqual(typeof role, 'string');
|
|
|
|
|
|
|
|
|
|
if (ORDERED_ROLES.indexOf(role) !== -1) return null;
|
|
|
|
|
|
|
|
|
|
return new BoxError(BoxError.BAD_FIELD, `Invalid role '${role}'`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function compareRoles(role1, role2) {
|
|
|
|
|
assert.strictEqual(typeof role1, 'string');
|
|
|
|
|
assert.strictEqual(typeof role2, 'string');
|
|
|
|
|
|
|
|
|
|
let roleInt1 = ORDERED_ROLES.indexOf(role1);
|
|
|
|
|
let roleInt2 = ORDERED_ROLES.indexOf(role2);
|
|
|
|
|
|
|
|
|
|
return roleInt1 - roleInt2;
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-31 15:28:42 -08:00
|
|
|
function validateAppPasswordName(name) {
|
|
|
|
|
assert.strictEqual(typeof name, 'string');
|
|
|
|
|
|
|
|
|
|
if (name.length < 1) return new BoxError(BoxError.BAD_FIELD, 'name must be atleast 1 char');
|
|
|
|
|
if (name.length >= 200) return new BoxError(BoxError.BAD_FIELD, 'name too long');
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getAppPassword(id, callback) {
|
|
|
|
|
assert.strictEqual(typeof id, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
userdb.getAppPassword(id, function (error, result) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
callback(null, _.omit(result, 'hashedPassword'));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addAppPassword(userId, identifier, name, callback) {
|
|
|
|
|
assert.strictEqual(typeof userId, 'string');
|
|
|
|
|
assert.strictEqual(typeof identifier, 'string');
|
|
|
|
|
assert.strictEqual(typeof name, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
let error = validateAppPasswordName(name);
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
2020-02-01 18:12:33 -08:00
|
|
|
if (identifier.length < 1) return callback(new BoxError(BoxError.BAD_FIELD, 'identifier must be atleast 1 char'));
|
2020-01-31 15:28:42 -08:00
|
|
|
|
2020-02-26 17:19:24 +01:00
|
|
|
const password = hat(16 * 4);
|
2020-01-31 15:28:42 -08:00
|
|
|
const hashedPassword = crypto.createHash('sha256').update(password).digest('base64');
|
|
|
|
|
|
|
|
|
|
var appPassword = {
|
|
|
|
|
id: 'uid-' + uuid.v4(),
|
|
|
|
|
name,
|
|
|
|
|
userId,
|
|
|
|
|
identifier,
|
|
|
|
|
password,
|
|
|
|
|
hashedPassword
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
userdb.addAppPassword(appPassword.id, appPassword, function (error) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
callback(null, _.omit(appPassword, 'hashedPassword'));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getAppPasswords(userId, callback) {
|
|
|
|
|
assert.strictEqual(typeof userId, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
userdb.getAppPasswords(userId, function (error, results) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
results.map(r => delete r.hashedPassword);
|
|
|
|
|
|
|
|
|
|
callback(null, results);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function delAppPassword(id, callback) {
|
|
|
|
|
assert.strictEqual(typeof id, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
|
|
|
|
userdb.delAppPassword(id, function (error) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
|
|
|
|
|
callback(null);
|
|
|
|
|
});
|
|
|
|
|
}
|
2020-07-09 22:33:36 -07:00
|
|
|
|
2021-04-29 12:49:48 -07:00
|
|
|
function getAvatarUrl(user, callback) {
|
2020-07-09 22:33:36 -07:00
|
|
|
assert.strictEqual(typeof user, 'object');
|
2021-04-29 12:49:48 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
2020-07-09 22:33:36 -07:00
|
|
|
|
2021-04-29 12:49:48 -07:00
|
|
|
userdb.getAvatar(user.id, function (error, avatar) {
|
|
|
|
|
if (error) return callback(error);
|
|
|
|
|
if (avatar) return callback(null, `${settings.adminOrigin()}/api/v1/profile/avatar/${user.id}`);
|
2020-07-09 22:33:36 -07:00
|
|
|
|
2021-04-29 12:49:48 -07:00
|
|
|
const emailHash = require('crypto').createHash('md5').update(user.email).digest('hex');
|
|
|
|
|
return callback(null, `https://www.gravatar.com/avatar/${emailHash}.jpg`);
|
|
|
|
|
});
|
2020-07-09 22:33:36 -07:00
|
|
|
}
|
|
|
|
|
|
2021-04-29 12:49:48 -07:00
|
|
|
function getAvatar(id, callback) {
|
2020-07-09 22:33:36 -07:00
|
|
|
assert.strictEqual(typeof id, 'string');
|
|
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2021-04-29 12:49:48 -07:00
|
|
|
userdb.getAvatar(id, function (error, avatar) {
|
|
|
|
|
if (error) return callback(error);
|
2020-07-09 22:33:36 -07:00
|
|
|
|
2021-04-29 12:49:48 -07:00
|
|
|
return callback(null, avatar);
|
2020-07-09 22:33:36 -07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-29 12:49:48 -07:00
|
|
|
function setAvatar(id, avatar, callback) {
|
2020-07-09 22:33:36 -07:00
|
|
|
assert.strictEqual(typeof id, 'string');
|
2021-04-29 12:49:48 -07:00
|
|
|
assert(avatar === null || Buffer.isBuffer(avatar));
|
2020-07-09 22:33:36 -07:00
|
|
|
assert.strictEqual(typeof callback, 'function');
|
|
|
|
|
|
2021-04-29 12:49:48 -07:00
|
|
|
userdb.setAvatar(id, avatar, callback);
|
2020-07-09 22:33:36 -07:00
|
|
|
}
|