ProjectDeleter.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. const _ = require('lodash')
  2. const { db, ObjectId } = require('../../infrastructure/mongodb')
  3. const Modules = require('../../infrastructure/Modules')
  4. const { callbackify } = require('util')
  5. const { Project } = require('../../models/Project')
  6. const { DeletedProject } = require('../../models/DeletedProject')
  7. const { ProjectAuditLogEntry } = require('../../models/ProjectAuditLogEntry')
  8. const Errors = require('../Errors/Errors')
  9. const logger = require('@overleaf/logger')
  10. const Settings = require('@overleaf/settings')
  11. const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
  12. const TagsHandler = require('../Tags/TagsHandler')
  13. const ProjectDetailsHandler = require('./ProjectDetailsHandler')
  14. const CollaboratorsHandler = require('../Collaborators/CollaboratorsHandler')
  15. const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
  16. const DocstoreManager = require('../Docstore/DocstoreManager')
  17. const EditorRealTimeController = require('../Editor/EditorRealTimeController')
  18. const HistoryManager = require('../History/HistoryManager')
  19. const FilestoreHandler = require('../FileStore/FileStoreHandler')
  20. const ChatApiHandler = require('../Chat/ChatApiHandler')
  21. const { promiseMapWithLimit } = require('@overleaf/promise-utils')
  22. const { READ_PREFERENCE_SECONDARY } = require('../../infrastructure/mongodb')
  23. const PROJECT_EXPIRATION_BATCH_SIZE = 10000
  24. module.exports = {
  25. markAsDeletedByExternalSource: callbackify(markAsDeletedByExternalSource),
  26. unmarkAsDeletedByExternalSource: callbackify(unmarkAsDeletedByExternalSource),
  27. deleteUsersProjects: callbackify(deleteUsersProjects),
  28. expireDeletedProjectsAfterDuration: callbackify(
  29. expireDeletedProjectsAfterDuration
  30. ),
  31. restoreProject: callbackify(restoreProject),
  32. archiveProject: callbackify(archiveProject),
  33. unarchiveProject: callbackify(unarchiveProject),
  34. trashProject: callbackify(trashProject),
  35. untrashProject: callbackify(untrashProject),
  36. deleteProject: callbackify(deleteProject),
  37. undeleteProject: callbackify(undeleteProject),
  38. expireDeletedProject: callbackify(expireDeletedProject),
  39. promises: {
  40. archiveProject,
  41. unarchiveProject,
  42. trashProject,
  43. untrashProject,
  44. deleteProject,
  45. undeleteProject,
  46. expireDeletedProject,
  47. markAsDeletedByExternalSource,
  48. unmarkAsDeletedByExternalSource,
  49. deleteUsersProjects,
  50. expireDeletedProjectsAfterDuration,
  51. restoreProject,
  52. },
  53. }
  54. async function markAsDeletedByExternalSource(projectId) {
  55. logger.debug(
  56. { projectId },
  57. 'marking project as deleted by external data source'
  58. )
  59. await Project.updateOne(
  60. { _id: projectId },
  61. { deletedByExternalDataSource: true }
  62. ).exec()
  63. EditorRealTimeController.emitToRoom(
  64. projectId,
  65. 'projectRenamedOrDeletedByExternalSource'
  66. )
  67. }
  68. async function unmarkAsDeletedByExternalSource(projectId) {
  69. await Project.updateOne(
  70. { _id: projectId },
  71. { deletedByExternalDataSource: false }
  72. ).exec()
  73. }
  74. async function deleteUsersProjects(userId) {
  75. const projects = await Project.find({ owner_ref: userId }).exec()
  76. logger.info(
  77. { userId, projectCount: projects.length },
  78. 'found user projects to delete'
  79. )
  80. await promiseMapWithLimit(5, projects, project => deleteProject(project._id))
  81. logger.info({ userId }, 'deleted all user projects')
  82. await CollaboratorsHandler.promises.removeUserFromAllProjects(userId)
  83. }
  84. async function expireDeletedProjectsAfterDuration() {
  85. const deletedProjects = await DeletedProject.find(
  86. {
  87. 'deleterData.deletedAt': {
  88. $lt: new Date(Date.now() - Settings.projectHardDeletionDelay),
  89. },
  90. project: { $type: 'object' },
  91. },
  92. { 'deleterData.deletedProjectId': 1 }
  93. )
  94. .limit(PROJECT_EXPIRATION_BATCH_SIZE)
  95. .read(READ_PREFERENCE_SECONDARY)
  96. const projectIds = _.shuffle(
  97. deletedProjects.map(
  98. deletedProject => deletedProject.deleterData.deletedProjectId
  99. )
  100. )
  101. logger.info(
  102. {
  103. projectCount: projectIds.length,
  104. retentionPeriodInDays:
  105. Settings.projectHardDeletionDelay / (1000 * 60 * 60 * 24),
  106. },
  107. 'expiring batch of deleted projects'
  108. )
  109. try {
  110. for (const projectId of projectIds) {
  111. await expireDeletedProject(projectId)
  112. }
  113. logger.info(
  114. { projectCount: projectIds.length },
  115. 'batch of deleted projects expired successfully'
  116. )
  117. } catch (error) {
  118. logger.warn(
  119. { error },
  120. 'something went wrong expiring batch of deleted projects'
  121. )
  122. throw error
  123. }
  124. }
  125. async function restoreProject(projectId) {
  126. await Project.updateOne(
  127. { _id: projectId },
  128. { $unset: { archived: true } }
  129. ).exec()
  130. }
  131. async function archiveProject(projectId, userId) {
  132. await Project.updateOne(
  133. { _id: projectId },
  134. {
  135. $addToSet: { archived: new ObjectId(userId) },
  136. $pull: { trashed: new ObjectId(userId) },
  137. }
  138. )
  139. }
  140. async function unarchiveProject(projectId, userId) {
  141. await Project.updateOne(
  142. { _id: projectId },
  143. { $pull: { archived: new ObjectId(userId) } }
  144. )
  145. }
  146. async function trashProject(projectId, userId) {
  147. await Project.updateOne(
  148. { _id: projectId },
  149. {
  150. $addToSet: { trashed: new ObjectId(userId) },
  151. $pull: { archived: new ObjectId(userId) },
  152. }
  153. )
  154. }
  155. async function untrashProject(projectId, userId) {
  156. await Project.updateOne(
  157. { _id: projectId },
  158. { $pull: { trashed: new ObjectId(userId) } }
  159. )
  160. }
  161. async function deleteProject(projectId, options = {}) {
  162. try {
  163. const project = await Project.findOne({ _id: projectId }).exec()
  164. if (!project) {
  165. throw new Errors.NotFoundError('project not found')
  166. }
  167. await DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete(
  168. projectId
  169. )
  170. try {
  171. // OPTIMIZATION: flush docs out of mongo
  172. await DocstoreManager.promises.archiveProject(projectId)
  173. } catch (err) {
  174. // It is OK to fail here, the docs will get hard-deleted eventually after
  175. // the grace-period for soft-deleted projects has passed.
  176. logger.warn(
  177. { projectId, err },
  178. 'failed archiving doc via docstore as part of project soft-deletion'
  179. )
  180. }
  181. const memberIds = await CollaboratorsGetter.promises.getMemberIds(projectId)
  182. // fire these jobs in the background
  183. for (const memberId of memberIds) {
  184. TagsHandler.promises
  185. .removeProjectFromAllTags(memberId, projectId)
  186. .catch(err => {
  187. logger.err(
  188. { err, memberId, projectId },
  189. 'failed to remove project from tags'
  190. )
  191. })
  192. }
  193. const deleterData = {
  194. deletedAt: new Date(),
  195. deleterId:
  196. options.deleterUser != null ? options.deleterUser._id : undefined,
  197. deleterIpAddress: options.ipAddress,
  198. deletedProjectId: project._id,
  199. deletedProjectOwnerId: project.owner_ref,
  200. deletedProjectCollaboratorIds: project.collaberator_refs,
  201. deletedProjectReadOnlyIds: project.readOnly_refs,
  202. deletedProjectReviewerIds: project.reviewer_refs,
  203. deletedProjectReadWriteTokenAccessIds:
  204. project.tokenAccessReadAndWrite_refs,
  205. deletedProjectOverleafId: project.overleaf
  206. ? project.overleaf.id
  207. : undefined,
  208. deletedProjectOverleafHistoryId:
  209. project.overleaf && project.overleaf.history
  210. ? project.overleaf.history.id
  211. : undefined,
  212. deletedProjectReadOnlyTokenAccessIds: project.tokenAccessReadOnly_refs,
  213. deletedProjectReadWriteToken: project.tokens.readAndWrite,
  214. deletedProjectReadOnlyToken: project.tokens.readOnly,
  215. deletedProjectLastUpdatedAt: project.lastUpdated,
  216. }
  217. Object.keys(deleterData).forEach(key =>
  218. deleterData[key] === undefined ? delete deleterData[key] : ''
  219. )
  220. await DeletedProject.updateOne(
  221. { 'deleterData.deletedProjectId': projectId },
  222. { project, deleterData },
  223. { upsert: true }
  224. )
  225. await Project.deleteOne({ _id: projectId }).exec()
  226. logger.info(
  227. { projectId, userId: project.owner_ref },
  228. 'successfully deleted project'
  229. )
  230. } catch (err) {
  231. logger.warn({ err }, 'problem deleting project')
  232. throw err
  233. }
  234. }
  235. async function undeleteProject(projectId, options = {}) {
  236. projectId = new ObjectId(projectId)
  237. const deletedProject = await DeletedProject.findOne({
  238. 'deleterData.deletedProjectId': projectId,
  239. }).exec()
  240. if (!deletedProject) {
  241. throw new Errors.NotFoundError('project_not_found')
  242. }
  243. if (!deletedProject.project) {
  244. throw new Errors.NotFoundError('project_too_old_to_restore')
  245. }
  246. const restored = new Project(deletedProject.project)
  247. if (options.userId) {
  248. restored.owner_ref = options.userId
  249. }
  250. // if we're undeleting, we want the document to show up
  251. restored.name = await ProjectDetailsHandler.promises.generateUniqueName(
  252. deletedProject.deleterData.deletedProjectOwnerId,
  253. restored.name + ' (Restored)'
  254. )
  255. restored.archived = undefined
  256. if (restored.deletedDocs && restored.deletedDocs.length > 0) {
  257. await promiseMapWithLimit(10, restored.deletedDocs, async deletedDoc => {
  258. // back fill context of deleted docs
  259. const { _id: docId, name, deletedAt } = deletedDoc
  260. await DocstoreManager.promises.deleteDoc(
  261. projectId,
  262. docId,
  263. name,
  264. deletedAt
  265. )
  266. })
  267. restored.deletedDocs = []
  268. }
  269. // we can't use Mongoose to re-insert the project, as it won't
  270. // create a new document with an _id already specified. We need to
  271. // insert it directly into the collection
  272. await db.projects.insertOne(restored)
  273. await DeletedProject.deleteOne({ _id: deletedProject._id }).exec()
  274. }
  275. async function expireDeletedProject(projectId) {
  276. try {
  277. logger.info({ projectId }, 'expiring deleted project')
  278. const activeProject = await Project.findById(projectId).exec()
  279. if (activeProject) {
  280. // That project is active. The deleted project record might be there
  281. // because of an incomplete delete or undelete operation. Clean it up and
  282. // return.
  283. logger.info(
  284. { projectId },
  285. 'deleted project record found but project is active'
  286. )
  287. await DeletedProject.deleteOne({
  288. 'deleterData.deletedProjectId': projectId,
  289. })
  290. return
  291. }
  292. const deletedProject = await DeletedProject.findOne({
  293. 'deleterData.deletedProjectId': projectId,
  294. }).exec()
  295. if (!deletedProject) {
  296. throw new Errors.NotFoundError(
  297. `No deleted project found for project id ${projectId}`
  298. )
  299. }
  300. if (!deletedProject.project) {
  301. logger.warn(
  302. { projectId },
  303. `Attempted to expire already-expired deletedProject`
  304. )
  305. return
  306. }
  307. const userId = deletedProject.deleterData?.deletedProjectOwnerId?.toString()
  308. const historyId =
  309. deletedProject.project.overleaf &&
  310. deletedProject.project.overleaf.history &&
  311. deletedProject.project.overleaf.history.id
  312. logger.info({ projectId, userId }, 'destroying expired project data')
  313. await Promise.all([
  314. DocstoreManager.promises.destroyProject(deletedProject.project._id),
  315. HistoryManager.promises.deleteProject(
  316. deletedProject.project._id,
  317. historyId
  318. ),
  319. FilestoreHandler.promises.deleteProject(deletedProject.project._id),
  320. ChatApiHandler.promises.destroyProject(deletedProject.project._id),
  321. ProjectAuditLogEntry.deleteMany({ projectId }),
  322. Modules.promises.hooks.fire('projectExpired', deletedProject.project._id),
  323. ])
  324. logger.info(
  325. { projectId, userId },
  326. 'redacting PII from the deleted project record'
  327. )
  328. await DeletedProject.updateOne(
  329. {
  330. _id: deletedProject._id,
  331. },
  332. {
  333. $set: {
  334. 'deleterData.deleterIpAddress': null,
  335. project: null,
  336. },
  337. }
  338. ).exec()
  339. logger.info({ projectId, userId }, 'expired deleted project successfully')
  340. } catch (error) {
  341. logger.warn({ projectId, error }, 'error expiring deleted project')
  342. throw error
  343. }
  344. }