SAMLIdentityManager.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. const EmailHandler = require('../Email/EmailHandler')
  2. const Errors = require('../Errors/Errors')
  3. const logger = require('logger-sharelatex')
  4. const OError = require('@overleaf/o-error')
  5. const { User } = require('../../models/User')
  6. const UserGetter = require('../User/UserGetter')
  7. const UserUpdater = require('../User/UserUpdater')
  8. async function _addIdentifier(
  9. userId,
  10. externalUserId,
  11. providerId,
  12. hasEntitlement,
  13. institutionEmail
  14. ) {
  15. // first check if institutionEmail linked to another account
  16. // before adding the identifier for the email
  17. const user = await UserGetter.promises.getUserByAnyEmail(institutionEmail)
  18. if (user && user._id.toString() !== userId.toString()) {
  19. const existingEmailData = user.emails.find(
  20. emailData => emailData.email === institutionEmail
  21. )
  22. if (existingEmailData && existingEmailData.samlProviderId) {
  23. // email exists and institution link.
  24. // Return back to requesting page with error
  25. throw new Errors.SAMLIdentityExistsError()
  26. } else {
  27. // Only email exists but not linked, so redirect to linking page
  28. // which will tell this user to log out to link
  29. throw new Errors.EmailExistsError()
  30. }
  31. }
  32. providerId = providerId.toString()
  33. hasEntitlement = !!hasEntitlement
  34. const query = {
  35. _id: userId,
  36. 'samlIdentifiers.providerId': {
  37. $ne: providerId
  38. }
  39. }
  40. const update = {
  41. $push: {
  42. samlIdentifiers: {
  43. hasEntitlement,
  44. externalUserId,
  45. providerId
  46. }
  47. }
  48. }
  49. // First update user.samlIdentifiers
  50. let updatedUser = User.findOneAndUpdate(query, update, { new: true }).exec()
  51. try {
  52. updatedUser = User.findOneAndUpdate(query, update, { new: true }).exec()
  53. } catch (err) {
  54. if (err && err.code === 11000) {
  55. throw new Errors.SAMLIdentityExistsError()
  56. } else if (err != null) {
  57. logger.log(err, userId, 'failed to add institution SAML identifier')
  58. throw new OError(err)
  59. }
  60. }
  61. return updatedUser
  62. }
  63. function _getUserQuery(providerId, externalUserId) {
  64. externalUserId = externalUserId.toString()
  65. providerId = providerId.toString()
  66. const query = {
  67. 'samlIdentifiers.externalUserId': externalUserId,
  68. 'samlIdentifiers.providerId': providerId
  69. }
  70. return query
  71. }
  72. async function _addInstitutionEmail(userId, email, providerId) {
  73. const user = await UserGetter.promises.getUser(userId)
  74. const query = {
  75. _id: userId,
  76. 'emails.email': email
  77. }
  78. const update = {
  79. $set: {
  80. 'emails.$.samlProviderId': providerId.toString()
  81. }
  82. }
  83. if (user == null) {
  84. logger.log(userId, 'could not find user for institution SAML linking')
  85. throw new Errors.NotFoundError('user not found')
  86. }
  87. const emailAlreadyAssociated = user.emails.find(e => e.email === email)
  88. if (emailAlreadyAssociated && emailAlreadyAssociated.confirmedAt) {
  89. await UserUpdater.promises.updateUser(query, update)
  90. } else if (emailAlreadyAssociated) {
  91. // add and confirm email
  92. await UserUpdater.promises.confirmEmail(user._id, email)
  93. await UserUpdater.promises.updateUser(query, update)
  94. } else {
  95. // add and confirm email
  96. await UserUpdater.promises.addEmailAddress(user._id, email)
  97. await UserUpdater.promises.confirmEmail(user._id, email)
  98. await UserUpdater.promises.updateUser(query, update)
  99. }
  100. }
  101. async function _sendLinkedEmail(userId, providerName) {
  102. const user = await UserGetter.promises.getUser(userId, { email: 1 })
  103. const emailOptions = {
  104. to: user.email,
  105. provider: providerName
  106. }
  107. EmailHandler.sendEmail(
  108. 'emailThirdPartyIdentifierLinked',
  109. emailOptions,
  110. error => {
  111. if (error != null) {
  112. logger.warn(error)
  113. }
  114. }
  115. )
  116. }
  117. function _sendUnlinkedEmail(primaryEmail, providerName) {
  118. const emailOptions = {
  119. to: primaryEmail,
  120. provider: providerName
  121. }
  122. EmailHandler.sendEmail(
  123. 'emailThirdPartyIdentifierUnlinked',
  124. emailOptions,
  125. error => {
  126. if (error != null) {
  127. logger.warn(error)
  128. }
  129. }
  130. )
  131. }
  132. async function getUser(providerId, externalUserId) {
  133. if (providerId == null || externalUserId == null) {
  134. throw new Error(
  135. `invalid arguments: providerId: ${providerId}, externalUserId: ${externalUserId}`
  136. )
  137. }
  138. providerId = providerId.toString()
  139. externalUserId = externalUserId.toString()
  140. const query = _getUserQuery(providerId, externalUserId)
  141. let user = await User.findOne(query).exec()
  142. if (!user) {
  143. throw new Errors.SAMLUserNotFoundError()
  144. }
  145. return user
  146. }
  147. async function linkAccounts(
  148. userId,
  149. externalUserId,
  150. institutionEmail,
  151. providerId,
  152. providerName,
  153. hasEntitlement
  154. ) {
  155. await _addIdentifier(
  156. userId,
  157. externalUserId,
  158. providerId,
  159. hasEntitlement,
  160. institutionEmail
  161. )
  162. await _addInstitutionEmail(userId, institutionEmail, providerId)
  163. await _sendLinkedEmail(userId, providerName)
  164. }
  165. async function unlinkAccounts(userId, primaryEmail, providerId, providerName) {
  166. providerId = providerId.toString()
  167. const query = {
  168. _id: userId
  169. }
  170. const update = {
  171. $pull: {
  172. samlIdentifiers: {
  173. providerId
  174. }
  175. }
  176. }
  177. await User.update(query, update).exec()
  178. _sendUnlinkedEmail(primaryEmail, providerName)
  179. }
  180. async function updateEntitlement(userId, providerId, hasEntitlement) {
  181. providerId = providerId.toString()
  182. hasEntitlement = !!hasEntitlement
  183. const query = {
  184. _id: userId,
  185. 'samlIdentifiers.providerId': providerId.toString()
  186. }
  187. const update = {
  188. $set: {
  189. 'samlIdentifiers.$.hasEntitlement': hasEntitlement
  190. }
  191. }
  192. await User.update(query, update).exec()
  193. }
  194. function entitlementAttributeMatches(entitlementAttribute, entitlementMatcher) {
  195. if (
  196. typeof entitlementAttribute !== 'string' ||
  197. typeof entitlementMatcher !== 'string'
  198. ) {
  199. return false
  200. }
  201. const entitlementRegExp = new RegExp(entitlementMatcher)
  202. return !!entitlementAttribute.match(entitlementRegExp)
  203. }
  204. function userHasEntitlement(user, providerId) {
  205. providerId = providerId.toString()
  206. if (!user || !Array.isArray(user.samlIdentifiers)) {
  207. return false
  208. }
  209. for (const samlIdentifier of user.samlIdentifiers) {
  210. if (providerId && samlIdentifier.providerId !== providerId) {
  211. continue
  212. }
  213. if (samlIdentifier.hasEntitlement) {
  214. return true
  215. }
  216. }
  217. return false
  218. }
  219. const SAMLIdentityManager = {
  220. entitlementAttributeMatches,
  221. getUser,
  222. linkAccounts,
  223. unlinkAccounts,
  224. updateEntitlement,
  225. userHasEntitlement
  226. }
  227. module.exports = SAMLIdentityManager