ThirdPartyIdentityManager.js 6.4 KB

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