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.

715 lines
20 KiB

6 years ago
  1. /* global WIKI */
  2. const bcrypt = require('bcryptjs-then')
  3. const _ = require('lodash')
  4. const tfa = require('node-2fa')
  5. const jwt = require('jsonwebtoken')
  6. const Model = require('objection').Model
  7. const validate = require('validate.js')
  8. const bcryptRegexp = /^\$2[ayb]\$[0-9]{2}\$[A-Za-z0-9./]{53}$/
  9. /**
  10. * Users model
  11. */
  12. module.exports = class User extends Model {
  13. static get tableName() { return 'users' }
  14. static get jsonSchema () {
  15. return {
  16. type: 'object',
  17. required: ['email'],
  18. properties: {
  19. id: {type: 'integer'},
  20. email: {type: 'string', format: 'email'},
  21. name: {type: 'string', minLength: 1, maxLength: 255},
  22. providerId: {type: 'string'},
  23. password: {type: 'string'},
  24. role: {type: 'string', enum: ['admin', 'guest', 'user']},
  25. tfaIsActive: {type: 'boolean', default: false},
  26. tfaSecret: {type: 'string'},
  27. jobTitle: {type: 'string'},
  28. location: {type: 'string'},
  29. pictureUrl: {type: 'string'},
  30. isSystem: {type: 'boolean'},
  31. isActive: {type: 'boolean'},
  32. isVerified: {type: 'boolean'},
  33. createdAt: {type: 'string'},
  34. updatedAt: {type: 'string'}
  35. }
  36. }
  37. }
  38. static get relationMappings() {
  39. return {
  40. groups: {
  41. relation: Model.ManyToManyRelation,
  42. modelClass: require('./groups'),
  43. join: {
  44. from: 'users.id',
  45. through: {
  46. from: 'userGroups.userId',
  47. to: 'userGroups.groupId'
  48. },
  49. to: 'groups.id'
  50. }
  51. },
  52. provider: {
  53. relation: Model.BelongsToOneRelation,
  54. modelClass: require('./authentication'),
  55. join: {
  56. from: 'users.providerKey',
  57. to: 'authentication.key'
  58. }
  59. },
  60. defaultEditor: {
  61. relation: Model.BelongsToOneRelation,
  62. modelClass: require('./editors'),
  63. join: {
  64. from: 'users.editorKey',
  65. to: 'editors.key'
  66. }
  67. },
  68. locale: {
  69. relation: Model.BelongsToOneRelation,
  70. modelClass: require('./locales'),
  71. join: {
  72. from: 'users.localeCode',
  73. to: 'locales.code'
  74. }
  75. }
  76. }
  77. }
  78. async $beforeUpdate(opt, context) {
  79. await super.$beforeUpdate(opt, context)
  80. this.updatedAt = new Date().toISOString()
  81. if (!(opt.patch && this.password === undefined)) {
  82. await this.generateHash()
  83. }
  84. }
  85. async $beforeInsert(context) {
  86. await super.$beforeInsert(context)
  87. this.createdAt = new Date().toISOString()
  88. this.updatedAt = new Date().toISOString()
  89. await this.generateHash()
  90. }
  91. // ------------------------------------------------
  92. // Instance Methods
  93. // ------------------------------------------------
  94. async generateHash() {
  95. if (this.password) {
  96. if (bcryptRegexp.test(this.password)) { return }
  97. this.password = await bcrypt.hash(this.password, 12)
  98. }
  99. }
  100. async verifyPassword(pwd) {
  101. if (await bcrypt.compare(pwd, this.password) === true) {
  102. return true
  103. } else {
  104. throw new WIKI.Error.AuthLoginFailed()
  105. }
  106. }
  107. async enableTFA() {
  108. let tfaInfo = tfa.generateSecret({
  109. name: WIKI.config.site.title
  110. })
  111. return this.$query.patch({
  112. tfaIsActive: true,
  113. tfaSecret: tfaInfo.secret
  114. })
  115. }
  116. async disableTFA() {
  117. return this.$query.patch({
  118. tfaIsActive: false,
  119. tfaSecret: ''
  120. })
  121. }
  122. async verifyTFA(code) {
  123. let result = tfa.verifyToken(this.tfaSecret, code)
  124. return (result && _.has(result, 'delta') && result.delta === 0)
  125. }
  126. getGlobalPermissions() {
  127. return _.uniq(_.flatten(_.map(this.groups, 'permissions')))
  128. }
  129. getGroups() {
  130. return _.uniq(_.map(this.groups, 'id'))
  131. }
  132. // ------------------------------------------------
  133. // Model Methods
  134. // ------------------------------------------------
  135. static async processProfile({ profile, providerKey }) {
  136. const provider = _.get(WIKI.auth.strategies, providerKey, {})
  137. provider.info = _.find(WIKI.data.authentication, ['key', providerKey])
  138. // Find existing user
  139. let user = await WIKI.models.users.query().findOne({
  140. providerId: _.toString(profile.id),
  141. providerKey
  142. })
  143. // Parse email
  144. let primaryEmail = ''
  145. if (_.isArray(profile.emails)) {
  146. const e = _.find(profile.emails, ['primary', true])
  147. primaryEmail = (e) ? e.value : _.first(profile.emails).value
  148. } else if (_.isString(profile.email) && profile.email.length > 5) {
  149. primaryEmail = profile.email
  150. } else if (_.isString(profile.mail) && profile.mail.length > 5) {
  151. primaryEmail = profile.mail
  152. } else if (profile.user && profile.user.email && profile.user.email.length > 5) {
  153. primaryEmail = profile.user.email
  154. } else {
  155. throw new Error('Missing or invalid email address from profile.')
  156. }
  157. primaryEmail = _.toLower(primaryEmail)
  158. // Find pending social user
  159. if (!user) {
  160. user = await WIKI.models.users.query().findOne({
  161. email: primaryEmail,
  162. providerId: null,
  163. providerKey
  164. })
  165. if (user) {
  166. user = await user.$query().patchAndFetch({
  167. providerId: _.toString(profile.id)
  168. })
  169. }
  170. }
  171. // Parse display name
  172. let displayName = ''
  173. if (_.isString(profile.displayName) && profile.displayName.length > 0) {
  174. displayName = profile.displayName
  175. } else if (_.isString(profile.name) && profile.name.length > 0) {
  176. displayName = profile.name
  177. } else {
  178. displayName = primaryEmail.split('@')[0]
  179. }
  180. // Parse picture URL
  181. let pictureUrl = _.get(profile, 'picture', _.get(user, 'pictureUrl', null))
  182. // Update existing user
  183. if (user) {
  184. if (!user.isActive) {
  185. throw new WIKI.Error.AuthAccountBanned()
  186. }
  187. if (user.isSystem) {
  188. throw new Error('This is a system reserved account and cannot be used.')
  189. }
  190. user = await user.$query().patchAndFetch({
  191. email: primaryEmail,
  192. name: displayName,
  193. pictureUrl: pictureUrl
  194. })
  195. return user
  196. }
  197. // Self-registration
  198. if (provider.selfRegistration) {
  199. // Check if email domain is whitelisted
  200. if (_.get(provider, 'domainWhitelist', []).length > 0) {
  201. const emailDomain = _.last(primaryEmail.split('@'))
  202. if (!_.includes(provider.domainWhitelist, emailDomain)) {
  203. throw new WIKI.Error.AuthRegistrationDomainUnauthorized()
  204. }
  205. }
  206. // Create account
  207. user = await WIKI.models.users.query().insertAndFetch({
  208. providerKey: providerKey,
  209. providerId: _.toString(profile.id),
  210. email: primaryEmail,
  211. name: displayName,
  212. pictureUrl: pictureUrl,
  213. localeCode: WIKI.config.lang.code,
  214. defaultEditor: 'markdown',
  215. tfaIsActive: false,
  216. isSystem: false,
  217. isActive: true,
  218. isVerified: true
  219. })
  220. // Assign to group(s)
  221. if (provider.autoEnrollGroups.length > 0) {
  222. await user.$relatedQuery('groups').relate(provider.autoEnrollGroups)
  223. }
  224. return user
  225. }
  226. throw new Error('You are not authorized to login.')
  227. }
  228. static async login (opts, context) {
  229. if (_.has(WIKI.auth.strategies, opts.strategy)) {
  230. const strInfo = _.find(WIKI.data.authentication, ['key', opts.strategy])
  231. // Inject form user/pass
  232. if (strInfo.useForm) {
  233. _.set(context.req, 'body.email', opts.username)
  234. _.set(context.req, 'body.password', opts.password)
  235. }
  236. // Authenticate
  237. return new Promise((resolve, reject) => {
  238. WIKI.auth.passport.authenticate(opts.strategy, {
  239. session: !strInfo.useForm,
  240. scope: strInfo.scopes ? strInfo.scopes : null
  241. }, async (err, user, info) => {
  242. if (err) { return reject(err) }
  243. if (!user) { return reject(new WIKI.Error.AuthLoginFailed()) }
  244. // Must Change Password?
  245. if (user.mustChangePwd) {
  246. try {
  247. const pwdChangeToken = await WIKI.models.userKeys.generateToken({
  248. kind: 'changePwd',
  249. userId: user.id
  250. })
  251. return resolve({
  252. mustChangePwd: true,
  253. continuationToken: pwdChangeToken
  254. })
  255. } catch (err) {
  256. WIKI.logger.warn(err)
  257. return reject(new WIKI.Error.AuthGenericError())
  258. }
  259. }
  260. // Is 2FA required?
  261. if (user.tfaIsActive) {
  262. try {
  263. const tfaToken = await WIKI.models.userKeys.generateToken({
  264. kind: 'tfa',
  265. userId: user.id
  266. })
  267. return resolve({
  268. tfaRequired: true,
  269. continuationToken: tfaToken
  270. })
  271. } catch (err) {
  272. WIKI.logger.warn(err)
  273. return reject(new WIKI.Error.AuthGenericError())
  274. }
  275. }
  276. context.req.logIn(user, { session: !strInfo.useForm }, async err => {
  277. if (err) { return reject(err) }
  278. const jwtToken = await WIKI.models.users.refreshToken(user)
  279. resolve({ jwt: jwtToken.token })
  280. })
  281. })(context.req, context.res, () => {})
  282. })
  283. } else {
  284. throw new WIKI.Error.AuthProviderInvalid()
  285. }
  286. }
  287. static async refreshToken(user) {
  288. if (_.isSafeInteger(user)) {
  289. user = await WIKI.models.users.query().findById(user).eager('groups').modifyEager('groups', builder => {
  290. builder.select('groups.id', 'permissions')
  291. })
  292. if (!user) {
  293. WIKI.logger.warn(`Failed to refresh token for user ${user}: Not found.`)
  294. throw new WIKI.Error.AuthGenericError()
  295. }
  296. } else if (_.isNil(user.groups)) {
  297. await user.$relatedQuery('groups').select('groups.id', 'permissions')
  298. }
  299. return {
  300. token: jwt.sign({
  301. id: user.id,
  302. email: user.email,
  303. name: user.name,
  304. pictureUrl: user.pictureUrl,
  305. timezone: user.timezone,
  306. localeCode: user.localeCode,
  307. defaultEditor: user.defaultEditor,
  308. permissions: user.getGlobalPermissions(),
  309. groups: user.getGroups()
  310. }, {
  311. key: WIKI.config.certs.private,
  312. passphrase: WIKI.config.sessionSecret
  313. }, {
  314. algorithm: 'RS256',
  315. expiresIn: WIKI.config.auth.tokenExpiration,
  316. audience: WIKI.config.auth.audience,
  317. issuer: 'urn:wiki.js'
  318. }),
  319. user
  320. }
  321. }
  322. static async loginTFA (opts, context) {
  323. if (opts.securityCode.length === 6 && opts.loginToken.length === 64) {
  324. let result = await WIKI.redis.get(`tfa:${opts.loginToken}`)
  325. if (result) {
  326. let userId = _.toSafeInteger(result)
  327. if (userId && userId > 0) {
  328. let user = await WIKI.models.users.query().findById(userId)
  329. if (user && user.verifyTFA(opts.securityCode)) {
  330. return Promise.fromCallback(clb => {
  331. context.req.logIn(user, clb)
  332. }).return({
  333. succeeded: true,
  334. message: 'Login Successful'
  335. }).catch(err => {
  336. WIKI.logger.warn(err)
  337. throw new WIKI.Error.AuthGenericError()
  338. })
  339. } else {
  340. throw new WIKI.Error.AuthTFAFailed()
  341. }
  342. }
  343. }
  344. }
  345. throw new WIKI.Error.AuthTFAInvalid()
  346. }
  347. /**
  348. * Change Password from a Mandatory Password Change after Login
  349. */
  350. static async loginChangePassword ({ continuationToken, newPassword }, context) {
  351. if (!newPassword || newPassword.length < 6) {
  352. throw new WIKI.Error.InputInvalid('Password must be at least 6 characters!')
  353. }
  354. const usr = await WIKI.models.userKeys.validateToken({
  355. kind: 'changePwd',
  356. token: continuationToken
  357. })
  358. if (usr) {
  359. await WIKI.models.users.query().patch({
  360. password: newPassword,
  361. mustChangePwd: false
  362. }).findById(usr.id)
  363. return new Promise((resolve, reject) => {
  364. context.req.logIn(usr, { session: false }, async err => {
  365. if (err) { return reject(err) }
  366. const jwtToken = await WIKI.models.users.refreshToken(usr)
  367. resolve({ jwt: jwtToken.token })
  368. })
  369. })
  370. } else {
  371. throw new WIKI.Error.UserNotFound()
  372. }
  373. }
  374. /**
  375. * Create a new user
  376. *
  377. * @param {Object} param0 User Fields
  378. */
  379. static async createNewUser ({ providerKey, email, passwordRaw, name, groups, mustChangePassword, sendWelcomeEmail }) {
  380. // Input sanitization
  381. email = _.toLower(email)
  382. // Input validation
  383. let validation = null
  384. if (providerKey === 'local') {
  385. validation = validate({
  386. email,
  387. passwordRaw,
  388. name
  389. }, {
  390. email: {
  391. email: true,
  392. length: {
  393. maximum: 255
  394. }
  395. },
  396. passwordRaw: {
  397. presence: {
  398. allowEmpty: false
  399. },
  400. length: {
  401. minimum: 6
  402. }
  403. },
  404. name: {
  405. presence: {
  406. allowEmpty: false
  407. },
  408. length: {
  409. minimum: 2,
  410. maximum: 255
  411. }
  412. }
  413. }, { format: 'flat' })
  414. } else {
  415. validation = validate({
  416. email,
  417. name
  418. }, {
  419. email: {
  420. email: true,
  421. length: {
  422. maximum: 255
  423. }
  424. },
  425. name: {
  426. presence: {
  427. allowEmpty: false
  428. },
  429. length: {
  430. minimum: 2,
  431. maximum: 255
  432. }
  433. }
  434. }, { format: 'flat' })
  435. }
  436. if (validation && validation.length > 0) {
  437. throw new WIKI.Error.InputInvalid(validation[0])
  438. }
  439. // Check if email already exists
  440. const usr = await WIKI.models.users.query().findOne({ email, providerKey })
  441. if (!usr) {
  442. // Create the account
  443. let newUsrData = {
  444. providerKey,
  445. email,
  446. name,
  447. locale: 'en',
  448. defaultEditor: 'markdown',
  449. tfaIsActive: false,
  450. isSystem: false,
  451. isActive: true,
  452. isVerified: true,
  453. mustChangePwd: false
  454. }
  455. if (providerKey === `local`) {
  456. newUsrData.password = passwordRaw
  457. newUsrData.mustChangePwd = (mustChangePassword === true)
  458. }
  459. const newUsr = await WIKI.models.users.query().insert(newUsrData)
  460. // Assign to group(s)
  461. if (groups.length > 0) {
  462. await newUsr.$relatedQuery('groups').relate(groups)
  463. }
  464. if (sendWelcomeEmail) {
  465. // Send welcome email
  466. await WIKI.mail.send({
  467. template: 'accountWelcome',
  468. to: email,
  469. subject: `Welcome to the wiki ${WIKI.config.title}`,
  470. data: {
  471. preheadertext: `You've been invited to the wiki ${WIKI.config.title}`,
  472. title: `You've been invited to the wiki ${WIKI.config.title}`,
  473. content: `Click the button below to access the wiki.`,
  474. buttonLink: `${WIKI.config.host}/login`,
  475. buttonText: 'Login'
  476. },
  477. text: `You've been invited to the wiki ${WIKI.config.title}: ${WIKI.config.host}/login`
  478. })
  479. }
  480. } else {
  481. throw new WIKI.Error.AuthAccountAlreadyExists()
  482. }
  483. }
  484. /**
  485. * Update an existing user
  486. *
  487. * @param {Object} param0 User ID and fields to update
  488. */
  489. static async updateUser ({ id, email, name, newPassword, groups, location, jobTitle, timezone }) {
  490. const usr = await WIKI.models.users.query().findById(id)
  491. if (usr) {
  492. let usrData = {}
  493. if (!_.isEmpty(email) && email !== usr.email) {
  494. const dupUsr = await WIKI.models.users.query().select('id').where({
  495. email,
  496. providerKey: usr.providerKey
  497. })
  498. if (dupUsr) {
  499. throw new WIKI.Error.AuthAccountAlreadyExists()
  500. }
  501. usrData.email = email
  502. }
  503. if (!_.isEmpty(name) && name !== usr.name) {
  504. usrData.name = _.trim(name)
  505. }
  506. if (!_.isEmpty(newPassword)) {
  507. if (newPassword.length < 6) {
  508. throw new WIKI.Error.InputInvalid('Password must be at least 6 characters!')
  509. }
  510. usrData.password = newPassword
  511. }
  512. if (_.isArray(groups)) {
  513. const usrGroupsRaw = await usr.$relatedQuery('groups')
  514. const usrGroups = _.map(usrGroupsRaw, 'id')
  515. // Relate added groups
  516. const addUsrGroups = _.difference(groups, usrGroups)
  517. for (const grp of addUsrGroups) {
  518. await usr.$relatedQuery('groups').relate(grp)
  519. }
  520. // Unrelate removed groups
  521. const remUsrGroups = _.difference(usrGroups, groups)
  522. for (const grp of remUsrGroups) {
  523. await usr.$relatedQuery('groups').unrelate().where('groupId', grp)
  524. }
  525. }
  526. if (!_.isEmpty(location) && location !== usr.location) {
  527. usrData.location = _.trim(location)
  528. }
  529. if (!_.isEmpty(jobTitle) && jobTitle !== usr.jobTitle) {
  530. usrData.jobTitle = _.trim(jobTitle)
  531. }
  532. if (!_.isEmpty(timezone) && timezone !== usr.timezone) {
  533. usrData.timezone = timezone
  534. }
  535. await WIKI.models.users.query().patch(usrData).findById(id)
  536. } else {
  537. throw new WIKI.Error.UserNotFound()
  538. }
  539. }
  540. /**
  541. * Register a new user (client-side registration)
  542. *
  543. * @param {Object} param0 User fields
  544. * @param {Object} context GraphQL Context
  545. */
  546. static async register ({ email, password, name, verify = false, bypassChecks = false }, context) {
  547. const localStrg = await WIKI.models.authentication.getStrategy('local')
  548. // Check if self-registration is enabled
  549. if (localStrg.selfRegistration || bypassChecks) {
  550. // Input sanitization
  551. email = _.toLower(email)
  552. // Input validation
  553. const validation = validate({
  554. email,
  555. password,
  556. name
  557. }, {
  558. email: {
  559. email: true,
  560. length: {
  561. maximum: 255
  562. }
  563. },
  564. password: {
  565. presence: {
  566. allowEmpty: false
  567. },
  568. length: {
  569. minimum: 6
  570. }
  571. },
  572. name: {
  573. presence: {
  574. allowEmpty: false
  575. },
  576. length: {
  577. minimum: 2,
  578. maximum: 255
  579. }
  580. }
  581. }, { format: 'flat' })
  582. if (validation && validation.length > 0) {
  583. throw new WIKI.Error.InputInvalid(validation[0])
  584. }
  585. // Check if email domain is whitelisted
  586. if (_.get(localStrg, 'domainWhitelist.v', []).length > 0 && !bypassChecks) {
  587. const emailDomain = _.last(email.split('@'))
  588. if (!_.includes(localStrg.domainWhitelist.v, emailDomain)) {
  589. throw new WIKI.Error.AuthRegistrationDomainUnauthorized()
  590. }
  591. }
  592. // Check if email already exists
  593. const usr = await WIKI.models.users.query().findOne({ email, providerKey: 'local' })
  594. if (!usr) {
  595. // Create the account
  596. const newUsr = await WIKI.models.users.query().insert({
  597. provider: 'local',
  598. email,
  599. name,
  600. password,
  601. locale: 'en',
  602. defaultEditor: 'markdown',
  603. tfaIsActive: false,
  604. isSystem: false,
  605. isActive: true,
  606. isVerified: false
  607. })
  608. // Assign to group(s)
  609. if (_.get(localStrg, 'autoEnrollGroups.v', []).length > 0) {
  610. await newUsr.$relatedQuery('groups').relate(localStrg.autoEnrollGroups.v)
  611. }
  612. if (verify) {
  613. // Create verification token
  614. const verificationToken = await WIKI.models.userKeys.generateToken({
  615. kind: 'verify',
  616. userId: newUsr.id
  617. })
  618. // Send verification email
  619. await WIKI.mail.send({
  620. template: 'accountVerify',
  621. to: email,
  622. subject: 'Verify your account',
  623. data: {
  624. preheadertext: 'Verify your account in order to gain access to the wiki.',
  625. title: 'Verify your account',
  626. content: 'Click the button below in order to verify your account and gain access to the wiki.',
  627. buttonLink: `${WIKI.config.host}/verify/${verificationToken}`,
  628. buttonText: 'Verify'
  629. },
  630. text: `You must open the following link in your browser to verify your account and gain access to the wiki: ${WIKI.config.host}/verify/${verificationToken}`
  631. })
  632. }
  633. return true
  634. } else {
  635. throw new WIKI.Error.AuthAccountAlreadyExists()
  636. }
  637. } else {
  638. throw new WIKI.Error.AuthRegistrationDisabled()
  639. }
  640. }
  641. static async getGuestUser () {
  642. const user = await WIKI.models.users.query().findById(2).eager('groups').modifyEager('groups', builder => {
  643. builder.select('groups.id', 'permissions')
  644. })
  645. if (!user) {
  646. WIKI.logger.error('CRITICAL ERROR: Guest user is missing!')
  647. process.exit(1)
  648. }
  649. user.permissions = user.getGlobalPermissions()
  650. return user
  651. }
  652. }