ThirdPartyIdentityManager.js 6.1 KB

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