Initial sync up with master after Prettier
This commit is contained in:
494
core/user.js
494
core/user.js
@@ -2,17 +2,14 @@
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const userDb = require('./database.js').dbs.user;
|
||||
const Config = require('./config.js').get;
|
||||
const userGroup = require('./user_group.js');
|
||||
const {
|
||||
Errors,
|
||||
ErrorReasons
|
||||
} = require('./enig_error.js');
|
||||
const Events = require('./events.js');
|
||||
const UserProps = require('./user_property.js');
|
||||
const Log = require('./logger.js').log;
|
||||
const StatLog = require('./stat_log.js');
|
||||
const userDb = require('./database.js').dbs.user;
|
||||
const Config = require('./config.js').get;
|
||||
const userGroup = require('./user_group.js');
|
||||
const { Errors, ErrorReasons } = require('./enig_error.js');
|
||||
const Events = require('./events.js');
|
||||
const UserProps = require('./user_property.js');
|
||||
const Log = require('./logger.js').log;
|
||||
const StatLog = require('./stat_log.js');
|
||||
|
||||
// deps
|
||||
const crypto = require('crypto');
|
||||
@@ -40,24 +37,25 @@ module.exports = class User {
|
||||
|
||||
static get AuthFactors() {
|
||||
return {
|
||||
None : 0, // Not yet authenticated in any way
|
||||
Factor1 : 1, // username + password/pubkey/etc. checked out
|
||||
Factor2 : 2, // validated with 2FA of some sort such as OTP
|
||||
None: 0, // Not yet authenticated in any way
|
||||
Factor1: 1, // username + password/pubkey/etc. checked out
|
||||
Factor2: 2, // validated with 2FA of some sort such as OTP
|
||||
};
|
||||
}
|
||||
|
||||
static get PBKDF2() {
|
||||
return {
|
||||
iterations : 1000,
|
||||
keyLen : 128,
|
||||
saltLen : 32,
|
||||
iterations: 1000,
|
||||
keyLen: 128,
|
||||
saltLen: 32,
|
||||
};
|
||||
}
|
||||
|
||||
static get StandardPropertyGroups() {
|
||||
return {
|
||||
auth : [
|
||||
UserProps.PassPbkdf2Salt, UserProps.PassPbkdf2Dk,
|
||||
auth: [
|
||||
UserProps.PassPbkdf2Salt,
|
||||
UserProps.PassPbkdf2Dk,
|
||||
UserProps.AuthPubKey,
|
||||
],
|
||||
};
|
||||
@@ -65,10 +63,10 @@ module.exports = class User {
|
||||
|
||||
static get AccountStatus() {
|
||||
return {
|
||||
disabled : 0, // +op disabled
|
||||
inactive : 1, // inactive, aka requires +op approval/activation
|
||||
active : 2, // standard, active
|
||||
locked : 3, // locked out (too many bad login attempts, etc.)
|
||||
disabled: 0, // +op disabled
|
||||
inactive: 1, // inactive, aka requires +op approval/activation
|
||||
active: 2, // standard, active
|
||||
locked: 3, // locked out (too many bad login attempts, etc.)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,7 +83,7 @@ module.exports = class User {
|
||||
}
|
||||
|
||||
isValid() {
|
||||
if(this.userId <= 0 || this.username.length < Config().users.usernameMin) {
|
||||
if (this.userId <= 0 || this.username.length < Config().users.usernameMin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -93,13 +91,15 @@ module.exports = class User {
|
||||
}
|
||||
|
||||
hasValidPasswordProperties() {
|
||||
const salt = this.getProperty(UserProps.PassPbkdf2Salt);
|
||||
const dk = this.getProperty(UserProps.PassPbkdf2Dk);
|
||||
const salt = this.getProperty(UserProps.PassPbkdf2Salt);
|
||||
const dk = this.getProperty(UserProps.PassPbkdf2Dk);
|
||||
|
||||
if(!salt || !dk ||
|
||||
(salt.length !== User.PBKDF2.saltLen * 2) ||
|
||||
(dk.length !== User.PBKDF2.keyLen * 2))
|
||||
{
|
||||
if (
|
||||
!salt ||
|
||||
!dk ||
|
||||
salt.length !== User.PBKDF2.saltLen * 2 ||
|
||||
dk.length !== User.PBKDF2.keyLen * 2
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -110,21 +110,23 @@ module.exports = class User {
|
||||
return User.isRootUserId(this.userId);
|
||||
}
|
||||
|
||||
isSysOp() { // alias to isRoot()
|
||||
isSysOp() {
|
||||
// alias to isRoot()
|
||||
return this.isRoot();
|
||||
}
|
||||
|
||||
isGroupMember(groupNames) {
|
||||
if(_.isString(groupNames)) {
|
||||
groupNames = [ groupNames ];
|
||||
if (_.isString(groupNames)) {
|
||||
groupNames = [groupNames];
|
||||
}
|
||||
|
||||
const isMember = groupNames.some(gn => (-1 !== this.groups.indexOf(gn)));
|
||||
const isMember = groupNames.some(gn => -1 !== this.groups.indexOf(gn));
|
||||
return isMember;
|
||||
}
|
||||
|
||||
getSanitizedName(type='username') {
|
||||
const name = 'real' === type ? this.getProperty(UserProps.RealName) : this.username;
|
||||
getSanitizedName(type = 'username') {
|
||||
const name =
|
||||
'real' === type ? this.getProperty(UserProps.RealName) : this.username;
|
||||
return sanatizeFilename(name) || `user${this.userId.toString()}`;
|
||||
}
|
||||
|
||||
@@ -153,21 +155,21 @@ module.exports = class User {
|
||||
}
|
||||
|
||||
getLegacySecurityLevel() {
|
||||
if(this.isRoot() || this.isGroupMember('sysops')) {
|
||||
if (this.isRoot() || this.isGroupMember('sysops')) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
if(this.isGroupMember('users')) {
|
||||
if (this.isGroupMember('users')) {
|
||||
return 30;
|
||||
}
|
||||
|
||||
return 10; // :TODO: Is this what we want?
|
||||
return 10; // :TODO: Is this what we want?
|
||||
}
|
||||
|
||||
processFailedLogin(userId, cb) {
|
||||
async.waterfall(
|
||||
[
|
||||
(callback) => {
|
||||
callback => {
|
||||
return User.getUser(userId, callback);
|
||||
},
|
||||
(tempUser, callback) => {
|
||||
@@ -182,20 +184,26 @@ module.exports = class User {
|
||||
},
|
||||
(tempUser, failedAttempts, callback) => {
|
||||
const lockAccount = _.get(Config(), 'users.failedLogin.lockAccount');
|
||||
if(lockAccount > 0 && failedAttempts >= lockAccount) {
|
||||
if (lockAccount > 0 && failedAttempts >= lockAccount) {
|
||||
const props = {
|
||||
[ UserProps.AccountStatus ] : User.AccountStatus.locked,
|
||||
[ UserProps.AccountLockedTs ] : StatLog.now,
|
||||
[UserProps.AccountStatus]: User.AccountStatus.locked,
|
||||
[UserProps.AccountLockedTs]: StatLog.now,
|
||||
};
|
||||
if(!_.has(tempUser.properties, UserProps.AccountLockedPrevStatus)) {
|
||||
props[UserProps.AccountLockedPrevStatus] = tempUser.getProperty(UserProps.AccountStatus);
|
||||
if (
|
||||
!_.has(tempUser.properties, UserProps.AccountLockedPrevStatus)
|
||||
) {
|
||||
props[UserProps.AccountLockedPrevStatus] =
|
||||
tempUser.getProperty(UserProps.AccountStatus);
|
||||
}
|
||||
Log.info( { userId, failedAttempts }, '(Re)setting account to locked due to failed logins');
|
||||
Log.info(
|
||||
{ userId, failedAttempts },
|
||||
'(Re)setting account to locked due to failed logins'
|
||||
);
|
||||
return tempUser.persistProperties(props, callback);
|
||||
}
|
||||
|
||||
return cb(null);
|
||||
}
|
||||
},
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
@@ -205,24 +213,27 @@ module.exports = class User {
|
||||
|
||||
unlockAccount(cb) {
|
||||
const prevStatus = this.getProperty(UserProps.AccountLockedPrevStatus);
|
||||
if(!prevStatus) {
|
||||
return cb(null); // nothing to do
|
||||
if (!prevStatus) {
|
||||
return cb(null); // nothing to do
|
||||
}
|
||||
|
||||
this.persistProperty(UserProps.AccountStatus, prevStatus, err => {
|
||||
if(err) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
return this.removeProperties( [ UserProps.AccountLockedPrevStatus, UserProps.AccountLockedTs ], cb);
|
||||
return this.removeProperties(
|
||||
[UserProps.AccountLockedPrevStatus, UserProps.AccountLockedTs],
|
||||
cb
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static get AuthFactor1Types() {
|
||||
return {
|
||||
SSHPubKey : 'sshPubKey',
|
||||
Password : 'password',
|
||||
TLSClient : 'tlsClientAuth',
|
||||
SSHPubKey: 'sshPubKey',
|
||||
Password: 'password',
|
||||
TLSClient: 'tlsClientAuth',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -232,33 +243,42 @@ module.exports = class User {
|
||||
const tempAuthInfo = {};
|
||||
|
||||
const validatePassword = (props, callback) => {
|
||||
User.generatePasswordDerivedKey(authInfo.password, props[UserProps.PassPbkdf2Salt], (err, dk) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
User.generatePasswordDerivedKey(
|
||||
authInfo.password,
|
||||
props[UserProps.PassPbkdf2Salt],
|
||||
(err, dk) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
//
|
||||
// Use constant time comparison here for security feel-goods
|
||||
//
|
||||
const passDkBuf = Buffer.from(dk, 'hex');
|
||||
const propsDkBuf = Buffer.from(props[UserProps.PassPbkdf2Dk], 'hex');
|
||||
|
||||
return callback(
|
||||
crypto.timingSafeEqual(passDkBuf, propsDkBuf)
|
||||
? null
|
||||
: Errors.AccessDenied('Invalid password')
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Use constant time comparison here for security feel-goods
|
||||
//
|
||||
const passDkBuf = Buffer.from(dk, 'hex');
|
||||
const propsDkBuf = Buffer.from(props[UserProps.PassPbkdf2Dk], 'hex');
|
||||
|
||||
return callback(crypto.timingSafeEqual(passDkBuf, propsDkBuf) ?
|
||||
null :
|
||||
Errors.AccessDenied('Invalid password')
|
||||
);
|
||||
});
|
||||
);
|
||||
};
|
||||
|
||||
const validatePubKey = (props, callback) => {
|
||||
const pubKeyActual = ssh2.utils.parseKey(props[UserProps.AuthPubKey]);
|
||||
if(!pubKeyActual) {
|
||||
if (!pubKeyActual) {
|
||||
return callback(Errors.AccessDenied('Invalid public key'));
|
||||
}
|
||||
|
||||
if(authInfo.pubKey.key.algo != pubKeyActual.type ||
|
||||
!crypto.timingSafeEqual(authInfo.pubKey.key.data, pubKeyActual.getPublicSSH()))
|
||||
{
|
||||
if (
|
||||
authInfo.pubKey.key.algo != pubKeyActual.type ||
|
||||
!crypto.timingSafeEqual(
|
||||
authInfo.pubKey.key.data,
|
||||
pubKeyActual.getPublicSSH()
|
||||
)
|
||||
) {
|
||||
return callback(Errors.AccessDenied('Invalid public key'));
|
||||
}
|
||||
|
||||
@@ -270,7 +290,7 @@ module.exports = class User {
|
||||
function fetchUserId(callback) {
|
||||
// get user ID
|
||||
User.getUserIdAndName(username, (err, uid, un) => {
|
||||
tempAuthInfo.userId = uid;
|
||||
tempAuthInfo.userId = uid;
|
||||
tempAuthInfo.username = un;
|
||||
|
||||
return callback(err);
|
||||
@@ -278,19 +298,23 @@ module.exports = class User {
|
||||
},
|
||||
function getRequiredAuthProperties(callback) {
|
||||
// fetch properties required for authentication
|
||||
User.loadProperties(tempAuthInfo.userId, { names : User.StandardPropertyGroups.auth }, (err, props) => {
|
||||
return callback(err, props);
|
||||
});
|
||||
User.loadProperties(
|
||||
tempAuthInfo.userId,
|
||||
{ names: User.StandardPropertyGroups.auth },
|
||||
(err, props) => {
|
||||
return callback(err, props);
|
||||
}
|
||||
);
|
||||
},
|
||||
function validatePassOrPubKey(props, callback) {
|
||||
if(User.AuthFactor1Types.SSHPubKey === authInfo.type) {
|
||||
if (User.AuthFactor1Types.SSHPubKey === authInfo.type) {
|
||||
return validatePubKey(props, callback);
|
||||
}
|
||||
return validatePassword(props, callback);
|
||||
},
|
||||
function initProps(callback) {
|
||||
User.loadProperties(tempAuthInfo.userId, (err, allProps) => {
|
||||
if(!err) {
|
||||
if (!err) {
|
||||
tempAuthInfo.properties = allProps;
|
||||
}
|
||||
|
||||
@@ -298,33 +322,51 @@ module.exports = class User {
|
||||
});
|
||||
},
|
||||
function checkAccountStatus(callback) {
|
||||
const accountStatus = parseInt(tempAuthInfo.properties[UserProps.AccountStatus], 10);
|
||||
if(User.AccountStatus.disabled === accountStatus) {
|
||||
return callback(Errors.AccessDenied('Account disabled', ErrorReasons.Disabled));
|
||||
const accountStatus = parseInt(
|
||||
tempAuthInfo.properties[UserProps.AccountStatus],
|
||||
10
|
||||
);
|
||||
if (User.AccountStatus.disabled === accountStatus) {
|
||||
return callback(
|
||||
Errors.AccessDenied('Account disabled', ErrorReasons.Disabled)
|
||||
);
|
||||
}
|
||||
if(User.AccountStatus.inactive === accountStatus) {
|
||||
return callback(Errors.AccessDenied('Account inactive', ErrorReasons.Inactive));
|
||||
if (User.AccountStatus.inactive === accountStatus) {
|
||||
return callback(
|
||||
Errors.AccessDenied('Account inactive', ErrorReasons.Inactive)
|
||||
);
|
||||
}
|
||||
|
||||
if(User.AccountStatus.locked === accountStatus) {
|
||||
const autoUnlockMinutes = _.get(Config(), 'users.failedLogin.autoUnlockMinutes');
|
||||
const lockedTs = moment(tempAuthInfo.properties[UserProps.AccountLockedTs]);
|
||||
if(autoUnlockMinutes && lockedTs.isValid()) {
|
||||
if (User.AccountStatus.locked === accountStatus) {
|
||||
const autoUnlockMinutes = _.get(
|
||||
Config(),
|
||||
'users.failedLogin.autoUnlockMinutes'
|
||||
);
|
||||
const lockedTs = moment(
|
||||
tempAuthInfo.properties[UserProps.AccountLockedTs]
|
||||
);
|
||||
if (autoUnlockMinutes && lockedTs.isValid()) {
|
||||
const minutesSinceLocked = moment().diff(lockedTs, 'minutes');
|
||||
if(minutesSinceLocked >= autoUnlockMinutes) {
|
||||
if (minutesSinceLocked >= autoUnlockMinutes) {
|
||||
// allow the login - we will clear any lock there
|
||||
Log.info(
|
||||
{ username, userId : tempAuthInfo.userId, lockedAt : lockedTs.format() },
|
||||
{
|
||||
username,
|
||||
userId: tempAuthInfo.userId,
|
||||
lockedAt: lockedTs.format(),
|
||||
},
|
||||
'Locked account will now be unlocked due to auto-unlock minutes policy'
|
||||
);
|
||||
return callback(null);
|
||||
}
|
||||
}
|
||||
return callback(Errors.AccessDenied('Account is locked', ErrorReasons.Locked));
|
||||
return callback(
|
||||
Errors.AccessDenied('Account is locked', ErrorReasons.Locked)
|
||||
);
|
||||
}
|
||||
|
||||
// anything else besides active is still not allowed
|
||||
if(User.AccountStatus.active !== accountStatus) {
|
||||
if (User.AccountStatus.active !== accountStatus) {
|
||||
return callback(Errors.AccessDenied('Account is not active'));
|
||||
}
|
||||
|
||||
@@ -332,26 +374,34 @@ module.exports = class User {
|
||||
},
|
||||
function initGroups(callback) {
|
||||
userGroup.getGroupsForUser(tempAuthInfo.userId, (err, groups) => {
|
||||
if(!err) {
|
||||
if (!err) {
|
||||
tempAuthInfo.groups = groups;
|
||||
}
|
||||
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
},
|
||||
],
|
||||
err => {
|
||||
if(err) {
|
||||
if (err) {
|
||||
//
|
||||
// If we failed login due to something besides an inactive or disabled account,
|
||||
// we need to update failure status and possibly lock the account.
|
||||
//
|
||||
// If locked already, update the lock timestamp -- ie, extend the lockout period.
|
||||
//
|
||||
if(![ErrorReasons.Disabled, ErrorReasons.Inactive].includes(err.reasonCode) && tempAuthInfo.userId) {
|
||||
if (
|
||||
![ErrorReasons.Disabled, ErrorReasons.Inactive].includes(
|
||||
err.reasonCode
|
||||
) &&
|
||||
tempAuthInfo.userId
|
||||
) {
|
||||
self.processFailedLogin(tempAuthInfo.userId, persistErr => {
|
||||
if(persistErr) {
|
||||
Log.warn( { error : persistErr.message }, 'Failed to persist failed login information');
|
||||
if (persistErr) {
|
||||
Log.warn(
|
||||
{ error: persistErr.message },
|
||||
'Failed to persist failed login information'
|
||||
);
|
||||
}
|
||||
return cb(err); // pass along original error
|
||||
});
|
||||
@@ -360,16 +410,18 @@ module.exports = class User {
|
||||
}
|
||||
} else {
|
||||
// everything checks out - load up info
|
||||
self.userId = tempAuthInfo.userId;
|
||||
self.username = tempAuthInfo.username;
|
||||
self.properties = tempAuthInfo.properties;
|
||||
self.groups = tempAuthInfo.groups;
|
||||
self.authFactor = User.AuthFactors.Factor1;
|
||||
self.userId = tempAuthInfo.userId;
|
||||
self.username = tempAuthInfo.username;
|
||||
self.properties = tempAuthInfo.properties;
|
||||
self.groups = tempAuthInfo.groups;
|
||||
self.authFactor = User.AuthFactors.Factor1;
|
||||
|
||||
//
|
||||
// If 2FA/OTP is required, this user is not quite authenticated yet.
|
||||
//
|
||||
self.authenticated = !(self.getProperty(UserProps.AuthFactor2OTP) ? true : false);
|
||||
self.authenticated = !(self.getProperty(UserProps.AuthFactor2OTP)
|
||||
? true
|
||||
: false);
|
||||
|
||||
self.removeProperty(UserProps.FailedLoginAttempts);
|
||||
|
||||
@@ -378,8 +430,11 @@ module.exports = class User {
|
||||
// the user's previous status & clean up props.
|
||||
//
|
||||
self.unlockAccount(unlockErr => {
|
||||
if(unlockErr) {
|
||||
Log.warn( { error : unlockErr.message }, 'Failed to unlock account');
|
||||
if (unlockErr) {
|
||||
Log.warn(
|
||||
{ error: unlockErr.message },
|
||||
'Failed to unlock account'
|
||||
);
|
||||
}
|
||||
return cb(null);
|
||||
});
|
||||
@@ -388,18 +443,23 @@ module.exports = class User {
|
||||
);
|
||||
}
|
||||
|
||||
create(createUserInfo , cb) {
|
||||
create(createUserInfo, cb) {
|
||||
assert(0 === this.userId);
|
||||
const config = Config();
|
||||
|
||||
if(this.username.length < config.users.usernameMin || this.username.length > config.users.usernameMax) {
|
||||
if (
|
||||
this.username.length < config.users.usernameMin ||
|
||||
this.username.length > config.users.usernameMax
|
||||
) {
|
||||
return cb(Errors.Invalid('Invalid username length'));
|
||||
}
|
||||
|
||||
const self = this;
|
||||
|
||||
// :TODO: set various defaults, e.g. default activation status, etc.
|
||||
self.properties[UserProps.AccountStatus] = config.users.requireActivation ? User.AccountStatus.inactive : User.AccountStatus.active;
|
||||
self.properties[UserProps.AccountStatus] = config.users.requireActivation
|
||||
? User.AccountStatus.inactive
|
||||
: User.AccountStatus.active;
|
||||
|
||||
async.waterfall(
|
||||
[
|
||||
@@ -410,17 +470,19 @@ module.exports = class User {
|
||||
trans.run(
|
||||
`INSERT INTO user (user_name)
|
||||
VALUES (?);`,
|
||||
[ self.username ],
|
||||
function inserted(err) { // use classic function for |this|
|
||||
if(err) {
|
||||
[self.username],
|
||||
function inserted(err) {
|
||||
// use classic function for |this|
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.userId = this.lastID;
|
||||
|
||||
// Do not require activation for userId 1 (root/admin)
|
||||
if(User.RootUserID === self.userId) {
|
||||
self.properties[UserProps.AccountStatus] = User.AccountStatus.active;
|
||||
if (User.RootUserID === self.userId) {
|
||||
self.properties[UserProps.AccountStatus] =
|
||||
User.AccountStatus.active;
|
||||
}
|
||||
|
||||
return callback(null, trans);
|
||||
@@ -428,21 +490,25 @@ module.exports = class User {
|
||||
);
|
||||
},
|
||||
function genAuthCredentials(trans, callback) {
|
||||
User.generatePasswordDerivedKeyAndSalt(createUserInfo.password, (err, info) => {
|
||||
if(err) {
|
||||
return callback(err);
|
||||
}
|
||||
User.generatePasswordDerivedKeyAndSalt(
|
||||
createUserInfo.password,
|
||||
(err, info) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.properties[UserProps.PassPbkdf2Salt] = info.salt;
|
||||
self.properties[UserProps.PassPbkdf2Dk] = info.dk;
|
||||
return callback(null, trans);
|
||||
});
|
||||
self.properties[UserProps.PassPbkdf2Salt] = info.salt;
|
||||
self.properties[UserProps.PassPbkdf2Dk] = info.dk;
|
||||
return callback(null, trans);
|
||||
}
|
||||
);
|
||||
},
|
||||
function setInitialGroupMembership(trans, callback) {
|
||||
// Assign initial groups. Must perform a clone: #235 - All users are sysops (and I can't un-sysop them)
|
||||
self.groups = [...config.users.defaultGroups];
|
||||
|
||||
if(User.RootUserID === self.userId) { // root/SysOp?
|
||||
if (User.RootUserID === self.userId) {
|
||||
// root/SysOp?
|
||||
self.groups.push('sysops');
|
||||
}
|
||||
|
||||
@@ -454,17 +520,16 @@ module.exports = class User {
|
||||
});
|
||||
},
|
||||
function sendEvent(trans, callback) {
|
||||
Events.emit(
|
||||
Events.getSystemEvents().NewUser,
|
||||
{
|
||||
user : Object.assign({}, self, { sessionId : createUserInfo.sessionId } )
|
||||
}
|
||||
);
|
||||
Events.emit(Events.getSystemEvents().NewUser, {
|
||||
user: Object.assign({}, self, {
|
||||
sessionId: createUserInfo.sessionId,
|
||||
}),
|
||||
});
|
||||
return callback(null, trans);
|
||||
}
|
||||
},
|
||||
],
|
||||
(err, trans) => {
|
||||
if(trans) {
|
||||
if (trans) {
|
||||
trans[err ? 'rollback' : 'commit'](transErr => {
|
||||
return cb(err ? err : transErr);
|
||||
});
|
||||
@@ -491,7 +556,7 @@ module.exports = class User {
|
||||
userGroup.addUserToGroups(self.userId, self.groups, trans, err => {
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
},
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
@@ -503,9 +568,9 @@ module.exports = class User {
|
||||
userDb.run(
|
||||
`REPLACE INTO user_property (user_id, prop_name, prop_value)
|
||||
VALUES (?, ?, ?);`,
|
||||
[ userId, propName, propValue ],
|
||||
[userId, propName, propValue],
|
||||
err => {
|
||||
if(cb) {
|
||||
if (cb) {
|
||||
return cb(err, propValue);
|
||||
}
|
||||
}
|
||||
@@ -519,7 +584,7 @@ module.exports = class User {
|
||||
incrementProperty(propName, incrementBy) {
|
||||
incrementBy = incrementBy || 1;
|
||||
let newValue = parseInt(this.getProperty(propName));
|
||||
if(newValue) {
|
||||
if (newValue) {
|
||||
newValue += incrementBy;
|
||||
} else {
|
||||
newValue = incrementBy;
|
||||
@@ -550,9 +615,9 @@ module.exports = class User {
|
||||
userDb.run(
|
||||
`DELETE FROM user_property
|
||||
WHERE user_id = ? AND prop_name = ?;`,
|
||||
[ this.userId, propName ],
|
||||
[this.userId, propName],
|
||||
err => {
|
||||
if(cb) {
|
||||
if (cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
@@ -560,18 +625,21 @@ module.exports = class User {
|
||||
}
|
||||
|
||||
removeProperties(propNames, cb) {
|
||||
async.each(propNames, (name, next) => {
|
||||
return this.removeProperty(name, next);
|
||||
},
|
||||
err => {
|
||||
if(cb) {
|
||||
return cb(err);
|
||||
async.each(
|
||||
propNames,
|
||||
(name, next) => {
|
||||
return this.removeProperty(name, next);
|
||||
},
|
||||
err => {
|
||||
if (cb) {
|
||||
return cb(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
persistProperties(properties, transOrDb, cb) {
|
||||
if(!_.isFunction(cb) && _.isFunction(transOrDb)) {
|
||||
if (!_.isFunction(cb) && _.isFunction(transOrDb)) {
|
||||
cb = transOrDb;
|
||||
transOrDb = userDb;
|
||||
}
|
||||
@@ -586,31 +654,34 @@ module.exports = class User {
|
||||
VALUES (?, ?, ?);`
|
||||
);
|
||||
|
||||
async.each(Object.keys(properties), (propName, nextProp) => {
|
||||
stmt.run(self.userId, propName, properties[propName], err => {
|
||||
return nextProp(err);
|
||||
});
|
||||
},
|
||||
err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
async.each(
|
||||
Object.keys(properties),
|
||||
(propName, nextProp) => {
|
||||
stmt.run(self.userId, propName, properties[propName], err => {
|
||||
return nextProp(err);
|
||||
});
|
||||
},
|
||||
err => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
stmt.finalize( () => {
|
||||
return cb(null);
|
||||
});
|
||||
});
|
||||
stmt.finalize(() => {
|
||||
return cb(null);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
setNewAuthCredentials(password, cb) {
|
||||
User.generatePasswordDerivedKeyAndSalt(password, (err, info) => {
|
||||
if(err) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const newProperties = {
|
||||
[ UserProps.PassPbkdf2Salt ] : info.salt,
|
||||
[ UserProps.PassPbkdf2Dk ] : info.dk,
|
||||
[UserProps.PassPbkdf2Salt]: info.salt,
|
||||
[UserProps.PassPbkdf2Dk]: info.dk,
|
||||
};
|
||||
|
||||
this.persistProperties(newProperties, err => {
|
||||
@@ -621,7 +692,7 @@ module.exports = class User {
|
||||
|
||||
getAge() {
|
||||
const birthdate = this.getProperty(UserProps.Birthdate);
|
||||
if(birthdate) {
|
||||
if (birthdate) {
|
||||
return moment().diff(birthdate, 'years');
|
||||
}
|
||||
}
|
||||
@@ -643,18 +714,18 @@ module.exports = class User {
|
||||
userGroup.getGroupsForUser(userId, (err, groups) => {
|
||||
return callback(null, userName, properties, groups);
|
||||
});
|
||||
}
|
||||
},
|
||||
],
|
||||
(err, userName, properties, groups) => {
|
||||
const user = new User();
|
||||
user.userId = userId;
|
||||
user.username = userName;
|
||||
user.properties = properties;
|
||||
user.groups = groups;
|
||||
user.userId = userId;
|
||||
user.username = userName;
|
||||
user.properties = properties;
|
||||
user.groups = groups;
|
||||
|
||||
// explicitly NOT an authenticated user!
|
||||
user.authenticated = false;
|
||||
user.authFactor = User.AuthFactors.None;
|
||||
user.authenticated = false;
|
||||
user.authFactor = User.AuthFactors.None;
|
||||
|
||||
return cb(err, user);
|
||||
}
|
||||
@@ -697,7 +768,7 @@ module.exports = class User {
|
||||
}
|
||||
|
||||
static isRootUserId(userId) {
|
||||
return (User.RootUserID === userId);
|
||||
return User.RootUserID === userId;
|
||||
}
|
||||
|
||||
static getUserIdAndName(username, cb) {
|
||||
@@ -705,13 +776,13 @@ module.exports = class User {
|
||||
`SELECT id, user_name
|
||||
FROM user
|
||||
WHERE user_name LIKE ?;`,
|
||||
[ username ],
|
||||
[username],
|
||||
(err, row) => {
|
||||
if(err) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
if(row) {
|
||||
if (row) {
|
||||
return cb(null, row.id, row.user_name);
|
||||
}
|
||||
|
||||
@@ -729,13 +800,13 @@ module.exports = class User {
|
||||
FROM user_property
|
||||
WHERE prop_name='${UserProps.RealName}' AND prop_value LIKE ?
|
||||
);`,
|
||||
[ realName ],
|
||||
[realName],
|
||||
(err, row) => {
|
||||
if(err) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
if(row) {
|
||||
if (row) {
|
||||
return cb(null, row.id, row.user_name);
|
||||
}
|
||||
|
||||
@@ -746,7 +817,7 @@ module.exports = class User {
|
||||
|
||||
static getUserIdAndNameByLookup(lookup, cb) {
|
||||
User.getUserIdAndName(lookup, (err, userId, userName) => {
|
||||
if(err) {
|
||||
if (err) {
|
||||
User.getUserIdAndNameByRealName(lookup, (err, userId, userName) => {
|
||||
return cb(err, userId, userName);
|
||||
});
|
||||
@@ -761,13 +832,13 @@ module.exports = class User {
|
||||
`SELECT user_name
|
||||
FROM user
|
||||
WHERE id = ?;`,
|
||||
[ userId ],
|
||||
[userId],
|
||||
(err, row) => {
|
||||
if(err) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
if(row) {
|
||||
if (row) {
|
||||
return cb(null, row.user_name);
|
||||
}
|
||||
|
||||
@@ -777,31 +848,35 @@ module.exports = class User {
|
||||
}
|
||||
|
||||
static loadProperties(userId, options, cb) {
|
||||
if(!cb && _.isFunction(options)) {
|
||||
if (!cb && _.isFunction(options)) {
|
||||
cb = options;
|
||||
options = {};
|
||||
}
|
||||
|
||||
let sql =
|
||||
`SELECT prop_name, prop_value
|
||||
let sql = `SELECT prop_name, prop_value
|
||||
FROM user_property
|
||||
WHERE user_id = ?`;
|
||||
|
||||
if(options.names) {
|
||||
if (options.names) {
|
||||
sql += ` AND prop_name IN("${options.names.join('","')}");`;
|
||||
} else {
|
||||
sql += ';';
|
||||
}
|
||||
|
||||
let properties = {};
|
||||
userDb.each(sql, [ userId ], (err, row) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
userDb.each(
|
||||
sql,
|
||||
[userId],
|
||||
(err, row) => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
properties[row.prop_name] = row.prop_value;
|
||||
},
|
||||
err => {
|
||||
return cb(err, err ? null : properties);
|
||||
}
|
||||
properties[row.prop_name] = row.prop_value;
|
||||
}, (err) => {
|
||||
return cb(err, err ? null : properties);
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
// :TODO: make this much more flexible - propValue should allow for case-insensitive compare, etc.
|
||||
@@ -812,9 +887,9 @@ module.exports = class User {
|
||||
`SELECT user_id
|
||||
FROM user_property
|
||||
WHERE prop_name = ? AND prop_value = ?;`,
|
||||
[ propName, propValue ],
|
||||
[propName, propValue],
|
||||
(err, row) => {
|
||||
if(row) {
|
||||
if (row) {
|
||||
userIds.push(row.user_id);
|
||||
}
|
||||
},
|
||||
@@ -840,7 +915,7 @@ module.exports = class User {
|
||||
static getUserList(options, cb) {
|
||||
const userList = [];
|
||||
|
||||
options.properties = options.properties || [ UserProps.RealName ];
|
||||
options.properties = options.properties || [UserProps.RealName];
|
||||
|
||||
const asList = [];
|
||||
const joinList = [];
|
||||
@@ -848,7 +923,9 @@ module.exports = class User {
|
||||
const dbProp = options.properties[i];
|
||||
const propName = options.propsCamelCase ? _.camelCase(dbProp) : dbProp;
|
||||
asList.push(`p${i}.prop_value AS ${propName}`);
|
||||
joinList.push(`LEFT OUTER JOIN user_property p${i} ON p${i}.user_id = u.id AND p${i}.prop_name = '${dbProp}'`);
|
||||
joinList.push(
|
||||
`LEFT OUTER JOIN user_property p${i} ON p${i}.user_id = u.id AND p${i}.prop_name = '${dbProp}'`
|
||||
);
|
||||
}
|
||||
|
||||
userDb.each(
|
||||
@@ -871,7 +948,7 @@ module.exports = class User {
|
||||
async.waterfall(
|
||||
[
|
||||
function getSalt(callback) {
|
||||
User.generatePasswordDerivedKeySalt( (err, salt) => {
|
||||
User.generatePasswordDerivedKeySalt((err, salt) => {
|
||||
return callback(err, salt);
|
||||
});
|
||||
},
|
||||
@@ -879,17 +956,17 @@ module.exports = class User {
|
||||
User.generatePasswordDerivedKey(password, salt, (err, dk) => {
|
||||
return callback(err, salt, dk);
|
||||
});
|
||||
}
|
||||
},
|
||||
],
|
||||
(err, salt, dk) => {
|
||||
return cb(err, { salt : salt, dk : dk } );
|
||||
return cb(err, { salt: salt, dk: dk });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static generatePasswordDerivedKeySalt(cb) {
|
||||
crypto.randomBytes(User.PBKDF2.saltLen, (err, salt) => {
|
||||
if(err) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
return cb(null, salt.toString('hex'));
|
||||
@@ -899,12 +976,19 @@ module.exports = class User {
|
||||
static generatePasswordDerivedKey(password, salt, cb) {
|
||||
password = Buffer.from(password).toString('hex');
|
||||
|
||||
crypto.pbkdf2(password, salt, User.PBKDF2.iterations, User.PBKDF2.keyLen, 'sha1', (err, dk) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
crypto.pbkdf2(
|
||||
password,
|
||||
salt,
|
||||
User.PBKDF2.iterations,
|
||||
User.PBKDF2.keyLen,
|
||||
'sha1',
|
||||
(err, dk) => {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
return cb(null, dk.toString('hex'));
|
||||
});
|
||||
return cb(null, dk.toString('hex'));
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user