remove_deleted_users_from_token_access_refs.js 5.6 KB

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