You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

414 lines
12 KiB

5 years ago
5 years ago
  1. const passport = require('passport')
  2. const passportJWT = require('passport-jwt')
  3. const _ = require('lodash')
  4. const jwt = require('jsonwebtoken')
  5. const ms = require('ms')
  6. const moment = require('moment')
  7. const Promise = require('bluebird')
  8. const crypto = Promise.promisifyAll(require('crypto'))
  9. const pem2jwk = require('pem-jwk').pem2jwk
  10. const securityHelper = require('../helpers/security')
  11. /* global WIKI */
  12. module.exports = {
  13. strategies: {},
  14. guest: {
  15. cacheExpiration: moment.utc().subtract(1, 'd')
  16. },
  17. groups: {},
  18. validApiKeys: [],
  19. /**
  20. * Initialize the authentication module
  21. */
  22. init() {
  23. this.passport = passport
  24. passport.serializeUser((user, done) => {
  25. done(null, user.id)
  26. })
  27. passport.deserializeUser(async (id, done) => {
  28. try {
  29. const user = await WIKI.models.users.query().findById(id).withGraphFetched('groups').modifyGraph('groups', builder => {
  30. builder.select('groups.id', 'permissions')
  31. })
  32. if (user) {
  33. done(null, user)
  34. } else {
  35. done(new Error(WIKI.lang.t('auth:errors:usernotfound')), null)
  36. }
  37. } catch (err) {
  38. done(err, null)
  39. }
  40. })
  41. this.reloadGroups()
  42. this.reloadApiKeys()
  43. return this
  44. },
  45. /**
  46. * Load authentication strategies
  47. */
  48. async activateStrategies() {
  49. try {
  50. // Unload any active strategies
  51. WIKI.auth.strategies = {}
  52. const currentStrategies = _.keys(passport._strategies)
  53. _.pull(currentStrategies, 'session')
  54. _.forEach(currentStrategies, stg => { passport.unuse(stg) })
  55. // Load JWT
  56. passport.use('jwt', new passportJWT.Strategy({
  57. jwtFromRequest: securityHelper.extractJWT,
  58. secretOrKey: WIKI.config.certs.public,
  59. audience: WIKI.config.auth.audience,
  60. issuer: 'urn:wiki.js',
  61. algorithms: ['RS256']
  62. }, (jwtPayload, cb) => {
  63. cb(null, jwtPayload)
  64. }))
  65. // Load enabled strategies
  66. const enabledStrategies = await WIKI.models.authentication.getStrategies()
  67. for (let idx in enabledStrategies) {
  68. const stg = enabledStrategies[idx]
  69. if (!stg.isEnabled) { continue }
  70. try {
  71. const strategy = require(`../modules/authentication/${stg.key}/authentication.js`)
  72. stg.config.callbackURL = `${WIKI.config.host}/login/${stg.key}/callback`
  73. strategy.init(passport, stg.config)
  74. strategy.config = stg.config
  75. WIKI.auth.strategies[stg.key] = {
  76. ...strategy,
  77. ...stg
  78. }
  79. WIKI.logger.info(`Authentication Strategy ${stg.key}: [ OK ]`)
  80. } catch (err) {
  81. WIKI.logger.error(`Authentication Strategy ${stg.key}: [ FAILED ]`)
  82. WIKI.logger.error(err)
  83. }
  84. }
  85. } catch (err) {
  86. WIKI.logger.error(`Failed to initialize Authentication Strategies: [ ERROR ]`)
  87. WIKI.logger.error(err)
  88. }
  89. },
  90. /**
  91. * Authenticate current request
  92. *
  93. * @param {Express Request} req
  94. * @param {Express Response} res
  95. * @param {Express Next Callback} next
  96. */
  97. authenticate(req, res, next) {
  98. WIKI.auth.passport.authenticate('jwt', {session: false}, async (err, user, info) => {
  99. if (err) { return next() }
  100. // Expired but still valid within N days, just renew
  101. if (info instanceof Error && info.name === 'TokenExpiredError' &&
  102. moment().subtract(ms(WIKI.config.auth.tokenRenewal), 'ms').isBefore(info.expiredAt)) {
  103. const jwtPayload = jwt.decode(securityHelper.extractJWT(req))
  104. try {
  105. const newToken = await WIKI.models.users.refreshToken(jwtPayload.id)
  106. user = newToken.user
  107. user.permissions = user.getGlobalPermissions()
  108. user.groups = user.getGroups()
  109. req.user = user
  110. // Try headers, otherwise cookies for response
  111. if (req.get('content-type') === 'application/json') {
  112. res.set('new-jwt', newToken.token)
  113. } else {
  114. res.cookie('jwt', newToken.token, { expires: moment().add(365, 'days').toDate() })
  115. }
  116. } catch (errc) {
  117. WIKI.logger.warn(errc)
  118. return next()
  119. }
  120. }
  121. // JWT is NOT valid, set as guest
  122. if (!user) {
  123. if (WIKI.auth.guest.cacheExpiration.isSameOrBefore(moment.utc())) {
  124. WIKI.auth.guest = await WIKI.models.users.getGuestUser()
  125. WIKI.auth.guest.cacheExpiration = moment.utc().add(1, 'm')
  126. }
  127. req.user = WIKI.auth.guest
  128. return next()
  129. }
  130. // Process API tokens
  131. if (_.has(user, 'api')) {
  132. if (!WIKI.config.api.isEnabled) {
  133. return next(new Error('API is disabled. You must enable it from the Administration Area first.'))
  134. } else if (_.includes(WIKI.auth.validApiKeys, user.api)) {
  135. req.user = {
  136. id: 1,
  137. email: 'api@localhost',
  138. name: 'API',
  139. pictureUrl: null,
  140. timezone: 'America/New_York',
  141. localeCode: 'en',
  142. permissions: _.get(WIKI.auth.groups, `${user.grp}.permissions`, []),
  143. groups: [user.grp],
  144. getGlobalPermissions () {
  145. return req.user.permissions
  146. },
  147. getGroups () {
  148. return req.user.groups
  149. }
  150. }
  151. return next()
  152. } else {
  153. return next(new Error('API Key is invalid or was revoked.'))
  154. }
  155. }
  156. // JWT is valid
  157. req.logIn(user, { session: false }, (errc) => {
  158. if (errc) { return next(errc) }
  159. next()
  160. })
  161. })(req, res, next)
  162. },
  163. /**
  164. * Check if user has access to resource
  165. *
  166. * @param {User} user
  167. * @param {Array<String>} permissions
  168. * @param {String|Boolean} path
  169. */
  170. checkAccess(user, permissions = [], page = false) {
  171. const userPermissions = user.permissions ? user.permissions : user.getGlobalPermissions()
  172. // System Admin
  173. if (_.includes(userPermissions, 'manage:system')) {
  174. return true
  175. }
  176. // Check Global Permissions
  177. if (_.intersection(userPermissions, permissions).length < 1) {
  178. return false
  179. }
  180. // Check Page Rules
  181. if (page && user.groups) {
  182. let checkState = {
  183. deny: false,
  184. match: false,
  185. specificity: ''
  186. }
  187. user.groups.forEach(grp => {
  188. const grpId = _.isObject(grp) ? _.get(grp, 'id', 0) : grp
  189. _.get(WIKI.auth.groups, `${grpId}.pageRules`, []).forEach(rule => {
  190. if (_.intersection(rule.roles, permissions).length > 0) {
  191. switch (rule.match) {
  192. case 'START':
  193. if (_.startsWith(`/${page.path}`, `/${rule.path}`)) {
  194. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['END', 'REGEX', 'EXACT', 'TAG'] })
  195. }
  196. break
  197. case 'END':
  198. if (_.endsWith(page.path, rule.path)) {
  199. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['REGEX', 'EXACT', 'TAG'] })
  200. }
  201. break
  202. case 'REGEX':
  203. const reg = new RegExp(rule.path)
  204. if (reg.test(page.path)) {
  205. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: ['EXACT', 'TAG'] })
  206. }
  207. break
  208. case 'TAG':
  209. _.get(page, 'tags', []).forEach(tag => {
  210. if (tag.tag === rule.path) {
  211. checkState = this._applyPageRuleSpecificity({
  212. rule,
  213. checkState,
  214. higherPriority: ['EXACT']
  215. })
  216. }
  217. })
  218. break
  219. case 'EXACT':
  220. if (`/${page.path}` === `/${rule.path}`) {
  221. checkState = this._applyPageRuleSpecificity({ rule, checkState, higherPriority: [] })
  222. }
  223. break
  224. }
  225. }
  226. })
  227. })
  228. return (checkState.match && !checkState.deny)
  229. }
  230. return false
  231. },
  232. /**
  233. * Check and apply Page Rule specificity
  234. *
  235. * @access private
  236. */
  237. _applyPageRuleSpecificity ({ rule, checkState, higherPriority = [] }) {
  238. if (rule.path.length === checkState.specificity.length) {
  239. // Do not override higher priority rules
  240. if (_.includes(higherPriority, checkState.match)) {
  241. return checkState
  242. }
  243. // Do not override a previous DENY rule with same match
  244. if (rule.match === checkState.match && checkState.deny && !rule.deny) {
  245. return checkState
  246. }
  247. } else if (rule.path.length < checkState.specificity.length) {
  248. // Do not override higher specificity rules
  249. return checkState
  250. }
  251. return {
  252. deny: rule.deny,
  253. match: rule.match,
  254. specificity: rule.path
  255. }
  256. },
  257. /**
  258. * Reload Groups from DB
  259. */
  260. async reloadGroups () {
  261. const groupsArray = await WIKI.models.groups.query()
  262. this.groups = _.keyBy(groupsArray, 'id')
  263. WIKI.auth.guest.cacheExpiration = moment.utc().subtract(1, 'd')
  264. },
  265. /**
  266. * Reload valid API Keys from DB
  267. */
  268. async reloadApiKeys () {
  269. const keys = await WIKI.models.apiKeys.query().select('id').where('isRevoked', false).andWhere('expiration', '>', moment.utc().toISOString())
  270. this.validApiKeys = _.map(keys, 'id')
  271. },
  272. /**
  273. * Generate New Authentication Public / Private Key Certificates
  274. */
  275. async regenerateCertificates () {
  276. WIKI.logger.info('Regenerating certificates...')
  277. _.set(WIKI.config, 'sessionSecret', (await crypto.randomBytesAsync(32)).toString('hex'))
  278. const certs = crypto.generateKeyPairSync('rsa', {
  279. modulusLength: 2048,
  280. publicKeyEncoding: {
  281. type: 'pkcs1',
  282. format: 'pem'
  283. },
  284. privateKeyEncoding: {
  285. type: 'pkcs1',
  286. format: 'pem',
  287. cipher: 'aes-256-cbc',
  288. passphrase: WIKI.config.sessionSecret
  289. }
  290. })
  291. _.set(WIKI.config, 'certs', {
  292. jwk: pem2jwk(certs.publicKey),
  293. public: certs.publicKey,
  294. private: certs.privateKey
  295. })
  296. await WIKI.configSvc.saveToDb([
  297. 'certs',
  298. 'sessionSecret'
  299. ])
  300. await WIKI.auth.activateStrategies()
  301. WIKI.events.outbound.emit('reloadAuthStrategies')
  302. WIKI.logger.info('Regenerated certificates: [ COMPLETED ]')
  303. },
  304. /**
  305. * Reset Guest User
  306. */
  307. async resetGuestUser() {
  308. WIKI.logger.info('Resetting guest account...')
  309. const guestGroup = await WIKI.models.groups.query().where('id', 2).first()
  310. await WIKI.models.users.query().delete().where({
  311. providerKey: 'local',
  312. email: 'guest@example.com'
  313. }).orWhere('id', 2)
  314. const guestUser = await WIKI.models.users.query().insert({
  315. id: 2,
  316. provider: 'local',
  317. email: 'guest@example.com',
  318. name: 'Guest',
  319. password: '',
  320. locale: 'en',
  321. defaultEditor: 'markdown',
  322. tfaIsActive: false,
  323. isSystem: true,
  324. isActive: true,
  325. isVerified: true
  326. })
  327. await guestUser.$relatedQuery('groups').relate(guestGroup.id)
  328. WIKI.logger.info('Guest user has been reset: [ COMPLETED ]')
  329. },
  330. /**
  331. * Subscribe to HA propagation events
  332. */
  333. subscribeToEvents() {
  334. WIKI.events.inbound.on('reloadGroups', () => {
  335. WIKI.auth.reloadGroups()
  336. })
  337. WIKI.events.inbound.on('reloadApiKeys', () => {
  338. WIKI.auth.reloadApiKeys()
  339. })
  340. WIKI.events.inbound.on('reloadAuthStrategies', () => {
  341. WIKI.auth.activateStrategies()
  342. })
  343. },
  344. /**
  345. * Get all user permissions for a specific page
  346. */
  347. getEffectivePermissions (req, page) {
  348. return {
  349. comments: {
  350. read: WIKI.config.features.featurePageComments ? WIKI.auth.checkAccess(req.user, ['read:comments'], page) : false,
  351. write: WIKI.config.features.featurePageComments ? WIKI.auth.checkAccess(req.user, ['write:comments'], page) : false,
  352. manage: WIKI.config.features.featurePageComments ? WIKI.auth.checkAccess(req.user, ['manage:comments'], page) : false
  353. },
  354. history: {
  355. read: WIKI.auth.checkAccess(req.user, ['read:history'], page)
  356. },
  357. source: {
  358. read: WIKI.auth.checkAccess(req.user, ['read:source'], page)
  359. },
  360. pages: {
  361. read: WIKI.auth.checkAccess(req.user, ['read:pages'], page),
  362. write: WIKI.auth.checkAccess(req.user, ['write:pages'], page),
  363. manage: WIKI.auth.checkAccess(req.user, ['manage:pages'], page),
  364. delete: WIKI.auth.checkAccess(req.user, ['delete:pages'], page),
  365. script: WIKI.auth.checkAccess(req.user, ['write:scripts'], page),
  366. style: WIKI.auth.checkAccess(req.user, ['write:styles'], page)
  367. },
  368. system: {
  369. manage: WIKI.auth.checkAccess(req.user, ['manage:system'], page)
  370. }
  371. }
  372. }
  373. }