remove_deleted_users_from_token_access_refs.js 6.8 KB

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