2017-06-21 00:14:14 +00:00
'use strict' ;
2017-07-26 19:42:05 +00:00
const config = require ( 'config' ) ;
2017-06-21 00:14:14 +00:00
const knex = require ( '../lib/knex' ) ;
const hasher = require ( 'node-object-hash' ) ( ) ;
const { enforce , filterObject } = require ( '../lib/helpers' ) ;
2018-11-18 14:38:52 +00:00
const interoperableErrors = require ( '../../shared/interoperable-errors' ) ;
const passwordValidator = require ( '../../shared/password-validator' ) ( ) ;
2017-06-21 00:14:14 +00:00
const dtHelpers = require ( '../lib/dt-helpers' ) ;
2018-04-29 16:13:40 +00:00
const tools = require ( '../lib/tools' ) ;
2018-04-02 09:58:32 +00:00
const crypto = require ( 'crypto' ) ;
2017-07-08 13:48:34 +00:00
const settings = require ( './settings' ) ;
2018-04-29 16:13:40 +00:00
const { getTrustedUrl } = require ( '../lib/urls' ) ;
2018-11-17 22:26:45 +00:00
const { tUI } = require ( '../lib/translate' ) ;
2017-07-08 13:48:34 +00:00
const bluebird = require ( 'bluebird' ) ;
2017-06-30 14:11:02 +00:00
const bcrypt = require ( 'bcrypt-nodejs' ) ;
2019-04-22 00:41:40 +00:00
const bcryptHash = bluebird . promisify ( bcrypt . hash . bind ( bcrypt ) ) ;
const bcryptCompare = bluebird . promisify ( bcrypt . compare . bind ( bcrypt ) ) ;
2017-07-08 13:48:34 +00:00
2018-04-29 16:13:40 +00:00
const mailers = require ( '../lib/mailers' ) ;
2017-06-21 00:14:14 +00:00
2017-07-24 11:43:32 +00:00
const passport = require ( '../lib/passport' ) ;
const namespaceHelpers = require ( '../lib/namespace-helpers' ) ;
2017-07-26 19:42:05 +00:00
const allowedKeys = new Set ( [ 'username' , 'name' , 'email' , 'password' , 'namespace' , 'role' ] ) ;
2017-06-30 14:11:02 +00:00
const ownAccountAllowedKeys = new Set ( [ 'name' , 'email' , 'password' ] ) ;
2017-07-26 19:42:05 +00:00
const allowedKeysExternal = new Set ( [ 'username' , 'namespace' , 'role' ] ) ;
const hashKeys = new Set ( [ 'username' , 'name' , 'email' , 'namespace' , 'role' ] ) ;
2017-07-27 14:11:22 +00:00
const shares = require ( './shares' ) ;
2017-07-29 19:42:07 +00:00
const contextHelpers = require ( '../lib/context-helpers' ) ;
2017-07-08 13:48:34 +00:00
2017-07-09 21:16:47 +00:00
function hash ( entity ) {
return hasher . hash ( filterObject ( entity , hashKeys ) ) ;
2017-06-21 00:14:14 +00:00
}
2019-01-04 20:31:01 +00:00
async function _getByTx ( tx , context , key , value , extraColumns = [ ] ) {
2017-08-13 18:11:58 +00:00
const columns = [ 'id' , 'username' , 'name' , 'email' , 'namespace' , 'role' , ... extraColumns ] ;
2017-06-21 00:14:14 +00:00
2019-01-04 20:31:01 +00:00
const user = await tx ( 'users' ) . select ( columns ) . where ( key , value ) . first ( ) ;
2017-06-21 00:14:14 +00:00
2017-07-08 13:48:34 +00:00
if ( ! user ) {
2018-04-02 09:58:32 +00:00
shares . throwPermissionDenied ( ) ;
2017-07-26 19:42:05 +00:00
}
2019-01-04 20:31:01 +00:00
// Note that getRestrictedAccessToken relies to this check to see whether a user may impersonate another. If "manageUsers" here were to be changed to something like "viewUsers", then
// a corresponding check has to be added to getRestrictedAccessToken
2017-07-29 19:42:07 +00:00
await shares . enforceEntityPermission ( context , 'namespace' , user . namespace , 'manageUsers' ) ;
2017-06-29 21:22:33 +00:00
2017-06-21 00:14:14 +00:00
return user ;
}
2019-01-04 20:31:01 +00:00
async function _getBy ( context , key , value , extraColumns = [ ] ) {
return await knex . transaction ( async tx => {
return await _getByTx ( tx , context , key , value , extraColumns ) ;
} ) ;
}
2017-07-26 19:42:05 +00:00
async function getById ( context , id ) {
return await _getBy ( context , 'id' , id ) ;
2017-07-08 13:48:34 +00:00
}
2017-07-26 19:42:05 +00:00
async function serverValidate ( context , data , isOwnAccount ) {
2017-06-29 21:22:33 +00:00
const result = { } ;
2017-07-26 19:42:05 +00:00
if ( ! isOwnAccount ) {
await shares . enforceTypePermission ( context , 'namespace' , 'manageUsers' ) ;
}
2017-06-30 14:11:02 +00:00
if ( ! isOwnAccount && data . username ) {
2017-06-29 21:22:33 +00:00
const query = knex ( 'users' ) . select ( [ 'id' ] ) . where ( 'username' , data . username ) ;
if ( data . id ) {
// Id is not set in entity creation form
query . andWhereNot ( 'id' , data . id ) ;
}
const user = await query . first ( ) ;
result . username = {
exists : ! ! user
} ;
2017-06-21 00:14:14 +00:00
}
2017-06-30 14:11:02 +00:00
if ( isOwnAccount && data . currentPassword ) {
const user = await knex ( 'users' ) . select ( [ 'id' , 'password' ] ) . where ( 'id' , data . id ) . first ( ) ;
result . currentPassword = { } ;
result . currentPassword . incorrect = ! await bcryptCompare ( data . currentPassword , user . password ) ;
}
2017-06-29 21:22:33 +00:00
if ( data . email ) {
2017-06-30 14:11:02 +00:00
const query = knex ( 'users' ) . select ( [ 'id' ] ) . where ( 'email' , data . email ) ;
if ( data . id ) {
// Id is not set in entity creation form
query . andWhereNot ( 'id' , data . id ) ;
}
const user = await query . first ( ) ;
2017-06-29 21:22:33 +00:00
result . email = { } ;
result . email . invalid = await tools . validateEmail ( data . email ) !== 0 ;
2017-06-30 14:11:02 +00:00
result . email . exists = ! ! user ;
2017-06-29 21:22:33 +00:00
}
2017-06-21 00:14:14 +00:00
2017-06-29 21:22:33 +00:00
return result ;
2017-06-21 00:14:14 +00:00
}
2017-07-26 19:42:05 +00:00
async function listDTAjax ( context , params ) {
return await dtHelpers . ajaxListWithPermissions (
context ,
[ { entityTypeId : 'namespace' , requiredOperations : [ 'manageUsers' ] } ] ,
2017-07-24 11:43:32 +00:00
params ,
2017-07-27 14:11:22 +00:00
builder => builder
. from ( 'users' )
. innerJoin ( 'namespaces' , 'namespaces.id' , 'users.namespace' )
. innerJoin ( 'generated_role_names' , 'generated_role_names.role' , 'users.role' )
. where ( 'generated_role_names.entity_type' , 'global' ) ,
[ 'users.id' , 'users.username' , 'users.name' , 'namespaces.name' , 'generated_role_names.name' ]
2017-07-24 11:43:32 +00:00
) ;
2017-06-21 00:14:14 +00:00
}
2017-08-11 06:51:30 +00:00
async function _validateAndPreprocess ( tx , entity , isCreate , isOwnAccount ) {
enforce ( await tools . validateEmail ( entity . email ) === 0 , 'Invalid email' ) ;
2017-06-29 21:22:33 +00:00
2017-08-11 06:51:30 +00:00
await namespaceHelpers . validateEntity ( tx , entity ) ;
2017-07-24 11:43:32 +00:00
2017-08-11 06:51:30 +00:00
const otherUserWithSameEmailQuery = tx ( 'users' ) . where ( 'email' , entity . email ) ;
if ( entity . id ) {
otherUserWithSameEmailQuery . andWhereNot ( 'id' , entity . id ) ;
2017-06-29 21:22:33 +00:00
}
2017-07-24 11:43:32 +00:00
if ( await otherUserWithSameEmailQuery . first ( ) ) {
2017-06-30 14:11:02 +00:00
throw new interoperableErrors . DuplicitEmailError ( ) ;
2017-06-29 21:22:33 +00:00
}
2017-06-30 14:11:02 +00:00
if ( ! isOwnAccount ) {
2017-08-11 06:51:30 +00:00
const otherUserWithSameUsernameQuery = tx ( 'users' ) . where ( 'username' , entity . username ) ;
2019-01-04 20:31:01 +00:00
if ( ! isCreate ) {
2017-08-11 06:51:30 +00:00
otherUserWithSameUsernameQuery . andWhereNot ( 'id' , entity . id ) ;
2017-06-30 14:11:02 +00:00
}
2017-07-24 11:43:32 +00:00
if ( await otherUserWithSameUsernameQuery . first ( ) ) {
2017-06-30 14:11:02 +00:00
throw new interoperableErrors . DuplicitNameError ( ) ;
}
}
2017-08-11 06:51:30 +00:00
enforce ( entity . role in config . roles . global , 'Unknown role' ) ;
2017-06-30 14:11:02 +00:00
2017-08-11 06:51:30 +00:00
enforce ( ! isCreate || entity . password . length > 0 , 'Password not set' ) ;
2017-06-29 21:22:33 +00:00
2017-08-11 06:51:30 +00:00
if ( entity . password ) {
const passwordValidatorResults = passwordValidator . test ( entity . password ) ;
2017-06-29 21:22:33 +00:00
if ( passwordValidatorResults . errors . length > 0 ) {
// This is not an interoperable error because this is not supposed to happen unless the client is tampered with.
throw new Error ( 'Invalid password' ) ;
}
2017-08-11 06:51:30 +00:00
entity . password = await bcryptHash ( entity . password , null , null ) ;
2017-06-29 21:22:33 +00:00
} else {
2017-08-11 06:51:30 +00:00
delete entity . password ;
2017-06-29 21:22:33 +00:00
}
}
2017-07-27 14:11:22 +00:00
async function create ( context , user ) {
2017-07-27 19:41:25 +00:00
let id ;
2017-06-30 14:11:02 +00:00
await knex . transaction ( async tx => {
2017-08-11 06:51:30 +00:00
await shares . enforceEntityPermissionTx ( tx , context , 'namespace' , user . namespace , 'manageUsers' ) ;
2017-07-26 19:42:05 +00:00
if ( passport . isAuthMethodLocal ) {
await _validateAndPreprocess ( tx , user , true ) ;
2017-07-27 14:11:22 +00:00
2017-07-27 19:41:25 +00:00
const ids = await tx ( 'users' ) . insert ( filterObject ( user , allowedKeys ) ) ;
id = ids [ 0 ] ;
2017-07-08 13:48:34 +00:00
2017-07-26 19:42:05 +00:00
} else {
const filteredUser = filterObject ( user , allowedKeysExternal ) ;
enforce ( user . role in config . roles . global , 'Unknown role' ) ;
2017-07-27 14:11:22 +00:00
2017-07-26 19:42:05 +00:00
await namespaceHelpers . validateEntity ( tx , user ) ;
2017-07-27 14:11:22 +00:00
2017-07-27 19:41:25 +00:00
const ids = await tx ( 'users' ) . insert ( filteredUser ) ;
id = ids [ 0 ] ;
2017-07-26 19:42:05 +00:00
}
2017-07-27 19:41:25 +00:00
2017-08-14 20:53:29 +00:00
await shares . rebuildPermissionsTx ( tx , { userId : id } ) ;
2017-07-26 19:42:05 +00:00
} ) ;
2017-07-27 19:41:25 +00:00
return id ;
2017-07-08 13:48:34 +00:00
}
2017-07-27 14:11:22 +00:00
async function updateWithConsistencyCheck ( context , user , isOwnAccount ) {
2017-06-21 00:14:14 +00:00
await knex . transaction ( async tx => {
2017-07-27 19:41:25 +00:00
const existing = await tx ( 'users' ) . where ( 'id' , user . id ) . first ( ) ;
2017-07-26 19:42:05 +00:00
if ( ! existing ) {
shares . throwPermissionDenied ( ) ;
2017-06-21 00:14:14 +00:00
}
2017-07-26 19:42:05 +00:00
const existingHash = hash ( existing ) ;
if ( existingHash !== user . originalHash ) {
2017-06-21 00:14:14 +00:00
throw new interoperableErrors . ChangedError ( ) ;
}
2017-07-26 19:42:05 +00:00
if ( ! isOwnAccount ) {
2017-08-11 06:51:30 +00:00
await shares . enforceEntityPermissionTx ( tx , context , 'namespace' , user . namespace , 'manageUsers' ) ;
await shares . enforceEntityPermissionTx ( tx , context , 'namespace' , existing . namespace , 'manageUsers' ) ;
2017-06-30 14:11:02 +00:00
}
2017-07-26 19:42:05 +00:00
if ( passport . isAuthMethodLocal ) {
await _validateAndPreprocess ( tx , user , false , isOwnAccount ) ;
if ( isOwnAccount && user . password ) {
if ( ! await bcryptCompare ( user . currentPassword , existing . password ) ) {
throw new interoperableErrors . IncorrectPasswordError ( ) ;
}
}
await tx ( 'users' ) . where ( 'id' , user . id ) . update ( filterObject ( user , isOwnAccount ? ownAccountAllowedKeys : allowedKeys ) ) ;
} else {
2017-07-27 19:41:25 +00:00
enforce ( ! isOwnAccount , 'Local user management is required' ) ;
2017-07-26 19:42:05 +00:00
enforce ( user . role in config . roles . global , 'Unknown role' ) ;
await namespaceHelpers . validateEntity ( tx , user ) ;
await tx ( 'users' ) . where ( 'id' , user . id ) . update ( filterObject ( user , allowedKeysExternal ) ) ;
}
2017-07-27 14:11:22 +00:00
// Removes the default shares based on the user role and rebuilds permissions.
// rebuildPermissions adds the default shares based on the user role, which will reflect the changes
// done to the user.
if ( existing . namespace !== user . namespace || existing . role !== user . role ) {
await shares . removeDefaultShares ( tx , existing ) ;
}
2017-08-14 20:53:29 +00:00
await shares . rebuildPermissionsTx ( tx , { userId : user . id } ) ;
2017-06-21 00:14:14 +00:00
} ) ;
}
2017-07-09 13:41:53 +00:00
async function remove ( context , userId ) {
2017-06-29 21:22:33 +00:00
enforce ( userId !== 1 , 'Admin cannot be deleted' ) ;
2017-07-09 13:41:53 +00:00
enforce ( context . user . id !== userId , 'User cannot delete himself/herself' ) ;
2017-07-26 19:42:05 +00:00
await knex . transaction ( async tx => {
const existing = await tx ( 'users' ) . where ( 'id' , userId ) . first ( ) ;
if ( ! existing ) {
shares . throwPermissionDenied ( ) ;
}
2017-08-11 06:51:30 +00:00
await shares . enforceEntityPermissionTx ( tx , context , 'namespace' , existing . namespace , 'manageUsers' ) ;
2017-07-26 19:42:05 +00:00
2017-07-27 14:11:22 +00:00
await tx ( 'users' ) . where ( 'id' , userId ) . del ( ) ;
2017-07-26 19:42:05 +00:00
} ) ;
2017-06-21 00:14:14 +00:00
}
2017-07-08 13:48:34 +00:00
async function getByAccessToken ( accessToken ) {
2017-07-29 19:42:07 +00:00
return await _getBy ( contextHelpers . getAdminContext ( ) , 'access_token' , accessToken ) ;
2017-07-08 13:48:34 +00:00
}
async function getByUsername ( username ) {
2017-07-29 19:42:07 +00:00
return await _getBy ( contextHelpers . getAdminContext ( ) , 'username' , username ) ;
2017-07-08 13:48:34 +00:00
}
2019-01-04 20:31:01 +00:00
async function getByUsernameIfPasswordMatch ( context , username , password ) {
2017-07-08 16:57:41 +00:00
try {
2019-01-05 22:56:16 +00:00
const user = await _getBy ( context , 'username' , username , [ 'password' ] ) ;
2017-07-08 13:48:34 +00:00
2017-07-08 16:57:41 +00:00
if ( ! await bcryptCompare ( password , user . password ) ) {
throw new interoperableErrors . IncorrectPasswordError ( ) ;
}
2017-07-08 13:48:34 +00:00
2017-09-17 14:36:23 +00:00
delete user . password ;
2017-07-08 16:57:41 +00:00
return user ;
} catch ( err ) {
if ( err instanceof interoperableErrors . NotFoundError ) {
throw new interoperableErrors . IncorrectPasswordError ( ) ;
}
throw err ;
}
2017-07-08 13:48:34 +00:00
}
async function getAccessToken ( userId ) {
2017-07-29 19:42:07 +00:00
const user = await _getBy ( contextHelpers . getAdminContext ( ) , 'id' , userId , [ 'access_token' ] ) ;
2017-07-08 13:48:34 +00:00
return user . access _token ;
}
async function resetAccessToken ( userId ) {
const token = crypto . randomBytes ( 20 ) . toString ( 'hex' ) . toLowerCase ( ) ;
2017-07-26 19:42:05 +00:00
await knex ( 'users' ) . where ( { id : userId } ) . update ( { access _token : token } ) ;
2017-07-08 13:48:34 +00:00
return token ;
}
2018-12-15 14:15:48 +00:00
async function sendPasswordReset ( locale , usernameOrEmail ) {
2017-07-08 13:48:34 +00:00
enforce ( passport . isAuthMethodLocal , 'Local user management is required' ) ;
await knex . transaction ( async tx => {
const user = await tx ( 'users' ) . where ( 'username' , usernameOrEmail ) . orWhere ( 'email' , usernameOrEmail ) . select ( [ 'id' , 'username' , 'email' , 'name' ] ) . first ( ) ;
if ( user ) {
const resetToken = crypto . randomBytes ( 16 ) . toString ( 'base64' ) . replace ( /[^a-z0-9]/gi , '' ) ;
await tx ( 'users' ) . where ( 'id' , user . id ) . update ( {
reset _token : resetToken ,
reset _expire : new Date ( Date . now ( ) + 60 * 60 * 1000 )
} ) ;
2018-04-29 16:13:40 +00:00
const { adminEmail } = await settings . get ( contextHelpers . getAdminContext ( ) , [ 'adminEmail' ] ) ;
2017-07-08 13:48:34 +00:00
2018-04-29 16:13:40 +00:00
const mailer = await mailers . getOrCreateMailer ( ) ;
2018-09-18 08:30:13 +00:00
await mailer . sendTransactionalMail ( {
2017-07-08 13:48:34 +00:00
to : {
address : user . email
} ,
2018-12-15 14:15:48 +00:00
subject : tUI ( 'mailerPasswordChangeRequest' , locale )
2017-07-08 13:48:34 +00:00
} , {
2018-12-15 14:15:48 +00:00
html : 'users/password-reset-html.hbs' ,
text : 'users/password-reset-text.hbs' ,
locale ,
2017-07-08 13:48:34 +00:00
data : {
2018-12-15 19:09:07 +00:00
title : tUI ( 'mailtrain' , locale ) ,
2017-07-08 13:48:34 +00:00
username : user . username ,
name : user . name ,
2019-01-05 22:56:16 +00:00
confirmUrl : getTrustedUrl ( ` login/reset/ ${ encodeURIComponent ( user . username ) } / ${ encodeURIComponent ( resetToken ) } ` )
2017-07-08 13:48:34 +00:00
}
} ) ;
}
// We intentionally silently ignore the situation when user is not found. This is not to reveal if a user exists in the system.
} ) ;
}
async function isPasswordResetTokenValid ( username , resetToken ) {
enforce ( passport . isAuthMethodLocal , 'Local user management is required' ) ;
const user = await knex ( 'users' ) . select ( [ 'id' ] ) . where ( { username , reset _token : resetToken } ) . andWhere ( 'reset_expire' , '>' , new Date ( ) ) . first ( ) ;
return ! ! user ;
}
2017-07-08 16:57:41 +00:00
async function resetPassword ( username , resetToken , password ) {
2017-07-08 13:48:34 +00:00
enforce ( passport . isAuthMethodLocal , 'Local user management is required' ) ;
await knex . transaction ( async tx => {
const user = await tx ( 'users' ) . select ( [ 'id' ] ) . where ( {
username ,
reset _token : resetToken
} ) . andWhere ( 'reset_expire' , '>' , new Date ( ) ) . first ( ) ;
if ( user ) {
const passwordValidatorResults = passwordValidator . test ( password ) ;
if ( passwordValidatorResults . errors . length > 0 ) {
// This is not an interoperable error because this is not supposed to happen unless the client is tampered with.
throw new Error ( 'Invalid password' ) ;
}
password = await bcryptHash ( password , null , null ) ;
await tx ( 'users' ) . where ( { username } ) . update ( {
password ,
reset _token : null ,
reset _expire : null
} ) ;
} else {
2017-07-09 21:16:47 +00:00
throw new interoperableErrors . InvalidTokenError ( ) ;
2017-07-08 13:48:34 +00:00
}
} ) ;
}
2018-04-02 09:58:32 +00:00
const restrictedAccessTokenMethods = { } ;
const restrictedAccessTokens = new Map ( ) ;
function registerRestrictedAccessTokenMethod ( method , getHandlerFromParams ) {
restrictedAccessTokenMethods [ method ] = getHandlerFromParams ;
}
2018-04-22 07:00:04 +00:00
async function getRestrictedAccessToken ( context , method , params ) {
2018-04-02 09:58:32 +00:00
const token = crypto . randomBytes ( 24 ) . toString ( 'hex' ) . toLowerCase ( ) ;
const tokenEntry = {
token ,
userId : context . user . id ,
2018-04-22 07:00:04 +00:00
handler : await restrictedAccessTokenMethods [ method ] ( params ) ,
2018-04-02 09:58:32 +00:00
expires : Date . now ( ) + 120 * 1000
} ;
restrictedAccessTokens . set ( token , tokenEntry ) ;
return token ;
}
2018-05-09 02:07:01 +00:00
async function refreshRestrictedAccessToken ( context , token ) {
const tokenEntry = restrictedAccessTokens . get ( token ) ;
if ( tokenEntry && tokenEntry . userId === context . user . id ) {
tokenEntry . expires = Date . now ( ) + 120 * 1000
} else {
shares . throwPermissionDenied ( ) ;
}
}
2018-04-02 09:58:32 +00:00
async function getByRestrictedAccessToken ( token ) {
const now = Date . now ( ) ;
for ( const entry of restrictedAccessTokens . values ( ) ) {
if ( entry . expires < now ) {
restrictedAccessTokens . delete ( entry . token ) ;
}
}
const tokenEntry = restrictedAccessTokens . get ( token ) ;
if ( tokenEntry ) {
const user = await getById ( contextHelpers . getAdminContext ( ) , tokenEntry . userId ) ;
2019-01-04 20:31:01 +00:00
user . restrictedAccessMethod = tokenEntry . method ;
2018-04-02 09:58:32 +00:00
user . restrictedAccessHandler = tokenEntry . handler ;
2018-05-09 02:07:01 +00:00
user . restrictedAccessToken = tokenEntry . token ;
2019-01-04 20:31:01 +00:00
user . restrictedAccessParams = tokenEntry . params ;
2018-04-02 09:58:32 +00:00
return user ;
} else {
shares . throwPermissionDenied ( ) ;
}
}
2018-09-09 22:55:44 +00:00
module . exports . listDTAjax = listDTAjax ;
module . exports . remove = remove ;
module . exports . updateWithConsistencyCheck = updateWithConsistencyCheck ;
module . exports . create = create ;
module . exports . hash = hash ;
module . exports . getById = getById ;
module . exports . serverValidate = serverValidate ;
module . exports . getByAccessToken = getByAccessToken ;
module . exports . getByUsername = getByUsername ;
module . exports . getByUsernameIfPasswordMatch = getByUsernameIfPasswordMatch ;
module . exports . getAccessToken = getAccessToken ;
module . exports . resetAccessToken = resetAccessToken ;
module . exports . sendPasswordReset = sendPasswordReset ;
module . exports . isPasswordResetTokenValid = isPasswordResetTokenValid ;
module . exports . resetPassword = resetPassword ;
module . exports . getByRestrictedAccessToken = getByRestrictedAccessToken ;
module . exports . getRestrictedAccessToken = getRestrictedAccessToken ;
module . exports . refreshRestrictedAccessToken = refreshRestrictedAccessToken ;
module . exports . registerRestrictedAccessTokenMethod = registerRestrictedAccessTokenMethod ;