ub/modules/Session.js

const sessionBinding = process.binding('ub_session')
const EventEmitter = require('events').EventEmitter

// cache for lazy session props
let _id, _userID
let _sessionCached = {
  uData: undefined,
  callerIP: undefined,
  userRoles: undefined,
  userLang: undefined
}
/**
 * Contains information about the logged in user.
 * Server reassign properties of this object each time `endpoint` handler are executed
 *
 * Implements {@link EventEmitter} and will emit `login` event each time user logged in
 * or `loginFailed` event with 2 parameters(userID, isLocked) when user UB authentication failed
 * @namespace
 * @global
 * @mixes EventEmitter
 */
const Session = {
}

// add EventEmitter to Session object
EventEmitter.call(Session)
Object.assign(Session, EventEmitter.prototype)

/**
 * Called by server when server enter into new user context
 * @private
 * @param sessionID
 * @param userID
 */
Session.reset = function (sessionID, userID) {
  _id = sessionID
  _userID = userID
  _sessionCached.uData = undefined
  _sessionCached.callerIP = undefined
  _sessionCached.userRoles = undefined
  _sessionCached.userLang = undefined
}

/**
 * Current session identifier. === 0 if session not started, ===1 in case authentication not used, >1 in case user authorized
 * @member {number} id
 * @memberOf Session
 * @readonly
 */
Object.defineProperty(Session, 'id', {
  enumerable: true,
  get: function () {
    return _id
  }
})
/**
 * Logged-in user identifier (from uba_user.ID). Undefined if Session.id is 0 or 1 (no authentication running)
 * @member {number} userID
 * @memberOf Session
 * @readonly
 */
Object.defineProperty(Session, 'userID', {
  enumerable: true,
  get: function () {
    return _userID
  }
})
/**
 * Logged-in user role IDs in CSV format. ==="" if no authentication running
 * @deprecated Use Session.uData.roleIDs - an array of roles IDs
 * @member {number} userRoles
 * @memberOf Session
 * @readonly
 */
Object.defineProperty(Session, 'userRoles', {
  enumerable: true,
  get: function () {
    if (_sessionCached.userRoles === undefined) {
      _sessionCached.userRoles = this.uData.roleIDs.join(',')
    }
    return _sessionCached.userRoles
  }
})
/**
 * Logged-in user role names in CSV format. ==="" if no authentication running
 * @deprecated Use Session.uData.roles
 * @member {string} userRoleNames
 * @memberOf Session
 * @readonly
 */
Object.defineProperty(Session, 'userRoleNames', {
  enumerable: true,
  get: function () {
    return this.uData.roles
  }
})

/**
 * Logged-in user language. ==="" if no authentication running
 * @member {string} userLang
 * @memberOf Session
 * @readonly
 */
Object.defineProperty(Session, 'userLang', {
  enumerable: true,
  get: function () {
    if (_sessionCached.userLang === undefined) {
      _sessionCached.userLang = sessionBinding.userLang()
    }
    return _sessionCached.userLang
  }
})

/**
 * Custom properties, defined in {@link Session.login Session.on('login')} handlers for logged-in user.
 * We strongly recommend to **not modify** value of uData outside the `Session.on('login')` handler -
 * such modification is not persisted between calls.
 *
 * Properties documented below are added by `@unitybase/uba` model, but other model can define his own properties.
 *
 * @member {Object} uData
 * @memberOf Session
 * @property {number} userID Logged in user ID. The same as Session.userID. Added by `uba` model
 * @property {string} login Logged in user name. Added by `uba` model
 * @property {string} roles Logged in user roles names separated by comma. In most case better to use uData.roleIDs array. Added by `uba` model
 * @property {Array<number>} roleIDs Array or role IDs for logged in user roles. Added by `uba` model
 * @property {string} [employeeShortFIO] Short name of employee. Added by `org` model
 * @property {string} [employeeFullFIO] Full name of employee
 * @property {number} [employeeID] Employee ID
 * @property {string} [staffUnitFullName]
 * @property {string} [staffUnitName]
 * @property {number} [staffUnitID] permanent staffUnitID. Added by `org` model
 * @property {number} [employeeOnStaffID] permanent employeeOnStaffID. Added by `org` model
 * @property {number} [parentID] permanent staffUnitID parent. Added by `org` model
 * @property {string} [parentUnityEntity] permanent staffUnitID parent entity type. Added by `org` model
 * @property {string} [orgUnitIDs] all orgUnit's IDs as CSV string. Added by `org` model
 * @property {string} [permanentOrgUnitIDs] all user orgUnit ID's permanent employeeOnStaffIDs in CSV. Added by `org` model
 * @property {string} [tempStaffUnitIDs] array temporary staffUnitIDs in CSV. Added by `org` model
 * @property {string} [tempEmployeeOnStaffIDs] array of temporary employeeOnStaffIDs in CSV. Added by `org` model
 * @property {string} [assistantStaffUnitIDs] array of assistant staffUnitIDs in CSV. Added by `org` model
 * @property {string} [assistantEmployeeOnStaffIDs] array of assistant employeeOnStaffIDs  in CSV. Added by `org` model
 * @property {string} [allStaffUnitIDs] array of all (permanent + temporary + assistant) staffUnitIDs in CSV. Added by `org` model
 * @property {string} [allEmployeeOnStaffIDs] array of all (permanent + temporary + assistant) employeeOnStaffIds in CSV. Added by `org` model
 * @property {string} [tempPositions] stringified array ob temporary position objects: {staffUnitID, employeeOnStaffID}. Added by `org` model
 * @property {string} [assistantPositions] stringified array ob assistant position objects: {staffUnitID, employeeOnStaffID}. Added by `org` model
 * @property {string} [allPositions] stringified array of permanent + temporary + assistant position objects: {staffUnitID, employeeOnStaffID}. Added by `org` model
 * @readonly
 */
Object.defineProperty(Session, 'uData', {
  enumerable: true,
  get: function () {
    if (_sessionCached.uData === undefined) {
      let d = sessionBinding.userDataJSON()
      _sessionCached.uData = d ? JSON.parse(d) : {}
    }
    return _sessionCached.uData
  }
})
/**
 * IP address of a user. May differ from IP address current user login from.
 * May be empty if request come from localhost.
 * @member {string} callerIP
 * @memberOf Session
 * @readonly
 */
Object.defineProperty(Session, 'callerIP', {
  enumerable: true,
  get: function () {
    if (_sessionCached.callerIP === undefined) {
      _sessionCached.callerIP = sessionBinding.callerIP()
    }
    return _sessionCached.callerIP
  }
})
/**
 * Create new session for userID
 * @method
 * @param {Number} userID ID of  user
 * @param {String} [secret] secret word. If defined then session secretWord is `JSON.parse(returns).result+secret`
 * @returns {String} JSON string like answer on auth request
 */
Session.setUser = sessionBinding.switchUser
/**
 * Call function as admin.
 * Built-in "always alive"(newer expired) `admin` session is always created when the application starts,
 * so this is very cheap method - it will not trigger Session.login event every time context is switched (Session.setUser and Session.runAsUser does)
 * Can be used in scheduled tasks, not-authorized methods, etc. to obtain a `admin` Session context
 * @param {Function} func Function to be called in admin context
 * @returns {*}
 */
Session.runAsAdmin = function (func) {
  let result
  sessionBinding.switchToAdmin()
  try {
    result = func()
  } finally {
    sessionBinding.switchToOriginal()
  }
  return result
}
/**
 * Call function as custom user.
 * New session will be created. Will fire `login` event.
 * @param userID ID of  user
 * @param func Function to be called in user's session.
 * @returns {*}
 */
Session.runAsUser = function (userID, func) {
  let result
  sessionBinding.switchUser(userID)
  try {
    result = func()
  } finally {
    sessionBinding.switchToOriginal()
  }
  return result
}

/**
 * Fires just after user successfully logged-in but before auth response is written to client.
 * Model developer can subscribe to this event and add some model specific data to Session.uData.
 *
 * Since all uData content is passed to client and accessible on client via
 *  $App.connection.userData(`someCustomProperty`) do not add there a security sensitive data.
 *
 * Standard models like `@unitybase/uba` and `@unitybase/org` are subscribed to this event and add
 * most useful information to the uData - {@see Session.uData Session.uData} documentation.
 * Never override `uData` using `Session.uData = {...}`, in this case you delete uData properties,
 * defined in other application models.
 * Instead define or remove properties using `Session.uData.myProperty = ...`
 * or use `delete Session.uData.myProperty` if you need to undefine something.
 *
 * Example below add `someCustomProperty` to Session.uData:
 *
 *      // @param {THTTPRequest} req
 *      Session.on('login', function (req) {
 *          var uData = Session.uData
 *          uData.someCustomProperty = 'Hello!'
 *      })
 *
 * See real life example inside `@unitybase/org/org.js`.
 * @event login
 * @memberOf Session
 */

/**
 * Fires in case new user registered in system and authentication schema support
 * "registration" feature.
 *
 * Currently only CERT and UB schemas support this feature
 *
 * For CERT schema user registered means `auth` endpoint is called with registration=1 parameter.
 *
 * For UB schema user registered means 'publicRegistration' endpoint has been called and user confirmed
 * registration by email otp
 *
 * Inside event handler server-side Session object is in INCONSISTENT state and you must not use it!!
 * Only parameter (stringified object), passed to event is valid user-relative information.
 *
 * For CERT schema parameter is look like
 *      {
 *          "authType": 'CERT',
 *          "id_cert": '<id_cert>',
 *          "user_name": '<user_name>',
 *          "additional": '',
 *          "certification_b64": '<certification_b64>'
 *      }
 *
 * For UB schema parameter is look like
 *      {
 *          "authType": 'UB',
 *          "publicRegistration": true,
 *          userID,
              userOtpData
 *      }
 *
 * Each AUTH schema can pass his own object as a event parameter, but all schema add `authType`.
 * Below is a sample code for CERT schema:
 *
 *      Session.on('registration', function(registrationParams){
 *
 *      }
 *
 * @memberOf Session
 * @event registration
 */

/**
 * Legacy event **CERT authentication schema** only
 *
 * For CERT schema user registered means `auth` endpoint is called with registration=1 parameter.
 *
 * Called before start event "registration" and before starting check the user. You can create new user inside this event.
 *
 * Parameter is look like
 *
 *      {
 *          "authType": 'CERT',
 *          "serialSign": '<serialSign>',
 *          "name": '<user name>',
 *          "additional": '',
 *          "issuer": '<issuer>',
 *          "serial": '<serial>',
 *          "certification_b64": '<certification_b64>'
 *      }
 *
 * @memberOf Session
 * @event newUserRegistration
 */

/**
 * Fires in case `auth` endpoint is called with authentication schema UB and userName is founded in database,
 * but password is incorrect.
 *
 * If wrong passord is entered more  than `UBA.passwordPolicy.maxInvalidAttempts`(from ubs_settings) times
 * user will be locked
 *
 * 2 parameters are passes to this event userID(Number) and isUserLocked(Boolean)
 *
 *      Session.on('loginFailed', function(userID, isLocked){
 *          if (isLocked)
 *              console.log('User with id ', userID, 'entered wrong password and locked');
 *          else
 *              console.log('User with id ', userID, 'entered wrong password');
 *      })
 *
 * @memberOf Session
 * @event loginFailed
 */

/**
 * Fires in case of any security violation:
 *
 *  - user is blocked or not exists (in uba_user)
 *  - user provide wrong credential (password, domain, encripted secret key, certificate etc)
 *  - for 2-factor auth schemas - too many sessions in pending state (max is 128)
 *  - access to endpoint "%" deny for user (endpoint name not present in uba_role.allowedAppMethods for eny user roles)
 *  - password for user is expired (see ubs_settings UBA.passwordPolicy.maxDurationDays key)
 *  - entity method access deny by ELS (see rules in uba_els)
 *
 * Single parameter is passes to this event `reason: string`
 *
 *      Session.on('securityViolation', function(reason){
 *          console.log('Security violation for user with ID', Session.userID, 'from', Session.callerIP, 'reason', reason);
 *      })
 *
 * @memberOf Session
 * @event securityViolation
 */

module.exports = Session