remove_deleted_users_from_token_access_refs.mjs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import {
  2. db,
  3. READ_PREFERENCE_SECONDARY,
  4. } from '../app/src/infrastructure/mongodb.js'
  5. import { batchedUpdate } from '@overleaf/mongo-utils/batchedUpdate.js'
  6. import mongodb from 'mongodb-legacy'
  7. import minimist from 'minimist'
  8. import CollaboratorsHandler from '../app/src/Features/Collaborators/CollaboratorsHandler.js'
  9. import { fileURLToPath } from 'node:url'
  10. const { ObjectId } = mongodb
  11. const argv = minimist(process.argv.slice(2), {
  12. string: ['projects'],
  13. boolean: ['dry-run', 'help'],
  14. alias: {
  15. projects: 'p',
  16. },
  17. default: {
  18. 'dry-run': true,
  19. },
  20. })
  21. const DRY_RUN = argv['dry-run']
  22. const PROJECTS_LIST = argv.projects
  23. async function findUserIds() {
  24. const userIds = new Set()
  25. const cursor = db.users.find(
  26. {},
  27. {
  28. projection: { _id: 1 },
  29. readPreference: READ_PREFERENCE_SECONDARY,
  30. }
  31. )
  32. for await (const user of cursor) {
  33. userIds.add(user._id.toString())
  34. if (userIds.size % 1_000_000 === 0) {
  35. console.log(`=> ${userIds.size} users added`, new Date().toISOString())
  36. }
  37. }
  38. console.log(`=> User ids count: ${userIds.size}`)
  39. return userIds
  40. }
  41. async function fixProjectsWithInvalidTokenAccessRefsIds(
  42. DRY_RUN,
  43. PROJECTS_LIST
  44. ) {
  45. if (DRY_RUN) {
  46. console.log('=> Doing dry run')
  47. }
  48. const DELETED_USER_COLLABORATOR_IDS = new Set()
  49. const PROJECTS_WITH_DELETED_USER = new Set()
  50. // get a set of all users ids as an in-memory cache
  51. const userIds = await findUserIds()
  52. // default query for finding all projects with non-existing/null or non-empty token access fields
  53. let query = {
  54. $or: [
  55. { tokenAccessReadOnly_refs: { $not: { $type: 'array' } } },
  56. { tokenAccessReadAndWrite_refs: { $not: { $type: 'array' } } },
  57. { 'tokenAccessReadOnly_refs.0': { $exists: true } },
  58. { 'tokenAccessReadAndWrite_refs.0': { $exists: true } },
  59. ],
  60. }
  61. const projectIds = PROJECTS_LIST?.split(',').map(
  62. projectId => new ObjectId(projectId)
  63. )
  64. // query for finding projects passed in as args
  65. if (projectIds) {
  66. query = { $and: [{ _id: { $in: projectIds } }] }
  67. }
  68. await batchedUpdate(
  69. db.projects,
  70. query,
  71. async projects => {
  72. for (const project of projects) {
  73. const isTokenAccessFieldMissing =
  74. !project.tokenAccessReadOnly_refs ||
  75. !project.tokenAccessReadAndWrite_refs
  76. project.tokenAccessReadOnly_refs ??= []
  77. project.tokenAccessReadAndWrite_refs ??= []
  78. // update the token access fields if necessary
  79. if (isTokenAccessFieldMissing) {
  80. if (DRY_RUN) {
  81. console.log(
  82. `=> DRY RUN - would fix non-existing token access fields in project ${project._id.toString()}`
  83. )
  84. } else {
  85. const fields = [
  86. 'tokenAccessReadOnly_refs',
  87. 'tokenAccessReadAndWrite_refs',
  88. ]
  89. for (const field of fields) {
  90. await db.projects.updateOne(
  91. {
  92. _id: project._id,
  93. [field]: { $not: { $type: 'array' } },
  94. },
  95. { $set: { [field]: [] } }
  96. )
  97. }
  98. console.log(
  99. `=> Fixed non-existing token access fields in project ${project._id.toString()}`
  100. )
  101. }
  102. }
  103. // find the set of user ids that are in the token access fields
  104. // i.e. the set of collaborators
  105. const collaboratorIds = new Set()
  106. for (const roUserId of project.tokenAccessReadOnly_refs) {
  107. collaboratorIds.add(roUserId.toString())
  108. }
  109. for (const rwUserId of project.tokenAccessReadAndWrite_refs) {
  110. collaboratorIds.add(rwUserId.toString())
  111. }
  112. // determine which collaborator ids are not in the `users` collection
  113. // i.e. the user has been deleted
  114. const deletedUserIds = new Set()
  115. for (const collaboratorId of collaboratorIds) {
  116. if (!userIds.has(collaboratorId)) {
  117. deletedUserIds.add(collaboratorId)
  118. }
  119. }
  120. // double-check that users doesn't exist in the users collection
  121. // we don't want to remove users that were added after the initial query
  122. const existingUsersCursor = db.users.find(
  123. { _id: { $in: [...deletedUserIds].map(id => new ObjectId(id)) } },
  124. { _id: 1 }
  125. )
  126. for await (const user of existingUsersCursor) {
  127. const id = user._id.toString()
  128. deletedUserIds.delete(id)
  129. // add the user id to the cache
  130. userIds.add(id)
  131. }
  132. // remove the actual deleted users
  133. for (const deletedUserId of deletedUserIds) {
  134. DELETED_USER_COLLABORATOR_IDS.add(deletedUserId)
  135. PROJECTS_WITH_DELETED_USER.add(project._id.toString())
  136. console.log(
  137. '=> Found deleted user id:',
  138. deletedUserId,
  139. 'in project:',
  140. project._id.toString()
  141. )
  142. if (DRY_RUN) {
  143. console.log(
  144. `=> DRY RUN - would remove deleted ${deletedUserId} from all projects (found in project ${project._id.toString()})`
  145. )
  146. continue
  147. }
  148. console.log(
  149. `=> Removing deleted ${deletedUserId} from all projects (found in project ${project._id.toString()})`
  150. )
  151. await CollaboratorsHandler.promises.removeUserFromAllProjects(
  152. new ObjectId(deletedUserId)
  153. )
  154. }
  155. }
  156. },
  157. { tokenAccessReadOnly_refs: 1, tokenAccessReadAndWrite_refs: 1 }
  158. )
  159. console.log(
  160. `=> ${DRY_RUN ? 'DRY RUN - would delete' : 'Deleted'} user ids (${
  161. DELETED_USER_COLLABORATOR_IDS.size
  162. })`
  163. )
  164. if (DELETED_USER_COLLABORATOR_IDS.size) {
  165. console.log(Array.from(DELETED_USER_COLLABORATOR_IDS).join('\n'))
  166. }
  167. console.log(
  168. `=> Projects with deleted user ids (${PROJECTS_WITH_DELETED_USER.size})`
  169. )
  170. if (PROJECTS_WITH_DELETED_USER.size) {
  171. console.log(Array.from(PROJECTS_WITH_DELETED_USER).join('\n'))
  172. }
  173. }
  174. async function main(DRY_RUN, PROJECTS_LIST) {
  175. await fixProjectsWithInvalidTokenAccessRefsIds(DRY_RUN, PROJECTS_LIST)
  176. }
  177. export default main
  178. if (fileURLToPath(import.meta.url) === process.argv[1]) {
  179. if (argv.help || argv._.length > 1) {
  180. console.error(`Usage: node scripts/remove_deleted_users_from_token_access_refs.mjs [OPTS]
  181. Finds or removes deleted user ids from token access fields
  182. "tokenAccessReadOnly_refs" and "tokenAccessReadAndWrite_refs" in the "projects" collection.
  183. If no projects are specified, all projects will be processed.
  184. Options:
  185. --dry-run finds projects and deleted users but does not do any updates
  186. --projects list of projects ids to be fixed (comma separated)
  187. `)
  188. process.exit(1)
  189. }
  190. try {
  191. await main(DRY_RUN, PROJECTS_LIST)
  192. console.error('Done')
  193. process.exit(0)
  194. } catch (error) {
  195. console.error(error)
  196. process.exit(1)
  197. }
  198. }