ThirdPartyIdentityManager.js 6.1 KB

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