| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- import { db, READ_PREFERENCE_SECONDARY } from '../lib/mongodb.mjs'
- import { batchedUpdate } from '@overleaf/mongo-utils/batchedUpdate.js'
- import mongodb from 'mongodb'
- import logger from '@overleaf/logger'
- import OError from '@overleaf/o-error'
- const { ObjectId } = mongodb
- async function findUserIds() {
- const userIds = new Set()
- const cursor = db.users.find(
- {},
- {
- projection: { _id: 1 },
- readPreference: READ_PREFERENCE_SECONDARY,
- }
- )
- for await (const user of cursor) {
- userIds.add(user._id.toString())
- if (userIds.size % 1_000_000 === 0) {
- console.log(`=> ${userIds.size} users added`, new Date().toISOString())
- }
- }
- console.log(`=> User ids count: ${userIds.size}`)
- return userIds
- }
- export default async function fixProjectsWithInvalidTokenAccessRefsIds() {
- const DELETED_USER_COLLABORATOR_IDS = new Set()
- const PROJECTS_WITH_DELETED_USER = new Set()
- // get a set of all users ids as an in-memory cache
- const userIds = await findUserIds()
- // default query for finding all projects with non-existing/null or non-empty token access fields
- const query = {
- $or: [
- { tokenAccessReadOnly_refs: { $not: { $type: 'array' } } },
- { tokenAccessReadAndWrite_refs: { $not: { $type: 'array' } } },
- { 'tokenAccessReadOnly_refs.0': { $exists: true } },
- { 'tokenAccessReadAndWrite_refs.0': { $exists: true } },
- ],
- }
- await batchedUpdate(
- db.projects,
- query,
- async projects => {
- for (const project of projects) {
- const isTokenAccessFieldMissing =
- !project.tokenAccessReadOnly_refs ||
- !project.tokenAccessReadAndWrite_refs
- project.tokenAccessReadOnly_refs ??= []
- project.tokenAccessReadAndWrite_refs ??= []
- // update the token access fields if necessary
- if (isTokenAccessFieldMissing) {
- const fields = [
- 'tokenAccessReadOnly_refs',
- 'tokenAccessReadAndWrite_refs',
- ]
- for (const field of fields) {
- await db.projects.updateOne(
- {
- _id: project._id,
- [field]: { $not: { $type: 'array' } },
- },
- { $set: { [field]: [] } }
- )
- }
- console.log(
- `=> Fixed non-existing token access fields in project ${project._id.toString()}`
- )
- }
- // find the set of user ids that are in the token access fields
- // i.e. the set of collaborators
- const collaboratorIds = new Set()
- for (const roUserId of project.tokenAccessReadOnly_refs) {
- collaboratorIds.add(roUserId.toString())
- }
- for (const rwUserId of project.tokenAccessReadAndWrite_refs) {
- collaboratorIds.add(rwUserId.toString())
- }
- // determine which collaborator ids are not in the `users` collection
- // i.e. the user has been deleted
- const deletedUserIds = new Set()
- for (const collaboratorId of collaboratorIds) {
- if (!userIds.has(collaboratorId)) {
- deletedUserIds.add(collaboratorId)
- }
- }
- // double-check that users doesn't exist in the users collection
- // we don't want to remove users that were added after the initial query
- const existingUsersCursor = db.users.find(
- { _id: { $in: [...deletedUserIds].map(id => new ObjectId(id)) } },
- { _id: 1 }
- )
- for await (const user of existingUsersCursor) {
- const id = user._id.toString()
- deletedUserIds.delete(id)
- // add the user id to the cache
- userIds.add(id)
- }
- // remove the actual deleted users
- for (const deletedUserId of deletedUserIds) {
- DELETED_USER_COLLABORATOR_IDS.add(deletedUserId)
- PROJECTS_WITH_DELETED_USER.add(project._id.toString())
- console.log(
- '=> Found deleted user id:',
- deletedUserId,
- 'in project:',
- project._id.toString()
- )
- console.log(
- `=> Removing deleted ${deletedUserId} from all projects (found in project ${project._id.toString()})`
- )
- await removeUserFromAllProjects(new ObjectId(deletedUserId))
- }
- }
- },
- { tokenAccessReadOnly_refs: 1, tokenAccessReadAndWrite_refs: 1 }
- )
- console.log(`Deleted user ids (${DELETED_USER_COLLABORATOR_IDS.size})`)
- if (DELETED_USER_COLLABORATOR_IDS.size) {
- console.log(Array.from(DELETED_USER_COLLABORATOR_IDS).join('\n'))
- }
- console.log(
- `=> Projects with deleted user ids (${PROJECTS_WITH_DELETED_USER.size})`
- )
- if (PROJECTS_WITH_DELETED_USER.size) {
- console.log(Array.from(PROJECTS_WITH_DELETED_USER).join('\n'))
- }
- }
- // Copied from services/web/app/src/Features/Collaborators/CollaboratorsHandler.js
- async function removeUserFromAllProjects(userId) {
- const { readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly } =
- await dangerouslyGetAllProjectsUserIsMemberOf(userId, { _id: 1 })
- const allProjects = readAndWrite
- .concat(readOnly)
- .concat(tokenReadAndWrite)
- .concat(tokenReadOnly)
- logger.info(
- {
- userId,
- readAndWriteCount: readAndWrite.length,
- readOnlyCount: readOnly.length,
- tokenReadAndWriteCount: tokenReadAndWrite.length,
- tokenReadOnlyCount: tokenReadOnly.length,
- },
- 'removing user from projects'
- )
- for (const project of allProjects) {
- await removeUserFromProject(project._id, userId)
- }
- logger.info(
- {
- userId,
- allProjectsCount: allProjects.length,
- },
- 'removed user from all projects'
- )
- }
- // Copied from services/web/app/src/Features/Collaborators/CollaboratorsHandler.js
- async function removeUserFromProject(projectId, userId) {
- try {
- await db.projects.updateOne(
- { _id: projectId },
- {
- $pull: {
- collaberator_refs: userId,
- readOnly_refs: userId,
- reviewer_refs: userId,
- pendingEditor_refs: userId,
- pendingReviewer_refs: userId,
- tokenAccessReadOnly_refs: userId,
- tokenAccessReadAndWrite_refs: userId,
- archived: userId,
- trashed: userId,
- },
- }
- )
- } catch (err) {
- throw OError.tag(err, 'problem removing user from project collaborators', {
- projectId,
- userId,
- })
- }
- }
- // Copied from services/web/app/src/Features/Collaborators/CollaboratorsGetter.js
- // This function returns all the projects that a user is a member of, regardless of
- // the current state of the project, so it includes those projects where token access
- // has been disabled.
- async function dangerouslyGetAllProjectsUserIsMemberOf(userId, fields) {
- const readAndWrite = await db.projects
- .find({ collaberator_refs: userId }, fields)
- .toArray()
- const readOnly = await db.projects
- .find({ readOnly_refs: userId }, fields)
- .toArray()
- const tokenReadAndWrite = await db.projects
- .find({ tokenAccessReadAndWrite_refs: userId }, fields)
- .toArray()
- const tokenReadOnly = await db.projects
- .find({ tokenAccessReadOnly_refs: userId }, fields)
- .toArray()
- return { readAndWrite, readOnly, tokenReadAndWrite, tokenReadOnly }
- }
|