SAMLIdentityManager.js 6.8 KB

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