remove_deleted_users_from_token_access_refs.mjs 6.9 KB

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