ThirdPartyIdentityManager.mjs 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import UserAuditLogHandler from '../../../../app/src/Features/User/UserAuditLogHandler.mjs'
  2. import EmailHandler from '../../../../app/src/Features/Email/EmailHandler.mjs'
  3. import EmailOptionsHelper from '../../../../app/src/Features/Email/EmailOptionsHelper.mjs'
  4. import Errors from '../Errors/Errors.js'
  5. import _ from 'lodash'
  6. import logger from '@overleaf/logger'
  7. import settings from '@overleaf/settings'
  8. import { User } from '../../../../app/src/models/User.mjs'
  9. import { callbackify } from '@overleaf/promise-utils'
  10. import OError from '@overleaf/o-error'
  11. const oauthProviders = settings.oauthProviders || {}
  12. async function getUser(providerId, externalUserId) {
  13. if (providerId == null || externalUserId == null) {
  14. throw new OError('invalid SSO arguments', {
  15. externalUserId,
  16. providerId,
  17. })
  18. }
  19. const query = _getUserQuery(providerId, externalUserId)
  20. const user = await User.findOne(query).exec()
  21. if (!user) {
  22. throw new Errors.ThirdPartyUserNotFoundError()
  23. }
  24. return user
  25. }
  26. async function login(providerId, externalUserId, externalData) {
  27. const user = await ThirdPartyIdentityManager.promises.getUser(
  28. providerId,
  29. externalUserId
  30. )
  31. if (!externalData) {
  32. return user
  33. }
  34. const query = _getUserQuery(providerId, externalUserId)
  35. const update = _thirdPartyIdentifierUpdate(
  36. user,
  37. providerId,
  38. externalUserId,
  39. externalData
  40. )
  41. return await User.findOneAndUpdate(query, update, { new: true }).exec()
  42. }
  43. async function link(
  44. userId,
  45. providerId,
  46. externalUserId,
  47. externalData,
  48. auditLog,
  49. retry
  50. ) {
  51. const accountLinked = true
  52. if (!oauthProviders[providerId]) {
  53. throw new Error('Not a valid provider')
  54. }
  55. await UserAuditLogHandler.promises.addEntry(
  56. userId,
  57. 'link-sso',
  58. auditLog.initiatorId,
  59. auditLog.ipAddress,
  60. {
  61. providerId,
  62. }
  63. )
  64. const query = {
  65. _id: userId,
  66. 'thirdPartyIdentifiers.providerId': {
  67. $ne: providerId,
  68. },
  69. }
  70. const update = {
  71. $push: {
  72. thirdPartyIdentifiers: {
  73. externalUserId,
  74. externalData,
  75. providerId,
  76. },
  77. },
  78. }
  79. // add new tpi only if an entry for the provider does not exist
  80. // projection includes thirdPartyIdentifiers for tests
  81. let res
  82. try {
  83. res = await User.findOneAndUpdate(query, update, { new: 1 }).exec()
  84. } catch (err) {
  85. if (err.code === 11000) {
  86. throw new Errors.ThirdPartyIdentityExistsError({
  87. info: { externalUserId },
  88. })
  89. }
  90. throw err
  91. }
  92. if (res) {
  93. _sendSecurityAlert(accountLinked, providerId, res, userId)
  94. return res
  95. }
  96. if (retry) {
  97. // if already retried then throw error
  98. throw new Error('update failed')
  99. }
  100. // attempt to clear existing entry then retry
  101. await ThirdPartyIdentityManager.promises.unlink(userId, providerId, auditLog)
  102. return await ThirdPartyIdentityManager.promises.link(
  103. userId,
  104. providerId,
  105. externalUserId,
  106. externalData,
  107. auditLog,
  108. true
  109. )
  110. }
  111. async function unlink(userId, providerId, auditLog) {
  112. const accountLinked = false
  113. if (!oauthProviders[providerId]) {
  114. throw new Error('Not a valid provider')
  115. }
  116. await UserAuditLogHandler.promises.addEntry(
  117. userId,
  118. 'unlink-sso',
  119. auditLog.initiatorId,
  120. auditLog.ipAddress,
  121. {
  122. ...(auditLog.extraInfo || {}),
  123. providerId,
  124. }
  125. )
  126. const query = {
  127. _id: userId,
  128. }
  129. const update = {
  130. $pull: {
  131. thirdPartyIdentifiers: {
  132. providerId,
  133. },
  134. },
  135. }
  136. // projection includes thirdPartyIdentifiers for tests
  137. const res = await User.findOneAndUpdate(query, update, { new: 1 })
  138. if (!res) {
  139. throw new Error('update failed')
  140. }
  141. _sendSecurityAlert(accountLinked, providerId, res, userId)
  142. return res
  143. }
  144. function _getUserQuery(providerId, externalUserId) {
  145. externalUserId = externalUserId.toString()
  146. providerId = providerId.toString()
  147. const query = {
  148. 'thirdPartyIdentifiers.externalUserId': externalUserId,
  149. 'thirdPartyIdentifiers.providerId': providerId,
  150. }
  151. return query
  152. }
  153. function _sendSecurityAlert(accountLinked, providerId, user, userId) {
  154. const providerName = oauthProviders[providerId].name
  155. const emailOptions = EmailOptionsHelper.linkOrUnlink(
  156. accountLinked,
  157. providerName,
  158. user.email
  159. )
  160. EmailHandler.promises
  161. .sendEmail('securityAlert', emailOptions)
  162. .catch(error => {
  163. logger.error(
  164. { err: error, userId },
  165. `could not send security alert email when ${emailOptions.action.toLowerCase()}`
  166. )
  167. })
  168. }
  169. function _thirdPartyIdentifierUpdate(
  170. user,
  171. providerId,
  172. externalUserId,
  173. externalData
  174. ) {
  175. providerId = providerId.toString()
  176. // get third party identifier object from array
  177. const thirdPartyIdentifier = user.thirdPartyIdentifiers.find(
  178. tpi =>
  179. tpi.externalUserId === externalUserId && tpi.providerId === providerId
  180. )
  181. // do recursive merge of new data over existing data
  182. _.merge(thirdPartyIdentifier.externalData, externalData)
  183. const update = { 'thirdPartyIdentifiers.$': thirdPartyIdentifier }
  184. return update
  185. }
  186. const ThirdPartyIdentityManager = {
  187. getUser: callbackify(getUser),
  188. login: callbackify(login),
  189. link: callbackify(link),
  190. unlink: callbackify(unlink),
  191. }
  192. ThirdPartyIdentityManager.promises = {
  193. getUser,
  194. login,
  195. link,
  196. unlink,
  197. }
  198. export default ThirdPartyIdentityManager