remove_deleted_users_from_token_access_refs.mjs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import { db, READ_PREFERENCE_SECONDARY } from '../lib/mongodb.mjs'
  2. import { batchedUpdate } from '@overleaf/mongo-utils/batchedUpdate.js'
  3. import mongodb from 'mongodb'
  4. import logger from '@overleaf/logger'
  5. import OError from '@overleaf/o-error'
  6. const { ObjectId } = mongodb
  7. async function findUserIds() {
  8. const userIds = new Set()
  9. const cursor = db.users.find(
  10. {},
  11. {
  12. projection: { _id: 1 },
  13. readPreference: READ_PREFERENCE_SECONDARY,
  14. }
  15. )
  16. for await (const user of cursor) {
  17. userIds.add(user._id.toString())
  18. if (userIds.size % 1_000_000 === 0) {
  19. console.log(`=> ${userIds.size} users added`, new Date().toISOString())
  20. }
  21. }
  22. console.log(`=> User ids count: ${userIds.size}`)
  23. return userIds
  24. }
  25. export default async function fixProjectsWithInvalidTokenAccessRefsIds() {
  26. const DELETED_USER_COLLABORATOR_IDS = new Set()
  27. const PROJECTS_WITH_DELETED_USER = new Set()
  28. // get a set of all users ids as an in-memory cache
  29. const userIds = await findUserIds()
  30. // default query for finding all projects with non-existing/null or non-empty token access fields
  31. const query = {
  32. $or: [
  33. { tokenAccessReadOnly_refs: { $not: { $type: 'array' } } },
  34. { tokenAccessReadAndWrite_refs: { $not: { $type: 'array' } } },
  35. { 'tokenAccessReadOnly_refs.0': { $exists: true } },
  36. { 'tokenAccessReadAndWrite_refs.0': { $exists: true } },
  37. ],
  38. }
  39. await batchedUpdate(
  40. db.projects,
  41. query,
  42. async projects => {
  43. for (const project of projects) {
  44. const isTokenAccessFieldMissing =
  45. !project.tokenAccessReadOnly_refs ||
  46. !project.tokenAccessReadAndWrite_refs
  47. project.tokenAccessReadOnly_refs ??= []
  48. project.tokenAccessReadAndWrite_refs ??= []
  49. // update the token access fields if necessary
  50. if (isTokenAccessFieldMissing) {
  51. const fields = [
  52. 'tokenAccessReadOnly_refs',
  53. 'tokenAccessReadAndWrite_refs',
  54. ]
  55. for (const field of fields) {
  56. await db.projects.updateOne(
  57. {
  58. _id: project._id,
  59. [field]: { $not: { $type: 'array' } },
  60. },
  61. { $set: { [field]: [] } }
  62. )
  63. }
  64. console.log(
  65. `=> Fixed non-existing token access fields in project ${project._id.toString()}`
  66. )
  67. }
  68. // find the set of user ids that are in the token access fields
  69. // i.e. the set of collaborators
  70. const collaboratorIds = new Set()
  71. for (const roUserId of project.tokenAccessReadOnly_refs) {
  72. collaboratorIds.add(roUserId.toString())
  73. }
  74. for (const rwUserId of project.tokenAccessReadAndWrite_refs) {
  75. collaboratorIds.add(rwUserId.toString())
  76. }
  77. // determine which collaborator ids are not in the `users` collection
  78. // i.e. the user has been deleted
  79. const deletedUserIds = new Set()
  80. for (const collaboratorId of collaboratorIds) {
  81. if (!userIds.has(collaboratorId)) {
  82. deletedUserIds.add(collaboratorId)
  83. }
  84. }
  85. // double-check that users doesn't exist in the users collection
  86. // we don't want to remove users that were added after the initial query
  87. const existingUsersCursor = db.users.find(
  88. { _id: { $in: [...deletedUserIds].map(id => new ObjectId(id)) } },
  89. { _id: 1 }
  90. )
  91. for await (const user of existingUsersCursor) {
  92. const id = user._id.toString()
  93. deletedUserIds.delete(id)
  94. // add the user id to the cache
  95. userIds.add(id)
  96. }
  97. // remove the actual deleted users
  98. for (const deletedUserId of deletedUserIds) {
  99. DELETED_USER_COLLABORATOR_IDS.add(deletedUserId)
  100. PROJECTS_WITH_DELETED_USER.add(project._id.toString())
  101. console.log(
  102. '=> Found deleted user id:',
  103. deletedUserId,
  104. 'in project:',
  105. project._id.toString()
  106. )
  107. console.log(
  108. `=> Removing deleted ${deletedUserId} from all projects (found in project ${project._id.toString()})`
  109. )
  110. await removeUserFromAllProjects(new ObjectId(deletedUserId))
  111. }
  112. }
  113. },
  114. { tokenAccessReadOnly_refs: 1, tokenAccessReadAndWrite_refs: 1 }
  115. )
  116. console.log(`Deleted user ids (${DELETED_USER_COLLABORATOR_IDS.size})`)
  117. if (DELETED_USER_COLLABORATOR_IDS.size) {
  118. console.log(Array.from(DELETED_USER_COLLABORATOR_IDS).join('\n'))
  119. }
  120. console.log(
  121. `=> Projects with deleted user ids (${PROJECTS_WITH_DELETED_USER.size})`
  122. )
  123. if (PROJECTS_WITH_DELETED_USER.size) {
  124. console.log(Array.from(PROJECTS_WITH_DELETED_USER).join('\n'))
  125. }
  126. }
  127. // Copied from services/web/app/src/Features/Collaborators/CollaboratorsHandler.js
  128. async function removeUserFromAllProjects(userId) {
  129. const { readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly } =
  130. await dangerouslyGetAllProjectsUserIsMemberOf(userId, { _id: 1 })
  131. const allProjects = readAndWrite
  132. .concat(readOnly)
  133. .concat(tokenReadAndWrite)
  134. .concat(tokenReadOnly)
  135. logger.info(
  136. {
  137. userId,
  138. readAndWriteCount: readAndWrite.length,
  139. readOnlyCount: readOnly.length,
  140. tokenReadAndWriteCount: tokenReadAndWrite.length,
  141. tokenReadOnlyCount: tokenReadOnly.length,
  142. },
  143. 'removing user from projects'
  144. )
  145. for (const project of allProjects) {
  146. await removeUserFromProject(project._id, userId)
  147. }
  148. logger.info(
  149. {
  150. userId,
  151. allProjectsCount: allProjects.length,
  152. },
  153. 'removed user from all projects'
  154. )
  155. }
  156. // Copied from services/web/app/src/Features/Collaborators/CollaboratorsHandler.js
  157. async function removeUserFromProject(projectId, userId) {
  158. try {
  159. await db.projects.updateOne(
  160. { _id: projectId },
  161. {
  162. $pull: {
  163. collaberator_refs: userId,
  164. readOnly_refs: userId,
  165. reviewer_refs: userId,
  166. pendingEditor_refs: userId,
  167. pendingReviewer_refs: userId,
  168. tokenAccessReadOnly_refs: userId,
  169. tokenAccessReadAndWrite_refs: userId,
  170. archived: userId,
  171. trashed: userId,
  172. },
  173. }
  174. )
  175. } catch (err) {
  176. throw OError.tag(err, 'problem removing user from project collaborators', {
  177. projectId,
  178. userId,
  179. })
  180. }
  181. }
  182. // Copied from services/web/app/src/Features/Collaborators/CollaboratorsGetter.js
  183. // This function returns all the projects that a user is a member of, regardless of
  184. // the current state of the project, so it includes those projects where token access
  185. // has been disabled.
  186. async function dangerouslyGetAllProjectsUserIsMemberOf(userId, fields) {
  187. const readAndWrite = await db.projects
  188. .find({ collaberator_refs: userId }, fields)
  189. .toArray()
  190. const readOnly = await db.projects
  191. .find({ readOnly_refs: userId }, fields)
  192. .toArray()
  193. const tokenReadAndWrite = await db.projects
  194. .find({ tokenAccessReadAndWrite_refs: userId }, fields)
  195. .toArray()
  196. const tokenReadOnly = await db.projects
  197. .find({ tokenAccessReadOnly_refs: userId }, fields)
  198. .toArray()
  199. return { readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly }
  200. }