Revert "Add no-use-before-define linter rule"
This reverts commit fdcc5d68a2.
Unfortunately, this requires us to move exports to the bottom.
This in turn causes circular dep issues and also access of
exports.GLOBAL_VAR in the global context
This commit is contained in:
405
src/users.js
405
src/users.js
@@ -68,14 +68,6 @@ exports = module.exports = {
|
||||
compareRoles,
|
||||
};
|
||||
|
||||
const ORDERED_ROLES = [ exports.ROLE_USER, exports.ROLE_USER_MANAGER, exports.ROLE_MAIL_MANAGER, exports.ROLE_ADMIN, exports.ROLE_OWNER ];
|
||||
|
||||
// the avatar and backgroundImage fields are special and not added here to reduce response sizes
|
||||
const USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'creationTime', 'inviteToken', 'resetToken', 'displayName', 'language',
|
||||
'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime', 'loginLocationsJson', 'notificationConfigJson' ].join(',');
|
||||
|
||||
const DEFAULT_GHOST_LIFETIME = 6 * 60 * 60 * 1000; // 6 hours
|
||||
|
||||
const appPasswords = require('./apppasswords.js'),
|
||||
assert = require('node:assert'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
@@ -105,6 +97,12 @@ const appPasswords = require('./apppasswords.js'),
|
||||
validator = require('./validator.js'),
|
||||
_ = require('./underscore.js');
|
||||
|
||||
// the avatar and backgroundImage fields are special and not added here to reduce response sizes
|
||||
const USERS_FIELDS = [ 'id', 'username', 'email', 'fallbackEmail', 'password', 'salt', 'creationTime', 'inviteToken', 'resetToken', 'displayName', 'language',
|
||||
'twoFactorAuthenticationEnabled', 'twoFactorAuthenticationSecret', 'active', 'source', 'role', 'resetTokenCreationTime', 'loginLocationsJson', 'notificationConfigJson' ].join(',');
|
||||
|
||||
const DEFAULT_GHOST_LIFETIME = 6 * 60 * 60 * 1000; // 6 hours
|
||||
|
||||
const CRYPTO_SALT_SIZE = 64; // 512-bit salt
|
||||
const CRYPTO_ITERATIONS = 10000; // iterations
|
||||
const CRYPTO_KEY_LENGTH = 512; // bits
|
||||
@@ -201,6 +199,28 @@ function removePrivateFields(user) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function validateRole(role) {
|
||||
assert.strictEqual(typeof role, 'string');
|
||||
|
||||
const ORDERED_ROLES = [ exports.ROLE_USER, exports.ROLE_USER_MANAGER, exports.ROLE_MAIL_MANAGER, exports.ROLE_ADMIN, exports.ROLE_OWNER ];
|
||||
|
||||
if (ORDERED_ROLES.includes(role)) 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');
|
||||
|
||||
const ORDERED_ROLES = [ exports.ROLE_USER, exports.ROLE_USER_MANAGER, exports.ROLE_MAIL_MANAGER, exports.ROLE_ADMIN, exports.ROLE_OWNER ];
|
||||
|
||||
const roleInt1 = ORDERED_ROLES.indexOf(role1);
|
||||
const roleInt2 = ORDERED_ROLES.indexOf(role2);
|
||||
|
||||
return roleInt1 - roleInt2;
|
||||
}
|
||||
|
||||
async function add(email, data, auditSource) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert(data && typeof data === 'object');
|
||||
@@ -290,181 +310,6 @@ async function add(email, data, auditSource) {
|
||||
return user.id;
|
||||
}
|
||||
|
||||
async function setGhost(user, password, expiresAt) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof expiresAt, 'number');
|
||||
|
||||
if (!user.username) throw new BoxError(BoxError.BAD_STATE, 'user has no username yet');
|
||||
|
||||
expiresAt = expiresAt || (Date.now() + DEFAULT_GHOST_LIFETIME);
|
||||
|
||||
const ghostData = await settings.getJson(settings.GHOSTS_CONFIG_KEY) || {};
|
||||
ghostData[user.username] = { password, expiresAt };
|
||||
|
||||
await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData);
|
||||
}
|
||||
|
||||
// returns true if ghost user was matched
|
||||
async function verifyGhost(username, password) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
|
||||
const ghostData = await settings.getJson(settings.GHOSTS_CONFIG_KEY) || {};
|
||||
|
||||
// either the username is an object with { password, expiresAt } or a string with the password which will expire on first match
|
||||
if (username in ghostData) {
|
||||
if (typeof ghostData[username] === 'object') {
|
||||
if (ghostData[username].expiresAt < Date.now()) {
|
||||
debug('verifyGhost: password expired');
|
||||
delete ghostData[username];
|
||||
|
||||
await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData);
|
||||
|
||||
return false;
|
||||
} else if (ghostData[username].password === password) {
|
||||
debug('verifyGhost: matched ghost user');
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if(ghostData[username] === password) {
|
||||
debug('verifyGhost: matched ghost user');
|
||||
delete ghostData[username];
|
||||
|
||||
await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function verifyAppPassword(userId, password, identifier) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
|
||||
const results = await appPasswords.list(userId);
|
||||
|
||||
const hashedPasswords = results.filter(r => r.identifier === identifier).map(r => r.hashedPassword);
|
||||
const hash = crypto.createHash('sha256').update(password).digest('base64');
|
||||
|
||||
if (hashedPasswords.includes(hash)) return;
|
||||
|
||||
throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Password is not valid');
|
||||
}
|
||||
|
||||
// identifier is only used to check if password is valid for a specific app
|
||||
async function verify(user, password, identifier, options) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
if (!user.active) {
|
||||
debug(`verify: ${user.username} is not active`);
|
||||
throw new BoxError(BoxError.NOT_FOUND, 'User not active');
|
||||
}
|
||||
|
||||
// for just invited users the username may be still null
|
||||
if (user.username) {
|
||||
const valid = await verifyGhost(user.username, password);
|
||||
|
||||
if (valid) {
|
||||
debug(`verify: ${user.username} authenticated via impersonation`);
|
||||
user.ghost = true;
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
const [error] = await safe(verifyAppPassword(user.id, password, identifier));
|
||||
if (!error) { // matched app password
|
||||
debug(`verify: ${user.username || user.id} matched app password`);
|
||||
user.appPassword = true;
|
||||
return user;
|
||||
}
|
||||
|
||||
let localTotpCheck = true; // does 2fa need to be verified with local database 2fa creds
|
||||
if (user.source === 'ldap') {
|
||||
await externalLdap.verifyPassword(user.username, password, options);
|
||||
const externalLdapConfig = await externalLdap.getConfig();
|
||||
localTotpCheck = user.twoFactorAuthenticationEnabled && !externalLdap.supports2FA(externalLdapConfig);
|
||||
} else {
|
||||
const saltBinary = Buffer.from(user.salt, 'hex');
|
||||
const [error, derivedKey] = await safe(pbkdf2Async(password, saltBinary, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST));
|
||||
if (error) throw new BoxError(BoxError.CRYPTO_ERROR, error);
|
||||
|
||||
const derivedKeyHex = Buffer.from(derivedKey, 'binary').toString('hex');
|
||||
if (derivedKeyHex !== user.password) {
|
||||
debug(`verify: ${user.username || user.id} provided incorrect password`);
|
||||
throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Wrong password');
|
||||
}
|
||||
|
||||
localTotpCheck = user.twoFactorAuthenticationEnabled;
|
||||
}
|
||||
|
||||
if (localTotpCheck && !options.skipTotpCheck) {
|
||||
if (!options.totpToken) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'A totpToken must be provided');
|
||||
const verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: options.totpToken, window: 2 });
|
||||
if (!verified) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid totpToken');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async function verifyWithId(id, password, identifier, options) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
const user = await get(id);
|
||||
if (!user) {
|
||||
debug(`verifyWithId: ${id} not found`);
|
||||
throw new BoxError(BoxError.NOT_FOUND, 'User not found');
|
||||
}
|
||||
|
||||
return await verify(user, password, identifier, options);
|
||||
}
|
||||
|
||||
async function verifyWithUsername(username, password, identifier, options) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
const user = await getByUsername(username.toLowerCase());
|
||||
if (user) return await verify(user, password, identifier, options);
|
||||
|
||||
const [error, newUserId] = await safe(externalLdap.maybeCreateUser(username.toLowerCase()));
|
||||
if (error && error.reason === BoxError.BAD_STATE) {
|
||||
debug(`verifyWithUsername: ${username} not found`);
|
||||
throw new BoxError(BoxError.NOT_FOUND, 'User not found'); // no external ldap or no auto create
|
||||
}
|
||||
if (error) {
|
||||
debug(`verifyWithUsername: failed to auto create user ${username}. %o`, error);
|
||||
throw new BoxError(BoxError.NOT_FOUND, 'User not found');
|
||||
}
|
||||
|
||||
return await verifyWithId(newUserId, password, identifier, options);
|
||||
}
|
||||
|
||||
async function verifyWithEmail(email, password, identifier, options) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
const user = await getByEmail(email.toLowerCase());
|
||||
if (!user) {
|
||||
debug(`verifyWithEmail: ${email} no such user`);
|
||||
throw new BoxError(BoxError.NOT_FOUND, 'User not found');
|
||||
}
|
||||
|
||||
return await verify(user, password, identifier, options);
|
||||
}
|
||||
|
||||
async function del(user, auditSource) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert(auditSource && typeof auditSource === 'object');
|
||||
@@ -712,6 +557,181 @@ async function getSuperadmins() {
|
||||
return await listByRole(exports.ROLE_OWNER);
|
||||
}
|
||||
|
||||
async function setGhost(user, password, expiresAt) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof expiresAt, 'number');
|
||||
|
||||
if (!user.username) throw new BoxError(BoxError.BAD_STATE, 'user has no username yet');
|
||||
|
||||
expiresAt = expiresAt || (Date.now() + DEFAULT_GHOST_LIFETIME);
|
||||
|
||||
const ghostData = await settings.getJson(settings.GHOSTS_CONFIG_KEY) || {};
|
||||
ghostData[user.username] = { password, expiresAt };
|
||||
|
||||
await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData);
|
||||
}
|
||||
|
||||
// returns true if ghost user was matched
|
||||
async function verifyGhost(username, password) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
|
||||
const ghostData = await settings.getJson(settings.GHOSTS_CONFIG_KEY) || {};
|
||||
|
||||
// either the username is an object with { password, expiresAt } or a string with the password which will expire on first match
|
||||
if (username in ghostData) {
|
||||
if (typeof ghostData[username] === 'object') {
|
||||
if (ghostData[username].expiresAt < Date.now()) {
|
||||
debug('verifyGhost: password expired');
|
||||
delete ghostData[username];
|
||||
|
||||
await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData);
|
||||
|
||||
return false;
|
||||
} else if (ghostData[username].password === password) {
|
||||
debug('verifyGhost: matched ghost user');
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else if(ghostData[username] === password) {
|
||||
debug('verifyGhost: matched ghost user');
|
||||
delete ghostData[username];
|
||||
|
||||
await settings.setJson(settings.GHOSTS_CONFIG_KEY, ghostData);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function verifyAppPassword(userId, password, identifier) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
|
||||
const results = await appPasswords.list(userId);
|
||||
|
||||
const hashedPasswords = results.filter(r => r.identifier === identifier).map(r => r.hashedPassword);
|
||||
const hash = crypto.createHash('sha256').update(password).digest('base64');
|
||||
|
||||
if (hashedPasswords.includes(hash)) return;
|
||||
|
||||
throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Password is not valid');
|
||||
}
|
||||
|
||||
// identifier is only used to check if password is valid for a specific app
|
||||
async function verify(user, password, identifier, options) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
if (!user.active) {
|
||||
debug(`verify: ${user.username} is not active`);
|
||||
throw new BoxError(BoxError.NOT_FOUND, 'User not active');
|
||||
}
|
||||
|
||||
// for just invited users the username may be still null
|
||||
if (user.username) {
|
||||
const valid = await verifyGhost(user.username, password);
|
||||
|
||||
if (valid) {
|
||||
debug(`verify: ${user.username} authenticated via impersonation`);
|
||||
user.ghost = true;
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
const [error] = await safe(verifyAppPassword(user.id, password, identifier));
|
||||
if (!error) { // matched app password
|
||||
debug(`verify: ${user.username || user.id} matched app password`);
|
||||
user.appPassword = true;
|
||||
return user;
|
||||
}
|
||||
|
||||
let localTotpCheck = true; // does 2fa need to be verified with local database 2fa creds
|
||||
if (user.source === 'ldap') {
|
||||
await externalLdap.verifyPassword(user.username, password, options);
|
||||
const externalLdapConfig = await externalLdap.getConfig();
|
||||
localTotpCheck = user.twoFactorAuthenticationEnabled && !externalLdap.supports2FA(externalLdapConfig);
|
||||
} else {
|
||||
const saltBinary = Buffer.from(user.salt, 'hex');
|
||||
const [error, derivedKey] = await safe(pbkdf2Async(password, saltBinary, CRYPTO_ITERATIONS, CRYPTO_KEY_LENGTH, CRYPTO_DIGEST));
|
||||
if (error) throw new BoxError(BoxError.CRYPTO_ERROR, error);
|
||||
|
||||
const derivedKeyHex = Buffer.from(derivedKey, 'binary').toString('hex');
|
||||
if (derivedKeyHex !== user.password) {
|
||||
debug(`verify: ${user.username || user.id} provided incorrect password`);
|
||||
throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Wrong password');
|
||||
}
|
||||
|
||||
localTotpCheck = user.twoFactorAuthenticationEnabled;
|
||||
}
|
||||
|
||||
if (localTotpCheck && !options.skipTotpCheck) {
|
||||
if (!options.totpToken) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'A totpToken must be provided');
|
||||
const verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: options.totpToken, window: 2 });
|
||||
if (!verified) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'Invalid totpToken');
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async function verifyWithId(id, password, identifier, options) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
const user = await get(id);
|
||||
if (!user) {
|
||||
debug(`verifyWithId: ${id} not found`);
|
||||
throw new BoxError(BoxError.NOT_FOUND, 'User not found');
|
||||
}
|
||||
|
||||
return await verify(user, password, identifier, options);
|
||||
}
|
||||
|
||||
async function verifyWithUsername(username, password, identifier, options) {
|
||||
assert.strictEqual(typeof username, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
const user = await getByUsername(username.toLowerCase());
|
||||
if (user) return await verify(user, password, identifier, options);
|
||||
|
||||
const [error, newUserId] = await safe(externalLdap.maybeCreateUser(username.toLowerCase()));
|
||||
if (error && error.reason === BoxError.BAD_STATE) {
|
||||
debug(`verifyWithUsername: ${username} not found`);
|
||||
throw new BoxError(BoxError.NOT_FOUND, 'User not found'); // no external ldap or no auto create
|
||||
}
|
||||
if (error) {
|
||||
debug(`verifyWithUsername: failed to auto create user ${username}. %o`, error);
|
||||
throw new BoxError(BoxError.NOT_FOUND, 'User not found');
|
||||
}
|
||||
|
||||
return await verifyWithId(newUserId, password, identifier, options);
|
||||
}
|
||||
|
||||
async function verifyWithEmail(email, password, identifier, options) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
const user = await getByEmail(email.toLowerCase());
|
||||
if (!user) {
|
||||
debug(`verifyWithEmail: ${email} no such user`);
|
||||
throw new BoxError(BoxError.NOT_FOUND, 'User not found');
|
||||
}
|
||||
|
||||
return await verify(user, password, identifier, options);
|
||||
}
|
||||
|
||||
async function getPasswordResetLink(user, auditSource) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
@@ -969,24 +989,6 @@ async function disableTwoFactorAuthentication(user, auditSource) {
|
||||
await update(user, { twoFactorAuthenticationEnabled: false, twoFactorAuthenticationSecret: '' }, auditSource);
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
const roleInt1 = ORDERED_ROLES.indexOf(role1);
|
||||
const roleInt2 = ORDERED_ROLES.indexOf(role2);
|
||||
|
||||
return roleInt1 - roleInt2;
|
||||
}
|
||||
|
||||
async function getAvatar(user) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
|
||||
@@ -1042,3 +1044,4 @@ function parseDisplayName(displayName) {
|
||||
const lastName = displayName.substring(idx+1);
|
||||
return { firstName, lastName, middleName };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user