| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478 | const passport = require('passport')const passportJWT = require('passport-jwt')const _ = require('lodash')const jwt = require('jsonwebtoken')const ms = require('ms')const { DateTime } = require('luxon')const Promise = require('bluebird')const crypto = Promise.promisifyAll(require('crypto'))const pem2jwk = require('pem-jwk').pem2jwkconst securityHelper = require('../helpers/security')/* global WIKI */module.exports = {  strategies: {},  guest: {    cacheExpiration: DateTime.utc().minus({ days: 1 })  },  groups: {},  validApiKeys: [],  revocationList: require('./cache').init(),  /**   * Initialize the authentication module   */  init() {    this.passport = passport    passport.serializeUser((user, done) => {      done(null, user.id)    })    passport.deserializeUser(async (id, done) => {      try {        const user = await WIKI.models.users.query().findById(id).withGraphFetched('groups').modifyGraph('groups', builder => {          builder.select('groups.id', 'permissions')        })        if (user) {          done(null, user)        } else {          done(new Error(WIKI.lang.t('auth:errors:usernotfound')), null)        }      } catch (err) {        done(err, null)      }    })    this.reloadGroups()    this.reloadApiKeys()    return this  },  /**   * Load authentication strategies   */  async activateStrategies () {    try {      // Unload any active strategies      WIKI.auth.strategies = {}      const currentStrategies = _.keys(passport._strategies)      _.pull(currentStrategies, 'session')      _.forEach(currentStrategies, stg => { passport.unuse(stg) })      // Load JWT      passport.use('jwt', new passportJWT.Strategy({        jwtFromRequest: securityHelper.extractJWT,        secretOrKey: WIKI.config.certs.public,        audience: WIKI.config.auth.audience,        issuer: 'urn:wiki.js',        algorithms: ['RS256']      }, (jwtPayload, cb) => {        cb(null, jwtPayload)      }))      // Load enabled strategies      const enabledStrategies = await WIKI.models.authentication.getStrategies()      for (let idx in enabledStrategies) {        const stg = enabledStrategies[idx]        try {          const strategy = require(`../modules/authentication/${stg.strategyKey}/authentication.js`)          stg.config.callbackURL = `${WIKI.config.host}/login/${stg.key}/callback`          strategy.init(passport, stg.config)          strategy.config = stg.config          WIKI.auth.strategies[stg.key] = {            ...strategy,            ...stg          }          WIKI.logger.info(`Authentication Strategy ${stg.displayName}: [ OK ]`)        } catch (err) {          WIKI.logger.error(`Authentication Strategy ${stg.displayName} (${stg.key}): [ FAILED ]`)          WIKI.logger.error(err)        }      }    } catch (err) {      WIKI.logger.error(`Failed to initialize Authentication Strategies: [ ERROR ]`)      WIKI.logger.error(err)    }  },  /**   * Authenticate current request   *   * @param {Express Request} req   * @param {Express Response} res   * @param {Express Next Callback} next   */  authenticate (req, res, next) {    WIKI.auth.passport.authenticate('jwt', {session: false}, async (err, user, info) => {      if (err) { return next() }      let mustRevalidate = false      // Expired but still valid within N days, just renew      if (info instanceof Error && info.name === 'TokenExpiredError') {        const expiredDate = (info.expiredAt instanceof Date) ? info.expiredAt.toISOString() : info.expiredAt        if (DateTime.utc().minus(ms(WIKI.config.auth.tokenRenewal)) < DateTime.fromISO(expiredDate)) {          mustRevalidate = true        }      }      // Check if user / group is in revocation list      if (user && !user.api && !mustRevalidate) {        const uRevalidate = WIKI.auth.revocationList.get(`u${_.toString(user.id)}`)        if (uRevalidate && user.iat < uRevalidate) {          mustRevalidate = true        } else if (DateTime.fromSeconds(user.iat) <= WIKI.startedAt) { // Prevent new / restarted instance from allowing revoked tokens          mustRevalidate = true        } else {          for (const gid of user.groups) {            const gRevalidate = WIKI.auth.revocationList.get(`g${_.toString(gid)}`)            if (gRevalidate && user.iat < gRevalidate) {              mustRevalidate = true              break            }          }        }      }      // Revalidate and renew token      if (mustRevalidate) {        const jwtPayload = jwt.decode(securityHelper.extractJWT(req))        try {          const newToken = await WIKI.models.users.refreshToken(jwtPayload.id)          user = newToken.user          user.permissions = user.getGlobalPermissions()          user.groups = user.getGroups()          req.user = user          // Try headers, otherwise cookies for response          if (req.get('content-type') === 'application/json') {            res.set('new-jwt', newToken.token)          } else {            res.cookie('jwt', newToken.token, { expires: DateTime.utc().plus({ days: 365 }).toJSDate() })          }        } catch (errc) {          WIKI.logger.warn(errc)          return next()        }      }      // JWT is NOT valid, set as guest      if (!user) {        if (WIKI.auth.guest.cacheExpiration <= DateTime.utc()) {          WIKI.auth.guest = await WIKI.models.users.getGuestUser()          WIKI.auth.guest.cacheExpiration = DateTime.utc().plus({ minutes: 1 })        }        req.user = WIKI.auth.guest        return next()      }      // Process API tokens      if (_.has(user, 'api')) {        if (!WIKI.config.api.isEnabled) {          return next(new Error('API is disabled. You must enable it from the Administration Area first.'))        } else if (_.includes(WIKI.auth.validApiKeys, user.api)) {          req.user = {            id: 1,            email: 'api@localhost',            name: 'API',            pictureUrl: null,            timezone: 'America/New_York',            localeCode: 'en',            permissions: _.get(WIKI.auth.groups, `${user.grp}.permissions`, []),            groups: [user.grp],            getGlobalPermissions () {              return req.user.permissions            },            getGroups () {              return req.user.groups            }          }          return next()        } else {          return next(new Error('API Key is invalid or was revoked.'))        }      }      // JWT is valid      req.logIn(user, { session: false }, (errc) => {        if (errc) { return next(errc) }        next()      })    })(req, res, next)  },  /**   * Check if user has access to resource   *   * @param {User} user   * @param {Array<String>} permissions   * @param {String|Boolean} path   */  checkAccess(user, permissions = [], page = false) {    const userPermissions = user.permissions ? user.permissions : user.getGlobalPermissions()    // System Admin    if (_.includes(userPermissions, 'manage:system')) {      return true    }    // Check Global Permissions    if (_.intersection(userPermissions, permissions).length < 1) {      return false    }    // Skip if no page rule to check    if (!page) {      return true    }    // Check Page Rules    if (user.groups) {      let checkState = {        deny: false,        match: false,        specificity: ''      }      user.groups.forEach(grp => {        const grpId = _.isObject(grp) ? _.get(grp, 'id', 0) : grp        _.get(WIKI.auth.groups, `${grpId}.pageRules`, []).forEach(rule => {          if (_.intersection(rule.roles, permissions).length > 0) {            switch (rule.match) {              case 'START':                if (_.startsWith(`/${page.path}`, `/${rule.path}`)) {                  checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['END', 'REGEX', 'EXACT', 'TAG'] })                }                break              case 'END':                if (_.endsWith(page.path, rule.path)) {                  checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['REGEX', 'EXACT', 'TAG'] })                }                break              case 'REGEX':                const reg = new RegExp(rule.path)                if (reg.test(page.path)) {                  checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['EXACT', 'TAG'] })                }                break              case 'TAG':                _.get(page, 'tags', []).forEach(tag => {                  if (tag.tag === rule.path) {                    checkState = this._applyPageRuleSpecificity({                      rule,                      checkState,                      higherPriority: ['EXACT']                    })                  }                })                break              case 'EXACT':                if (`/${page.path}` === `/${rule.path}`) {                  checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: [] })                }                break            }          }        })      })      return (checkState.match && !checkState.deny)    }    return false  },  /**   * Check for exclusive permissions (contain any X permission(s) but not any Y permission(s))   *   * @param {User} user   * @param {Array<String>} includePermissions   * @param {Array<String>} excludePermissions   */  checkExclusiveAccess(user, includePermissions = [], excludePermissions = []) {    const userPermissions = user.permissions ? user.permissions : user.getGlobalPermissions()    // Check Inclusion Permissions    if (_.intersection(userPermissions, includePermissions).length < 1) {      return false    }    // Check Exclusion Permissions    if (_.intersection(userPermissions, excludePermissions).length > 0) {      return false    }    return true  },  /**   * Check and apply Page Rule specificity   *   * @access private   */  _applyPageRuleSpecificity ({ rule, checkState, higherPriority = [] }) {    if (rule.path.length === checkState.specificity.length) {      // Do not override higher priority rules      if (_.includes(higherPriority, checkState.match)) {        return checkState      }      // Do not override a previous DENY rule with same match      if (rule.match === checkState.match && checkState.deny && !rule.deny) {        return checkState      }    } else if (rule.path.length < checkState.specificity.length) {      // Do not override higher specificity rules      return checkState    }    return {      deny: rule.deny,      match: rule.match,      specificity: rule.path    }  },  /**   * Reload Groups from DB   */  async reloadGroups () {    const groupsArray = await WIKI.models.groups.query()    this.groups = _.keyBy(groupsArray, 'id')    WIKI.auth.guest.cacheExpiration = DateTime.utc().minus({ days: 1 })  },  /**   * Reload valid API Keys from DB   */  async reloadApiKeys () {    const keys = await WIKI.models.apiKeys.query().select('id').where('isRevoked', false).andWhere('expiration', '>', DateTime.utc().toISO())    this.validApiKeys = _.map(keys, 'id')  },  /**   * Generate New Authentication Public / Private Key Certificates   */  async regenerateCertificates () {    WIKI.logger.info('Regenerating certificates...')    _.set(WIKI.config, 'sessionSecret', (await crypto.randomBytesAsync(32)).toString('hex'))    const certs = crypto.generateKeyPairSync('rsa', {      modulusLength: 2048,      publicKeyEncoding: {        type: 'pkcs1',        format: 'pem'      },      privateKeyEncoding: {        type: 'pkcs1',        format: 'pem',        cipher: 'aes-256-cbc',        passphrase: WIKI.config.sessionSecret      }    })    _.set(WIKI.config, 'certs', {      jwk: pem2jwk(certs.publicKey),      public: certs.publicKey,      private: certs.privateKey    })    await WIKI.configSvc.saveToDb([      'certs',      'sessionSecret'    ])    await WIKI.auth.activateStrategies()    WIKI.events.outbound.emit('reloadAuthStrategies')    WIKI.logger.info('Regenerated certificates: [ COMPLETED ]')  },  /**   * Reset Guest User   */  async resetGuestUser() {    WIKI.logger.info('Resetting guest account...')    const guestGroup = await WIKI.models.groups.query().where('id', 2).first()    await WIKI.models.users.query().delete().where({      providerKey: 'local',      email: 'guest@example.com'    }).orWhere('id', 2)    const guestUser = await WIKI.models.users.query().insert({      id: 2,      provider: 'local',      email: 'guest@example.com',      name: 'Guest',      password: '',      locale: 'en',      defaultEditor: 'markdown',      tfaIsActive: false,      isSystem: true,      isActive: true,      isVerified: true    })    await guestUser.$relatedQuery('groups').relate(guestGroup.id)    WIKI.logger.info('Guest user has been reset: [ COMPLETED ]')  },  /**   * Subscribe to HA propagation events   */  subscribeToEvents() {    WIKI.events.inbound.on('reloadGroups', () => {      WIKI.auth.reloadGroups()    })    WIKI.events.inbound.on('reloadApiKeys', () => {      WIKI.auth.reloadApiKeys()    })    WIKI.events.inbound.on('reloadAuthStrategies', () => {      WIKI.auth.activateStrategies()    })    WIKI.events.inbound.on('addAuthRevoke', (args) => {      WIKI.auth.revokeUserTokens(args)    })  },  /**   * Get all user permissions for a specific page   */  getEffectivePermissions (req, page) {    return {      comments: {        read: WIKI.config.features.featurePageComments ? WIKI.auth.checkAccess(req.user, ['read:comments'], page) : false,        write: WIKI.config.features.featurePageComments ? WIKI.auth.checkAccess(req.user, ['write:comments'], page) : false,        manage: WIKI.config.features.featurePageComments ? WIKI.auth.checkAccess(req.user, ['manage:comments'], page) : false      },      history: {        read: WIKI.auth.checkAccess(req.user, ['read:history'], page)      },      source: {        read: WIKI.auth.checkAccess(req.user, ['read:source'], page)      },      pages: {        read: WIKI.auth.checkAccess(req.user, ['read:pages'], page),        write: WIKI.auth.checkAccess(req.user, ['write:pages'], page),        manage: WIKI.auth.checkAccess(req.user, ['manage:pages'], page),        delete: WIKI.auth.checkAccess(req.user, ['delete:pages'], page),        script: WIKI.auth.checkAccess(req.user, ['write:scripts'], page),        style: WIKI.auth.checkAccess(req.user, ['write:styles'], page)      },      system: {        manage: WIKI.auth.checkAccess(req.user, ['manage:system'], page)      }    }  },  /**   * Add user / group ID to JWT revocation list, forcing all requests to be validated against the latest permissions   */  revokeUserTokens ({ id, kind = 'u' }) {    WIKI.auth.revocationList.set(`${kind}${_.toString(id)}`, Math.round(DateTime.utc().minus({ seconds: 5 }).toSeconds()), Math.ceil(ms(WIKI.config.auth.tokenExpiration) / 1000))  }}
 |