OwnershipTransferHandler.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. const logger = require('@overleaf/logger')
  2. const { Project } = require('../../models/Project')
  3. const ProjectGetter = require('../Project/ProjectGetter')
  4. const UserGetter = require('../User/UserGetter')
  5. const CollaboratorsHandler = require('./CollaboratorsHandler')
  6. const EmailHandler = require('../Email/EmailHandler')
  7. const Errors = require('../Errors/Errors')
  8. const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
  9. const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher')
  10. const ProjectAuditLogHandler = require('../Project/ProjectAuditLogHandler')
  11. const AnalyticsManager = require('../Analytics/AnalyticsManager')
  12. module.exports = {
  13. promises: { transferOwnership },
  14. }
  15. async function transferOwnership(projectId, newOwnerId, options = {}) {
  16. const {
  17. allowTransferToNonCollaborators,
  18. sessionUserId,
  19. skipEmails,
  20. ipAddress,
  21. } = options
  22. // Fetch project and user
  23. const [project, newOwner] = await Promise.all([
  24. _getProject(projectId),
  25. _getUser(newOwnerId),
  26. ])
  27. // Exit early if the transferee is already the project owner
  28. const previousOwnerId = project.owner_ref
  29. if (previousOwnerId.equals(newOwnerId)) {
  30. return
  31. }
  32. // Check that user is already a collaborator
  33. if (
  34. !allowTransferToNonCollaborators &&
  35. !_userIsCollaborator(newOwner, project)
  36. ) {
  37. throw new Errors.UserNotCollaboratorError({ info: { userId: newOwnerId } })
  38. }
  39. // Track the change of ownership in BigQuery.
  40. AnalyticsManager.recordEventForUserInBackground(
  41. previousOwnerId,
  42. 'project-ownership-transfer',
  43. { projectId, newOwnerId }
  44. )
  45. // Transfer ownership
  46. await ProjectAuditLogHandler.promises.addEntry(
  47. projectId,
  48. 'transfer-ownership',
  49. sessionUserId,
  50. ipAddress,
  51. { previousOwnerId, newOwnerId }
  52. )
  53. // Determine which permissions to give old owner based on
  54. // new owner's existing permissions
  55. const newPermissions =
  56. _getUserPermissions(newOwner, project) || PrivilegeLevels.READ_ONLY
  57. await _transferOwnership(
  58. projectId,
  59. previousOwnerId,
  60. newOwnerId,
  61. newPermissions
  62. )
  63. // Flush project to TPDS
  64. await TpdsProjectFlusher.promises.flushProjectToTpds(projectId)
  65. // Send confirmation emails
  66. const previousOwner = await UserGetter.promises.getUser(previousOwnerId)
  67. if (!skipEmails) {
  68. await _sendEmails(project, previousOwner, newOwner)
  69. }
  70. }
  71. async function _getProject(projectId) {
  72. const project = await ProjectGetter.promises.getProject(projectId, {
  73. owner_ref: 1,
  74. collaberator_refs: 1,
  75. readOnly_refs: 1,
  76. reviewer_refs: 1,
  77. name: 1,
  78. })
  79. if (project == null) {
  80. throw new Errors.ProjectNotFoundError({ info: { projectId } })
  81. }
  82. return project
  83. }
  84. async function _getUser(userId) {
  85. const user = await UserGetter.promises.getUser(userId)
  86. if (user == null) {
  87. throw new Errors.UserNotFoundError({ info: { userId } })
  88. }
  89. return user
  90. }
  91. function _getUserPermissions(user, project) {
  92. const collaboratorIds = project.collaberator_refs || []
  93. const readOnlyIds = project.readOnly_refs || []
  94. const reviewerIds = project.reviewer_refs || []
  95. if (collaboratorIds.some(collaboratorId => collaboratorId.equals(user._id))) {
  96. return PrivilegeLevels.READ_AND_WRITE
  97. } else if (
  98. reviewerIds.some(collaboratorId => collaboratorId.equals(user._id))
  99. ) {
  100. return PrivilegeLevels.REVIEW
  101. } else if (
  102. readOnlyIds.some(collaboratorId => collaboratorId.equals(user._id))
  103. ) {
  104. return PrivilegeLevels.READ_ONLY
  105. }
  106. }
  107. function _userIsCollaborator(user, project) {
  108. return Boolean(_getUserPermissions(user, project))
  109. }
  110. async function _transferOwnership(
  111. projectId,
  112. previousOwnerId,
  113. newOwnerId,
  114. newPermissions
  115. ) {
  116. await CollaboratorsHandler.promises.removeUserFromProject(
  117. projectId,
  118. newOwnerId
  119. )
  120. await Project.updateOne(
  121. { _id: projectId },
  122. { $set: { owner_ref: newOwnerId } }
  123. ).exec()
  124. await CollaboratorsHandler.promises.addUserIdToProject(
  125. projectId,
  126. newOwnerId,
  127. previousOwnerId,
  128. newPermissions
  129. )
  130. }
  131. async function _sendEmails(project, previousOwner, newOwner) {
  132. if (previousOwner == null) {
  133. // The previous owner didn't exist. This is not supposed to happen, but
  134. // since we're changing the owner anyway, we'll just warn
  135. logger.warn(
  136. { projectId: project._id, ownerId: previousOwner._id },
  137. 'Project owner did not exist before ownership transfer'
  138. )
  139. } else {
  140. // Send confirmation emails
  141. await Promise.all([
  142. EmailHandler.promises.sendEmail(
  143. 'ownershipTransferConfirmationPreviousOwner',
  144. {
  145. to: previousOwner.email,
  146. project,
  147. newOwner,
  148. }
  149. ),
  150. EmailHandler.promises.sendEmail('ownershipTransferConfirmationNewOwner', {
  151. to: newOwner.email,
  152. project,
  153. previousOwner,
  154. }),
  155. ])
  156. }
  157. }